par(md): add simple list
This commit is contained in:
parent
1d708bc95f
commit
0c7fb2ee56
2 changed files with 161 additions and 8 deletions
|
@ -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
102
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_OPEN_ATX_HEADING = /^\s{0,3}(#{1,6})(\s|$)/;
|
||||||
const RE_CLOSE_ATX_HEADING = /(^|\s+)#*\s*$/;
|
const RE_CLOSE_ATX_HEADING = /(^|\s+)#*\s*$/;
|
||||||
|
|
||||||
|
const RE_LIST_ITEM = /^\s{0,3}([-+*])(\s|$)/;
|
||||||
|
|
||||||
// TODO: make better regex for destination
|
// TODO: make better regex for destination
|
||||||
const RE_LINK = /\[([\s\S]*?)]\((?:([^\s]*)|<(.+?)>)(?: ('|")(.+?)\4)?\)/;
|
const RE_LINK = /\[([\s\S]*?)]\((?:([^\s]*)|<(.+?)>)(?: ('|")(.+?)\4)?\)/;
|
||||||
|
|
||||||
|
@ -18,6 +20,7 @@ export class MarkdownParser implements Parser {
|
||||||
while (readStr.length) {
|
while (readStr.length) {
|
||||||
const newReadStr = skipEmptyLine(readStr) ??
|
const newReadStr = skipEmptyLine(readStr) ??
|
||||||
parseAtxHeading(astDoc, readStr) ??
|
parseAtxHeading(astDoc, readStr) ??
|
||||||
|
parseList(astDoc, readStr) ??
|
||||||
parseParagraph(astDoc, readStr);
|
parseParagraph(astDoc, readStr);
|
||||||
if (isNil(newReadStr)) break;
|
if (isNil(newReadStr)) break;
|
||||||
readStr = newReadStr;
|
readStr = newReadStr;
|
||||||
|
@ -27,12 +30,33 @@ export class MarkdownParser implements Parser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function DocChild(content: AstDocumentChild): Elem {
|
function List(ast: AstList): Elem {
|
||||||
switch (content.kind) {
|
// 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:
|
case AstKind.AtxHeading:
|
||||||
return Heading(content);
|
return Heading(ast);
|
||||||
case AstKind.Paragraph:
|
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;
|
if (!readStr.length) return null;
|
||||||
|
|
||||||
const paragraph: AstParagraph = {
|
const paragraph: AstParagraph = {
|
||||||
|
@ -108,7 +170,8 @@ function parseParagraph(ast: AstDocument, readStr: string): string | null {
|
||||||
|
|
||||||
let paragraphInlineContent = "";
|
let paragraphInlineContent = "";
|
||||||
while (!RE_EMPTY_LINE.test(readStr)) {
|
while (!RE_EMPTY_LINE.test(readStr)) {
|
||||||
console.log({ readStr });
|
const listMatch = RE_LIST_ITEM.exec(readStr);
|
||||||
|
if (!isNil(listMatch)) break;
|
||||||
paragraphInlineContent += readStr.includes("\n")
|
paragraphInlineContent += readStr.includes("\n")
|
||||||
? readStr.slice(0, readStr.indexOf("\n") + 1)
|
? readStr.slice(0, readStr.indexOf("\n") + 1)
|
||||||
: readStr;
|
: readStr;
|
||||||
|
@ -161,7 +224,10 @@ function parseText(
|
||||||
if (!readStr.length) return null;
|
if (!readStr.length) return null;
|
||||||
|
|
||||||
const parts = readStr.split("\n").filter(Boolean).map(
|
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);
|
ast.content.push(...parts);
|
||||||
|
@ -172,7 +238,20 @@ function parseText(
|
||||||
// AST
|
// AST
|
||||||
|
|
||||||
type AstDocument = BaseAstItem<AstKind.Document, AstDocumentChild[]>;
|
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
|
interface AstAtxHeading
|
||||||
extends BaseAstItem<AstKind.AtxHeading, AstInlineContent[]> {
|
extends BaseAstItem<AstKind.AtxHeading, AstInlineContent[]> {
|
||||||
|
@ -181,6 +260,11 @@ interface AstAtxHeading
|
||||||
|
|
||||||
type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
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 AstParagraph = BaseAstItem<AstKind.Paragraph, AstInlineContent[]>;
|
||||||
|
|
||||||
type AstInlineContent = AstText | AstLink;
|
type AstInlineContent = AstText | AstLink;
|
||||||
|
@ -201,6 +285,8 @@ enum AstKind {
|
||||||
Document,
|
Document,
|
||||||
AtxHeading,
|
AtxHeading,
|
||||||
Paragraph,
|
Paragraph,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
Link,
|
Link,
|
||||||
Text,
|
Text,
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue