UNPKG

10.9 kBJavaScriptView Raw
1const u = require('unist-builder');
2const remark = require('remark');
3const mergeConfig = require('../merge_config');
4const toc = require('remark-toc');
5const links = require('remark-reference-links');
6const hljs = require('highlight.js');
7const GithubSlugger = require('github-slugger');
8const LinkerStack = require('./util/linker_stack');
9const rerouteLinks = require('./util/reroute_links');
10const _formatType = require('./util/format_type');
11
12const DEFAULT_LANGUAGE = 'javascript';
13
14/**
15 * Given a hierarchy-nested set of comments, generate an remark-compatible
16 * Abstract Syntax Tree usable for generating Markdown output
17 *
18 * @param comments nested comment
19 * @param {Object} args currently none accepted
20 * @param {boolean} [args.markdownToc=true] whether to include a table of contents
21 * in markdown output.
22 * @param {Object} [args.hljs={}] config to be passed to highlightjs for code highlighting:
23 * consult hljs.configure for the full list.
24 * @returns {Promise<Object>} returns an eventual Markdown value
25 */
26function markdownAST(comments, args) {
27 return mergeConfig(args).then(config => buildMarkdownAST(comments, config));
28}
29
30function buildMarkdownAST(comments, config) {
31 // Configure code highlighting
32 const hljsOptions = config.hljs || {};
33 hljs.configure(hljsOptions);
34
35 const linkerStack = new LinkerStack(config).namespaceResolver(
36 comments,
37 namespace => {
38 const slugger = new GithubSlugger();
39 return '#' + slugger.slug(namespace);
40 }
41 );
42
43 const formatType = _formatType.bind(undefined, linkerStack.link);
44
45 const generatorComment = [
46 u(
47 'html',
48 '<!-- Generated by documentation.js. Update this documentation by updating the source code. -->'
49 )
50 ];
51
52 const tableOfContentsHeading = [
53 u('heading', { depth: 3 }, [u('text', 'Table of Contents')])
54 ];
55
56 /**
57 * Generate an AST chunk for a comment at a given depth: this is
58 * split from the main function to handle hierarchially nested comments
59 *
60 * @param {number} depth nesting of the comment, starting at 1
61 * @param {Object} comment a single comment
62 * @returns {Object} remark-compatible AST
63 */
64 function generate(depth, comment) {
65 function typeSection(comment) {
66 return (
67 comment.type &&
68 u('paragraph', [u('text', 'Type: ')].concat(formatType(comment.type)))
69 );
70 }
71
72 function paramList(params) {
73 if (params.length === 0) return [];
74 return u(
75 'list',
76 { ordered: false },
77 params.map(param =>
78 u(
79 'listItem',
80 [
81 u(
82 'paragraph',
83 [
84 u('inlineCode', param.name),
85 u('text', ' '),
86 !!param.type && u('strong', formatType(param.type)),
87 u('text', ' ')
88 ]
89 .concat(param.description ? param.description.children : [])
90 .concat([
91 !!param.default &&
92 u('paragraph', [
93 u('text', ' (optional, default '),
94 u('inlineCode', param.default),
95 u('text', ')')
96 ])
97 ])
98 .filter(Boolean)
99 )
100 ]
101 .concat(param.properties && paramList(param.properties))
102 .filter(Boolean)
103 )
104 )
105 );
106 }
107
108 function paramSection(comment) {
109 return (
110 comment.params.length > 0 && [
111 u('heading', { depth: depth + 1 }, [u('text', 'Parameters')]),
112 paramList(comment.params)
113 ]
114 );
115 }
116
117 function propertySection(comment) {
118 return (
119 comment.properties.length > 0 && [
120 u('heading', { depth: depth + 1 }, [u('text', 'Properties')]),
121 propertyList(comment.properties)
122 ]
123 );
124 }
125
126 function propertyList(properties) {
127 return u(
128 'list',
129 { ordered: false },
130 properties.map(property =>
131 u(
132 'listItem',
133 [
134 u(
135 'paragraph',
136 [
137 u('inlineCode', property.name),
138 u('text', ' '),
139 u('strong', formatType(property.type)),
140 u('text', ' ')
141 ]
142 .concat(
143 property.description ? property.description.children : []
144 )
145 .filter(Boolean)
146 ),
147 property.properties && propertyList(property.properties)
148 ].filter(Boolean)
149 )
150 )
151 );
152 }
153
154 function examplesSection(comment) {
155 return (
156 comment.examples.length > 0 &&
157 [u('heading', { depth: depth + 1 }, [u('text', 'Examples')])].concat(
158 comment.examples.reduce(function(memo, example) {
159 const language = hljsOptions.highlightAuto
160 ? hljs.highlightAuto(example.description).language
161 : DEFAULT_LANGUAGE;
162 return memo
163 .concat(
164 example.caption
165 ? [u('paragraph', [u('emphasis', example.caption)])]
166 : []
167 )
168 .concat([u('code', { lang: language }, example.description)]);
169 }, [])
170 )
171 );
172 }
173
174 function returnsSection(comment) {
175 return (
176 comment.returns.length > 0 &&
177 comment.returns.map(returns =>
178 u(
179 'paragraph',
180 [
181 u('text', 'Returns '),
182 u('strong', formatType(returns.type)),
183 u('text', ' ')
184 ].concat(returns.description ? returns.description.children : [])
185 )
186 )
187 );
188 }
189
190 function throwsSection(comment) {
191 return (
192 comment.throws.length > 0 &&
193 u(
194 'list',
195 { ordered: false },
196 comment.throws.map(returns =>
197 u('listItem', [
198 u(
199 'paragraph',
200 [
201 u('text', 'Throws '),
202 u('strong', formatType(returns.type)),
203 u('text', ' ')
204 ].concat(
205 returns.description ? returns.description.children : []
206 )
207 )
208 ])
209 )
210 )
211 );
212 }
213
214 function augmentsLink(comment) {
215 return (
216 comment.augments.length > 0 &&
217 u('paragraph', [
218 u('strong', [
219 u('text', 'Extends '),
220 u('text', comment.augments.map(tag => tag.name).join(', '))
221 ])
222 ])
223 );
224 }
225
226 function seeLink(comment) {
227 return (
228 comment.sees.length > 0 &&
229 u(
230 'list',
231 { ordered: false },
232 comment.sees.map(see =>
233 u('listItem', [
234 u('strong', [u('text', 'See: ')].concat(see.children))
235 ])
236 )
237 )
238 );
239 }
240
241 function githubLink(comment) {
242 return (
243 comment.context &&
244 comment.context.github &&
245 u('paragraph', [
246 u(
247 'link',
248 {
249 title: 'Source code on GitHub',
250 url: comment.context.github.url
251 },
252 [
253 u(
254 'text',
255 comment.context.github.path +
256 ':' +
257 comment.context.loc.start.line +
258 '-' +
259 comment.context.loc.end.line
260 )
261 ]
262 )
263 ])
264 );
265 }
266
267 function metaSection(comment) {
268 const meta = [
269 'version',
270 'since',
271 'copyright',
272 'author',
273 'license',
274 'deprecated'
275 ].filter(tag => comment[tag]);
276 return (
277 !!meta.length &&
278 [u('strong', [u('text', 'Meta')])].concat(
279 u(
280 'list',
281 { ordered: false },
282 meta.map(tag => {
283 let metaContent;
284 if (tag === 'copyright' || tag === 'deprecated') {
285 metaContent = comment[tag];
286 } else {
287 metaContent = u('text', comment[tag]);
288 }
289 return u('listItem', [
290 u('paragraph', [
291 u('strong', [u('text', tag)]),
292 u('text', ': '),
293 metaContent
294 ])
295 ]);
296 })
297 )
298 )
299 );
300 }
301
302 if (comment.kind === 'note') {
303 return [u('heading', { depth }, [u('text', comment.name || '')])]
304 .concat(comment.description)
305 .concat(
306 !!comment.members.static.length &&
307 comment.members.static.reduce(
308 (memo, child) => memo.concat(generate(depth + 1, child)),
309 []
310 )
311 )
312 .filter(Boolean);
313 }
314
315 return [u('heading', { depth }, [u('text', comment.name || '')])]
316 .concat(githubLink(comment))
317 .concat(augmentsLink(comment))
318 .concat(seeLink(comment))
319 .concat(comment.description ? comment.description.children : [])
320 .concat(typeSection(comment))
321 .concat(paramSection(comment))
322 .concat(propertySection(comment))
323 .concat(examplesSection(comment))
324 .concat(throwsSection(comment))
325 .concat(returnsSection(comment))
326 .concat(metaSection(comment))
327 .concat(
328 !!comment.members.global.length &&
329 comment.members.global.reduce(
330 (memo, child) => memo.concat(generate(depth + 1, child)),
331 []
332 )
333 )
334 .concat(
335 !!comment.members.instance.length &&
336 comment.members.instance.reduce(
337 (memo, child) => memo.concat(generate(depth + 1, child)),
338 []
339 )
340 )
341 .concat(
342 !!comment.members.static.length &&
343 comment.members.static.reduce(
344 (memo, child) => memo.concat(generate(depth + 1, child)),
345 []
346 )
347 )
348 .concat(
349 !!comment.members.inner.length &&
350 comment.members.inner.reduce(
351 (memo, child) => memo.concat(generate(depth + 1, child)),
352 []
353 )
354 )
355 .filter(Boolean);
356 }
357
358 let root = rerouteLinks(
359 linkerStack.link,
360 u(
361 'root',
362 generatorComment
363 .concat(config.markdownToc ? tableOfContentsHeading : [])
364 .concat(
365 comments.reduce(
366 (memo, comment) => memo.concat(generate(2, comment)),
367 []
368 )
369 )
370 )
371 );
372
373 const pluginRemark = remark();
374 if (config.markdownToc)
375 pluginRemark.use(toc, {
376 tight: true,
377 maxDepth: config.markdownTocMaxDepth
378 });
379 if (config.noReferenceLinks !== true) pluginRemark.use(links);
380 root = pluginRemark.run(root);
381
382 return Promise.resolve(root);
383}
384
385module.exports = markdownAST;