import * as MarkdownIt from "markdown-it" ; import * as util from "util" ; import * as ast from "../../ast" ; import { UserError } from "../../errors/user-error" ; import * as files from "../../filesystem/index" ; import * as html from "../html" ; import { TagMapper } from "../tag-mapper" ; import { ClosingTagParser } from "./closing-tag-parser" ; import { OpenNodeTracker } from "./open-node-tracker" ; export interface MarkdownItNode { /** HTML attributes. Format: `[[name1, value1], [name2, value2]]` */ readonly attrs : [ string , string ] [ ] | null ; /** An array of child nodes (inline and img tokens) */ readonly children : MarkdownItNode [ ] | null ; /** In a case of self-closing tag (code, html, fence, etc.), it has contents of this tag. */ readonly content : string ; /** Source map info. Format: `[line_begin, line_end]` */ readonly map : [ number , number ] | null ; /** HTML tag name, e.g. "p" */ readonly tag : string ; /** Type of the token, e.g. "paragraph_open" */ readonly type : string ; } export type MarkdownItAst = MarkdownItNode [ ] ; export type MarkdownItNodeAttrs = string [ ] [ ] ; /** MarkdownParser parses Markdown. */ export class MarkdownParser { private readonly closingTagParser : ClosingTagParser ; /** parses HTML snippets into ast.NodeLists */ private readonly htmlParser : html . Parser ; /** MarkdownIt instance */ private readonly markdownIt : MarkdownIt ; /** helps map Markdown to HTML node types */ private readonly tagMapper : TagMapper ; constructor ( ) { this . markdownIt = new MarkdownIt ( { html : true , linkify : false , } ) ; this . tagMapper = new TagMapper ( ) ; this . closingTagParser = new ClosingTagParser ( this . tagMapper ) ; this . htmlParser = new html . Parser ( this . tagMapper ) ; } /** returns the standard AST representing the given Markdown text */ parse ( text , sourceDir , file ) { const mdAST = this . markdownIt . parse ( text , { } ) ; return this . standardizeAST ( mdAST , new files . Location ( sourceDir , file , 1 ) , new OpenNodeTracker ( ) ) ; } /** Converts the given MarkdownIt AST into the standard AST format */ private standardizeAST ( mdAST , parentLocation , ont ) { const result = new ast . NodeList ( ) ; let currentLine = parentLocation . line ; for ( const node of mdAST ) { currentLine = Math . max ( ( node . map || [ 0 , 0 ] ) [ 0 ] + 1 , currentLine ) ; if ( node . type === "image" ) { result . push ( ... this . standardizeImageNode ( node , parentLocation . withLine ( currentLine ) ) ) ; continue ; } if ( node . children ) { result . push ( ... this . standardizeAST ( node . children , parentLocation . withLine ( currentLine ) , ont ) ) ; continue ; } if ( node . type === "softbreak" ) { currentLine += 1 ; continue ; } result . push ( ... this . standardizeNode ( node , parentLocation . withLine ( currentLine ) , ont ) ) ; } return result ; } /** Returns the standardized version of the given MarkdownIt node */ private standardizeNode ( mdNode , location , ont ) { if ( mdNode . type === "text" && mdNode . content === "" ) { return new ast . NodeList ( ) ; } if ( mdNode . type === "heading_open" ) { return this . standardizeHeadingOpen ( mdNode , location ) ; } if ( mdNode . type === "heading_close" ) { return this . standardizeHeadingClose ( mdNode , location ) ; } if ( mdNode . type === "code_inline" ) { return this . standardizeCodeInline ( mdNode , location ) ; } if ( mdNode . type === "fence" ) { return this . standardizeFence ( mdNode , location ) ; } if ( mdNode . type === "code_block" ) { return this . standardizeEmbeddedCodeblock ( mdNode , location ) ; } if ( mdNode . type === "html_inline" || mdNode . type === "html_block" ) { if ( this . closingTagParser . isClosingTag ( mdNode . content ) ) { return this . standardizeClosingHTMLTag ( mdNode , ont , location ) ; } else { return this . standardizeHTMLBlock ( mdNode , ont , location ) ; } } if ( mdNode . type . endsWith ( "_open" ) ) { return this . standardizeOpeningNode ( mdNode , location , ont ) ; } if ( mdNode . type . endsWith ( "_close" ) ) { return this . standardizeClosingNode ( mdNode , location , ont ) ; } if ( mdNode . type === "text" ) { return this . standardizeTextNode ( mdNode , location ) ; } if ( this . tagMapper . isStandaloneTag ( mdNode . tag ) ) { return this . standizeStandaloneTag ( mdNode , location ) ; } throw new Error ( `unknown MarkdownIt node: ${ util . inspect ( mdNode ) }` ) ; } private standardizeImageNode ( mdNode , location ) { const result = new ast . NodeList ( ) ; const attributes = standardizeMarkdownItAttributes ( mdNode . attrs ) ; for ( const childNode of mdNode . children || [ ] ) { attributes . alt += childNode . content ; } result . push ( new ast . Node ( { attributes , content : "" , location , tag : "img" , type : "image" , } ) ) ; return result ; } private standardizeHeadingOpen ( mdNode , location ) { const result = new ast . NodeList ( ) ; result . push ( new ast . Node ( { attributes : standardizeMarkdownItAttributes ( mdNode . attrs ) , content : "" , location , tag : mdNode . tag as ast . NodeTag , type : `${ mdNode . tag }_open` as ast . NodeType , } ) ) ; return result ; } private standardizeHeadingClose ( node , location ) { const result = new ast . NodeList ( ) ; result . push ( new ast . Node ( { attributes : standardizeMarkdownItAttributes ( node . attrs ) , content : "" , location , tag : ( "/" + node . tag ) as ast . NodeTag , type : `${ node . tag }_close` as ast . NodeType , } ) ) ; return result ; } private standardizeCodeInline ( mdNode , location ) { const result = new ast . NodeList ( ) ; result . push ( new ast . Node ( { attributes : standardizeMarkdownItAttributes ( mdNode . attrs ) , content : "" , location , tag : "code" , type : "code_open" , } ) ) ; result . push ( new ast . Node ( { attributes : { } , content : mdNode . content , location , tag : "" , type : "text" , } ) ) ; result . push ( new ast . Node ( { attributes : { } , content : "" , location , tag : "/code" , type : "code_close" , } ) ) ; return result ; } private standardizeEmbeddedCodeblock ( mdNode , location ) { const result = new ast . NodeList ( ) ; result . push ( new ast . Node ( { attributes : standardizeMarkdownItAttributes ( mdNode . attrs ) , content : "" , location , tag : "pre" , type : "fence_open" , } ) ) ; result . push ( new ast . Node ( { attributes : { } , content : mdNode . content . trim ( ) , location , tag : "" , type : "text" , } ) ) ; result . push ( new ast . Node ( { attributes : { } , content : "" , location : location . withLine ( ( mdNode . map || [ 0 , 0 ] ) [ 1 ] ) , tag : "/pre" , type : "fence_close" , } ) ) ; return result ; } private standardizeFence ( mdNode , location ) { const result = new ast . NodeList ( ) ; result . push ( new ast . Node ( { attributes : standardizeMarkdownItAttributes ( mdNode . attrs ) , content : "" , location , tag : "pre" , type : "fence_open" , } ) ) ; result . push ( new ast . Node ( { attributes : { } , content : mdNode . content . trim ( ) , location : location . withLine ( location . line + 1 ) , tag : "" , type : "text" , } ) ) ; result . push ( new ast . Node ( { attributes : { } , content : "" , location : location . withLine ( ( mdNode . map || [ 0 , 0 ] ) [ 1 ] ) , tag : "/pre" , type : "fence_close" , } ) ) ; return result ; } private standardizeClosingHTMLTag ( mdNode , ont , location ) { const result = new ast . NodeList ( ) ; const parsed = this . closingTagParser . parse ( mdNode . content , location ) [ 0 ] ; if ( parsed . tag === "/a" ) { if ( ont . has ( "link_open" ) ) { parsed . type = "link_close" ; } else if ( ont . has ( "anchor_open" ) ) { parsed . type = "anchor_close" ; } else { throw new UserError ( `Found neither open link nor anchor for node '${ mdNode . content }'` , "I found a </a> tag here but there isn't an opening <a ...> tag above." , location ) ; } } ont . close ( parsed . type , location ) ; result . push ( parsed ) ; return result ; } private standardizeHTMLBlock ( mdNode , ont , location ) { const result = new ast . NodeList ( ) ; const parsed = this . htmlParser . parse ( mdNode . content , location ) ; for ( const node of parsed ) { if ( node . type . endsWith ( "_open" ) ) { ont . open ( node , ( mdNode . map || [ 0 , 0 ] ) [ 1 ] ) ; } if ( node . type . endsWith ( "_close" ) ) { ont . close ( node . type , location ) ; } } result . push ( ... parsed ) ; return result ; } private standardizeOpeningNode ( mdNode , location , ont ) { const result = new ast . NodeList ( ) ; result . push ( new ast . Node ( { attributes : standardizeMarkdownItAttributes ( mdNode . attrs ) , content : mdNode . content . trim ( ) , location , tag : this . tagMapper . tagForType ( mdNode . type as ast . NodeType ) , type : mdNode . type as ast . NodeType , } ) ) ; ont . open ( result [ 0 ] , ( mdNode . map || [ 0 , 0 ] ) [ 1 ] ) ; return result ; } private standardizeClosingNode ( mdNode , location , ont ) { const result = new ast . NodeList ( ) ; const openingNodeEndLine = ont . close ( mdNode . type as ast . NodeType , location ) ; let closingTagLine = location . line ; if ( openingNodeEndLine ) { closingTagLine = openingNodeEndLine ; } result . push ( new ast . Node ( { attributes : standardizeMarkdownItAttributes ( mdNode . attrs ) , content : mdNode . content . trim ( ) , location : location . withLine ( closingTagLine ) , tag : this . tagMapper . tagForType ( mdNode . type as ast . NodeType ) , type : mdNode . type as ast . NodeType , } ) ) ; return result ; } private standardizeTextNode ( mdNode , location ) { const result = new ast . NodeList ( ) ; result . push ( new ast . Node ( { attributes : standardizeMarkdownItAttributes ( mdNode . attrs ) , content : mdNode . content . trim ( ) , location , tag : this . tagMapper . tagForType ( mdNode . type as ast . NodeType ) , type : mdNode . type as ast . NodeType , } ) ) ; return result ; } private standizeStandaloneTag ( mdNode , location ) { const result = new ast . NodeList ( ) ; result . push ( new ast . Node ( { attributes : standardizeMarkdownItAttributes ( mdNode . attrs ) , content : mdNode . content . trim ( ) , location , tag : mdNode . tag as ast . NodeTag , type : mdNode . type as ast . NodeType , } ) ) ; return result ; } } /** returns the given attributes from a MarkdownIt node in the standard AST format */ export function standardizeMarkdownItAttributes ( attrs ) { const result = { } ; if ( attrs ) { for ( const [ name , value ] of attrs ) { result [ name ] = value ; } } return result ; }