| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418 | 
							- 'use strict'
 
- /* global winston */
 
- const Promise = require('bluebird')
 
- const md = require('markdown-it')
 
- const mdEmoji = require('markdown-it-emoji')
 
- const mdTaskLists = require('markdown-it-task-lists')
 
- const mdAbbr = require('markdown-it-abbr')
 
- const mdAnchor = require('markdown-it-anchor')
 
- 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')
 
- const mdRemove = require('remove-markdown')
 
- // Load plugins
 
- var mkdown = md({
 
-   html: true,
 
-   breaks: appconfig.features.linebreaks,
 
-   linkify: true,
 
-   typography: true,
 
-   highlight(str, lang) {
 
-     if (appconfig.theme.code.colorize && lang && hljs.getLanguage(lang)) {
 
-       try {
 
-         return '<pre class="hljs"><code>' + hljs.highlight(lang, str, true).value + '</code></pre>'
 
-       } catch (err) {
 
-         return '<pre><code>' + _.escape(str) + '</code></pre>'
 
-       }
 
-     }
 
-     return '<pre><code>' + _.escape(str) + '</code></pre>'
 
-   }
 
- })
 
-   .use(mdEmoji)
 
-   .use(mdTaskLists)
 
-   .use(mdAbbr)
 
-   .use(mdAnchor, {
 
-     slugify: _.kebabCase,
 
-     permalink: true,
 
-     permalinkClass: 'toc-anchor nc-icon-outline location_bookmark-add',
 
-     permalinkSymbol: '',
 
-     permalinkBefore: true
 
-   })
 
-   .use(mdFootnote)
 
-   .use(mdExternalLinks, {
 
-     externalClassName: 'external-link',
 
-     internalClassName: 'internal-link'
 
-   })
 
-   .use(mdExpandTabs, {
 
-     tabWidth: 4
 
-   })
 
-   .use(mdAttrs)
 
- if (appconfig.features.mathjax) {
 
-   mkdown.use(mdMathjax)
 
- }
 
- // Rendering rules
 
- mkdown.renderer.rules.emoji = function (token, idx) {
 
-   return '<i class="twa twa-' + _.replace(token[idx].markup, /_/g, '-') + '"></i>'
 
- }
 
- // Video rules
 
- const videoRules = [
 
-   {
 
-     selector: 'a.youtube',
 
-     regexp: new RegExp(/(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|&v(?:i)?=))([^#&?]*).*/i),
 
-     output: '<iframe width="640" height="360" src="https://www.youtube.com/embed/{0}?rel=0" frameborder="0" allowfullscreen></iframe>'
 
-   },
 
-   {
 
-     selector: 'a.vimeo',
 
-     regexp: new RegExp(/vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^/]*)\/videos\/|album\/(?:\d+)\/video\/|)(\d+)(?:$|\/|\?)/i),
 
-     output: '<iframe src="https://player.vimeo.com/video/{0}" width="640" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>'
 
-   },
 
-   {
 
-     selector: 'a.dailymotion',
 
-     regexp: new RegExp(/(?:dailymotion\.com(?:\/embed)?(?:\/video|\/hub)|dai\.ly)\/([0-9a-z]+)(?:[-_0-9a-zA-Z]+(?:#video=)?([a-z0-9]+)?)?/i),
 
-     output: '<iframe width="640" height="360" src="//www.dailymotion.com/embed/video/{0}?endscreen-enable=false" frameborder="0" allowfullscreen></iframe>'
 
-   },
 
-   {
 
-     selector: 'a.video',
 
-     regexp: false,
 
-     output: '<video width="640" height="360" controls preload="metadata"><source src="{0}" type="video/mp4"></video>'
 
-   }
 
- ]
 
- // 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'
 
-     }
 
-   }
 
- })
 
- /**
 
-  * Parse markdown content and build TOC tree
 
-  *
 
-  * @param      {(Function|string)}  content  Markdown content
 
-  * @return     {Array}             TOC tree
 
-  */
 
