Przeglądaj źródła

feat: plantuml in markdown preview

NGPixel 5 lat temu
rodzic
commit
53da387082

+ 15 - 3
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 `<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, {
@@ -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')

+ 190 - 0
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 '?'
+}

+ 1 - 0
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",

+ 5 - 0
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

+ 5 - 1
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

+ 3 - 3
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

+ 5 - 0
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"