Skip to content

Commit

Permalink
Use the same markdown-it instance for text editor and display
Browse files Browse the repository at this point in the history
Fixes configuration differences like underline parsing
  • Loading branch information
ChiriVulpes committed Jan 10, 2025
1 parent 76563ef commit 53547d0
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 139 deletions.
135 changes: 4 additions & 131 deletions src/ui/component/core/TextEditor.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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]+'
Expand All @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions src/ui/utility/MarkdownContent.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,7 +16,7 @@ const handlers: MarkdownContentHandler[] = []
Component.extend(component => component.extend<MarkdownContentExtensions>(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)
Expand Down
143 changes: 143 additions & 0 deletions src/utility/string/Markdown.ts
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 0 additions & 6 deletions src/utility/string/Strings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import MarkdownIt from 'markdown-it'
import MarkdownItHTML from 'utility/string/MarkdownItHTML'

namespace Strings {
export type Replace<STRING extends string, MATCH extends string, REPLACE extends string> =
Expand Down Expand Up @@ -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

0 comments on commit 53547d0

Please sign in to comment.