- const parseTree = (content) => {
 
-   content = content.replace(/<!--(.|\t|\n|\r)*?-->/g, '')
 
-   let tokens = md().parse(content, {})
 
-   let tocArray = []
 
-   // -> Extract headings and their respective levels
 
-   for (let i = 0; i < tokens.length; i++) {
 
-     if (tokens[i].type !== 'heading_close') {
 
-       continue
 
-     }
 
-     const heading = tokens[i - 1]
 
-     const headingclose = tokens[i]
 
-     if (heading.type === 'inline') {
 
-       let content = ''
 
-       let anchor = ''
 
-       if (heading.children && heading.children.length > 0 && heading.children[0].type === 'link_open') {
 
-         content = mdRemove(heading.children[1].content)
 
-         anchor = _.kebabCase(content)
 
-       } else {
 
-         content = mdRemove(heading.content)
 
-         anchor = _.kebabCase(heading.children.reduce((acc, t) => acc + t.content, ''))
 
-       }
 
-       tocArray.push({
 
-         content,
 
-         anchor,
 
-         level: +headingclose.tag.substr(1, 1)
 
-       })
 
-     }
 
-   }
 
-   // -> Exclude levels deeper than 2
 
-   _.remove(tocArray, (n) => { return n.level > 2 })
 
-   // -> Build tree from flat array
 
-   return _.reduce(tocArray, (tree, v) => {
 
-     let treeLength = tree.length - 1
 
-     if (v.level < 2) {
 
-       tree.push({
 
-         content: v.content,
 
-         anchor: v.anchor,
 
-         nodes: []
 
-       })
 
-     } else {
 
-       let lastNodeLevel = 1
 
-       let GetNodePath = (startPos) => {
 
-         lastNodeLevel++
 
-         if (_.isEmpty(startPos)) {
 
-           startPos = 'nodes'
 
-         }
 
-         if (lastNodeLevel === v.level) {
 
-           return startPos
 
-         } else {
 
-           return GetNodePath(startPos + '[' + (_.at(tree[treeLength], startPos).length - 1) + '].nodes')
 
-         }
 
-       }
 
-       let lastNodePath = GetNodePath()
 
-       let lastNode = _.get(tree[treeLength], lastNodePath)
 
-       if (lastNode) {
 
-         lastNode.push({
 
-           content: v.content,
 
-           anchor: v.anchor,
 
-           nodes: []
 
-         })
 
-         _.set(tree[treeLength], lastNodePath, lastNode)
 
-       }
 
-     }
 
-     return tree
 
-   }, [])
 
- }
 
- /**
 
-  * Parse markdown content to HTML
 
-  *
 
-  * @param      {String}    content  Markdown content
 
-  * @return     {Promise<String>} Promise
 
-  */
 
