1'use strict'; 2 3const _ = require('lodash'); 4const cheerio = require('cheerio'); 5const fs = require('fs'); 6const marky = require('marky-markdown'); 7const path = require('path'); 8const util = require('../common/util'); 9 10const basePath = path.join(__dirname, '..', '..'); 11const docPath = path.join(basePath, 'doc'); 12const readmePath = path.join(docPath, 'README.md'); 13 14const highlights = { 15 'html': [ 16 'string' 17 ], 18 'js': [ 19 'comment', 20 'console', 21 'delimiter', 22 'method', 23 'modifier', 24 'name', 25 'numeric', 26 'string', 27 'support', 28 'type' 29 ] 30}; 31 32const exts = _.keys(highlights); 33 34/** 35 * Converts Lodash method references into documentation links. 36 * 37 * @private 38 * @param {Object} $ The Cheerio object. 39 */ 40function autoLink($) { 41 $('.doc-container code').each(function() { 42 const $code = $(this); 43 const html = $code.html(); 44 if (/^_\.\w+$/.test(html)) { 45 const id = html.split('.')[1]; 46 $code.replaceWith(`<a href="#${ id }"><code>_.${ id }</code></a>`); 47 } 48 }); 49} 50 51/** 52 * Removes horizontal rules from the document. 53 * 54 * @private 55 * @param {Object} $ The Cheerio object. 56 */ 57function removeHorizontalRules($) { 58 $('hr').remove(); 59} 60 61/** 62 * Removes marky-markdown specific ids and class names. 63 * 64 * @private 65 * @param {Object} $ The Cheerio object. 66 */ 67function removeMarkyAttributes($) { 68 $('[id^="user-content-"]') 69 .attr('class', null) 70 .attr('id', null); 71 72 $(':header:not(h3) > a').each(function() { 73 const $a = $(this); 74 $a.replaceWith($a.html()); 75 }); 76} 77 78/** 79 * Renames "_" id and anchor references to "lodash". 80 * 81 * @private 82 * @param {Object} $ The Cheerio object. 83 */ 84function renameLodashId($) { 85 $('#_').attr('id', 'lodash'); 86 $('[href="#_"]').attr('href', '#lodash'); 87} 88 89/** 90 * Repairs broken marky-markdown headers. 91 * See https://github.com/npm/marky-markdown/issues/217 for more details. 92 * 93 * @private 94 * @param {Object} $ The Cheerio object. 95 */ 96function repairMarkyHeaders($) { 97 $('p:empty + h3').prev().remove(); 98 99 $('h3 ~ p:empty').each(function() { 100 const $p = $(this); 101 let node = this.prev; 102 while ((node = node.prev) && node.name != 'h3' && node.name != 'p') { 103 $p.prepend(node.next); 104 } 105 }); 106 107 $('h3 code em').parent().each(function() { 108 const $code = $(this); 109 $code.html($code.html().replace(/<\/?em>/g, '_')); 110 }); 111} 112 113/** 114 * Cleans up highlights blocks by removing extraneous class names and elements. 115 * 116 * @private 117 * @param {Object} $ The Cheerio object. 118 */ 119function tidyHighlights($) { 120 $('.highlight').each(function() { 121 let $spans; 122 const $parent = $(this); 123 const classes = $parent.find('.source,.text').first().attr('class').split(' '); 124 const ext = _(classes).intersection(exts).last(); 125 126 $parent.addClass(ext); 127 128 // Remove line indicators for single line snippets. 129 $parent.children('pre').each(function() { 130 const $divs = $(this).children('div'); 131 if ($divs.length == 1) { 132 $divs.replaceWith($divs.html()); 133 } 134 }); 135 // Remove extraneous class names. 136 $parent.find('[class]').each(function() { 137 const $element = $(this); 138 const classes = $element.attr('class').split(' '); 139 const attr = _(classes).intersection(highlights[ext]).join(' '); 140 $element.attr('class', attr || null); 141 }); 142 // Collapse nested comment highlights. 143 $parent.find(`[class~="comment"]`).each(function() { 144 const $element = $(this); 145 $element.text($element.text().trim()); 146 }); 147 // Collapse nested string highlights. 148 $parent.find(`[class~="string"]`).each(function() { 149 const $element = $(this); 150 $element.text($element.text()); 151 }); 152 // Collapse nested spans. 153 while (($spans = $parent.find('span:not([class])')).length) { 154 $spans.each(function() { 155 let $span = $(this); 156 while ($span[0] && $span[0].name == 'span' && !$span.attr('class')) { 157 const $parent = $span.parent(); 158 $span.replaceWith($span.html()); 159 $span = $parent; 160 } 161 }); 162 } 163 }); 164} 165 166/*----------------------------------------------------------------------------*/ 167 168/** 169 * Creates the documentation HTML. 170 * 171 * @private 172 */ 173function build() { 174 const markdown = fs 175 // Load markdown. 176 .readFileSync(readmePath, 'utf8') 177 // Uncomment docdown HTML hints. 178 .replace(/(<)!--\s*|\s*--(>)/g, '$1$2') 179 // Convert source and npm package links to anchors. 180 .replace(/\[source\]\(([^)]+)\) \[npm package\]\(([^)]+)\)/g, (match, href1, href2) => 181 `<p><a href="${ href1 }">source</a> <a href="${ href2 }">npm package</a></p>` 182 ); 183 184 const $ = cheerio.load(marky(markdown, { 185 'enableHeadingLinkIcons': false, 186 'sanitize': false 187 })); 188 189 const $header = $('h1').first().remove(); 190 const version = $header.find('span').first().text().trim().slice(1); 191 192 // Auto-link Lodash method references. 193 autoLink($); 194 // Rename "_" id references to "lodash". 195 renameLodashId($); 196 // Remove docdown horizontal rules. 197 removeHorizontalRules($); 198 // Remove marky-markdown attribute additions. 199 removeMarkyAttributes($); 200 // Repair marky-markdown wrapping around headers. 201 repairMarkyHeaders($); 202 // Cleanup highlights. 203 tidyHighlights($); 204 205 const html = [ 206 // Append YAML front matter. 207 '---', 208 'id: docs', 209 'layout: docs', 210 'title: Lodash Documentation', 211 'version: ' + (version || null), 212 '---', 213 '', 214 // Wrap in raw tags to avoid Liquid template tag processing. 215 '{% raw %}', 216 $.html().trim(), 217 '{% endraw %}', 218 '' 219 ].join('\n'); 220 221 fs.writeFile(path.join(docPath, version + '.html'), html, util.pitch); 222} 223 224build(); 225