diff --git a/client/components/editor/editor-markdown.vue b/client/components/editor/editor-markdown.vue index 4385e2bd..9e03d95a 100644 --- a/client/components/editor/editor-markdown.vue +++ b/client/components/editor/editor-markdown.vue @@ -119,7 +119,7 @@ span {{$t('editor:markup.insertAssets')}} v-tooltip(right, color='teal') 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 span {{$t('editor:markup.insertBlock')}} v-tooltip(right, color='teal') @@ -222,6 +222,7 @@ import mdImsize from 'markdown-it-imsize' import katex from 'katex' import 'katex/dist/contrib/mhchem' import twemoji from 'twemoji' +import plantuml from './markdown/plantuml' // Prism (Syntax Highlighting) import Prism from 'prismjs' @@ -257,7 +258,11 @@ const md = new MarkdownIt({ linkify: true, typography: true, highlight(str, lang) { - return `
${_.escape(str)}
` + if (['mermaid', 'plantuml'].includes(lang)) { + return `
${_.escape(str)}
` + } else { + return `
${_.escape(str)}
` + } } }) .use(mdAttrs, { @@ -293,6 +298,13 @@ md.renderer.rules.paragraph_open = injectLineNumbers md.renderer.rules.heading_open = injectLineNumbers md.renderer.rules.blockquote_open = injectLineNumbers +// ======================================== +// PLANTUML +// ======================================== + +// TODO: Use same options as defined in backend +plantuml.init(md, {}) + // ======================================== // KATEX // ======================================== @@ -542,7 +554,7 @@ export default { }) }, 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++ const mermaidDef = elm.innerText const mmElm = document.createElement('div') diff --git a/client/components/editor/markdown/plantuml.js b/client/components/editor/markdown/plantuml.js new file mode 100644 index 00000000..0e89b218 --- /dev/null +++ b/client/components/editor/markdown/plantuml.js @@ -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 '?' +} diff --git a/package.json b/package.json index 33305128..dd6e1943 100644 --- a/package.json +++ b/package.json @@ -258,6 +258,7 @@ "moment-timezone-data-webpack-plugin": "1.3.0", "offline-plugin": "5.0.7", "optimize-css-assets-webpack-plugin": "5.0.3", + "pako": "1.0.11", "postcss-cssnext": "3.1.0", "postcss-flexbugs-fixes": "4.2.0", "postcss-flexibility": "2.0.0", diff --git a/server/modules/rendering/markdown-core/definition.yml b/server/modules/rendering/markdown-core/definition.yml index dc58622b..1dd91a1d 100644 --- a/server/modules/rendering/markdown-core/definition.yml +++ b/server/modules/rendering/markdown-core/definition.yml @@ -12,24 +12,28 @@ props: title: Allow HTML hint: Enable HTML tags in content order: 1 + public: true linkify: type: Boolean default: true title: Automatically convert links hint: Links will automatically be converted to clickable links. order: 2 + public: true linebreaks: type: Boolean default: true title: Automatically convert line breaks hint: Add linebreaks within paragraphs. order: 3 + public: true typographer: type: Boolean default: false title: Typographer hint: Enable some language-neutral replacement + quotes beautification order: 4 + public: true quotes: type: String default: English @@ -49,3 +53,4 @@ props: - Russian - Spanish - Swedish + public: true diff --git a/server/modules/rendering/markdown-plantuml/definition.yml b/server/modules/rendering/markdown-plantuml/definition.yml index cebb9c15..e8b156fc 100644 --- a/server/modules/rendering/markdown-plantuml/definition.yml +++ b/server/modules/rendering/markdown-plantuml/definition.yml @@ -8,22 +8,25 @@ dependsOn: markdownCore props: server: type: String - default: https://www.plantuml.com/plantuml + default: https://plantuml.requarks.io title: PlantUML Server hint: PlantUML server used for image generation order: 1 + public: true openMarker: type: String default: "```plantuml" title: Open Marker hint: String to use as opening delimiter order: 2 + public: true closeMarker: type: String default: "```" title: Close Marker hint: String to use as closing delimiter order: 3 + public: true imageFormat: type: String default: svg @@ -35,3 +38,4 @@ props: - latex - ascii order: 4 + public: true diff --git a/server/modules/rendering/markdown-plantuml/renderer.js b/server/modules/rendering/markdown-plantuml/renderer.js index ddcb5fc1..10ee0479 100644 --- a/server/modules/rendering/markdown-plantuml/renderer.js +++ b/server/modules/rendering/markdown-plantuml/renderer.js @@ -7,12 +7,12 @@ const zlib = require('zlib') module.exports = { init (mdinst, conf) { mdinst.use((md, opts) => { - const openMarker = opts.openMarker || '@startuml' + const openMarker = opts.openMarker || '```plantuml' const openChar = openMarker.charCodeAt(0) - const closeMarker = opts.closeMarker || '@enduml' + const closeMarker = opts.closeMarker || '```' const closeChar = closeMarker.charCodeAt(0) 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) => { let nextLine diff --git a/yarn.lock b/yarn.lock index c19ea9c5..45b2fb0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11304,6 +11304,11 @@ packet-reader@1.0.0: resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" 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: version "1.0.10" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732"