Compare commits
2 Commits
53c4f4039c
...
1d708bc95f
Author | SHA1 | Date |
---|---|---|
Dmitriy Pleshevskiy | 1d708bc95f | |
Dmitriy Pleshevskiy | c9598e7997 |
|
@ -113,3 +113,81 @@ world`;
|
|||
assertEquals(ren.render(par.parse(input)), "<p>hello world</p>");
|
||||
},
|
||||
});
|
||||
|
||||
// Link
|
||||
|
||||
Deno.test({
|
||||
name: "should parse link",
|
||||
fn: () => {
|
||||
const par = new MarkdownParser();
|
||||
|
||||
assertEquals(
|
||||
ren.render(par.parse("[]()")),
|
||||
'<p><a href="#"></a></p>',
|
||||
);
|
||||
assertEquals(
|
||||
ren.render(par.parse("[hello]()")),
|
||||
'<p><a href="#">hello</a></p>',
|
||||
);
|
||||
assertEquals(
|
||||
ren.render(par.parse("[hello]()")),
|
||||
'<p><a href="#">hello</a></p>',
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "should parse link destination",
|
||||
fn: () => {
|
||||
const par = new MarkdownParser();
|
||||
|
||||
assertEquals(
|
||||
ren.render(par.parse("[](/hello)")),
|
||||
'<p><a href="/hello"></a></p>',
|
||||
);
|
||||
assertEquals(
|
||||
ren.render(par.parse("[](/hello?key=value&key2=value2)")),
|
||||
'<p><a href="/hello?key=value&key2=value2"></a></p>',
|
||||
);
|
||||
assertEquals(
|
||||
ren.render(par.parse("[hello](https://example.com)")),
|
||||
'<p><a href="https://example.com">hello</a></p>',
|
||||
);
|
||||
assertEquals(
|
||||
ren.render(par.parse("[hello](mailto:john@example.com)")),
|
||||
'<p><a href="mailto:john@example.com">hello</a></p>',
|
||||
);
|
||||
assertEquals(
|
||||
ren.render(par.parse("[](/привет)")),
|
||||
'<p><a href="/%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82"></a></p>',
|
||||
);
|
||||
assertEquals(
|
||||
ren.render(par.parse("[](</hello world>)")),
|
||||
'<p><a href="/hello%20world"></a></p>',
|
||||
);
|
||||
assertEquals(
|
||||
ren.render(par.parse("[](</hello world?key=value value2&key2=value3>)")),
|
||||
'<p><a href="/hello%20world?key=value%20value2&key2=value3"></a></p>',
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "should parse link title",
|
||||
fn: () => {
|
||||
const par = new MarkdownParser();
|
||||
|
||||
assertEquals(
|
||||
ren.render(par.parse("[](/hello 'hello')")),
|
||||
'<p><a href="/hello" title="hello"></a></p>',
|
||||
);
|
||||
assertEquals(
|
||||
ren.render(par.parse('[hello](/hello "world")')),
|
||||
'<p><a href="/hello" title="world">hello</a></p>',
|
||||
);
|
||||
assertEquals(
|
||||
ren.render(par.parse('[hello](</hello world> "hello world")')),
|
||||
'<p><a href="/hello%20world" title="hello world">hello</a></p>',
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
62
par/md.ts
62
par/md.ts
|
@ -1,5 +1,5 @@
|
|||
import { AnyNode, Elem, Fragment, TextNode } from "../core/node.ts";
|
||||
import { isNil } from "../core/utils.ts";
|
||||
import { isNil, Nilable } from "../core/utils.ts";
|
||||
import { Parser } from "./types.ts";
|
||||
|
||||
const RE_EMPTY_LINE = /^\s*$/;
|
||||
|
@ -7,6 +7,9 @@ const RE_EMPTY_LINE = /^\s*$/;
|
|||
const RE_OPEN_ATX_HEADING = /^\s{0,3}(#{1,6})(\s|$)/;
|
||||
const RE_CLOSE_ATX_HEADING = /(^|\s+)#*\s*$/;
|
||||
|
||||
// TODO: make better regex for destination
|
||||
const RE_LINK = /\[([\s\S]*?)]\((?:([^\s]*)|<(.+?)>)(?: ('|")(.+?)\4)?\)/;
|
||||
|
||||
export class MarkdownParser implements Parser {
|
||||
parse(input: string): AnyNode {
|
||||
const astDoc: AstDocument = { kind: AstKind.Document, content: [] };
|
||||
|
@ -41,8 +44,15 @@ function Paragraph(ast: AstParagraph): Elem {
|
|||
return new Elem("p", {}, ast.content.map(InlineContent));
|
||||
}
|
||||
|
||||
function InlineContent(ast: AstInlineContent): TextNode {
|
||||
return Text(ast);
|
||||
function InlineContent(ast: AstInlineContent): AnyNode {
|
||||
return ast.kind === AstKind.Link ? Link(ast) : Text(ast);
|
||||
}
|
||||
|
||||
function Link(ast: AstLink): Elem {
|
||||
const attrs: Record<string, string> = { href: ast.destination || "#" };
|
||||
if (ast.title) attrs.title = ast.title;
|
||||
|
||||
return new Elem("a", attrs, ast.content.map(Text));
|
||||
}
|
||||
|
||||
function Text(ast: AstText): TextNode {
|
||||
|
@ -105,8 +115,6 @@ function parseParagraph(ast: AstDocument, readStr: string): string | null {
|
|||
readStr = readStr.slice(paragraphInlineContent.length);
|
||||
}
|
||||
|
||||
console.log({ paragraphInlineContent, readStr });
|
||||
|
||||
if (paragraphInlineContent.length) {
|
||||
parseInlineContent(paragraph, paragraphInlineContent);
|
||||
}
|
||||
|
@ -120,13 +128,45 @@ function parseInlineContent(
|
|||
): string | null {
|
||||
if (!readStr.length) return null;
|
||||
|
||||
const linkMatch = RE_LINK.exec(readStr);
|
||||
if (!isNil(linkMatch)) {
|
||||
const astLink: AstLink = {
|
||||
kind: AstKind.Link,
|
||||
destination: encodeURI(linkMatch[3] ?? linkMatch[2]),
|
||||
title: linkMatch[5],
|
||||
content: [],
|
||||
};
|
||||
|
||||
// 1. parse before link
|
||||
parseText(ast, readStr.slice(0, linkMatch.index));
|
||||
|
||||
// 2. create link and parse inner content for link
|
||||
ast.content.push(astLink);
|
||||
parseText(astLink, linkMatch[1]);
|
||||
|
||||
// 3. parse rest text
|
||||
return parseInlineContent(
|
||||
ast,
|
||||
readStr.slice(linkMatch.index + linkMatch[0].length),
|
||||
);
|
||||
} else {
|
||||
return parseText(ast, readStr);
|
||||
}
|
||||
}
|
||||
|
||||
function parseText(
|
||||
ast: AstAtxHeading | AstParagraph | AstLink,
|
||||
readStr: string,
|
||||
): string | null {
|
||||
if (!readStr.length) return null;
|
||||
|
||||
const parts = readStr.split("\n").filter(Boolean).map(
|
||||
(textPart): AstText => ({ kind: AstKind.Text, content: textPart }),
|
||||
);
|
||||
|
||||
ast.content = parts;
|
||||
ast.content.push(...parts);
|
||||
|
||||
return readStr;
|
||||
return "";
|
||||
}
|
||||
|
||||
// AST
|
||||
|
@ -143,7 +183,12 @@ type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|||
|
||||
type AstParagraph = BaseAstItem<AstKind.Paragraph, AstInlineContent[]>;
|
||||
|
||||
type AstInlineContent = AstText;
|
||||
type AstInlineContent = AstText | AstLink;
|
||||
|
||||
interface AstLink extends BaseAstItem<AstKind.Link, AstText[]> {
|
||||
destination: string;
|
||||
title: Nilable<string>;
|
||||
}
|
||||
|
||||
type AstText = BaseAstItem<AstKind.Text, string>;
|
||||
|
||||
|
@ -156,5 +201,6 @@ enum AstKind {
|
|||
Document,
|
||||
AtxHeading,
|
||||
Paragraph,
|
||||
Link,
|
||||
Text,
|
||||
}
|
||||
|
|
|
@ -19,24 +19,34 @@ Deno.test({
|
|||
Deno.test({
|
||||
name: "should render element",
|
||||
fn: () => {
|
||||
const el = E("p", [], "hello world");
|
||||
|
||||
const ren = new HtmlStrRenderer();
|
||||
const res = ren.render(el);
|
||||
|
||||
assertEquals(res, "<p>hello world</p>");
|
||||
assertEquals(ren.render(E("p", [])), "<p></p>");
|
||||
assertEquals(ren.render(E("p", [], "hello world")), "<p>hello world</p>");
|
||||
assertEquals(
|
||||
ren.render(E("p", [], ["hello", "world"])),
|
||||
"<p>hello world</p>",
|
||||
);
|
||||
assertEquals(
|
||||
ren.render(E("p", [], [E("span", [], "hello"), E("span", [], "world")])),
|
||||
"<p><span>hello</span><span>world</span></p>",
|
||||
);
|
||||
assertEquals(
|
||||
ren.render(E("p", [], ["hello", E("span", [], "world")])),
|
||||
"<p>hello <span>world</span></p>",
|
||||
);
|
||||
assertEquals(
|
||||
ren.render(E("p", [], [E("span", [], "hello"), "world"])),
|
||||
"<p><span>hello</span> world</p>",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "should render empty fragment as empty string",
|
||||
fn: () => {
|
||||
const frag = F([]);
|
||||
|
||||
const ren = new HtmlStrRenderer();
|
||||
const res = ren.render(frag);
|
||||
|
||||
assertEquals(res, "");
|
||||
assertEquals(ren.render(F([])), "");
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -52,7 +62,7 @@ Deno.test({
|
|||
const ren = new HtmlStrRenderer();
|
||||
const res = ren.render(frag);
|
||||
|
||||
assertEquals(res, 'hello world<div class="hello"></div><p>world</p>');
|
||||
assertEquals(res, 'hello world <div class="hello"></div><p>world</p>');
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -79,7 +79,9 @@ function encodeHtmlFragment(
|
|||
node: Fragment,
|
||||
hooks: HtmlStrRendererHooks,
|
||||
): string {
|
||||
return concat(node.children.map((ch) => encodeAnyNode(ch, hooks)));
|
||||
return concatEncodedNodes(
|
||||
node.children.map((ch) => encodeAnyNode(ch, hooks)),
|
||||
);
|
||||
}
|
||||
|
||||
function encodeHtmlElement(
|
||||
|
@ -90,7 +92,11 @@ function encodeHtmlElement(
|
|||
if (isSelfClosedTagName(tagName)) return open;
|
||||
|
||||
const encodedChildren = children.map((ch) => encodeAnyNode(ch, hooks));
|
||||
return `${open}${concat(encodedChildren)}</${tagName}>`;
|
||||
return `${open}${concatEncodedNodes(encodedChildren)}</${tagName}>`;
|
||||
}
|
||||
|
||||
function concatEncodedNodes(encodedChildren: string[]): string {
|
||||
return join(" ", encodedChildren).replace(/>\s+?</g, "><");
|
||||
}
|
||||
|
||||
function encodeAttrs(
|
||||
|
|
Loading…
Reference in New Issue