Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Commit

Permalink
chore(docs): generate header ids for better linking
Browse files Browse the repository at this point in the history
- generate ids for all headers
- collect defined anchors
- check broken links (even if the page exists, but the anchor/id does not)
  • Loading branch information
vojtajina authored and IgorMinar committed Oct 18, 2013
1 parent c22adbf commit e8cc85f
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 62 deletions.
65 changes: 62 additions & 3 deletions docs/spec/domSpec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
var DOM = require('../src/dom.js').DOM;
var normalizeHeaderToId = require('../src/dom.js').normalizeHeaderToId;

describe('dom', function() {
var dom;
Expand All @@ -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() {
Expand All @@ -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() {
Expand All @@ -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&#39;s-jqlite')).toBe('angulars-jqlite');
});
});

});
52 changes: 28 additions & 24 deletions docs/spec/ngdocSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'})];
});

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:');
});
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 say which link doesn\'t exist', function() {
ngdoc.merge(docs);
expect(console.log.argsForCall[0][0]).toContain('non-existing-link');
});
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 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');
});
});

Expand Down Expand Up @@ -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>' +
Expand All @@ -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');
});
});
Expand Down
73 changes: 56 additions & 17 deletions docs/src/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

exports.DOM = DOM;
exports.htmlEscape = htmlEscape;
exports.normalizeHeaderToId = normalizeHeaderToId;

//////////////////////////////////////////////////////////

Expand All @@ -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 = {
Expand All @@ -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) {
Expand Down Expand Up @@ -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};
}
Expand Down
2 changes: 2 additions & 0 deletions docs/src/gen-docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit e8cc85f

Please sign in to comment.