feat: plantuml in markdown preview

This commit is contained in:
NGPixel 2020-05-08 22:51:32 -04:00
parent cc9f022051
commit 53da387082
7 changed files with 224 additions and 7 deletions

View File

@ -119,7 +119,7 @@
span {{$t('editor:markup.insertAssets')}} span {{$t('editor:markup.insertAssets')}}
v-tooltip(right, color='teal') v-tooltip(right, color='teal')
template(v-slot:activator='{ on }') template(v-slot:activator='{ on }')
v-btn.mt-3.animated.fadeInLeft.wait-p2s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalBlocks`)', disabled).mx-0 v-btn.mt-3.animated.fadeInLeft.wait-p2s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalBlocks`)').mx-0
v-icon(:color='activeModal === `editorModalBlocks` ? `teal` : ``') mdi-view-dashboard-outline v-icon(:color='activeModal === `editorModalBlocks` ? `teal` : ``') mdi-view-dashboard-outline
span {{$t('editor:markup.insertBlock')}} span {{$t('editor:markup.insertBlock')}}
v-tooltip(right, color='teal') v-tooltip(right, color='teal')
@ -222,6 +222,7 @@ import mdImsize from 'markdown-it-imsize'
import katex from 'katex' import katex from 'katex'
import 'katex/dist/contrib/mhchem' import 'katex/dist/contrib/mhchem'
import twemoji from 'twemoji' import twemoji from 'twemoji'
import plantuml from './markdown/plantuml'
// Prism (Syntax Highlighting) // Prism (Syntax Highlighting)
import Prism from 'prismjs' import Prism from 'prismjs'
@ -257,7 +258,11 @@ const md = new MarkdownIt({
linkify: true, linkify: true,
typography: true, typography: true,
highlight(str, lang) { highlight(str, lang) {
return `<pre class="line-numbers"><code class="language-${lang}">${_.escape(str)}</code></pre>` if (['mermaid', 'plantuml'].includes(lang)) {
return `<pre class="codeblock-${lang}"><code>${_.escape(str)}</code></pre>`
} else {
return `<pre class="line-numbers"><code class="language-${lang}">${_.escape(str)}</code></pre>`
}
} }
}) })
.use(mdAttrs, { .use(mdAttrs, {
@ -293,6 +298,13 @@ md.renderer.rules.paragraph_open = injectLineNumbers
md.renderer.rules.heading_open = injectLineNumbers md.renderer.rules.heading_open = injectLineNumbers
md.renderer.rules.blockquote_open = injectLineNumbers md.renderer.rules.blockquote_open = injectLineNumbers
// ========================================
// PLANTUML
// ========================================
// TODO: Use same options as defined in backend
plantuml.init(md, {})
// ======================================== // ========================================
// KATEX // KATEX
// ======================================== // ========================================
@ -542,7 +554,7 @@ export default {
}) })
}, },
renderMermaidDiagrams () { renderMermaidDiagrams () {
document.querySelectorAll('.editor-markdown-preview pre.line-numbers > code.language-mermaid').forEach(elm => { document.querySelectorAll('.editor-markdown-preview pre.codeblock-mermaid > code').forEach(elm => {
mermaidId++ mermaidId++
const mermaidDef = elm.innerText const mermaidDef = elm.innerText
const mmElm = document.createElement('div') const mmElm = document.createElement('div')

View File

@ -0,0 +1,190 @@
const pako = require('pako')
// ------------------------------------
// Markdown - PlantUML Preprocessor
// ------------------------------------
module.exports = {
init (mdinst, conf) {
mdinst.use((md, opts) => {
const openMarker = opts.openMarker || '```plantuml'
const openChar = openMarker.charCodeAt(0)
const closeMarker = opts.closeMarker || '```'
const closeChar = closeMarker.charCodeAt(0)
const imageFormat = opts.imageFormat || 'svg'
const server = opts.server || 'https://plantuml.requarks.io'
md.block.ruler.before('fence', 'uml_diagram', (state, startLine, endLine, silent) => {
let nextLine
let markup
let params
let token
let i
let autoClosed = false
let start = state.bMarks[startLine] + state.tShift[startLine]
let max = state.eMarks[startLine]
// Check out the first character quickly,
// this should filter out most of non-uml blocks
//
if (openChar !== state.src.charCodeAt(start)) { return false }
// Check out the rest of the marker string
//
for (i = 0; i < openMarker.length; ++i) {
if (openMarker[i] !== state.src[start + i]) { return false }
}
markup = state.src.slice(start, start + i)
params = state.src.slice(start + i, max)
// Since start is found, we can report success here in validation mode
//
if (silent) { return true }
// Search for the end of the block
//
nextLine = startLine
for (;;) {
nextLine++
if (nextLine >= endLine) {
// unclosed block should be autoclosed by end of document.
// also block seems to be autoclosed by end of parent
break
}
start = state.bMarks[nextLine] + state.tShift[nextLine]
max = state.eMarks[nextLine]
if (start < max && state.sCount[nextLine] < state.blkIndent) {
// non-empty line with negative indent should stop the list:
// - ```
// test
break
}
if (closeChar !== state.src.charCodeAt(start)) {
// didn't find the closing fence
continue
}
if (state.sCount[nextLine] > state.sCount[startLine]) {
// closing fence should not be indented with respect of opening fence
continue
}
var closeMarkerMatched = true
for (i = 0; i < closeMarker.length; ++i) {
if (closeMarker[i] !== state.src[start + i]) {
closeMarkerMatched = false
break
}
}
if (!closeMarkerMatched) {
continue
}
// make sure tail has spaces only
if (state.skipSpaces(start + i) < max) {
continue
}
// found!
autoClosed = true
break
}
const contents = state.src
.split('\n')
.slice(startLine + 1, nextLine)
.join('\n')
// We generate a token list for the alt property, to mimic what the image parser does.
let altToken = []
// Remove leading space if any.
let alt = params ? params.slice(1) : 'uml diagram'
state.md.inline.parse(
alt,
state.md,
state.env,
altToken
)
var zippedCode = encode64(pako.deflate('@startuml\n' + contents + '\n@enduml', { to: 'string' }))
token = state.push('uml_diagram', 'img', 0)
// alt is constructed from children. No point in populating it here.
token.attrs = [ [ 'src', `${server}/${imageFormat}/${zippedCode}` ], [ 'alt', '' ], ['class', 'uml-diagram'] ]
token.block = true
token.children = altToken
token.info = params
token.map = [ startLine, nextLine ]
token.markup = markup
state.line = nextLine + (autoClosed ? 1 : 0)
return true
}, {
alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]
})
md.renderer.rules.uml_diagram = md.renderer.rules.image
}, {
openMarker: conf.openMarker,
closeMarker: conf.closeMarker,
imageFormat: conf.imageFormat,
server: conf.server
})
}
}
function encode64 (data) {
let r = ''
for (let i = 0; i < data.length; i += 3) {
if (i + 2 === data.length) {
r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), 0)
} else if (i + 1 === data.length) {
r += append3bytes(data.charCodeAt(i), 0, 0)
} else {
r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), data.charCodeAt(i + 2))
}
}
return r
}
function append3bytes (b1, b2, b3) {
let c1 = b1 >> 2
let c2 = ((b1 & 0x3) << 4) | (b2 >> 4)
let c3 = ((b2 & 0xF) << 2) | (b3 >> 6)
let c4 = b3 & 0x3F
let r = ''
r += encode6bit(c1 & 0x3F)
r += encode6bit(c2 & 0x3F)
r += encode6bit(c3 & 0x3F)
r += encode6bit(c4 & 0x3F)
return r
}
function encode6bit(raw) {
let b = raw
if (b < 10) {
return String.fromCharCode(48 + b)
}
b -= 10
if (b < 26) {
return String.fromCharCode(65 + b)
}
b -= 26
if (b < 26) {
return String.fromCharCode(97 + b)
}
b -= 26
if (b === 0) {
return '-'
}
if (b === 1) {
return '_'
}
return '?'
}

View File

@ -258,6 +258,7 @@
"moment-timezone-data-webpack-plugin": "1.3.0", "moment-timezone-data-webpack-plugin": "1.3.0",
"offline-plugin": "5.0.7", "offline-plugin": "5.0.7",
"optimize-css-assets-webpack-plugin": "5.0.3", "optimize-css-assets-webpack-plugin": "5.0.3",
"pako": "1.0.11",
"postcss-cssnext": "3.1.0", "postcss-cssnext": "3.1.0",
"postcss-flexbugs-fixes": "4.2.0", "postcss-flexbugs-fixes": "4.2.0",
"postcss-flexibility": "2.0.0", "postcss-flexibility": "2.0.0",

View File

@ -12,24 +12,28 @@ props:
title: Allow HTML title: Allow HTML
hint: Enable HTML tags in content hint: Enable HTML tags in content
order: 1 order: 1
public: true
linkify: linkify:
type: Boolean type: Boolean
default: true default: true
title: Automatically convert links title: Automatically convert links
hint: Links will automatically be converted to clickable links. hint: Links will automatically be converted to clickable links.
order: 2 order: 2
public: true
linebreaks: linebreaks:
type: Boolean type: Boolean
default: true default: true
title: Automatically convert line breaks title: Automatically convert line breaks
hint: Add linebreaks within paragraphs. hint: Add linebreaks within paragraphs.
order: 3 order: 3
public: true
typographer: typographer:
type: Boolean type: Boolean
default: false default: false
title: Typographer title: Typographer
hint: Enable some language-neutral replacement + quotes beautification hint: Enable some language-neutral replacement + quotes beautification
order: 4 order: 4
public: true
quotes: quotes:
type: String type: String
default: English default: English
@ -49,3 +53,4 @@ props:
- Russian - Russian
- Spanish - Spanish
- Swedish - Swedish
public: true

View File

@ -8,22 +8,25 @@ dependsOn: markdownCore
props: props:
server: server:
type: String type: String
default: https://www.plantuml.com/plantuml default: https://plantuml.requarks.io
title: PlantUML Server title: PlantUML Server
hint: PlantUML server used for image generation hint: PlantUML server used for image generation
order: 1 order: 1
public: true
openMarker: openMarker:
type: String type: String
default: "```plantuml" default: "```plantuml"
title: Open Marker title: Open Marker
hint: String to use as opening delimiter hint: String to use as opening delimiter
order: 2 order: 2
public: true
closeMarker: closeMarker:
type: String type: String
default: "```" default: "```"
title: Close Marker title: Close Marker
hint: String to use as closing delimiter hint: String to use as closing delimiter
order: 3 order: 3
public: true
imageFormat: imageFormat:
type: String type: String
default: svg default: svg
@ -35,3 +38,4 @@ props:
- latex - latex
- ascii - ascii
order: 4 order: 4
public: true

View File

@ -7,12 +7,12 @@ const zlib = require('zlib')
module.exports = { module.exports = {
init (mdinst, conf) { init (mdinst, conf) {
mdinst.use((md, opts) => { mdinst.use((md, opts) => {
const openMarker = opts.openMarker || '@startuml' const openMarker = opts.openMarker || '```plantuml'
const openChar = openMarker.charCodeAt(0) const openChar = openMarker.charCodeAt(0)
const closeMarker = opts.closeMarker || '@enduml' const closeMarker = opts.closeMarker || '```'
const closeChar = closeMarker.charCodeAt(0) const closeChar = closeMarker.charCodeAt(0)
const imageFormat = opts.imageFormat || 'svg' const imageFormat = opts.imageFormat || 'svg'
const server = opts.server || 'https://www.plantuml.com/plantuml' const server = opts.server || 'https://plantuml.requarks.io'
md.block.ruler.before('fence', 'uml_diagram', (state, startLine, endLine, silent) => { md.block.ruler.before('fence', 'uml_diagram', (state, startLine, endLine, silent) => {
let nextLine let nextLine

View File

@ -11304,6 +11304,11 @@ packet-reader@1.0.0:
resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74"
integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==
pako@1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
pako@~1.0.5: pako@~1.0.5:
version "1.0.10" version "1.0.10"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732"