par(md): add simple paragraph block
This commit is contained in:
parent
328343ca76
commit
5f1b6f2b1c
|
@ -4,6 +4,8 @@ import { MarkdownParser } from "./md.ts";
|
||||||
|
|
||||||
const ren = new HtmlStrRenderer();
|
const ren = new HtmlStrRenderer();
|
||||||
|
|
||||||
|
// Misc
|
||||||
|
|
||||||
Deno.test({
|
Deno.test({
|
||||||
name: "should skip new line character",
|
name: "should skip new line character",
|
||||||
fn: () => {
|
fn: () => {
|
||||||
|
@ -14,6 +16,8 @@ Deno.test({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ATX Header
|
||||||
|
|
||||||
Deno.test({
|
Deno.test({
|
||||||
name: "should parse empty ATX header",
|
name: "should parse empty ATX header",
|
||||||
fn: () => {
|
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
138
par/md.ts
|
@ -9,57 +9,35 @@ const RE_CLOSE_ATX_HEADING = /(^|\s+)#*\s*$/;
|
||||||
|
|
||||||
export class MarkdownParser implements Parser {
|
export class MarkdownParser implements Parser {
|
||||||
parse(input: string): AnyNode {
|
parse(input: string): AnyNode {
|
||||||
const ast: AstDocument = { kind: AstKind.Document, content: [] };
|
const astDoc: AstDocument = { kind: AstKind.Document, content: [] };
|
||||||
|
|
||||||
let readStr = input;
|
let readStr = input;
|
||||||
|
|
||||||
while (readStr.trim().length) {
|
while (readStr.length) {
|
||||||
{
|
// 1. clear new line character
|
||||||
// 1. clear new line character
|
const match = RE_NEW_LINE.exec(readStr);
|
||||||
const match = RE_NEW_LINE.exec(readStr);
|
if (!isNil(match)) {
|
||||||
if (!isNil(match)) {
|
readStr = readStr.slice(match[0].length);
|
||||||
readStr = readStr.slice(match[0].length);
|
continue;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. try to find atx heading sequence
|
// 2. try to find atx heading sequence
|
||||||
const match = RE_OPEN_ATX_HEADING.exec(readStr);
|
const newReadStr = parseAtxHeading(astDoc, readStr) ??
|
||||||
if (!isNil(match)) {
|
parseParagraph(astDoc, readStr);
|
||||||
readStr = readStr.slice(match[0].length);
|
if (isNil(newReadStr)) break;
|
||||||
|
readStr = newReadStr;
|
||||||
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));
|
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));
|
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 {
|
function Text(ast: AstText): TextNode {
|
||||||
return new TextNode(ast.content);
|
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
|
// AST
|
||||||
|
|
||||||
type AstDocument = BaseAstItem<AstKind.Document, AstDocumentChild[]>;
|
type AstDocument = BaseAstItem<AstKind.Document, AstDocumentChild[]>;
|
||||||
type AstDocumentChild = AstAtxHeading;
|
type AstDocumentChild = AstAtxHeading | AstParagraph;
|
||||||
|
|
||||||
interface AstAtxHeading extends BaseAstItem<AstKind.AtxHeading, AstText[]> {
|
interface AstAtxHeading extends BaseAstItem<AstKind.AtxHeading, AstText[]> {
|
||||||
level: HeadingLevel;
|
level: HeadingLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AstParagraph = BaseAstItem<AstKind.Paragraph, AstText[]>;
|
||||||
|
|
||||||
type AstText = BaseAstItem<AstKind.Text, string>;
|
type AstText = BaseAstItem<AstKind.Text, string>;
|
||||||
|
|
||||||
type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
|
@ -92,5 +141,6 @@ interface BaseAstItem<K extends AstKind, Cont> {
|
||||||
enum AstKind {
|
enum AstKind {
|
||||||
Document,
|
Document,
|
||||||
AtxHeading,
|
AtxHeading,
|
||||||
|
Paragraph,
|
||||||
Text,
|
Text,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue