import { AnyNode, Elem, Fragment, TextNode } from "../core/node.ts"; import { isNil } from "../core/utils.ts"; import { Parser } from "./types.ts"; const RE_NEW_LINE = /^\r?\n/; const RE_OPEN_ATX_HEADING = /^\s{0,3}(#{1,6})(\s|$)/; const RE_CLOSE_ATX_HEADING = /(^|\s+)#*\s*$/; export class MarkdownParser implements Parser { parse(input: string): AnyNode { const ast: AstDocument = { kind: AstKind.Document, content: [] }; let readStr = input; while (readStr.trim().length) { { // 1. clear new line character const match = RE_NEW_LINE.exec(readStr); if (!isNil(match)) { readStr = readStr.slice(match[0].length); } } // 2. try to find atx heading sequence const match = RE_OPEN_ATX_HEADING.exec(readStr); if (!isNil(match)) { readStr = readStr.slice(match[0].length); const atxHeading: AstAtxHeading = { kind: AstKind.AtxHeading, level: match[1].length as HeadingLevel, content: [], }; ast.content.push(atxHeading); if (match[2].length > 0) { const endMatch = RE_CLOSE_ATX_HEADING.exec(readStr); const headingContent = !isNil(endMatch) ? readStr.slice(0, endMatch.index) : readStr.includes("\n") ? readStr.slice(0, readStr.indexOf("\n") + 1) : readStr; readStr = readStr.slice( headingContent.length + (endMatch?.[0].length ?? 0), ); if (headingContent.length) { const text: AstText = { kind: AstKind.Text, content: headingContent.trim(), }; atxHeading.content.push(text); } } } else { break; } } return new Fragment(ast.content.map(Heading)); } } function Heading(ast: AstAtxHeading): Elem { return new Elem(`h${ast.level}`, {}, ast.content.map(Text)); } function Text(ast: AstText): TextNode { return new TextNode(ast.content); } // AST type AstDocument = BaseAstItem; type AstDocumentChild = AstAtxHeading; interface AstAtxHeading extends BaseAstItem { level: HeadingLevel; } type AstText = BaseAstItem; type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; interface BaseAstItem { kind: K; content: Cont; } enum AstKind { Document, AtxHeading, Text, }