diff --git a/aio/tools/transforms/angular-api-package/content-rules/noMarkdownHeadings.js b/aio/tools/transforms/angular-api-package/content-rules/noMarkdownHeadings.js new file mode 100644 index 0000000000..427fb9bc90 --- /dev/null +++ b/aio/tools/transforms/angular-api-package/content-rules/noMarkdownHeadings.js @@ -0,0 +1,50 @@ +/** + * A factory for creating a rule for the `checkContentRules` processor, which disallows markdown + * headings in a content property. + * + * @param {...number|string} disallowedHeadings + * Each parameter identifies heading levels that are not allowed. They can be in the form of: + * + * - a number (e.g. 1), which implies that the specified heading is not allowed + * - a range (e.g. '2,3'), which implies the range of headings that are not allowed + * + * (A range can be open ended on the upper bound by not specifying a value after the comma.) + * + * @example + * To create a rule that will only allow level 3 headings: + * + * ``` + * const rule = createNoMarkdownHeadingRule(1, 2, '4,'); + * ``` + * + */ +module.exports = function createrNoMarkdownHeadingRule() { + const args = Array.prototype.slice.apply(arguments); + const disallowedHeadings = args.map(arg => `#{${arg}}`); + if (!disallowedHeadings.length) { + disallowedHeadings.push('#{1,}'); + } + const regex = new RegExp(`^ {0,3}(${disallowedHeadings.join('|')}) +.*$`, 'mg'); + return (doc, prop, value) => { + let match, matches = []; + while(match = regex.exec(value)) { // eslint-disable-line no-cond-assign + matches.push(match[0]); + } + if (matches.length) { + const list = listify(matches.map(match => `"${match}"`)); + return `Invalid headings found in "${prop}" property: ${list}.`; + } + }; +}; + + +/** + * Convert an array of strings in to a human list - e.g separated by commas and the word `and`. + * @param {string[]} values The strings to convert to a list + */ +function listify(values) { + if (values.length <= 1) return values; + const last = values[values.length - 1]; + const rest = values.slice(0, values.length - 1); + return [rest.join(', '), last].join(' and '); +} \ No newline at end of file diff --git a/aio/tools/transforms/angular-api-package/content-rules/noMarkdownHeadings.spec.js b/aio/tools/transforms/angular-api-package/content-rules/noMarkdownHeadings.spec.js new file mode 100644 index 0000000000..82c68185ff --- /dev/null +++ b/aio/tools/transforms/angular-api-package/content-rules/noMarkdownHeadings.spec.js @@ -0,0 +1,59 @@ +const createNoMarkdownHeadings = require('./noMarkdownHeadings'); + +describe('createNoMarkdownHeadings rule', () => { + + const noMarkdownHeadings = createNoMarkdownHeadings(); + + it('should return `undefined` if there is no heading in a value', () => { + expect(noMarkdownHeadings({}, 'description', 'some ## text')) + .toBeUndefined(); + }); + + it('should return an error message if there is a markdown heading in a single line value', () => { + expect(noMarkdownHeadings({}, 'description', '# heading 1')) + .toEqual('Invalid headings found in "description" property: "# heading 1".'); + }); + + it('should return an error message if there is a markdown heading in a multiline value', () => { + expect(noMarkdownHeadings({}, 'description', 'some text\n# heading 1')) + .toEqual('Invalid headings found in "description" property: "# heading 1".'); + }); + + it('should cope with up to 3 spaces before the heading marker', () => { + expect(noMarkdownHeadings({}, 'description', ' # heading 1')) + .toEqual('Invalid headings found in "description" property: " # heading 1".'); + expect(noMarkdownHeadings({}, 'description', ' # heading 1')) + .toEqual('Invalid headings found in "description" property: " # heading 1".'); + expect(noMarkdownHeadings({}, 'description', ' # heading 1')) + .toEqual('Invalid headings found in "description" property: " # heading 1".'); + }); + + it('should return an error message for each heading found', () => { + expect(noMarkdownHeadings({}, 'description', '# heading 1\nsome text\n## heading 2\nmore text\n### heading 3')) + .toEqual('Invalid headings found in "description" property: "# heading 1", "## heading 2" and "### heading 3".'); + }); + + describe('(specified heading levels)', () => { + it('should take heading levels into account', () => { + const noTopLevelHeadings = createNoMarkdownHeadings(1); + expect(noTopLevelHeadings({}, 'description', '# top level')) + .toEqual('Invalid headings found in "description" property: "# top level".'); + expect(noTopLevelHeadings({}, 'description', '## second level')) + .toBeUndefined(); + expect(noTopLevelHeadings({}, 'description', '### third level')) + .toBeUndefined(); + expect(noTopLevelHeadings({}, 'description', '#### fourth level')) + .toBeUndefined(); + + const allowLevel3Headings = createNoMarkdownHeadings(1, 2, '4,'); + expect(allowLevel3Headings({}, 'description', '# top level')) + .toEqual('Invalid headings found in "description" property: "# top level".'); + expect(allowLevel3Headings({}, 'description', '## second level')) + .toEqual('Invalid headings found in "description" property: "## second level".'); + expect(allowLevel3Headings({}, 'description', '### third level')) + .toBeUndefined(); + expect(allowLevel3Headings({}, 'description', '#### fourth level')) + .toEqual('Invalid headings found in "description" property: "#### fourth level".'); + }); + }); +});