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.

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