diff --git a/docs/content/cookbook/component-relative-paths.md b/docs/content/cookbook/component-relative-paths.md index b8b8c3ca1c..ecace6d84c 100644 --- a/docs/content/cookbook/component-relative-paths.md +++ b/docs/content/cookbook/component-relative-paths.md @@ -88,12 +88,20 @@ The result looks like this: and download the source code from there or simply read the pertinent source here. -{@tabs} - {@example 'cb-component-relative-paths/ts/app/some.component.ts' -title='app/some.component.ts'} - {@example 'cb-component-relative-paths/ts/app/some.component.html' -title='app/some.component.html'} - {@example 'cb-component-relative-paths/ts/app/some.component.css' -title='app/some.component.css'} - {@example 'cb-component-relative-paths/ts/app/app.component.ts' -title='app/app.component.ts'} -{@endtabs} + + + {@example 'cb-component-relative-paths/ts/app/some.component.ts'} + + + {@example 'cb-component-relative-paths/ts/app/some.component.html'} + + + {@example 'cb-component-relative-paths/ts/app/some.component.css'} + + + {@example 'cb-component-relative-paths/ts/app/app.component.ts'} + + {@a why-default} diff --git a/docs/templates/content.template.html b/docs/templates/content.template.html new file mode 100644 index 0000000000..8546da54e4 --- /dev/null +++ b/docs/templates/content.template.html @@ -0,0 +1,2 @@ +

{$ doc.title $}

