refactor: Pre-render TeX + MathML server-side to SVG

This commit is contained in:
NGPixel 2017-06-24 15:54:31 -04:00 committed by Nicolas Giard
parent 13d355bd1c
commit 3d9aa18c05
20 changed files with 759 additions and 680 deletions

View File

@ -1,5 +0,0 @@
window.MathJax = {
root: '/js/mathjax',
delayStartupUntil: 'configured'
}
;

View File

@ -65,56 +65,6 @@ module.exports = Promise.mapSeries([
} }
}) })
}, },
/**
* MathJax
*/
() => {
return fs.accessAsync('./assets/js/mathjax').then(() => {
console.info(colors.white(' └── ') + colors.magenta('MathJax directory already exists. Task aborted.'))
return true
}).catch(err => {
if (err.code === 'ENOENT') {
console.info(colors.white(' └── ') + colors.green('Copy MathJax dependencies to assets...'))
return fs.ensureDirAsync('./assets/js/mathjax').then(() => {
return fs.copyAsync('./node_modules/mathjax', './assets/js/mathjax', {
filter: (src, dest) => {
let srcNormalized = src.replace(/\\/g, '/')
let shouldCopy = false
console.info(colors.white(' ' + srcNormalized))
_.forEach([
'/node_modules/mathjax',
'/node_modules/mathjax/jax',
'/node_modules/mathjax/jax/input',
'/node_modules/mathjax/jax/output'
], chk => {
if (srcNormalized.endsWith(chk)) {
shouldCopy = true
}
})
_.forEach([
'/node_modules/mathjax/extensions',
'/node_modules/mathjax/MathJax.js',
'/node_modules/mathjax/jax/element',
'/node_modules/mathjax/jax/input/MathML',
'/node_modules/mathjax/jax/input/TeX',
'/node_modules/mathjax/jax/output/SVG'
], chk => {
if (srcNormalized.indexOf(chk) > 0) {
shouldCopy = true
}
})
if (shouldCopy && srcNormalized.indexOf('/fonts/') > 0 && srcNormalized.indexOf('/STIX-Web') <= 1) {
shouldCopy = false
}
return shouldCopy
}
})
})
} else {
throw err
}
})
},
/** /**
* i18n * i18n
*/ */
@ -136,21 +86,6 @@ module.exports = Promise.mapSeries([
}) })
}) })
}, },
/**
* Bundle pre-init scripts
*/
() => {
console.info(colors.white(' └── ') + colors.green('Bundling pre-init scripts...'))
let preInitContent = ''
return fs.readdirAsync('./client/js/pre-init').map(f => {
let fPath = path.join('./client/js/pre-init/', f)
return fs.readFileAsync(fPath, 'utf8').then(fContent => {
preInitContent += fContent + ';\n'
})
}).then(() => {
return fs.outputFileAsync('./.build/_preinit.js', preInitContent, 'utf8')
})
},
/** /**
* Delete Fusebox cache * Delete Fusebox cache
*/ */

View File

