Преглед изворни кода

feat: add asciidoc editor module (#5954)

* feat: add asciidoc editor module

* fix storage file extension for asciidoc pages

* fix: asciidoc editor + rendering improvements

* fix: description list css improvements

Co-authored-by: NGPixel <github@ngpixel.com>
Boris пре 2 година
родитељ
комит
54dbf9ad00

+ 0 - 12
client/components/admin.vue

@@ -65,15 +65,6 @@
             v-list-item(to='/comments')
             v-list-item(to='/comments')
               v-list-item-avatar(size='24', tile): v-icon mdi-comment-text-outline
               v-list-item-avatar(size='24', tile): v-icon mdi-comment-text-outline
               v-list-item-title {{ $t('admin:comments.title') }}
               v-list-item-title {{ $t('admin:comments.title') }}
-            v-list-item(to='/editor', disabled)
-              v-list-item-avatar(size='24', tile): v-icon(color='grey lighten-2') mdi-playlist-edit
-              v-list-item-title {{ $t('admin:editor.title') }}
-            v-list-item(to='/extensions')
-              v-list-item-avatar(size='24', tile): v-icon mdi-chip
-              v-list-item-title {{ $t('admin:extensions.title') }}
-            v-list-item(to='/logging', disabled)
-              v-list-item-avatar(size='24', tile): v-icon(color='grey lighten-2') mdi-script-text-outline
-              v-list-item-title {{ $t('admin:logging.title') }}
             v-list-item(to='/rendering', color='primary')
             v-list-item(to='/rendering', color='primary')
               v-list-item-avatar(size='24', tile): v-icon mdi-cogs
               v-list-item-avatar(size='24', tile): v-icon mdi-cogs
               v-list-item-title {{ $t('admin:rendering.title') }}
               v-list-item-title {{ $t('admin:rendering.title') }}
@@ -104,9 +95,6 @@
             v-list-item(to='/utilities', color='primary', v-if='hasPermission(`manage:system`)')
             v-list-item(to='/utilities', color='primary', v-if='hasPermission(`manage:system`)')
               v-list-item-avatar(size='24', tile): v-icon mdi-wrench-outline
               v-list-item-avatar(size='24', tile): v-icon mdi-wrench-outline
               v-list-item-title {{ $t('admin:utilities.title') }}
               v-list-item-title {{ $t('admin:utilities.title') }}
-            v-list-item(to='/webhooks', v-if='hasPermission(`manage:system`)', disabled)
-              v-list-item-avatar(size='24', tile): v-icon(color='grey lighten-2') mdi-webhook
-              v-list-item-title {{ $t('admin:webhooks.title') }}
             v-list-group(
             v-list-group(
               to='/dev'
               to='/dev'
               no-action
               no-action

+ 1 - 0
client/components/editor.vue

@@ -77,6 +77,7 @@ export default {
     editorApi: () => import(/* webpackChunkName: "editor-api", webpackMode: "lazy" */ './editor/editor-api.vue'),
     editorApi: () => import(/* webpackChunkName: "editor-api", webpackMode: "lazy" */ './editor/editor-api.vue'),
     editorCode: () => import(/* webpackChunkName: "editor-code", webpackMode: "lazy" */ './editor/editor-code.vue'),
     editorCode: () => import(/* webpackChunkName: "editor-code", webpackMode: "lazy" */ './editor/editor-code.vue'),
     editorCkeditor: () => import(/* webpackChunkName: "editor-ckeditor", webpackMode: "lazy" */ './editor/editor-ckeditor.vue'),
     editorCkeditor: () => import(/* webpackChunkName: "editor-ckeditor", webpackMode: "lazy" */ './editor/editor-ckeditor.vue'),
+    editorAsciidoc: () => import(/* webpackChunkName: "editor-asciidoc", webpackMode: "lazy" */ './editor/editor-asciidoc.vue'),
     editorMarkdown: () => import(/* webpackChunkName: "editor-markdown", webpackMode: "lazy" */ './editor/editor-markdown.vue'),
     editorMarkdown: () => import(/* webpackChunkName: "editor-markdown", webpackMode: "lazy" */ './editor/editor-markdown.vue'),
     editorRedirect: () => import(/* webpackChunkName: "editor-redirect", webpackMode: "lazy" */ './editor/editor-redirect.vue'),
     editorRedirect: () => import(/* webpackChunkName: "editor-redirect", webpackMode: "lazy" */ './editor/editor-redirect.vue'),
     editorModalEditorselect: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-editorselect.vue'),
     editorModalEditorselect: () => import(/* webpackChunkName: "editor", webpackMode: "eager" */ './editor/editor-modal-editorselect.vue'),

+ 8 - 2
client/components/editor/markdown/fold.js → client/components/editor/common/cmFold.js

@@ -7,7 +7,13 @@ const maxDepth = 100
 const codeBlockStartMatch = /^`{3}[a-zA-Z0-9]+$/
 const codeBlockStartMatch = /^`{3}[a-zA-Z0-9]+$/
 const codeBlockEndMatch = /^`{3}$/
 const codeBlockEndMatch = /^`{3}$/
 
 
-CodeMirror.registerHelper('fold', 'markdown', function (cm, start) {
+export default {
+  register(lang) {
+    CodeMirror.registerHelper('fold', lang, foldHandler)
+  }
+}
+
+function foldHandler (cm, start) {
   const firstLine = cm.getLine(start.line)
   const firstLine = cm.getLine(start.line)
   const lastLineNo = cm.lastLine()
   const lastLineNo = cm.lastLine()
   let end
   let end
@@ -59,4 +65,4 @@ CodeMirror.registerHelper('fold', 'markdown', function (cm, start) {
     from: CodeMirror.Pos(start.line, firstLine.length),
     from: CodeMirror.Pos(start.line, firstLine.length),
     to: CodeMirror.Pos(end, cm.getLine(end).length)
     to: CodeMirror.Pos(end, cm.getLine(end).length)
   }
   }
-})
+}

+ 707 - 0
client/components/editor/editor-asciidoc.vue

@@ -0,0 +1,707 @@
+<template lang='pug'>
+  .editor-asciidoc
+    v-toolbar.editor-asciidoc-toolbar(dense, color='primary', dark, flat, style='overflow-x: hidden;')
+      template(v-if='isModalShown')
+        v-spacer
+        v-btn.animated.fadeInRight(text, @click='closeAllModal')
+          v-icon(left) mdi-arrow-left-circle
+          span {{$t('editor:backToEditor')}}
+      template(v-else)
+        v-tooltip(bottom, color='primary')
+          template(v-slot:activator='{ on }')
+            v-btn.animated.fadeIn(icon, tile, v-on='on', @click='toggleMarkup({ start: `**` })').mx-0
+              v-icon mdi-format-bold
+          span {{$t('editor:markup.bold')}}
+        v-tooltip(bottom, color='primary')
+          template(v-slot:activator='{ on }')
+            v-btn.animated.fadeIn.wait-p1s(icon, tile, v-on='on', @click='toggleMarkup({ start: `__` })').mx-0
+              v-icon mdi-format-italic
+          span {{$t('editor:markup.italic')}}
+        v-menu(offset-y, open-on-hover)
+          template(v-slot:activator='{ on }')
+            v-btn.animated.fadeIn.wait-p3s(icon, tile, v-on='on').mx-0
+              v-icon mdi-format-header-pound
+          v-list.py-0
+            template(v-for='(n, idx) in 6')
+              v-list-item(@click='setHeaderLine(n)', :key='idx')
+                v-list-item-action
+                  v-icon(:size='24 - (idx - 1) * 2') mdi-format-header-{{n}}
+                v-list-item-title {{$t('editor:markup.heading', { level: n })}}
+              v-divider(v-if='idx < 5')
+        v-tooltip(bottom, color='primary')
+          template(v-slot:activator='{ on }')
+            v-btn.animated.fadeIn.wait-p4s(icon, tile, v-on='on', @click='toggleMarkup({ start: `~` })').mx-0
+              v-icon mdi-format-subscript
+          span {{$t('editor:markup.subscript')}}
+        v-tooltip(bottom, color='primary')
+          template(v-slot:activator='{ on }')
+            v-btn.animated.fadeIn.wait-p5s(icon, tile, v-on='on', @click='toggleMarkup({ start: `^` })').mx-0
+              v-icon mdi-format-superscript
+          span {{$t('editor:markup.superscript')}}
+        v-menu(offset-y, open-on-hover)
+          template(v-slot:activator='{ on }')
+            v-btn.animated.fadeIn.wait-p6s(icon, tile, v-on='on').mx-0
+              v-icon mdi-alpha-t-box-outline
+          v-list.py-0
+            v-list-item(@click='insertBeforeEachLine({ content: `> `})')
+              v-list-item-action
+                v-icon mdi-alpha-t-box-outline
+              v-list-item-title {{$t('editor:markup.blockquote')}}
+            v-divider
+            v-list-item(@click='insertBeforeEachLine({ content: `NOTE: `})')
+              v-list-item-action
+                v-icon(color='blue') mdi-alpha-n-box-outline
+              v-list-item-title {{'Note blockquote'}}
+            v-divider
+            v-list-item(@click='insertBeforeEachLine({ content: `TIP: `})')
+              v-list-item-action
+                v-icon(color='success') mdi-alpha-t-box-outline
+              v-list-item-title {{'Tip blockquote'}}
+            v-divider
+            v-list-item(@click='insertBeforeEachLine({ content: `WARNING: `})')
+              v-list-item-action
+                v-icon(color='warning') mdi-alpha-w-box-outline
+              v-list-item-title {{$t('editor:markup.blockquoteWarning')}}
+            v-divider
+            v-list-item(@click='insertBeforeEachLine({ content: `CAUTION: `})')
+              v-list-item-action
+                v-icon(color='purple') mdi-alpha-c-box-outline
+              v-list-item-title {{'Caution blockquote'}}
+            v-list-item(@click='insertBeforeEachLine({ content: `IMPORTANT: `})')
+              v-list-item-action
+                v-icon(color='error') mdi-alpha-i-box-outline
+              v-list-item-title {{'Important blockquote'}}
+            v-divider
+        template(v-if='$vuetify.breakpoint.mdAndUp')
+          v-spacer
+          v-tooltip(bottom, color='primary')
+            template(v-slot:activator='{ on }')
+              v-btn.animated.fadeIn.wait-p2s(icon, tile, v-on='on', @click='previewShown = !previewShown').mx-0
+                v-icon mdi-book-open-outline
+            span {{$t('editor:markup.togglePreviewPane')}}
+
+    .editor-asciidoc-main
+      .editor-asciidoc-sidebar
+        v-tooltip(right, color='teal')
+          template(v-slot:activator='{ on }')
+            v-btn.animated.fadeInLeft(icon, tile, v-on='on', dark, @click='insertLink').mx-0
+              v-icon mdi-link-plus
+          span {{$t('editor:markup.insertLink')}}
+        v-tooltip(right, color='teal')
+          template(v-slot:activator='{ on }')
+            v-btn.mt-3.animated.fadeInLeft.wait-p1s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalMedia`)').mx-0
+              v-icon(:color='activeModal === `editorModalMedia` ? `teal` : ``') mdi-folder-multiple-image
+          span {{$t('editor:markup.insertAssets')}}
+        v-tooltip(right, color='teal')
+          template(v-slot:activator='{ on }')
+            v-btn.mt-3.animated.fadeInLeft.wait-p5s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalDrawio`)').mx-0
+              v-icon mdi-chart-multiline
+          span {{$t('editor:markup.insertDiagram')}}
+        template(v-if='$vuetify.breakpoint.mdAndUp')
+          v-spacer
+          v-tooltip(right, color='teal')
+            template(v-slot:activator='{ on }')
+              v-btn.mt-3.animated.fadeInLeft.wait-p8s(icon, tile, v-on='on', dark, @click='toggleFullscreen').mx-0
+                v-icon mdi-arrow-expand-all
+            span {{$t('editor:markup.distractionFreeMode')}}
+      .editor-asciidoc-editor
+        textarea(ref='cm')
+      transition(name='editor-asciidoc-preview')
+        .editor-asciidoc-preview(v-if='previewShown')
+          .editor-asciidoc-preview-content.contents(ref='editorPreviewContainer')
+            div(
+              ref='editorPreview'
+              v-html='previewHTML'
+              )
+
+    v-system-bar.editor-asciidoc-sysbar(dark, status, color='grey darken-3')
+      .caption.editor-asciidoc-sysbar-locale {{locale.toUpperCase()}}
+      .caption.px-3 /{{path}}
+      template(v-if='$vuetify.breakpoint.mdAndUp')
+        v-spacer
+        .caption AsciiDoc
+        v-spacer
+        .caption Ln {{cursorPos.line + 1}}, Col {{cursorPos.ch + 1}}
+    page-selector(mode='select', v-model='insertLinkDialog', :open-handler='insertLinkHandler', :path='path', :locale='locale')
+</template>
+
+<script>
+import _ from 'lodash'
+import { get, sync } from 'vuex-pathify'
+import DOMPurify from 'dompurify'
+
+// ========================================
+// IMPORTS
+// ========================================
+
+// Code Mirror
+import CodeMirror from 'codemirror'
+import 'codemirror/lib/codemirror.css'
+
+// Language
+import 'codemirror-asciidoc'
+
+// Addons
+import 'codemirror/addon/selection/active-line.js'
+import 'codemirror/addon/display/fullscreen.js'
+import 'codemirror/addon/display/fullscreen.css'
+import 'codemirror/addon/selection/mark-selection.js'
+import 'codemirror/addon/search/searchcursor.js'
+import 'codemirror/addon/hint/show-hint.js'
+import 'codemirror/addon/fold/foldcode.js'
+import 'codemirror/addon/fold/foldgutter.js'
+import 'codemirror/addon/fold/foldgutter.css'
+import cmFold from './common/cmFold'
+
+// ========================================
+// INIT
+// ========================================
+const asciidoctor = require('asciidoctor')()
+const cheerio = require('cheerio')
+
+// Platform detection
+const CtrlKey = /Mac/.test(navigator.platform) ? 'Cmd' : 'Ctrl'
+
+// ========================================
+// HELPER FUNCTIONS
+// ========================================
+
+cmFold.register('asciidoc')
+
+// ========================================
+// Vue Component
+// ========================================
+
+export default {
+  data() {
+    return {
+      cm: null,
+      cursorPos: { ch: 0, line: 1 },
+      previewShown: true, // TODO
+      insertLinkDialog: false,
+      helpShown: false,
+      previewHTML: ''
+    }
+  },
+  computed: {
+    isMobile() {
+      return this.$vuetify.breakpoint.smAndDown
+    },
+    isModalShown() {
+      return this.helpShown || this.activeModal !== ''
+    },
+    locale: get('page/locale'),
+    path: get('page/path'),
+    mode: get('editor/mode'),
+    activeModal: sync('editor/activeModal')
+  },
+
+  methods: {
+    toggleModal(key) {
+      this.activeModal = (this.activeModal === key) ? '' : key
+      this.helpShown = false
+    },
+    closeAllModal() {
+      this.activeModal = ''
+      this.helpShown = false
+    },
+    onCmInput: _.debounce(function(newContent) {
+      this.processContent(newContent)
+    }, 600),
+    processContent(newContent) {
+      this.processMarkers(this.cm.firstLine(), this.cm.lastLine())
+      let html = asciidoctor.convert(newContent, {
+        standalone: false,
+        safe: 'safe',
+        attributes: {
+          showtitle: true,
+          icons: 'font'
+        }
+      })
+      const $ = cheerio.load(html, {
+        decodeEntities: true
+      })
+
+      $('pre.highlight > code.language-diagram').each((i, elm) => {
+        const diagramContent = Buffer.from($(elm).html(), 'base64').toString()
+        $(elm).parent().replaceWith(`<pre class="diagram">${diagramContent}</div>`)
+      })
+
+      this.previewHTML = DOMPurify.sanitize($.html(), {
+        ADD_TAGS: ['foreignObject']
+      })
+    },
+    /**
+     * Insert content at cursor
+     */
+    insertAtCursor({ content }) {
+      const cursor = this.cm.doc.getCursor('head')
+      this.cm.doc.replaceRange(content, cursor)
+    },
+    /**
+     * Insert content after current line
+     */
+    insertAfter({ content, newLine }) {
+      const curLine = this.cm.doc.getCursor('to').line
+      const lineLength = this.cm.doc.getLine(curLine).length
+      this.cm.doc.replaceRange(newLine ? `\n${content}\n` : content, { line: curLine, ch: lineLength + 1 })
+    },
+    /**
+     * Insert content before current line
+     */
+    insertBeforeEachLine({ content, after }) {
+      let lines = []
+      if (!this.cm.doc.somethingSelected()) {
+        lines.push(this.cm.doc.getCursor('head').line)
+      } else {
+        lines = _.flatten(this.cm.doc.listSelections().map(sl => {
+          const range = Math.abs(sl.anchor.line - sl.head.line) + 1
+          const lowestLine = (sl.anchor.line > sl.head.line) ? sl.head.line : sl.anchor.line
+          return _.times(range, l => l + lowestLine)
+        }))
+      }
+      lines.forEach(ln => {
+        let lineContent = this.cm.doc.getLine(ln)
+        const lineLength = lineContent.length
+        if (_.startsWith(lineContent, content)) {
+          lineContent = lineContent.substring(content.length)
+        }
+
+        this.cm.doc.replaceRange(content + lineContent, { line: ln, ch: 0 }, { line: ln, ch: lineLength })
+      })
+      if (after) {
+        const lastLine = _.last(lines)
+        this.cm.doc.replaceRange(`\n${after}\n`, { line: lastLine, ch: this.cm.doc.getLine(lastLine).length + 1 })
+      }
+    },
+    /**
+     * Update cursor state
+     */
+    positionSync(cm) {
+      this.cursorPos = cm.getCursor('head')
+    },
+    toggleMarkup({ start, end }) {
+      if (!end) { end = start }
+      if (!this.cm.doc.somethingSelected()) {
+        return this.$store.commit('showNotification', {
+          message: this.$t('editor:markup.noSelectionError'),
+          style: 'warning',
+          icon: 'warning'
+        })
+      }
+      this.cm.doc.replaceSelections(this.cm.doc.getSelections().map(s => start + s + end))
+    },
+    setHeaderLine(lvl) {
+      const curLine = this.cm.doc.getCursor('head').line
+      let lineContent = this.cm.doc.getLine(curLine)
+      const lineLength = lineContent.length
+      if (_.startsWith(lineContent, '=')) {
+        lineContent = lineContent.replace(/^(=+ )/, '')
+      }
+      lineContent = _.times(lvl, n => '=').join('') + ` ` + lineContent
+      this.cm.doc.replaceRange(lineContent, { line: curLine, ch: 0 }, { line: curLine, ch: lineLength })
+    },
+
+    toggleFullscreen () {
+      this.cm.setOption('fullScreen', true)
+    },
+    refresh() {
+      this.$nextTick(() => {
+        this.cm.refresh()
+      })
+    },
+    insertLink () {
+      this.insertLinkDialog = true
+    },
+    insertLinkHandler ({ locale, path }) {
+      const lastPart = _.last(path.split('/'))
+      this.insertAtCursor({
+        content: siteLangs.length > 0 ? `link:/${locale}/${path}[${lastPart}]` : `link:/${path}[${lastPart}]`
+      })
+    },
+    processMarkers (from, to) {
+      let found = null
+      let foundStart = 0
+      this.cm.doc.getAllMarks().forEach(mk => {
+        if (mk.__kind) {
+          mk.clear()
+        }
+      })
+      this.cm.eachLine(from, to, ln => {
+        const line = ln.lineNo()
+        if (ln.text.startsWith('```diagram')) {
+          found = 'diagram'
+          foundStart = line
+        } else if (ln.text === '```' && found) {
+          switch (found) {
+            // ------------------------------
+            // -> DIAGRAM
+            // ------------------------------
+            case 'diagram': {
+              if (line - foundStart !== 2) {
+                return
+              }
+              this.addMarker({
+                kind: 'diagram',
+                from: { line: foundStart, ch: 3 },
+                to: { line: foundStart, ch: 10 },
+                text: 'Edit Diagram',
+                action: ((start, end) => {
+                  return (ev) => {
+                    this.cm.doc.setSelection({ line: start, ch: 0 }, { line: end, ch: 3 })
+                    try {
+                      const raw = this.cm.doc.getLine(end - 1)
+                      this.$store.set('editor/activeModalData', Buffer.from(raw, 'base64').toString())
+                      this.toggleModal(`editorModalDrawio`)
+                    } catch (err) {
+                      return this.$store.commit('showNotification', {
+                        message: 'Failed to process diagram data.',
+                        style: 'warning',
+                        icon: 'warning'
+                      })
+                    }
+                  }
+                })(foundStart, line)
+              })
+              if (ln.height > 0) {
+                this.cm.foldCode(foundStart)
+              }
+              break
+            }
+          }
+          found = null
+        }
+      })
+    },
+    addMarker ({ kind, from, to, text, action }) {
+      const markerElm = document.createElement('span')
+      markerElm.appendChild(document.createTextNode(text))
+      markerElm.className = 'CodeMirror-buttonmarker'
+      markerElm.addEventListener('click', action)
+      this.cm.markText(from, to, { replacedWith: markerElm, __kind: kind })
+    }
+  },
+  mounted() {
+    this.$store.set('editor/editorKey', 'asciidoc')
+
+    if (this.mode === 'create') {
+      this.$store.set('editor/content', '== header\n\ncontent')
+    }
+
+    // Initialize CodeMirror
+
+    this.cm = CodeMirror.fromTextArea(this.$refs.cm, {
+      tabSize: 2,
+      mode: 'asciidoc',
+      theme: 'wikijs-dark',
+      lineNumbers: true,
+      lineWrapping: true,
+      line: true,
+      styleActiveLine: true,
+      highlightSelectionMatches: {
+        annotateScrollbar: true
+      },
+      viewportMargin: 50,
+      inputStyle: 'contenteditable',
+      allowDropFileTypes: ['image/jpg', 'image/png', 'image/svg', 'image/jpeg', 'image/gif'],
+      direction: siteConfig.rtl ? 'rtl' : 'ltr',
+      foldGutter: true,
+      gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter']
+
+    })
+    this.cm.setValue(this.$store.get('editor/content'))
+    this.cm.on('change', c => {
+      this.$store.set('editor/content', c.getValue())
+      this.onCmInput(this.$store.get('editor/content'))
+    })
+    if (this.$vuetify.breakpoint.mdAndUp) {
+      this.cm.setSize(null, 'calc(100vh - 137px)')
+    } else {
+      this.cm.setSize(null, 'calc(100vh - 112px - 16px)')
+    }
+
+    // Set Keybindings
+
+    const keyBindings = {
+      'F11' (c) {
+        c.setOption('fullScreen', !c.getOption('fullScreen'))
+      },
+      'Esc' (c) {
+        if (c.getOption('fullScreen')) c.setOption('fullScreen', false)
+      }
+    }
+    _.set(keyBindings, `${CtrlKey}-B`, c => {
+      this.toggleMarkup({ start: `**` })
+      return false
+    })
+    _.set(keyBindings, `${CtrlKey}-I`, c => {
+      this.toggleMarkup({ start: `__` })
+      return false
+    })
+
+    this.cm.setOption('extraKeys', keyBindings)
+
+    // Handle cursor movement
+
+    this.cm.on('cursorActivity', c => {
+      this.positionSync(c)
+    })
+
+    // Render initial preview
+    this.processContent(this.$store.get('editor/content'))
+
+    this.$root.$on('editorInsert', opts => {
+      switch (opts.kind) {
+        case 'IMAGE':
+          let img = `image::${opts.path}[${opts.text}]`
+          this.insertAtCursor({
+            content: img
+          })
+          break
+        case 'BINARY':
+          this.insertAtCursor({
+            content: `link:${opts.path}[${opts.text}]`
+          })
+          break
+        case 'DIAGRAM':
+          const selStartLine = this.cm.getCursor('from').line
+          const selEndLine = this.cm.getCursor('to').line + 1
+          this.cm.doc.replaceSelection('```diagram\n' + opts.text + '\n```\n', 'start')
+          this.processMarkers(selStartLine, selEndLine)
+          break
+      }
+    })
+
+    // Handle save conflict
+    this.$root.$on('saveConflict', () => {
+      this.toggleModal(`editorModalConflict`)
+    })
+    this.$root.$on('overwriteEditorContent', () => {
+      this.cm.setValue(this.$store.get('editor/content'))
+    })
+  },
+  beforeDestroy() {
+    this.$root.$off('editorInsert')
+  }
+}
+</script>
+
+<style lang='scss'>
+$editor-ascii-height: calc(100vh - 137px);
+$editor-ascii-height-mobile: calc(100vh - 112px - 16px);
+
+.editor-asciidoc {
+  &-main {
+    display: flex;
+    width: 100%;
+  }
+
+  &-editor {
+    background-color: darken(mc('grey', '900'), 4.5%);
+    flex: 1 1 50%;
+    display: block;
+    height: $editor-ascii-height;
+    position: relative;
+
+    @include until($tablet) {
+      height: $editor-ascii-height-mobile;
+    }
+  }
+
+  &-preview {
+    flex: 1 1 50%;
+    background-color: mc('grey', '100');
+    position: relative;
+    height: $editor-ascii-height;
+    overflow: hidden;
+    padding: 1rem;
+
+    @at-root .theme--dark & {
+      background-color: mc('grey', '900');
+    }
+
+    @include until($tablet) {
+      display: none;
+    }
+
+    &-enter-active, &-leave-active {
+      transition: max-width .5s ease;
+      max-width: 50vw;
+
+      .editor-code-preview-content {
+        width: 50vw;
+        overflow:hidden;
+      }
+    }
+    &-enter, &-leave-to {
+      max-width: 0;
+    }
+
+    &-content {
+      height: $editor-ascii-height;
+      overflow-y: scroll;
+      padding: 0;
+      width: calc(100% + 17px);
+      // -ms-overflow-style: none;
+
+      // &::-webkit-scrollbar {
+      //   width: 0px;
+      //   background: transparent;
+      // }
+
+      @include until($tablet) {
+        height: $editor-ascii-height-mobile;
+      }
+
+      > div {
+        outline: none;
+      }
+
+      p.line {
+        overflow-wrap: break-word;
+      }
+
+      .tabset {
+        background-color: mc('teal', '700');
+        color: mc('teal', '100') !important;
+        padding: 5px 12px;
+        font-size: 14px;
+        font-weight: 500;
+        border-radius: 5px 0 0 0;
+        font-style: italic;
+
+        &::after {
+          display: none;
+        }
+
+        &-header {
+          background-color: mc('teal', '500');
+          color: #FFF !important;
+          padding: 5px 12px;
+          font-size: 14px;
+          font-weight: 500;
+          margin-top: 0 !important;
+
+          &::after {
+            display: none;
+          }
+        }
+
+        &-content {
+          border-left: 5px solid mc('teal', '500');
+          background-color: mc('teal', '50');
+          padding: 0 15px 15px;
+          overflow: hidden;
+
+          @at-root .theme--dark & {
+            background-color: rgba(mc('teal', '500'), .1);
+          }
+        }
+      }
+    }
+  }
+
+  &-toolbar {
+    background-color: mc('blue', '700');
+    background-image: linear-gradient(to bottom, mc('blue', '700') 0%, mc('blue','800') 100%);
+    color: #FFF;
+
+    .v-toolbar__content {
+      padding-left: 64px;
+
+      @include until($tablet) {
+        padding-left: 8px;
+      }
+    }
+  }
+
+  &-insert:not(.v-speed-dial--right) {
+    @include from($tablet) {
+      left: 50%;
+      margin-left: -28px;
+    }
+  }
+
+  &-sidebar {
+    background-color: mc('grey', '900');
+    width: 64px;
+    display: flex;
+    flex-direction: column;
+    justify-content: flex-start;
+    align-items: center;
+    padding: 24px 0;
+
+    @include until($tablet) {
+      padding: 12px 0;
+      width: 40px;
+    }
+  }
+
+  &-sysbar {
+    padding-left: 0;
+
+    &-locale {
+      background-color: rgba(255,255,255,.25);
+      display:inline-flex;
+      padding: 0 12px;
+      height: 24px;
+      width: 63px;
+      justify-content: center;
+      align-items: center;
+    }
+  }
+
+  // ==========================================
+  // Fix FAB revealing under codemirror
+  // ==========================================
+
+  .speed-dial--fixed {
+    z-index: 8;
+  }
+
+  // ==========================================
+  // CODE MIRROR
+  // ==========================================
+
+  .CodeMirror {
+    height: auto;
+    font-family: 'Roboto Mono', monospace;
+    font-size: .9rem;
+
+    .cm-header-1 {
+      font-size: 1.5rem;
+    }
+    .cm-header-2 {
+      font-size: 1.25rem;
+    }
+    .cm-header-3 {
+      font-size: 1.15rem;
+    }
+    .cm-header-4 {
+      font-size: 1.1rem;
+    }
+    .cm-header-5 {
+      font-size: 1.05rem;
+    }
+    .cm-header-6 {
+      font-size: 1.025rem;
+    }
+  }
+
+  .CodeMirror-wrap pre.CodeMirror-line, .CodeMirror-wrap pre.CodeMirror-line-like {
+    word-break: break-word;
+  }
+
+  .CodeMirror-focused .cm-matchhighlight {
+    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==);
+    background-position: bottom;
+    background-repeat: repeat-x;
+  }
+  .cm-matchhighlight {
+    background-color: mc('grey', '800');
+  }
+  .CodeMirror-selection-highlight-scrollbar {
+    background-color: mc('green', '600');
+  }
+}
+</style>

+ 5 - 29
client/components/editor/editor-markdown.vue

@@ -124,44 +124,19 @@
           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, disabled, @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')
-          template(v-slot:activator='{ on }')
-            v-btn.mt-3.animated.fadeInLeft.wait-p3s(icon, tile, v-on='on', dark, disabled).mx-0
-              v-icon mdi-code-braces
-          span {{$t('editor:markup.insertCodeBlock')}}
-        v-tooltip(right, color='teal')
-          template(v-slot:activator='{ on }')
-            v-btn.mt-3.animated.fadeInLeft.wait-p4s(icon, tile, v-on='on', dark, disabled).mx-0
-              v-icon mdi-movie
-          span {{$t('editor:markup.insertVideoAudio')}}
-        v-tooltip(right, color='teal')
-          template(v-slot:activator='{ on }')
-            v-btn.mt-3.animated.fadeInLeft.wait-p5s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalDrawio`)').mx-0
+            v-btn.mt-3.animated.fadeInLeft.wait-p2s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalDrawio`)').mx-0
               v-icon mdi-chart-multiline
               v-icon mdi-chart-multiline
           span {{$t('editor:markup.insertDiagram')}}
           span {{$t('editor:markup.insertDiagram')}}
-        v-tooltip(right, color='teal')
-          template(v-slot:activator='{ on }')
-            v-btn.mt-3.animated.fadeInLeft.wait-p6s(icon, tile, v-on='on', dark, disabled).mx-0
-              v-icon mdi-function-variant
-          span {{$t('editor:markup.insertMathExpression')}}
-        v-tooltip(right, color='teal')
-          template(v-slot:activator='{ on }')
-            v-btn.mt-3.animated.fadeInLeft.wait-p7s(icon, tile, v-on='on', dark, disabled).mx-0
-              v-icon mdi-table-plus
-          span {{$t('editor:markup.tableHelper')}}
         template(v-if='$vuetify.breakpoint.mdAndUp')
         template(v-if='$vuetify.breakpoint.mdAndUp')
           v-spacer
           v-spacer
           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-p8s(icon, tile, v-on='on', dark, @click='toggleFullscreen').mx-0
+              v-btn.mt-3.animated.fadeInLeft.wait-p3s(icon, tile, v-on='on', dark, @click='toggleFullscreen').mx-0
                 v-icon mdi-arrow-expand-all
                 v-icon mdi-arrow-expand-all
             span {{$t('editor:markup.distractionFreeMode')}}
             span {{$t('editor:markup.distractionFreeMode')}}
           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-p9s(icon, tile, v-on='on', dark, @click='toggleHelp').mx-0
+              v-btn.mt-3.animated.fadeInLeft.wait-p4s(icon, tile, v-on='on', dark, @click='toggleHelp').mx-0
                 v-icon(:color='helpShown ? `teal` : ``') mdi-help-circle
                 v-icon(:color='helpShown ? `teal` : ``') mdi-help-circle
             span {{$t('editor:markup.markdownFormattingHelp')}}
             span {{$t('editor:markup.markdownFormattingHelp')}}
       .editor-markdown-editor
       .editor-markdown-editor
@@ -220,7 +195,6 @@ import 'codemirror/addon/hint/show-hint.js'
 import 'codemirror/addon/fold/foldcode.js'
 import 'codemirror/addon/fold/foldcode.js'
 import 'codemirror/addon/fold/foldgutter.js'
 import 'codemirror/addon/fold/foldgutter.js'
 import 'codemirror/addon/fold/foldgutter.css'
 import 'codemirror/addon/fold/foldgutter.css'
-import './markdown/fold'
 
 
 // Markdown-it
 // Markdown-it
 import MarkdownIt from 'markdown-it'
 import MarkdownIt from 'markdown-it'
@@ -251,6 +225,7 @@ import mermaid from 'mermaid'
 // Helpers
 // Helpers
 import katexHelper from './common/katex'
 import katexHelper from './common/katex'
 import tabsetHelper from './markdown/tabset'
 import tabsetHelper from './markdown/tabset'
+import cmFold from './common/cmFold'
 
 
 // ========================================
 // ========================================
 // INIT
 // INIT
@@ -337,6 +312,7 @@ 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
 
 
+cmFold.register('markdown')
 // ========================================
 // ========================================
 // PLANTUML
 // PLANTUML
 // ========================================
 // ========================================

+ 30 - 149
client/components/editor/editor-modal-editorselect.vue

@@ -6,57 +6,7 @@
         .subtitle-1.white--text {{$t('editor:select.title')}}
         .subtitle-1.white--text {{$t('editor:select.title')}}
         v-container(grid-list-lg, fluid)
         v-container(grid-list-lg, fluid)
           v-layout(row, wrap, justify-center)
           v-layout(row, wrap, justify-center)
-            v-flex(xs4)
-              v-hover
-                template(v-slot:default='{ hover }')
-                  v-card.radius-7.primary.animated.fadeInUp(
-                    hover
-                    light
-                    ripple
-                    )
-                    v-card-text.text-center(@click='')
-                      img(src='/_assets/svg/editor-icon-api.svg', alt='API', style='width: 36px; opacity: .5;')
-                      .body-2.blue--text.mt-2.text--lighten-2 API Docs
-                      .caption.blue--text.text--lighten-1 REST / GraphQL
-                    v-fade-transition
-                      v-overlay(
-                        v-if='hover'
-                        absolute
-                        color='primary'
-                        opacity='.8'
-                        )
-                        .body-2.mt-7 Coming Soon
-            v-flex(xs4)
-              v-hover
-                template(v-slot:default='{ hover }')
-                  v-card.radius-7.primary.animated.fadeInUp.wait-p1s(
-                    hover
-                    light
-                    ripple
-                    )
-                    v-card-text.text-center(@click='')
-                      img(src='/_assets/svg/editor-icon-wikitext.svg', alt='WikiText', style='width: 36px; opacity: .5;')
-                      .body-2.blue--text.mt-2.text--lighten-2 Blog
-                      .caption.blue--text.text--lighten-1 Timeline of Posts
-                    v-fade-transition
-                      v-overlay(
-                        v-if='hover'
-                        absolute
-                        color='primary'
-                        opacity='.8'
-                        )
-                        .body-2.mt-7 Coming Soon
-            v-flex(xs4)
-              v-card.radius-7.animated.fadeInUp.wait-p2s(
-                hover
-                light
-                ripple
-                )
-                v-card-text.text-center(@click='selectEditor("code")')
-                  img(src='/_assets/svg/editor-icon-code.svg', alt='Code', style='width: 36px;')
-                  .body-2.primary--text.mt-2 Code
-                  .caption.grey--text Raw HTML
-            v-flex(xs4)
+            v-flex(xs6)
               v-card.radius-7.animated.fadeInUp.wait-p1s(
               v-card.radius-7.animated.fadeInUp.wait-p1s(
                 hover
                 hover
                 light
                 light
@@ -66,28 +16,8 @@
                   img(src='/_assets/svg/editor-icon-markdown.svg', alt='Markdown', style='width: 36px;')
                   img(src='/_assets/svg/editor-icon-markdown.svg', alt='Markdown', style='width: 36px;')
                   .body-2.primary--text.mt-2 Markdown
                   .body-2.primary--text.mt-2 Markdown
                   .caption.grey--text Plain Text Formatting
                   .caption.grey--text Plain Text Formatting
-            v-flex(xs4)
-              v-hover
-                template(v-slot:default='{ hover }')
-                  v-card.radius-7.primary.animated.fadeInUp.wait-p2s(
-                    hover
-                    light
-                    ripple
-                    )
-                    v-card-text.text-center(@click='')
-                      img(src='/_assets/svg/editor-icon-tabular.svg', alt='Tabular', style='width: 36px; opacity: .5;')
-                      .body-2.blue--text.mt-2.text--lighten-2 Tabular
-                      .caption.blue--text.text--lighten-1 Excel-like
-                    v-fade-transition
-                      v-overlay(
-                        v-if='hover'
-                        absolute
-                        color='primary'
-                        opacity='.8'
-                        )
-                        .body-2.mt-7 Coming Soon
-            v-flex(xs4)
-              v-card.radius-7.animated.fadeInUp.wait-p3s(
+            v-flex(xs6)
+              v-card.radius-7.animated.fadeInUp.wait-p2s(
                 hover
                 hover
                 light
                 light
                 ripple
                 ripple
@@ -96,85 +26,36 @@
                   img(src='/_assets/svg/editor-icon-ckeditor.svg', alt='Visual Editor', style='width: 36px;')
                   img(src='/_assets/svg/editor-icon-ckeditor.svg', alt='Visual Editor', style='width: 36px;')
                   .body-2.mt-2.primary--text Visual Editor
                   .body-2.mt-2.primary--text Visual Editor
                   .caption.grey--text Rich-text WYSIWYG
                   .caption.grey--text Rich-text WYSIWYG
-        //- .caption.blue--text.text--lighten-2 {{$t('editor:select.cannotChange')}}
-
-    v-card.radius-7.mt-2(color='teal darken-3', dark)
-      v-card-text.text-center.py-4
-        .subtitle-1.white--text {{$t('editor:select.customView')}}
-        v-container(grid-list-lg, fluid)
-          v-layout(row, wrap, justify-center)
             v-flex(xs4)
             v-flex(xs4)
-              v-hover
-                template(v-slot:default='{ hover }')
-                  v-card.radius-7.animated.fadeInUp(
-                    hover
-                    light
-                    ripple
-                    )
-                    v-card-text.text-center(@click='fromTemplate')
-                      img(src='/_assets/svg/icon-cube.svg', alt='From Template', style='width: 42px; opacity: .5;')
-                      .body-2.mt-1.teal--text From Template
-                      .caption.grey--text Use an existing page...
+              v-card.radius-7.animated.fadeInUp.wait-p3s(
+                hover
+                light
+                ripple
+                )
+                v-card-text.text-center(@click='selectEditor("asciidoc")')
+                  img(src='/_assets/svg/editor-icon-asciidoc.svg', alt='AsciiDoc', style='width: 36px;')
+                  .body-2.primary--text.mt-2 AsciiDoc
+                  .caption.grey--text Plain Text Formatting
             v-flex(xs4)
             v-flex(xs4)
-              v-hover
-                template(v-slot:default='{ hover }')
-                  v-card.radius-7.teal.animated.fadeInUp.wait-p1s(
-                    hover
-                    light
-                    ripple
-                    )
-                    //- v-card-text.text-center(@click='selectEditor("redirect")')
-                    v-card-text.text-center(@click='')
-                      img(src='/_assets/svg/icon-route.svg', alt='Redirection', style='width: 42px; opacity: .5;')
-                      .body-2.mt-1.teal--text.text--lighten-2 Redirection
-                      .caption.teal--text.text--lighten-1 Redirect the user to...
+              v-card.radius-7.animated.fadeInUp.wait-p4s(
+                hover
+                light
+                ripple
+                )
+                v-card-text.text-center(@click='selectEditor("code")')
+                  img(src='/_assets/svg/editor-icon-code.svg', alt='Code', style='width: 36px;')
+                  .body-2.primary--text.mt-2 Code
+                  .caption.grey--text Raw HTML
             v-flex(xs4)
             v-flex(xs4)
-              v-hover
-                template(v-slot:default='{ hover }')
-                  v-card.radius-7.teal.animated.fadeInUp.wait-p2s(
-                    hover
-                    light
-                    ripple
-                    )
-                    v-card-text.text-center(@click='')
-                      img(src='/_assets/svg/icon-sewing-patch.svg', alt='Code', style='width: 42px; opacity: .5;')
-                      .body-2.mt-1.teal--text.text--lighten-2 Embed
-                      .caption.teal--text.text--lighten-1 Include external pages
-                    v-fade-transition
-                      v-overlay(
-                        v-if='hover'
-                        absolute
-                        color='teal'
-                        opacity='.8'
-                        )
-                        .body-2.mt-7 Coming Soon
-    v-hover
-      template(v-slot:default='{ hover }')
-        v-card.radius-7.mt-2(color='indigo darken-3', dark)
-          v-toolbar(dense, flat, color='light-green darken-3')
-            v-spacer
-            .caption.mr-1 or convert from
-            v-btn.mx-1.animated.fadeInUp(depressed, color='light-green darken-2', @click='', disabled)
-              v-icon(left) mdi-alpha-a-circle
-              .body-2.text-none AsciiDoc
-            v-btn.mx-1.animated.fadeInUp.wait-p1s(depressed, color='light-green darken-2', @click='', disabled)
-              v-icon(left) mdi-alpha-c-circle
-              .body-2.text-none CREOLE
-            v-btn.mx-1.animated.fadeInUp.wait-p2s(depressed, color='light-green darken-2', @click='', disabled)
-              v-icon(left) mdi-alpha-t-circle
-              .body-2.text-none Textile
-            v-btn.mx-1.animated.fadeInUp.wait-p3s(depressed, color='light-green darken-2', @click='', disabled)
-              v-icon(left) mdi-alpha-w-circle
-              .body-2.text-none WikiText
-            v-spacer
-          v-fade-transition
-            v-overlay(
-              v-if='hover'
-              absolute
-              color='light-green darken-3'
-              opacity='.8'
-              )
-              .body-2 Coming Soon
+              v-card.radius-7.animated.fadeInUp.wait-p5s(
+                hover
+                light
+                ripple
+                )
+                v-card-text.text-center(@click='fromTemplate')
+                  img(src='/_assets/svg/icon-cube.svg', alt='From Template', style='width: 42px; opacity: .5;')
+                  .body-2.mt-1.teal--text From Template
+                  .caption.grey--text Use an existing page...
 
 
     page-selector(mode='select', v-model='templateDialogIsShown', :open-handler='fromTemplateHandle', :path='path', :locale='locale', must-exist)
     page-selector(mode='select', v-model='templateDialogIsShown', :open-handler='fromTemplateHandle', :path='path', :locale='locale', must-exist)
 </template>
 </template>

+ 1 - 0
client/static/svg/editor-icon-asciidoc.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 40 40" width="80px" height="80px"><path fill="#fff" d="M1.5 4.5H38.5V35.5H1.5z"/><path fill="#4788c7" d="M38,5v30H2V5H38 M39,4H1v32h38V4L39,4z"/><path fill="#98ccfd" d="M2 5H38V10H2z"/><path fill="#4788c7" d="M29 16H10c-.552 0-1-.448-1-1v0c0-.552.448-1 1-1h19c.552 0 1 .448 1 1v0C30 15.552 29.552 16 29 16zM11.5 22h-2C9.224 22 9 21.776 9 21.5l0 0C9 21.224 9.224 21 9.5 21h2c.276 0 .5.224.5.5l0 0C12 21.776 11.776 22 11.5 22zM29.5 22h-15c-.276 0-.5-.224-.5-.5l0 0c0-.276.224-.5.5-.5h15c.276 0 .5.224.5.5l0 0C30 21.776 29.776 22 29.5 22zM11.5 26h-2C9.224 26 9 25.776 9 25.5l0 0C9 25.224 9.224 25 9.5 25h2c.276 0 .5.224.5.5l0 0C12 25.776 11.776 26 11.5 26zM29.5 26h-15c-.276 0-.5-.224-.5-.5l0 0c0-.276.224-.5.5-.5h15c.276 0 .5.224.5.5l0 0C30 25.776 29.776 26 29.5 26zM11.5 30h-2C9.224 30 9 29.776 9 29.5l0 0C9 29.224 9.224 29 9.5 29h2c.276 0 .5.224.5.5l0 0C12 29.776 11.776 30 11.5 30zM29.5 30h-15c-.276 0-.5-.224-.5-.5l0 0c0-.276.224-.5.5-.5h15c.276 0 .5.224.5.5l0 0C30 29.776 29.776 30 29.5 30z"/></svg>

+ 159 - 0
client/themes/default/scss/app.scss

@@ -356,6 +356,148 @@
     }
     }
   }
   }
 
 
+  // ---------------------------------
+  // ASCIIDOC SPECIFIC
+  // ---------------------------------
+
+  .admonitionblock {
+    margin: 1rem 0;
+    position: relative;
+
+    table {
+      border: none;
+      background-color: transparent;
+      width: 100%;
+    }
+
+    td.icon {
+      border-bottom-left-radius: 7px;
+      border-top-left-radius: 7px;
+      text-align: center;
+      width: 56px;
+
+      &::before {
+        display: inline-block;
+        font: normal normal normal 24px/1 "Material Design Icons", sans-serif !important;
+      }
+    }
+
+    td.content {
+      border-bottom-right-radius: 7px;
+      border-top-right-radius: 7px;
+    }
+
+    &.note {
+      td.icon {
+        background-color: mc('blue', '300');
+        color: mc('blue', '50');
+        &::before {
+          content: "\F02FC";
+        }
+      }
+      td.content {
+        color: darken(mc('blue', '900'), 10%);
+        background-color: mc('blue', '50');
+
+        @at-root .theme--dark & {
+          background-color: mc('blue', '900');
+          color: mc('blue', '50');
+        }
+      }
+    }
+    &.tip {
+      td.icon {
+        background-color: mc('green', '300');
+        color: mc('green', '50');
+        &::before {
+          content: "\F0335";
+        }
+      }
+      td.content {
+        color: darken(mc('green', '900'), 10%);
+        background-color: mc('green', '50');
+
+        @at-root .theme--dark & {
+          background-color: mc('green', '900');
+          color: mc('green', '50');
+        }
+      }
+    }
+    &.warning {
+      background-color: transparent !important;
+
+      td.icon {
+        background-color: mc('orange', '300');
+        color: #FFF;
+        &::before {
+          content: "\F0026";
+        }
+      }
+      td.content {
+        color: darken(mc('orange', '900'), 10%);
+        background-color: mc('orange', '50');
+
+        @at-root .theme--dark & {
+          background-color: darken(mc('orange', '900'), 5%);
+          color: mc('orange', '100');
+        }
+      }
+    }
+    &.caution {
+      td.icon {
+        background-color: mc('purple', '300');
+        color: mc('purple', '50');
+        &::before {
+          content: "\f0238";
+        }
+      }
+      td.content {
+        color: darken(mc('purple', '900'), 10%);
+        background-color: mc('purple', '50');
+
+        @at-root .theme--dark & {
+          background-color: mc('purple', '900');
+          color: mc('purple', '100');
+        }
+      }
+    }
+    &.important {
+      td.icon {
+        background-color: mc('red', '300');
+        color: mc('red', '50');
+        &::before {
+          content: "\F0159";
+        }
+      }
+      td.content {
+        color: darken(mc('red', '900'), 10%);
+        background-color: mc('red', '50');
+
+        @at-root .theme--dark & {
+          background-color: mc('red', '900');
+          color: mc('red', '100');
+        }
+      }
+    }
+  }
+
+  .exampleblock {
+    > .title {
+      font-style: italic;
+      font-size: 1rem !important;
+      color: #7a2717;
+
+      @at-root .theme--dark & {
+        color: mc('brown', '300');
+      }
+    }
+    > .content {
+      border: 1px solid mc('grey', '200');
+      border-radius: 7px;
+      margin-bottom: 12px;
+      padding: 16px;
+    }
+  }
   // ---------------------------------
   // ---------------------------------
   // LISTS
   // LISTS
   // ---------------------------------
   // ---------------------------------
@@ -547,6 +689,23 @@
     }
     }
   }
   }
 
 
