build(aio): move doc-gen stuff from angular.io (#14097)

This commit is contained in:
Peter Bacon Darwin
2017-01-26 14:03:53 +00:00
committed by Igor Minar
parent d1d0ce7613
commit b7763559cd
135 changed files with 5031 additions and 1 deletions
@@ -0,0 +1,14 @@
/**
* The point of this reader is to tag all the files that are going to be used as examples in the
* documentation.
* Later on we can extract the regions, via "shredding"; and we can also construct runnable examples
* for passing to plunker and the like.
*/
module.exports = function exampleFileReader(log) {
return {
name: 'exampleFileReader',
getDocs: function(fileInfo) {
return [{docType: 'example-file', content: fileInfo.content, startingLine: 1}];
}
};
};
+29
View File
@@ -0,0 +1,29 @@
var Package = require('dgeni').Package;
var jsdocPackage = require('dgeni-packages/jsdoc');
module.exports =
new Package('examples', [jsdocPackage])
.factory(require('./inline-tag-defs/example'))
// .factory(require('./inline-tag-defs/exampleTabs'))
.factory(require('./services/parseArgString'))
.factory(require('./services/getExampleFilename'))
.factory(require('./services/example-map'))
.factory(require('./file-readers/example-reader'))
.factory(require('./services/region-parser'))
.processor(require('./processors/collect-examples'))
.config(function(readFilesProcessor, exampleFileReader) {
readFilesProcessor.fileReaders.push(exampleFileReader);
})
.config(function(inlineTagProcessor, exampleInlineTagDef) {
inlineTagProcessor.inlineTagDefinitions.push(exampleInlineTagDef);
// inlineTagProcessor.inlineTagDefinitions.push(exampleTabsInlineTagDef);
})
.config(function(computePathsProcessor) {
computePathsProcessor.pathTemplates.push(
{docTypes: ['example-region'], getPath: function() {}, getOutputPath: function() {}});
});
@@ -0,0 +1,57 @@
var path = require('canonical-path');
var fs = require('fs');
var entities = require('entities');
/**
* @dgService exampleInlineTagDef
* @description
* Process inline example tags (of the form {@example relativePath region -title='some title'
* -stylePattern='{some style pattern}' }),
* replacing them with code from a shredded file
* Examples:
* {@example core/application_spec.ts hello-app -title='Sample component' }
* {@example core/application_spec.ts -region=hello-app -title='Sample component' }
* @kind function
*/
module.exports = function exampleInlineTagDef(
parseArgString, exampleMap, getExampleFilename, createDocMessage, log, collectExamples) {
return {
name: 'example',
description:
'Process inline example tags (of the form {@example some/uri Some Title}), replacing them with HTML anchors',
handler: function(doc, tagName, tagDescription) {
const EXAMPLES_FOLDER = collectExamples.exampleFolders[0];
var tagArgs = parseArgString(entities.decodeHTML(tagDescription));
var unnamedArgs = tagArgs._;
var relativePath = unnamedArgs[0];
var regionName = tagArgs.region || (unnamedArgs.length > 1 ? unnamedArgs[1] : null);
var title = tagArgs.title || (unnamedArgs.length > 2 ? unnamedArgs[2] : null);
var stylePattern = tagArgs.stylePattern; // TODO: not yet implemented here
var exampleFile = exampleMap[EXAMPLES_FOLDER][relativePath];
if (!exampleFile) {
log.error(
createDocMessage('Missing example file... relativePath: "' + relativePath + '".', doc));
log.error(
'Example files available are:', Object.keys(exampleMap[EXAMPLES_FOLDER]).join('\n'));
return '';
}
var sourceCode = exampleFile.regions[regionName];
if (!sourceCode) {
log.error(createDocMessage(
'Missing example region... relativePath: "' + relativePath + '", region: "' +
regionName + '".',
doc));
log.error('Regions available are:', Object.keys[exampleFile.regions]);
return '';
}
return sourceCode.renderedContent;
}
};
};
@@ -0,0 +1,61 @@
var path = require('canonical-path');
var fs = require('fs');
/**
* @dgService exampleTabsInlineTagDef
* @description
* Process inline example tags (of the form {@example relativePath region -title='some title'
* -stylePattern='{some style pattern}' }),
* replacing them with a jade makeExample mixin call.
* Examples:
* {@exampleTabs core/application_spec.ts,core/application_spec.ts "hello-app,hello-app2"
* -titles="Hello app1, Hello app2" }
* {@exampleTabs core/application_spec.ts,core/application_spec.ts regions="hello-app,hello-app2"
* -titles="Hello app1, Hello app2" }
* @kind function
*/
module.exports = function exampleTabsInlineTagDef(
getLinkInfo, parseArgString, createDocMessage, log) {
return {
name: 'exampleTabs',
description:
'Process inline example tags (of the form {@example some/uri Some Title}), replacing them with HTML anchors',
handler: function(doc, tagName, tagDescription) {
var tagArgs = parseArgString(tagDescription);
var unnamedArgs = tagArgs._;
var relativePaths = unnamedArgs[0].split(',');
var regions = tagArgs.regions || (unnamedArgs.length > 1 ? unnamedArgs[1] : null);
var titles = tagArgs.titles || (unnamedArgs.length > 2 ? unnamedArgs[2] : null);
if (regions) {
regions = regions.split(',');
}
// TODO: not yet implemented here
var stylePatterns = tagArgs.stylePattern;
var mixinPaths = relativePaths.map(function(relativePath, ix) {
var fragFileName = getApiFragmentFileName(relativePath, regions && regions[ix]);
if (!fs.existsSync(fragFileName)) {
// TODO: log.warn(createDocMessage('Invalid example (unable to locate fragment file: ' +
// quote(fragFileName) + ")", doc));
}
return path.join('_api', relativePath);
});
var comma = ', '
var pathsArg = quote(mixinPaths.join(','));
var regionsArg = regions ? quote(regions.join(',')) : 'null';
var titlesArg = titles ? quote(titles) : 'null';
var res = ['+makeTabs(', pathsArg, comma, regionsArg, comma, titlesArg, ')'].join('');
return res;
}
};
};
function quote(str) {
if (str == null || str.length === 0) return str;
str = str.replace('\'', '\'\'');
return '\'' + str + '\'';
}
@@ -0,0 +1,67 @@
const {mapObject} = require('../utils');
module.exports = function collectExamples(exampleMap, regionParser, log, createDocMessage) {
return {
$runAfter: ['files-read'],
$runBefore: ['parsing-tags'],
$validate: {exampleFolders: {presence: true}},
$process: function(docs) {
const exampleFolders = this.exampleFolders;
const regionDocs = [];
docs = docs.filter((doc) => {
if (doc.docType === 'example-file') {
try {
// find the first matching folder
exampleFolders.some((folder) => {
if (doc.fileInfo.relativePath.indexOf(folder) === 0) {
const relativePath =
doc.fileInfo.relativePath.substr(folder.length).replace(/^\//, '');
exampleMap[folder] = exampleMap[folder] || {};
exampleMap[folder][relativePath] = doc;
const parsedRegions = regionParser(doc.content, doc.fileInfo.extension);
log.debug(
'found example file', folder, relativePath, Object.keys(parsedRegions.regions));
doc.renderedContent = parsedRegions.contents;
// Map each region into a doc that can be put through the rendering pipeline
doc.regions = mapObject(parsedRegions.regions, (regionName, regionContents) => {
const regionDoc =
createRegionDoc(folder, relativePath, regionName, regionContents);
regionDocs.push(regionDoc);
return regionDoc;
});
return true;
}
});
return false;
} catch (e) {
throw new Error(createDocMessage(e.message, doc, e));
}
} else {
return true;
}
});
return docs.concat(regionDocs);
}
};
};
function createRegionDoc(folder, relativePath, regionName, regionContents) {
const path = folder + '/' + relativePath;
const id = path + '#' + regionName
return {
docType: 'example-region',
path: path,
name: regionName,
id: id,
aliases: [id],
contents: regionContents
};
}
@@ -0,0 +1,193 @@
var testPackage = require('../../helpers/test-package');
var Dgeni = require('dgeni');
var path = require('path');
describe('collectExampleRegions processor', () => {
var injector, processor, exampleMap, regionParser;
beforeEach(function() {
regionParser = jasmine.createSpy('regionParser').and.callFake(function(contents, extension) {
return { contents: 'PARSED:' + contents, regions: {dummy: extension} }
});
const dgeni =
new Dgeni([testPackage('examples-package', true).factory('regionParser', function() {
return regionParser;
})]);
injector = dgeni.configureInjector();
exampleMap = injector.get('exampleMap');
processor = injector.get('collectExamples');
processor.exampleFolders = ['examples-1', 'examples-2'];
});
it('should identify example files that are in the exampleFolders', () => {
const docs = [
createDoc('A', 'examples-1/x/app.js'), createDoc('B', 'examples-1/y/index.html'),
createDoc('C', 'examples-2/s/app.js'), createDoc('D', 'examples-2/t/style.css'),
createDoc('E', 'other/b/c.js')
];
processor.$process(docs);
expect(exampleMap['examples-1']['x/app.js']).toBeDefined();
expect(exampleMap['examples-1']['y/index.html']).toBeDefined();
expect(exampleMap['examples-2']['s/app.js']).toBeDefined();
expect(exampleMap['examples-2']['t/style.css']).toBeDefined();
expect(exampleMap['other']).toBeUndefined();
});
it('should remove example files from the docs collection', () => {
const docs = [
createDoc('Example A', 'examples-1/x/app.js'),
createDoc('Example B', 'examples-1/y/index.html'),
createDoc('Other doc 1', 'examples-2/t/style.css', 'content'),
createDoc('Example C', 'examples-2/s/app.js'),
createDoc('Other doc 2', 'other/b/c.js', 'content')
];
const processedDocs = processor.$process(docs);
expect(processedDocs.filter(doc => doc.docType === 'example-file')).toEqual([]);
});
it('should not remove docs from the docs collection that are not example files', () => {
const docs = [
createDoc('Example A', 'examples-1/x/app.js'),
createDoc('Example B', 'examples-1/y/index.html'),
createDoc('Other doc 1', 'examples-2/t/style.css', 'content'),
createDoc('Example C', 'examples-2/s/app.js'),
createDoc('Other doc 2', 'other/b/c.js', 'content')
];
const processedDocs = processor.$process(docs);
expect(processedDocs.filter(doc => doc.docType !== 'example-file'))
.toEqual(jasmine.objectContaining([
createDoc('Other doc 1', 'examples-2/t/style.css', 'content'),
createDoc('Other doc 2', 'other/b/c.js', 'content')
]));
});
it('should call `regionParser` from with the content and file extension of each example doc',
() => {
const docs = [
createDoc('Example A', 'examples-1/x/app.js'),
createDoc('Example B', 'examples-1/y/index.html'),
createDoc('Other doc 1', 'examples-2/t/style.css', 'content'),
createDoc('Example C', 'examples-2/s/app.js'),
createDoc('Other doc 2', 'other/b/c.js', 'content')
];
const processedDocs = processor.$process(docs);
expect(regionParser).toHaveBeenCalledTimes(3);
expect(regionParser).toHaveBeenCalledWith('Example A', 'js');
expect(regionParser).toHaveBeenCalledWith('Example B', 'html');
expect(regionParser).toHaveBeenCalledWith('Example C', 'js');
});
it('should attach parsed content as renderedContent to the example file docs', () => {
const docs = [
createDoc('A', 'examples-1/x/app.js'),
createDoc('B', 'examples-1/y/index.html'),
createDoc('C', 'examples-2/s/app.js'),
createDoc('D', 'examples-2/t/style.css'),
];
processor.$process(docs);
expect(exampleMap['examples-1']['x/app.js'].renderedContent).toEqual('PARSED:A');
expect(exampleMap['examples-1']['y/index.html'].renderedContent).toEqual('PARSED:B');
expect(exampleMap['examples-2']['s/app.js'].renderedContent).toEqual('PARSED:C');
expect(exampleMap['examples-2']['t/style.css'].renderedContent).toEqual('PARSED:D');
});
it('should create region docs for each region in the example file docs', () => {
const docs = [
createDoc('/* #docregion X */\nA', 'examples-1/x/app.js'),
createDoc('<!-- #docregion Y -->\nB', 'examples-1/y/index.html'),
createDoc('/* #docregion Z */\nC', 'examples-2/t/style.css'),
];
const newDocs = processor.$process(docs);
expect(newDocs.length).toEqual(3);
expect(newDocs).toEqual([
jasmine.objectContaining({
docType: 'example-region',
name: 'dummy',
id: 'examples-1/x/app.js#dummy',
contents: 'js'
}),
jasmine.objectContaining({
docType: 'example-region',
name: 'dummy',
id: 'examples-1/y/index.html#dummy',
contents: 'html'
}),
jasmine.objectContaining({
docType: 'example-region',
name: 'dummy',
id: 'examples-2/t/style.css#dummy',
contents: 'css'
})
]);
});
it('should attach region docs to the example file docs', () => {
const docs = [
createDoc('/* #docregion X */\nA', 'examples-1/x/app.js'),
createDoc('<!-- #docregion Y -->\nB', 'examples-1/y/index.html'),
createDoc('/* #docregion Z */\nC', 'examples-2/t/style.css'),
];
processor.$process(docs);
expect(exampleMap['examples-1']['x/app.js'].regions).toEqual({
dummy: {
docType: 'example-region',
path: 'examples-1/x/app.js',
name: 'dummy',
id: 'examples-1/x/app.js#dummy',
aliases: ['examples-1/x/app.js#dummy'],
contents: 'js'
}
});
expect(exampleMap['examples-1']['y/index.html'].regions).toEqual({
dummy: {
docType: 'example-region',
path: 'examples-1/y/index.html',
name: 'dummy',
id: 'examples-1/y/index.html#dummy',
aliases: ['examples-1/y/index.html#dummy'],
contents: 'html'
}
});
expect(exampleMap['examples-2']['t/style.css'].regions).toEqual({
dummy: {
docType: 'example-region',
path: 'examples-2/t/style.css',
name: 'dummy',
id: 'examples-2/t/style.css#dummy',
aliases: ['examples-2/t/style.css#dummy'],
contents: 'css'
}
});
});
});
function createDoc(content, relativePath, docType) {
return {
fileInfo: {relativePath: relativePath, extension: path.extname(relativePath).substr(1)},
content: content,
docType: docType || 'example-file',
startingLine: 1
};
}
@@ -0,0 +1,3 @@
module.exports = function exampleMap() {
return {};
};
@@ -0,0 +1,9 @@
module.exports = function getExampleFilename() {
function getExampleFilenameImpl(relativePath) {
return getExampleFilenameImpl.examplesFolder + relativePath;
}
getExampleFilenameImpl.examplesFolder = '@angular/examples/';
return getExampleFilenameImpl;
};
@@ -0,0 +1,55 @@
/**
* @dgService parseArgString
* @description
* processes an arg string in 'almost' the same fashion that the command processor does
* and returns an args object in yargs format.
* @kind function
* @param {String} str The arg string to process
* @return {Object} The args parsed into a yargs format.
*/
module.exports = function parseArgString() {
return function parseArgStringImpl(str) {
// regex from npm string-argv
//[^\s'"] Match if not a space ' or "
//+|['] or Match '
//([^']*) Match anything that is not '
//['] Close match if '
//+|["] or Match "
//([^"]*) Match anything that is not "
//["] Close match if "
var rx = /[^\s'"]+|[']([^']*?)[']|["]([^"]*?)["]/gi;
var value = str;
var unnammedArgs = [];
var args = {_: unnammedArgs};
var match, key;
do {
// Each call to exec returns the next regex match as an array
match = rx.exec(value);
if (match !== null) {
// Index 1 in the array is the captured group if it exists
// Index 0 is the matched text, which we use if no captured group exists
var arg = match[2] ? match[2] : (match[1] ? match[1] : match[0]);
if (key) {
args[key] = arg;
key = null;
} else {
if (arg.substr(arg.length - 1) === '=') {
key = arg.substr(0, arg.length - 1);
// remove leading '-' if it exists.
if (key.substr(0, 1) == '-') {
key = key.substr(1);
}
} else {
unnammedArgs.push(arg)
key = null;
}
}
}
} while (match !== null);
return args;
}
}
@@ -0,0 +1,7 @@
// These kind of comments are used CSS and other languages that do not support inline comments
module.exports = {
regionStartMatcher: /^\s*\/\*\s*#docregion\s*(.*)\s*\*\/\s*$/,
regionEndMatcher: /^\s*\/\*\s*#enddocregion\s*(.*)\s*\*\/\s*$/,
plasterMatcher: /^\s*\/\*\s*#docplaster\s*(.*)\s*\*\/\s*$/,
createPlasterComment: plaster => `/* ${plaster} */`
};
@@ -0,0 +1,55 @@
const matcher = require('./block-c');
describe('block-c region-matcher', () => {
it('should match start annotations', () => {
let matches;
matches = matcher.regionStartMatcher.exec('/* #docregion A b c */');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c ');
matches = matcher.regionStartMatcher.exec('/*#docregion A b c*/');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.regionStartMatcher.exec('/* #docregion */');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('');
});
it('should match end annotations', () => {
let matches;
matches = matcher.regionEndMatcher.exec('/* #enddocregion A b c */');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c ');
matches = matcher.regionEndMatcher.exec('/*#enddocregion A b c*/');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.regionEndMatcher.exec('/* #enddocregion */');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('');
});
it('should match plaster annotations', () => {
let matches;
matches = matcher.plasterMatcher.exec('/* #docplaster A b c */');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c ');
matches = matcher.plasterMatcher.exec('/*#docplaster A b c*/');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.plasterMatcher.exec('/* #docplaster */');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('');
});
it('should create a plaster comment', () => {
expect(matcher.createPlasterComment('... elided ...')).toEqual('/* ... elided ... */');
});
});
@@ -0,0 +1,7 @@
// These kind of comments are used in HTML
module.exports = {
regionStartMatcher: /^\s*<!--\s*#docregion\s*(.*)\s*-->\s*$/,
regionEndMatcher: /^\s*<!--\s*#enddocregion\s*(.*)\s*-->\s*$/,
plasterMatcher: /^\s*<!--\s*#docplaster\s*(.*)\s*-->\s*$/,
createPlasterComment: plaster => `<!-- ${plaster} -->`
};
@@ -0,0 +1,55 @@
const matcher = require('./html');
describe('html region-matcher', () => {
it('should match start annotations', () => {
let matches;
matches = matcher.regionStartMatcher.exec('<!-- #docregion A b c -->');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c ');
matches = matcher.regionStartMatcher.exec('<!--#docregion A b c-->');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.regionStartMatcher.exec('<!-- #docregion -->');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('');
});
it('should match end annotations', () => {
let matches;
matches = matcher.regionEndMatcher.exec('<!-- #enddocregion A b c -->');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c ');
matches = matcher.regionEndMatcher.exec('<!--#enddocregion A b c-->');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.regionEndMatcher.exec('<!-- #enddocregion -->');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('');
});
it('should match plaster annotations', () => {
let matches;
matches = matcher.plasterMatcher.exec('<!-- #docplaster A b c -->');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c ');
matches = matcher.plasterMatcher.exec('<!--#docplaster A b c-->');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.plasterMatcher.exec('<!-- #docplaster -->');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('');
});
it('should create a plaster comment', () => {
expect(matcher.createPlasterComment('... elided ...')).toEqual('<!-- ... elided ... -->');
});
});
@@ -0,0 +1,7 @@
// These kind of comments are used in languages that do not support block comments, such as Jade
module.exports = {
regionStartMatcher: /^\s*\/\/\s*#docregion\s*(.*)\s*$/,
regionEndMatcher: /^\s*\/\/\s*#enddocregion\s*(.*)\s*$/,
plasterMatcher: /^\s*\/\/\s*#docplaster\s*(.*)\s*$/,
createPlasterComment: plaster => `// ${plaster}`
};
@@ -0,0 +1,55 @@
const matcher = require('./inline-c-only');
describe('inline-c-only region-matcher', () => {
it('should match start annotations', () => {
let matches;
matches = matcher.regionStartMatcher.exec('// #docregion A b c');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.regionStartMatcher.exec('//#docregion A b c');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.regionStartMatcher.exec('// #docregion');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('');
});
it('should match end annotations', () => {
let matches;
matches = matcher.regionEndMatcher.exec('// #enddocregion A b c');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.regionEndMatcher.exec('//#enddocregion A b c');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.regionEndMatcher.exec('// #enddocregion');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('');
});
it('should match plaster annotations', () => {
let matches;
matches = matcher.plasterMatcher.exec('// #docplaster A b c');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.plasterMatcher.exec('//#docplaster A b c');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.plasterMatcher.exec('// #docplaster');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('');
});
it('should create a plaster comment', () => {
expect(matcher.createPlasterComment('... elided ...')).toEqual('// ... elided ...');
});
});
@@ -0,0 +1,7 @@
// This comment type is used in C like languages such as JS, TS, Dart, etc
module.exports = {
regionStartMatcher: /^\s*\/\/\s*#docregion\s*(.*)\s*$/,
regionEndMatcher: /^\s*\/\/\s*#enddocregion\s*(.*)\s*$/,
plasterMatcher: /^\s*\/\/\s*#docplaster\s*(.*)\s*$/,
createPlasterComment: plaster => `/* ${plaster} */`
};
@@ -0,0 +1,55 @@
const matcher = require('./inline-c');
describe('inline-c region-matcher', () => {
it('should match start annotations', () => {
let matches;
matches = matcher.regionStartMatcher.exec('// #docregion A b c');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.regionStartMatcher.exec('//#docregion A b c');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.regionStartMatcher.exec('// #docregion');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('');
});
it('should match end annotations', () => {
let matches;
matches = matcher.regionEndMatcher.exec('// #enddocregion A b c');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.regionEndMatcher.exec('//#enddocregion A b c');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.regionEndMatcher.exec('// #enddocregion');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('');
});
it('should match plaster annotations', () => {
let matches;
matches = matcher.plasterMatcher.exec('// #docplaster A b c');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.plasterMatcher.exec('//#docplaster A b c');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.plasterMatcher.exec('// #docplaster');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('');
});
it('should create a plaster comment', () => {
expect(matcher.createPlasterComment('... elided ...')).toEqual('/* ... elided ... */');
});
});
@@ -0,0 +1,7 @@
// These type of comments are used in hash comment based languages such as bash and Yaml
module.exports = {
regionStartMatcher: /^\s*#\s*#docregion\s*(.*)\s*$/,
regionEndMatcher: /^\s*#\s*#enddocregion\s*(.*)\s*$/,
plasterMatcher: /^\s*#\s*#docplaster\s*(.*)\s*$/,
createPlasterComment: plaster => `# ${plaster}`
};
@@ -0,0 +1,54 @@
const matcher = require('./inline-hash');
describe('inline-hash region-matcher', () => {
it('should match start annotations', () => {
let matches;
matches = matcher.regionStartMatcher.exec('# #docregion A b c');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.regionStartMatcher.exec('##docregion A b c');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.regionStartMatcher.exec('# #docregion');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('');
});
it('should match end annotations', () => {
let matches;
matches = matcher.regionEndMatcher.exec('# #enddocregion A b c');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.regionEndMatcher.exec('##enddocregion A b c');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.regionEndMatcher.exec('# #enddocregion');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('');
});
it('should match plaster annotations', () => {
let matches;
matches = matcher.plasterMatcher.exec('# #docplaster A b c');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.plasterMatcher.exec('##docplaster A b c');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('A b c');
matches = matcher.plasterMatcher.exec('# #docplaster');
expect(matches).not.toBeNull();
expect(matches[1]).toEqual('');
});
it('should create a plaster comment',
() => { expect(matcher.createPlasterComment('... elided ...')).toEqual('# ... elided ...'); });
});
@@ -0,0 +1,112 @@
const blockC = require('./region-matchers/block-c');
const html = require('./region-matchers/html');
const inlineC = require('./region-matchers/inline-c');
const inlineCOnly = require('./region-matchers/inline-c-only');
const inlineHash = require('./region-matchers/inline-hash');
const NO_NAME_REGION = '';
const DEFAULT_PLASTER = '. . .';
const {mapObject} = require('../utils');
module.exports = function regionParser() {
return regionParserImpl;
};
regionParserImpl.regionMatchers = {
ts: inlineC,
js: inlineC,
es6: inlineC,
dart: inlineC,
html: html,
css: blockC,
yaml: inlineHash,
jade: inlineCOnly
};
/**
* @param contents string
* @param fileType string
* @returns {contents: string, regions: {[regionName: string]: string}}
*/
function regionParserImpl(contents, fileType) {
const regionMatcher = regionParserImpl.regionMatchers[fileType];
const openRegions = [];
const regionMap = {};
if (regionMatcher) {
let plaster = regionMatcher.createPlasterComment(DEFAULT_PLASTER);
const lines = contents.split(/\r?\n/).filter((line, index) => {
const startRegion = line.match(regionMatcher.regionStartMatcher);
const endRegion = line.match(regionMatcher.regionEndMatcher);
const updatePlaster = line.match(regionMatcher.plasterMatcher);
// start region processing
if (startRegion) {
// open up the specified region
const regionName = getRegionName(startRegion[1]);
const region = regionMap[regionName];
if (region) {
if (region.open) {
throw new RegionParserError(
`Tried to open a region, named "${regionName}", that is already open`, index);
}
region.open = true;
region.lines.push(plaster);
} else {
regionMap[regionName] = {lines: [], open: true};
}
openRegions.push(regionName);
// end region processing
} else if (endRegion) {
if (openRegions.length === 0) {
throw new RegionParserError('Tried to close a region when none are open', index);
}
// close down the specified region (or most recent if no name is given)
const regionName = getRegionName(endRegion[1]) || openRegions[openRegions.length - 1];
const region = regionMap[regionName];
if (!region || !region.open) {
throw new RegionParserError(
`Tried to close a region, named "${regionName}", that is not open`, index);
}
region.open = false;
removeLast(openRegions, regionName);
// doc plaster processing
} else if (updatePlaster) {
plaster = regionMatcher.createPlasterComment(updatePlaster[1].trim());
// simple line of content processing
} else {
openRegions.forEach(regionName => regionMap[regionName].lines.push(line));
// do not filter out this line from the content
return true;
}
// this line contained an annotation so let's filter it out
return false;
});
return {
contents: lines.join('\n'),
regions: mapObject(regionMap, (regionName, region) => region.lines.join('\n'))
};
} else {
return {contents, regions: {}};
}
}
function getRegionName(input) {
return input.trim();
}
function removeLast(array, item) {
const index = array.lastIndexOf(item);
array.splice(index, 1);
}
function RegionParserError(message, lineNum) {
this.message = `regionParser: ${message} (at line ${lineNum}).`;
this.lineNum = lineNum;
this.stack = (new Error()).stack;
}
RegionParserError.prototype = Object.create(Error.prototype);
RegionParserError.prototype.constructor = RegionParserError;
@@ -0,0 +1,143 @@
var testPackage = require('../../helpers/test-package');
var Dgeni = require('dgeni');
const testRegionMatcher = {
regionStartMatcher: /^\s*\/\*\s*#docregion\s+(.*)\s*\*\/\s*$/,
regionEndMatcher: /^\s*\/\*\s*#enddocregion\s+(.*)\s*\*\/\s*$/,
plasterMatcher: /^\s*\/\*\s*#docplaster\s+(.*)\s*\*\/\s*$/,
createPlasterComment: plaster => `/* ${plaster} */`
};
describe('regionParser service', () => {
var dgeni, injector, regionParser;
beforeEach(function() {
dgeni = new Dgeni([testPackage('examples-package', true)]);
injector = dgeni.configureInjector();
regionParser = injector.get('regionParser');
regionParser.regionMatchers = {'test-type': testRegionMatcher};
});
it('should return just the contents if there is no region-matcher for the file type', () => {
const output = regionParser('some contents', 'unknown');
expect(output).toEqual({contents: 'some contents', regions: {}});
});
it('should return just the contents if there is a region-matcher but no regions', () => {
const output = regionParser('some contents', 'test-type');
expect(output).toEqual({contents: 'some contents', regions: {}});
});
it('should remove start region annotations from the contents', () => {
const output = regionParser(
t('/* #docregion */', 'abc', '/* #docregion X */', 'def', '/* #docregion Y */', 'ghi'),
'test-type');
expect(output.contents).toEqual(t('abc', 'def', 'ghi'));
});
it('should remove end region annotations from the contents', () => {
const output = regionParser(
t('/* #docregion */', 'abc', '/* #docregion X */', 'def', '/* #enddocregion X */',
'/* #docregion Y */', 'ghi', '/* #enddocregion Y */', '/* #enddocregion */'),
'test-type');
expect(output.contents).toEqual(t('abc', 'def', 'ghi'));
});
it('should remove doc plaster annotations from the contents', () => {
const output =
regionParser(t('/* #docplaster ... elided ... */', 'abc', 'def', 'ghi'), 'test-type');
expect(output.contents).toEqual(t('abc', 'def', 'ghi'));
});
it('should capture the rest of the contents for a region with no end region annotation', () => {
const output = regionParser(
t('/* #docregion */', 'abc', '/* #docregion X */', 'def', '/* #docregion Y */', 'ghi'),
'test-type');
expect(output.regions['']).toEqual(t('abc', 'def', 'ghi'));
expect(output.regions['X']).toEqual(t('def', 'ghi'));
expect(output.regions['Y']).toEqual(t('ghi'));
});
it('should capture the contents for a region up to the end region annotation', () => {
const output = regionParser(
t('/* #docregion */', 'abc', '/* #enddocregion */', '/* #docregion X */', 'def',
'/* #enddocregion X */', '/* #docregion Y */', 'ghi', '/* #enddocregion Y */'),
'test-type');
expect(output.regions['']).toEqual(t('abc'));
expect(output.regions['X']).toEqual(t('def'));
expect(output.regions['Y']).toEqual(t('ghi'));
});
it('should close the most recently opened region if there is no region name', () => {
const output = regionParser(
t('/* #docregion X*/', 'abc', '/* #docregion Y */', 'def', '/* #enddocregion */', 'ghi',
'/* #enddocregion */'),
'test-type');
expect(output.regions['X']).toEqual(t('abc', 'def', 'ghi'));
expect(output.regions['Y']).toEqual(t('def'));
});
it('should handle overlapping regions', () => {
const output = regionParser(
t('/* #docregion X*/', 'abc', '/* #docregion Y */', 'def', '/* #enddocregion X */', 'ghi',
'/* #enddocregion Y */'),
'test-type');
expect(output.regions['X']).toEqual(t('abc', 'def'));
expect(output.regions['Y']).toEqual(t('def', 'ghi'));
});
it('should error if we attempt to open an already open region', () => {
expect(() => regionParser(t('/* #docregion */', 'abc', '/* #docregion */', 'def'), 'test-type'))
.toThrowError(
'regionParser: Tried to open a region, named "", that is already open (at line 2).');
expect(
() =>
regionParser(t('/* #docregion X */', 'abc', '/* #docregion X */', 'def'), 'test-type'))
.toThrowError(
'regionParser: Tried to open a region, named "X", that is already open (at line 2).');
});
it('should error if we attempt to close an already closed region', () => {
expect(() => regionParser(t('abc', '/* #enddocregion */', 'def'), 'test-type'))
.toThrowError('regionParser: Tried to close a region when none are open (at line 1).');
expect(
() =>
regionParser(t('/* #docregion */', 'abc', '/* #enddocregion X */', 'def'), 'test-type'))
.toThrowError(
'regionParser: Tried to close a region, named "X", that is not open (at line 2).');
});
it('should handle whitespace in region names on single annotation', () => {
const output =
regionParser(t('/* #docregion A B*/', 'abc', '/* #docregion A C */', 'def'), 'test-type');
expect(output.regions['A B']).toEqual(t('abc', 'def'));
expect(output.regions['A C']).toEqual(t('def'));
});
it('should join multiple regions with the default plaster string (". . .")', () => {
const output = regionParser(
t('/* #docregion */', 'abc', '/* #enddocregion */', 'def', '/* #docregion */', 'ghi',
'/* #enddocregion */'),
'test-type');
expect(output.regions['']).toEqual(t('abc', '/* . . . */', 'ghi'));
});
it('should join multiple regions with the current plaster string', () => {
const output = regionParser(
t('/* #docregion */', 'abc', '/* #enddocregion */', 'def', '/* #docregion */', 'ghi',
'/* #enddocregion */', '/* #docplaster ... elided ... */', '/* #docregion A */', 'jkl',
'/* #enddocregion A */', 'mno', '/* #docregion A */', 'pqr', '/* #enddocregion A */'),
'test-type');
expect(output.regions['']).toEqual(t('abc', '/* . . . */', 'ghi'));
expect(output.regions['A']).toEqual(t('jkl', '/* ... elided ... */', 'pqr'));
});
});
function t() {
return Array.prototype.join.call(arguments, '\n');
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
mapObject(obj, mapper) {
const mappedObj = {};
Object.keys(obj).forEach(key => { mappedObj[key] = mapper(key, obj[key]); });
return mappedObj;
}
};