feat: markdown editor renderer

pull/6775/head
NGPixel 2 years ago
parent d2a18eca3c
commit 50054dfa4f
No known key found for this signature in database
GPG Key ID: B755FB6870B30F63

@ -8,7 +8,7 @@
"dev": true,
"scripts": {
"start": "cd .. && node server",
"dev": "cd .. && nodemon server"
"dev": "cd .. && nodemon server --watch server --ext js,json,graphql,gql"
},
"repository": {
"type": "git",

175
ux/package-lock.json generated

@ -50,9 +50,24 @@
"graphql-tag": "2.12.6",
"js-cookie": "3.0.1",
"jwt-decode": "3.1.2",
"katex": "0.16.4",
"lodash-es": "4.17.21",
"lowlight": "2.8.1",
"luxon": "3.3.0",
"markdown-it": "13.0.1",
"markdown-it-abbr": "1.0.4",
"markdown-it-attrs": "4.1.6",
"markdown-it-decorate": "1.2.2",
"markdown-it-emoji": "2.0.2",
"markdown-it-expand-tabs": "1.0.13",
"markdown-it-footnote": "3.0.3",
"markdown-it-imsize": "2.0.1",
"markdown-it-mark": "3.0.1",
"markdown-it-multimd-table": "4.2.1",
"markdown-it-sub": "1.0.0",
"markdown-it-sup": "1.0.0",
"markdown-it-task-lists": "2.1.1",
"pako": "2.1.0",
"pinia": "2.0.33",
"prosemirror-commands": "1.5.1",
"prosemirror-history": "1.3.0",
@ -68,6 +83,7 @@
"socket.io-client": "4.6.1",
"tabulator-tables": "5.4.4",
"tippy.js": "6.3.7",
"twemoji": "14.0.2",
"uuid": "9.0.0",
"v-network-graph": "0.9.1",
"vue": "3.2.47",
@ -4351,7 +4367,6 @@
},
"node_modules/graceful-fs": {
"version": "4.2.10",
"dev": true,
"license": "ISC"
},
"node_modules/grapheme-splitter": {
@ -5149,6 +5164,29 @@
"version": "3.1.2",
"license": "MIT"
},
"node_modules/katex": {
"version": "0.16.4",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.4.tgz",
"integrity": "sha512-WudRKUj8yyBeVDI4aYMNxhx5Vhh2PjpzQw1GRu/LVGqL4m1AxwD1GcUp0IMbdJaf5zsjtj8ghP0DOQRYhroNkw==",
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"
],
"dependencies": {
"commander": "^8.0.0"
},
"bin": {
"katex": "cli.js"
}
},
"node_modules/katex/node_modules/commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"engines": {
"node": ">= 12"
}
},
"node_modules/kind-of": {
"version": "6.0.3",
"dev": true,
@ -5260,6 +5298,11 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.repeat": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/lodash.repeat/-/lodash.repeat-4.1.0.tgz",
"integrity": "sha512-eWsgQW89IewS95ZOcr15HHCX6FVDxq3f2PNUIng3fyzsPev9imFQxIYdFZ6crl8L56UR6ZlGDLcEb3RZsCSSqw=="
},
"node_modules/lodash.truncate": {
"version": "4.4.2",
"dev": true,
@ -5361,6 +5404,75 @@
"markdown-it": "bin/markdown-it.js"
}
},
"node_modules/markdown-it-abbr": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/markdown-it-abbr/-/markdown-it-abbr-1.0.4.tgz",
"integrity": "sha512-ZeA4Z4SaBbYysZap5iZcxKmlPL6bYA8grqhzJIHB1ikn7njnzaP8uwbtuXc4YXD5LicI4/2Xmc0VwmSiFV04gg=="
},
"node_modules/markdown-it-attrs": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/markdown-it-attrs/-/markdown-it-attrs-4.1.6.tgz",
"integrity": "sha512-O7PDKZlN8RFMyDX13JnctQompwrrILuz2y43pW2GagcwpIIElkAdfeek+erHfxUOlXWPsjFeWmZ8ch1xtRLWpA==",
"engines": {
"node": ">=6"
},
"peerDependencies": {
"markdown-it": ">= 9.0.0"
}
},
"node_modules/markdown-it-decorate": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/markdown-it-decorate/-/markdown-it-decorate-1.2.2.tgz",
"integrity": "sha512-7BFWJ97KBXgkaPVjKHISQnhSW8RWQ7yRNXpr8pPUV2Rw4GHvGrgb6CelKCM+GSijP0uSLCAVfc/knWIz+2v/Sw=="
},
"node_modules/markdown-it-emoji": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-2.0.2.tgz",
"integrity": "sha512-zLftSaNrKuYl0kR5zm4gxXjHaOI3FAOEaloKmRA5hijmJZvSjmxcokOLlzycb/HXlUFWzXqpIEoyEMCE4i9MvQ=="
},
"node_modules/markdown-it-expand-tabs": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/markdown-it-expand-tabs/-/markdown-it-expand-tabs-1.0.13.tgz",
"integrity": "sha512-ODpk98FWkGIq2vkwm2NOLt4G6TRgy3M9eTa9SFm06pUyOd0zjjYAwkhsjiCDU42pzKuz0ChiwBO0utuOj3LNOA==",
"dependencies": {
"lodash.repeat": "^4.0.0"
}
},
"node_modules/markdown-it-footnote": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/markdown-it-footnote/-/markdown-it-footnote-3.0.3.tgz",
"integrity": "sha512-YZMSuCGVZAjzKMn+xqIco9d1cLGxbELHZ9do/TSYVzraooV8ypsppKNmUJ0fVH5ljkCInQAtFpm8Rb3eXSrt5w=="
},
"node_modules/markdown-it-imsize": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/markdown-it-imsize/-/markdown-it-imsize-2.0.1.tgz",
"integrity": "sha512-5SH90ademqcR8ifQCBXRCfIR4HGfZZOh5pO0j2TglulfSQH+SBXM4Iw/QlTUbSoUwVZArCYgECoMvktDS2kP3w=="
},
"node_modules/markdown-it-mark": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-3.0.1.tgz",
"integrity": "sha512-HyxjAu6BRsdt6Xcv6TKVQnkz/E70TdGXEFHRYBGLncRE9lBFwDNLVtFojKxjJWgJ+5XxUwLaHXy+2sGBbDn+4A=="
},
"node_modules/markdown-it-multimd-table": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/markdown-it-multimd-table/-/markdown-it-multimd-table-4.2.1.tgz",
"integrity": "sha512-0WEkr2Siw1I9TFaKEHwCXDRxIXWmuzht496Mb8yCkFnK+OVDqMSN6k5/FwyKlZIMtYNOK02e8o0uh3H0WMqstQ=="
},
"node_modules/markdown-it-sub": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/markdown-it-sub/-/markdown-it-sub-1.0.0.tgz",
"integrity": "sha512-z2Rm/LzEE1wzwTSDrI+FlPEveAAbgdAdPhdWarq/ZGJrGW/uCQbKAnhoCsE4hAbc3SEym26+W2z/VQB0cQiA9Q=="
},
"node_modules/markdown-it-sup": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz",
"integrity": "sha512-E32m0nV9iyhRR7CrhnzL5msqic7rL1juWre6TQNxsnApg7Uf+F97JOKxUijg5YwXz86lZ0mqfOnutoryyNdntQ=="
},
"node_modules/markdown-it-task-lists": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz",
"integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA=="
},
"node_modules/mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
@ -5789,6 +5901,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
},
"node_modules/param-case": {
"version": "2.1.1",
"dev": true,
@ -7237,6 +7354,62 @@
"version": "2.3.1",
"license": "0BSD"
},
"node_modules/twemoji": {
"version": "14.0.2",
"resolved": "https://registry.npmjs.org/twemoji/-/twemoji-14.0.2.tgz",
"integrity": "sha512-BzOoXIe1QVdmsUmZ54xbEH+8AgtOKUiG53zO5vVP2iUu6h5u9lN15NcuS6te4OY96qx0H7JK9vjjl9WQbkTRuA==",
"dependencies": {
"fs-extra": "^8.0.1",
"jsonfile": "^5.0.0",
"twemoji-parser": "14.0.0",
"universalify": "^0.1.2"
}
},
"node_modules/twemoji-parser": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/twemoji-parser/-/twemoji-parser-14.0.0.tgz",
"integrity": "sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA=="
},
"node_modules/twemoji/node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
},
"engines": {
"node": ">=6 <7 || >=8"
}
},
"node_modules/twemoji/node_modules/fs-extra/node_modules/jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/twemoji/node_modules/jsonfile": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-5.0.0.tgz",
"integrity": "sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==",
"dependencies": {
"universalify": "^0.1.2"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/twemoji/node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/type-check": {
"version": "0.4.0",
"dev": true,

@ -53,9 +53,24 @@
"graphql-tag": "2.12.6",
"js-cookie": "3.0.1",
"jwt-decode": "3.1.2",
"katex": "0.16.4",
"lodash-es": "4.17.21",
"lowlight": "2.8.1",
"luxon": "3.3.0",
"markdown-it": "13.0.1",
"markdown-it-abbr": "1.0.4",
"markdown-it-attrs": "4.1.6",
"markdown-it-decorate": "1.2.2",
"markdown-it-emoji": "2.0.2",
"markdown-it-expand-tabs": "1.0.13",
"markdown-it-footnote": "3.0.3",
"markdown-it-imsize": "2.0.1",
"markdown-it-mark": "3.0.1",
"markdown-it-multimd-table": "4.2.1",
"markdown-it-sub": "1.0.0",
"markdown-it-sup": "1.0.0",
"markdown-it-task-lists": "2.1.1",
"pako": "2.1.0",
"pinia": "2.0.33",
"prosemirror-commands": "1.5.1",
"prosemirror-history": "1.3.0",
@ -71,6 +86,7 @@
"socket.io-client": "4.6.1",
"tabulator-tables": "5.4.4",
"tippy.js": "6.3.7",
"twemoji": "14.0.2",
"uuid": "9.0.0",
"v-network-graph": "0.9.1",
"vue": "3.2.47",

@ -236,9 +236,10 @@
import { reactive, ref, shallowRef, nextTick, onBeforeMount, onMounted, watch } from 'vue'
import { useMeta, useQuasar, setCssVar } from 'quasar'
import { useI18n } from 'vue-i18n'
import { get, flatten, last, times, startsWith } from 'lodash-es'
import { get, flatten, last, times, startsWith, debounce } from 'lodash-es'
import { useEditorStore } from 'src/stores/editor'
import { usePageStore } from 'src/stores/page'
import { useSiteStore } from 'src/stores/site'
// Code Mirror
@ -260,6 +261,9 @@ import 'codemirror/addon/fold/foldcode.js'
import 'codemirror/addon/fold/foldgutter.js'
import 'codemirror/addon/fold/foldgutter.css'
// Markdown Renderer
import { MarkdownRenderer } from 'src/renderers/markdown'
// QUASAR
const $q = useQuasar()
@ -267,6 +271,7 @@ const $q = useQuasar()
// STORES
const editorStore = useEditorStore()
const pageStore = usePageStore()
const siteStore = useSiteStore()
// I18N
@ -279,12 +284,13 @@ const cm = shallowRef(null)
const cmRef = ref(null)
const state = reactive({
content: '',
previewShown: true,
previewHTML: '',
previewScrollSync: true
})
const md = new MarkdownRenderer({})
// Platform detection
const CtrlKey = /Mac/.test(navigator.platform) ? 'Cmd' : 'Ctrl'
@ -396,6 +402,12 @@ function toggleMarkup ({ start, end }) {
cm.value.doc.replaceSelections(cm.value.doc.getSelections().map(s => start + s + end))
}
const onCmInput = debounce(processContent, 600)
function processContent (newContent) {
state.previewHTML = md.render(newContent)
}
// MOUNTED
onMounted(async () => {
@ -424,12 +436,12 @@ onMounted(async () => {
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter']
})
cm.value.setValue(state.content)
cm.value.setValue(pageStore.content)
cm.value.on('change', c => {
editorStore.$patch({
pageStore.$patch({
content: c.getValue()
})
// onCmInput(editorStore.content)
onCmInput(pageStore.content)
})
cm.value.setSize(null, '100%')

@ -199,6 +199,16 @@ const editUrl = computed(() => {
// METHODS
async function discardChanges () {
// Is it the home page in create mode?
if (editorStore.mode === 'create' && pageStore.path === '' && pageStore.locale === 'en') {
editorStore.$patch({
isActive: false,
editor: ''
})
siteStore.overlay = 'Welcome'
return
}
$q.loading.show()
try {
editorStore.$patch({

@ -90,7 +90,14 @@ useMeta({
function createHomePage (editor) {
siteStore.overlay = ''
pageStore.pageCreate({ editor, locale: 'en', path: '' })
pageStore.pageCreate({
editor,
locale: 'en',
path: '',
title: t('welcome.homeDefault.title'),
description: t('welcome.homeDefault.description'),
content: t('welcome.homeDefault.content')
})
}
function loadAdmin () {

@ -1732,6 +1732,9 @@
"tags.selectOneMoreTagsHint": "Select one or more tags on the left.",
"welcome.admin": "Administration Area",
"welcome.createHome": "Create the homepage",
"welcome.homeDefault.content": "Write some content here...",
"welcome.homeDefault.description": "Welcome to my wiki!",
"welcome.homeDefault.title": "Home",
"welcome.subtitle": "Let's get started...",
"welcome.title": "Welcome to Wiki.js!"
}

@ -0,0 +1,99 @@
import MarkdownIt from 'markdown-it'
import mdAttrs from 'markdown-it-attrs'
import mdDecorate from 'markdown-it-decorate'
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 './modules/markdown-it-underline'
import 'katex/dist/contrib/mhchem'
import twemoji from 'twemoji'
import plantuml from './modules/plantuml'
import katexHelper from './modules/katex'
import { escape } from 'lodash-es'
export class MarkdownRenderer {
constructor (conf = {}) {
this.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(mdDecorate)
.use(underline)
.use(mdEmoji)
.use(mdTaskLists, { label: false, labelAfter: false })
.use(mdExpandTabs)
.use(mdAbbr)
.use(mdSup)
.use(mdSub)
.use(mdMultiTable, { multiline: true, rowspan: true, headerless: true })
.use(mdMark)
.use(mdFootnote)
// .use(mdImsize)
// -> PLANTUML
plantuml.init(this.md, {})
// -> KATEX
const macros = {}
this.md.inline.ruler.after('escape', 'katex_inline', katexHelper.katexInline)
this.md.renderer.rules.katex_inline = (tokens, idx) => {
try {
return katex.renderToString(tokens[idx].content, {
displayMode: false, macros
})
} catch (err) {
console.warn(err)
return tokens[idx].content
}
}
this.md.block.ruler.after('blockquote', 'katex_block', katexHelper.katexBlock, {
alt: ['paragraph', 'reference', 'blockquote', 'list']
})
this.md.renderer.rules.katex_block = (tokens, idx) => {
try {
return '<p>' + katex.renderToString(tokens[idx].content, {
displayMode: true, macros
}) + '</p>'
} catch (err) {
console.warn(err)
return tokens[idx].content
}
}
// -> TWEMOJI
this.md.renderer.rules.emoji = (token, idx) => {
return twemoji.parse(token[idx].content, {
callback (icon, opts) {
return `/_assets/svg/twemoji/${icon}.svg`
}
})
}
}
render (src) {
return this.md.render(src)
}
}

@ -0,0 +1,145 @@
// Test if potential opening or closing delimieter
// Assumes that there is a "$" at state.src[pos]
function isValidDelim (state, pos) {
const max = state.posMax
let canOpen = true
let canClose = true
const prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1
const nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1
// Check non-whitespace conditions for opening and closing, and
// check that closing delimeter isn't followed by a number
if (prevChar === 0x20/* " " */ || prevChar === 0x09/* \t */ ||
(nextChar >= 0x30/* "0" */ && nextChar <= 0x39/* "9" */)) {
canClose = false
}
if (nextChar === 0x20/* " " */ || nextChar === 0x09/* \t */) {
canOpen = false
}
return {
canOpen,
canClose
}
}
export default {
katexInline (state, silent) {
let match, token, res, pos
if (state.src[state.pos] !== '$') { return false }
res = isValidDelim(state, state.pos)
if (!res.canOpen) {
if (!silent) { state.pending += '$' }
state.pos += 1
return true
}
// First check for and bypass all properly escaped delimieters
// This loop will assume that the first leading backtick can not
// be the first character in state.src, which is known since
// we have found an opening delimieter already.
const start = state.pos + 1
match = start
while ((match = state.src.indexOf('$', match)) !== -1) {
// Found potential $, look for escapes, pos will point to
// first non escape when complete
pos = match - 1
while (state.src[pos] === '\\') { pos -= 1 }
// Even number of escapes, potential closing delimiter found
if (((match - pos) % 2) === 1) { break }
match += 1
}
// No closing delimter found. Consume $ and continue.
if (match === -1) {
if (!silent) { state.pending += '$' }
state.pos = start
return true
}
// Check if we have empty content, ie: $$. Do not parse.
if (match - start === 0) {
if (!silent) { state.pending += '$$' }
state.pos = start + 1
return true
}
// Check for valid closing delimiter
res = isValidDelim(state, match)
if (!res.canClose) {
if (!silent) { state.pending += '$' }
state.pos = start
return true
}
if (!silent) {
token = state.push('katex_inline', 'math', 0)
token.markup = '$'
token.content = state.src
// Extract the math part without the $
.slice(start, match)
// Escape the curly braces since they will be interpreted as
// attributes by markdown-it-attrs (the "curly_attributes"
// core rule)
.replaceAll('{', '{{')
.replaceAll('}', '}}')
}
state.pos = match + 1
return true
},
katexBlock (state, start, end, silent) {
let firstLine; let lastLine; let next; let lastPos; let found = false
let pos = state.bMarks[start] + state.tShift[start]
let max = state.eMarks[start]
if (pos + 2 > max) { return false }
if (state.src.slice(pos, pos + 2) !== '$$') { return false }
pos += 2
firstLine = state.src.slice(pos, max)
if (silent) { return true }
if (firstLine.trim().slice(-2) === '$$') {
// Single line expression
firstLine = firstLine.trim().slice(0, -2)
found = true
}
for (next = start; !found;) {
next++
if (next >= end) { break }
pos = state.bMarks[next] + state.tShift[next]
max = state.eMarks[next]
if (pos < max && state.tShift[next] < state.blkIndent) {
// non-empty line with negative indent should stop the list:
break
}
if (state.src.slice(pos, max).trim().slice(-2) === '$$') {
lastPos = state.src.slice(0, max).lastIndexOf('$$')
lastLine = state.src.slice(pos, lastPos)
found = true
}
}
state.line = next + 1
const token = state.push('katex_block', 'math', 0)
token.block = true
token.content = (firstLine && firstLine.trim() ? firstLine + '\n' : '') +
state.getLines(start + 1, next, state.tShift[start], true) +
(lastLine && lastLine.trim() ? lastLine : '')
token.map = [start, state.line]
token.markup = '$$'
return true
}
}

@ -0,0 +1,12 @@
function renderEm (tokens, idx, opts, env, slf) {
const token = tokens[idx]
if (token.markup === '_') {
token.tag = 'u'
}
return slf.renderToken(tokens, idx, opts)
}
export default (md) => {
md.renderer.rules.em_open = renderEm
md.renderer.rules.em_close = renderEm
}

@ -0,0 +1,187 @@
import pako from 'pako'
// ------------------------------------
// Markdown - PlantUML Preprocessor
// ------------------------------------
export default {
init (mdinst, conf) {
mdinst.use((md, opts) => {
const openMarker = opts.openMarker || '```plantuml'
const openChar = openMarker.charCodeAt(0)
const closeMarker = opts.closeMarker || '```'
const closeChar = closeMarker.charCodeAt(0)
const imageFormat = opts.imageFormat || 'svg'
const server = opts.server || 'https://plantuml.requarks.io'
md.block.ruler.before('fence', 'uml_diagram', (state, startLine, endLine, silent) => {
let nextLine
let i
let autoClosed = false
let start = state.bMarks[startLine] + state.tShift[startLine]
let max = state.eMarks[startLine]
// Check out the first character quickly,
// this should filter out most of non-uml blocks
//
if (openChar !== state.src.charCodeAt(start)) { return false }
// Check out the rest of the marker string
//
for (i = 0; i < openMarker.length; ++i) {
if (openMarker[i] !== state.src[start + i]) { return false }
}
const markup = state.src.slice(start, start + i)
const params = state.src.slice(start + i, max)
// Since start is found, we can report success here in validation mode
//
if (silent) { return true }
// Search for the end of the block
//
nextLine = startLine
for (;;) {
nextLine++
if (nextLine >= endLine) {
// unclosed block should be autoclosed by end of document.
// also block seems to be autoclosed by end of parent
break
}
start = state.bMarks[nextLine] + state.tShift[nextLine]
max = state.eMarks[nextLine]
if (start < max && state.sCount[nextLine] < state.blkIndent) {
// non-empty line with negative indent should stop the list:
// - ```
// test
break
}
if (closeChar !== state.src.charCodeAt(start)) {
// didn't find the closing fence
continue
}
if (state.sCount[nextLine] > state.sCount[startLine]) {
// closing fence should not be indented with respect of opening fence
continue
}
let closeMarkerMatched = true
for (i = 0; i < closeMarker.length; ++i) {
if (closeMarker[i] !== state.src[start + i]) {
closeMarkerMatched = false
break
}
}
if (!closeMarkerMatched) {
continue
}
// make sure tail has spaces only
if (state.skipSpaces(start + i) < max) {
continue
}
// found!
autoClosed = true
break
}
const contents = state.src
.split('\n')
.slice(startLine + 1, nextLine)
.join('\n')
// We generate a token list for the alt property, to mimic what the image parser does.
const altToken = []
// Remove leading space if any.
const alt = params ? params.slice(1) : 'uml diagram'
state.md.inline.parse(
alt,
state.md,
state.env,
altToken
)
const zippedCode = encode64(pako.deflate('@startuml\n' + contents + '\n@enduml', { to: 'string' }))
const token = state.push('uml_diagram', 'img', 0)
// alt is constructed from children. No point in populating it here.
token.attrs = [['src', `${server}/${imageFormat}/${zippedCode}`], ['alt', ''], ['class', 'uml-diagram']]
token.block = true
token.children = altToken
token.info = params
token.map = [startLine, nextLine]
token.markup = markup
state.line = nextLine + (autoClosed ? 1 : 0)
return true
}, {
alt: ['paragraph', 'reference', 'blockquote', 'list']
})
md.renderer.rules.uml_diagram = md.renderer.rules.image
}, {
openMarker: conf.openMarker,
closeMarker: conf.closeMarker,
imageFormat: conf.imageFormat,
server: conf.server
})
}
}
function encode64 (data) {
let r = ''
for (let i = 0; i < data.length; i += 3) {
if (i + 2 === data.length) {
r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), 0)
} else if (i + 1 === data.length) {
r += append3bytes(data.charCodeAt(i), 0, 0)
} else {
r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), data.charCodeAt(i + 2))
}
}
return r
}
function append3bytes (b1, b2, b3) {
const c1 = b1 >> 2
const c2 = ((b1 & 0x3) << 4) | (b2 >> 4)
const c3 = ((b2 & 0xF) << 2) | (b3 >> 6)
const c4 = b3 & 0x3F
let r = ''
r += encode6bit(c1 & 0x3F)
r += encode6bit(c2 & 0x3F)
r += encode6bit(c3 & 0x3F)
r += encode6bit(c4 & 0x3F)
return r
}
function encode6bit (raw) {
let b = raw
if (b < 10) {
return String.fromCharCode(48 + b)
}
b -= 10
if (b < 26) {
return String.fromCharCode(65 + b)
}
b -= 26
if (b < 26) {
return String.fromCharCode(97 + b)
}
b -= 26
if (b === 0) {
return '-'
}
if (b === 1) {
return '_'
}
return '?'
}

@ -173,7 +173,7 @@ export const usePageStore = defineStore('page', {
/**
* PAGE - CREATE
*/
pageCreate ({ editor, locale, path }) {
pageCreate ({ editor, locale, path, title = '', description = '', content = '' }) {
const editorStore = useEditorStore()
// if (['markdown', 'api'].includes(editor)) {
@ -196,14 +196,14 @@ export const usePageStore = defineStore('page', {
} else {
this.path = this.path.length < 2 ? 'new-page' : `${this.path}/new-page`
}
this.title = ''
this.description = ''
this.title = title ?? ''
this.description = description ?? ''
this.icon = 'las la-file-alt'
this.publishState = 'published'
this.relations = []
this.tags = []
this.content = ''
this.content = content ?? ''
this.render = ''
// -> View Mode

Loading…
Cancel
Save