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