diff --git a/src/ui/component/core/TextEditor.ts b/src/ui/component/core/TextEditor.ts index d0d0f2a..b140626 100644 --- a/src/ui/component/core/TextEditor.ts +++ b/src/ui/component/core/TextEditor.ts @@ -1,6 +1,4 @@ import quilt from 'lang/en-nz' -import type { StateInline, Token } from 'markdown-it' -import MarkdownIt from 'markdown-it' import Session from 'model/Session' import { baseKeymap, lift, setBlockType, toggleMark, wrapIn } from 'prosemirror-commands' import { dropCursor } from 'prosemirror-dropcursor' @@ -39,7 +37,8 @@ import Objects from 'utility/Objects' import type { UnsubscribeState } from 'utility/State' import State from 'utility/State' import Store from 'utility/Store' -import MarkdownItHTML from 'utility/string/MarkdownItHTML' +import Markdown from 'utility/string/Markdown' +import type MarkdownItHTML from 'utility/string/MarkdownItHTML' import type Strings from 'utility/string/Strings' import Time from 'utility/Time' import type { PartialRecord } from 'utility/Type' @@ -419,6 +418,8 @@ types<[Marks, Nodes]>() //////////////////////////////////// //#region Markdown +const markdown = Markdown.clone() + const REGEX_ATTRIBUTE = (() => { const attr_name = '[a-zA-Z_:][a-zA-Z0-9:._-]*' const unquoted = '[^"\'=<>`\\x00-\\x20]+' @@ -431,134 +432,6 @@ const REGEX_ATTRIBUTE = (() => { const REGEX_CSS_PROPERTY = /^[-a-zA-Z_][a-zA-Z0-9_-]*$/ -const markdown = new MarkdownIt('commonmark', { html: true, breaks: true }) -MarkdownItHTML.use(markdown, MarkdownItHTML.Options() - .disallowTags('img', 'figure', 'figcaption', 'map', 'area')) -markdown.inline.ruler.enable('strikethrough') -markdown.inline.ruler2.enable('strikethrough') - -//////////////////////////////////// -//#region Underline Parse -// Based on https://github.com/markdown-it/markdown-it/blob/0fe7ccb4b7f30236fb05f623be6924961d296d3d/lib/rules_inline/strikethrough.mjs - -markdown.inline.ruler.before('emphasis', 'underline', function underline_tokenize (state, silent) { - const start = state.pos - const marker = state.src.charCodeAt(start) - - if (silent || marker !== 0x5F/* _ */) - return false - - const scanned = state.scanDelims(state.pos, true) - let len = scanned.length - if (len < 2) - return false - - const ch = String.fromCharCode(marker) - - let token: Token - if (len % 2) { - token = state.push('text', '', 0) - token.content = ch - len-- - } - - for (let i = 0; i < len; i += 2) { - token = state.push('text', '', 0) - token.content = ch + ch - - state.delimiters.push({ - marker, - length: 0, // disable "rule of 3" length checks meant for emphasis - token: state.tokens.length - 1, - end: -1, - open: scanned.can_open, - close: scanned.can_close, - }) - } - - state.pos += scanned.length - return true -}) - -markdown.inline.ruler2.before('emphasis', 'underline', function underline_postProcess (state) { - const tokens_meta = state.tokens_meta - const max = state.tokens_meta.length - - postProcess(state, state.delimiters) - - for (let curr = 0; curr < max; curr++) { - const delimiters = tokens_meta[curr]?.delimiters - if (delimiters) - postProcess(state, delimiters) - } - - state.delimiters = state.delimiters.filter(delim => delim.marker !== 0x5F/* _ */) - return true - - function postProcess (state: StateInline, delimiters: StateInline.Delimiter[]) { - let token: Token - const loneMarkers: number[] = [] - const max = delimiters.length - - for (let i = 0; i < max; i++) { - const startDelim = delimiters[i] - - if (startDelim.marker !== 0x5F/* _ */) - continue - - if (startDelim.end === -1) - continue - - const endDelim = delimiters[startDelim.end] - - token = state.tokens[startDelim.token] - token.type = 'u_open' - token.tag = 'u' - token.nesting = 1 - token.markup = '__' - token.content = '' - - token = state.tokens[endDelim.token] - token.type = 'u_close' - token.tag = 'u' - token.nesting = -1 - token.markup = '__' - token.content = '' - - if (state.tokens[endDelim.token - 1].type === 'text' - && state.tokens[endDelim.token - 1].content === '_') { - loneMarkers.push(endDelim.token - 1) - } - } - - // If a marker sequence has an odd number of characters, it's splitted - // like this: `_____` -> `_` + `__` + `__`, leaving one marker at the - // start of the sequence. - // - // So, we have to move all those markers after subsequent u_close tags. - // - while (loneMarkers.length) { - const i = loneMarkers.pop() ?? 0 - let j = i + 1 - - while (j < state.tokens.length && state.tokens[j].type === 'u_close') { - j++ - } - - j-- - - if (i !== j) { - token = state.tokens[j] - state.tokens[j] = state.tokens[i] - state.tokens[i] = token - } - } - } -}) - -//#endregion -//////////////////////////////////// - interface MarkdownHTMLTokenRemapSpec { getAttrs: (token: MarkdownItHTML.Token) => Attrs | true | undefined } diff --git a/src/ui/utility/MarkdownContent.ts b/src/ui/utility/MarkdownContent.ts index 330083e..db292b2 100644 --- a/src/ui/utility/MarkdownContent.ts +++ b/src/ui/utility/MarkdownContent.ts @@ -1,5 +1,5 @@ import Component from 'ui/Component' -import Strings from 'utility/string/Strings' +import Markdown from 'utility/string/Markdown' interface MarkdownContentExtensions { setMarkdownContent (markdown: string): this @@ -16,7 +16,7 @@ const handlers: MarkdownContentHandler[] = [] Component.extend(component => component.extend(component => ({ setMarkdownContent (markdown) { component.classes.add('markdown') - component.element.innerHTML = Strings.markdown.render(markdown) + component.element.innerHTML = Markdown.render(markdown) for (const node of [...component.element.querySelectorAll('*')]) for (const handler of handlers) handler(node as HTMLElement) diff --git a/src/utility/string/Markdown.ts b/src/utility/string/Markdown.ts new file mode 100644 index 0000000..ec9a2ba --- /dev/null +++ b/src/utility/string/Markdown.ts @@ -0,0 +1,143 @@ +import type { StateInline, Token } from 'markdown-it' +import MarkdownIt from 'markdown-it' +import MarkdownItHTML from 'utility/string/MarkdownItHTML' + +export default Object.assign( + createMarkdownInstance(), + { + clone: createMarkdownInstance, + } +) + +function createMarkdownInstance () { + const Markdown = new MarkdownIt('commonmark', { html: true, breaks: true }) + + MarkdownItHTML.use(Markdown, MarkdownItHTML.Options() + .disallowTags('img', 'figure', 'figcaption', 'map', 'area')) + Markdown.inline.ruler.enable('strikethrough') + Markdown.inline.ruler2.enable('strikethrough') + + //////////////////////////////////// + //#region Underline Parse + // Based on https://github.com/Markdown-it/Markdown-it/blob/0fe7ccb4b7f30236fb05f623be6924961d296d3d/lib/rules_inline/strikethrough.mjs + + Markdown.inline.ruler.before('emphasis', 'underline', function underline_tokenize (state, silent) { + const start = state.pos + const marker = state.src.charCodeAt(start) + + if (silent || marker !== 0x5F/* _ */) + return false + + const scanned = state.scanDelims(state.pos, true) + let len = scanned.length + if (len < 2) + return false + + const ch = String.fromCharCode(marker) + + let token: Token + if (len % 2) { + token = state.push('text', '', 0) + token.content = ch + len-- + } + + for (let i = 0; i < len; i += 2) { + token = state.push('text', '', 0) + token.content = ch + ch + + state.delimiters.push({ + marker, + length: 0, // disable "rule of 3" length checks meant for emphasis + token: state.tokens.length - 1, + end: -1, + open: scanned.can_open, + close: scanned.can_close, + }) + } + + state.pos += scanned.length + return true + }) + + Markdown.inline.ruler2.before('emphasis', 'underline', function underline_postProcess (state) { + const tokens_meta = state.tokens_meta + const max = state.tokens_meta.length + + postProcess(state, state.delimiters) + + for (let curr = 0; curr < max; curr++) { + const delimiters = tokens_meta[curr]?.delimiters + if (delimiters) + postProcess(state, delimiters) + } + + state.delimiters = state.delimiters.filter(delim => delim.marker !== 0x5F/* _ */) + return true + + function postProcess (state: StateInline, delimiters: StateInline.Delimiter[]) { + let token: Token + const loneMarkers: number[] = [] + const max = delimiters.length + + for (let i = 0; i < max; i++) { + const startDelim = delimiters[i] + + if (startDelim.marker !== 0x5F/* _ */) + continue + + if (startDelim.end === -1) + continue + + const endDelim = delimiters[startDelim.end] + + token = state.tokens[startDelim.token] + token.type = 'u_open' + token.tag = 'u' + token.nesting = 1 + token.markup = '__' + token.content = '' + + token = state.tokens[endDelim.token] + token.type = 'u_close' + token.tag = 'u' + token.nesting = -1 + token.markup = '__' + token.content = '' + + if (state.tokens[endDelim.token - 1].type === 'text' + && state.tokens[endDelim.token - 1].content === '_') { + loneMarkers.push(endDelim.token - 1) + } + } + + // If a marker sequence has an odd number of characters, it's splitted + // like this: `_____` -> `_` + `__` + `__`, leaving one marker at the + // start of the sequence. + // + // So, we have to move all those markers after subsequent u_close tags. + // + while (loneMarkers.length) { + const i = loneMarkers.pop() ?? 0 + let j = i + 1 + + while (j < state.tokens.length && state.tokens[j].type === 'u_close') { + j++ + } + + j-- + + if (i !== j) { + token = state.tokens[j] + state.tokens[j] = state.tokens[i] + state.tokens[i] = token + } + } + } + }) + + //#endregion + //////////////////////////////////// + + return Markdown +} diff --git a/src/utility/string/Strings.ts b/src/utility/string/Strings.ts index 2e2f000..1fc09e3 100644 --- a/src/utility/string/Strings.ts +++ b/src/utility/string/Strings.ts @@ -1,5 +1,3 @@ -import MarkdownIt from 'markdown-it' -import MarkdownItHTML from 'utility/string/MarkdownItHTML' namespace Strings { export type Replace = @@ -263,10 +261,6 @@ namespace Strings { .map(word => word[0].toUpperCase() + word.slice(1)) .join('') } - - export const markdown = new MarkdownIt('commonmark', { html: true, breaks: true }) - MarkdownItHTML.use(markdown, MarkdownItHTML.Options() - .disallowTags('img', 'figure', 'figcaption', 'map', 'area')) } export default Strings