build(aio): add checkContentRules processor (#22759)
This processor will enable us to write rules about how the content should appear, such as: * no headings in markdown content * only one sentence per line * no single character parameter names * etc. PR Close #22759
This commit is contained in:
parent
af46d097ff
commit
e0ae74d40e
|
@ -30,6 +30,7 @@ module.exports = new Package('angular-base', [
|
||||||
.processor(require('./processors/fixInternalDocumentLinks'))
|
.processor(require('./processors/fixInternalDocumentLinks'))
|
||||||
.processor(require('./processors/copyContentAssets'))
|
.processor(require('./processors/copyContentAssets'))
|
||||||
.processor(require('./processors/renderLinkInfo'))
|
.processor(require('./processors/renderLinkInfo'))
|
||||||
|
.processor(require('./processors/checkContentRules'))
|
||||||
|
|
||||||
// overrides base packageInfo and returns the one for the 'angular/angular' repo.
|
// overrides base packageInfo and returns the one for the 'angular/angular' repo.
|
||||||
.factory('packageInfo', function() { return require(path.resolve(PROJECT_ROOT, 'package.json')); })
|
.factory('packageInfo', function() { return require(path.resolve(PROJECT_ROOT, 'package.json')); })
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A processor that can run arbitrary checking rules against properties of documents
|
||||||
|
|
||||||
|
* The configuration for the processor is via the `docTypeRules`.
|
||||||
|
* This is a hash of docTypes to rulesets.
|
||||||
|
* Each rules set is a hash of properties to rule functions.
|
||||||
|
*
|
||||||
|
* The processor will run each rule function against each matching property of each matching doc.
|
||||||
|
*
|
||||||
|
* An example rule might look like:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* function noMarkdownHeadings(doc, prop, value) {
|
||||||
|
* const match = /^\s?#+\s+.*$/m.exec(value);
|
||||||
|
* if (match) {
|
||||||
|
* return `Headings not allowed in "${prop}" property. Found "${match[0]}"`;
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
module.exports = function checkContentRules(log, createDocMessage) {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* {
|
||||||
|
* [docType]: {
|
||||||
|
* [property]: Array<(doc: Document, property: string, value: any) => string|undefined>
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
docTypeRules: {},
|
||||||
|
failOnContentErrors: false,
|
||||||
|
$runAfter: ['tags-extracted'],
|
||||||
|
$runBefore: ['processing-docs'],
|
||||||
|
$process(docs) {
|
||||||
|
const errors = [];
|
||||||
|
docs.forEach(doc => {
|
||||||
|
const docErrors = [];
|
||||||
|
const rules = this.docTypeRules[doc.docType] || {};
|
||||||
|
if (rules) {
|
||||||
|
Object.keys(rules).forEach(property => {
|
||||||
|
const ruleFns = rules[property];
|
||||||
|
ruleFns.forEach(ruleFn => {
|
||||||
|
const error = ruleFn(doc, property, doc[property]);
|
||||||
|
if (error) {
|
||||||
|
docErrors.push(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (docErrors.length) {
|
||||||
|
errors.push({ doc, errors: docErrors });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
log.error('Content contains errors');
|
||||||
|
errors.forEach(docError => {
|
||||||
|
const errors = docError.errors.join('\n ');
|
||||||
|
log.error(createDocMessage(errors + '\n ', docError.doc));
|
||||||
|
});
|
||||||
|
if (this.failOnContentErrors) {
|
||||||
|
throw new Error('Stopping due to content errors.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,108 @@
|
||||||
|
var testPackage = require('../../helpers/test-package');
|
||||||
|
var Dgeni = require('dgeni');
|
||||||
|
|
||||||
|
describe('checkContentRules processor', function() {
|
||||||
|
let processor, logger;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
const dgeni = new Dgeni([testPackage('angular-base-package')]);
|
||||||
|
const injector = dgeni.configureInjector();
|
||||||
|
processor = injector.get('checkContentRules');
|
||||||
|
logger = injector.get('log');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exist on the injector', () => {
|
||||||
|
expect(processor).toBeDefined();
|
||||||
|
expect(processor.$process).toEqual(jasmine.any(Function));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shpuld run at the right time', () => {
|
||||||
|
expect(processor.$runAfter).toEqual(['tags-extracted']);
|
||||||
|
expect(processor.$runBefore).toEqual(['processing-docs']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing if not configured', () => {
|
||||||
|
const docs = [{ docType: 'test', description: '## heading 2' }];
|
||||||
|
processor.$process(docs);
|
||||||
|
expect(docs).toEqual([{ docType: 'test', description: '## heading 2' }]);
|
||||||
|
|
||||||
|
expect(logger.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run configured rules against matching docs', () => {
|
||||||
|
const nameSpy1 = jasmine.createSpy('name 1');
|
||||||
|
const nameSpy2 = jasmine.createSpy('name 2');
|
||||||
|
const nameSpy3 = jasmine.createSpy('name 3');
|
||||||
|
const descriptionSpy1 = jasmine.createSpy('description 1');
|
||||||
|
const descriptionSpy2 = jasmine.createSpy('description 2');
|
||||||
|
const descriptionSpy3 = jasmine.createSpy('description 3');
|
||||||
|
|
||||||
|
processor.docTypeRules = {
|
||||||
|
'test1': {
|
||||||
|
name: [nameSpy1, nameSpy3],
|
||||||
|
description: [descriptionSpy1, descriptionSpy3]
|
||||||
|
},
|
||||||
|
'test2': {
|
||||||
|
name: [nameSpy2],
|
||||||
|
description: [descriptionSpy2]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const docs = [
|
||||||
|
{ docType: 'test1', description: 'test doc 1', name: 'test-1' },
|
||||||
|
{ docType: 'test2', description: 'test doc 2', name: 'test-2' }
|
||||||
|
];
|
||||||
|
processor.$process(docs);
|
||||||
|
expect(nameSpy1).toHaveBeenCalledTimes(1);
|
||||||
|
expect(nameSpy1).toHaveBeenCalledWith(docs[0], 'name', 'test-1');
|
||||||
|
expect(nameSpy2).toHaveBeenCalledTimes(1);
|
||||||
|
expect(nameSpy2).toHaveBeenCalledWith(docs[1], 'name', 'test-2');
|
||||||
|
expect(nameSpy3).toHaveBeenCalledTimes(1);
|
||||||
|
expect(nameSpy3).toHaveBeenCalledWith(docs[0], 'name', 'test-1');
|
||||||
|
expect(descriptionSpy1).toHaveBeenCalledTimes(1);
|
||||||
|
expect(descriptionSpy1).toHaveBeenCalledWith(docs[0], 'description', 'test doc 1');
|
||||||
|
expect(descriptionSpy2).toHaveBeenCalledTimes(1);
|
||||||
|
expect(descriptionSpy2).toHaveBeenCalledWith(docs[1], 'description', 'test doc 2');
|
||||||
|
expect(descriptionSpy3).toHaveBeenCalledTimes(1);
|
||||||
|
expect(descriptionSpy3).toHaveBeenCalledWith(docs[0], 'description', 'test doc 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log errors if the rule returns error messages', () => {
|
||||||
|
const nameSpy1 = jasmine.createSpy('name 1').and.returnValue('name error message');
|
||||||
|
const descriptionSpy1 = jasmine.createSpy('description 1').and.returnValue('description error message');
|
||||||
|
|
||||||
|
processor.docTypeRules = {
|
||||||
|
'test1': {
|
||||||
|
name: [nameSpy1],
|
||||||
|
description: [descriptionSpy1]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const docs = [
|
||||||
|
{ docType: 'test1', description: 'test doc 1', name: 'test-1' },
|
||||||
|
{ docType: 'test2', description: 'test doc 2', name: 'test-2' }
|
||||||
|
];
|
||||||
|
|
||||||
|
processor.$process(docs);
|
||||||
|
|
||||||
|
expect(logger.error).toHaveBeenCalledTimes(2);
|
||||||
|
expect(logger.error).toHaveBeenCalledWith('Content contains errors');
|
||||||
|
expect(logger.error).toHaveBeenCalledWith(`name error message
|
||||||
|
description error message
|
||||||
|
- doc "test-1" (test1) `);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if `failOnContentErrors` is true and errors are found', () => {
|
||||||
|
const errorRule = jasmine.createSpy('error rule').and.returnValue('some error');
|
||||||
|
processor.docTypeRules = {
|
||||||
|
'test': { description: [errorRule] }
|
||||||
|
};
|
||||||
|
processor.failOnContentErrors = true;
|
||||||
|
|
||||||
|
const docs = [
|
||||||
|
{ docType: 'test', description: 'test doc' },
|
||||||
|
];
|
||||||
|
expect(() => processor.$process(docs)).toThrowError('Stopping due to content errors.');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
Loading…
Reference in New Issue