diff --git a/frontend/utils/markdown.js b/frontend/utils/markdown.js index 01b47005..b0bc85e0 100644 --- a/frontend/utils/markdown.js +++ b/frontend/utils/markdown.js @@ -3,6 +3,7 @@ import marked from 'marked'; import sanitizedRenderer from 'marked-sanitized'; import highlight from 'highlight.js'; import emojify from './emojify'; +import toc from './toc'; import _ from 'lodash'; slug.defaults.mode = 'rfc3986'; @@ -24,8 +25,16 @@ renderer.heading = (text, level) => { `; }; -// TODO: This is syncronous and can be costly const convertToMarkdown = (text) => { + // Add TOC + text = toc.insert(text, { + slugify: (heading) => { + // FIXME: E.g. `&` gets messed up + const headingSlug = _.escape(slug(heading)); + return headingSlug; + }, + }); + return marked.parse(emojify(text), { renderer, gfm: true, diff --git a/frontend/utils/toc/index.js b/frontend/utils/toc/index.js new file mode 100644 index 00000000..036ac248 --- /dev/null +++ b/frontend/utils/toc/index.js @@ -0,0 +1,145 @@ +/* eslint-disable */ + +/** + * marked-toc + * + * Copyright (c) 2014 Jon Schlinkert, contributors. + * Licensed under the MIT license. + */ + +'use strict'; + +var marked = require('marked'); +var _ = require('lodash'); +var utils = require('./utils'); + +/** + * Expose `toc` + */ + +module.exports = toc; + +/** + * Default template to use for generating + * a table of contents. + */ + +var defaultTemplate = '<%= depth %><%= bullet %>[<%= heading %>](#<%= url %>)\n'; + +/** + * Create the table of contents object that + * will be used as context for the template. + * + * @param {String} `str` + * @param {Object} `options` + * @return {Object} + */ + +function generate(str, options) { + var opts = _.extend({ + firsth1: false, + blacklist: true, + omit: [], + maxDepth: 3, + slugify: function(text) { + return text; // Override this! + } + }, options); + + var toc = ''; + var tokens = marked.lexer(str); + var tocArray = []; + + // Remove the very first h1, true by default + if(opts.firsth1 === false) { + tokens.shift(); + } + + // Do any h1's still exist? + var h1 = _.some(tokens, {depth: 1}); + + tokens.filter(function (token) { + // Filter out everything but headings + if (token.type !== 'heading' || token.type === 'code') { + return false; + } + + // Since we removed the first h1, we'll check to see if other h1's + // exist. If none exist, then we unindent the rest of the TOC + if(!h1) { + token.depth = token.depth - 1; + } + + // Store original text and create an id for linking + token.heading = opts.strip ? utils.strip(token.text, opts) : token.text; + + // Create a "slugified" id for linking + token.id = opts.slugify(token.text); + + // Omit headings with these strings + var omissions = ['Table of Contents', 'TOC', 'TABLE OF CONTENTS']; + var omit = _.union([], opts.omit, omissions); + + if (utils.isMatch(omit, token.heading)) { + return; + } + + return true; + }).forEach(function (h) { + + if(h.depth > opts.maxDepth) { + return; + } + + var bullet = Array.isArray(opts.bullet) + ? opts.bullet[(h.depth - 1) % opts.bullet.length] + : opts.bullet; + + var data = _.extend({}, opts.data, { + depth : new Array((h.depth - 1) * 2 + 1).join(' '), + bullet : bullet ? bullet : '* ', + heading: h.heading, + url : h.id + }); + + tocArray.push(data); + toc += _.template(opts.template || defaultTemplate)(data); + }); + + return { + data: tocArray, + toc: opts.strip + ? utils.strip(toc, opts) + : toc + }; +} + +/** + * toc + */ + +function toc(str, options) { + return generate(str, options).toc; +} + +toc.raw = function(str, options) { + return generate(str, options); +}; + +toc.insert = function(content, options) { + var start = ''; + var stop = ''; + var re = /([\s\S]+?)/; + + // remove the existing TOC + content = content.replace(re, start); + + // generate new TOC + var newtoc = '\n\n' + + start + '\n\n' + + toc(content, options) + '\n' + + stop + '\n'; + + // If front-matter existed, put it back + return content.replace(start, newtoc); +}; diff --git a/frontend/utils/toc/utils.js b/frontend/utils/toc/utils.js new file mode 100644 index 00000000..6d7d9116 --- /dev/null +++ b/frontend/utils/toc/utils.js @@ -0,0 +1,75 @@ +/* eslint-disable */ + +/*! + * marked-toc + * + * Copyright (c) 2014 Jon Schlinkert, contributors. + * Licensed under the MIT license. + */ + +'use strict'; + +var _ = require('lodash'); +var utils = module.exports = {}; + + +utils.arrayify = function(arr) { + return !Array.isArray(arr) ? [arr] : arr; +}; + +utils.escapeRegex = function(re) { + return re.replace(/(\[|\]|\(|\)|\/|\.|\^|\$|\*|\+|\?)/g, '\\$1'); +}; + +utils.isDest = function(dest) { + return !dest || dest === 'undefined' || typeof dest === 'object'; +}; + +utils.isMatch = function (keys, str) { + keys = utils.arrayify(keys); + keys = (keys.length > 0) ? keys.join('|') : '.*'; + + // Escape certain characters, like '[', '(' + var k = utils.escapeRegex(String(keys)); + + // Build up the regex to use for replacement patterns + var re = new RegExp('(?:' + k + ')', 'g'); + if (String(str).match(re)) { + return true; + } else { + return false; + } +}; + +utils.sanitize = function(src) { + src = src.replace(/(\s*\[!|(?:\[.+ →\]\()).+/g, ''); + src = src.replace(/\s*\*\s*\[\].+/g, ''); + return src; +}; + +utils.slugify = function(str) { + str = str.replace(/\/\//g, '-'); + str = str.replace(/\//g, '-'); + str = str.replace(/\./g, '-'); + str = _.str.slugify(str); + str = str.replace(/^-/, ''); + str = str.replace(/-$/, ''); + return str; +}; + + +/** + * Strip certain words from headings. These can be + * overridden. Might seem strange but it makes + * sense in context. + */ + +var omit = ['grunt', 'helper', 'handlebars-helper', 'mixin', 'filter', 'assemble-contrib', 'assemble']; + +utils.strip = function (name, options) { + var opts = _.extend({}, options); + if(opts.omit === false) {omit = [];} + var exclusions = _.union(omit, utils.arrayify(opts.strip || [])); + var re = new RegExp('^(?:' + exclusions.join('|') + ')[-_]?', 'g'); + return name.replace(re, ''); +};