diff --git a/aio/transforms/examples-package/index.js b/aio/transforms/examples-package/index.js index a35ec22a03..ccad58cc33 100644 --- a/aio/transforms/examples-package/index.js +++ b/aio/transforms/examples-package/index.js @@ -13,6 +13,7 @@ module.exports = .factory(require('./services/getExampleRegion')) .processor(require('./processors/collect-examples')) + .processor(require('./processors/render-examples')) .config(function(readFilesProcessor, exampleFileReader) { readFilesProcessor.fileReaders.push(exampleFileReader); diff --git a/aio/transforms/examples-package/inline-tag-defs/example.js b/aio/transforms/examples-package/inline-tag-defs/example.js index b31553bd50..aa3e896f30 100644 --- a/aio/transforms/examples-package/inline-tag-defs/example.js +++ b/aio/transforms/examples-package/inline-tag-defs/example.js @@ -13,8 +13,7 @@ var entities = require('entities'); * {@example core/application_spec.ts -region=hello-app -title='Sample component' } * @kind function */ -module.exports = function exampleInlineTagDef( - parseArgString, exampleMap, createDocMessage, log, collectExamples, getExampleRegion) { +module.exports = function exampleInlineTagDef(parseArgString, createDocMessage, getExampleRegion) { return { name: 'example', description: diff --git a/aio/transforms/examples-package/processors/render-examples.js b/aio/transforms/examples-package/processors/render-examples.js new file mode 100644 index 0000000000..f2c2d0bba5 --- /dev/null +++ b/aio/transforms/examples-package/processors/render-examples.js @@ -0,0 +1,33 @@ +const { parseAttributes } = require('../utils'); + +/** + * Search the renderedContent looking for code examples that have a path (and optionally a region) attribute. + * When they are found replace their content with the appropriate doc-region parsed previously from an example file. + */ +module.exports = function renderExamples(getExampleRegion) { + return { + $runAfter: ['docs-rendered'], + $runBefore: ['writing-files'], + $process: function(docs) { + docs.forEach(doc => { + if (doc.renderedContent) { + // We match either `code-example` or `code-pane` elements that have a path attribute + doc.renderedContent = doc.renderedContent.replace(/<(code-example|code-pane)([^>]*)>[^<]*<\/\1>/, (original, element, attributes) => { + const attrMap = parseAttributes(attributes); + if (attrMap.path) { + // We found a path attribute so look up the example and rebuild the HTML + const exampleContent = getExampleRegion(doc, attrMap.path, attrMap.region); + delete attrMap.path; + delete attrMap.region; + attributes = Object.keys(attrMap).map(key => ` ${key}="${attrMap[key].replace(/"/g, '"')}"`).join(''); + return `<${element}${attributes}>\n${exampleContent}\n`; + } + // No path attribute so just ignore this one + return original; + }); + } + }); + } + }; +}; + diff --git a/aio/transforms/examples-package/processors/render-examples.spec.js b/aio/transforms/examples-package/processors/render-examples.spec.js new file mode 100644 index 0000000000..0b956f7593 --- /dev/null +++ b/aio/transforms/examples-package/processors/render-examples.spec.js @@ -0,0 +1,85 @@ +var testPackage = require('../../helpers/test-package'); +var Dgeni = require('dgeni'); +var path = require('path'); + +describe('renderExamples processor', () => { + var injector, processor, exampleMap, regionParser, collectExamples, exampleMap; + + beforeEach(function() { + const dgeni = new Dgeni([testPackage('examples-package', true)]); + injector = dgeni.configureInjector(); + + exampleMap = injector.get('exampleMap'); + processor = injector.get('renderExamples'); + collectExamples = injector.get('collectExamples'); + exampleMap = injector.get('exampleMap'); + + collectExamples.exampleFolders = ['examples']; + exampleMap['examples'] = { + 'test/url': { regions: { + '': { renderedContent: 'whole file' }, + 'region-1': { renderedContent: 'region 1 contents' } + } } + }; + }); + + it('should run before the correct processor', () => { + expect(processor.$runBefore).toEqual(['writing-files']) + }); + + it('should run after the correct processor', () => { + expect(processor.$runAfter).toEqual(['docs-rendered']); + }); + + ['code-example', 'code-pane'].forEach(CODE_TAG => + describe(CODE_TAG, () => { + it(`should ignore a <${CODE_TAG}> tags with no path attribute`, () => { + const docs = [ + { renderedContent: `Some text\n<${CODE_TAG}>Some code\n<${CODE_TAG} class="anti-pattern" title="Bad Code">do not do this` } + ]; + processor.$process(docs); + expect(docs[0].renderedContent).toEqual(`Some text\n<${CODE_TAG}>Some code\n<${CODE_TAG} class="anti-pattern" title="Bad Code">do not do this`); + }); + + it(`should replace the content of the <${CODE_TAG}> tag with the whole contents from an example file if a path is provided`, () => { + const docs = [ + { renderedContent: `<${CODE_TAG} path="test/url">Some code`} + ]; + processor.$process(docs); + expect(docs[0].renderedContent).toEqual(`<${CODE_TAG}>\nwhole file\n`); + }); + + it('should contain the region contents from the example file if a region is specified', () => { + const docs = [ + { renderedContent: `<${CODE_TAG} path="test/url" region="region-1">Some code` } + ]; + processor.$process(docs); + expect(docs[0].renderedContent).toEqual(`<${CODE_TAG}>\nregion 1 contents\n`); + }); + + it(`should replace the content of the <${CODE_TAG}> tag with the whole contents from an example file if the region is empty`, () => { + const docs = [ + { renderedContent: `<${CODE_TAG} path="test/url" region="">Some code` } + ]; + processor.$process(docs); + expect(docs[0].renderedContent).toEqual(`<${CODE_TAG}>\nwhole file\n`); + }); + + it('should remove the path and region attributes but leave the other attributes alone', () => { + const docs = [ + { renderedContent: `<${CODE_TAG} class="special" path="test/url" linenums="15" region="region-1" id="some-id">Some code` } + ]; + processor.$process(docs); + expect(docs[0].renderedContent).toEqual(`<${CODE_TAG} class="special" linenums="15" id="some-id">\nregion 1 contents\n`); + }); + + it('should cope with spaces and double quotes inside attribute values', () => { + const docs = [ + { renderedContent: `<${CODE_TAG} title='a "quoted" value' path="test/url">`} + ]; + processor.$process(docs); + expect(docs[0].renderedContent).toEqual(`<${CODE_TAG} title="a "quoted" value">\nwhole file\n`); + }); + }) + ); +}); \ No newline at end of file diff --git a/aio/transforms/examples-package/utils.js b/aio/transforms/examples-package/utils.js index d0580917a0..f4dbb85bec 100644 --- a/aio/transforms/examples-package/utils.js +++ b/aio/transforms/examples-package/utils.js @@ -1,7 +1,93 @@ module.exports = { + /** + * Transform the values of an object via a mapper function + * @param {Object} obj + * @param {Function} mapper + */ mapObject(obj, mapper) { const mappedObj = {}; Object.keys(obj).forEach(key => { mappedObj[key] = mapper(key, obj[key]); }); return mappedObj; + }, + + /** + * Parses the attributes from a string taken from an HTML element start tag + * E.g. ` a="one" b="two" ` + * @param {string} str + */ + parseAttributes(str) { + const attrMap = {}; + let index = 0, key, value; + + skipSpace(); + + while(index < str.length) { + takeAttribute(); + skipSpace(); + } + + function takeAttribute() { + const key = takeKey(); + skipSpace(); + if (tryEquals()) { + skipSpace(); + const quote = tryQuote(); + attrMap[key] = takeValue(quote); + // skip the closing quote or whitespace + index++; + } else { + attrMap[key] = true; + } + } + + + function skipSpace() { + while(index < str.length && /\s/.test(str[index])) { + index++; + } + } + + function tryEquals() { + if (str[index] === '=') { + index++; + return true; + } + } + + function takeKey() { + let startIndex = index; + while(index < str.length && /[^\s=]/.test(str[index])) { + index++; + } + return str.substring(startIndex, index); + } + + function tryQuote() { + const quote = str[index]; + if (['"', "'"].indexOf(quote) !== -1) { + index++; + return quote; + } + } + + function takeValue(quote) { + let startIndex = index; + + if (quote) { + while(index < str.length && str[index] !== quote) { + index++; + } + if (index >= str.length) { + throw new Error(`Unterminated quoted attribute value in \`${str}\`. Starting at ${startIndex}. Expected a ${quote} but got "end of string".`); + } + } else { + while(index < str.length && /\S/.test(str[index])) { + index++; + } + } + return str.substring(startIndex, index); + } + + return attrMap; } }; \ No newline at end of file diff --git a/aio/transforms/examples-package/utils.spec.js b/aio/transforms/examples-package/utils.spec.js new file mode 100644 index 0000000000..61b079d7c7 --- /dev/null +++ b/aio/transforms/examples-package/utils.spec.js @@ -0,0 +1,76 @@ +const { mapObject, parseAttributes } = require('./utils'); + +fdescribe('utils', () => { + describe('mapObject', () => { + it('creates a new object', () => { + const testObj = { a: 1 }; + const mappedObj = mapObject(testObj, (key, value) => value); + expect(mappedObj).toEqual(testObj); + expect(mappedObj).not.toBe(testObj); + }); + + it('maps the values via the mapper function', () => { + const testObj = { a: 1, b: 2 }; + const mappedObj = mapObject(testObj, (key, value) => value * 2); + expect(mappedObj).toEqual({ a: 2, b: 4 }); + }); + }); + + describe('parseAttributes', () => { + it('should parse empty string', () => { + const attrs = parseAttributes(''); + expect(attrs).toEqual({ }); + }); + + it('should parse blank string', () => { + const attrs = parseAttributes(' '); + expect(attrs).toEqual({ }); + }); + + it('should parse double quoted attributes', () => { + const attrs = parseAttributes('a="one" b="two"'); + expect(attrs).toEqual({ a: 'one', b: 'two' }); + }); + + it('should parse empty quoted attributes', () => { + const attrs = parseAttributes('a="" b="two"'); + expect(attrs).toEqual({ a: '', b: 'two' }); + }); + + it('should parse single quoted attributes', () => { + const attrs = parseAttributes('a=\'one\' b=\'two\''); + expect(attrs).toEqual({ a: 'one', b: 'two' }); + }); + + it('should ignore whitespace', () => { + const attrs = parseAttributes(' a = "one" b = "two" '); + expect(attrs).toEqual({ a: 'one', b: 'two' }); + }); + + it('should parse attributes with quotes within quotes', () => { + const attrs = parseAttributes('a=\'o"n"e\' b="t\'w\'o"'); + expect(attrs).toEqual({ a: 'o"n"e', b: 't\'w\'o' }); + }); + + it('should parse attributes with spaces in their values', () => { + const attrs = parseAttributes('a="one and two" b="three and four"'); + expect(attrs).toEqual({ a: 'one and two', b: 'three and four' }); + }); + + it('should parse empty attributes', () => { + const attrs = parseAttributes('a b="two"'); + expect(attrs).toEqual({ a: true, b: 'two' }); + }); + + it('should parse unquoted attributes', () => { + const attrs = parseAttributes('a=one b=two'); + expect(attrs).toEqual({ a: 'one', b: 'two' }); + }); + + it('should complain if a quoted attribute is not closed', () => { + expect(() => parseAttributes('a="" b="two')).toThrowError( + 'Unterminated quoted attribute value in `a="" b="two`. Starting at 8. Expected a " but got "end of string".' + ); + }) + }); +}); \ No newline at end of file