main
js 138 lines 4.14 KB
Raw
1 import { renderSafeMarkdown } from "/js/safe-markdown.js";
2
3 const FOOTNOTE_DEF_RE = /^\[\^([^\]]+)\]:\s*(.*)$/;
4
5 export function renderEditorPreviewMarkdown(markdown = "", fullMarkdown = markdown) {
6 return renderSafeMarkdown(prepareFootnotes(markdown, fullMarkdown), {
7 allowDataImages: true,
8 allowLatex: true,
9 openExternalLinksInNewTab: true,
10 });
11 }
12
13 export function buildMarkdownPages(markdown = "", fallbackTitle = "Markdown") {
14 const source = String(markdown || "");
15 return [{
16 index: 0,
17 title: fallbackTitle,
18 level: 0,
19 anchor: slugifyHeading(fallbackTitle),
20 start: 0,
21 end: source.length,
22 markdown: source,
23 }];
24 }
25
26 export function slugifyHeading(text = "", used = new Map()) {
27 const base = String(text || "")
28 .toLowerCase()
29 .replace(/<[^>]+>/g, "")
30 .replace(/[`*_~[\]()]/g, "")
31 .replace(/&[a-z0-9#]+;/gi, "")
32 .replace(/[^a-z0-9\s-]/g, "")
33 .trim()
34 .replace(/\s+/g, "-")
35 .replace(/-+/g, "-") || "section";
36 const count = used.get(base) || 0;
37 used.set(base, count + 1);
38 return count ? `${base}-${count + 1}` : base;
39 }
40
41 export function resolveDocumentRelativePath(documentPath = "", target = "") {
42 const value = String(target || "").trim();
43 if (!value) return "";
44 if (value.startsWith("/")) return normalizePath(value);
45 const base = parentPath(documentPath);
46 return normalizePath(`${base}/${value}`);
47 }
48
49 export function splitHref(href = "") {
50 const value = String(href || "").trim();
51 const hashIndex = value.indexOf("#");
52 if (hashIndex < 0) return { path: value, fragment: "" };
53 return {
54 path: value.slice(0, hashIndex),
55 fragment: decodeURIComponent(value.slice(hashIndex + 1) || ""),
56 };
57 }
58
59 export function isExternalHref(href = "") {
60 const value = String(href || "").trim();
61 return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(value) || value.startsWith("//");
62 }
63
64 export function isMarkdownPath(path = "") {
65 return /\.md(?:own)?$/i.test(String(path || "").split(/[?#]/, 1)[0]);
66 }
67
68 function prepareFootnotes(markdown = "", fullMarkdown = markdown) {
69 const definitions = [];
70 const body = [];
71 for (const line of String(fullMarkdown || "").split("\n")) {
72 const match = line.match(FOOTNOTE_DEF_RE);
73 if (match) {
74 definitions.push({ id: match[1], text: match[2] });
75 }
76 }
77 for (const line of String(markdown || "").split("\n")) {
78 if (line.match(FOOTNOTE_DEF_RE)) {
79 continue;
80 }
81 body.push(line);
82 }
83 if (!definitions.length) return markdown;
84
85 const counts = new Map();
86 let prepared = body.join("\n").replace(/\[\^([^\]]+)\]/g, (_all, id) => {
87 const number = definitions.findIndex((item) => item.id === id) + 1;
88 if (number <= 0) return `[^${id}]`;
89 const count = (counts.get(id) || 0) + 1;
90 counts.set(id, count);
91 const safeId = footnoteId(id);
92 return `<sup class="editor-footnote-ref"><a id="fnref-${safeId}-${count}" href="#fn-${safeId}">${number}</a></sup>`;
93 });
94
95 prepared += "\n\n<section class=\"editor-footnotes\" aria-label=\"Footnotes\">\n<ol>\n";
96 for (const definition of definitions) {
97 const safeId = footnoteId(definition.id);
98 prepared += `<li id="fn-${safeId}">${escapeHtml(definition.text)} <a class="editor-footnote-backref" href="#fnref-${safeId}-1">Back</a></li>\n`;
99 }
100 prepared += "</ol>\n</section>";
101 return prepared;
102 }
103
104 function footnoteId(id = "") {
105 return String(id || "")
106 .toLowerCase()
107 .replace(/[^a-z0-9_-]+/g, "-")
108 .replace(/^-+|-+$/g, "") || "note";
109 }
110
111 function parentPath(path = "") {
112 const normalized = String(path || "").split(/[?#]/, 1)[0].replace(/\/+$/, "");
113 const index = normalized.lastIndexOf("/");
114 if (index <= 0) return "/";
115 return normalized.slice(0, index);
116 }
117
118 function normalizePath(path = "") {
119 const absolute = String(path || "").startsWith("/");
120 const parts = [];
121 for (const part of String(path || "").split("/")) {
122 if (!part || part === ".") continue;
123 if (part === "..") {
124 parts.pop();
125 continue;
126 }
127 parts.push(part);
128 }
129 return `${absolute ? "/" : ""}${parts.join("/")}`;
130 }
131
132 function escapeHtml(value = "") {
133 return String(value || "")
134 .replace(/&/g, "&amp;")
135 .replace(/</g, "&lt;")
136 .replace(/>/g, "&gt;")
137 .replace(/"/g, "&quot;");
138 }