+{$ doc.description | marked $} \ No newline at end of file diff --git a/npm-shrinkwrap.clean.json b/npm-shrinkwrap.clean.json index ac858e0cb5..84ad8ed0fa 100644 --- a/npm-shrinkwrap.clean.json +++ b/npm-shrinkwrap.clean.json @@ -3351,6 +3351,10 @@ "version": "0.1.7", "dev": true }, + "has-color": { + "version": "0.1.7", + "dev": true + }, "has-cors": { "version": "1.1.0", "dev": true @@ -3387,6 +3391,10 @@ "version": "2.1.4", "dev": true }, + "html": { + "version": "1.0.0", + "dev": true + }, "htmlparser2": { "version": "3.9.2", "dev": true, @@ -4439,6 +4447,28 @@ "version": "0.3.4", "dev": true }, + "nomnom": { + "version": "1.8.1", + "dev": true, + "dependencies": { + "ansi-styles": { + "version": "1.0.0", + "dev": true + }, + "chalk": { + "version": "0.4.0", + "dev": true + }, + "strip-ansi": { + "version": "0.1.1", + "dev": true + }, + "underscore": { + "version": "1.6.0", + "dev": true + } + } + }, "nopt": { "version": "3.0.6", "dev": true @@ -5042,6 +5072,10 @@ "version": "2.5.1", "dev": true }, + "rho": { + "version": "0.3.0", + "dev": true + }, "right-align": { "version": "0.1.3", "dev": true diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index f3457bf0fb..4be9d7997f 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2735,7 +2735,7 @@ }, "canonical-path": { "version": "0.0.2", - "from": "canonical-path@0.0.2", + "from": "canonical-path@>=0.0.2 <0.0.3", "resolved": "https://registry.npmjs.org/canonical-path/-/canonical-path-0.0.2.tgz", "dev": true }, @@ -3476,7 +3476,7 @@ }, "dgeni-packages": { "version": "0.16.5", - "from": "dgeni-packages@>=0.16.5 <0.17.0", + "from": "dgeni-packages@>=0.16.0 <0.17.0", "resolved": "https://registry.npmjs.org/dgeni-packages/-/dgeni-packages-0.16.5.tgz", "dev": true, "dependencies": { @@ -4853,6 +4853,12 @@ "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.7.tgz", "dev": true }, + "has-color": { + "version": "0.1.7", + "from": "has-color@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", + "dev": true + }, "has-cors": { "version": "1.1.0", "from": "has-cors@1.1.0", @@ -4907,6 +4913,12 @@ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.1.4.tgz", "dev": true }, + "html": { + "version": "1.0.0", + "from": "html@*", + "resolved": "https://registry.npmjs.org/html/-/html-1.0.0.tgz", + "dev": true + }, "htmlparser2": { "version": "3.9.2", "from": "htmlparser2@>=3.7.3 <4.0.0", @@ -6451,6 +6463,38 @@ "resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.3.4.tgz", "dev": true }, + "nomnom": { + "version": "1.8.1", + "from": "nomnom@*", + "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.8.1.tgz", + "dev": true, + "dependencies": { + "ansi-styles": { + "version": "1.0.0", + "from": "ansi-styles@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "dev": true + }, + "chalk": { + "version": "0.4.0", + "from": "chalk@>=0.4.0 <0.5.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "dev": true + }, + "strip-ansi": { + "version": "0.1.1", + "from": "strip-ansi@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "dev": true + }, + "underscore": { + "version": "1.6.0", + "from": "underscore@>=1.6.0 <1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", + "dev": true + } + } + }, "nopt": { "version": "3.0.6", "from": "nopt@>=2.0.0 <3.0.0||>=3.0.0 <4.0.0", @@ -7344,6 +7388,12 @@ "resolved": "https://registry.npmjs.org/rewire/-/rewire-2.5.1.tgz", "dev": true }, + "rho": { + "version": "0.3.0", + "from": "rho@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/rho/-/rho-0.3.0.tgz", + "dev": true + }, "right-align": { "version": "0.1.3", "from": "right-align@>=0.1.1 <0.2.0", diff --git a/package.json b/package.json index 8e0ae92a4f..8275099409 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "zone.js": "^0.7.2" }, "optionalDependencies": { - "fsevents": "^1.0.14" + "fsevents": "^1.0.14" }, "devDependencies": { "@types/angularjs": "^1.5.13-alpha", @@ -77,6 +77,7 @@ "protractor": "^4.0.11", "react": "^0.14.0", "rewire": "^2.3.3", + "rho": "^0.3.0", "rollup": "^0.26.3", "rollup-plugin-commonjs": "^5.0.5", "selenium-webdriver": "^2.53.3", @@ -95,4 +96,4 @@ "yargs": "^3.31.0", "yarn": "^0.19.1" } -} \ No newline at end of file +} diff --git a/tools/docs/angular.io-package/index.js b/tools/docs/angular.io-package/index.js index 75eafa3365..bcd694a531 100644 --- a/tools/docs/angular.io-package/index.js +++ b/tools/docs/angular.io-package/index.js @@ -17,6 +17,7 @@ const linksPackage = require('../links-package'); const examplesPackage = require('../examples-package'); const targetPackage = require('../target-package'); const cheatsheetPackage = require('../cheatsheet-package'); +const rhoPackage = require('../rho-package'); const PROJECT_ROOT = path.resolve(__dirname, '../../..'); const API_SOURCE_PATH = path.resolve(PROJECT_ROOT, 'modules'); @@ -28,7 +29,7 @@ module.exports = 'angular.io', [ jsdocPackage, nunjucksPackage, typescriptPackage, linksPackage, examplesPackage, - gitPackage, targetPackage, cheatsheetPackage + gitPackage, targetPackage, cheatsheetPackage, rhoPackage ]) // Register the processors @@ -81,14 +82,25 @@ module.exports = readFilesProcessor.basePath = PROJECT_ROOT; readFilesProcessor.sourceFiles = [ - {basePath: CONTENTS_PATH, include: CONTENTS_PATH + '/cheatsheet/*.md'}, { + { + basePath: CONTENTS_PATH, + include: CONTENTS_PATH + '/cookbook/**/*.md', + fileReader: 'contentFileReader' + }, + {basePath: CONTENTS_PATH, include: CONTENTS_PATH + '/cheatsheet/*.md'}, + { basePath: API_SOURCE_PATH, include: API_SOURCE_PATH + '/@angular/examples/**/*', fileReader: 'exampleFileReader' - } + }, + { + basePath: CONTENTS_PATH, + include: CONTENTS_PATH + '/examples/**/*', + fileReader: 'exampleFileReader' + }, ]; - collectExamples.exampleFolders = ['@angular/examples']; + collectExamples.exampleFolders = ['@angular/examples', 'examples']; generateKeywordsProcessor.ignoreWordsFile = 'tools/docs/angular.io-package/ignore.words'; generateKeywordsProcessor.docTypesToIgnore = ['example-region']; @@ -113,7 +125,7 @@ module.exports = // Configure jsdoc-style tag parsing - .config(function(parseTagsProcessor, getInjectables) { + .config(function(parseTagsProcessor, getInjectables, inlineTagProcessor) { // Load up all the tag definitions in the tag-defs folder parseTagsProcessor.tagDefinitions = parseTagsProcessor.tagDefinitions.concat(getInjectables(requireFolder('./tag-defs'))); @@ -127,6 +139,8 @@ module.exports = tagDef.transforms = []; } }); + + inlineTagProcessor.inlineTagDefinitions.push(require('./inline-tag-defs/anchor')); }) @@ -212,7 +226,8 @@ module.exports = pathTemplate: GUIDE_SEGMENT + '/cheatsheet.json', outputPathTemplate: '${path}' }, - {docTypes: ['example-region'], getOutputPath: function() {}} + {docTypes: ['example-region'], getOutputPath: function() {}}, + {docTypes: ['content'], pathTemplate: '${id}', outputPathTemplate: '${path}.html'} ]; }); diff --git a/tools/docs/angular.io-package/inline-tag-defs/anchor.js b/tools/docs/angular.io-package/inline-tag-defs/anchor.js new file mode 100644 index 0000000000..0ab6c7ea67 --- /dev/null +++ b/tools/docs/angular.io-package/inline-tag-defs/anchor.js @@ -0,0 +1,7 @@ +module.exports = { + name: 'a', + description: 'A shorthand for creating heading anchors. Usage: `{@a some-id}`', + handler: function(doc, tagName, tagDescription, docs) { + return ''; + } +}; diff --git a/tools/docs/content-package/index.js b/tools/docs/content-package/index.js index c59a98c5e1..8909435500 100644 --- a/tools/docs/content-package/index.js +++ b/tools/docs/content-package/index.js @@ -15,6 +15,11 @@ module.exports = new Package('content', [jsdocPackage, linksPackage]) readFilesProcessor.fileReaders.push(contentFileReader); }) + .config(function(parseTagsProcessor, getInjectables) { + parseTagsProcessor.tagDefinitions = parseTagsProcessor.tagDefinitions.concat( + getInjectables(requireFolder('./tag-defs'))); + }) + // Configure ids and paths .config(function(computeIdsProcessor, computePathsProcessor) { @@ -33,3 +38,10 @@ module.exports = new Package('content', [jsdocPackage, linksPackage]) getAliases: function(doc) { return [doc.id]; } }); }); + +function requireFolder(folderPath) { + const absolutePath = path.resolve(__dirname, folderPath); + return fs.readdirSync(absolutePath) + .filter(p => !/[._]spec\.js$/.test(p)) // ignore spec files + .map(p => require(path.resolve(absolutePath, p))); +} \ No newline at end of file diff --git a/tools/docs/content-package/readers/content.js b/tools/docs/content-package/readers/content.js index 92aac21c68..8ddd61f377 100644 --- a/tools/docs/content-package/readers/content.js +++ b/tools/docs/content-package/readers/content.js @@ -20,7 +20,7 @@ module.exports = function contentFileReader() { getDocs: function(fileInfo) { // We return a single element array because content files only contain one document - return [{docType: 'guide', content: fileInfo.content, startingLine: 1}]; + return [{docType: 'content', content: fileInfo.content, startingLine: 1}]; } }; }; \ No newline at end of file diff --git a/tools/docs/content-package/readers/content.spec.js b/tools/docs/content-package/readers/content.spec.js index 53d8fc856d..5db4091cf3 100644 --- a/tools/docs/content-package/readers/content.spec.js +++ b/tools/docs/content-package/readers/content.spec.js @@ -37,7 +37,7 @@ describe('contentFileReader', function() { 'project/path/modules/someModule/foo/docs/subfolder/bar.ngdoc', 'A load of content', 'project/path'); expect(fileReader.getDocs(fileInfo)).toEqual([ - {docType: 'guide', content: 'A load of content', startingLine: 1} + {docType: 'content', content: 'A load of content', startingLine: 1} ]); }); }); diff --git a/tools/docs/content-package/tag-defs/intro.js b/tools/docs/content-package/tag-defs/intro.js new file mode 100644 index 0000000000..a4276442b3 --- /dev/null +++ b/tools/docs/content-package/tag-defs/intro.js @@ -0,0 +1,3 @@ +module.exports = function() { + return {name: 'intro'}; +}; diff --git a/tools/docs/content-package/tag-defs/title.js b/tools/docs/content-package/tag-defs/title.js new file mode 100644 index 0000000000..376f2087dc --- /dev/null +++ b/tools/docs/content-package/tag-defs/title.js @@ -0,0 +1,3 @@ +module.exports = function() { + return {name: 'title'}; +}; diff --git a/tools/docs/examples-package/inline-tag-defs/example.js b/tools/docs/examples-package/inline-tag-defs/example.js index a86f0d744c..162d67afaa 100644 --- a/tools/docs/examples-package/inline-tag-defs/example.js +++ b/tools/docs/examples-package/inline-tag-defs/example.js @@ -22,22 +22,27 @@ module.exports = function exampleInlineTagDef( handler: function(doc, tagName, tagDescription) { - const EXAMPLES_FOLDER = collectExamples.exampleFolders[0]; + const EXAMPLES_FOLDERS = collectExamples.exampleFolders; var tagArgs = parseArgString(entities.decodeHTML(tagDescription)); - var unnamedArgs = tagArgs._; var relativePath = unnamedArgs[0]; - var regionName = tagArgs.region || (unnamedArgs.length > 1 ? unnamedArgs[1] : null); + var regionName = tagArgs.region || (unnamedArgs.length > 1 ? unnamedArgs[1] : ''); 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]; + // Find the example in the folders + var exampleFile; + EXAMPLES_FOLDERS.some( + EXAMPLES_FOLDER => { return 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')); + 'Example files available are:', + EXAMPLES_FOLDERS.map( + EXAMPLES_FOLDER => Object.keys(exampleMap[EXAMPLES_FOLDER]).join('\n'))); return ''; } diff --git a/tools/docs/examples-package/services/region-parser.js b/tools/docs/examples-package/services/region-parser.js index 69d98e4919..19556bcc0d 100644 --- a/tools/docs/examples-package/services/region-parser.js +++ b/tools/docs/examples-package/services/region-parser.js @@ -42,19 +42,24 @@ function regionParserImpl(contents, fileType) { // 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}; + const regionNames = getRegionNames(startRegion[1]); + if (regionNames.length === 0) { + regionNames.push(''); } - openRegions.push(regionName); + regionNames.forEach(regionName => { + 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) { @@ -62,14 +67,20 @@ function regionParserImpl(contents, fileType) { 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); + const regionNames = getRegionNames(endRegion[1]); + if (regionNames.length === 0) { + regionNames.push(openRegions[openRegions.length - 1]); } - region.open = false; - removeLast(openRegions, regionName); + + regionNames.forEach(regionName => { + 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) { @@ -94,8 +105,8 @@ function regionParserImpl(contents, fileType) { } } -function getRegionName(input) { - return input.trim(); +function getRegionNames(input) { + return input.split(',').map(name => name.trim()).filter(name => name.length > 0); } function removeLast(array, item) { diff --git a/tools/docs/examples-package/services/region-parser.spec.js b/tools/docs/examples-package/services/region-parser.spec.js index 21bd5be351..c8c9a62db1 100644 --- a/tools/docs/examples-package/services/region-parser.spec.js +++ b/tools/docs/examples-package/services/region-parser.spec.js @@ -70,6 +70,11 @@ describe('regionParser service', () => { expect(output.regions['Y']).toEqual(t('ghi')); }); + it('should open a region with a null name if there is no region name', () => { + const output = regionParser(t('/* #docregion */', 'abc', '/* #enddocregion */'), 'test-type'); + expect(output.regions['']).toEqual('abc'); + }); + 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', @@ -136,6 +141,16 @@ describe('regionParser service', () => { expect(output.regions['']).toEqual(t('abc', '/* . . . */', 'ghi')); expect(output.regions['A']).toEqual(t('jkl', '/* ... elided ... */', 'pqr')); }); + + it('should parse multiple region names separated by commas', () => { + const output = regionParser( + t('/* #docregion , A, B */', 'abc', '/* #enddocregion B */', '/* #docregion C */', 'xyz', + '/* #enddocregion A, C, */'), + 'test-type'); + expect(output.regions['A']).toEqual(t('abc', 'xyz')); + expect(output.regions['B']).toEqual(t('abc')); + expect(output.regions['C']).toEqual(t('xyz')); + }) }); function t() { diff --git a/tools/docs/rho-package/index.js b/tools/docs/rho-package/index.js new file mode 100644 index 0000000000..9726720bf6 --- /dev/null +++ b/tools/docs/rho-package/index.js @@ -0,0 +1,9 @@ +var Package = require('dgeni').Package; + +/** + * @dgPackage rho + * @description Overrides the renderMarkdown service with an implementation based on Rho + */ +module.exports = new Package('rho', ['nunjucks']) + + .factory(require('./services/renderMarkdown')); diff --git a/tools/docs/rho-package/services/renderMarkdown.js b/tools/docs/rho-package/services/renderMarkdown.js new file mode 100644 index 0000000000..bcc858e87c --- /dev/null +++ b/tools/docs/rho-package/services/renderMarkdown.js @@ -0,0 +1,55 @@ +var rho = require('rho'); + +/** + * @dgService renderMarkdown + * @description + * Render the markdown in the given string as HTML. + */ +module.exports = function renderMarkdown() { + + + // TODO(petebd): We might want to remove the leading whitespace from the code + // block before it gets to the markdown code render function + + // We need to teach Rho about inline tags so that it doesn't try to process + // the inside of the tag + const emitNormal = rho.InlineCompiler.prototype.emitNormal; + rho.InlineCompiler.prototype.emitNormal = function(walk) { + if (this.emitText(walk)) return; + if (tryDgeniInlineTag(this, walk)) return; + emitNormal.call(this, walk); + }; + + rho.BlockCompiler.prototype.emitBlock = function(walk) { + walk.skipBlankLines(); + this.countBlockIndent(walk); + if (this.tryUnorderedList(walk)) return; + if (this.tryOrderedList(walk)) return; + if (this.tryDefinitionList(walk)) return; + if (this.tryHeading(walk)) return; + if (this.tryCodeBlock(walk)) return; + if (this.tryDiv(walk)) return; + if (this.tryHtml(walk)) return; + if (tryDgeniInlineTag(this, walk, true)) return; + if (this.tryHrTable(walk)) return; + this.emitParagraph(walk); + }; + + function tryDgeniInlineTag(compiler, walk, isBlock) { + if (!walk.at('{@')) return false; + + const startIdx = walk.position; + var endIdx = walk.indexOf('}'); + + if (endIdx === null) return false; + + if (isBlock) compiler.out.push('
'); + compiler.out.push(walk.substring(startIdx, endIdx + 1)); + if (isBlock) compiler.out.push('
'); + + walk.startFrom(endIdx + 2); + return true; + }; + + return function renderMarkdownImpl(content) { return rho.toHtml(content, true); }; +}; \ No newline at end of file diff --git a/tools/docs/rho-package/services/renderMarkdown.spec.js b/tools/docs/rho-package/services/renderMarkdown.spec.js new file mode 100644 index 0000000000..9790ffab1b --- /dev/null +++ b/tools/docs/rho-package/services/renderMarkdown.spec.js @@ -0,0 +1,41 @@ +const renderMarkdownFactory = require('./renderMarkdown'); +const renderMarkdown = renderMarkdownFactory(); + +describe('rho: renderMarkdown service', () => { + it('should convert markdown to HTML', () => { + const content = '# heading 1\n' + + '\n' + + 'A paragraph with *bold* and _italic_.\n' + + '\n' + + '* List item 1\n' + + '* List item 2'; + const output = renderMarkdown(content); + + expect(output).toEqual( + '

heading 1

\n' + + '

A paragraph with bold and italic.

\n' + + '\n'); + }); + + it('should not process markdown inside inline tags', () => { + const content = '# heading {@link some_url_path}'; + const output = renderMarkdown(content); + expect(output).toEqual('

heading {@link some_url_path}

\n'); + }); + + it('should not put block level inline tags inside paragraphs', () => { + const content = 'A paragraph.\n' + + '\n' + + '{@example blah **blah** blah }\n' + + '\n' + + 'Another paragraph'; + const output = renderMarkdown(content); + expect(output).toEqual( + '

A paragraph.

\n' + + '
{@example blah **blah** blah }
\n' + + '

Another paragraph

\n'); + }); +});