瀏覽代碼

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

NGPixel 8 年之前
父節點
當前提交
3d9aa18c05

+ 0 - 5
.build/_preinit.js

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

+ 0 - 65
.build/_tasks.js

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

+ 4 - 2
client/js/app.js

@@ -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,

+ 2 - 2
client/js/components/editor.component.js

@@ -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',

+ 41 - 0
client/js/components/history.vue

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

+ 69 - 69
client/js/components/search.vue

@@ -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 }')
@@ -18,81 +18,81 @@
 </template>
 
 <script>
-  export default {
-    data () {
-      return {
-        searchq: '',
-        searchres: [],
-        searchsuggest: [],
-        searchload: 0,
-        searchactive: false,
-        searchmoveidx: 0,
-        searchmovekey: '',
-        searchmovearr: []
+export default {
+  data() {
+    return {
+      searchq: '',
+      searchres: [],
+      searchsuggest: [],
+      searchload: 0,
+      searchactive: false,
+      searchmoveidx: 0,
+      searchmovekey: '',
+      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
       }
     },
-    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 = ''
-        }
+    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 = ''
     },
-    methods: {
-      useSuggestion: function (sug) {
-        this.searchq = sug
-      },
-      closeSearch: function() {
-        this.searchq = ''
-      },
-      moveSelectSearch: function () {
-        if (this.searchmoveidx < 1) { return }
-        let i = this.searchmoveidx - 1
+    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--
-        }
+      if (this.searchmovearr[i]) {
+        window.location.assign(siteRoot + '/' + this.searchmovearr[i].entryPath)
+      } else {
+        this.searchq = this.searchmovearr[i]
       }
     },
-    mounted: function () {
-      let self = this
-      $('main').on('click', self.closeSearch)
+    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>

+ 1 - 1
client/js/components/tree.vue

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

+ 0 - 20
client/js/pages/content-view.component.js

@@ -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()
   }
 }

+ 3 - 1
client/js/pages/source-view.component.js

@@ -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')

+ 0 - 4
client/js/pre-init/mathjax.js

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

+ 0 - 8
config.sample.yml

@@ -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
 # ---------------------------------------------------------------------

+ 0 - 8
fuse.js

@@ -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'
   }
 }
 

+ 0 - 8
npm/configs/config.docker.yml

@@ -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
 # ---------------------------------------------------------------------

+ 0 - 8
npm/configs/config.heroku.yml

@@ -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
 # ---------------------------------------------------------------------

+ 8 - 7
package.json

@@ -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",

+ 61 - 57
server/libs/entries.js

