2022-11-01 22:01:28 +08:00
|
|
|
import {
|
2023-10-29 19:10:30 +08:00
|
|
|
addParentPointers,
|
2023-10-03 20:16:33 +08:00
|
|
|
collectNodesOfType,
|
2022-11-01 22:01:28 +08:00
|
|
|
findNodeOfType,
|
2024-07-30 23:33:33 +08:00
|
|
|
type ParseTree,
|
2023-11-28 17:06:53 +08:00
|
|
|
removeParentPointers,
|
2022-11-01 22:01:28 +08:00
|
|
|
renderToText,
|
|
|
|
traverseTree,
|
2024-08-07 02:11:38 +08:00
|
|
|
} from "@silverbulletmd/silverbullet/lib/tree";
|
|
|
|
import {
|
|
|
|
encodePageRef,
|
|
|
|
parsePageRef,
|
|
|
|
} from "@silverbulletmd/silverbullet/lib/page_ref";
|
2024-07-30 23:33:33 +08:00
|
|
|
import { Fragment, renderHtml, type Tag } from "./html_render.ts";
|
2024-08-07 02:11:38 +08:00
|
|
|
import { isLocalPath } from "@silverbulletmd/silverbullet/lib/resolve";
|
|
|
|
import type { PageMeta } from "@silverbulletmd/silverbullet/types";
|
2024-12-14 16:57:46 +08:00
|
|
|
import * as TagConstants from "../../plugs/index/constants.ts";
|
2025-01-08 02:57:46 +08:00
|
|
|
import { extractHashtag } from "@silverbulletmd/silverbullet/lib/tags";
|
2022-11-01 22:01:28 +08:00
|
|
|
|
2023-11-02 19:35:30 +08:00
|
|
|
export type MarkdownRenderOptions = {
|
2022-11-01 22:01:28 +08:00
|
|
|
failOnUnknown?: true;
|
|
|
|
smartHardBreak?: true;
|
|
|
|
annotationPositions?: true;
|
2022-11-02 00:03:42 +08:00
|
|
|
attachmentUrlPrefix?: string;
|
2023-07-25 01:54:31 +08:00
|
|
|
preserveAttributes?: true;
|
2023-02-23 22:33:51 +08:00
|
|
|
// When defined, use to inline images as data: urls
|
2023-12-20 00:20:47 +08:00
|
|
|
translateUrls?: (url: string, type: "link" | "image") => string;
|
2022-11-01 22:01:28 +08:00
|
|
|
};
|
|
|
|
|
2023-10-29 19:10:30 +08:00
|
|
|
function cleanTags(values: (Tag | null)[], cleanWhitespace = false): Tag[] {
|
2022-11-01 22:01:28 +08:00
|
|
|
const result: Tag[] = [];
|
|
|
|
for (const value of values) {
|
2023-10-29 19:10:30 +08:00
|
|
|
if (cleanWhitespace && typeof value === "string" && value.match(/^\s+$/)) {
|
|
|
|
continue;
|
|
|
|
}
|
2022-11-01 22:01:28 +08:00
|
|
|
if (value) {
|
|
|
|
result.push(value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2024-05-19 17:05:48 +08:00
|
|
|
function preprocess(t: ParseTree) {
|
2023-10-29 19:10:30 +08:00
|
|
|
addParentPointers(t);
|
2022-11-01 22:01:28 +08:00
|
|
|
traverseTree(t, (node) => {
|
2023-10-29 19:10:30 +08:00
|
|
|
if (!node.type) {
|
|
|
|
if (node.text?.startsWith("\n")) {
|
|
|
|
const prevNodeIdx = node.parent!.children!.indexOf(node) - 1;
|
2024-03-02 20:09:05 +08:00
|
|
|
const prevNodeType = node.parent!.children![prevNodeIdx]?.type;
|
2024-03-10 19:28:13 +08:00
|
|
|
if (
|
|
|
|
prevNodeType?.includes("Heading") || prevNodeType?.includes("Table")
|
|
|
|
) {
|
2023-10-29 19:10:30 +08:00
|
|
|
node.text = node.text.slice(1);
|
2022-11-01 22:01:28 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function posPreservingRender(
|
|
|
|
t: ParseTree,
|
|
|
|
options: MarkdownRenderOptions = {},
|
|
|
|
): Tag | null {
|
|
|
|
const tag = render(t, options);
|
|
|
|
if (!options.annotationPositions) {
|
|
|
|
return tag;
|
|
|
|
}
|
|
|
|
if (!tag) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
if (typeof tag === "string") {
|
|
|
|
return tag;
|
|
|
|
}
|
|
|
|
if (t.from) {
|
|
|
|
if (!tag.attrs) {
|
|
|
|
tag.attrs = {};
|
|
|
|
}
|
|
|
|
tag.attrs["data-pos"] = "" + t.from;
|
|
|
|
}
|
|
|
|
return tag;
|
|
|
|
}
|
|
|
|
|
|
|
|
function render(
|
|
|
|
t: ParseTree,
|
|
|
|
options: MarkdownRenderOptions = {},
|
|
|
|
): Tag | null {
|
|
|
|
if (t.type?.endsWith("Mark") || t.type?.endsWith("Delimiter")) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
switch (t.type) {
|
|
|
|
case "Document":
|
|
|
|
return {
|
|
|
|
name: Fragment,
|
|
|
|
body: cleanTags(mapRender(t.children!)),
|
|
|
|
};
|
|
|
|
case "FrontMatter":
|
2023-05-24 02:53:53 +08:00
|
|
|
return null;
|
2022-11-01 22:01:28 +08:00
|
|
|
case "CommentBlock":
|
|
|
|
// Remove, for now
|
|
|
|
return null;
|
|
|
|
case "ATXHeading1":
|
|
|
|
return {
|
|
|
|
name: "h1",
|
|
|
|
body: cleanTags(mapRender(t.children!)),
|
|
|
|
};
|
|
|
|
case "ATXHeading2":
|
|
|
|
return {
|
|
|
|
name: "h2",
|
|
|
|
body: cleanTags(mapRender(t.children!)),
|
|
|
|
};
|
|
|
|
case "ATXHeading3":
|
|
|
|
return {
|
|
|
|
name: "h3",
|
|
|
|
body: cleanTags(mapRender(t.children!)),
|
|
|
|
};
|
|
|
|
case "ATXHeading4":
|
|
|
|
return {
|
|
|
|
name: "h4",
|
|
|
|
body: cleanTags(mapRender(t.children!)),
|
|
|
|
};
|
|
|
|
case "ATXHeading5":
|
|
|
|
return {
|
|
|
|
name: "h5",
|
|
|
|
body: cleanTags(mapRender(t.children!)),
|
|
|
|
};
|
2024-06-07 14:27:11 +08:00
|
|
|
case "ATXHeading6":
|
|
|
|
return {
|
|
|
|
name: "h6",
|
|
|
|
body: cleanTags(mapRender(t.children!)),
|
|
|
|
};
|
2022-11-01 22:01:28 +08:00
|
|
|
case "Paragraph":
|
|
|
|
return {
|
2023-10-29 19:10:30 +08:00
|
|
|
name: "span",
|
|
|
|
attrs: {
|
|
|
|
class: "p",
|
|
|
|
},
|
2022-11-01 22:01:28 +08:00
|
|
|
body: cleanTags(mapRender(t.children!)),
|
|
|
|
};
|
|
|
|
// Code blocks
|
|
|
|
case "FencedCode":
|
|
|
|
case "CodeBlock": {
|
2022-11-02 00:03:42 +08:00
|
|
|
// Clear out top-level indent blocks
|
2023-10-03 20:16:33 +08:00
|
|
|
const lang = findNodeOfType(t, "CodeInfo");
|
2022-11-02 00:03:42 +08:00
|
|
|
t.children = t.children!.filter((c) => c.type);
|
2022-11-01 22:01:28 +08:00
|
|
|
return {
|
|
|
|
name: "pre",
|
2023-10-03 20:16:33 +08:00
|
|
|
attrs: {
|
|
|
|
"data-lang": lang ? lang.children![0].text : undefined,
|
|
|
|
},
|
2022-11-01 22:01:28 +08:00
|
|
|
body: cleanTags(mapRender(t.children!)),
|
|
|
|
};
|
|
|
|
}
|
2022-11-02 00:03:42 +08:00
|
|
|
case "CodeInfo":
|
|
|
|
return null;
|
2022-11-01 22:01:28 +08:00
|
|
|
case "CodeText":
|
|
|
|
return t.children![0].text!;
|
|
|
|
case "Blockquote":
|
|
|
|
return {
|
|
|
|
name: "blockquote",
|
|
|
|
body: cleanTags(mapRender(t.children!)),
|
|
|
|
};
|
|
|
|
case "HardBreak":
|
|
|
|
return {
|
|
|
|
name: "br",
|
|
|
|
body: "",
|
|
|
|
};
|
|
|
|
// Basic styling
|
|
|
|
case "Emphasis":
|
|
|
|
return {
|
|
|
|
name: "em",
|
|
|
|
body: cleanTags(mapRender(t.children!)),
|
|
|
|
};
|
|
|
|
case "Highlight":
|
|
|
|
return {
|
|
|
|
name: "span",
|
|
|
|
attrs: {
|
|
|
|
class: "highlight",
|
|
|
|
},
|
|
|
|
body: cleanTags(mapRender(t.children!)),
|
|
|
|
};
|
2022-12-21 21:55:24 +08:00
|
|
|
case "Strikethrough":
|
|
|
|
return {
|
|
|
|
name: "del",
|
|
|
|
body: cleanTags(mapRender(t.children!)),
|
|
|
|
};
|
2022-11-01 22:01:28 +08:00
|
|
|
case "InlineCode":
|
|
|
|
return {
|
|
|
|
name: "tt",
|
|
|
|
body: cleanTags(mapRender(t.children!)),
|
|
|
|
};
|
|
|
|
case "BulletList":
|
|
|
|
return {
|
|
|
|
name: "ul",
|
2023-10-29 19:10:30 +08:00
|
|
|
body: cleanTags(mapRender(t.children!), true),
|
2022-11-01 22:01:28 +08:00
|
|
|
};
|
|
|
|
case "OrderedList":
|
|
|
|
return {
|
|
|
|
name: "ol",
|
2023-10-29 19:10:30 +08:00
|
|
|
body: cleanTags(mapRender(t.children!), true),
|
2022-11-01 22:01:28 +08:00
|
|
|
};
|
|
|
|
case "ListItem":
|
|
|
|
return {
|
|
|
|
name: "li",
|
2023-10-29 19:10:30 +08:00
|
|
|
body: cleanTags(mapRender(t.children!), true),
|
2022-11-01 22:01:28 +08:00
|
|
|
};
|
|
|
|
case "StrongEmphasis":
|
|
|
|
return {
|
|
|
|
name: "strong",
|
|
|
|
body: cleanTags(mapRender(t.children!)),
|
|
|
|
};
|
|
|
|
case "HorizontalRule":
|
|
|
|
return {
|
|
|
|
name: "hr",
|
|
|
|
body: "",
|
|
|
|
};
|
|
|
|
case "Link": {
|
2024-01-28 23:14:05 +08:00
|
|
|
const linkTextChildren = t.children!.slice(1, -4);
|
2022-11-02 00:03:42 +08:00
|
|
|
const urlNode = findNodeOfType(t, "URL");
|
|
|
|
if (!urlNode) {
|
|
|
|
return renderToText(t);
|
|
|
|
}
|
|
|
|
let url = urlNode.children![0].text!;
|
2024-05-28 02:33:41 +08:00
|
|
|
if (isLocalPath(url)) {
|
2024-03-13 15:26:40 +08:00
|
|
|
if (
|
|
|
|
options.attachmentUrlPrefix &&
|
|
|
|
!url.startsWith(options.attachmentUrlPrefix)
|
|
|
|
) {
|
|
|
|
url = `${options.attachmentUrlPrefix}${url}`;
|
|
|
|
}
|
2022-11-02 00:03:42 +08:00
|
|
|
}
|
2022-11-01 22:01:28 +08:00
|
|
|
return {
|
|
|
|
name: "a",
|
|
|
|
attrs: {
|
|
|
|
href: url,
|
|
|
|
},
|
2024-01-28 23:14:05 +08:00
|
|
|
body: cleanTags(mapRender(linkTextChildren)),
|
2022-11-01 22:01:28 +08:00
|
|
|
};
|
|
|
|
}
|
2024-06-13 17:47:03 +08:00
|
|
|
case "Autolink": {
|
|
|
|
const urlNode = findNodeOfType(t, "URL");
|
|
|
|
if (!urlNode) {
|
|
|
|
return renderToText(t);
|
|
|
|
}
|
|
|
|
let url = urlNode.children![0].text!;
|
|
|
|
if (isLocalPath(url)) {
|
|
|
|
if (
|
|
|
|
options.attachmentUrlPrefix &&
|
|
|
|
!url.startsWith(options.attachmentUrlPrefix)
|
|
|
|
) {
|
|
|
|
url = `${options.attachmentUrlPrefix}${url}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
name: "a",
|
|
|
|
attrs: {
|
|
|
|
href: url,
|
|
|
|
},
|
|
|
|
body: url,
|
|
|
|
};
|
|
|
|
}
|
2022-11-01 22:01:28 +08:00
|
|
|
case "Image": {
|
2024-05-28 02:33:41 +08:00
|
|
|
const altTextNode = findNodeOfType(t, "WikiLinkAlias") ||
|
|
|
|
t.children![1];
|
|
|
|
let altText = altTextNode && altTextNode.type !== "LinkMark"
|
|
|
|
? renderToText(altTextNode)
|
|
|
|
: "";
|
|
|
|
const dimReg = /\d*[^\|\s]*?[xX]\d*[^\|\s]*/.exec(altText);
|
|
|
|
let style = "";
|
|
|
|
if (dimReg) {
|
|
|
|
const [, width, widthUnit = "px", height, heightUnit = "px"] =
|
|
|
|
dimReg[0].match(/(\d*)(\S*?x?)??[xX](\d*)(.*)?/) ?? [];
|
|
|
|
if (width) {
|
|
|
|
style += `width: ${width}${widthUnit};`;
|
|
|
|
}
|
|
|
|
if (height) {
|
|
|
|
style += `height: ${height}${heightUnit};`;
|
|
|
|
}
|
|
|
|
altText = altText.replace(dimReg[0], "").replace("|", "");
|
|
|
|
}
|
|
|
|
|
|
|
|
const urlNode = findNodeOfType(t, "WikiLinkPage") ||
|
|
|
|
findNodeOfType(t, "URL");
|
2022-11-02 00:03:42 +08:00
|
|
|
if (!urlNode) {
|
|
|
|
return renderToText(t);
|
|
|
|
}
|
2024-05-28 02:33:41 +08:00
|
|
|
let url = renderToText(urlNode);
|
|
|
|
if (urlNode.type === "WikiLinkPage") {
|
|
|
|
url = "/" + url;
|
2022-11-01 22:01:28 +08:00
|
|
|
}
|
2024-05-28 02:33:41 +08:00
|
|
|
|
|
|
|
if (
|
|
|
|
isLocalPath(url) &&
|
|
|
|
options.attachmentUrlPrefix &&
|
|
|
|
!url.startsWith(options.attachmentUrlPrefix)
|
|
|
|
) {
|
|
|
|
url = `${options.attachmentUrlPrefix}${url}`;
|
|
|
|
}
|
|
|
|
|
2022-11-01 22:01:28 +08:00
|
|
|
return {
|
|
|
|
name: "img",
|
|
|
|
attrs: {
|
|
|
|
src: url,
|
|
|
|
alt: altText,
|
2024-05-28 02:33:41 +08:00
|
|
|
style: style,
|
2022-11-01 22:01:28 +08:00
|
|
|
},
|
|
|
|
body: "",
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Custom stuff
|
|
|
|
case "WikiLink": {
|
|
|
|
const ref = findNodeOfType(t, "WikiLinkPage")!.children![0].text!;
|
2023-08-18 02:37:55 +08:00
|
|
|
let linkText = ref.split("/").pop()!;
|
2022-11-27 18:20:43 +08:00
|
|
|
const aliasNode = findNodeOfType(t, "WikiLinkAlias");
|
|
|
|
if (aliasNode) {
|
|
|
|
linkText = aliasNode.children![0].text!;
|
|
|
|
}
|
2024-01-24 21:44:39 +08:00
|
|
|
const pageRef = parsePageRef(ref);
|
2022-11-01 22:01:28 +08:00
|
|
|
return {
|
|
|
|
name: "a",
|
|
|
|
attrs: {
|
2024-01-24 21:44:39 +08:00
|
|
|
href: `/${encodePageRef(pageRef)}`,
|
2023-11-12 19:08:09 +08:00
|
|
|
class: "wiki-link",
|
2023-10-03 20:16:33 +08:00
|
|
|
"data-ref": ref,
|
2022-11-01 22:01:28 +08:00
|
|
|
},
|
2022-11-27 18:20:43 +08:00
|
|
|
body: linkText,
|
2022-11-01 22:01:28 +08:00
|
|
|
};
|
|
|
|
}
|
|
|
|
case "NakedURL": {
|
|
|
|
const url = t.children![0].text!;
|
|
|
|
return {
|
|
|
|
name: "a",
|
|
|
|
attrs: {
|
|
|
|
href: url,
|
|
|
|
},
|
|
|
|
body: url,
|
|
|
|
};
|
|
|
|
}
|
2024-12-14 16:57:46 +08:00
|
|
|
case "Hashtag": {
|
|
|
|
const tagText: string = t.children![0].text!;
|
2022-11-01 22:01:28 +08:00
|
|
|
return {
|
2024-12-14 16:57:46 +08:00
|
|
|
name: "a",
|
2024-01-05 03:08:12 +08:00
|
|
|
attrs: {
|
2024-12-14 16:57:46 +08:00
|
|
|
class: "hashtag sb-hashtag",
|
2025-01-08 02:57:46 +08:00
|
|
|
"data-tag-name": extractHashtag(tagText),
|
|
|
|
href: `/${TagConstants.tagPrefix}${extractHashtag(tagText)}`,
|
2024-01-05 03:08:12 +08:00
|
|
|
},
|
2024-12-14 16:57:46 +08:00
|
|
|
body: tagText,
|
2022-11-01 22:01:28 +08:00
|
|
|
};
|
2024-12-14 16:57:46 +08:00
|
|
|
}
|
2023-10-03 20:16:33 +08:00
|
|
|
case "Task": {
|
|
|
|
let externalTaskRef = "";
|
|
|
|
collectNodesOfType(t, "WikiLinkPage").forEach((wikilink) => {
|
2024-01-24 18:58:33 +08:00
|
|
|
const pageRef = parsePageRef(wikilink.children![0].text!);
|
|
|
|
if (!externalTaskRef && (pageRef.pos !== undefined || pageRef.anchor)) {
|
|
|
|
externalTaskRef = wikilink.children![0].text!;
|
2023-10-03 20:16:33 +08:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2022-11-01 22:01:28 +08:00
|
|
|
return {
|
|
|
|
name: "span",
|
2023-10-03 20:16:33 +08:00
|
|
|
attrs: externalTaskRef
|
|
|
|
? {
|
|
|
|
"data-external-task-ref": externalTaskRef,
|
|
|
|
}
|
|
|
|
: {},
|
2022-11-01 22:01:28 +08:00
|
|
|
body: cleanTags(mapRender(t.children!)),
|
|
|
|
};
|
2023-10-03 20:16:33 +08:00
|
|
|
}
|
2023-09-01 22:57:29 +08:00
|
|
|
case "TaskState": {
|
|
|
|
// child[0] = marker, child[1] = state, child[2] = marker
|
|
|
|
const stateText = t.children![1].text!;
|
|
|
|
if ([" ", "x", "X"].includes(stateText)) {
|
|
|
|
return {
|
|
|
|
name: "input",
|
|
|
|
attrs: {
|
|
|
|
type: "checkbox",
|
2023-10-03 20:16:33 +08:00
|
|
|
checked: stateText !== " " ? "checked" : undefined,
|
|
|
|
"data-state": stateText,
|
2023-09-01 22:57:29 +08:00
|
|
|
},
|
|
|
|
body: "",
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
return {
|
|
|
|
name: "span",
|
|
|
|
attrs: {
|
|
|
|
class: "task-state",
|
|
|
|
},
|
|
|
|
body: stateText,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
2022-11-01 22:01:28 +08:00
|
|
|
case "NamedAnchor":
|
|
|
|
return {
|
|
|
|
name: "a",
|
|
|
|
attrs: {
|
|
|
|
name: t.children![0].text?.substring(1),
|
|
|
|
},
|
|
|
|
body: "",
|
|
|
|
};
|
|
|
|
case "CommandLink": {
|
2022-11-18 23:04:37 +08:00
|
|
|
// Child 0 is CommandLinkMark, child 1 is CommandLinkPage
|
2024-01-11 21:06:24 +08:00
|
|
|
const command = t.children![1].children![0].text!;
|
|
|
|
let commandText = command;
|
|
|
|
const aliasNode = findNodeOfType(t, "CommandLinkAlias");
|
2024-01-21 02:16:07 +08:00
|
|
|
const argsNode = findNodeOfType(t, "CommandLinkArgs");
|
|
|
|
let args: any = [];
|
|
|
|
|
|
|
|
if (argsNode) {
|
|
|
|
args = JSON.parse(`[${argsNode.children![0].text!}]`);
|
|
|
|
}
|
|
|
|
|
2024-01-11 21:06:24 +08:00
|
|
|
if (aliasNode) {
|
|
|
|
commandText = aliasNode.children![0].text!;
|
|
|
|
}
|
2022-11-01 22:01:28 +08:00
|
|
|
|
|
|
|
return {
|
|
|
|
name: "button",
|
|
|
|
attrs: {
|
2024-01-21 02:16:07 +08:00
|
|
|
"data-onclick": JSON.stringify(["command", command, args]),
|
2022-11-01 22:01:28 +08:00
|
|
|
},
|
|
|
|
body: commandText,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
case "DeadlineDate":
|
2023-11-12 19:08:09 +08:00
|
|
|
return {
|
|
|
|
name: "span",
|
|
|
|
attrs: {
|
|
|
|
class: "task-deadline",
|
|
|
|
},
|
|
|
|
body: renderToText(t),
|
|
|
|
};
|
2022-11-01 22:01:28 +08:00
|
|
|
|
|
|
|
// Tables
|
|
|
|
case "Table":
|
|
|
|
return {
|
|
|
|
name: "table",
|
|
|
|
body: cleanTags(mapRender(t.children!)),
|
|
|
|
};
|
|
|
|
case "TableHeader":
|
|
|
|
return {
|
|
|
|
name: "thead",
|
|
|
|
body: [
|
|
|
|
{
|
|
|
|
name: "tr",
|
2024-07-17 18:49:48 +08:00
|
|
|
body: cleanTags(mapRender(t.children!), true),
|
2022-11-01 22:01:28 +08:00
|
|
|
},
|
|
|
|
],
|
|
|
|
};
|
|
|
|
case "TableCell":
|
|
|
|
return {
|
|
|
|
name: "td",
|
|
|
|
body: cleanTags(mapRender(t.children!)),
|
|
|
|
};
|
2022-11-30 22:59:41 +08:00
|
|
|
case "TableRow": {
|
|
|
|
const children = t.children!;
|
|
|
|
const newChildren: ParseTree[] = [];
|
|
|
|
// Ensure there is TableCell in between every delimiter
|
|
|
|
let lookingForCell = false;
|
|
|
|
for (const child of children) {
|
|
|
|
if (child.type === "TableDelimiter" && lookingForCell) {
|
|
|
|
// We were looking for a cell, but didn't fine one: empty cell!
|
|
|
|
// Let's inject an empty one
|
|
|
|
newChildren.push({
|
|
|
|
type: "TableCell",
|
|
|
|
children: [],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (child.type === "TableDelimiter") {
|
|
|
|
lookingForCell = true;
|
|
|
|
}
|
|
|
|
if (child.type === "TableCell") {
|
|
|
|
lookingForCell = false;
|
|
|
|
}
|
|
|
|
newChildren.push(child);
|
|
|
|
}
|
2022-11-01 22:01:28 +08:00
|
|
|
return {
|
|
|
|
name: "tr",
|
2024-07-17 18:49:48 +08:00
|
|
|
body: cleanTags(mapRender(newChildren), true),
|
2022-11-01 22:01:28 +08:00
|
|
|
};
|
2022-11-30 22:59:41 +08:00
|
|
|
}
|
2023-07-25 01:54:31 +08:00
|
|
|
case "Attribute":
|
|
|
|
if (options.preserveAttributes) {
|
|
|
|
return {
|
|
|
|
name: "span",
|
|
|
|
attrs: {
|
|
|
|
class: "attribute",
|
|
|
|
},
|
|
|
|
body: renderToText(t),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return null;
|
2023-08-24 01:08:21 +08:00
|
|
|
case "Escape": {
|
|
|
|
return {
|
|
|
|
name: "span",
|
|
|
|
attrs: {
|
|
|
|
class: "escape",
|
|
|
|
},
|
|
|
|
body: t.children![0].text!.slice(1),
|
|
|
|
};
|
|
|
|
}
|
2023-11-28 17:06:53 +08:00
|
|
|
case "Entity":
|
|
|
|
return t.children![0].text!;
|
|
|
|
|
2024-01-05 03:08:12 +08:00
|
|
|
case "TemplateDirective": {
|
|
|
|
return {
|
|
|
|
name: "span",
|
|
|
|
attrs: {
|
|
|
|
class: "template-directive",
|
|
|
|
},
|
|
|
|
body: renderToText(t),
|
|
|
|
};
|
|
|
|
}
|
2024-06-07 14:21:16 +08:00
|
|
|
case "Superscript":
|
|
|
|
return {
|
|
|
|
name: "sup",
|
|
|
|
body: cleanTags(mapRender(t.children!)),
|
|
|
|
};
|
|
|
|
case "Subscript":
|
|
|
|
return {
|
|
|
|
name: "sub",
|
|
|
|
body: cleanTags(mapRender(t.children!)),
|
|
|
|
};
|
2024-10-05 21:37:36 +08:00
|
|
|
case "LuaDirective":
|
|
|
|
return {
|
|
|
|
name: "span",
|
|
|
|
attrs: {
|
|
|
|
class: "sb-lua-directive",
|
|
|
|
},
|
|
|
|
body: renderToText(t),
|
|
|
|
};
|
2024-01-05 03:08:12 +08:00
|
|
|
|
2022-11-01 22:01:28 +08:00
|
|
|
// Text
|
|
|
|
case undefined:
|
|
|
|
return t.text!;
|
|
|
|
default:
|
|
|
|
if (options.failOnUnknown) {
|
2023-11-28 17:06:53 +08:00
|
|
|
removeParentPointers(t);
|
2022-11-01 22:01:28 +08:00
|
|
|
console.error("Not handling", JSON.stringify(t, null, 2));
|
|
|
|
throw new Error(`Unknown markdown node type ${t.type}`);
|
|
|
|
} else {
|
|
|
|
// Falling back to rendering verbatim
|
2023-11-28 17:06:53 +08:00
|
|
|
removeParentPointers(t);
|
2022-11-01 22:01:28 +08:00
|
|
|
console.warn("Not handling", JSON.stringify(t, null, 2));
|
|
|
|
return renderToText(t);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function mapRender(children: ParseTree[]) {
|
|
|
|
return children.map((t) => posPreservingRender(t, options));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-26 20:04:32 +08:00
|
|
|
function traverseTag(
|
2023-02-23 22:33:51 +08:00
|
|
|
t: Tag,
|
2023-05-26 20:04:32 +08:00
|
|
|
fn: (t: Tag) => void,
|
|
|
|
) {
|
|
|
|
fn(t);
|
2023-02-23 22:33:51 +08:00
|
|
|
if (typeof t === "string") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (t.body) {
|
|
|
|
for (const child of t.body) {
|
2023-05-26 20:04:32 +08:00
|
|
|
traverseTag(child, fn);
|
2023-02-23 22:33:51 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-26 20:04:32 +08:00
|
|
|
export function renderMarkdownToHtml(
|
2022-11-01 22:01:28 +08:00
|
|
|
t: ParseTree,
|
|
|
|
options: MarkdownRenderOptions = {},
|
2024-07-13 19:56:00 +08:00
|
|
|
allPages: PageMeta[] = [],
|
2022-11-01 22:01:28 +08:00
|
|
|
) {
|
2024-05-19 17:05:48 +08:00
|
|
|
preprocess(t);
|
2022-11-01 22:01:28 +08:00
|
|
|
const htmlTree = posPreservingRender(t, options);
|
2024-07-13 19:56:00 +08:00
|
|
|
if (htmlTree) {
|
2023-05-26 20:04:32 +08:00
|
|
|
traverseTag(htmlTree, (t) => {
|
2023-02-23 22:33:51 +08:00
|
|
|
if (typeof t === "string") {
|
|
|
|
return;
|
|
|
|
}
|
2024-07-13 19:56:00 +08:00
|
|
|
if (t.name === "img" && options.translateUrls) {
|
2023-12-20 00:20:47 +08:00
|
|
|
t.attrs!.src = options.translateUrls!(t.attrs!.src!, "image");
|
2023-07-02 17:25:32 +08:00
|
|
|
}
|
|
|
|
|
2023-07-07 17:44:05 +08:00
|
|
|
if (t.name === "a" && t.attrs!.href) {
|
2024-07-13 19:56:00 +08:00
|
|
|
if (options.translateUrls) {
|
|
|
|
t.attrs!.href = options.translateUrls!(t.attrs!.href, "link");
|
|
|
|
}
|
|
|
|
if (t.attrs!["data-ref"]?.length) {
|
2024-07-24 13:03:27 +08:00
|
|
|
const pageRef = parsePageRef(t.attrs!["data-ref"]!);
|
|
|
|
const pageMeta = allPages.find((p) => pageRef.page === p.name);
|
2024-07-13 19:56:00 +08:00
|
|
|
if (pageMeta) {
|
2024-07-14 17:29:43 +08:00
|
|
|
t.body = [(pageMeta.pageDecoration?.prefix ?? "") + t.body];
|
2024-07-31 17:28:31 +08:00
|
|
|
if (pageMeta.pageDecoration?.cssClasses) {
|
|
|
|
t.attrs!.class += " sb-decorated-object " +
|
|
|
|
pageMeta.pageDecoration.cssClasses.join(" ").replaceAll(
|
|
|
|
/[^a-zA-Z0-9-_ ]/g,
|
|
|
|
"",
|
|
|
|
);
|
|
|
|
}
|
2024-07-13 19:56:00 +08:00
|
|
|
}
|
|
|
|
}
|
2024-05-28 02:33:41 +08:00
|
|
|
if (t.body.length === 0) {
|
|
|
|
t.body = [t.attrs!.href];
|
|
|
|
}
|
2023-02-23 22:33:51 +08:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2022-11-01 22:01:28 +08:00
|
|
|
return renderHtml(htmlTree);
|
|
|
|
}
|