par(md): add simple list

This commit is contained in:
Dmitriy Pleshevskiy 2022-06-13 17:55:47 +03:00
parent 1d708bc95f
commit 0c7fb2ee56
Signed by: pleshevskiy
GPG Key ID: 1B59187B161C0215
2 changed files with 161 additions and 8 deletions

View File

@ -191,3 +191,70 @@ Deno.test({
);
},
});
// List
Deno.test({
name: "should parse list with empty items",
fn: () => {
const par = new MarkdownParser();
assertEquals(
ren.render(par.parse("-")),
"<ul><li></li></ul>",
);
assertEquals(
ren.render(par.parse("- ")),
"<ul><li></li></ul>",
);
},
});
Deno.test({
name: "should parse list if line contains additional spaces",
fn: () => {
const expected = "<ul><li>hello</li></ul>";
const par = new MarkdownParser();
assertEquals(ren.render(par.parse(" - hello")), expected);
assertEquals(ren.render(par.parse(" - hello")), expected);
assertEquals(ren.render(par.parse(" - hello")), expected);
},
});
Deno.test({
name: "should not display a single paragraph in the list",
fn: () => {
const par = new MarkdownParser();
assertEquals(
ren.render(par.parse("- hello")),
"<ul><li>hello</li></ul>",
);
assertEquals(
ren.render(par.parse(`\
- hello
world`)),
"<ul><li>hello world</li></ul>",
);
assertEquals(
ren.render(par.parse(`\
- hello
world`)),
"<ul><li>hello world</li></ul>",
);
},
});
Deno.test({
name: "should parse many items in the list",
fn: () => {
const par = new MarkdownParser();
assertEquals(
ren.render(par.parse(`\
- hello
- world`)),
"<ul><li>hello</li><li>world</li></ul>",
);
},
});

102
par/md.ts
View File

@ -7,6 +7,8 @@ const RE_EMPTY_LINE = /^\s*$/;
const RE_OPEN_ATX_HEADING = /^\s{0,3}(#{1,6})(\s|$)/;
const RE_CLOSE_ATX_HEADING = /(^|\s+)#*\s*$/;
const RE_LIST_ITEM = /^\s{0,3}([-+*])(\s|$)/;
// TODO: make better regex for destination
const RE_LINK = /\[([\s\S]*?)]\((?:([^\s]*)|<(.+?)>)(?: ('|")(.+?)\4)?\)/;
@ -18,6 +20,7 @@ export class MarkdownParser implements Parser {
while (readStr.length) {
const newReadStr = skipEmptyLine(readStr) ??
parseAtxHeading(astDoc, readStr) ??
parseList(astDoc, readStr) ??
parseParagraph(astDoc, readStr);
if (isNil(newReadStr)) break;
readStr = newReadStr;
@ -27,12 +30,33 @@ export class MarkdownParser implements Parser {
}
}
function DocChild(content: AstDocumentChild): Elem {
switch (content.kind) {
function List(ast: AstList): Elem {
// switch (ast.kind)
return BulletList(ast);
}
function BulletList(ast: AstBulletList): Elem {
return new Elem("ul", {}, ast.content.map(ListItem));
}
function ListItem(ast: AstListItem): Elem {
return new Elem(
"li",
{},
ast.content.length === 1 && ast.content[0].kind === AstKind.Paragraph
? ast.content[0].content.map(InlineContent)
: ast.content.map(DocChild),
);
}
function DocChild(ast: AstDocumentChild): Elem {
switch (ast.kind) {
case AstKind.AtxHeading:
return Heading(content);
return Heading(ast);
case AstKind.Paragraph:
return Paragraph(content);
return Paragraph(ast);
case AstKind.List:
return List(ast);
}
}
@ -97,7 +121,45 @@ function parseAtxHeading(ast: AstDocument, readStr: string): string | null {
);
}
function parseParagraph(ast: AstDocument, readStr: string): string | null {
function parseList(ast: AstDocument, readStr: string): string | null {
if (!readStr.length) return null;
let listMatch = RE_LIST_ITEM.exec(readStr);
if (isNil(listMatch)) return null;
const astList: AstBulletList = {
kind: AstKind.List,
type: AstListType.Bullet,
bulletChar: listMatch[1] as ListBulletChar,
content: [],
};
ast.content.push(astList);
do {
const astListItem: AstListItem = {
kind: AstKind.ListItem,
content: [],
};
astList.content.push(astListItem);
readStr = readStr.slice(listMatch[0].length);
const newReadStr = // parseAtxHeading(astList, readStr) ??
// parseList(astList, readStr) ??
parseParagraph(astListItem, readStr);
if (isNil(newReadStr)) break;
readStr = newReadStr;
listMatch = RE_LIST_ITEM.exec(readStr);
} while (!isNil(listMatch));
return readStr;
}
function parseParagraph(
ast: AstDocument | AstListItem,
readStr: string,
): string | null {
if (!readStr.length) return null;
const paragraph: AstParagraph = {
@ -108,7 +170,8 @@ function parseParagraph(ast: AstDocument, readStr: string): string | null {
let paragraphInlineContent = "";
while (!RE_EMPTY_LINE.test(readStr)) {
console.log({ readStr });
const listMatch = RE_LIST_ITEM.exec(readStr);
if (!isNil(listMatch)) break;
paragraphInlineContent += readStr.includes("\n")
? readStr.slice(0, readStr.indexOf("\n") + 1)
: readStr;
@ -161,7 +224,10 @@ function parseText(
if (!readStr.length) return null;
const parts = readStr.split("\n").filter(Boolean).map(
(textPart): AstText => ({ kind: AstKind.Text, content: textPart }),
(textPart): AstText => ({
kind: AstKind.Text,
content: textPart.trimStart(),
}),
);
ast.content.push(...parts);
@ -172,7 +238,20 @@ function parseText(
// AST
type AstDocument = BaseAstItem<AstKind.Document, AstDocumentChild[]>;
type AstDocumentChild = AstAtxHeading | AstParagraph;
type AstDocumentChild = AstAtxHeading | AstBulletList | AstParagraph | AstList;
type AstList = AstBulletList; // | AstOrderedList
enum AstListType {
Bullet,
// Ordered,
}
type ListBulletChar = "-" | "+" | "*";
type AstListItem = BaseAstItem<AstKind.ListItem, AstListItemChild[]>;
type AstListItemChild = AstDocumentChild;
interface AstAtxHeading
extends BaseAstItem<AstKind.AtxHeading, AstInlineContent[]> {
@ -181,6 +260,11 @@ interface AstAtxHeading
type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
interface AstBulletList extends BaseAstItem<AstKind.List, AstListItem[]> {
type: AstListType.Bullet;
bulletChar: ListBulletChar;
}
type AstParagraph = BaseAstItem<AstKind.Paragraph, AstInlineContent[]>;
type AstInlineContent = AstText | AstLink;
@ -201,6 +285,8 @@ enum AstKind {
Document,
AtxHeading,
Paragraph,
List,
ListItem,
Link,
Text,
}