| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330 | 'use strict'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 hljs = require('highlight.js')const cheerio = require('cheerio')const _ = require('lodash')const mdRemove = require('remove-markdown')// Load pluginsvar mkdown = md({  html: true,  linkify: true,  typography: true,  highlight (str, lang) {    if (lang && hljs.getLanguage(lang)) {      try {        return '<pre class="hljs"><code>' + hljs.highlight(lang, str, true).value + '</code></pre>'      } catch (err) {        return '<pre><code>' + str + '</code></pre>'      }    }    return '<pre><code>' + str + '</code></pre>'  }})  .use(mdEmoji)  .use(mdTaskLists)  .use(mdAbbr)  .use(mdAnchor, {    slugify: _.kebabCase,    permalink: true,    permalinkClass: 'toc-anchor icon-anchor',    permalinkSymbol: '',    permalinkBefore: true  })  .use(mdFootnote)  .use(mdExternalLinks, {    externalClassName: 'external-link',    internalClassName: 'internal-link'  })  .use(mdExpandTabs, {    tabWidth: 4  })  .use(mdAttrs)if (appconfig) {  const mdMathjax = require('markdown-it-mathjax')  mkdown.use(mdMathjax())}// Rendering rulesmkdown.renderer.rules.emoji = function (token, idx) {  return '<i class="twa twa-' + _.replace(token[idx].markup, /_/g, '-') + '"></i>'}// Video rulesconst 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>'  }]// Non-markdown filterconst textRegex = new RegExp('\\b[a-z0-9-.,' + appdata.regex.cjk + appdata.regex.arabic + ']+\\b', 'g')/** * 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     {String}  HTML formatted content */const parseContent = (content) => {  let output = mkdown.render(content)  let cr = cheerio.load(output)  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.root().children('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')  })  output = cr.html()  return output}/** * 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 {      meta: parseMeta(content),      html: parseContent(content),      tree: parseTree(content)    }  },  parseContent,  parseMeta,  parseTree,  removeMarkdown}
 |