- const parseContent = (content) => {
 
-   let cr = cheerio.load(mkdown.render(content))
 
-   if (cr.root().children().length < 1) {
 
-     return ''
 
-   }
 
-   // -> Check for empty first element
 
-   let firstElm = cr.root().children().first()[0]
 
-   if (firstElm.type === 'tag' && firstElm.name === 'p') {
 
-     let firstElmChildren = firstElm.children
 
-     if (firstElmChildren.length < 1) {
 
-       firstElm.remove()
 
-     } else if (firstElmChildren.length === 1 && firstElmChildren[0].type === 'tag' && firstElmChildren[0].name === 'img') {
 
-       cr(firstElm).addClass('is-gapless')
 
-     }
 
-   }
 
-   // -> Remove links in headers
 
-   cr('h1 > a:not(.toc-anchor), h2 > a:not(.toc-anchor), h3 > a:not(.toc-anchor)').each((i, elm) => {
 
-     let txtLink = cr(elm).text()
 
-     cr(elm).replaceWith(txtLink)
 
-   })
 
-   // -> Re-attach blockquote styling classes to their parents
 
-   cr('blockquote').each((i, elm) => {
 
-     if (cr(elm).children().length > 0) {
 
-       let bqLastChild = cr(elm).children().last()[0]
 
-       let bqLastChildClasses = cr(bqLastChild).attr('class')
 
-       if (bqLastChildClasses && bqLastChildClasses.length > 0) {
 
-         cr(bqLastChild).removeAttr('class')
 
-         cr(elm).addClass(bqLastChildClasses)
 
-       }
 
-     }
 
-   })
 
-   // -> Enclose content below headers
 
-   cr('h2').each((i, elm) => {
 
-     let subH2Content = cr(elm).nextUntil('h1, h2')
 
-     cr(elm).after('<div class="indent-h2"></div>')
 
-     let subH2Container = cr(elm).next('.indent-h2')
 
-     _.forEach(subH2Content, (ch) => {
 
-       cr(subH2Container).append(ch)
 
-     })
 
-   })
 
-   cr('h3').each((i, elm) => {
 
-     let subH3Content = cr(elm).nextUntil('h1, h2, h3')
 
-     cr(elm).after('<div class="indent-h3"></div>')
 
-     let subH3Container = cr(elm).next('.indent-h3')
 
-     _.forEach(subH3Content, (ch) => {
 
-       cr(subH3Container).append(ch)
 
-     })
 
-   })
 
-   // Replace video links with embeds
 
-   _.forEach(videoRules, (vrule) => {
 
-     cr(vrule.selector).each((i, elm) => {
 
-       let originLink = cr(elm).attr('href')
 
-       if (vrule.regexp) {
 
-         let vidMatches = originLink.match(vrule.regexp)
 
-         if ((vidMatches && _.isArray(vidMatches))) {
 
-           vidMatches = _.filter(vidMatches, (f) => {
 
-             return f && _.isString(f)
 
-           })
 
-           originLink = _.last(vidMatches)
 
-         }
 
-       }
 
-       let processedLink = _.replace(vrule.output, '{0}', originLink)
 
-       cr(elm).replaceWith(processedLink)
 
-     })
 
-   })
 
-   // Apply align-center to parent
 
-   cr('img.align-center').each((i, elm) => {
 
-     cr(elm).parent().addClass('align-center')
 
-     cr(elm).removeClass('align-center')
 
-   })
 
-   // Mathjax Post-processor
 
-   if (appconfig.features.mathjax) {
 
-     return processMathjax(cr.html())
 
-   } else {
 
-     return Promise.resolve(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,
 
-               timeout: 30 * 1000
 
-             }, result => {
 
-               if (!result.errors) {
 
-                 resolve(result.svg)
 
-               } else {
 
-                 resolve(currentMatch[0])
 
-                 winston.warn(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)
 
- }
 
- /**
 
-  * Parse meta-data tags from content
 
-  *
 
-  * @param      {String}  content  Markdown content
 
-  * @return     {Object}  Properties found in the content and their values
 
-  */
 
- const parseMeta = (content) => {
 
-   let commentMeta = new RegExp('<!-- ?([a-zA-Z]+):(.*)-->', 'g')
 
-   let results = {}
 
-   let match
 
-   while ((match = commentMeta.exec(content)) !== null) {
 
-     results[_.toLower(match[1])] = _.trim(match[2])
 
-   }
 
-   return results
 
- }
 
- /**
 
-  * Strips non-text elements from Markdown content
 
-  *
 
-  * @param      {String}  content  Markdown-formatted content
 
-  * @return     {String}  Text-only version
 
-  */
 
- const removeMarkdown = (content) => {
 
-   return _.join(mdRemove(_.chain(content)
 
-     .replace(/<!-- ?([a-zA-Z]+):(.*)-->/g, '')
 
-     .replace(/```([^`]|`)+?```/g, '')
 
-     .replace(/`[^`]+`/g, '')
 
-     .replace(new RegExp('(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?', 'g'), '')
 
-     .deburr()
 
-     .toLower()
 
-     .value()
 
-   ).replace(/\r?\n|\r/g, ' ').match(textRegex), ' ')
 
- }
 
- module.exports = {
 
-   /**
 
-    * Parse content and return all data
 
-    *
 
-    * @param      {String}  content  Markdown-formatted content
 
-    * @return     {Object}  Object containing meta, html and tree data
 
-    */
 
-   parse(content) {
 
-     return parseContent(content).then(html => {
 
-       return {
 
-         meta: parseMeta(content),
 
-         html,
 
-         tree: parseTree(content)
 
-       }
 
-     })
 
-   },
 
-   parseContent,
 
-   parseMeta,
 
-   parseTree,
 
-   removeMarkdown
 
- }
 
 
  |