@@ -22,7 +22,7 @@ module.exports = {
    *
    * @return     {Object}  Entries model instance
    */
-  init () {
+  init() {
     let self = this
 
     self._repoPath = path.resolve(ROOTPATH, appconfig.paths.repo)
@@ -39,7 +39,7 @@ module.exports = {
    * @param      {String}  entryPath  The entry path
    * @return     {Promise<Boolean>}  True if exists, false otherwise
    */
-  exists (entryPath) {
+  exists(entryPath) {
     let self = this
 
     return self.fetchOriginal(entryPath, {
@@ -62,7 +62,7 @@ module.exports = {
    * @param      {String}           entryPath  The entry path
    * @return     {Promise<Object>}  Page Data
    */
-  fetch (entryPath) {
+  fetch(entryPath) {
     let self = this
 
     let cpath = entryHelper.getCachePath(entryPath)
@@ -97,7 +97,7 @@ module.exports = {
    * @param      {Object}           options    The options
    * @return     {Promise<Object>}  Page data
    */
-  fetchOriginal (entryPath, options) {
+  fetchOriginal(entryPath, options) {
     let self = this
 
     let fpath = entryHelper.getFullPath(entryPath)
@@ -115,43 +115,47 @@ 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
 
-          let pageData = {
-            markdown: (options.includeMarkdown) ? contents : '',
-            html: (options.parseMarkdown) ? mark.parseContent(contents) : '',
-            meta: (options.parseMeta) ? mark.parseMeta(contents) : {},
-            tree: (options.parseTree) ? mark.parseTree(contents) : []
-          }
+          return htmlProcessor.then(html => {
+            let pageData = {
+              markdown: (options.includeMarkdown) ? contents : '',
+              html,
+              meta: (options.parseMeta) ? mark.parseMeta(contents) : {},
+              tree: (options.parseTree) ? mark.parseTree(contents) : []
+            }
 
-          if (!pageData.meta.title) {
-            pageData.meta.title = _.startCase(entryPath)
-          }
+            if (!pageData.meta.title) {
+              pageData.meta.title = _.startCase(entryPath)
+            }
 
-          pageData.meta.path = entryPath
+            pageData.meta.path = entryPath
 
-          // Get parent
+            // 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)
+            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
+            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)
+              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
-              })
-            } else {
-              return true
-            }
-          }).return(pageData)
+              }
+            }).return(pageData)
+          })
         })
       } else {
         return false
@@ -167,7 +171,7 @@ module.exports = {
    * @param      {String}                 entryPath  The entry path
    * @return     {Promise<Object|False>}  The parent information.
    */
-  getParentInfo (entryPath) {
+  getParentInfo(entryPath) {
     if (_.includes(entryPath, '/')) {
       let parentParts = _.initial(_.split(entryPath, '/'))
       let parentPath = _.join(parentParts, '/')
@@ -202,7 +206,7 @@ module.exports = {
    * @param {Object} author The author user object
    * @return     {Promise<Boolean>}  True on success, false on failure
    */
-  update (entryPath, contents, author) {
+  update(entryPath, contents, author) {
     let self = this
     let fpath = entryHelper.getFullPath(entryPath)
 
@@ -228,7 +232,7 @@ module.exports = {
    * @param      {String}   entryPath  The entry path
    * @return     {Promise}  Promise of the operation
    */
-  updateCache (entryPath) {
+  updateCache(entryPath) {
     let self = this
 
     return self.fetchOriginal(entryPath, {
@@ -256,21 +260,21 @@ module.exports = {
       return db.Entry.findOneAndUpdate({
         _id: content.entryPath
       }, {
-        _id: content.entryPath,
-        title: content.meta.title || content.entryPath,
-        subtitle: content.meta.subtitle || '',
-        parentTitle: content.parent.title || '',
-        parentPath: parentPath,
-        isDirectory: false,
-        isEntry: true
-      }, {
-        new: true,
-        upsert: true
-      }).then(result => {
-        let plainResult = result.toObject()
-        plainResult.text = content.text
-        return plainResult
-      })
+          _id: content.entryPath,
+          title: content.meta.title || content.entryPath,
+          subtitle: content.meta.subtitle || '',
+          parentTitle: content.parent.title || '',
+          parentPath: parentPath,
+          isDirectory: false,
+          isEntry: true
+        }, {
+          new: true,
+          upsert: true
+        }).then(result => {
+          let plainResult = result.toObject()
+          plainResult.text = content.text
+          return plainResult
+        })
     }).then(result => {
       return self.updateTreeInfo().then(() => {
         return result
@@ -286,7 +290,7 @@ module.exports = {
    *
    * @returns {Promise<Boolean>} Promise of the operation
    */
-  updateTreeInfo () {
+  updateTreeInfo() {
     return db.Entry.distinct('parentPath', { parentPath: { $ne: '' } }).then(allPaths => {
       if (allPaths.length > 0) {
         return Promise.map(allPaths, pathItem => {
@@ -311,7 +315,7 @@ module.exports = {
    * @param {Object} author The author user object
    * @return {Promise<Boolean>} True on success, false on failure
    */
-  create (entryPath, contents, author) {
+  create(entryPath, contents, author) {
     let self = this
 
     return self.exists(entryPath).then((docExists) => {
@@ -338,7 +342,7 @@ module.exports = {
    * @param {Object} author The author user object
    * @return {Promise<Boolean>} True on success, false on failure
    */
-  makePersistent (entryPath, contents, author) {
+  makePersistent(entryPath, contents, author) {
     let fpath = entryHelper.getFullPath(entryPath)
 
     return fs.outputFileAsync(fpath, contents).then(() => {
@@ -354,7 +358,7 @@ module.exports = {
    * @param {Object} author The author user object
    * @return {Promise} Promise of the operation
    */
-  move (entryPath, newEntryPath, author) {
+  move(entryPath, newEntryPath, author) {
     let self = this
 
     if (_.isEmpty(entryPath) || entryPath === 'home') {
@@ -387,7 +391,7 @@ module.exports = {
    * @param      {String}           entryPath  The entry path
    * @return     {Promise<String>}  Starter content
    */
-  getStarter (entryPath) {
+  getStarter(entryPath) {
     let formattedTitle = _.startCase(_.last(_.split(entryPath, '/')))
 
     return fs.readFileAsync(path.join(SERVERPATH, 'app/content/create.md'), 'utf8').then((contents) => {
@@ -402,7 +406,7 @@ module.exports = {
    * @param {Object} usr Current user
    * @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 _.filter(results, r => {
         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 => {
       if (!entry) { return false }
       return git.getHistory(entryPath).then(history => {

+ 93 - 16
server/libs/markdown.js

@@ -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 {
-      meta: parseMeta(content),
-      html: parseContent(content),
-      tree: parseTree(content)
-    }
+    return parseContent(content).then(html => {
+      return {
+        meta: parseMeta(content),
+        html,
+        tree: parseTree(content)
+      }
+    })
   },
 
   parseContent,

+ 15 - 23
server/views/pages/history.pug

@@ -4,32 +4,24 @@ 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
+  .container.is-fluid
+    .columns.is-gapless
 
-  #page-type-history.page-type-container(data-entrypath=pageData.meta._id)
-    .container.is-fluid.has-mkcontent
-      .columns.is-gapless
+      .column.is-narrow.is-hidden-touch.sidebar
 
-        .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
-            .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
-
-        .column
-
-          .hero
-            h1.title#title= pageData.meta.title
-            if pageData.meta.subtitle
-              h2.subtitle= pageData.meta.subtitle
-          .content.mkcontent
-            != pageData.html
+      .column
+        history(current-path=pageData.meta._id)

+ 2 - 2
server/views/pages/view.pug

@@ -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)

文件差異過大導致無法顯示
+ 287 - 227
yarn.lock


部分文件因文件數量過多而無法顯示