From a1fb7da093605c833d084a7a2bf70818087357f0 Mon Sep 17 00:00:00 2001 From: Matt Travi Date: Sun, 20 Jun 2021 20:16:04 -0500 Subject: [PATCH 1/5] test(usage): defined a test to prevent adding a duplicate "Usage" heading --- package-lock.json | 9 +++++++++ package.json | 1 + .../features/step_definitions/readme-steps.js | 2 +- .../features/step_definitions/sections-steps.js | 10 +++++++++- test/integration/features/usage.feature | 9 +++++++++ 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2142a585..3b7ca495 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13536,6 +13536,15 @@ } } }, + "unist-util-find-all-after": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/unist-util-find-all-after/-/unist-util-find-all-after-3.0.2.tgz", + "integrity": "sha512-xaTC/AGZ0rIM2gM28YVRAFPIZpzbpDtU3dRmp7EXlNVA8ziQc4hY3H7BHXM1J49nEmiqc3svnqMReW+PGqbZKQ==", + "dev": true, + "requires": { + "unist-util-is": "^4.0.0" + } + }, "unist-util-find-all-between": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/unist-util-find-all-between/-/unist-util-find-all-between-2.1.0.tgz", diff --git a/package.json b/package.json index f53f96c7..c2512900 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "rollup-plugin-auto-external": "2.0.0", "sinon": "^11.1.1", "unist-util-find": "1.0.2", + "unist-util-find-all-after": "3.0.2", "unist-util-find-all-between": "^2.1.0" }, "dependencies": { diff --git a/test/integration/features/step_definitions/readme-steps.js b/test/integration/features/step_definitions/readme-steps.js index 08c00fdd..098f3ecd 100644 --- a/test/integration/features/step_definitions/readme-steps.js +++ b/test/integration/features/step_definitions/readme-steps.js @@ -17,7 +17,7 @@ ${this.projectDescription} 1. item 1 1. item 2 - +${this['usage-heading'] ? `## ${this['usage-heading']}\n\n` : ''} diff --git a/test/integration/features/step_definitions/sections-steps.js b/test/integration/features/step_definitions/sections-steps.js index 0a7f9edf..f799a3c9 100644 --- a/test/integration/features/step_definitions/sections-steps.js +++ b/test/integration/features/step_definitions/sections-steps.js @@ -1,5 +1,6 @@ import remark from 'remark'; import find from 'unist-util-find'; +import findAllAfter from 'unist-util-find-all-after'; import findBetween from 'unist-util-find-all-between'; import {Given, Then} from '@cucumber/cucumber'; import any from '@travi/any'; @@ -9,6 +10,10 @@ Given('the existing README has no {string} heading', async function (sectionName this[`${sectionName.toLowerCase()}-heading`] = null; }); +Given('the existing README has an existing {string} section', async function (sectionName) { + this[`${sectionName.toLowerCase()}-heading`] = sectionName; +}); + Given('content is provided for the {string} section', async function (sectionName) { this[sectionName.toLowerCase()] = any.sentence(); }); @@ -16,8 +21,11 @@ Given('content is provided for the {string} section', async function (sectionNam Then('there is a {string} heading', async function (sectionName) { const readmeTree = remark().parse(this.resultingContent); + const matchingSectionHeadings = findAllAfter(readmeTree, 1, {type: 'heading', depth: 2}) + .filter(sectionHeading => sectionName === sectionHeading.children[0].value); + assert.equal( - find(readmeTree, {type: 'heading', depth: 2, children: [{type: 'text', value: sectionName}]}).children.length, + matchingSectionHeadings.length, 1 ); diff --git a/test/integration/features/usage.feature b/test/integration/features/usage.feature index a46a16ef..b214adae 100644 --- a/test/integration/features/usage.feature +++ b/test/integration/features/usage.feature @@ -13,3 +13,12 @@ Feature: Usage And the existing README uses modern badge zones When a node is processed Then there is no "Usage" heading + + @wip + Scenario: Usage Definition Addition + Given the existing README has an existing "Usage" section + And the existing README uses modern badge zones + And content is provided for the "Usage" section + When a node is processed + Then there is a "Usage" heading + And the "Usage" content is populated From 2964acf1c8f2f36ae9132844d512acd2d9e8fa70 Mon Sep 17 00:00:00 2001 From: Matt Travi Date: Mon, 21 Jun 2021 01:46:19 -0500 Subject: [PATCH 2/5] refactor(section-injector): extracted the logic for injecting the usage header and content --- package-lock.json | 34 +++++++++--- package.json | 2 +- src/plugin.js | 30 ++-------- src/section-injector-test.js | 55 +++++++++++++++++++ src/section-injector.js | 23 ++++++++ .../step_definitions/sections-steps.js | 13 ++--- .../mdast-util-from-markdown.js | 3 + 7 files changed, 116 insertions(+), 44 deletions(-) create mode 100644 src/section-injector-test.js create mode 100644 src/section-injector.js create mode 100644 thirdparty-wrappers/mdast-util-from-markdown.js diff --git a/package-lock.json b/package-lock.json index 3b7ca495..137f6cab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5191,7 +5191,8 @@ "bail": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", - "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==" + "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", + "dev": true }, "balanced-match": { "version": "1.0.2", @@ -7502,7 +7503,8 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true }, "extend-shallow": { "version": "3.0.2", @@ -8678,7 +8680,8 @@ "is-buffer": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true }, "is-bzip2": { "version": "1.0.0", @@ -9548,7 +9551,8 @@ "longest-streak": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz", - "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==" + "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==", + "dev": true }, "lower-case": { "version": "2.0.2", @@ -9655,6 +9659,7 @@ "version": "0.6.5", "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz", "integrity": "sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==", + "dev": true, "requires": { "@types/unist": "^2.0.0", "longest-streak": "^2.0.0", @@ -9667,7 +9672,8 @@ "mdast-util-to-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", - "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==" + "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", + "dev": true } } }, @@ -11586,6 +11592,7 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/remark/-/remark-13.0.0.tgz", "integrity": "sha512-HDz1+IKGtOyWN+QgBiAT0kn+2s6ovOxHyPAFGKVE81VSzJ+mq7RwHFledEvB5F1p4iJvOah/LOKdFuzvRnNLCA==", + "dev": true, "requires": { "remark-parse": "^9.0.0", "remark-stringify": "^9.0.0", @@ -11878,6 +11885,7 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", "integrity": "sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==", + "dev": true, "requires": { "mdast-util-from-markdown": "^0.8.0" } @@ -11910,6 +11918,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-9.0.1.tgz", "integrity": "sha512-mWmNg3ZtESvZS8fv5PTvaPckdL4iNlCHTt8/e/8oN08nArHRHjNZMKzA/YW3+p7/lYqIw4nx1XsjCBo/AxNChg==", + "dev": true, "requires": { "mdast-util-to-markdown": "^0.6.0" } @@ -11986,7 +11995,8 @@ "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true }, "require-directory": { "version": "2.1.1", @@ -13211,7 +13221,8 @@ "trough": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", - "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==" + "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", + "dev": true }, "tsconfig-paths": { "version": "3.9.0", @@ -13338,6 +13349,7 @@ "version": "9.2.1", "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.1.tgz", "integrity": "sha512-juWjuI8Z4xFg8pJbnEZ41b5xjGUWGHqXALmBZ3FC3WX0PIx1CZBIIJ6mXbYMcf6Yw4Fi0rFUTA1cdz/BglbOhA==", + "dev": true, "requires": { "bail": "^1.0.0", "extend": "^3.0.0", @@ -13350,7 +13362,8 @@ "is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==" + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true } } }, @@ -13862,6 +13875,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", + "dev": true, "requires": { "@types/unist": "^2.0.0", "is-buffer": "^2.0.0", @@ -13879,6 +13893,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", + "dev": true, "requires": { "@types/unist": "^2.0.0", "unist-util-stringify-position": "^2.0.0" @@ -14227,7 +14242,8 @@ "zwitch": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", - "integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==" + "integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==", + "dev": true } } } diff --git a/package.json b/package.json index c2512900..ef62c387 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "unist-util-find-all-between": "^2.1.0" }, "dependencies": { - "remark": "^13.0.0", + "mdast-util-from-markdown": "^0.8.5", "unist-util-modify-children": "^2.0.0" } } diff --git a/src/plugin.js b/src/plugin.js index 86fbc201..511c650d 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -1,33 +1,11 @@ import modifyChildren from 'unist-util-modify-children'; -import remark from 'remark'; +import getSectionInjector from './section-injector'; -export default function ({usage}) { - const modify = modifyChildren((node, index, parent) => { - if ('html' === node.type && '' === node.value) { - parent.children.splice( - index, - 0, - { - type: 'heading', - depth: 2, - children: [{type: 'text', value: 'Usage'}] - } - ); - - return index + 2; - } - - if ('html' === node.type && '' === node.value) { - parent.children.splice(index, 0, remark.parse(usage)); - - return index + 2; - } - - return undefined; - }); +export default function (documentation) { + const modify = modifyChildren(getSectionInjector(documentation)); return function transformer(node) { - if (usage) { + if (documentation.usage) { modify(node); } }; diff --git a/src/section-injector-test.js b/src/section-injector-test.js new file mode 100644 index 00000000..1b344977 --- /dev/null +++ b/src/section-injector-test.js @@ -0,0 +1,55 @@ +import any from '@travi/any'; +import {assert} from 'chai'; +import sinon from 'sinon'; +import * as parser from '../thirdparty-wrappers/mdast-util-from-markdown'; +import getSectionInjector from './section-injector'; + +suite('section injector', () => { + let sandbox; + const index = any.integer({max: 20}); + + setup(() => { + sandbox = sinon.createSandbox(); + + sandbox.stub(parser, 'default'); + }); + + teardown(() => sandbox.restore()); + + test('that the "Usage" heading is added before the consuemr badge zone', () => { + const childrenOfParent = any.listOf(any.simpleObject, {min: index, max: index + 20}); + const injectSections = getSectionInjector({}); + + assert.equal( + injectSections({type: 'html', value: ''}, index, {children: childrenOfParent}), + index + 2 + ); + assert.deepEqual(childrenOfParent[index], {type: 'heading', depth: 2, children: [{type: 'text', value: 'Usage'}]}); + }); + + test('that the "Usage" content is added before the contribution badge zone', () => { + const usage = any.simpleObject(); + const usageContentTree = any.simpleObject(); + const childrenOfParent = any.listOf(any.simpleObject, {min: index, max: index + 20}); + const injectSections = getSectionInjector({usage}); + parser.default.withArgs(usage).returns(usageContentTree); + + assert.equal( + injectSections({type: 'html', value: ''}, index, {children: childrenOfParent}), + index + 2 + ); + assert.deepEqual(childrenOfParent[index], usageContentTree); + }); + + test('that other html nodes are skipped', () => { + const injectSections = getSectionInjector({}); + + assert.isUndefined(injectSections({type: 'html'}, index, {children: []})); + }); + + test('that other nodes are skipped', () => { + const injectSections = getSectionInjector({}); + + assert.isUndefined(injectSections({type: any.word()}, index, {children: []})); + }); +}); diff --git a/src/section-injector.js b/src/section-injector.js new file mode 100644 index 00000000..ba7ece39 --- /dev/null +++ b/src/section-injector.js @@ -0,0 +1,23 @@ +import parse from '../thirdparty-wrappers/mdast-util-from-markdown'; + +export default function ({usage}) { + return ({type, value}, index, parent) => { + if ('html' === type) { + if ('' === value) { + parent.children.splice(index, 0, {type: 'heading', depth: 2, children: [{type: 'text', value: 'Usage'}]}); + + return index + 2; + } + + if ('' === value) { + parent.children.splice(index, 0, parse(usage)); + + return index + 2; + } + + return undefined; + } + + return undefined; + }; +} diff --git a/test/integration/features/step_definitions/sections-steps.js b/test/integration/features/step_definitions/sections-steps.js index f799a3c9..993ee600 100644 --- a/test/integration/features/step_definitions/sections-steps.js +++ b/test/integration/features/step_definitions/sections-steps.js @@ -1,4 +1,4 @@ -import remark from 'remark'; +import parse from 'mdast-util-from-markdown'; import find from 'unist-util-find'; import findAllAfter from 'unist-util-find-all-after'; import findBetween from 'unist-util-find-all-between'; @@ -19,15 +19,12 @@ Given('content is provided for the {string} section', async function (sectionNam }); Then('there is a {string} heading', async function (sectionName) { - const readmeTree = remark().parse(this.resultingContent); + const readmeTree = parse(this.resultingContent); const matchingSectionHeadings = findAllAfter(readmeTree, 1, {type: 'heading', depth: 2}) .filter(sectionHeading => sectionName === sectionHeading.children[0].value); - assert.equal( - matchingSectionHeadings.length, - 1 - ); + assert.equal(matchingSectionHeadings.length, 1); const htmlElements = findBetween( readmeTree, @@ -40,13 +37,13 @@ Then('there is a {string} heading', async function (sectionName) { }); Then('there is no {string} heading', async function (sectionName) { - const readmeTree = remark().parse(this.resultingContent); + const readmeTree = parse(this.resultingContent); assert.isUndefined(find(readmeTree, {type: 'heading', depth: 2, children: [{type: 'text', value: sectionName}]})); }); Then('the {string} content is populated', async function (sectionName) { - const readmeTree = remark().parse(this.resultingContent); + const readmeTree = parse(this.resultingContent); const paragraphs = findBetween( readmeTree, diff --git a/thirdparty-wrappers/mdast-util-from-markdown.js b/thirdparty-wrappers/mdast-util-from-markdown.js new file mode 100644 index 00000000..77957777 --- /dev/null +++ b/thirdparty-wrappers/mdast-util-from-markdown.js @@ -0,0 +1,3 @@ +import parse from 'mdast-util-from-markdown'; + +export default parse; From 1259a1440b0e0df7d066e50948d4fc72521e66a7 Mon Sep 17 00:00:00 2001 From: Matt Travi Date: Mon, 21 Jun 2021 02:01:04 -0500 Subject: [PATCH 3/5] test(plugin): backfilled unit tests --- src/plugin-test.js | 27 ++++++++++++++++--- src/plugin.js | 2 +- .../unist-util-modify-children.js | 3 +++ 3 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 thirdparty-wrappers/unist-util-modify-children.js diff --git a/src/plugin-test.js b/src/plugin-test.js index bf62d05e..67b5909f 100644 --- a/src/plugin-test.js +++ b/src/plugin-test.js @@ -1,14 +1,19 @@ import any from '@travi/any'; import sinon from 'sinon'; +import {assert} from 'chai'; +import * as childrenModifier from '../thirdparty-wrappers/unist-util-modify-children'; +import * as sectionInjector from './section-injector'; import plugin from './plugin'; suite('plugin', () => { let sandbox, node; + const injector = any.simpleObject(); setup(() => { sandbox = sinon.createSandbox(); - // sandbox.stub(); + sandbox.stub(sectionInjector, 'default'); + sandbox.stub(childrenModifier, 'default'); node = {...any.simpleObject(), children: []}; }); @@ -16,8 +21,24 @@ suite('plugin', () => { teardown(() => sandbox.restore()); test('that the "Usage" section header is injected when not present', () => { - const transformer = plugin({usage: any.sentence()}); + const documentation = {usage: any.sentence()}; + const modifier = sinon.spy(); + sectionInjector.default.withArgs(documentation).returns(injector); + childrenModifier.default.withArgs(injector).returns(modifier); - transformer(node); + plugin(documentation)(node); + + assert.calledWith(modifier, node); + }); + + test('that the document is not updated when no usage content is provided', () => { + const documentation = {}; + const modifier = sinon.spy(); + sectionInjector.default.withArgs(documentation).returns(injector); + childrenModifier.default.withArgs(injector).returns(modifier); + + plugin(documentation)(node); + + assert.notCalled(modifier); }); }); diff --git a/src/plugin.js b/src/plugin.js index 511c650d..29c50ff2 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -1,4 +1,4 @@ -import modifyChildren from 'unist-util-modify-children'; +import modifyChildren from '../thirdparty-wrappers/unist-util-modify-children'; import getSectionInjector from './section-injector'; export default function (documentation) { diff --git a/thirdparty-wrappers/unist-util-modify-children.js b/thirdparty-wrappers/unist-util-modify-children.js new file mode 100644 index 00000000..a76391a7 --- /dev/null +++ b/thirdparty-wrappers/unist-util-modify-children.js @@ -0,0 +1,3 @@ +import modifyChildren from 'unist-util-modify-children'; + +export default modifyChildren; From 5b0830b1c0adbd051edfef210d05562d6b781e17 Mon Sep 17 00:00:00 2001 From: Matt Travi Date: Mon, 21 Jun 2021 02:06:50 -0500 Subject: [PATCH 4/5] test(remark): captured the direct test dependency --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index ef62c387..9a155b0e 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "npm-run-all": "4.1.5", "nyc": "15.1.0", "package-preview": "4.0.0", + "remark": "13.0.0", "remark-cli": "9.0.0", "remark-toc": "7.2.0", "remark-usage": "9.0.0", From 4388bc6b852f55d6656f7e3827b2d300e7e78167 Mon Sep 17 00:00:00 2001 From: Matt Travi Date: Mon, 21 Jun 2021 22:28:39 -0500 Subject: [PATCH 5/5] feat(toc): injected table of contents section when content is provided --- src/plugin-test.js | 13 ++++- src/plugin.js | 2 +- src/section-injector-test.js | 57 ++++++++++++++++++- src/section-injector.js | 22 +++++-- .../features/step_definitions/common-steps.js | 3 +- .../step_definitions/sections-steps.js | 8 ++- test/integration/features/toc.feature | 32 +++++++++++ 7 files changed, 124 insertions(+), 13 deletions(-) create mode 100644 test/integration/features/toc.feature diff --git a/src/plugin-test.js b/src/plugin-test.js index 67b5909f..a8f5b6b8 100644 --- a/src/plugin-test.js +++ b/src/plugin-test.js @@ -31,7 +31,18 @@ suite('plugin', () => { assert.calledWith(modifier, node); }); - test('that the document is not updated when no usage content is provided', () => { + test('that the "Table of Contents" section header is injected when not present', () => { + const documentation = {toc: any.sentence()}; + const modifier = sinon.spy(); + sectionInjector.default.withArgs(documentation).returns(injector); + childrenModifier.default.withArgs(injector).returns(modifier); + + plugin(documentation)(node); + + assert.calledWith(modifier, node); + }); + + test('that the document is not updated when no usage or toc content is provided', () => { const documentation = {}; const modifier = sinon.spy(); sectionInjector.default.withArgs(documentation).returns(injector); diff --git a/src/plugin.js b/src/plugin.js index 29c50ff2..76c1428b 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -5,7 +5,7 @@ export default function (documentation) { const modify = modifyChildren(getSectionInjector(documentation)); return function transformer(node) { - if (documentation.usage) { + if (documentation.usage || documentation.toc) { modify(node); } }; diff --git a/src/section-injector-test.js b/src/section-injector-test.js index 1b344977..774c06d1 100644 --- a/src/section-injector-test.js +++ b/src/section-injector-test.js @@ -7,6 +7,8 @@ import getSectionInjector from './section-injector'; suite('section injector', () => { let sandbox; const index = any.integer({max: 20}); + const usage = any.simpleObject(); + const toc = any.simpleObject(); setup(() => { sandbox = sinon.createSandbox(); @@ -16,9 +18,9 @@ suite('section injector', () => { teardown(() => sandbox.restore()); - test('that the "Usage" heading is added before the consuemr badge zone', () => { + test('that the "Usage" heading is added before the consumer badge zone', () => { const childrenOfParent = any.listOf(any.simpleObject, {min: index, max: index + 20}); - const injectSections = getSectionInjector({}); + const injectSections = getSectionInjector({usage}); assert.equal( injectSections({type: 'html', value: ''}, index, {children: childrenOfParent}), @@ -27,8 +29,18 @@ suite('section injector', () => { assert.deepEqual(childrenOfParent[index], {type: 'heading', depth: 2, children: [{type: 'text', value: 'Usage'}]}); }); + test('that the "Usage" heading is not added if content is not defined', () => { + const childrenOfParent = any.listOf(any.simpleObject, {min: index, max: index + 20}); + const injectSections = getSectionInjector({}); + + assert.isUndefined(injectSections( + {type: 'html', value: ''}, + index, + {children: childrenOfParent} + )); + }); + test('that the "Usage" content is added before the contribution badge zone', () => { - const usage = any.simpleObject(); const usageContentTree = any.simpleObject(); const childrenOfParent = any.listOf(any.simpleObject, {min: index, max: index + 20}); const injectSections = getSectionInjector({usage}); @@ -41,6 +53,45 @@ suite('section injector', () => { assert.deepEqual(childrenOfParent[index], usageContentTree); }); + test('that the "Usage" content is not added if content is not defined', () => { + const usageContentTree = any.simpleObject(); + const childrenOfParent = any.listOf(any.simpleObject, {min: index, max: index + 20}); + const injectSections = getSectionInjector({}); + parser.default.withArgs(usage).returns(usageContentTree); + + assert.isUndefined(injectSections( + {type: 'html', value: ''}, + index, + {children: childrenOfParent} + )); + }); + + test('that the "Table of Contents" heading is added after the status badge zone', () => { + const childrenOfParent = any.listOf(any.simpleObject, {min: index, max: index + 20}); + const node = {type: 'html', value: ''}; + const tocContentTree = any.simpleObject(); + const injectSections = getSectionInjector({toc}); + parser.default.withArgs(toc).returns(tocContentTree); + + assert.equal(injectSections(node, index, {children: childrenOfParent}), index + 3); + assert.deepEqual(childrenOfParent[index], node); + assert.deepEqual( + childrenOfParent[index + 1], + {type: 'heading', depth: 2, children: [{type: 'text', value: 'Table of Contents'}]} + ); + assert.deepEqual(childrenOfParent[index + 2], tocContentTree); + }); + + test('that the "Table of Contents" heading is not added if content is not provided', () => { + const childrenOfParent = any.listOf(any.simpleObject, {min: index, max: index + 20}); + const node = {type: 'html', value: ''}; + const tocContentTree = any.simpleObject(); + const injectSections = getSectionInjector({}); + parser.default.withArgs(toc).returns(tocContentTree); + + assert.isUndefined(injectSections(node, index, {children: childrenOfParent})); + }); + test('that other html nodes are skipped', () => { const injectSections = getSectionInjector({}); diff --git a/src/section-injector.js b/src/section-injector.js index ba7ece39..0a7f9738 100644 --- a/src/section-injector.js +++ b/src/section-injector.js @@ -1,15 +1,29 @@ import parse from '../thirdparty-wrappers/mdast-util-from-markdown'; -export default function ({usage}) { - return ({type, value}, index, parent) => { +export default function ({usage, toc}) { + return (node, index, parent) => { + const {type, value} = node; + if ('html' === type) { - if ('' === value) { + if (toc && '' === value) { + parent.children.splice( + index, + 1, + node, + {type: 'heading', depth: 2, children: [{type: 'text', value: 'Table of Contents'}]}, + parse(toc) + ); + + return index + 3; + } + + if (usage && '' === value) { parent.children.splice(index, 0, {type: 'heading', depth: 2, children: [{type: 'text', value: 'Usage'}]}); return index + 2; } - if ('' === value) { + if (usage && '' === value) { parent.children.splice(index, 0, parse(usage)); return index + 2; diff --git a/test/integration/features/step_definitions/common-steps.js b/test/integration/features/step_definitions/common-steps.js index 9f106ee3..e79aa591 100644 --- a/test/integration/features/step_definitions/common-steps.js +++ b/test/integration/features/step_definitions/common-steps.js @@ -5,9 +5,10 @@ import remarkReadme from '@form8ion/remark-readme'; When('a node is processed', async function () { const usageContents = this.usage; + const tocContents = this['table of contents']; remark() - .use(remarkReadme, {usage: usageContents}) + .use(remarkReadme, {usage: usageContents, toc: tocContents}) .process(this.existingReadmeContent, (err, file) => { if (err) throw err; diff --git a/test/integration/features/step_definitions/sections-steps.js b/test/integration/features/step_definitions/sections-steps.js index 993ee600..ec497cad 100644 --- a/test/integration/features/step_definitions/sections-steps.js +++ b/test/integration/features/step_definitions/sections-steps.js @@ -28,7 +28,7 @@ Then('there is a {string} heading', async function (sectionName) { const htmlElements = findBetween( readmeTree, - {type: 'heading', depth: 2, children: [{type: 'text', value: 'Usage'}]}, + {type: 'heading', depth: 2, children: [{type: 'text', value: sectionName}]}, {type: 'html', value: ''}, 'html' ); @@ -47,8 +47,10 @@ Then('the {string} content is populated', async function (sectionName) { const paragraphs = findBetween( readmeTree, - {type: 'heading', depth: 2, children: [{type: 'text', value: 'Usage'}]}, - {type: 'html', value: ''}, + {type: 'heading', depth: 2, children: [{type: 'text', value: sectionName}]}, + 'Table of Contents' === sectionName && this.usage + ? {type: 'heading', depth: 2, children: [{type: 'text', value: 'Usage'}]} + : {type: 'html', value: ``}, 'paragraph' ); diff --git a/test/integration/features/toc.feature b/test/integration/features/toc.feature new file mode 100644 index 00000000..a51d086d --- /dev/null +++ b/test/integration/features/toc.feature @@ -0,0 +1,32 @@ +Feature: Table of Contents + + Scenario: Initial TOC Definition + Given the existing README has no "Table of Contents" heading + And the existing README uses modern badge zones + And content is provided for the "Table of Contents" section + When a node is processed + Then there is a "Table of Contents" heading + And the "Table of Contents" content is populated + + Scenario: Initial TOC Definition with existing Usage Section + Given the existing README has no "Table of Contents" heading + And the existing README has an existing "Usage" section + And the existing README uses modern badge zones + And content is provided for the "Table of Contents" section + When a node is processed + Then there is a "Table of Contents" heading + And the "Table of Contents" content is populated + + Scenario: No TOC Definition Provided + Given the existing README has no "Table of Contents" heading + And the existing README uses modern badge zones + When a node is processed + Then there is no "Table of Contents" heading + + Scenario: TOC Definition Provided When Section Already Exists + Given the existing README has an existing "Table of Contents" section + And the existing README uses modern badge zones + And content is provided for the "Table of Contents" section + When a node is processed + Then there is a "Table of Contents" heading + And the "Table of Contents" content is populated