+  dl {
+    dt {
+      margin-top: 0.3em;
+      margin-bottom: 0.3em;
+      font-weight: bold;
+    }
+
+    dd {
+      margin-left: 1.125em;
+      margin-bottom: 0.75em;
+
+      > p {
+        padding: 0;
+      }
+    }
+  }
+
   // ---------------------------------
   // ---------------------------------
   // CODE
   // CODE
   // ---------------------------------
   // ---------------------------------

+ 2 - 0
package.json

@@ -36,6 +36,7 @@
     "node": ">=10.12"
     "node": ">=10.12"
   },
   },
   "dependencies": {
   "dependencies": {
+    "asciidoctor": "2.2.6",
     "@azure/storage-blob": "12.12.0",
     "@azure/storage-blob": "12.12.0",
     "@exlinc/keycloak-passport": "1.0.2",
     "@exlinc/keycloak-passport": "1.0.2",
     "@joplin/turndown-plugin-gfm": "1.0.45",
     "@joplin/turndown-plugin-gfm": "1.0.45",
@@ -231,6 +232,7 @@
     "clean-webpack-plugin": "3.0.0",
     "clean-webpack-plugin": "3.0.0",
     "clipboard": "2.0.11",
     "clipboard": "2.0.11",
     "codemirror": "5.58.2",
     "codemirror": "5.58.2",
+    "codemirror-asciidoc": "1.0.4",
     "copy-webpack-plugin": "6.2.1",
     "copy-webpack-plugin": "6.2.1",
     "core-js": "3.6.5",
     "core-js": "3.6.5",
     "css-loader": "4.3.0",
     "css-loader": "4.3.0",

+ 1 - 0
server/helpers/page.js

@@ -10,6 +10,7 @@ const unsafeCharsRegex = /[\x00-\x1f\x80-\x9f\\"|<>:*?]/
 
 
 const contentToExt = {
 const contentToExt = {
   markdown: 'md',
   markdown: 'md',
+  asciidoc: 'adoc',
   html: 'html'
   html: 'html'
 }
 }
 const extToContent = _.invert(contentToExt)
 const extToContent = _.invert(contentToExt)

+ 2 - 0
server/models/editors.js

@@ -101,6 +101,8 @@ module.exports = class Editor extends Model {
         return 'markdown'
         return 'markdown'
       case 'html':
       case 'html':
         return 'ckeditor'
         return 'ckeditor'
+      case 'asciidoc':
+        return 'asciidoc'
       default:
       default:
         return 'code'
         return 'code'
     }
     }

+ 6 - 0
server/modules/editor/asciidoc/definition.yml

@@ -0,0 +1,6 @@
+key: asciidoc
+title: Asciidoc
+description: Basic Asciidoc editor
+contentType: asciidoc
+author: dzruyk
+props: {}

+ 20 - 0
server/modules/rendering/asciidoc-core/definition.yml

@@ -0,0 +1,20 @@
+key: asciidocCore
+title: Core
+description: Basic Asciidoc Parser
+author: dzruyk (Based on asciidoctor.js renderer)
+input: asciidoc
+output: html
+icon: mdi-sitemap
+enabledDefault: true
+props:
+  safeMode:
+    type: String
+    default: server
+    title: Safe Mode
+    hint: Sets the safe mode to use when parsing content to HTML.
+    order: 1
+    enum:
+      - unsafe
+      - safe
+      - server
+      - secure

+ 26 - 0
server/modules/rendering/asciidoc-core/renderer.js

@@ -0,0 +1,26 @@
+const asciidoctor = require('asciidoctor')()
+const cheerio = require('cheerio')
+
+module.exports = {
+  async render() {
+    const html = asciidoctor.convert(this.input, {
+      standalone: false,
+      safe: this.config.safeMode,
+      attributes: {
+        showtitle: true,
+        icons: 'font'
+      }
+    })
+
+    const $ = cheerio.load(html, {
+      decodeEntities: true
+    })
+
+    $('pre.highlight > code.language-diagram').each((i, elm) => {
+      const diagramContent = Buffer.from($(elm).html(), 'base64').toString()
+      $(elm).parent().replaceWith(`<pre class="diagram">${diagramContent}</div>`)
+    })
+
+    return $.html()
+  }
+}

+ 71 - 0
yarn.lock

@@ -177,6 +177,21 @@
   dependencies:
   dependencies:
     tslib "~2.0.1"
     tslib "~2.0.1"
 
 
+"@asciidoctor/cli@3.5.0":
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/@asciidoctor/cli/-/cli-3.5.0.tgz#0b0a0204880b325971fb2af33bf490ab67672d8f"
+  integrity sha512-/VMHXcZBnZ9vgWfmqk9Hu0x0gMjPLup0YGq/xA8qCQuk11kUIZNMVQwgSsIUzOEwJqIUD7CgncJdtfwv1Ndxuw==
+  dependencies:
+    yargs "16.2.0"
+
+"@asciidoctor/core@2.2.6":
+  version "2.2.6"
+  resolved "https://registry.yarnpkg.com/@asciidoctor/core/-/core-2.2.6.tgz#a59a9e8ab48ac0a615d5a3200214d3071291c5d5"
+  integrity sha512-TmB2K5UfpDpSbCNBBntXzKHcAk2EA3/P68jmWvmJvglVUdkO9V6kTAuXVe12+h6C4GK0ndwuCrHHtEVcL5t6pQ==
+  dependencies:
+    asciidoctor-opal-runtime "0.3.3"
+    unxhr "1.0.1"
+
 "@azure/abort-controller@^1.0.0":
 "@azure/abort-controller@^1.0.0":
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-1.0.1.tgz#8510935b25ac051e58920300e9d7b511ca6e656a"
   resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-1.0.1.tgz#8510935b25ac051e58920300e9d7b511ca6e656a"
@@ -5168,6 +5183,22 @@ asap@^2.0.0, asap@~2.0.3:
   resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
   resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
   integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
   integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
 
 
+asciidoctor-opal-runtime@0.3.3:
+  version "0.3.3"
+  resolved "https://registry.yarnpkg.com/asciidoctor-opal-runtime/-/asciidoctor-opal-runtime-0.3.3.tgz#2667635f858d3eb3fdfcf6795cf68138e2040174"
+  integrity sha512-/CEVNiOia8E5BMO9FLooo+Kv18K4+4JBFRJp8vUy/N5dMRAg+fRNV4HA+o6aoSC79jVU/aT5XvUpxSxSsTS8FQ==
+  dependencies:
+    glob "7.1.3"
+    unxhr "1.0.1"
+
+asciidoctor@2.2.6:
+  version "2.2.6"
+  resolved "https://registry.yarnpkg.com/asciidoctor/-/asciidoctor-2.2.6.tgz#43b5fec8ab91ed2d8d1815c75067cfa29da2e568"
+  integrity sha512-EXG3+F2pO21B+COfQmV/WgEgGiy7nG/mJiS/o5DXpaT2q82FRZWPVkbMZrpDvpu4pjXe5c754RbZR9Vz0L0Vtw==
+  dependencies:
+    "@asciidoctor/cli" "3.5.0"
+    "@asciidoctor/core" "2.2.6"
+
 asn1.js@^4.0.0:
 asn1.js@^4.0.0:
   version "4.10.1"
   version "4.10.1"
   resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
   resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
@@ -6681,6 +6712,11 @@ code-point-at@^1.0.0:
   resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
   resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
   integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
   integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
 
 
+codemirror-asciidoc@1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/codemirror-asciidoc/-/codemirror-asciidoc-1.0.4.tgz#7439fcc38f30f1408ab144d156bf398ed35b2cc0"
+  integrity sha512-U+G8+ToPONYFGkwTprxpFzV6EV1bCA9zChAA8uT2YAnKFn357JMWL2VkdIPy2yP5N/X13GzslMOGaAk1UNE3rA==
+
 codemirror@5.58.2:
 codemirror@5.58.2:
   version "5.58.2"
   version "5.58.2"
   resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.58.2.tgz#ed54a1796de1498688bea1cdd4e9eeb187565d1b"
   resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.58.2.tgz#ed54a1796de1498688bea1cdd4e9eeb187565d1b"
@@ -10252,6 +10288,18 @@ glob-parent@~5.1.2:
   dependencies:
   dependencies:
     is-glob "^4.0.1"
     is-glob "^4.0.1"
 
 
+glob@7.1.3:
+  version "7.1.3"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
+  integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
 glob@^6.0.1:
 glob@^6.0.1:
   version "6.0.4"
   version "6.0.4"
   resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
   resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
@@ -19425,6 +19473,11 @@ untildify@^4.0.0:
   resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
   resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
   integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==
   integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==
 
 
+unxhr@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/unxhr/-/unxhr-1.0.1.tgz#92200322d66c728993de771f9e01eeb21f41bc7b"
+  integrity sha512-MAhukhVHyaLGDjyDYhy8gVjWJyhTECCdNsLwlMoGFoNJ3o79fpQhtQuzmAE4IxCMDwraF4cW8ZjpAV0m9CRQbg==
+
 upath@^1.1.1:
 upath@^1.1.1:
   version "1.2.0"
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
   resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
@@ -20445,6 +20498,11 @@ y18n@^5.0.2:
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18"
   integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==
   integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==
 
 
+y18n@^5.0.5:
+  version "5.0.8"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
+  integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
+
 yaeti@^0.0.6:
 yaeti@^0.0.6:
   version "0.0.6"
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577"
   resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577"
@@ -20514,6 +20572,19 @@ yargs@16.1.0:
     y18n "^5.0.2"
     y18n "^5.0.2"
     yargs-parser "^20.2.2"
     yargs-parser "^20.2.2"
 
 
+yargs@16.2.0:
+  version "16.2.0"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
+  integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
+  dependencies:
+    cliui "^7.0.2"
+    escalade "^3.1.1"
+    get-caller-file "^2.0.5"
+    require-directory "^2.1.1"
+    string-width "^4.2.0"
+    y18n "^5.0.5"
+    yargs-parser "^20.2.2"
+
 yargs@^13.3.2:
 yargs@^13.3.2:
   version "13.3.2"
   version "13.3.2"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"