298 lines
9.8 KiB
JavaScript
298 lines
9.8 KiB
JavaScript
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 = {// {{{
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": ''',
|
|
}// }}}
|
|
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)
|
|
}// }}}
|
|
}
|