| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141 | <template lang='pug'>  .editor-markdown    v-toolbar.editor-markdown-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-tooltip(bottom, color='primary')          template(v-slot:activator='{ on }')            v-btn.animated.fadeIn.wait-p2s(icon, tile, v-on='on', @click='toggleMarkup({ start: `~~` })').mx-0              v-icon mdi-format-strikethrough          span {{$t('editor:markup.strikethrough')}}        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: `> `, after: `{.is-info}`})')              v-list-item-action                v-icon(color='blue') mdi-alpha-i-box-outline              v-list-item-title {{$t('editor:markup.blockquoteInfo')}}            v-divider            v-list-item(@click='insertBeforeEachLine({ content: `> `, after: `{.is-success}`})')              v-list-item-action                v-icon(color='success') mdi-alpha-s-box-outline              v-list-item-title {{$t('editor:markup.blockquoteSuccess')}}            v-divider            v-list-item(@click='insertBeforeEachLine({ content: `> `, after: `{.is-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: `> `, after: `{.is-danger}`})')              v-list-item-action                v-icon(color='error') mdi-alpha-e-box-outline              v-list-item-title {{$t('editor:markup.blockquoteError')}}            v-divider        v-tooltip(bottom, color='primary')          template(v-slot:activator='{ on }')            v-btn.animated.fadeIn.wait-p7s(icon, tile, v-on='on', @click='insertBeforeEachLine({ content: `- `})').mx-0              v-icon mdi-format-list-bulleted          span {{$t('editor:markup.unorderedList')}}        v-tooltip(bottom, color='primary')          template(v-slot:activator='{ on }')            v-btn.animated.fadeIn.wait-p8s(icon, tile, v-on='on', @click='insertBeforeEachLine({ content: `1. `})').mx-0              v-icon mdi-format-list-numbered          span {{$t('editor:markup.orderedList')}}        v-tooltip(bottom, color='primary')          template(v-slot:activator='{ on }')            v-btn.animated.fadeIn.wait-p9s(icon, tile, v-on='on', @click='toggleMarkup({ start: "`" })').mx-0              v-icon mdi-code-tags          span {{$t('editor:markup.inlineCode')}}        v-tooltip(bottom, color='primary')          template(v-slot:activator='{ on }')            v-btn.animated.fadeIn.wait-p10s(icon, tile, v-on='on', @click='toggleMarkup({ start: `<kbd>`, end: `</kbd>` })').mx-0              v-icon mdi-keyboard-variant          span {{$t('editor:markup.keyboardKey')}}        v-tooltip(bottom, color='primary')          template(v-slot:activator='{ on }')            v-btn.animated.fadeIn.wait-p11s(icon, tile, v-on='on', @click='insertAfter({ content: `---`, newLine: true })').mx-0              v-icon mdi-minus          span {{$t('editor:markup.horizontalBar')}}        template(v-if='$vuetify.breakpoint.mdAndUp')          v-spacer          v-tooltip(bottom, color='primary', v-if='previewShown')            template(v-slot:activator='{ on }')              v-btn.animated.fadeIn.wait-p1s(icon, tile, v-on='on', @click='spellModeActive = !spellModeActive').mx-0                v-icon(:color='spellModeActive ? `amber` : `white`') mdi-spellcheck            span {{$t('editor:markup.toggleSpellcheck')}}          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-markdown-main      .editor-markdown-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-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-icon mdi-chart-multiline          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')          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')}}          v-tooltip(right, color='teal')            template(v-slot:activator='{ on }')              v-btn.mt-3.animated.fadeInLeft.wait-p9s(icon, tile, v-on='on', dark, @click='toggleHelp').mx-0                v-icon(:color='helpShown ? `teal` : ``') mdi-help-circle            span {{$t('editor:markup.markdownFormattingHelp')}}      .editor-markdown-editor        textarea(ref='cm')      transition(name='editor-markdown-preview')        .editor-markdown-preview(v-if='previewShown')          .editor-markdown-preview-content.contents(ref='editorPreviewContainer')            div(              ref='editorPreview'              v-html='previewHTML'              :spellcheck='spellModeActive'              :contenteditable='spellModeActive'              @blur='spellModeActive = false'              )    v-system-bar.editor-markdown-sysbar(dark, status, color='grey darken-3')      .caption.editor-markdown-sysbar-locale {{locale.toUpperCase()}}      .caption.px-3 /{{path}}      template(v-if='$vuetify.breakpoint.mdAndUp')        v-spacer        .caption Markdown        v-spacer        .caption Ln {{cursorPos.line + 1}}, Col {{cursorPos.ch + 1}}    markdown-help(v-if='helpShown')    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 markdownHelp from './markdown/help.vue'import gql from 'graphql-tag'import DOMPurify from 'dompurify'/* global siteConfig, siteLangs */// ========================================// IMPORTS// ========================================// Code Mirrorimport CodeMirror from 'codemirror'import 'codemirror/lib/codemirror.css'// Languageimport 'codemirror/mode/markdown/markdown.js'// Addonsimport '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 './markdown/fold'// Markdown-itimport MarkdownIt from 'markdown-it'import mdAttrs from 'markdown-it-attrs'import mdEmoji from 'markdown-it-emoji'import mdTaskLists from 'markdown-it-task-lists'import mdExpandTabs from 'markdown-it-expand-tabs'import mdAbbr from 'markdown-it-abbr'import mdSup from 'markdown-it-sup'import mdSub from 'markdown-it-sub'import mdMark from 'markdown-it-mark'import mdMultiTable from 'markdown-it-multimd-table'import mdFootnote from 'markdown-it-footnote'import mdImsize from 'markdown-it-imsize'import katex from 'katex'import underline from '../../libs/markdown-it-underline'import 'katex/dist/contrib/mhchem'import twemoji from 'twemoji'import plantuml from './markdown/plantuml'// Prism (Syntax Highlighting)import Prism from 'prismjs'// Mermaidimport mermaid from 'mermaid'// Helpersimport katexHelper from './common/katex'import tabsetHelper from './markdown/tabset'// ========================================// INIT// ========================================// Platform detectionconst CtrlKey = /Mac/.test(navigator.platform) ? 'Cmd' : 'Ctrl'// Prism ConfigPrism.plugins.autoloader.languages_path = '/_assets/js/prism/'Prism.plugins.NormalizeWhitespace.setDefaults({  'remove-trailing': true,  'remove-indent': true,  'left-trim': true,  'right-trim': true,  'remove-initial-line-feed': true,  'tabs-to-spaces': 2})// Markdown Instanceconst md = new MarkdownIt({  html: true,  breaks: true,  linkify: true,  typography: true,  highlight(str, lang) {    if (lang === 'diagram') {      return `<pre class="diagram">` + Buffer.from(str, 'base64').toString() + `</pre>`    } else 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, {    allowedAttributes: ['id', 'class', 'target']  })  .use(underline)  .use(mdEmoji)  .use(mdTaskLists, {label: true, labelAfter: true})  .use(mdExpandTabs)  .use(mdAbbr)  .use(mdSup)  .use(mdSub)  .use(mdMultiTable, {multiline: true, rowspan: true, headerless: true})  .use(mdMark)  .use(mdFootnote)  .use(mdImsize)// DOMPurify fix for draw.ioDOMPurify.addHook('uponSanitizeElement', (elm) => {  if (elm.querySelectorAll) {    const breaks = elm.querySelectorAll('foreignObject br, foreignObject p')    if (breaks && breaks.length) {      for (let i = 0; i < breaks.length; i++) {        breaks[i].parentNode.replaceChild(          document.createElement('div'),          breaks[i]        )      }    }  }})// ========================================// HELPER FUNCTIONS// ========================================// Inject line numbers for preview scroll synclet linesMap = []function injectLineNumbers (tokens, idx, options, env, slf) {  let line  if (tokens[idx].map && tokens[idx].level === 0) {    line = tokens[idx].map[0]    tokens[idx].attrJoin('class', 'line')    tokens[idx].attrSet('data-line', String(line))    linesMap.push(line)  }  return slf.renderToken(tokens, idx, options, env, slf)}md.renderer.rules.paragraph_open = injectLineNumbersmd.renderer.rules.heading_open = injectLineNumbersmd.renderer.rules.blockquote_open = injectLineNumbers// ========================================// PLANTUML// ========================================// TODO: Use same options as defined in backendplantuml.init(md, {})// ========================================// KATEX// ========================================md.inline.ruler.after('escape', 'katex_inline', katexHelper.katexInline)md.renderer.rules.katex_inline = (tokens, idx) => {  try {    return katex.renderToString(tokens[idx].content, {      displayMode: false    })  } catch (err) {    console.warn(err)    return tokens[idx].content  }}md.block.ruler.after('blockquote', 'katex_block', katexHelper.katexBlock, {  alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]})md.renderer.rules.katex_block = (tokens, idx) => {  try {    return `<p>` + katex.renderToString(tokens[idx].content, {      displayMode: true    }) + `</p>`  } catch (err) {    console.warn(err)    return tokens[idx].content  }}// ========================================// TWEMOJI// ========================================md.renderer.rules.emoji = (token, idx) => {  return twemoji.parse(token[idx].content, {    callback (icon, opts) {      return `/_assets/svg/twemoji/${icon}.svg`    }  })}// ========================================// Vue Component// ========================================let mermaidId = 0export default {  components: {    markdownHelp  },  props: {    save: {      type: Function,      default: () => {}    }  },  data() {    return {      fabInsertMenu: false,      cm: null,      cursorPos: { ch: 0, line: 1 },      previewShown: true,      previewHTML: '',      helpShown: false,      spellModeActive: false,      insertLinkDialog: false    }  },  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')  },  watch: {    previewShown (newValue, oldValue) {      if (newValue && !oldValue) {        this.$nextTick(() => {          this.renderMermaidDiagrams()          Prism.highlightAllUnder(this.$refs.editorPreview)          Array.from(this.$refs.editorPreview.querySelectorAll('pre.line-numbers')).forEach(pre => pre.classList.add('prismjs'))        })      }    },    spellModeActive (newValue, oldValue) {      if (newValue) {        this.$nextTick(() => {          this.$refs.editorPreview.focus()        })      }    }  },  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),    onCmPaste (cm, ev) {      // const clipItems = (ev.clipboardData || ev.originalEvent.clipboardData).items      // for (let clipItem of clipItems) {      //   if (_.startsWith(clipItem.type, 'image/')) {      //     const file = clipItem.getAsFile()      //     const reader = new FileReader()      //     reader.onload = evt => {      //       this.$store.commit(`loadingStart`, 'editor-paste-image')      //       this.insertAfter({      //         content: ``,      //         newLine: true      //       })      //     }      //     reader.readAsDataURL(file)      //   }      // }    },    processContent (newContent) {      linesMap = []      // this.$store.set('editor/content', newContent)      this.processMarkers(this.cm.firstLine(), this.cm.lastLine())      this.previewHTML = DOMPurify.sanitize(md.render(newContent), {        ADD_TAGS: ['foreignObject']      })      this.$nextTick(() => {        tabsetHelper.format()        this.renderMermaidDiagrams()        Prism.highlightAllUnder(this.$refs.editorPreview)        Array.from(this.$refs.editorPreview.querySelectorAll('pre.line-numbers')).forEach(pre => pre.classList.add('prismjs'))        this.scrollSync(this.cm)      })    },    /**     * Update cursor state     */    positionSync(cm) {      this.cursorPos = cm.getCursor('head')    },    /**     * Wrap selection with start / end tags     */    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))    },    /**     * Set current line as header     */    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 })    },    /**     * Get the header lever of the current line     */    getHeaderLevel(cm) {      const curLine = this.cm.doc.getCursor('head').line      let lineContent = this.cm.doc.getLine(curLine)      let lvl = 0      const result = lineContent.match(/^(#+) /)      if (result) {        lvl = _.get(result, '[1]', '').length      }      return lvl    },    /**     * 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 scroll sync     */    scrollSync: _.debounce(function (cm) {      if (!this.previewShown || cm.somethingSelected()) { return }      let currentLine = cm.getCursor().line      if (currentLine < 3) {        this.Velocity(this.$refs.editorPreview, 'stop', true)        this.Velocity(this.$refs.editorPreview.firstChild, 'scroll', { offset: '-50', duration: 1000, container: this.$refs.editorPreviewContainer })      } else {        let closestLine = _.findLast(linesMap, n => n <= currentLine)        let destElm = this.$refs.editorPreview.querySelector(`[data-line='${closestLine}']`)        if (destElm) {          this.Velocity(this.$refs.editorPreview, 'stop', true)          this.Velocity(destElm, 'scroll', { offset: '-100', duration: 1000, container: this.$refs.editorPreviewContainer })        }      }    }, 500),    toggleHelp () {      this.helpShown = !this.helpShown      this.activeModal = ''    },    toggleFullscreen () {      this.cm.setOption('fullScreen', true)    },    refresh() {      this.$nextTick(() => {        this.cm.refresh()      })    },    renderMermaidDiagrams () {      document.querySelectorAll('.editor-markdown-preview pre.codeblock-mermaid > code').forEach(elm => {        mermaidId++        const mermaidDef = elm.innerText        const mmElm = document.createElement('div')        mmElm.innerHTML = `<div id="mermaid-id-${mermaidId}">${mermaid.render(`mermaid-id-${mermaidId}`, mermaidDef)}</div>`        elm.parentElement.replaceWith(mmElm)      })    },    autocomplete (cm, change) {      if (cm.getModeAt(cm.getCursor()).name !== 'markdown') {        return      }      // Links      if (change.text[0] === '(') {        const curLine = cm.getLine(change.from.line).substring(0, change.from.ch)        if (curLine[curLine.length - 1] === ']') {          cm.showHint({            hint: async (cm, options) => {              const cur = cm.getCursor()              const curLine = cm.getLine(cur.line).substring(0, cur.ch)              const queryString = curLine.substring(curLine.lastIndexOf('[')+1,curLine.length-2)              const token = cm.getTokenAt(cur)              try {                const respRaw = await this.$apollo.query({                  query: gql`                    query ($query: String!, $locale: String) {                      pages {                        search(query:$query, locale:$locale) {                          results {                            title                            path                            locale                          }                          totalHits                        }                      }                    }                  `,                  variables: {                    query: queryString,                    locale: this.locale                  },                  fetchPolicy: 'cache-first'                })                const resp = _.get(respRaw, 'data.pages.search', {})                if (resp && resp.totalHits > 0) {                  return {                    list: resp.results.map(r => ({                      text: '(' + (siteLangs.length > 0 ? `/${r.locale}/${r.path}` : `/${r.path}`) + ')',                      displayText: siteLangs.length > 0 ? `/${r.locale}/${r.path} - ${r.title}` : `/${r.path} - ${r.title}`                    })),                    from: CodeMirror.Pos(cur.line, token.start),                    to: CodeMirror.Pos(cur.line, token.end)                  }                }              } catch (err) {}              return {                list: [],                from: CodeMirror.Pos(cur.line, token.start),                to: CodeMirror.Pos(cur.line, token.end)              }            }          })        }      }    },    insertLink () {      this.insertLinkDialog = true    },    insertLinkHandler ({ locale, path }) {      const lastPart = _.last(path.split('/'))      this.insertAtCursor({        content: siteLangs.length > 0 ? `[${lastPart}](/${locale}/${path})` : `[${lastPart}](/${path})`      })    },    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', 'markdown')    if (this.mode === 'create' && !this.$store.get('editor/content')) {      this.$store.set('editor/content', '# Header\nYour content here')    }    // Initialize Mermaid API    mermaid.initialize({      startOnLoad: false,      theme: this.$vuetify.theme.dark ? `dark` : `default`    })    // Initialize CodeMirror    this.cm = CodeMirror.fromTextArea(this.$refs.cm, {      tabSize: 2,      mode: 'text/markdown',      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 - 112px - 24px)')    } 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}-S`, c => {      this.save()      return false    })    _.set(keyBindings, `${CtrlKey}-B`, c => {      this.toggleMarkup({ start: `**` })      return false    })    _.set(keyBindings, `${CtrlKey}-I`, c => {      this.toggleMarkup({ start: `*` })      return false    })    _.set(keyBindings, `${CtrlKey}-Alt-Right`, c => {      let lvl = this.getHeaderLevel(c)      if (lvl >= 6) { lvl = 5 }      this.setHeaderLine(lvl + 1)      return false    })    _.set(keyBindings, `${CtrlKey}-Alt-Left`, c => {      let lvl = this.getHeaderLevel(c)      if (lvl <= 1) { lvl = 2 }      this.setHeaderLine(lvl - 1)      return false    })    this.cm.setOption('extraKeys', keyBindings)    this.cm.on('inputRead', this.autocomplete)    // Handle cursor movement    this.cm.on('cursorActivity', c => {      this.positionSync(c)      this.scrollSync(c)    })    // Handle special paste    this.cm.on('paste', this.onCmPaste)    // Render initial preview    this.processContent(this.$store.get('editor/content'))    this.refresh()    this.$root.$on('editorInsert', opts => {      switch (opts.kind) {        case 'IMAGE':          let img = ``          if (opts.align && opts.align !== '') {            img += `{.align-${opts.align}}`          }          this.insertAtCursor({            content: img          })          break        case 'BINARY':          this.insertAtCursor({            content: `[${opts.text}](${opts.path})`          })          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-height: calc(100vh - 112px - 24px);$editor-height-mobile: calc(100vh - 112px - 16px);.editor-markdown {  &-main {    display: flex;    width: 100%;  }  &-editor {    background-color: darken(mc('grey', '900'), 4.5%);    flex: 1 1 50%;    display: block;    height: $editor-height;    position: relative;    @include until($tablet) {      height: $editor-height-mobile;    }  }  &-preview {    flex: 1 1 50%;    background-color: mc('grey', '100');    position: relative;    height: $editor-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-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-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');  }}// HINT DROPDOWN.CodeMirror-hints {  position: absolute;  z-index: 10;  overflow: hidden;  list-style: none;  margin: 0;  padding: 1px;  box-shadow: 2px 3px 5px rgba(0,0,0,.2);  border: 1px solid mc('grey', '700');  background: mc('grey', '900');  font-family: 'Roboto Mono', monospace;  font-size: .9rem;  max-height: 150px;  overflow-y: auto;  min-width: 250px;  max-width: 80vw;}.CodeMirror-hint {  margin: 0;  padding: 0 4px;  white-space: pre;  color: #FFF;  cursor: pointer;}li.CodeMirror-hint-active {  background: mc('blue', '500');  color: #FFF;}</style>
 |