File size: 4,240 Bytes
0e9afe0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import { renderSafeMarkdown } from "/js/safe-markdown.js";

const FOOTNOTE_DEF_RE = /^\[\^([^\]]+)\]:\s*(.*)$/;

export function renderEditorPreviewMarkdown(markdown = "", fullMarkdown = markdown) {
  return renderSafeMarkdown(prepareFootnotes(markdown, fullMarkdown), {
    allowDataImages: true,
    allowLatex: true,
    openExternalLinksInNewTab: true,
  });
}

export function buildMarkdownPages(markdown = "", fallbackTitle = "Markdown") {
  const source = String(markdown || "");
  return [{
    index: 0,
    title: fallbackTitle,
    level: 0,
    anchor: slugifyHeading(fallbackTitle),
    start: 0,
    end: source.length,
    markdown: source,
  }];
}

export function slugifyHeading(text = "", used = new Map()) {
  const base = String(text || "")
    .toLowerCase()
    .replace(/<[^>]+>/g, "")
    .replace(/[`*_~[\]()]/g, "")
    .replace(/&[a-z0-9#]+;/gi, "")
    .replace(/[^a-z0-9\s-]/g, "")
    .trim()
    .replace(/\s+/g, "-")
    .replace(/-+/g, "-") || "section";
  const count = used.get(base) || 0;
  used.set(base, count + 1);
  return count ? `${base}-${count + 1}` : base;
}

export function resolveDocumentRelativePath(documentPath = "", target = "") {
  const value = String(target || "").trim();
  if (!value) return "";
  if (value.startsWith("/")) return normalizePath(value);
  const base = parentPath(documentPath);
  return normalizePath(`${base}/${value}`);
}

export function splitHref(href = "") {
  const value = String(href || "").trim();
  const hashIndex = value.indexOf("#");
  if (hashIndex < 0) return { path: value, fragment: "" };
  return {
    path: value.slice(0, hashIndex),
    fragment: decodeURIComponent(value.slice(hashIndex + 1) || ""),
  };
}

export function isExternalHref(href = "") {
  const value = String(href || "").trim();
  return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(value) || value.startsWith("//");
}

export function isMarkdownPath(path = "") {
  return /\.md(?:own)?$/i.test(String(path || "").split(/[?#]/, 1)[0]);
}

function prepareFootnotes(markdown = "", fullMarkdown = markdown) {
  const definitions = [];
  const body = [];
  for (const line of String(fullMarkdown || "").split("\n")) {
    const match = line.match(FOOTNOTE_DEF_RE);
    if (match) {
      definitions.push({ id: match[1], text: match[2] });
    }
  }
  for (const line of String(markdown || "").split("\n")) {
    if (line.match(FOOTNOTE_DEF_RE)) {
      continue;
    }
    body.push(line);
  }
  if (!definitions.length) return markdown;

  const counts = new Map();
  let prepared = body.join("\n").replace(/\[\^([^\]]+)\]/g, (_all, id) => {
    const number = definitions.findIndex((item) => item.id === id) + 1;
    if (number <= 0) return `[^${id}]`;
    const count = (counts.get(id) || 0) + 1;
    counts.set(id, count);
    const safeId = footnoteId(id);
    return `<sup class="editor-footnote-ref"><a id="fnref-${safeId}-${count}" href="#fn-${safeId}">${number}</a></sup>`;
  });

  prepared += "\n\n<section class=\"editor-footnotes\" aria-label=\"Footnotes\">\n<ol>\n";
  for (const definition of definitions) {
    const safeId = footnoteId(definition.id);
    prepared += `<li id="fn-${safeId}">${escapeHtml(definition.text)} <a class="editor-footnote-backref" href="#fnref-${safeId}-1">Back</a></li>\n`;
  }
  prepared += "</ol>\n</section>";
  return prepared;
}

function footnoteId(id = "") {
  return String(id || "")
    .toLowerCase()
    .replace(/[^a-z0-9_-]+/g, "-")
    .replace(/^-+|-+$/g, "") || "note";
}

function parentPath(path = "") {
  const normalized = String(path || "").split(/[?#]/, 1)[0].replace(/\/+$/, "");
  const index = normalized.lastIndexOf("/");
  if (index <= 0) return "/";
  return normalized.slice(0, index);
}

function normalizePath(path = "") {
  const absolute = String(path || "").startsWith("/");
  const parts = [];
  for (const part of String(path || "").split("/")) {
    if (!part || part === ".") continue;
    if (part === "..") {
      parts.pop();
      continue;
    }
    parts.push(part);
  }
  return `${absolute ? "/" : ""}${parts.join("/")}`;
}

function escapeHtml(value = "") {
  return String(value || "")
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;");
}