feat: editor + page rendering improvements

pull/6775/head
NGPixel 2 years ago
parent 593822e023
commit 80b1cbff5c
No known key found for this signature in database
GPG Key ID: B755FB6870B30F63

@ -10,9 +10,9 @@ 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.mjs'
import mdImsize from './modules/markdown-it-imsize.mjs'
import mdUnderline from './modules/markdown-it-underline.mjs'
// import 'katex/dist/contrib/mhchem'
import twemoji from 'twemoji'
import plantuml from './modules/plantuml.mjs'
@ -51,7 +51,7 @@ export async function render (input, config) {
} else if (['mermaid', 'plantuml'].includes(lang)) {
return `<pre class="codeblock-${lang}"><code>${escape(str)}</code></pre>`
} else {
const highlighted = lang ? hljs.highlight(str, { language: lang, ignoreIllegals: true }) : hljs.highlightAuto(str)
const highlighted = lang ? hljs.highlight(str, { language: lang, ignoreIllegals: true }) : { value: str }
const lineCount = highlighted.value.match(/\n/g).length
const lineNums = lineCount > 1 ? `<span aria-hidden="true" class="line-numbers-rows">${times(lineCount, n => '<span></span>').join('')}</span>` : ''
return `<pre class="codeblock ${lineCount > 1 && 'line-numbers'}"><code class="language-${lang}">${highlighted.value}${lineNums}</code></pre>`
@ -70,10 +70,10 @@ export async function render (input, config) {
.use(mdSub)
.use(mdMark)
.use(mdFootnote)
// .use(mdImsize)
.use(mdImsize)
if (config.underline) {
md.use(underline)
md.use(mdUnderline)
}
if (config.mdmultiTable) {

@ -0,0 +1,259 @@
// Adapted from markdown-it-imsize plugin by @tatsy
// Original source https://github.com/tatsy/markdown-it-imsize/blob/master/lib/index.js
function renderImSize (state, silent) {
let attrs
let code
let label
let pos
let ref
let res
let title
let width = ''
let height = ''
let token
let tokens
let start
let href = ''
const oldPos = state.pos
const max = state.posMax
if (state.src.charCodeAt(state.pos) !== 0x21/* ! */) { return false }
if (state.src.charCodeAt(state.pos + 1) !== 0x5B/* [ */) { return false }
const labelStart = state.pos + 2
const labelEnd = state.md.helpers.parseLinkLabel(state, state.pos + 1, false)
// parser failed to find ']', so it's not a valid link
if (labelEnd < 0) { return false }
pos = labelEnd + 1
if (pos < max && state.src.charCodeAt(pos) === 0x28/* ( */) {
//
// Inline link
//
// [link]( <href> "title" )
// ^^ skipping these spaces
pos++
for (; pos < max; pos++) {
code = state.src.charCodeAt(pos)
if (code !== 0x20 && code !== 0x0A) { break }
}
if (pos >= max) { return false }
// [link]( <href> "title" )
// ^^^^^^ parsing link destination
start = pos
res = state.md.helpers.parseLinkDestination(state.src, pos, state.posMax)
if (res.ok) {
href = state.md.normalizeLink(res.str)
if (state.md.validateLink(href)) {
pos = res.pos
} else {
href = ''
}
}
// [link]( <href> "title" )
// ^^ skipping these spaces
start = pos
for (; pos < max; pos++) {
code = state.src.charCodeAt(pos)
if (code !== 0x20 && code !== 0x0A) { break }
}
// [link]( <href> "title" )
// ^^^^^^^ parsing link title
res = state.md.helpers.parseLinkTitle(state.src, pos, state.posMax)
if (pos < max && start !== pos && res.ok) {
title = res.str
pos = res.pos
// [link]( <href> "title" )
// ^^ skipping these spaces
for (; pos < max; pos++) {
code = state.src.charCodeAt(pos)
if (code !== 0x20 && code !== 0x0A) { break }
}
} else {
title = ''
}
// [link]( <href> "title" =WxH )
// ^^^^ parsing image size
if (pos - 1 >= 0) {
code = state.src.charCodeAt(pos - 1)
// there must be at least one white spaces
// between previous field and the size
if (code === 0x20) {
res = parseImageSize(state.src, pos, state.posMax)
if (res.ok) {
width = res.width
height = res.height
pos = res.pos
// [link]( <href> "title" =WxH )
// ^^ skipping these spaces
for (; pos < max; pos++) {
code = state.src.charCodeAt(pos)
if (code !== 0x20 && code !== 0x0A) { break }
}
}
}
}
if (pos >= max || state.src.charCodeAt(pos) !== 0x29/* ) */) {
state.pos = oldPos
return false
}
pos++
} else {
//
// Link reference
//
if (typeof state.env.references === 'undefined') { return false }
// [foo] [bar]
// ^^ optional whitespace (can include newlines)
for (; pos < max; pos++) {
code = state.src.charCodeAt(pos)
if (code !== 0x20 && code !== 0x0A) { break }
}
if (pos < max && state.src.charCodeAt(pos) === 0x5B/* [ */) {
start = pos + 1
pos = state.md.helpers.parseLinkLabel(state, pos)
if (pos >= 0) {
label = state.src.slice(start, pos++)
} else {
pos = labelEnd + 1
}
} else {
pos = labelEnd + 1
}
// covers label === '' and label === undefined
// (collapsed reference link and shortcut reference link respectively)
if (!label) { label = state.src.slice(labelStart, labelEnd) }
ref = state.env.references[state.md.utils.normalizeReference(label)]
if (!ref) {
state.pos = oldPos
return false
}
href = ref.href
title = ref.title
}
//
// We found the end of the link, and know for a fact it's a valid link;
// so all that's left to do is to call tokenizer.
//
if (!silent) {
state.pos = labelStart
state.posMax = labelEnd
const newState = new state.md.inline.State(
state.src.slice(labelStart, labelEnd),
state.md,
state.env,
tokens = []
)
newState.md.inline.tokenize(newState)
token = state.push('image', 'img', 0)
token.attrs = attrs = [['src', href],
['alt', '']]
token.children = tokens
if (title) {
attrs.push(['title', title])
}
if (width !== '') {
attrs.push(['width', width])
}
if (height !== '') {
attrs.push(['height', height])
}
}
state.pos = pos
state.posMax = max
return true
}
function parseNextNumber (str, pos, max) {
let code
const start = pos
const result = {
ok: false,
pos,
value: ''
}
code = str.charCodeAt(pos)
while ((pos < max && (code >= 0x30 /* 0 */ && code <= 0x39 /* 9 */)) || code === 0x25 /* % */) {
code = str.charCodeAt(++pos)
}
result.ok = true
result.pos = pos
result.value = str.slice(start, pos)
return result
}
function parseImageSize (str, pos, max) {
let code
const result = {
ok: false,
pos: 0,
width: '',
height: ''
}
if (pos >= max) { return result }
code = str.charCodeAt(pos)
if (code !== 0x3d /* = */) { return result }
pos++
// size must follow = without any white spaces as follows
// (1) =300x200
// (2) =300x
// (3) =x200
code = str.charCodeAt(pos)
if (code !== 0x78 /* x */ && (code < 0x30 || code > 0x39) /* [0-9] */) {
return result
}
// parse width
const resultW = parseNextNumber(str, pos, max)
pos = resultW.pos
// next charactor must be 'x'
code = str.charCodeAt(pos)
if (code !== 0x78 /* x */) { return result }
pos++
// parse height
const resultH = parseNextNumber(str, pos, max)
pos = resultH.pos
result.width = resultW.value
result.height = resultH.value
result.pos = pos
result.ok = true
return result
}
export default (md) => {
md.inline.ruler.before('emphasis', 'image', renderImSize)
}

@ -116,14 +116,16 @@
:href='siteStore.docsBase + `/editor/${editorStore.editor}`'
target='_blank'
type='a'
)
)
q-tooltip {{ t(`common.actions.viewDocs`) }}
q-btn.q-ml-sm.acrylic-btn(
icon='las la-cog'
flat
color='grey'
:aria-label='t(`editor.settings`)'
@click='openEditorSettings'
)
)
q-tooltip {{ t(`editor.settings`) }}
template(v-if='editorStore.isActive || editorStore.hasPendingChanges')
q-btn.acrylic-btn.q-ml-sm(
flat
@ -139,8 +141,8 @@
flat
icon='las la-check'
color='positive'
label='Create Page'
aria-label='Create Page'
:label='t(`editor.createPage`)'
:aria-label='t(`editor.createPage`)'
no-caps
@click='createPage'
)
@ -149,19 +151,21 @@
flat
icon='las la-check'
color='positive'
label='Save Changes'
aria-label='Save Changes'
:label='t(`common.actions.saveChanges`)'
:aria-label='t(`common.actions.saveChanges`)'
:disabled='!editorStore.hasPendingChanges'
no-caps
@click='saveChanges'
)
@click.exact='saveChanges(false)'
@click.ctrl.exact='saveChanges(true)'
)
q-tooltip {{ t(`editor.saveAndCloseTip`) }}
template(v-else-if='userStore.can(`edit:pages`)')
q-btn.acrylic-btn.q-ml-md(
flat
icon='las la-edit'
color='deep-orange-9'
label='Edit'
aria-label='Edit'
:label='t(`common.actions.edit`)'
:aria-label='t(`common.actions.edit`)'
no-caps
@click='editPage'
)
@ -258,7 +262,7 @@ async function discardChanges () {
$q.loading.hide()
}
async function saveChanges () {
async function saveChanges (closeAfter = false) {
if (siteStore.features.reasonForChange !== 'off') {
$q.dialog({
component: defineAsyncComponent(() => import('../components/PageReasonForChangeDialog.vue')),
@ -269,14 +273,14 @@ async function saveChanges () {
editorStore.$patch({
reasonForChange: reason
})
saveChangesCommit()
saveChangesCommit(closeAfter)
})
} else {
saveChangesCommit()
saveChangesCommit(closeAfter)
}
}
async function saveChangesCommit () {
async function saveChangesCommit (closeAfter = false) {
$q.loading.show()
try {
await pageStore.pageSave()
@ -284,6 +288,12 @@ async function saveChangesCommit () {
type: 'positive',
message: 'Page saved successfully.'
})
if (closeAfter) {
editorStore.$patch({
isActive: false,
editor: ''
})
}
} catch (err) {
$q.notify({
type: 'negative',

@ -1,6 +1,6 @@
.page-contents {
color: #424242;
font-size: 14px;
font-size: 16px;
> *:first-child {
margin-top: 0;
@ -15,7 +15,7 @@
// ---------------------------------
a {
color: $blue;
color: $blue-8;
&.is-internal-link.is-invalid-page {
color: $red-8;
@ -40,7 +40,7 @@
}
@at-root .body--dark & {
color: $blue-2;
color: $blue-4;
}
}
@ -51,6 +51,7 @@
h1, h2, h3, h4, h5, h6 {
padding: 0;
margin: 0;
font-weight: 400;
position: relative;
line-height: normal;
@ -65,14 +66,11 @@
}
}
P + h2 {
margin-top: 12px;
}
h1 {
font-size: 3em;
font-weight: 500;
padding: 12px 0;
// color: var(--q-primary);
}
h2 {
font-size: 2.4em;
@ -92,6 +90,29 @@
font-size: 1.25em;
}
* + h1 {
margin-top: .5em;
padding-top: .5em;
// border-top: 2px solid var(--q-primary);
position: relative;
&::before {
position: absolute;
width: 100%;
height: 1px;
content: ' ';
background: linear-gradient(to right, var(--q-primary), transparent);
top: 0;
left: -16px;
}
}
*:not(h1) + h2 {
margin-top: .5em;
padding-top: .5em;
border-top: 1px dotted #CCC;
}
.toc-anchor {
display: none;
position: absolute;
@ -107,12 +128,8 @@
// ---------------------------------
p {
padding: 1rem 0 0 0;
margin: 0;
@at-root .page-contents > div > p:first-child {
padding-top: 0;
}
padding: 0;
margin: .3em 0 1em 0;
}
// ---------------------------------
@ -120,7 +137,7 @@
// ---------------------------------
blockquote {
padding: 0 1rem 1rem 1rem;
padding: 1em 1em .3em 1em;
background-color: $blue-grey-1;
border-left: 55px solid $blue-grey-5;
border-radius: .5rem;
@ -239,21 +256,29 @@
// ---------------------------------
ol, ul:not(.tabset-tabs) {
padding-top: 1rem;
width: 100%;
li > p {
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
@at-root .is-rtl & {
padding-left: 0;
padding-right: 1rem;
padding-right: 1em;
}
li > ul, li > ol {
padding-top: .5rem;
padding-left: 1rem;
padding-left: 1em;
@at-root .is-rtl & {
padding-left: 0;
padding-right: 1rem;
padding-right: 1em;
}
}
@ -413,6 +438,58 @@
}
}
// ---------------------------------
// TASK LISTS
// ---------------------------------
.contains-task-list {
padding-left: 1em;
}
.task-list-item {
position: relative;
list-style-type: none;
&-checkbox[disabled] {
width: 1.1rem;
height: 1.1rem;
top: 2px;
position: relative;
margin-right: .4em;
background-color: $dark-5;
border-width: 0;
&::after {
position: absolute;
left: 0;
top: 0;
font-family: "Material Design Icons";
font-size: 1.25em;
font-weight: normal;
content: '\F0131';
color: $grey-10;
display: block;
border: none;
background-color: #FFF;
line-height: 1em;
cursor: default;
@at-root .body--dark & {
color: #FFF;
background-color: $dark-6;
}
}
&[checked]::after {
content: '\F0C52';
}
}
.contains-task-list {
padding: .5rem 0 0 1.5rem;
}
}
// ---------------------------------
// CODE
// ---------------------------------

@ -1417,6 +1417,7 @@
"editor.conflict.whatToDo": "What do you want to do?",
"editor.conflict.whatToDoLocal": "Use your current local version and ignore the latest changes.",
"editor.conflict.whatToDoRemote": "Use the remote version (latest) and discard your changes.",
"editor.createPage": "Create Page",
"editor.markup.admonitionDanger": "Danger / Important Admonition",
"editor.markup.admonitionInfo": "Info / Note Admonition",
"editor.markup.admonitionSuccess": "Tip / Success Admonition",
@ -1554,6 +1555,7 @@
"editor.save.processing": "Rendering",
"editor.save.saved": "Saved",
"editor.save.updateSuccess": "Page updated successfully.",
"editor.saveAndCloseTip": "Ctrl / Cmd + Click to save and close",
"editor.select.cannotChange": "This cannot be changed once the page is created.",
"editor.select.customView": "or create a custom view?",
"editor.select.title": "Which editor do you want to use for this page?",

@ -229,12 +229,12 @@ const state = reactive({
const thumbStyle = {
right: '2px',
borderRadius: '5px',
backgroundColor: '#000',
backgroundColor: $q.dark.isActive ? '#FFF' : '#000',
width: '5px',
opacity: 0.15
}
const barStyle = {
backgroundColor: '#FAFAFA',
backgroundColor: $q.dark.isActive ? '#161b22' : '#FAFAFA',
width: '9px',
opacity: 1
}

@ -10,9 +10,9 @@ 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 mdUnderline from './modules/markdown-it-underline'
import mdImsize from './modules/markdown-it-imsize'
import 'katex/dist/contrib/mhchem'
import twemoji from 'twemoji'
import plantuml from './modules/plantuml'
@ -52,7 +52,7 @@ export class MarkdownRenderer {
} else if (['mermaid', 'plantuml'].includes(lang)) {
return `<pre class="codeblock-${lang}"><code>${escape(str)}</code></pre>`
} else {
const highlighted = lang ? hljs.highlight(str, { language: lang, ignoreIllegals: true }) : hljs.highlightAuto(str)
const highlighted = lang ? hljs.highlight(str, { language: lang, ignoreIllegals: true }) : { value: str }
const lineCount = highlighted.value.match(/\n/g).length
const lineNums = lineCount > 1 ? `<span aria-hidden="true" class="line-numbers-rows">${times(lineCount, n => '<span></span>').join('')}</span>` : ''
return `<pre class="codeblock ${lineCount > 1 && 'line-numbers'}"><code class="language-${lang}">${highlighted.value}${lineNums}</code></pre>`
@ -71,10 +71,10 @@ export class MarkdownRenderer {
.use(mdSub)
.use(mdMark)
.use(mdFootnote)
// .use(mdImsize)
.use(mdImsize)
if (config.underline) {
this.md.use(underline)
this.md.use(mdUnderline)
}
if (config.mdmultiTable) {

@ -0,0 +1,259 @@
// Adapted from markdown-it-imsize plugin by @tatsy
// Original source https://github.com/tatsy/markdown-it-imsize/blob/master/lib/index.js
function renderImSize (state, silent) {
let attrs
let code
let label
let pos
let ref
let res
let title
let width = ''
let height = ''
let token
let tokens
let start
let href = ''
const oldPos = state.pos
const max = state.posMax
if (state.src.charCodeAt(state.pos) !== 0x21/* ! */) { return false }
if (state.src.charCodeAt(state.pos + 1) !== 0x5B/* [ */) { return false }
const labelStart = state.pos + 2
const labelEnd = state.md.helpers.parseLinkLabel(state, state.pos + 1, false)
// parser failed to find ']', so it's not a valid link
if (labelEnd < 0) { return false }
pos = labelEnd + 1
if (pos < max && state.src.charCodeAt(pos) === 0x28/* ( */) {
//
// Inline link
//
// [link]( <href> "title" )
// ^^ skipping these spaces
pos++
for (; pos < max; pos++) {
code = state.src.charCodeAt(pos)
if (code !== 0x20 && code !== 0x0A) { break }
}
if (pos >= max) { return false }
// [link]( <href> "title" )
// ^^^^^^ parsing link destination
start = pos
res = state.md.helpers.parseLinkDestination(state.src, pos, state.posMax)
if (res.ok) {
href = state.md.normalizeLink(res.str)
if (state.md.validateLink(href)) {
pos = res.pos
} else {
href = ''
}
}
// [link]( <href> "title" )
// ^^ skipping these spaces
start = pos
for (; pos < max; pos++) {
code = state.src.charCodeAt(pos)
if (code !== 0x20 && code !== 0x0A) { break }
}
// [link]( <href> "title" )
// ^^^^^^^ parsing link title
res = state.md.helpers.parseLinkTitle(state.src, pos, state.posMax)
if (pos < max && start !== pos && res.ok) {
title = res.str
pos = res.pos
// [link]( <href> "title" )
// ^^ skipping these spaces
for (; pos < max; pos++) {
code = state.src.charCodeAt(pos)
if (code !== 0x20 && code !== 0x0A) { break }
}
} else {
title = ''
}
// [link]( <href> "title" =WxH )
// ^^^^ parsing image size
if (pos - 1 >= 0) {
code = state.src.charCodeAt(pos - 1)
// there must be at least one white spaces
// between previous field and the size
if (code === 0x20) {
res = parseImageSize(state.src, pos, state.posMax)
if (res.ok) {
width = res.width
height = res.height
pos = res.pos
// [link]( <href> "title" =WxH )
// ^^ skipping these spaces
for (; pos < max; pos++) {
code = state.src.charCodeAt(pos)
if (code !== 0x20 && code !== 0x0A) { break }
}
}
}
}
if (pos >= max || state.src.charCodeAt(pos) !== 0x29/* ) */) {
state.pos = oldPos
return false
}
pos++
} else {
//
// Link reference
//
if (typeof state.env.references === 'undefined') { return false }
// [foo] [bar]
// ^^ optional whitespace (can include newlines)
for (; pos < max; pos++) {
code = state.src.charCodeAt(pos)
if (code !== 0x20 && code !== 0x0A) { break }
}
if (pos < max && state.src.charCodeAt(pos) === 0x5B/* [ */) {
start = pos + 1
pos = state.md.helpers.parseLinkLabel(state, pos)
if (pos >= 0) {
label = state.src.slice(start, pos++)
} else {
pos = labelEnd + 1
}
} else {
pos = labelEnd + 1
}
// covers label === '' and label === undefined
// (collapsed reference link and shortcut reference link respectively)
if (!label) { label = state.src.slice(labelStart, labelEnd) }
ref = state.env.references[state.md.utils.normalizeReference(label)]
if (!ref) {
state.pos = oldPos
return false
}
href = ref.href
title = ref.title
}
//
// We found the end of the link, and know for a fact it's a valid link;
// so all that's left to do is to call tokenizer.
//
if (!silent) {
state.pos = labelStart
state.posMax = labelEnd
const newState = new state.md.inline.State(
state.src.slice(labelStart, labelEnd),
state.md,
state.env,
tokens = []
)
newState.md.inline.tokenize(newState)
token = state.push('image', 'img', 0)
token.attrs = attrs = [['src', href],
['alt', '']]
token.children = tokens
if (title) {
attrs.push(['title', title])
}
if (width !== '') {
attrs.push(['width', width])
}
if (height !== '') {
attrs.push(['height', height])
}
}
state.pos = pos
state.posMax = max
return true
}
function parseNextNumber (str, pos, max) {
let code
const start = pos
const result = {
ok: false,
pos,
value: ''
}
code = str.charCodeAt(pos)
while ((pos < max && (code >= 0x30 /* 0 */ && code <= 0x39 /* 9 */)) || code === 0x25 /* % */) {
code = str.charCodeAt(++pos)
}
result.ok = true
result.pos = pos
result.value = str.slice(start, pos)
return result
}
function parseImageSize (str, pos, max) {
let code
const result = {
ok: false,
pos: 0,
width: '',
height: ''
}
if (pos >= max) { return result }
code = str.charCodeAt(pos)
if (code !== 0x3d /* = */) { return result }
pos++
// size must follow = without any white spaces as follows
// (1) =300x200
// (2) =300x
// (3) =x200
code = str.charCodeAt(pos)
if (code !== 0x78 /* x */ && (code < 0x30 || code > 0x39) /* [0-9] */) {
return result
}
// parse width
const resultW = parseNextNumber(str, pos, max)
pos = resultW.pos
// next charactor must be 'x'
code = str.charCodeAt(pos)
if (code !== 0x78 /* x */) { return result }
pos++
// parse height
const resultH = parseNextNumber(str, pos, max)
pos = resultH.pos
result.width = resultW.value
result.height = resultH.value
result.pos = pos
result.ok = true
return result
}
export default (md) => {
md.inline.ruler.before('emphasis', 'image', renderImSize)
}
Loading…
Cancel
Save