@ -1,6 +1,6 @@
'use strict' 'use strict'
/* global $ */ /* global $, siteRoot */
/* eslint-disable no-new */ /* eslint-disable no-new */
import Vue from 'vue' import Vue from 'vue'
@ -64,6 +64,7 @@ import colorPickerComponent from './components/color-picker.vue'
import editorCodeblockComponent from './components/editor-codeblock.vue' import editorCodeblockComponent from './components/editor-codeblock.vue'
import editorFileComponent from './components/editor-file.vue' import editorFileComponent from './components/editor-file.vue'
import editorVideoComponent from './components/editor-video.vue' import editorVideoComponent from './components/editor-video.vue'
import historyComponent from './components/history.vue'
import loadingSpinnerComponent from './components/loading-spinner.vue' import loadingSpinnerComponent from './components/loading-spinner.vue'
import modalCreatePageComponent from './components/modal-create-page.vue' import modalCreatePageComponent from './components/modal-create-page.vue'
import modalCreateUserComponent from './components/modal-create-user.vue' import modalCreateUserComponent from './components/modal-create-user.vue'
@ -130,7 +131,7 @@ i18next
.use(i18nextXHR) .use(i18nextXHR)
.init({ .init({
backend: { backend: {
loadPath: '/js/i18n/{{lng}}.json' loadPath: siteRoot + '/js/i18n/{{lng}}.json'
}, },
lng: siteLang, lng: siteLang,
fallbackLng: siteLang fallbackLng: siteLang
@ -176,6 +177,7 @@ $(() => {
editorCodeblock: editorCodeblockComponent, editorCodeblock: editorCodeblockComponent,
editorFile: editorFileComponent, editorFile: editorFileComponent,
editorVideo: editorVideoComponent, editorVideo: editorVideoComponent,
history: historyComponent,
loadingSpinner: loadingSpinnerComponent, loadingSpinner: loadingSpinnerComponent,
modalCreatePage: modalCreatePageComponent, modalCreatePage: modalCreatePageComponent,
modalCreateUser: modalCreateUserComponent, modalCreateUser: modalCreateUserComponent,

View File

@ -1,6 +1,6 @@
'use strict' 'use strict'
/* global $ */ /* global $, siteRoot */
let mde let mde
@ -30,7 +30,7 @@ export default {
return resp.json() return resp.json()
}).then(resp => { }).then(resp => {
if (resp.ok) { if (resp.ok) {
window.location.assign('/' + self.currentPath) window.location.assign(siteRoot + '/' + self.currentPath)
} else { } else {
self.$store.dispatch('alert', { self.$store.dispatch('alert', {
style: 'red', style: 'red',

View File

@ -0,0 +1,41 @@
<template lang="pug">
div {{ currentPath }}
</template>
<script>
export default {
name: 'history',
props: ['currentPath'],
data() {
return {
tree: []
}
},
methods: {
fetch(basePath) {
let self = this
self.$store.dispatch('startLoading')
self.$nextTick(() => {
socket.emit('treeFetch', { basePath }, (data) => {
if (self.tree.length > 0) {
let branch = self._.last(self.tree)
branch.hasChildren = true
self._.find(branch.pages, { _id: basePath }).isActive = true
}
self.tree.push({
hasChildren: false,
pages: data
})
self.$store.dispatch('stopLoading')
})
})
},
goto(entryPath) {
window.location.assign(siteRoot + '/' + entryPath)
}
},
mounted() {
}
}
</script>

View File

@ -10,7 +10,7 @@
li(v-if='searchres.length === 0') li(v-if='searchres.length === 0')
a: em {{ $t('search.nomatch') }} a: em {{ $t('search.nomatch') }}
li(v-for='sres in searchres', v-bind:class='{ "is-active": searchmovekey === "res." + sres.entryPath }') li(v-for='sres in searchres', v-bind:class='{ "is-active": searchmovekey === "res." + sres.entryPath }')
a(v-bind:href='"/" + sres.entryPath') {{ sres.title }} a(v-bind:href='siteRoot + "/" + sres.entryPath') {{ sres.title }}
p.searchresults-label(v-if='searchsuggest.length > 0') {{ $t('search.didyoumean') }} p.searchresults-label(v-if='searchsuggest.length > 0') {{ $t('search.didyoumean') }}
ul.searchresults-list(v-if='searchsuggest.length > 0') ul.searchresults-list(v-if='searchsuggest.length > 0')
li(v-for='sug in searchsuggest', v-bind:class='{ "is-active": searchmovekey === "sug." + sug }') li(v-for='sug in searchsuggest', v-bind:class='{ "is-active": searchmovekey === "sug." + sug }')
@ -18,81 +18,81 @@
</template> </template>
<script> <script>
export default { export default {
data () { data() {
return { return {
searchq: '', searchq: '',
searchres: [], searchres: [],
searchsuggest: [], searchsuggest: [],
searchload: 0, searchload: 0,
searchactive: false, searchactive: false,
searchmoveidx: 0, searchmoveidx: 0,
searchmovekey: '', searchmovekey: '',
searchmovearr: [] searchmovearr: []
}
},
watch: {
searchq: function (val, oldVal) {
let self = this
self.searchmoveidx = 0
if (val.length >= 3) {
self.searchactive = true
self.searchload++
socket.emit('search', { terms: val }, (data) => {
self.searchres = data.match
self.searchsuggest = data.suggest
self.searchmovearr = self._.concat([], self.searchres, self.searchsuggest)
if (self.searchload > 0) { self.searchload-- }
})
} else {
self.searchactive = false
self.searchres = []
self.searchsuggest = []
self.searchmovearr = []
self.searchload = 0
}
},
searchmoveidx: function (val, oldVal) {
if (val > 0) {
this.searchmovekey = (this.searchmovearr[val - 1])
? 'res.' + this.searchmovearr[val - 1].entryPath
: 'sug.' + this.searchmovearr[val - 1]
} else {
this.searchmovekey = ''
}
}
},
methods: {
useSuggestion: function (sug) {
this.searchq = sug
},
closeSearch: function() {
this.searchq = ''
},
moveSelectSearch: function () {
if (this.searchmoveidx < 1) { return }
let i = this.searchmoveidx - 1
if (this.searchmovearr[i]) {
window.location.assign('/' + this.searchmovearr[i].entryPath)
} else {
this.searchq = this.searchmovearr[i]
}
},
moveDownSearch: function () {
if (this.searchmoveidx < this.searchmovearr.length) {
this.searchmoveidx++
}
},
moveUpSearch: function () {
if (this.searchmoveidx > 0) {
this.searchmoveidx--
}
}
},
mounted: function () {
let self = this
$('main').on('click', self.closeSearch)
} }
},
watch: {
searchq: function (val, oldVal) {
let self = this
self.searchmoveidx = 0
if (val.length >= 3) {
self.searchactive = true
self.searchload++
socket.emit('search', { terms: val }, (data) => {
self.searchres = data.match
self.searchsuggest = data.suggest
self.searchmovearr = self._.concat([], self.searchres, self.searchsuggest)
if (self.searchload > 0) { self.searchload-- }
})
} else {
self.searchactive = false
self.searchres = []
self.searchsuggest = []
self.searchmovearr = []
self.searchload = 0
}
},
searchmoveidx: function (val, oldVal) {
if (val > 0) {
this.searchmovekey = (this.searchmovearr[val - 1])
? 'res.' + this.searchmovearr[val - 1].entryPath
: 'sug.' + this.searchmovearr[val - 1]
} else {
this.searchmovekey = ''
}
}
},
methods: {
useSuggestion: function (sug) {
this.searchq = sug
},
closeSearch: function () {
this.searchq = ''
},
moveSelectSearch: function () {
if (this.searchmoveidx < 1) { return }
let i = this.searchmoveidx - 1
if (this.searchmovearr[i]) {
window.location.assign(siteRoot + '/' + this.searchmovearr[i].entryPath)
} else {
this.searchq = this.searchmovearr[i]
}
},
moveDownSearch: function () {
if (this.searchmoveidx < this.searchmovearr.length) {
this.searchmoveidx++
}
},
moveUpSearch: function () {
if (this.searchmoveidx > 0) {
this.searchmoveidx--
}
}
},
mounted: function () {
let self = this
$('main').on('click', self.closeSearch)
} }
}
</script> </script>

View File

@ -16,7 +16,7 @@
<script> <script>
export default { export default {
name: '', name: 'tree',
data () { data () {
return { return {
tree: [] tree: []

View File

@ -2,8 +2,6 @@
/* global $ */ /* global $ */
import MathJax from 'mathjax'
export default { export default {
name: 'content-view', name: 'content-view',
data() { data() {
@ -19,23 +17,5 @@ export default {
return false return false
}) })
}) })
MathJax.Hub.Config({
jax: ['input/TeX', 'input/MathML', 'output/SVG'],
extensions: ['tex2jax.js', 'mml2jax.js'],
TeX: {
extensions: ['AMSmath.js', 'AMSsymbols.js', 'noErrors.js', 'noUndefined.js']
},
SVG: {
scale: 120,
font: 'STIX-Web'
},
tex2jax: {
preview: 'none'
},
showMathMenu: false,
showProcessingMessages: false,
messageStyle: 'none'
})
MathJax.Hub.Configured()
} }
} }

View File

@ -1,5 +1,7 @@
'use strict' 'use strict'
/* global siteRoot */
export default { export default {
name: 'source-view', name: 'source-view',
data() { data() {
@ -7,7 +9,7 @@ export default {
}, },
mounted() { mounted() {
let self = this let self = this
FuseBox.import('/js/ace/ace.js', (ace) => { FuseBox.import(siteRoot + '/js/ace/ace.js', (ace) => {
let scEditor = ace.edit('source-display') let scEditor = ace.edit('source-display')
scEditor.setTheme('ace/theme/dawn') scEditor.setTheme('ace/theme/dawn')
scEditor.getSession().setMode('ace/mode/markdown') scEditor.getSession().setMode('ace/mode/markdown')

View File

@ -1,4 +0,0 @@
window.MathJax = {
root: '/js/mathjax',
delayStartupUntil: 'configured'
}

View File

@ -134,14 +134,6 @@ git:
# Whether to use user email as author in commits # Whether to use user email as author in commits
showUserEmail: true showUserEmail: true
# ---------------------------------------------------------------------
# Features
# ---------------------------------------------------------------------
# You can enable / disable specific features below
features:
mathjax: true
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# External Logging # External Logging
# --------------------------------------------------------------------- # ---------------------------------------------------------------------

View File

@ -53,17 +53,9 @@ const ALIASES = {
'vue-lodash': 'vue-lodash/dist/vue-lodash.min.js' 'vue-lodash': 'vue-lodash/dist/vue-lodash.min.js'
} }
const SHIMS = { const SHIMS = {
_preinit: {
source: '.build/_preinit.js',
exports: '_preinit'
},
jquery: { jquery: {
source: 'node_modules/jquery/dist/jquery.js', source: 'node_modules/jquery/dist/jquery.js',
exports: '$' exports: '$'
},
mathjax: {
source: 'node_modules/mathjax/MathJax.js',
exports: 'MathJax'
} }
} }

View File

@ -132,14 +132,6 @@ git:
# Whether to use user email as author in commits # Whether to use user email as author in commits
showUserEmail: true showUserEmail: true
# ---------------------------------------------------------------------
# Features
# ---------------------------------------------------------------------
# You can enable / disable specific features below
features:
mathjax: true
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# External Logging # External Logging
# --------------------------------------------------------------------- # ---------------------------------------------------------------------

View File

@ -132,14 +132,6 @@ git:
# Whether to use user email as author in commits # Whether to use user email as author in commits
showUserEmail: $(WIKI_SHOW_USER_EMAIL) showUserEmail: $(WIKI_SHOW_USER_EMAIL)
# ---------------------------------------------------------------------
# Features
# ---------------------------------------------------------------------
# You can enable / disable specific features below
features:
mathjax: true
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# External Logging # External Logging
# --------------------------------------------------------------------- # ---------------------------------------------------------------------

View File

@ -85,6 +85,7 @@
"markdown-it-footnote": "^3.0.1", "markdown-it-footnote": "^3.0.1",
"markdown-it-mathjax": "^2.0.0", "markdown-it-mathjax": "^2.0.0",
"markdown-it-task-lists": "^2.0.1", "markdown-it-task-lists": "^2.0.1",
"mathjax-node": "^1.1.0",
"memdown": "^1.2.4", "memdown": "^1.2.4",
"mime-types": "^2.1.15", "mime-types": "^2.1.15",
"moment": "^2.18.1", "moment": "^2.18.1",
@ -100,7 +101,7 @@
"passport-facebook": "^2.1.1", "passport-facebook": "^2.1.1",
"passport-github2": "^0.1.10", "passport-github2": "^0.1.10",
"passport-google-oauth20": "^1.0.0", "passport-google-oauth20": "^1.0.0",
"passport-ldapauth": "^1.0.0", "passport-ldapauth": "^2.0.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"passport-slack": "0.0.7", "passport-slack": "0.0.7",
"passport-windowslive": "^1.0.2", "passport-windowslive": "^1.0.2",
@ -134,9 +135,9 @@
"brace": "^0.10.0", "brace": "^0.10.0",
"colors": "^1.1.2", "colors": "^1.1.2",
"consolidate": "^0.14.5", "consolidate": "^0.14.5",
"eslint": "^3.19.0", "eslint": "^4.0.0",
"eslint-config-standard": "^10.2.1", "eslint-config-standard": "^10.2.1",
"eslint-plugin-import": "^2.3.0", "eslint-plugin-import": "^2.6.0",
"eslint-plugin-node": "^5.0.0", "eslint-plugin-node": "^5.0.0",
"eslint-plugin-promise": "^3.5.0", "eslint-plugin-promise": "^3.5.0",
"eslint-plugin-standard": "^3.0.1", "eslint-plugin-standard": "^3.0.1",
@ -154,12 +155,12 @@
"node-sass": "^4.5.3", "node-sass": "^4.5.3",
"nodemon": "^1.11.0", "nodemon": "^1.11.0",
"pug-lint": "^2.4.0", "pug-lint": "^2.4.0",
"snyk": "^1.34.3", "snyk": "^1.36.0",
"twemoji-awesome": "^1.0.6", "twemoji-awesome": "^1.0.6",
"typescript": "^2.3.4", "typescript": "^2.3.4",
"uglify-es": "^3.0.15", "uglify-es": "^3.0.19",
"uglify-js": "^3.0.15", "uglify-js": "^3.0.19",
"vee-validate": "^2.0.0-rc.5", "vee-validate": "^2.0.0-rc.6",
"vue": "^2.3.4", "vue": "^2.3.4",
"vue-clipboards": "^1.0.2", "vue-clipboards": "^1.0.2",
"vue-lodash": "^1.0.3", "vue-lodash": "^1.0.3",

View File

@ -22,7 +22,7 @@ module.exports = {
* *
* @return {Object} Entries model instance * @return {Object} Entries model instance
*/ */
init () { init() {
let self = this let self = this
self._repoPath = path.resolve(ROOTPATH, appconfig.paths.repo) self._repoPath = path.resolve(ROOTPATH, appconfig.paths.repo)
@ -39,7 +39,7 @@ module.exports = {
* @param {String} entryPath The entry path * @param {String} entryPath The entry path
* @return {Promise<Boolean>} True if exists, false otherwise * @return {Promise<Boolean>} True if exists, false otherwise
*/ */
exists (entryPath) { exists(entryPath) {
let self = this let self = this
return self.fetchOriginal(entryPath, { return self.fetchOriginal(entryPath, {
@ -62,7 +62,7 @@ module.exports = {
* @param {String} entryPath The entry path * @param {String} entryPath The entry path
* @return {Promise<Object>} Page Data * @return {Promise<Object>} Page Data
*/ */
fetch (entryPath) { fetch(entryPath) {
let self = this let self = this
let cpath = entryHelper.getCachePath(entryPath) let cpath = entryHelper.getCachePath(entryPath)
@ -97,7 +97,7 @@ module.exports = {
* @param {Object} options The options * @param {Object} options The options
* @return {Promise<Object>} Page data * @return {Promise<Object>} Page data
*/ */
fetchOriginal (entryPath, options) { fetchOriginal(entryPath, options) {
let self = this let self = this
let fpath = entryHelper.getFullPath(entryPath) let fpath = entryHelper.getFullPath(entryPath)
@ -115,43 +115,47 @@ module.exports = {
return fs.statAsync(fpath).then((st) => { return fs.statAsync(fpath).then((st) => {
if (st.isFile()) { if (st.isFile()) {
return fs.readFileAsync(fpath, 'utf8').then((contents) => { return fs.readFileAsync(fpath, 'utf8').then((contents) => {
let htmlProcessor = (options.parseMarkdown) ? mark.parseContent(contents) : Promise.resolve('')
// Parse contents // Parse contents
let pageData = { return htmlProcessor.then(html => {
markdown: (options.includeMarkdown) ? contents : '', let pageData = {
html: (options.parseMarkdown) ? mark.parseContent(contents) : '', markdown: (options.includeMarkdown) ? contents : '',
meta: (options.parseMeta) ? mark.parseMeta(contents) : {}, html,
tree: (options.parseTree) ? mark.parseTree(contents) : [] meta: (options.parseMeta) ? mark.parseMeta(contents) : {},
} tree: (options.parseTree) ? mark.parseTree(contents) : []
if (!pageData.meta.title) {
pageData.meta.title = _.startCase(entryPath)
}
pageData.meta.path = entryPath
// Get parent
let parentPromise = (options.includeParentInfo) ? self.getParentInfo(entryPath).then((parentData) => {
return (pageData.parent = parentData)
}).catch((err) => { // eslint-disable-line handle-callback-err
return (pageData.parent = false)
}) : Promise.resolve(true)
return parentPromise.then(() => {
// Cache to disk
if (options.cache) {
let cacheData = JSON.stringify(_.pick(pageData, ['html', 'meta', 'tree', 'parent']), false, false, false)
return fs.writeFileAsync(cpath, cacheData).catch((err) => {
winston.error('Unable to write to cache! Performance may be affected.')
winston.error(err)
return true
})
} else {
return true
} }
}).return(pageData)
if (!pageData.meta.title) {
pageData.meta.title = _.startCase(entryPath)
}
pageData.meta.path = entryPath
// Get parent
let parentPromise = (options.includeParentInfo) ? self.getParentInfo(entryPath).then((parentData) => {
return (pageData.parent = parentData)
}).catch((err) => { // eslint-disable-line handle-callback-err
return (pageData.parent = false)
}) : Promise.resolve(true)
return parentPromise.then(() => {
// Cache to disk
if (options.cache) {
let cacheData = JSON.stringify(_.pick(pageData, ['html', 'meta', 'tree', 'parent']), false, false, false)
return fs.writeFileAsync(cpath, cacheData).catch((err) => {
winston.error('Unable to write to cache! Performance may be affected.')
winston.error(err)
return true
})
} else {
return true
}
}).return(pageData)
})
}) })
} else { } else {
return false return false
@ -167,7 +171,7 @@ module.exports = {
* @param {String} entryPath The entry path * @param {String} entryPath The entry path
* @return {Promise<Object|False>} The parent information. * @return {Promise<Object|False>} The parent information.
*/ */
getParentInfo (entryPath) { getParentInfo(entryPath) {
if (_.includes(entryPath, '/')) { if (_.includes(entryPath, '/')) {
let parentParts = _.initial(_.split(entryPath, '/')) let parentParts = _.initial(_.split(entryPath, '/'))
let parentPath = _.join(parentParts, '/') let parentPath = _.join(parentParts, '/')
@ -202,7 +206,7 @@ module.exports = {
* @param {Object} author The author user object * @param {Object} author The author user object
* @return {Promise<Boolean>} True on success, false on failure * @return {Promise<Boolean>} True on success, false on failure
*/ */
update (entryPath, contents, author) { update(entryPath, contents, author) {
let self = this let self = this
let fpath = entryHelper.getFullPath(entryPath) let fpath = entryHelper.getFullPath(entryPath)
@ -228,7 +232,7 @@ module.exports = {
* @param {String} entryPath The entry path * @param {String} entryPath The entry path
* @return {Promise} Promise of the operation * @return {Promise} Promise of the operation
*/ */
updateCache (entryPath) { updateCache(entryPath) {
let self = this let self = this
return self.fetchOriginal(entryPath, { return self.fetchOriginal(entryPath, {
@ -256,21 +260,21 @@ module.exports = {
return db.Entry.findOneAndUpdate({ return db.Entry.findOneAndUpdate({
_id: content.entryPath _id: content.entryPath
}, { }, {
_id: content.entryPath, _id: content.entryPath,
title: content.meta.title || content.entryPath, title: content.meta.title || content.entryPath,
subtitle: content.meta.subtitle || '', subtitle: content.meta.subtitle || '',
parentTitle: content.parent.title || '', parentTitle: content.parent.title || '',
parentPath: parentPath, parentPath: parentPath,
isDirectory: false, isDirectory: false,
isEntry: true isEntry: true
}, { }, {
new: true, new: true,
upsert: true upsert: true
}).then(result => { }).then(result => {
let plainResult = result.toObject() let plainResult = result.toObject()
plainResult.text = content.text plainResult.text = content.text
return plainResult return plainResult
}) })
}).then(result => { }).then(result => {
return self.updateTreeInfo().then(() => { return self.updateTreeInfo().then(() => {
return result return result
@ -286,7 +290,7 @@ module.exports = {
* *
* @returns {Promise<Boolean>} Promise of the operation * @returns {Promise<Boolean>} Promise of the operation
*/ */
updateTreeInfo () { updateTreeInfo() {
return db.Entry.distinct('parentPath', { parentPath: { $ne: '' } }).then(allPaths => { return db.Entry.distinct('parentPath', { parentPath: { $ne: '' } }).then(allPaths => {
if (allPaths.length > 0) { if (allPaths.length > 0) {
return Promise.map(allPaths, pathItem => { return Promise.map(allPaths, pathItem => {
@ -311,7 +315,7 @@ module.exports = {
* @param {Object} author The author user object * @param {Object} author The author user object
* @return {Promise<Boolean>} True on success, false on failure * @return {Promise<Boolean>} True on success, false on failure
*/ */
create (entryPath, contents, author) { create(entryPath, contents, author) {
let self = this let self = this
return self.exists(entryPath).then((docExists) => { return self.exists(entryPath).then((docExists) => {
@ -338,7 +342,7 @@ module.exports = {
* @param {Object} author The author user object * @param {Object} author The author user object
* @return {Promise<Boolean>} True on success, false on failure * @return {Promise<Boolean>} True on success, false on failure
*/ */
makePersistent (entryPath, contents, author) { makePersistent(entryPath, contents, author) {
let fpath = entryHelper.getFullPath(entryPath) let fpath = entryHelper.getFullPath(entryPath)
return fs.outputFileAsync(fpath, contents).then(() => { return fs.outputFileAsync(fpath, contents).then(() => {
@ -354,7 +358,7 @@ module.exports = {
* @param {Object} author The author user object * @param {Object} author The author user object
* @return {Promise} Promise of the operation * @return {Promise} Promise of the operation
*/ */
move (entryPath, newEntryPath, author) { move(entryPath, newEntryPath, author) {
let self = this let self = this
if (_.isEmpty(entryPath) || entryPath === 'home') { if (_.isEmpty(entryPath) || entryPath === 'home') {
@ -387,7 +391,7 @@ module.exports = {
* @param {String} entryPath The entry path * @param {String} entryPath The entry path
* @return {Promise<String>} Starter content * @return {Promise<String>} Starter content
*/ */
getStarter (entryPath) { getStarter(entryPath) {
let formattedTitle = _.startCase(_.last(_.split(entryPath, '/'))) let formattedTitle = _.startCase(_.last(_.split(entryPath, '/')))
return fs.readFileAsync(path.join(SERVERPATH, 'app/content/create.md'), 'utf8').then((contents) => { return fs.readFileAsync(path.join(SERVERPATH, 'app/content/create.md'), 'utf8').then((contents) => {
@ -402,7 +406,7 @@ module.exports = {
* @param {Object} usr Current user * @param {Object} usr Current user
* @return {Promise<Array>} List of entries * @return {Promise<Array>} List of entries
*/ */
getFromTree (basePath, usr) { getFromTree(basePath, usr) {
return db.Entry.find({ parentPath: basePath }, 'title parentPath isDirectory isEntry').sort({ title: 'asc' }).then(results => { return db.Entry.find({ parentPath: basePath }, 'title parentPath isDirectory isEntry').sort({ title: 'asc' }).then(results => {
return _.filter(results, r => { return _.filter(results, r => {
return rights.checkRole('/' + r._id, usr.rights, 'read') return rights.checkRole('/' + r._id, usr.rights, 'read')
@ -410,7 +414,7 @@ module.exports = {
}) })
}, },
getHistory (entryPath) { getHistory(entryPath) {
return db.Entry.findOne({ _id: entryPath, isEntry: true }).then(entry => { return db.Entry.findOne({ _id: entryPath, isEntry: true }).then(entry => {
if (!entry) { return false } if (!entry) { return false }
return git.getHistory(entryPath).then(history => { return git.getHistory(entryPath).then(history => {

View File

@ -1,5 +1,6 @@
'use strict' 'use strict'
const Promise = require('bluebird')
const md = require('markdown-it') const md = require('markdown-it')
const mdEmoji = require('markdown-it-emoji') const mdEmoji = require('markdown-it-emoji')
const mdTaskLists = require('markdown-it-task-lists') const mdTaskLists = require('markdown-it-task-lists')
@ -9,6 +10,8 @@ const mdFootnote = require('markdown-it-footnote')
const mdExternalLinks = require('markdown-it-external-links') const mdExternalLinks = require('markdown-it-external-links')
const mdExpandTabs = require('markdown-it-expand-tabs') const mdExpandTabs = require('markdown-it-expand-tabs')
const mdAttrs = require('markdown-it-attrs') const mdAttrs = require('markdown-it-attrs')
const mdMathjax = require('markdown-it-mathjax')()
const mathjax = require('mathjax-node')
const hljs = require('highlight.js') const hljs = require('highlight.js')
const cheerio = require('cheerio') const cheerio = require('cheerio')
const _ = require('lodash') const _ = require('lodash')
@ -50,11 +53,7 @@ var mkdown = md({
tabWidth: 4 tabWidth: 4
}) })
.use(mdAttrs) .use(mdAttrs)
.use(mdMathjax)
if (appconfig) {
const mdMathjax = require('markdown-it-mathjax')
mkdown.use(mdMathjax())
}
// Rendering rules // Rendering rules
@ -87,9 +86,40 @@ const videoRules = [
} }
] ]
// Non-markdown filter // Regex
const textRegex = new RegExp('\\b[a-z0-9-.,' + appdata.regex.cjk + appdata.regex.arabic + ']+\\b', 'g') const textRegex = new RegExp('\\b[a-z0-9-.,' + appdata.regex.cjk + appdata.regex.arabic + ']+\\b', 'g')
const mathRegex = [
{
format: 'TeX',
regex: /\\\[([\s\S]*?)\\\]/g
},
{
format: 'inline-TeX',
regex: /\\\((.*?)\\\)/g
},
{
format: 'MathML',
regex: /<math([\s\S]*?)<\/math>/g
}
]
// MathJax
mathjax.config({
MathJax: {
jax: ['input/TeX', 'input/MathML', 'output/SVG'],
extensions: ['tex2jax.js', 'mml2jax.js'],
TeX: {
extensions: ['AMSmath.js', 'AMSsymbols.js', 'noErrors.js', 'noUndefined.js']
},
SVG: {
scale: 120,
font: 'STIX-Web'
}
}
})
mathjax.start()
/** /**
* Parse markdown content and build TOC tree * Parse markdown content and build TOC tree
@ -177,11 +207,10 @@ const parseTree = (content) => {
* Parse markdown content to HTML * Parse markdown content to HTML
* *
* @param {String} content Markdown content * @param {String} content Markdown content
* @return {String} HTML formatted content * @return {Promise<String>} Promise
*/ */
const parseContent = (content) => { const parseContent = (content) => {
let output = mkdown.render(content) let cr = cheerio.load(mkdown.render(content))
let cr = cheerio.load(output)
if (cr.root().children().length < 1) { if (cr.root().children().length < 1) {
return '' return ''
@ -265,9 +294,55 @@ const parseContent = (content) => {
cr(elm).removeClass('align-center') cr(elm).removeClass('align-center')
}) })
output = cr.html() // Mathjax Post-processor
return output return processMathjax(cr.html())
}
/**
* Process MathJax expressions
*
* @param {String} content HTML content
* @returns {Promise<String>} Promise
*/
const processMathjax = (content) => {
let matchStack = []
let replaceStack = []
let currentMatch
let mathjaxState = {}
_.forEach(mathRegex, mode => {
do {
currentMatch = mode.regex.exec(content)
if (currentMatch) {
matchStack.push(currentMatch[0])
replaceStack.push(
new Promise((resolve, reject) => {
mathjax.typeset({
math: (mode.format === 'MathML') ? currentMatch[0] : currentMatch[1],
format: mode.format,
speakText: false,
svg: true,
state: mathjaxState
}, result => {
if (!result.errors) {
resolve(result.svg)
} else {
reject(new Error(result.errors.join(', ')))
}
})
})
)
}
} while (currentMatch)
})
return (matchStack.length > 0) ? Promise.all(replaceStack).then(results => {
_.forEach(matchStack, (repMatch, idx) => {
content = content.replace(repMatch, results[idx])
})
return content
}) : Promise.resolve(content)
} }
/** /**
@ -314,11 +389,13 @@ module.exports = {
* @return {Object} Object containing meta, html and tree data * @return {Object} Object containing meta, html and tree data
*/ */
parse(content) { parse(content) {
return { return parseContent(content).then(html => {
meta: parseMeta(content), return {
html: parseContent(content), meta: parseMeta(content),
tree: parseTree(content) html,
} tree: parseTree(content)
}
})
}, },
parseContent, parseContent,

View File

@ -4,32 +4,24 @@ block rootNavRight
i.nav-item#notifload i.nav-item#notifload
.nav-item .nav-item
a.button(href='/' + pageData.meta._id) a.button(href='/' + pageData.meta._id)
i.icon-circle-check i.nc-icon-outline.ui-3_select
span= t('nav.viewlatest') span= t('nav.viewlatest')
block content block content
.container.is-fluid
.columns.is-gapless
#page-type-history.page-type-container(data-entrypath=pageData.meta._id) .column.is-narrow.is-hidden-touch.sidebar
.container.is-fluid.has-mkcontent
.columns.is-gapless
.column.is-narrow.is-hidden-touch.sidebar aside.stickyscroll
.sidebar-label
span= t('sidebar.pastversions')
ul.sidebar-menu
each item, index in pageData.history
- var itemDate = moment(item.date)
li: a.is-multiline(class={ 'is-active': index < 1 }, href='', title=itemDate.format('LLLL'))
span= itemDate.calendar(null, { sameElse: 'llll'})
span.is-small= item.commitAbbr
aside.stickyscroll .column
.sidebar-label history(current-path=pageData.meta._id)
span= t('sidebar.pastversions')
ul.sidebar-menu
each item, index in pageData.history
- var itemDate = moment(item.date)
li: a.is-multiline(class={ 'is-active': index < 1 }, href='', title=itemDate.format('LLLL'))
span= itemDate.calendar(null, { sameElse: 'llll'})
span.is-small= item.commitAbbr
.column
.hero
h1.title#title= pageData.meta.title
if pageData.meta.subtitle
h2.subtitle= pageData.meta.subtitle
.content.mkcontent
!= pageData.html

View File

@ -18,8 +18,8 @@ block rootNavRight
a.button.is-outlined(href='/source/' + pageData.meta.path) a.button.is-outlined(href='/source/' + pageData.meta.path)
i.nc-icon-outline.education_paper i.nc-icon-outline.education_paper
span= t('nav.source') span= t('nav.source')
//- a.button.is-outlined(href='/hist/' + pageData.meta.path) a.button.is-outlined(href='/hist/' + pageData.meta.path)
i.icon-clock i.nc-icon-outline.ui-2_time
span= t('nav.history') span= t('nav.history')
if rights.write if rights.write
a.button(href='/edit/' + pageData.meta.path) a.button(href='/edit/' + pageData.meta.path)

808
yarn.lock

File diff suppressed because it is too large Load Diff