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
*/
@ -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
*/

View File

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

View File

@ -1,6 +1,6 @@
'use strict'
/* global $ */
/* global $, siteRoot */
let mde
@ -30,7 +30,7 @@ export default {
return resp.json()
}).then(resp => {
if (resp.ok) {
window.location.assign('/' + self.currentPath)
window.location.assign(siteRoot + '/' + self.currentPath)
} else {
self.$store.dispatch('alert', {
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')
a: em {{ $t('search.nomatch') }}
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') }}
ul.searchresults-list(v-if='searchsuggest.length > 0')
li(v-for='sug in searchsuggest', v-bind:class='{ "is-active": searchmovekey === "sug." + sug }')
@ -74,7 +74,7 @@
let i = this.searchmoveidx - 1
if (this.searchmovearr[i]) {
window.location.assign('/' + this.searchmovearr[i].entryPath)
window.location.assign(siteRoot + '/' + this.searchmovearr[i].entryPath)
} else {
this.searchq = this.searchmovearr[i]
}

View File

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

View File

@ -2,8 +2,6 @@
/* global $ */
import MathJax from 'mathjax'
export default {
name: 'content-view',
data() {
@ -19,23 +17,5 @@ export default {
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'
/* global siteRoot */
export default {
name: 'source-view',
data() {
@ -7,7 +9,7 @@ export default {
},
mounted() {
let self = this
FuseBox.import('/js/ace/ace.js', (ace) => {
FuseBox.import(siteRoot + '/js/ace/ace.js', (ace) => {
let scEditor = ace.edit('source-display')
scEditor.setTheme('ace/theme/dawn')
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
showUserEmail: true
# ---------------------------------------------------------------------
# Features
# ---------------------------------------------------------------------
# You can enable / disable specific features below
features:
mathjax: true
# ---------------------------------------------------------------------
# External Logging
# ---------------------------------------------------------------------

View File

@ -53,17 +53,9 @@ const ALIASES = {
'vue-lodash': 'vue-lodash/dist/vue-lodash.min.js'
}
const SHIMS = {
_preinit: {
source: '.build/_preinit.js',
exports: '_preinit'
},
jquery: {
source: 'node_modules/jquery/dist/jquery.js',
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
showUserEmail: true
# ---------------------------------------------------------------------
# Features
# ---------------------------------------------------------------------
# You can enable / disable specific features below
features:
mathjax: true
# ---------------------------------------------------------------------
# External Logging
# ---------------------------------------------------------------------

View File

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

View File

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

View File

@ -115,11 +115,14 @@ module.exports = {
return fs.statAsync(fpath).then((st) => {
if (st.isFile()) {
return fs.readFileAsync(fpath, 'utf8').then((contents) => {
let htmlProcessor = (options.parseMarkdown) ? mark.parseContent(contents) : Promise.resolve('')
// Parse contents
return htmlProcessor.then(html => {
let pageData = {
markdown: (options.includeMarkdown) ? contents : '',
html: (options.parseMarkdown) ? mark.parseContent(contents) : '',
html,
meta: (options.parseMeta) ? mark.parseMeta(contents) : {},
tree: (options.parseTree) ? mark.parseTree(contents) : []
}
@ -153,6 +156,7 @@ module.exports = {
}
}).return(pageData)
})
})
} else {
return false
}

View File

@ -1,5 +1,6 @@
'use strict'
const Promise = require('bluebird')
const md = require('markdown-it')
const mdEmoji = require('markdown-it-emoji')
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 mdExpandTabs = require('markdown-it-expand-tabs')
const mdAttrs = require('markdown-it-attrs')
const mdMathjax = require('markdown-it-mathjax')()
const mathjax = require('mathjax-node')
const hljs = require('highlight.js')
const cheerio = require('cheerio')
const _ = require('lodash')
@ -50,11 +53,7 @@ var mkdown = md({
tabWidth: 4
})
.use(mdAttrs)
if (appconfig) {
const mdMathjax = require('markdown-it-mathjax')
mkdown.use(mdMathjax())
}
.use(mdMathjax)
// 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 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
@ -177,11 +207,10 @@ const parseTree = (content) => {
* Parse markdown content to HTML
*
* @param {String} content Markdown content
* @return {String} HTML formatted content
* @return {Promise<String>} Promise
*/
const parseContent = (content) => {
let output = mkdown.render(content)
let cr = cheerio.load(output)
let cr = cheerio.load(mkdown.render(content))
if (cr.root().children().length < 1) {
return ''
@ -265,9 +294,55 @@ const parseContent = (content) => {
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
*/
parse(content) {
return parseContent(content).then(html => {
return {
meta: parseMeta(content),
html: parseContent(content),
html,
tree: parseTree(content)
}
})
},
parseContent,

View File

@ -4,13 +4,11 @@ block rootNavRight
i.nav-item#notifload
.nav-item
a.button(href='/' + pageData.meta._id)
i.icon-circle-check
i.nc-icon-outline.ui-3_select
span= t('nav.viewlatest')
block content
#page-type-history.page-type-container(data-entrypath=pageData.meta._id)
.container.is-fluid.has-mkcontent
.container.is-fluid
.columns.is-gapless
.column.is-narrow.is-hidden-touch.sidebar
@ -26,10 +24,4 @@ block content
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
history(current-path=pageData.meta._id)

View File

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

808
yarn.lock

File diff suppressed because it is too large Load Diff