From 0c7fb2ee56d7fb9ef4aea3839c6d60307be84987 Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy <dmitriy@ideascup.me> Date: Mon, 13 Jun 2022 17:55:47 +0300 Subject: [PATCH] par(md): add simple list --- par/md.test.ts | 67 ++++++++++++++++++++++++++++++++ par/md.ts | 102 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 161 insertions(+), 8 deletions(-) diff --git a/par/md.test.ts b/par/md.test.ts index 08e18e8..46dbf84 100644 --- a/par/md.test.ts +++ b/par/md.test.ts @@ -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>", + ); + }, +}); diff --git a/par/md.ts b/par/md.ts index 4de23f4..78987fc 100644 --- a/par/md.ts +++ b/par/md.ts @@ -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, }