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"