Added markdown rendering

This commit is contained in:
Magnus Åhall 2026-05-15 08:22:43 +02:00
parent 26ca510785
commit 5a0340c226
172 changed files with 12198 additions and 8338 deletions

View file

@ -0,0 +1,298 @@
import { Marked } from './lib/node_modules/marked/lib/marked.esm.js'
import markedTokenPosition from './lib/node_modules/marked-token-position/lib/index.esm.js'
const other = {// {{{
codeRemoveIndent: /^(?: {1,4}| {0,3}\t)/gm,
outputLinkReplace: /\\([\[\]])/g,
indentCodeCompensation: /^(\s+)(?:```)/,
beginningSpace: /^\s+/,
endingHash: /#$/,
startingSpaceChar: /^ /,
endingSpaceChar: / $/,
nonSpaceChar: /[^ ]/,
newLineCharGlobal: /\n/g,
tabCharGlobal: /\t/g,
multipleSpaceGlobal: /\s+/g,
blankLine: /^[ \t]*$/,
doubleBlankLine: /\n[ \t]*\n[ \t]*$/,
blockquoteStart: /^ {0,3}>/,
blockquoteSetextReplace: /\n {0,3}((?:=+|-+) *)(?=\n|$)/g,
blockquoteSetextReplace2: /^ {0,3}>[ \t]?/gm,
listReplaceNesting: /^ {1,4}(?=( {4})*[^ ])/g,
listIsTask: /^\[[ xX]\] +\S/,
listReplaceTask: /^\[[ xX]\] +/,
listTaskCheckbox: /\[[ xX]\]/,
anyLine: /\n.*\n/,
hrefBrackets: /^<(.*)>$/,
tableDelimiter: /[:|]/,
tableAlignChars: /^\||\| *$/g,
tableRowBlankLine: /\n[ \t]*$/,
tableAlignRight: /^ *-+: *$/,
tableAlignCenter: /^ *:-+: *$/,
tableAlignLeft: /^ *:-+ *$/,
startATag: /^<a /i,
endATag: /^<\/a>/i,
startPreScriptTag: /^<(pre|code|kbd|script)(\s|>)/i,
endPreScriptTag: /^<\/(pre|code|kbd|script)(\s|>)/i,
startAngleBracket: /^</,
endAngleBracket: />$/,
pedanticHrefTitle: /^([^'"]*[^\s])\s+(['"])(.*)\2/,
unicodeAlphaNumeric: /[\p{L}\p{N}]/u,
escapeTest: /[&<>"']/,
escapeReplace: /[&<>"']/g,
escapeTestNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,
escapeReplaceNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,
caret: /(^|[^\[])\^/g,
percentDecode: /%25/g,
findPipe: /\|/g,
splitPipe: / \|/,
slashPipe: /\\\|/g,
carriageReturn: /\r\n|\r/g,
spaceLine: /^ +$/gm,
notSpaceStart: /^\S*/,
endingNewline: /\n$/,
listItemRegex: (bull) => new RegExp(`^( {0,3}${bull})((?:[\t ][^\\n]*)?(?:\\n|$))`),
nextBulletRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),
hrRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),
fencesBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:\`\`\`|~~~)`),
headingBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}#`),
htmlBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}<(?:[a-z].*>|!--)`, 'i'),
blockquoteBeginRegex: (indent) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}>`),
}// }}}
const escapeReplacements = {// {{{
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
}// }}}
const getEscapeReplacement = (ch) => escapeReplacements[ch]
function cleanUrl(href) {// {{{
try {
href = encodeURI(href).replace(other.percentDecode, '%')
} catch {
return null
}
return href
}// }}}
function escapeHtmlEntities(html, encode) {// {{{
if (encode) {
if (other.escapeTest.test(html)) {
return html.replace(other.escapeReplace, getEscapeReplacement)
}
} else {
if (other.escapeTestNoEncode.test(html)) {
return html.replace(other.escapeReplaceNoEncode, getEscapeReplacement)
}
}
return html
}// }}}
export class MarkedPosition {
constructor() {// {{{
window.setpos = (event) => {
event.stopPropagation()
event.preventDefault()
_mbus.dispatch('MARKDOWN_EDIT', {
position: {
start: event.target.dataset.offsetStart,
end: event.target.dataset.offsetEnd,
}
})
}
this.marked = new Marked()
this.marked.use(markedTokenPosition())
this.marked.use({
renderer: {
heading(token) {
const content = this.parser.parseInline(token.tokens)
return `<h${token.depth} onclick="setpos(event)" onclick="setpos(this)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${content}</h${token.depth}>\n`
},
paragraph(token) {
const content = this.parser.parseInline(token.tokens)
return `<p onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${content}</p>\n`
},
list(token) {
const ordered = token.ordered
const start = token.start
let body = ''
for (let j = 0; j < token.items.length; j++) {
const item = token.items[j]
body += this.listitem(item)
}
const type = ordered ? 'ol' : 'ul'
const startAttr = (ordered && start !== 1) ? (' start="' + start + '"') : ''
return '<' + type + startAttr + '>\n' + body + '</' + type + '>\n'
},
listitem(token) {
return `<li onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parse(token.tokens)}</li>\n`
},
code(token) {
const langString = (token.lang || '').match(other.notSpaceStart)?.[0]
const code = token.text.replace(other.endingNewline, '') + '\n'
if (!langString) {
return `<pre onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"><code>`
+ (token.escaped ? code : escapeHtmlEntities(code, true))
+ '</code></pre>\n'
}
return `<pre onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"><code class="language-`
+ escapeHtmlEntities(langString)
+ '">'
+ (token.escaped ? code : escapeHtmlEntities(code, true))
+ '</code></pre>\n'
},
blockquote(token) {
const body = this.parser.parse(token.tokens)
return `<blockquote onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">\n${body}</blockquote>\n`
},
html(token) {
return token.text
},
def(token) {
return ''
},
hr(token) {
return `<hr onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">\n`
},
checkbox(token) {
return `<input onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"`
+ (token.checked ? 'checked="" ' : '')
+ 'disabled="" type="checkbox"> '
},
table(token) {
let header = '';
// header
let cell = '';
for (let j = 0; j < token.header.length; j++) {
cell += this.tablecell(token.header[j]);
}
header += this.tablerow({ text: cell });
let body = '';
for (let j = 0; j < token.rows.length; j++) {
const row = token.rows[j];
cell = '';
for (let k = 0; k < row.length; k++) {
cell += this.tablecell(row[k]);
}
body += this.tablerow({ text: cell });
}
if (body) body = `<tbody>${body}</tbody>`;
return '<table>\n'
+ '<thead>\n'
+ header
+ '</thead>\n'
+ body
+ '</table>\n'
},
tablerow(token) {
return `<tr>\n${token.text}</tr>\n`
},
tablecell(token) {
let ofs = ''
if (token.tokens.length > 0) {
const start = token.tokens[0].position.start.offset
const end = token.tokens[0].position.end.offset
ofs = `onclick="setpos(event)" data-offset-start="${start}" data-offset-end="${end}"`
}
const content = this.parser.parseInline(token.tokens);
const type = token.header ? 'th' : 'td';
const tag = token.align
? `<${type} ${ofs} align="${token.align}">`
: `<${type} ${ofs}>`;
return tag + content + `</${type}>\n`
},
strong(token) {
return `<strong onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</strong>`
},
em(token) {
return `<em onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</em>`
},
codespan(token) {
return `<code onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${escapeHtmlEntities(token.text, true)}</code>`
},
br(token) {
return `<br onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">`
},
del(token) {
return `<del onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</del>`
},
link(token) {
const text = this.parser.parseInline(token.tokens)
const cleanHref = cleanUrl(token.href)
if (cleanHref === null) {
return text
}
token.href = cleanHref
let out = '<a onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}" href="' + token.href + '"'
if (token.title) {
out += ' title="' + (escapeHtmlEntities(token.title)) + '"'
}
out += '>' + text + '</a>'
return out
},
image(token) {
if (token.tokens) {
token.text = this.parser.parseInline(token.tokens, this.parser.textRenderer)
}
const cleanHref = cleanUrl(token.href)
if (cleanHref === null) {
return escapeHtmlEntities(token.text)
}
token.href = cleanHref
let out = `<img onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}" src="${token.href}" alt="${escapeHtmlEntities(token.text)}"`
if (token.title) {
out += ` title="${escapeHtmlEntities(token.title)}"`
}
out += '>'
return out
},
text(token) {
return 'tokens' in token && token.tokens
? this.parser.parseInline(token.tokens)
: ('escaped' in token && token.escaped ? token.text : escapeHtmlEntities(token.text))
},
}
})
}// }}}
parse(text) {// {{{
return this.marked.parse(text)
}// }}}
}