build(aio): move doc-gen stuff from angular.io (#14097)
This commit is contained in:
committed by
Igor Minar
parent
d1d0ce7613
commit
b7763559cd
@@ -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}];
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
mapObject(obj, mapper) {
|
||||
const mappedObj = {};
|
||||
Object.keys(obj).forEach(key => { mappedObj[key] = mapper(key, obj[key]); });
|
||||
return mappedObj;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user