markdown.js 4.0 KB

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