You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

185 lines
4.0 KiB

  1. "use strict";
  2. var Promise = require('bluebird'),
  3. md = require('markdown-it'),
  4. mdEmoji = require('markdown-it-emoji'),
  5. mdTaskLists = require('markdown-it-task-lists'),
  6. mdAbbr = require('markdown-it-abbr'),
  7. mdAnchor = require('markdown-it-anchor'),
  8. mdFootnote = require('markdown-it-footnote'),
  9. mdExternalLinks = require('markdown-it-external-links'),
  10. mdExpandTabs = require('markdown-it-expand-tabs'),
  11. mdAttrs = require('markdown-it-attrs'),
  12. hljs = require('highlight.js'),
  13. cheerio = require('cheerio'),
  14. _ = require('lodash');
  15. // Load plugins
  16. var mkdown = md({
  17. html: true,
  18. linkify: true,
  19. typography: true,
  20. highlight(str, lang) {
  21. if (lang && hljs.getLanguage(lang)) {
  22. try {
  23. return '<pre class="hljs"><code>' + hljs.highlight(lang, str, true).value + '</code></pre>';
  24. } catch (err) {
  25. return '<pre><code>' + str + '</code></pre>';
  26. }
  27. }
  28. return '<pre><code>' + str + '</code></pre>';
  29. }
  30. })
  31. .use(mdEmoji)
  32. .use(mdTaskLists)
  33. .use(mdAbbr)
  34. .use(mdAnchor, {
  35. slugify: _.kebabCase,
  36. permalink: true,
  37. permalinkClass: 'toc-anchor',
  38. permalinkSymbol: '#',
  39. permalinkBefore: true
  40. })
  41. .use(mdFootnote)
  42. .use(mdExternalLinks, {
  43. externalClassName: 'external-link',
  44. internalClassName: 'internal-link'
  45. })
  46. .use(mdExpandTabs, {
  47. tabWidth: 4
  48. })
  49. .use(mdAttrs);
  50. // Rendering rules
  51. mkdown.renderer.rules.emoji = function(token, idx) {
  52. return '<i class="twa twa-' + token[idx].markup + '"></i>';
  53. };
  54. /**
  55. * Parse markdown content and build TOC tree
  56. *
  57. * @param {(Function|string)} content Markdown content
  58. * @return {Array} TOC tree
  59. */
  60. const parseTree = (content) => {
  61. let tokens = md().parse(content, {});
  62. let tocArray = [];
  63. //-> Extract headings and their respective levels
  64. for (let i = 0; i < tokens.length; i++) {
  65. if (tokens[i].type !== "heading_close") {
  66. continue;
  67. }
  68. const heading = tokens[i - 1];
  69. const heading_close = tokens[i];
  70. if (heading.type === "inline") {
  71. let content = "";
  72. let anchor = "";
  73. if (heading.children && heading.children[0].type === "link_open") {
  74. content = heading.children[1].content;
  75. anchor = _.kebabCase(content);
  76. } else {
  77. content = heading.content
  78. anchor = _.kebabCase(heading.children.reduce((acc, t) => acc + t.content, ""));
  79. }
  80. tocArray.push({
  81. content,
  82. anchor,
  83. level: +heading_close.tag.substr(1, 1)
  84. });
  85. }
  86. }
  87. //-> Exclude levels deeper than 2
  88. _.remove(tocArray, (n) => { return n.level > 2; });
  89. //-> Build tree from flat array
  90. return _.reduce(tocArray, (tree, v) => {
  91. let treeLength = tree.length - 1;
  92. if(v.level < 2) {
  93. tree.push({
  94. content: v.content,
  95. anchor: v.anchor,
  96. nodes: []
  97. });
  98. } else {
  99. let lastNodeLevel = 1;
  100. let GetNodePath = (startPos) => {
  101. lastNodeLevel++;
  102. if(_.isEmpty(startPos)) {
  103. startPos = 'nodes';
  104. }
  105. if(lastNodeLevel === v.level) {
  106. return startPos;
  107. } else {
  108. return GetNodePath(startPos + '[' + (_.at(tree[treeLength], startPos).length - 1) + '].nodes');
  109. }
  110. };
  111. let lastNodePath = GetNodePath();
  112. let lastNode = _.get(tree[treeLength], lastNodePath);
  113. if(lastNode) {
  114. lastNode.push({
  115. content: v.content,
  116. anchor: v.anchor,
  117. nodes: []
  118. });
  119. _.set(tree[treeLength], lastNodePath, lastNode);
  120. }
  121. }
  122. return tree;
  123. }, []);
  124. };
  125. /**
  126. * Parse markdown content to HTML
  127. *
  128. * @param {String} content Markdown content
  129. * @return {String} HTML formatted content
  130. */
  131. const parseContent = (content) => {
  132. let output = mkdown.render(content);
  133. let cr = cheerio.load(output);
  134. cr('table').addClass('table is-bordered is-striped is-narrow');
  135. output = cr.html();
  136. return output;
  137. };
  138. const parseMeta = (content) => {
  139. let commentMeta = new RegExp('<!-- ?([a-zA-Z]+):(.*)-->','g');
  140. let results = {}, match;
  141. while(match = commentMeta.exec(content)) {
  142. results[_.toLower(match[1])] = _.trim(match[2]);
  143. }
  144. return results;
  145. };
  146. module.exports = {
  147. parse(content) {
  148. return {
  149. meta: parseMeta(content),
  150. html: parseContent(content),
  151. tree: parseTree(content)
  152. };
  153. },
  154. parseContent,
  155. parseMeta,
  156. parseTree
  157. };