mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-16 23:30:23 +00:00
chore(docs): generate header ids for better linking
- generate ids for all headers - collect defined anchors - check broken links (even if the page exists, but the anchor/id does not)
This commit is contained in:
parent
c22adbf160
commit
e8cc85f733
5 changed files with 193 additions and 62 deletions
|
|
@ -1,4 +1,5 @@
|
|||
var DOM = require('../src/dom.js').DOM;
|
||||
var normalizeHeaderToId = require('../src/dom.js').normalizeHeaderToId;
|
||||
|
||||
describe('dom', function() {
|
||||
var dom;
|
||||
|
|
@ -7,6 +8,31 @@ describe('dom', function() {
|
|||
dom = new DOM();
|
||||
});
|
||||
|
||||
describe('html', function() {
|
||||
it('should add ids to all h tags', function() {
|
||||
dom.html('<h1>Some Header</h1>');
|
||||
expect(dom.toString()).toContain('<h1 id="some-header">Some Header</h1>');
|
||||
});
|
||||
|
||||
it('should collect <a name> anchors too', function() {
|
||||
dom.html('<h2>Xxx <a name="foo"></a> and bar <a name="bar"></a>');
|
||||
expect(dom.anchors).toContain('foo');
|
||||
expect(dom.anchors).toContain('bar');
|
||||
})
|
||||
});
|
||||
|
||||
it('should collect h tag ids', function() {
|
||||
dom.h('Page Title', function() {
|
||||
dom.html('<h1>Second</h1>xxx <h2>Third</h2>');
|
||||
dom.h('Another Header', function() {});
|
||||
});
|
||||
|
||||
expect(dom.anchors).toContain('page-title');
|
||||
expect(dom.anchors).toContain('second');
|
||||
expect(dom.anchors).toContain('second_third');
|
||||
expect(dom.anchors).toContain('another-header');
|
||||
});
|
||||
|
||||
describe('h', function() {
|
||||
|
||||
it('should render using function', function() {
|
||||
|
|
@ -25,7 +51,7 @@ describe('dom', function() {
|
|||
this.html('<h1>sub-heading</h1>');
|
||||
});
|
||||
expect(dom.toString()).toContain('<h1 id="heading">heading</h1>');
|
||||
expect(dom.toString()).toContain('<h2>sub-heading</h2>');
|
||||
expect(dom.toString()).toContain('<h2 id="sub-heading">sub-heading</h2>');
|
||||
});
|
||||
|
||||
it('should properly number nested headings', function() {
|
||||
|
|
@ -40,12 +66,45 @@ describe('dom', function() {
|
|||
|
||||
expect(dom.toString()).toContain('<h1 id="heading">heading</h1>');
|
||||
expect(dom.toString()).toContain('<h2 id="heading2">heading2</h2>');
|
||||
expect(dom.toString()).toContain('<h3>heading3</h3>');
|
||||
expect(dom.toString()).toContain('<h3 id="heading2_heading3">heading3</h3>');
|
||||
|
||||
expect(dom.toString()).toContain('<h1 id="other1">other1</h1>');
|
||||
expect(dom.toString()).toContain('<h2>other2</h2>');
|
||||
expect(dom.toString()).toContain('<h2 id="other2">other2</h2>');
|
||||
});
|
||||
|
||||
|
||||
it('should add nested ids to all h tags', function() {
|
||||
dom.h('Page Title', function() {
|
||||
dom.h('Second', function() {
|
||||
dom.html('some <h1>Third</h1>');
|
||||
});
|
||||
});
|
||||
|
||||
var resultingHtml = dom.toString();
|
||||
expect(resultingHtml).toContain('<h1 id="page-title">Page Title</h1>');
|
||||
expect(resultingHtml).toContain('<h2 id="second">Second</h2>');
|
||||
expect(resultingHtml).toContain('<h3 id="second_third">Third</h3>');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('normalizeHeaderToId', function() {
|
||||
it('should ignore content in the parenthesis', function() {
|
||||
expect(normalizeHeaderToId('One (more)')).toBe('one');
|
||||
});
|
||||
|
||||
it('should ignore html content', function() {
|
||||
expect(normalizeHeaderToId('Section <a name="section"></a>')).toBe('section');
|
||||
});
|
||||
|
||||
it('should ignore special characters', function() {
|
||||
expect(normalizeHeaderToId('Section \'!?')).toBe('section');
|
||||
});
|
||||
|
||||
it('should ignore html entities', function() {
|
||||
expect(normalizeHeaderToId('angular's-jqlite')).toBe('angulars-jqlite');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -262,33 +262,37 @@ describe('ngdoc', function() {
|
|||
expect(docs[0].events).toEqual([eventA, eventB]);
|
||||
expect(docs[0].properties).toEqual([propA, propB]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('checkBrokenLinks', function() {
|
||||
var docs;
|
||||
|
||||
describe('links checking', function() {
|
||||
var docs;
|
||||
beforeEach(function() {
|
||||
spyOn(console, 'log');
|
||||
docs = [new Doc({section: 'api', id: 'fake.id1', links: ['non-existing-link']}),
|
||||
new Doc({section: 'api', id: 'fake.id2'}),
|
||||
new Doc({section: 'api', id: 'fake.id3'})];
|
||||
});
|
||||
beforeEach(function() {
|
||||
spyOn(console, 'log');
|
||||
docs = [new Doc({section: 'api', id: 'fake.id1', anchors: ['one']}),
|
||||
new Doc({section: 'api', id: 'fake.id2'}),
|
||||
new Doc({section: 'api', id: 'fake.id3'})];
|
||||
});
|
||||
|
||||
it('should log warning when any link doesn\'t exist', function() {
|
||||
ngdoc.merge(docs);
|
||||
expect(console.log).toHaveBeenCalled();
|
||||
expect(console.log.argsForCall[0][0]).toContain('WARNING:');
|
||||
});
|
||||
it('should log warning when a linked page does not exist', function() {
|
||||
docs.push(new Doc({section: 'api', id: 'with-broken.link', links: ['non-existing-link']}))
|
||||
ngdoc.checkBrokenLinks(docs);
|
||||
expect(console.log).toHaveBeenCalled();
|
||||
var warningMsg = console.log.argsForCall[0][0]
|
||||
expect(warningMsg).toContain('WARNING:');
|
||||
expect(warningMsg).toContain('non-existing-link');
|
||||
expect(warningMsg).toContain('api/with-broken.link');
|
||||
});
|
||||
|
||||
it('should say which link doesn\'t exist', function() {
|
||||
ngdoc.merge(docs);
|
||||
expect(console.log.argsForCall[0][0]).toContain('non-existing-link');
|
||||
});
|
||||
|
||||
it('should say where is the non-existing link', function() {
|
||||
ngdoc.merge(docs);
|
||||
expect(console.log.argsForCall[0][0]).toContain('api/fake.id1');
|
||||
});
|
||||
it('should log warning when a linked anchor does not exist', function() {
|
||||
docs.push(new Doc({section: 'api', id: 'with-broken.link', links: ['api/fake.id1#non-existing']}))
|
||||
ngdoc.checkBrokenLinks(docs);
|
||||
expect(console.log).toHaveBeenCalled();
|
||||
var warningMsg = console.log.argsForCall[0][0]
|
||||
expect(warningMsg).toContain('WARNING:');
|
||||
expect(warningMsg).toContain('non-existing');
|
||||
expect(warningMsg).toContain('api/with-broken.link');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -524,7 +528,7 @@ describe('ngdoc', function() {
|
|||
doc.ngdoc = 'filter';
|
||||
doc.parse();
|
||||
expect(doc.html()).toContain(
|
||||
'<h3 id="Animations">Animations</h3>\n' +
|
||||
'<h3 id="usage_animations">Animations</h3>\n' +
|
||||
'<div class="animations">' +
|
||||
'<ul>' +
|
||||
'<li>enter - Add text</li>' +
|
||||
|
|
@ -541,7 +545,7 @@ describe('ngdoc', function() {
|
|||
var doc = new Doc('@ngdoc overview\n@name angular\n@description\n#heading\ntext');
|
||||
doc.parse();
|
||||
expect(doc.html()).toContain('text');
|
||||
expect(doc.html()).toContain('<h2>heading</h2>');
|
||||
expect(doc.html()).toContain('<h2 id="heading">heading</h2>');
|
||||
expect(doc.html()).not.toContain('Description');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
exports.DOM = DOM;
|
||||
exports.htmlEscape = htmlEscape;
|
||||
exports.normalizeHeaderToId = normalizeHeaderToId;
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
|
||||
|
|
@ -16,10 +17,36 @@ function htmlEscape(text){
|
|||
.replace(/\}\}/g, '<span>}}</span>');
|
||||
}
|
||||
|
||||
function nonEmpty(header) {
|
||||
return !!header;
|
||||
}
|
||||
|
||||
function idFromCurrentHeaders(headers) {
|
||||
if (headers.length === 1) return headers[0];
|
||||
// Do not include the first level title, as that's the title of the page.
|
||||
return headers.slice(1).filter(nonEmpty).join('_');
|
||||
}
|
||||
|
||||
function normalizeHeaderToId(header) {
|
||||
if (typeof header !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return header.toLowerCase()
|
||||
.replace(/<.*>/g, '') // html tags
|
||||
.replace(/[\!\?\:\.\']/g, '') // special characters
|
||||
.replace(/&#\d\d;/g, '') // html entities
|
||||
.replace(/\(.*\)/mg, '') // stuff in parenthesis
|
||||
.replace(/\s$/, '') // trailing spaces
|
||||
.replace(/\s+/g, '-'); // replace whitespaces with dashes
|
||||
}
|
||||
|
||||
|
||||
function DOM() {
|
||||
this.out = [];
|
||||
this.headingDepth = 0;
|
||||
this.currentHeaders = [];
|
||||
this.anchors = [];
|
||||
}
|
||||
|
||||
var INLINE_TAGS = {
|
||||
|
|
@ -44,17 +71,28 @@ DOM.prototype = {
|
|||
},
|
||||
|
||||
html: function(html) {
|
||||
if (html) {
|
||||
var headingDepth = this.headingDepth;
|
||||
for ( var i = 10; i > 0; --i) {
|
||||
html = html
|
||||
.replace(new RegExp('<h' + i + '(.*?)>([\\s\\S]+)<\/h' + i +'>', 'gm'), function(_, attrs, content){
|
||||
var tag = 'h' + (i + headingDepth);
|
||||
return '<' + tag + attrs + '>' + content + '</' + tag + '>';
|
||||
});
|
||||
}
|
||||
this.out.push(html);
|
||||
}
|
||||
if (!html) return;
|
||||
|
||||
var self = this;
|
||||
// rewrite header levels, add ids and collect the ids
|
||||
html = html.replace(/<h(\d)(.*?)>([\s\S]+?)<\/h\1>/gm, function(_, level, attrs, content) {
|
||||
level = parseInt(level, 10) + self.headingDepth; // change header level based on the context
|
||||
|
||||
self.currentHeaders[level - 1] = normalizeHeaderToId(content);
|
||||
self.currentHeaders.length = level;
|
||||
|
||||
var id = idFromCurrentHeaders(self.currentHeaders);
|
||||
self.anchors.push(id);
|
||||
return '<h' + level + attrs + ' id="' + id + '">' + content + '</h' + level + '>';
|
||||
});
|
||||
|
||||
// collect anchors
|
||||
html = html.replace(/<a name="(\w*)">/g, function(match, anchor) {
|
||||
self.anchors.push(anchor);
|
||||
return match;
|
||||
});
|
||||
|
||||
this.out.push(html);
|
||||
},
|
||||
|
||||
tag: function(name, attr, text) {
|
||||
|
|
@ -85,17 +123,18 @@ DOM.prototype = {
|
|||
|
||||
h: function(heading, content, fn){
|
||||
if (content==undefined || (content instanceof Array && content.length == 0)) return;
|
||||
|
||||
this.headingDepth++;
|
||||
this.currentHeaders[this.headingDepth - 1] = normalizeHeaderToId(heading);
|
||||
this.currentHeaders.length = this.headingDepth;
|
||||
|
||||
var className = null,
|
||||
anchor = null;
|
||||
if (typeof heading == 'string') {
|
||||
var id = heading.
|
||||
replace(/\(.*\)/mg, '').
|
||||
replace(/[^\d\w\$]/mg, '.').
|
||||
replace(/-+/gm, '-').
|
||||
replace(/-*$/gm, '');
|
||||
var id = idFromCurrentHeaders(this.currentHeaders);
|
||||
this.anchors.push(id);
|
||||
anchor = {'id': id};
|
||||
var classNameValue = id.toLowerCase().replace(/[._]/mg, '-');
|
||||
var classNameValue = this.currentHeaders[this.headingDepth - 1]
|
||||
if(classNameValue == 'hide') classNameValue = '';
|
||||
className = {'class': classNameValue};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ writer.makeDir('build/docs/', true).then(function() {
|
|||
fileFutures.push(writer.output('partials/' + doc.section + '/' + id + '.html', doc.html()));
|
||||
});
|
||||
|
||||
ngdoc.checkBrokenLinks(docs);
|
||||
|
||||
writeTheRest(fileFutures);
|
||||
|
||||
return Q.deep(fileFutures);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ exports.trim = trim;
|
|||
exports.metadata = metadata;
|
||||
exports.scenarios = scenarios;
|
||||
exports.merge = merge;
|
||||
exports.checkBrokenLinks = checkBrokenLinks;
|
||||
exports.Doc = Doc;
|
||||
|
||||
exports.ngVersions = function() {
|
||||
|
|
@ -169,6 +170,7 @@ function Doc(text, file, line) {
|
|||
this.methods = this.methods || [];
|
||||
this.events = this.events || [];
|
||||
this.links = this.links || [];
|
||||
this.anchors = this.anchors || [];
|
||||
}
|
||||
Doc.METADATA_IGNORE = (function() {
|
||||
var words = fs.readFileSync(__dirname + '/ignore.words', 'utf8');
|
||||
|
|
@ -242,6 +244,14 @@ Doc.prototype = {
|
|||
* @returns {string} Absolute url
|
||||
*/
|
||||
convertUrlToAbsolute: function(url) {
|
||||
var hashIdx = url.indexOf('#');
|
||||
|
||||
// Lowercase hash parts of the links,
|
||||
// so that we can keep correct API names even when the urls are lowercased.
|
||||
if (hashIdx !== -1) {
|
||||
url = url.substr(0, hashIdx) + url.substr(hashIdx).toLowerCase();
|
||||
}
|
||||
|
||||
if (url.substr(-1) == '/') return url + 'index';
|
||||
if (url.match(/\//)) return url;
|
||||
return this.section + '/' + url;
|
||||
|
|
@ -569,6 +579,8 @@ Doc.prototype = {
|
|||
dom.h('Example', self.example, dom.html);
|
||||
});
|
||||
|
||||
self.anchors = dom.anchors;
|
||||
|
||||
return dom.toString();
|
||||
|
||||
//////////////////////////
|
||||
|
|
@ -606,7 +618,7 @@ Doc.prototype = {
|
|||
dom.html('<a href="api/ngAnimate.$animate">Click here</a> to learn more about the steps involved in the animation.');
|
||||
}
|
||||
if(params.length > 0) {
|
||||
dom.html('<h2 id="parameters">Parameters</h2>');
|
||||
dom.html('<h2>Parameters</h2>');
|
||||
dom.html('<table class="variables-matrix table table-bordered table-striped">');
|
||||
dom.html('<thead>');
|
||||
dom.html('<tr>');
|
||||
|
|
@ -660,7 +672,7 @@ Doc.prototype = {
|
|||
html_usage_returns: function(dom) {
|
||||
var self = this;
|
||||
if (self.returns) {
|
||||
dom.html('<h2 id="returns">Returns</h2>');
|
||||
dom.html('<h2>Returns</h2>');
|
||||
dom.html('<table class="variables-matrix">');
|
||||
dom.html('<tr>');
|
||||
dom.html('<td>');
|
||||
|
|
@ -1211,22 +1223,7 @@ function merge(docs){
|
|||
});
|
||||
|
||||
for(var i = 0; i < docs.length;) {
|
||||
var doc = docs[i];
|
||||
|
||||
// check links - do they exist ?
|
||||
doc.links.forEach(function(link) {
|
||||
// convert #id to path#id
|
||||
if (link[0] == '#') {
|
||||
link = doc.section + '/' + doc.id.split('#').shift() + link;
|
||||
}
|
||||
link = link.split('#').shift();
|
||||
if (!byFullId[link]) {
|
||||
console.log('WARNING: In ' + doc.section + '/' + doc.id + ', non existing link: "' + link + '"');
|
||||
}
|
||||
});
|
||||
|
||||
// merge into parents
|
||||
if (findParent(doc, 'method') || findParent(doc, 'property') || findParent(doc, 'event')) {
|
||||
if (findParent(docs[i], 'method') || findParent(docs[i], 'property') || findParent(docs[i], 'event')) {
|
||||
docs.splice(i, 1);
|
||||
} else {
|
||||
i++;
|
||||
|
|
@ -1255,6 +1252,36 @@ function merge(docs){
|
|||
}
|
||||
//////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
function checkBrokenLinks(docs) {
|
||||
var byFullId = Object.create(null);
|
||||
|
||||
docs.forEach(function(doc) {
|
||||
byFullId[doc.section + '/' + doc.id] = doc;
|
||||
});
|
||||
|
||||
docs.forEach(function(doc) {
|
||||
doc.links.forEach(function(link) {
|
||||
// convert #id to path#id
|
||||
if (link[0] == '#') {
|
||||
link = doc.section + '/' + doc.id.split('#').shift() + link;
|
||||
}
|
||||
|
||||
var parts = link.split('#');
|
||||
var pageLink = parts[0];
|
||||
var anchorLink = parts[1];
|
||||
var linkedPage = byFullId[pageLink];
|
||||
|
||||
if (!linkedPage) {
|
||||
console.log('WARNING: ' + doc.section + '/' + doc.id + ' (defined in ' + doc.file + ') points to a non existing page "' + link + '"!');
|
||||
} else if (anchorLink && linkedPage.anchors.indexOf(anchorLink) === -1) {
|
||||
console.log('WARNING: ' + doc.section + '/' + doc.id + ' (defined in ' + doc.file + ') points to a non existing anchor "' + link + '"!');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function property(name) {
|
||||
return function(value){
|
||||
return value[name];
|
||||
|
|
|
|||
Loading…
Reference in a new issue