From c2ab10d4701597212587c2aa8130274439dae3d6 Mon Sep 17 00:00:00 2001 From: Ryan Hendrickson Date: Sat, 2 Sep 2017 03:15:30 -0400 Subject: [PATCH] feat: groups in toc --- __tests__/__snapshots__/test.js.snap | 49 ++++++++++++++++ __tests__/fixture/sections.config.yml | 12 ++++ __tests__/fixture/sections.input.js | 24 ++++++++ __tests__/lib/__snapshots__/sort.js.snap | 66 +++++++++++++++++++++- __tests__/lib/sort.js | 18 +++++- __tests__/test.js | 9 +++ docs/CONFIG.md | 19 +++++++ src/hierarchy.js | 71 ++++++++++++++++++------ src/output/markdown_ast.js | 13 ++++- src/sort.js | 54 +++++++++++------- 10 files changed, 290 insertions(+), 45 deletions(-) create mode 100644 __tests__/fixture/sections.config.yml create mode 100644 __tests__/fixture/sections.input.js diff --git a/__tests__/__snapshots__/test.js.snap b/__tests__/__snapshots__/test.js.snap index 57a8751a4..2657f91e8 100644 --- a/__tests__/__snapshots__/test.js.snap +++ b/__tests__/__snapshots__/test.js.snap @@ -34,6 +34,55 @@ World " `; +exports[`config with nested sections 1`] = ` +" + +## Alpha + + + + +### third + +This function is third + +### first + +This function is first + +## Bravo + +Contains a subsection! + + +### Charlie + +Second is in here + + +#### second + +This class has some members + +##### foo + +second::foo + +**Parameters** + +- \`pork\` + +##### bar + +second::bar + +**Parameters** + +- \`beans\` +- \`rice\` +" +`; + exports[`external modules option 1`] = ` Array [ Object { diff --git a/__tests__/fixture/sections.config.yml b/__tests__/fixture/sections.config.yml new file mode 100644 index 000000000..0efa7b0ed --- /dev/null +++ b/__tests__/fixture/sections.config.yml @@ -0,0 +1,12 @@ +toc: + - name: Alpha + children: + - third + - first + - name: Bravo + description: Contains a subsection! + children: + - name: Charlie + description: Second is in here + children: + - second diff --git a/__tests__/fixture/sections.input.js b/__tests__/fixture/sections.input.js new file mode 100644 index 000000000..1a11c2122 --- /dev/null +++ b/__tests__/fixture/sections.input.js @@ -0,0 +1,24 @@ +/** + * This function is first + */ +function first() {} + +/** + * This class has some members + */ +function second() {} + +/** + * second::foo + */ +second.prototype.foo = function(pork) {}; + +/** + * second::bar + */ +second.prototype.bar = function(beans, rice) {}; + +/** + * This function is third + */ +function third() {} diff --git a/__tests__/lib/__snapshots__/sort.js.snap b/__tests__/lib/__snapshots__/sort.js.snap index 134b42c51..26a127dbb 100644 --- a/__tests__/lib/__snapshots__/sort.js.snap +++ b/__tests__/lib/__snapshots__/sort.js.snap @@ -3,9 +3,67 @@ exports[`sort toc with files 1`] = ` Array [ Object { - "file": "test/fixture/snowflake.md", + "description": Object { + "children": Array [ + Object { + "children": Array [ + Object { + "position": Position { + "end": Object { + "column": 16, + "line": 1, + "offset": 15, + }, + "indent": Array [], + "start": Object { + "column": 3, + "line": 1, + "offset": 2, + }, + }, + "type": "text", + "value": "The Snowflake", + }, + ], + "depth": 1, + "position": Position { + "end": Object { + "column": 16, + "line": 1, + "offset": 15, + }, + "indent": Array [], + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "heading", + }, + ], + "position": Object { + "end": Object { + "column": 1, + "line": 2, + "offset": 16, + }, + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "root", + }, "kind": "note", "name": "snowflake", + "path": Array [ + Object { + "name": "snowflake", + "scope": "static", + }, + ], }, Object { "context": Object { @@ -86,6 +144,12 @@ Array [ }, "kind": "note", "name": "snowflake", + "path": Array [ + Object { + "name": "snowflake", + "scope": "static", + }, + ], }, Object { "context": Object { diff --git a/__tests__/lib/sort.js b/__tests__/lib/sort.js index cdc7b2a11..099b671ea 100644 --- a/__tests__/lib/sort.js +++ b/__tests__/lib/sort.js @@ -87,7 +87,13 @@ test('sort stream with configuration and a section', function() { } } }, - kind: 'note' + kind: 'note', + path: [ + { + name: 'This is the banana type', + scope: 'static' + } + ] }; expect( @@ -161,7 +167,13 @@ test('sort an already-sorted stream containing a section/description', function( } } }, - kind: 'note' + kind: 'note', + path: [ + { + name: 'This is the banana type', + scope: 'static' + } + ] }; var config = { @@ -180,7 +192,7 @@ test('sort toc with files', function() { var snowflake = { name: 'snowflake', - file: 'test/fixture/snowflake.md' + file: path.join(__dirname, '../fixture/snowflake.md') }; expect( diff --git a/__tests__/test.js b/__tests__/test.js index cbcbbacca..604ae889d 100644 --- a/__tests__/test.js +++ b/__tests__/test.js @@ -179,6 +179,15 @@ test('config', async function() { expect(md).toMatchSnapshot(); }); +test('config with nested sections', async function() { + var file = path.join(__dirname, 'fixture', 'sections.input.js'); + const out = await documentation.build([file], { + config: path.join(__dirname, 'fixture', 'sections.config.yml') + }); + const md = await outputMarkdown(out, {}); + expect(md).toMatchSnapshot(); +}); + test('multi-file input', async function() { const result = await documentation.build( [ diff --git a/docs/CONFIG.md b/docs/CONFIG.md index 7abdfc5ff..6dab84d58 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -53,3 +53,22 @@ and areas on the sphere. ``` it would produce the same output as the previous example. + +## Groups + +The `children` property can be used to group content under headings instead of just arranging them in order. Example: + +```yml +toc: + - name: Geography + children: + - Map + - LngLat + - LngLatBounds + - name: Navigation + description: | + Here are some helper functions for navigation. + children: + - shortestPath + - salesman +``` diff --git a/src/hierarchy.js b/src/hierarchy.js index f8bb120fb..b4067a5d0 100644 --- a/src/hierarchy.js +++ b/src/hierarchy.js @@ -55,32 +55,63 @@ module.exports = function(comments) { members: getMembers() }; + const namesToUnroot = []; + comments.forEach(comment => { - var path = []; + let path = comment.path; + if (!path) { + path = []; + + if (comment.memberof) { + // TODO: full namepath parsing + path = comment.memberof + .split('.') + .map(segment => ({ scope: 'static', name: segment })); + } - if (comment.memberof) { - // TODO: full namepath parsing - path = comment.memberof.split('.').map(segment => ['static', segment]); - } + if (!comment.name) { + comment.errors.push({ + message: 'could not determine @name for hierarchy' + }); + } - if (!comment.name) { - comment.errors.push({ - message: 'could not determine @name for hierarchy' + path.push({ + scope: comment.scope || 'static', + name: comment.name || 'unknown_' + id++ }); } - path.push([comment.scope || 'static', comment.name || 'unknown_' + id++]); - var node = root; while (path.length) { - var segment = path.shift(), scope = segment[0], name = segment[1]; + var segment = path.shift(), + scope = segment.scope, + name = segment.name; if (!hasOwnProperty.call(node.members[scope], name)) { - node.members[scope][name] = { - comments: [], - members: getMembers() - }; + // If segment.toc is true, everything up to this point in the path + // represents how the documentation should be nested, but not how the + // actual code is nested. To ensure that child members end up in the + // right places in the tree, we temporarily push the same node a second + // time to the root of the tree, and unroot it after all the comments + // have found their homes. + if ( + segment.toc && + node !== root && + hasOwnProperty.call(root.members[scope], name) + ) { + node.members[scope][name] = root.members[scope][name]; + namesToUnroot.push(name); + } else { + const newNode = (node.members[scope][name] = { + comments: [], + members: getMembers() + }); + if (segment.toc && node !== root) { + root.members[scope][name] = newNode; + namesToUnroot.push(name); + } + } } node = node.members[scope][name]; @@ -88,6 +119,9 @@ module.exports = function(comments) { node.comments.push(comment); }); + namesToUnroot.forEach(function(name) { + delete root.members.static[name]; + }); /* * Massage the hierarchy into a format more suitable for downstream consumers: @@ -107,7 +141,8 @@ module.exports = function(comments) { * Person~say // the inner method named "say." */ function toComments(nodes, root, hasUndefinedParent, path) { - var result = [], scope; + var result = [], + scope; path = path || []; @@ -119,7 +154,9 @@ module.exports = function(comments) { node.members[scope], root || result, !node.comments.length, - node.comments.length ? path.concat(node.comments[0]) : [] + node.comments.length && node.comments[0].kind !== 'note' + ? path.concat(node.comments[0]) + : [] ); } diff --git a/src/output/markdown_ast.js b/src/output/markdown_ast.js index ab1b1dae7..3096f096e 100644 --- a/src/output/markdown_ast.js +++ b/src/output/markdown_ast.js @@ -303,9 +303,16 @@ function buildMarkdownAST( } if (comment.kind === 'note') { - return [u('heading', { depth }, [u('text', comment.name || '')])].concat( - comment.description - ); + return [u('heading', { depth }, [u('text', comment.name || '')])] + .concat(comment.description) + .concat( + !!comment.members.static.length && + comment.members.static.reduce( + (memo, child) => memo.concat(generate(depth + 1, child)), + [] + ) + ) + .filter(Boolean); } return [u('heading', { depth }, [u('text', comment.name || '')])] diff --git a/src/sort.js b/src/sort.js index 0a8b0176f..4c800b5d7 100644 --- a/src/sort.js +++ b/src/sort.js @@ -18,28 +18,19 @@ module.exports = function sortDocs(comments: Array, options: Object) { if (!options || !options.toc) { return sortComments(comments, options && options.sortOrder); } - var indexes = options.toc.reduce(function(memo, val, i) { + let i = 0; + const indexes: { [?string]: number } = Object.create(null); + const toBeSorted: { [?string]: boolean } = Object.create(null); + const paths: { + [?string]: Array<{ scope: Scope, name: string }> + } = Object.create(null); + const fixed = []; + const walk = function(tocPath, val) { if (typeof val === 'object' && val.name) { val.kind = 'note'; - memo[val.name] = i; - } else { - memo[val] = i; - } - return memo; - }, Object.create(null)); - var toBeSorted = options.toc.reduce(function(memo, val) { - if (typeof val === 'string') { - memo[val] = false; - } - return memo; - }, Object.create(null)); - // Table of contents 'theme' entries: defined as objects - // in the YAML list - var fixed = options.toc - .filter(val => typeof val === 'object' && val.name) - .map(function(val) { + indexes[val.name] = i++; if (typeof val.file === 'string') { - var filename = val.file; + let filename = val.file; if (!path.isAbsolute(val.file)) { filename = path.join(process.cwd(), val.file); } @@ -50,14 +41,34 @@ module.exports = function sortDocs(comments: Array, options: Object) { } catch (err) { process.stderr.write(chalk.red(`Failed to read file ${filename}`)); } + } else if (!val.description) { + val.description = ''; } if (typeof val.description === 'string') { val.description = parseMarkdown(val.description); } - return val; - }); + const childPath = tocPath.concat({ scope: 'static', name: val.name }); + val.path = childPath; + if (val.children) { + val.children.forEach(walk.bind(null, childPath)); + } + fixed.push(val); + } else { + indexes[val] = i++; + toBeSorted[val] = false; + paths[val] = tocPath.concat({ scope: 'static', name: val, toc: true }); + } + }; + // Table of contents 'theme' entries: defined as objects + // in the YAML list + options.toc.forEach(walk.bind(null, [])); var unfixed = []; comments.forEach(function(comment) { + const commentPath = paths[comment.name]; + if (commentPath) { + comment.path = commentPath; + } + // If comment is of kind 'note', this means that we must be _re_ sorting // the list, and the TOC note entries were already added to the list. Bail // out here so that we don't add duplicates. @@ -78,6 +89,7 @@ module.exports = function sortDocs(comments: Array, options: Object) { if (indexes[a.name] !== undefined && indexes[b.name] !== undefined) { return indexes[a.name] - indexes[b.name]; } + return 0; }); sortComments(unfixed, options.sortOrder); Object.keys(toBeSorted)