mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-23 08:58:11 -07:00
211 lines
4.9 KiB
JavaScript
211 lines
4.9 KiB
JavaScript
/**
|
|
* @typedef {Object} InlinePattern
|
|
* @property {RegExp} pattern
|
|
* @property {(match: RegExpExecArray) => HTMLElement} create
|
|
*/
|
|
|
|
/** @type {InlinePattern[]} */
|
|
const INLINE_PATTERNS = [
|
|
{
|
|
pattern: /`([^`\n]+)`/,
|
|
create(match) {
|
|
const code = document.createElement("code");
|
|
code.textContent = match[1];
|
|
return code;
|
|
},
|
|
},
|
|
{
|
|
pattern: /\[([^\]\n]+)\]\(((?:https?:\/\/|\/|#)[^\s)]+)\)/,
|
|
create(match) {
|
|
const anchor = document.createElement("a");
|
|
anchor.href = match[2];
|
|
appendInline(anchor, match[1]);
|
|
|
|
if (anchor.origin !== window.location.origin) {
|
|
anchor.target = "_blank";
|
|
anchor.rel = "noreferrer";
|
|
}
|
|
|
|
return anchor;
|
|
},
|
|
},
|
|
{
|
|
pattern: /\*\*([^*\n]+)\*\*/,
|
|
create(match) {
|
|
const strong = document.createElement("strong");
|
|
appendInline(strong, match[1]);
|
|
return strong;
|
|
},
|
|
},
|
|
{
|
|
pattern: /__([^_\n]+)__/,
|
|
create(match) {
|
|
const strong = document.createElement("strong");
|
|
appendInline(strong, match[1]);
|
|
return strong;
|
|
},
|
|
},
|
|
{
|
|
pattern: /\*([^*\n]+)\*/,
|
|
create(match) {
|
|
const emphasis = document.createElement("em");
|
|
appendInline(emphasis, match[1]);
|
|
return emphasis;
|
|
},
|
|
},
|
|
{
|
|
pattern: /_([^_\n]+)_/,
|
|
create(match) {
|
|
const emphasis = document.createElement("em");
|
|
appendInline(emphasis, match[1]);
|
|
return emphasis;
|
|
},
|
|
},
|
|
];
|
|
|
|
/**
|
|
* @param {HTMLElement} parent
|
|
* @param {string} source
|
|
*/
|
|
function appendInline(parent, source) {
|
|
let remaining = source;
|
|
|
|
while (remaining) {
|
|
/** @type {{ entry: InlinePattern, match: RegExpExecArray } | undefined} */
|
|
let token;
|
|
|
|
for (const entry of INLINE_PATTERNS) {
|
|
const match = entry.pattern.exec(remaining);
|
|
if (match && (!token || match.index < token.match.index)) {
|
|
token = { entry, match };
|
|
}
|
|
}
|
|
|
|
if (!token) {
|
|
parent.append(remaining);
|
|
return;
|
|
}
|
|
|
|
const { entry, match } = token;
|
|
parent.append(remaining.slice(0, match.index), entry.create(match));
|
|
remaining = remaining.slice(match.index + match[0].length);
|
|
}
|
|
}
|
|
|
|
/** @param {string} line */
|
|
function isBlockStart(line) {
|
|
return (
|
|
/^```/.test(line) ||
|
|
/^#{1,4}\s/.test(line) ||
|
|
/^>\s?/.test(line) ||
|
|
/^(?:[-+*]|\d+\.)\s+/.test(line)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {HTMLElement} parent
|
|
* @param {string} text
|
|
*/
|
|
function appendTextBlock(parent, text) {
|
|
appendInline(parent, text.trim());
|
|
}
|
|
|
|
/**
|
|
* @param {HTMLElement} target
|
|
* @param {string} source
|
|
*/
|
|
export function renderMarkdown(target, source) {
|
|
const lines = source.replaceAll("\r\n", "\n").split("\n");
|
|
const fragment = document.createDocumentFragment();
|
|
let index = 0;
|
|
|
|
while (index < lines.length) {
|
|
const line = lines[index];
|
|
|
|
if (!line.trim()) {
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
const fence = /^```([^\s`]*)\s*$/.exec(line);
|
|
if (fence) {
|
|
const pre = document.createElement("pre");
|
|
const code = document.createElement("code");
|
|
const content = [];
|
|
index += 1;
|
|
|
|
while (index < lines.length && !/^```\s*$/.test(lines[index])) {
|
|
content.push(lines[index]);
|
|
index += 1;
|
|
}
|
|
|
|
code.textContent = content.join("\n");
|
|
if (fence[1]) code.dataset.language = fence[1];
|
|
pre.append(code);
|
|
fragment.append(pre);
|
|
index += index < lines.length ? 1 : 0;
|
|
continue;
|
|
}
|
|
|
|
const heading = /^(#{1,4})\s+(.+)$/.exec(line);
|
|
if (heading) {
|
|
const element = document.createElement(`h${heading[1].length}`);
|
|
appendTextBlock(element, heading[2]);
|
|
fragment.append(element);
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (/^>\s?/.test(line)) {
|
|
const quote = document.createElement("blockquote");
|
|
const content = [];
|
|
|
|
while (index < lines.length && /^>\s?/.test(lines[index])) {
|
|
content.push(lines[index].replace(/^>\s?/, ""));
|
|
index += 1;
|
|
}
|
|
|
|
appendTextBlock(quote, content.join(" "));
|
|
fragment.append(quote);
|
|
continue;
|
|
}
|
|
|
|
const listItem = /^(?:([-+*])|(\d+)\.)\s+(.+)$/.exec(line);
|
|
if (listItem) {
|
|
const ordered = Boolean(listItem[2]);
|
|
const list = document.createElement(ordered ? "ol" : "ul");
|
|
|
|
while (index < lines.length) {
|
|
const itemMatch = /^(?:([-+*])|(\d+)\.)\s+(.+)$/.exec(lines[index]);
|
|
if (!itemMatch || Boolean(itemMatch[2]) !== ordered) break;
|
|
|
|
const item = document.createElement("li");
|
|
appendTextBlock(item, itemMatch[3]);
|
|
list.append(item);
|
|
index += 1;
|
|
}
|
|
|
|
fragment.append(list);
|
|
continue;
|
|
}
|
|
|
|
const paragraph = document.createElement("p");
|
|
const content = [line];
|
|
index += 1;
|
|
|
|
while (
|
|
index < lines.length &&
|
|
lines[index].trim() &&
|
|
!isBlockStart(lines[index])
|
|
) {
|
|
content.push(lines[index]);
|
|
index += 1;
|
|
}
|
|
|
|
appendTextBlock(paragraph, content.join(" "));
|
|
fragment.append(paragraph);
|
|
}
|
|
|
|
target.replaceChildren(fragment);
|
|
}
|