mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 17:56:25 +00:00
236 lines
8.2 KiB
JavaScript
236 lines
8.2 KiB
JavaScript
import markdownit from 'markdown-it'
|
|
import sanitize from './markdown-it-sanitize-html'
|
|
import emoji from 'markdown-it-emoji'
|
|
import math from '@rokt33r/markdown-it-math'
|
|
import _ from 'lodash'
|
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
|
import {lastFindInArray} from './utils'
|
|
|
|
// FIXME We should not depend on global variable.
|
|
const katex = window.katex
|
|
const config = ConfigManager.get()
|
|
|
|
function createGutter (str, firstLineNumber) {
|
|
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
|
|
const lastLineNumber = (str.match(/\n/g) || []).length + firstLineNumber - 1
|
|
const lines = []
|
|
for (let i = firstLineNumber; i <= lastLineNumber; i++) {
|
|
lines.push('<span class="CodeMirror-linenumber">' + i + '</span>')
|
|
}
|
|
return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
|
|
}
|
|
|
|
class Markdown {
|
|
constructor (options = {}) {
|
|
const defaultOptions = {
|
|
typographer: true,
|
|
linkify: true,
|
|
html: true,
|
|
xhtmlOut: true,
|
|
breaks: true,
|
|
highlight: function (str, lang) {
|
|
const delimiter = ':'
|
|
const langInfo = lang.split(delimiter)
|
|
const langType = langInfo[0]
|
|
const fileName = langInfo[1] || ''
|
|
const firstLineNumber = parseInt(langInfo[2], 10)
|
|
|
|
if (langType === 'flowchart') {
|
|
return `<pre class="flowchart">${str}</pre>`
|
|
}
|
|
if (langType === 'sequence') {
|
|
return `<pre class="sequence">${str}</pre>`
|
|
}
|
|
return '<pre class="code CodeMirror">' +
|
|
'<span class="filename">' + fileName + '</span>' +
|
|
createGutter(str, firstLineNumber) +
|
|
'<code class="' + langType + '">' +
|
|
str +
|
|
'</code></pre>'
|
|
}
|
|
}
|
|
|
|
const updatedOptions = Object.assign(defaultOptions, options)
|
|
this.md = markdownit(updatedOptions)
|
|
|
|
// Sanitize use rinput before other plugins
|
|
this.md.use(sanitize, {
|
|
allowedTags: ['iframe', 'input',
|
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7', 'h8', 'br', 'b', 'i', 'strong', 'em', 'a', 'pre', 'code', 'img', 'tt',
|
|
'div', 'ins', 'del', 'sup', 'sub', 'p', 'ol', 'ul', 'table', 'thead', 'tbody', 'tfoot', 'blockquote',
|
|
'dl', 'dt', 'dd', 'kbd', 'q', 'samp', 'var', 'hr', 'ruby', 'rt', 'rp', 'li', 'tr', 'td', 'th', 's', 'strike', 'summary', 'details'
|
|
],
|
|
allowedAttributes: {
|
|
'*': [
|
|
'abbr', 'accept', 'accept-charset',
|
|
'accesskey', 'action', 'align', 'alt', 'axis',
|
|
'border', 'cellpadding', 'cellspacing', 'char',
|
|
'charoff', 'charset', 'checked',
|
|
'clear', 'cols', 'colspan', 'color',
|
|
'compact', 'coords', 'datetime', 'dir',
|
|
'disabled', 'enctype', 'for', 'frame',
|
|
'headers', 'height', 'hreflang',
|
|
'hspace', 'ismap', 'label', 'lang',
|
|
'maxlength', 'media', 'method',
|
|
'multiple', 'name', 'nohref', 'noshade',
|
|
'nowrap', 'open', 'prompt', 'readonly', 'rel', 'rev',
|
|
'rows', 'rowspan', 'rules', 'scope',
|
|
'selected', 'shape', 'size', 'span',
|
|
'start', 'summary', 'tabindex', 'target',
|
|
'title', 'type', 'usemap', 'valign', 'value',
|
|
'vspace', 'width', 'itemprop'
|
|
],
|
|
'a': ['href'],
|
|
'div': ['itemscope', 'itemtype'],
|
|
'blockquote': ['cite'],
|
|
'del': ['cite'],
|
|
'ins': ['cite'],
|
|
'q': ['cite'],
|
|
'img': ['src', 'width', 'height'],
|
|
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
|
|
'input': ['type', 'id', 'checked']
|
|
},
|
|
allowedIframeHostnames: ['www.youtube.com']
|
|
})
|
|
|
|
this.md.use(emoji, {
|
|
shortcuts: {}
|
|
})
|
|
this.md.use(math, {
|
|
inlineOpen: config.preview.latexInlineOpen,
|
|
inlineClose: config.preview.latexInlineClose,
|
|
blockOpen: config.preview.latexBlockOpen,
|
|
blockClose: config.preview.latexBlockClose,
|
|
inlineRenderer: function (str) {
|
|
let output = ''
|
|
try {
|
|
output = katex.renderToString(str.trim())
|
|
} catch (err) {
|
|
output = `<span class="katex-error">${err.message}</span>`
|
|
}
|
|
return output
|
|
},
|
|
blockRenderer: function (str) {
|
|
let output = ''
|
|
try {
|
|
output = katex.renderToString(str.trim(), { displayMode: true })
|
|
} catch (err) {
|
|
output = `<div class="katex-error">${err.message}</div>`
|
|
}
|
|
return output
|
|
}
|
|
})
|
|
this.md.use(require('markdown-it-imsize'))
|
|
this.md.use(require('markdown-it-footnote'))
|
|
this.md.use(require('markdown-it-multimd-table'))
|
|
this.md.use(require('markdown-it-named-headers'), {
|
|
slugify: (header) => {
|
|
return encodeURI(header.trim()
|
|
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
|
|
.replace(/\s+/g, '-'))
|
|
.replace(/\-+$/, '')
|
|
}
|
|
})
|
|
this.md.use(require('markdown-it-kbd'))
|
|
|
|
const deflate = require('markdown-it-plantuml/lib/deflate')
|
|
this.md.use(require('markdown-it-plantuml'), '', {
|
|
generateSource: function (umlCode) {
|
|
const s = unescape(encodeURIComponent(umlCode))
|
|
const zippedCode = deflate.encode64(
|
|
deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9)
|
|
)
|
|
return `http://www.plantuml.com/plantuml/svg/${zippedCode}`
|
|
}
|
|
})
|
|
|
|
// Override task item
|
|
this.md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
|
|
let content, terminate, i, l, token
|
|
let nextLine = startLine + 1
|
|
const terminatorRules = state.md.block.ruler.getRules('paragraph')
|
|
const endLine = state.lineMax
|
|
|
|
// jump line-by-line until empty one or EOF
|
|
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
|
|
// this would be a code block normally, but after paragraph
|
|
// it's considered a lazy continuation regardless of what's there
|
|
if (state.sCount[nextLine] - state.blkIndent > 3) { continue }
|
|
|
|
// quirk for blockquotes, this line should already be checked by that rule
|
|
if (state.sCount[nextLine] < 0) { continue }
|
|
|
|
// Some tags can terminate paragraph without empty line.
|
|
terminate = false
|
|
for (i = 0, l = terminatorRules.length; i < l; i++) {
|
|
if (terminatorRules[i](state, nextLine, endLine, true)) {
|
|
terminate = true
|
|
break
|
|
}
|
|
}
|
|
if (terminate) { break }
|
|
}
|
|
|
|
content = state.getLines(startLine, nextLine, state.blkIndent, false).trim()
|
|
|
|
state.line = nextLine
|
|
|
|
token = state.push('paragraph_open', 'p', 1)
|
|
token.map = [startLine, state.line]
|
|
|
|
if (state.parentType === 'list') {
|
|
const match = content.match(/^\[( |x)\] ?(.+)/i)
|
|
if (match) {
|
|
const liToken = lastFindInArray(state.tokens, token => token.type === 'list_item_open')
|
|
if (liToken) {
|
|
if (!liToken.attrs) {
|
|
liToken.attrs = []
|
|
}
|
|
liToken.attrs.push(['class', 'taskListItem'])
|
|
}
|
|
content = `<label class='taskListItem${match[1] !== ' ' ? ' checked' : ''}' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
|
|
}
|
|
}
|
|
|
|
token = state.push('inline', '', 0)
|
|
token.content = content
|
|
token.map = [startLine, state.line]
|
|
token.children = []
|
|
|
|
token = state.push('paragraph_close', 'p', -1)
|
|
|
|
return true
|
|
})
|
|
|
|
// Add line number attribute for scrolling
|
|
const originalRender = this.md.renderer.render
|
|
this.md.renderer.render = (tokens, options, env) => {
|
|
tokens.forEach((token) => {
|
|
switch (token.type) {
|
|
case 'heading_open':
|
|
case 'paragraph_open':
|
|
case 'blockquote_open':
|
|
case 'table_open':
|
|
token.attrPush(['data-line', token.map[0]])
|
|
}
|
|
})
|
|
const result = originalRender.call(this.md.renderer, tokens, options, env)
|
|
return result
|
|
}
|
|
// FIXME We should not depend on global variable.
|
|
window.md = this.md
|
|
}
|
|
|
|
render (content) {
|
|
if (!_.isString(content)) content = ''
|
|
return this.md.render(content)
|
|
}
|
|
|
|
normalizeLinkText (linkText) {
|
|
return this.md.normalizeLinkText(linkText)
|
|
}
|
|
}
|
|
|
|
export default Markdown
|
|
|