par(md): add simple paragraph block

This commit is contained in:
Dmitriy Pleshevskiy 2022-06-12 00:40:05 +03:00
parent 328343ca76
commit 5f1b6f2b1c
Signed by: pleshevskiy
GPG Key ID: 1B59187B161C0215
2 changed files with 109 additions and 44 deletions

View File

@ -4,6 +4,8 @@ import { MarkdownParser } from "./md.ts";
const ren = new HtmlStrRenderer();
// Misc
Deno.test({
name: "should skip new line character",
fn: () => {
@ -14,6 +16,8 @@ Deno.test({
},
});
// ATX Header
Deno.test({
name: "should parse empty ATX header",
fn: () => {
@ -86,3 +90,14 @@ Deno.test({
);
},
});
// Paragraph
Deno.test({
name: "should parse paragraph",
fn: () => {
const par = new MarkdownParser();
assertEquals(ren.render(par.parse("hello")), "<p>hello</p>");
},
});

138
par/md.ts
View File

@ -9,57 +9,35 @@ const RE_CLOSE_ATX_HEADING = /(^|\s+)#*\s*$/;
export class MarkdownParser implements Parser {
parse(input: string): AnyNode {
const ast: AstDocument = { kind: AstKind.Document, content: [] };
const astDoc: 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);
}
while (readStr.length) {
// 1. clear new line character
const match = RE_NEW_LINE.exec(readStr);
if (!isNil(match)) {
readStr = readStr.slice(match[0].length);
continue;
}
// 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;
}
const newReadStr = parseAtxHeading(astDoc, readStr) ??
parseParagraph(astDoc, readStr);
if (isNil(newReadStr)) break;
readStr = newReadStr;
}
return new Fragment(ast.content.map(Heading));
return new Fragment(astDoc.content.map(DocChild));
}
}
function DocChild(content: AstDocumentChild): Elem {
switch (content.kind) {
case AstKind.AtxHeading:
return Heading(content);
case AstKind.Paragraph:
return Paragraph(content);
}
}
@ -67,19 +45,90 @@ function Heading(ast: AstAtxHeading): Elem {
return new Elem(`h${ast.level}`, {}, ast.content.map(Text));
}
function Paragraph(ast: AstParagraph): Elem {
return new Elem("p", {}, ast.content.map(Text));
}
function Text(ast: AstText): TextNode {
return new TextNode(ast.content);
}
// parse utils
function parseAtxHeading(ast: AstDocument, readStr: string): string | null {
const match = RE_OPEN_ATX_HEADING.exec(readStr);
if (isNil(match)) return null;
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) return readStr;
const endMatch = RE_CLOSE_ATX_HEADING.exec(readStr);
const headingInlineContent = !isNil(endMatch)
? readStr.slice(0, endMatch.index)
: readStr.includes("\n")
? readStr.slice(0, readStr.indexOf("\n") + 1)
: readStr;
parseInlineContent(atxHeading, headingInlineContent);
return readStr.slice(
headingInlineContent.length + (endMatch?.[0].length ?? 0),
);
}
function parseParagraph(ast: AstDocument, readStr: string): string | null {
if (!readStr.length) return null;
const paragraph: AstParagraph = {
kind: AstKind.Paragraph,
content: [],
};
ast.content.push(paragraph);
const paragraphInlineContent = readStr.includes("\n")
? readStr.slice(0, readStr.indexOf("\n") + 1)
: readStr;
parseInlineContent(paragraph, paragraphInlineContent);
return readStr.slice(paragraphInlineContent.length);
}
function parseInlineContent(
ast: AstAtxHeading | AstParagraph,
readStr: string,
): string | null {
if (!readStr.length) return null;
const text: AstText = {
kind: AstKind.Text,
content: readStr.trim(),
};
ast.content.push(text);
return readStr;
}
// AST
type AstDocument = BaseAstItem<AstKind.Document, AstDocumentChild[]>;
type AstDocumentChild = AstAtxHeading;
type AstDocumentChild = AstAtxHeading | AstParagraph;
interface AstAtxHeading extends BaseAstItem<AstKind.AtxHeading, AstText[]> {
level: HeadingLevel;
}
type AstParagraph = BaseAstItem<AstKind.Paragraph, AstText[]>;
type AstText = BaseAstItem<AstKind.Text, string>;
type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
@ -92,5 +141,6 @@ interface BaseAstItem<K extends AstKind, Cont> {
enum AstKind {
Document,
AtxHeading,
Paragraph,
Text,
}