silverbullet/common/markdown_parser/parser.test.ts

243 lines
8.5 KiB
TypeScript
Raw Permalink Normal View History

2024-02-29 22:23:05 +08:00
import {
collectNodesOfType,
findNodeOfType,
renderToText,
} from "@silverbulletmd/silverbullet/lib/tree";
2024-08-22 02:13:40 +08:00
import { assert, assertEquals, assertNotEquals } from "@std/assert";
import { parseMarkdown } from "./parser.ts";
import { extractHashtag, renderHashtag } from "../../plug-api/lib/tags.ts";
const sample1 = `---
type: page
tags:
- hello
- world
---
# This is a doc
2022-11-27 15:48:01 +08:00
Here is a [[wiki link]] and a [[wiki link|alias]].
Supper`;
const sampleInvalid1 = `---
name: Zef
# This is a doc
Supper`;
Deno.test("Test parser", () => {
2024-08-22 02:13:40 +08:00
let tree = parseMarkdown(sample1);
2022-11-27 15:48:01 +08:00
// console.log("tree", JSON.stringify(tree, null, 2));
// Check if rendering back to text works
assertEquals(renderToText(tree), sample1);
2022-11-27 15:48:01 +08:00
// Find wiki link and wiki link alias
const links = collectNodesOfType(tree, "WikiLink");
assertEquals(links.length, 2);
const nameNode = findNodeOfType(links[0], "WikiLinkPage");
2024-07-30 21:17:34 +08:00
assertEquals(nameNode!.children![0].text, "wiki link");
2022-11-27 15:48:01 +08:00
// Check if alias is parsed properly
const aliasNode = findNodeOfType(links[1], "WikiLinkAlias");
2024-07-30 21:17:34 +08:00
assertEquals(aliasNode!.children![0].text, "alias");
2022-11-27 15:48:01 +08:00
// Find frontmatter
let node = findNodeOfType(tree, "FrontMatter");
assertNotEquals(node, undefined);
2024-08-22 02:13:40 +08:00
tree = parseMarkdown(sampleInvalid1);
node = findNodeOfType(tree, "FrontMatter");
// console.log("Invalid node", node);
assertEquals(node, undefined);
});
2023-07-25 01:54:31 +08:00
const inlineAttributeSample = `
2023-07-26 23:12:56 +08:00
Hello there [a link](http://zef.plus)
[age: 100]
[age:: 200]
Here's a more [ambiguous: case](http://zef.plus)
And one with nested brackets: [array: [1, 2, 3]]
2023-07-25 01:54:31 +08:00
`;
Deno.test("Test inline attribute syntax", () => {
2024-08-22 02:13:40 +08:00
const tree = parseMarkdown(inlineAttributeSample);
// console.log("Attribute parsed", JSON.stringify(tree, null, 2));
2023-07-26 23:12:56 +08:00
const attributes = collectNodesOfType(tree, "Attribute");
let nameNode = findNodeOfType(attributes[0], "AttributeName");
2024-07-30 21:17:34 +08:00
assertEquals(nameNode!.children![0].text, "age");
2023-07-26 23:12:56 +08:00
let valueNode = findNodeOfType(attributes[0], "AttributeValue");
2024-07-30 21:17:34 +08:00
assertEquals(valueNode!.children![0].text, "100");
2023-07-26 23:12:56 +08:00
nameNode = findNodeOfType(attributes[1], "AttributeName");
2024-07-30 21:17:34 +08:00
assertEquals(nameNode!.children![0].text, "age");
2023-07-26 23:12:56 +08:00
valueNode = findNodeOfType(attributes[1], "AttributeValue");
2024-07-30 21:17:34 +08:00
assertEquals(valueNode!.children![0].text, "200");
2023-07-26 23:12:56 +08:00
nameNode = findNodeOfType(attributes[2], "AttributeName");
2024-07-30 21:17:34 +08:00
assertEquals(nameNode!.children![0].text, "array");
2023-07-26 23:12:56 +08:00
valueNode = findNodeOfType(attributes[2], "AttributeValue");
2024-07-30 21:17:34 +08:00
assertEquals(valueNode!.children![0].text, "[1, 2, 3]");
2023-07-25 01:54:31 +08:00
});
Deno.test("Test template directive parsing", () => {
2024-08-22 02:13:40 +08:00
const tree = parseMarkdown("Simple {{name}} and {{count({ page })}}");
assert(findNodeOfType(tree, "TemplateDirective"));
});
const multiStatusTaskExample = `
* [ ] Task 1
- [x] Task 2
* [TODO] Task 3
`;
Deno.test("Test multi-status tasks", () => {
2024-08-22 02:13:40 +08:00
const tree = parseMarkdown(multiStatusTaskExample);
// console.log("Tasks parsed", JSON.stringify(tree, null, 2));
const tasks = collectNodesOfType(tree, "Task");
assertEquals(tasks.length, 3);
// Check " " checkbox state parsing
assertEquals(tasks[0].children![0].children![1].text, " ");
assertEquals(tasks[1].children![0].children![1].text, "x");
assertEquals(tasks[2].children![0].children![1].text, "TODO");
});
const commandLinkSample = `
{[Some: Command]}
{[Other: Command|Alias]}
{[Command: Space | Spaces ]}
`;
Deno.test("Test command links", () => {
2024-08-22 02:13:40 +08:00
const tree = parseMarkdown(commandLinkSample);
const commands = collectNodesOfType(tree, "CommandLink");
// console.log("Command links parsed", JSON.stringify(commands, null, 2));
assertEquals(commands.length, 3);
assertEquals(commands[0].children![1].children![0].text, "Some: Command");
assertEquals(commands[1].children![1].children![0].text, "Other: Command");
assertEquals(commands[1].children![3].children![0].text, "Alias");
assertEquals(commands[2].children![1].children![0].text, "Command: Space ");
assertEquals(commands[2].children![3].children![0].text, " Spaces ");
});
const commandLinkArgsSample = `
{[Args: Command]("with", "args")}
{[Othargs: Command|Args alias]("other", "args", 123)}
`;
Deno.test("Test command link arguments", () => {
2024-08-22 02:13:40 +08:00
const tree = parseMarkdown(commandLinkArgsSample);
const commands = collectNodesOfType(tree, "CommandLink");
assertEquals(commands.length, 2);
const args1 = findNodeOfType(commands[0], "CommandLinkArgs");
assertEquals(args1!.children![0].text, '"with", "args"');
const args2 = findNodeOfType(commands[1], "CommandLinkArgs");
assertEquals(args2!.children![0].text, '"other", "args", 123');
});
Deno.test("Test directive parser", () => {
const simpleExample = `Simple {{.}}`;
2024-08-22 02:13:40 +08:00
let tree = parseMarkdown(simpleExample);
assertEquals(renderToText(tree), simpleExample);
const eachExample = `{{#each .}}Sup{{/each}}`;
2024-08-22 02:13:40 +08:00
tree = parseMarkdown(eachExample);
const ifExample = `{{#if true}}Sup{{/if}}`;
2024-08-22 02:13:40 +08:00
tree = parseMarkdown(ifExample);
assertEquals(renderToText(tree), ifExample);
const ifElseExample = `{{#if true}}Sup{{else}}Sup2{{/if}}`;
2024-08-22 02:13:40 +08:00
tree = parseMarkdown(ifElseExample);
assertEquals(renderToText(tree), ifElseExample);
2024-08-22 02:13:40 +08:00
// console.log("Final tree", JSON.stringify(tree, null, 2));
const letExample = `{{#let @p = true}}{{/let}}`;
2024-08-22 02:13:40 +08:00
tree = parseMarkdown(letExample);
assertEquals(renderToText(tree), letExample);
});
Deno.test("Test lua directive parser", () => {
2025-01-09 17:27:41 +08:00
const simpleExample = `Simple \${query_coll("page limit 3", template[==[
* Hello there {name}]==])}`;
console.log(JSON.stringify(parseMarkdown(simpleExample), null, 2));
});
const hashtagSample = `
Hashtags, e.g. #mytag but ignore in code \`#mytag\`.
They can contain slashes like #level/beginner, single quotes, and dashes: #Mike's-idea.
Can be just #a single letter.
But no other #interpunction: #exclamation! #question?
There is a way to write #<tag with spaces>
These cannot span #<multiple
lines>
#no#spacing also works.
Hashtags can start with number if there's something after it: #3dprint #15-52_Trip-to-NYC.
But magazine issue #1 or #123 are not hashtags.
Should support other languages, like #żółć or #
`;
Deno.test("Test hashtag parser", () => {
const tree = parseMarkdown(hashtagSample);
const hashtags = collectNodesOfType(tree, "Hashtag");
assertEquals(hashtags.length, 14);
assertEquals(hashtags[0].children![0].text, "#mytag");
assertEquals(hashtags[1].children![0].text, "#level/beginner");
assertEquals(hashtags[2].children![0].text, "#Mike's-idea");
assertEquals(hashtags[3].children![0].text, "#a");
assertEquals(hashtags[4].children![0].text, "#interpunction");
assertEquals(hashtags[5].children![0].text, "#exclamation");
assertEquals(hashtags[6].children![0].text, "#question");
assertEquals(hashtags[7].children![0].text, "#<tag with spaces>");
// multiple lines not allowed
assertEquals(hashtags[8].children![0].text, "#no");
assertEquals(hashtags[9].children![0].text, "#spacing");
assertEquals(hashtags[10].children![0].text, "#3dprint");
assertEquals(hashtags[11].children![0].text, "#15-52_Trip-to-NYC");
assertEquals(hashtags[12].children![0].text, "#żółć");
assertEquals(hashtags[13].children![0].text, "#井号");
});
Deno.test("Test hashtag helper functions", () => {
assertEquals(extractHashtag("#name"), "name");
assertEquals(extractHashtag("#123-content"), "123-content");
assertEquals(extractHashtag("#<escaped tag>"), "escaped tag");
assertEquals(
extractHashtag("#<allow < and # inside>"),
"allow < and # inside",
);
assertEquals(renderHashtag("simple"), "#simple");
assertEquals(renderHashtag("123-content"), "#123-content");
assertEquals(renderHashtag("with spaces"), "#<with spaces>");
assertEquals(renderHashtag("single'quote"), "#single'quote");
// should behave like this for all characters in tagRegex
assertEquals(renderHashtag("exclamation!"), "#<exclamation!>");
});
const nakedURLSample = `
http://abc.com is a URL
Also http://no-trailing-period.com. That's a URL. It ends with m, not '.'.
http://no-trailing-comma.com, that same a URL, ends with m (and not ',').
http://trailing-slash.com/. That ends with '/' (still not '.').
http://abc.com?e=2.71,pi=3.14 is a URL too.
http://abc.com?e=2.71. That is a URL, which ends with 1 (and not '.').
`;
Deno.test("Test NakedURL parser", () => {
const tree = parseMarkdown(nakedURLSample);
const urls = collectNodesOfType(tree, "NakedURL");
assertEquals(urls.map((x) => x.children![0].text), [
"http://abc.com",
"http://no-trailing-period.com",
"http://no-trailing-comma.com",
"http://trailing-slash.com/",
"http://abc.com?e=2.71,pi=3.14",
"http://abc.com?e=2.71",
]);
});