build(aio): support guide authoring

This commit implements various tags, inline tags and changes the markdown
renderer to use Rho. This enables us to generate guide type documents.
This commit is contained in:
Peter Bacon Darwin 2017-02-02 08:19:20 +00:00 committed by Igor Minar
parent 470997ebb9
commit 12f03b90fd
18 changed files with 315 additions and 44 deletions

View File

@ -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}
<md-tab-group>
<md-tab label="app/some.component.ts">
{@example 'cb-component-relative-paths/ts/app/some.component.ts'}
</md-tab>
<md-tab label="app/some.component.html">
{@example 'cb-component-relative-paths/ts/app/some.component.html'}
</md-tab>
<md-tab label="app/some.component.css">
{@example 'cb-component-relative-paths/ts/app/some.component.css'}
</md-tab>
<md-tab label="app/app.component.ts">
{@example 'cb-component-relative-paths/ts/app/app.component.ts'}
</md-tab>
<md-tab-group>
{@a why-default}

2
docs/templates/content.template.html vendored Normal file
View File

@ -0,0 +1,2 @@
<h1>{$ doc.title $}</h1>
{$ doc.description | marked $}

View File

@ -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

54
npm-shrinkwrap.json generated
View File

@ -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",

View File

@ -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"
}
}
}

View File

@ -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'}
];
});

View File

@ -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 '<a id="' + tagDescription.trim() + '"></a>';
}
};

View File

@ -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)));
}

View File

@ -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}];
}
};
};

View File

@ -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}
]);
});
});

View File

@ -0,0 +1,3 @@
module.exports = function() {
return {name: 'intro'};
};

View File

@ -0,0 +1,3 @@
module.exports = function() {
return {name: 'title'};
};

View File

@ -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 '';
}

View File

@ -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) {

View File

@ -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() {

View File

@ -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'));

View File

@ -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('<div>');
compiler.out.push(walk.substring(startIdx, endIdx + 1));
if (isBlock) compiler.out.push('</div>');
walk.startFrom(endIdx + 2);
return true;
};
return function renderMarkdownImpl(content) { return rho.toHtml(content, true); };
};

View File

@ -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(
'<h1>heading 1</h1>\n' +
'<p>A paragraph with <strong>bold</strong> and <em>italic</em>.</p>\n' +
'<ul>\n' +
' <li>List item 1</li>\n' +
' <li>List item 2</li>\n' +
'</ul>\n');
});
it('should not process markdown inside inline tags', () => {
const content = '# heading {@link some_url_path}';
const output = renderMarkdown(content);
expect(output).toEqual('<h1>heading {@link some_url_path}</h1>\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(
'<p>A paragraph.</p>\n' +
'<div>{@example blah **blah** blah }</div>\n' +
'<p>Another paragraph</p>\n');
});
});