Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
wiki-js
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
1
Issues
1
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
wiki-js
Commits
59f6b6fe
Unverified
Commit
59f6b6fe
authored
Apr 30, 2023
by
NGPixel
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: markdown preview sync + code blocks syntax highlighting
parent
281ffd23
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
231 additions
and
155 deletions
+231
-155
render-page.mjs
server/tasks/workers/render-page.mjs
+30
-30
package-lock.json
ux/package-lock.json
+0
-0
package.json
ux/package.json
+1
-0
EditorMarkdown.vue
ux/src/components/EditorMarkdown.vue
+73
-123
page-contents.scss
ux/src/css/page-contents.scss
+99
-0
markdown.js
ux/src/renderers/markdown.js
+28
-2
No files found.
server/tasks/workers/render-page.mjs
View file @
59f6b6fe
import
{
get
,
has
,
isEmpty
,
reduce
,
times
,
toSafeInteger
}
from
'lodash-es'
import
cheerio
from
'cheerio'
import
*
as
cheerio
from
'cheerio'
export
async
function
task
({
payload
})
{
WIKI
.
logger
.
info
(
`Rendering page
${
payload
.
id
}
...`
)
...
...
@@ -36,37 +36,37 @@ export async function task ({ payload }) {
}
// Parse TOC
const
$
=
cheerio
.
load
(
output
)
let
isStrict
=
$
(
'h1'
).
length
>
0
// <- Allows for documents using H2 as top level
//
const $ = cheerio.load(output)
//
let isStrict = $('h1').length > 0 // <- Allows for documents using H2 as top level
let
toc
=
{
root
:
[]
}
$
(
'h1,h2,h3,h4,h5,h6'
).
each
((
idx
,
el
)
=>
{
const
depth
=
toSafeInteger
(
el
.
name
.
substring
(
1
))
-
(
isStrict
?
1
:
2
)
let
leafPathError
=
false
const
leafPath
=
reduce
(
times
(
depth
),
(
curPath
,
curIdx
)
=>
{
if
(
has
(
toc
,
curPath
))
{
const
lastLeafIdx
=
_
.
get
(
toc
,
curPath
).
length
-
1
if
(
lastLeafIdx
>=
0
)
{
curPath
=
`
${
curPath
}
[
${
lastLeafIdx
}
].children`
}
else
{
leafPathError
=
true
}
}
return
curPath
},
'root'
)
if
(
leafPathError
)
{
return
}
const
leafSlug
=
$
(
'.toc-anchor'
,
el
).
first
().
attr
(
'href'
)
$
(
'.toc-anchor'
,
el
).
remove
()
get
(
toc
,
leafPath
).
push
({
label
:
$
(
el
).
text
().
trim
(),
key
:
leafSlug
.
substring
(
1
),
children
:
[]
})
})
//
$('h1,h2,h3,h4,h5,h6').each((idx, el) => {
//
const depth = toSafeInteger(el.name.substring(1)) - (isStrict ? 1 : 2)
//
let leafPathError = false
//
const leafPath = reduce(times(depth), (curPath, curIdx) => {
//
if (has(toc, curPath)) {
// const lastLeafIdx =
get(toc, curPath).length - 1
//
if (lastLeafIdx >= 0) {
//
curPath = `${curPath}[${lastLeafIdx}].children`
//
} else {
//
leafPathError = true
//
}
//
}
//
return curPath
//
}, 'root')
//
if (leafPathError) { return }
//
const leafSlug = $('.toc-anchor', el).first().attr('href')
//
$('.toc-anchor', el).remove()
//
get(toc, leafPath).push({
//
label: $(el).text().trim(),
//
key: leafSlug.substring(1),
//
children: []
//
})
//
})
// Save to DB
await
WIKI
.
db
.
pages
.
query
()
...
...
ux/package-lock.json
View file @
59f6b6fe
B
{
...
...
ux/package.json
View file @
59f6b6fe
...
...
@@ -53,6 +53,7 @@
"fuse.js"
:
"6.6.2"
,
"graphql"
:
"16.6.0"
,
"graphql-tag"
:
"2.12.6"
,
"highlight.js"
:
"11.7.0"
,
"js-cookie"
:
"3.0.1"
,
"jwt-decode"
:
"3.1.2"
,
"katex"
:
"0.16.4"
,
...
...
ux/src/components/EditorMarkdown.vue
View file @
59f6b6fe
...
...
@@ -217,7 +217,7 @@
@
click
=
'state.previewShown = false'
)
q
-
tooltip
(
anchor
=
'top middle'
self
=
'bottom middle'
)
{{
t
(
'editor.togglePreviewPane'
)
}}
.
editor
-
markdown
-
preview
-
content
.
page
-
contents
(
ref
=
'editorPreviewContainer'
)
.
editor
-
markdown
-
preview
-
content
.
page
-
contents
(
ref
=
'editorPreviewContainer
Ref
'
)
div
(
ref
=
'editorPreview'
v
-
html
=
'pageStore.render'
...
...
@@ -257,8 +257,8 @@ const { t } = useI18n()
// STATE
let
editor
const
cm
=
shallowRef
(
null
)
const
monacoRef
=
ref
(
null
)
const
editorPreviewContainerRef
=
ref
(
null
)
const
state
=
reactive
({
previewShown
:
true
,
...
...
@@ -267,9 +267,6 @@ const state = reactive({
const
md
=
new
MarkdownRenderer
({
}
)
// Platform detection
const
CtrlKey
=
/Mac/
.
test
(
navigator
.
platform
)
?
'Cmd'
:
'Ctrl'
// METHODS
function
insertAssets
()
{
...
...
@@ -325,9 +322,9 @@ function setHeaderLine (lvl, focus = true) {
/**
* Get the header lever of the current line
*/
function
getHeaderLevel
(
cm
)
{
const
curLine
=
cm
.
doc
.
getCursor
(
'head'
).
line
const
lineContent
=
cm
.
doc
.
getLine
(
curLine
)
function
getHeaderLevel
()
{
const
curLine
=
editor
.
getPosition
().
lineNumber
const
lineContent
=
editor
.
getModel
().
getLineContent
(
curLine
)
let
lvl
=
0
const
result
=
lineContent
.
match
(
/^
(
#+
)
/
)
if
(
result
)
{
...
...
@@ -356,13 +353,15 @@ function insertAtCursor ({ content, focus = true }) {
*/
function
insertAfter
({
content
,
newLine
,
focus
=
true
}
)
{
const
curLine
=
editor
.
getPosition
().
lineNumber
const
lineLength
=
editor
.
getModel
().
getLineContent
(
curLine
).
length
editor
.
executeEdits
(
''
,
[{
range
:
new
Range
(
curLine
+
1
,
1
,
curLine
+
1
,
1
),
text
:
newLine
?
`\n
${content
}
\n`
:
content
,
range
:
new
Range
(
curLine
,
lineLength
+
1
,
curLine
,
lineLength
+
1
),
text
:
newLine
?
`\n
\n${content
}
\n`
:
`\n${content
}
`
,
forceMoveMarkers
:
true
}
])
if
(
focus
)
{
editor
.
focus
()
editor
.
revealLineInCenterIfOutsideViewport
(
editor
.
getPosition
().
lineNumber
)
}
}
...
...
@@ -408,7 +407,7 @@ function insertBeforeEachLine ({ content, after, focus = true }) {
* Insert an Horizontal Bar
*/
function
insertHorizontalBar
()
{
insertAfter
({
content
:
'
\
n---
\
n
'
,
newLine
:
true
}
)
insertAfter
({
content
:
'
---
'
,
newLine
:
true
}
)
}
/**
...
...
@@ -482,11 +481,11 @@ onMounted(async () => {
editor
=
monaco
.
editor
.
create
(
monacoRef
.
value
,
{
automaticLayout
:
true
,
cursorBlinking
:
'blink'
,
cursorSmoothCaretAnimation
:
true
,
//
cursorSmoothCaretAnimation: true,
fontSize
:
16
,
formatOnType
:
true
,
language
:
'markdown'
,
lineNumbersMinChars
:
3
,
lineNumbersMinChars
:
4
,
padding
:
{
top
:
10
,
bottom
:
10
}
,
scrollBeyondLastLine
:
false
,
tabSize
:
2
,
...
...
@@ -495,7 +494,8 @@ onMounted(async () => {
wordWrap
:
'on'
}
)
window
.
edd
=
editor
// TODO: For debugging, remove at some point...
window
.
edInstance
=
editor
// -> Define Formatting Actions
editor
.
addAction
({
...
...
@@ -523,6 +523,29 @@ onMounted(async () => {
}
)
editor
.
addAction
({
id
:
'markdown.extension.editing.increaseHeaderLevel'
,
keybindings
:
[
monaco
.
KeyMod
.
CtrlCmd
|
monaco
.
KeyMod
.
Alt
|
monaco
.
KeyCode
.
RightArrow
],
label
:
'Increase Header Level'
,
precondition
:
''
,
run
(
ed
)
{
let
lvl
=
getHeaderLevel
()
if
(
lvl
>=
6
)
{
lvl
=
5
}
setHeaderLine
(
lvl
+
1
)
}
}
)
editor
.
addAction
({
id
:
'markdown.extension.editing.decreaseHeaderLevel'
,
keybindings
:
[
monaco
.
KeyMod
.
CtrlCmd
|
monaco
.
KeyMod
.
Alt
|
monaco
.
KeyCode
.
LeftArrow
],
label
:
'Decrease Header Level'
,
precondition
:
''
,
run
(
ed
)
{
let
lvl
=
getHeaderLevel
()
if
(
lvl
<=
1
)
{
lvl
=
2
}
setHeaderLine
(
lvl
-
1
)
}
}
)
editor
.
addAction
({
id
:
'save'
,
keybindings
:
[
monaco
.
KeyMod
.
CtrlCmd
|
monaco
.
KeyCode
.
KeyS
],
label
:
'Save'
,
...
...
@@ -531,6 +554,7 @@ onMounted(async () => {
}
}
)
// -> Handle content change
editor
.
onDidChangeModelContent
(
debounce
(
ev
=>
{
editorStore
.
$patch
({
lastChangeTimestamp
:
DateTime
.
utc
()
...
...
@@ -541,33 +565,39 @@ onMounted(async () => {
processContent
(
pageStore
.
content
)
}
,
500
))
editor
.
focus
()
// -> Handle cursor movement
editor
.
onDidChangeCursorPosition
(
debounce
(
ev
=>
{
if
(
!
state
.
previewScrollSync
||
!
state
.
previewShown
)
{
return
}
const
currentLine
=
editor
.
getPosition
().
lineNumber
if
(
currentLine
<
3
)
{
editorPreviewContainerRef
.
value
.
scrollTo
({
top
:
0
,
behavior
:
'smooth'
}
)
}
else
{
const
exactEl
=
editorPreviewContainerRef
.
value
.
querySelector
(
`[data-line='${currentLine
}
']`
)
if
(
exactEl
)
{
exactEl
.
scrollIntoView
({
behavior
:
'smooth'
}
)
}
else
{
const
closestLine
=
md
.
getClosestPreviewLine
(
currentLine
)
if
(
closestLine
)
{
const
closestEl
=
editorPreviewContainerRef
.
value
.
querySelector
(
`[data-line='${closestLine
}
']`
)
if
(
closestEl
)
{
closestEl
.
scrollIntoView
({
behavior
:
'smooth'
}
)
}
}
}
}
}
,
500
))
// -> Set Keybindings
// const keyBindings =
{
// [`$
{
CtrlKey
}
-
Alt
-
Right
`] (c) {
// let lvl = getHeaderLevel(c)
// if (lvl >= 6) { lvl = 5
}
// setHeaderLine(lvl + 1)
// return false
//
}
,
// [`
$
{
CtrlKey
}
-
Alt
-
Left
`] (c) {
// let lvl = getHeaderLevel(c)
// if (lvl <= 1) { lvl = 2
}
// setHeaderLine(lvl - 1)
// return false
//
}
//
}
// this.cm.on('inputRead', this.autocomplete)
// -> Post init
// // Handle cursor movement
// this.cm.on('cursorActivity', c => {
// this.positionSync(c)
// this.scrollSync(c)
//
}
)
editor
.
focus
()
// // Handle special paste
// this.cm.on('paste', this.onCmPaste)
nextTick
(()
=>
{
processContent
(
pageStore
.
content
)
}
)
EVENT_BUS
.
on
(
'insertAsset'
,
insertAssetClb
)
...
...
@@ -613,7 +643,8 @@ onBeforeUnmount(() => {
</script>
<style lang="scss">
$editor-height: calc(100vh - 64px - 94px - 2px);
$editor-height: calc(100vh - 64px - 96px);
$editor-preview-height: calc(100vh - 64px - 96px - 32px);
$editor-height-mobile: calc(100vh - 112px - 16px);
.editor-markdown {
...
...
@@ -658,7 +689,7 @@ $editor-height-mobile: calc(100vh - 112px - 16px);
background-color: $grey-2;
}
@at-root .body--dark & {
background-color: $dark-
4
;
background-color: $dark-
6
;
}
// @include until($tablet) {
// display: none;
...
...
@@ -690,7 +721,7 @@ $editor-height-mobile: calc(100vh - 112px - 16px);
}
}
&-content {
height: $editor-height;
height: $editor-
preview-
height;
overflow-y: scroll;
padding: 1rem;
max-width: calc(50vw - 57px);
...
...
@@ -744,7 +775,7 @@ $editor-height-mobile: calc(100vh - 112px - 16px);
}
&-toolbar {
background-color: $primary;
border-left:
5
0px solid darken($primary, 5%);
border-left:
6
0px solid darken($primary, 5%);
color: #FFF;
height: 32px;
}
...
...
@@ -759,86 +790,5 @@ $editor-height-mobile: calc(100vh - 112px - 16px);
align-items: center;
padding: 12px 0;
}
&-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;
}
}
// ==========================================
// 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: $grey-8;
}
.CodeMirror-selection-highlight-scrollbar {
background-color: $green-6;
}
}
// 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 $grey-7;
background: $grey-9;
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: $blue-5;
color: #FFF;
}
</style>
ux/src/css/page-contents.scss
View file @
59f6b6fe
...
...
@@ -6,6 +6,10 @@
margin-top
:
0
;
}
@at-root
.body--dark
&
{
color
:
#FFF
;
}
// ---------------------------------
// LINKS
// ---------------------------------
...
...
@@ -61,6 +65,10 @@
}
}
P
+
h2
{
margin-top
:
12px
;
}
h1
{
font-size
:
3em
;
font-weight
:
500
;
...
...
@@ -406,6 +414,95 @@
}
// ---------------------------------
// CODE
// ---------------------------------
// code {
// background-color: mc('indigo', '50');
// padding: 0 5px;
// color: mc('indigo', '800');
// font-family: 'Roboto Mono', monospace;
// font-weight: normal;
// font-size: 1rem;
// box-shadow: none;
// &::before, &::after {
// display: none;
// }
// @at-root .theme--dark & {
// background-color: darken(mc('grey', '900'), 5%);
// color: mc('indigo', '100');
// }
// }
pre
.codeblock
{
border
:
none
;
border-radius
:
5px
;
box-shadow
:
initial
;
background-color
:
$dark-5
;
padding
:
1rem
;
margin
:
1rem
0
;
overflow
:
auto
;
@at-root
.body--dark
&
{
background-color
:
$dark-5
;
}
>
code
{
background-color
:
transparent
;
padding
:
0
;
color
:
#FFF
;
box-shadow
:
initial
;
display
:
block
;
font-size
:
.85rem
;
font-family
:
'Roboto Mono'
,
monospace
;
&
:after
,
&
:before
{
content
:
initial
;
letter-spacing
:
initial
;
}
}
&
.line-numbers
{
counter-reset
:
linenumber
;
padding-left
:
3rem
;
>
code
{
position
:
relative
;
white-space
:
inherit
;
}
.line-numbers-rows
{
position
:
absolute
;
pointer-events
:
none
;
top
:
0
;
font-size
:
100%
;
left
:
-3
.8em
;
width
:
3em
;
letter-spacing
:
-1px
;
border-right
:
1px
solid
#999
;
-webkit-user-select
:
none
;
-moz-user-select
:
none
;
user-select
:
none
;
&
>
span
{
display
:
block
;
counter-increment
:
linenumber
;
&
:before
{
content
:
counter
(
linenumber
);
color
:
#999
;
display
:
block
;
padding-right
:
.8em
;
text-align
:
right
;
}
}
}
}
}
// ---------------------------------
// LEGACY
// ---------------------------------
...
...
@@ -417,3 +514,5 @@
padding
:
5px
;
}
}
@import
'highlight.js/styles/atom-one-dark-reasonable.css'
;
ux/src/renderers/markdown.js
View file @
59f6b6fe
...
...
@@ -18,7 +18,9 @@ import twemoji from 'twemoji'
import
plantuml
from
'./modules/plantuml'
import
katexHelper
from
'./modules/katex'
import
{
escape
}
from
'lodash-es'
import
hljs
from
'highlight.js'
import
{
escape
,
findLast
,
times
}
from
'lodash-es'
export
class
MarkdownRenderer
{
constructor
(
conf
=
{})
{
...
...
@@ -33,7 +35,10 @@ export class MarkdownRenderer {
}
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>`
const
highlighted
=
lang
?
hljs
.
highlight
(
str
,
{
language
:
lang
,
ignoreIllegals
:
true
})
:
hljs
.
highlightAuto
(
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>`
}
}
})
...
...
@@ -91,9 +96,30 @@ export class MarkdownRenderer {
}
})
}
// Inject line numbers for preview scroll sync
this
.
linesMap
=
[]
const
injectLineNumbers
=
(
tokens
,
idx
,
options
,
env
,
slf
)
=>
{
let
line
if
(
tokens
[
idx
].
map
&&
tokens
[
idx
].
level
===
0
)
{
line
=
tokens
[
idx
].
map
[
0
]
+
1
tokens
[
idx
].
attrJoin
(
'class'
,
'line'
)
tokens
[
idx
].
attrSet
(
'data-line'
,
String
(
line
))
this
.
linesMap
.
push
(
line
)
}
return
slf
.
renderToken
(
tokens
,
idx
,
options
,
env
,
slf
)
}
this
.
md
.
renderer
.
rules
.
paragraph_open
=
injectLineNumbers
this
.
md
.
renderer
.
rules
.
heading_open
=
injectLineNumbers
this
.
md
.
renderer
.
rules
.
blockquote_open
=
injectLineNumbers
}
render
(
src
)
{
this
.
linesMap
=
[]
return
this
.
md
.
render
(
src
)
}
getClosestPreviewLine
(
line
)
{
return
findLast
(
this
.
linesMap
,
n
=>
n
<=
line
)
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment