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

Commit e8cc85f

Browse files
vojtajinaIgorMinar
authored andcommitted
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)
1 parent c22adbf commit e8cc85f

File tree

5 files changed

+193
-62
lines changed

5 files changed

+193
-62
lines changed

docs/spec/domSpec.js

+62-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
var DOM = require('../src/dom.js').DOM;
2+
var normalizeHeaderToId = require('../src/dom.js').normalizeHeaderToId;
23

34
describe('dom', function() {
45
var dom;
@@ -7,6 +8,31 @@ describe('dom', function() {
78
dom = new DOM();
89
});
910

11+
describe('html', function() {
12+
it('should add ids to all h tags', function() {
13+
dom.html('<h1>Some Header</h1>');
14+
expect(dom.toString()).toContain('<h1 id="some-header">Some Header</h1>');
15+
});
16+
17+
it('should collect <a name> anchors too', function() {
18+
dom.html('<h2>Xxx <a name="foo"></a> and bar <a name="bar"></a>');
19+
expect(dom.anchors).toContain('foo');
20+
expect(dom.anchors).toContain('bar');
21+
})
22+
});
23+
24+
it('should collect h tag ids', function() {
25+
dom.h('Page Title', function() {
26+
dom.html('<h1>Second</h1>xxx <h2>Third</h2>');
27+
dom.h('Another Header', function() {});
28+
});
29+
30+
expect(dom.anchors).toContain('page-title');
31+
expect(dom.anchors).toContain('second');
32+
expect(dom.anchors).toContain('second_third');
33+
expect(dom.anchors).toContain('another-header');
34+
});
35+
1036
describe('h', function() {
1137

1238
it('should render using function', function() {
@@ -25,7 +51,7 @@ describe('dom', function() {
2551
this.html('<h1>sub-heading</h1>');
2652
});
2753
expect(dom.toString()).toContain('<h1 id="heading">heading</h1>');
28-
expect(dom.toString()).toContain('<h2>sub-heading</h2>');
54+
expect(dom.toString()).toContain('<h2 id="sub-heading">sub-heading</h2>');
2955
});
3056

3157
it('should properly number nested headings', function() {
@@ -40,12 +66,45 @@ describe('dom', function() {
4066

4167
expect(dom.toString()).toContain('<h1 id="heading">heading</h1>');
4268
expect(dom.toString()).toContain('<h2 id="heading2">heading2</h2>');
43-
expect(dom.toString()).toContain('<h3>heading3</h3>');
69+
expect(dom.toString()).toContain('<h3 id="heading2_heading3">heading3</h3>');
4470

4571
expect(dom.toString()).toContain('<h1 id="other1">other1</h1>');
46-
expect(dom.toString()).toContain('<h2>other2</h2>');
72+
expect(dom.toString()).toContain('<h2 id="other2">other2</h2>');
73+
});
74+
75+
76+
it('should add nested ids to all h tags', function() {
77+
dom.h('Page Title', function() {
78+
dom.h('Second', function() {
79+
dom.html('some <h1>Third</h1>');
80+
});
81+
});
82+
83+
var resultingHtml = dom.toString();
84+
expect(resultingHtml).toContain('<h1 id="page-title">Page Title</h1>');
85+
expect(resultingHtml).toContain('<h2 id="second">Second</h2>');
86+
expect(resultingHtml).toContain('<h3 id="second_third">Third</h3>');
87+
});
88+
89+
});
90+
91+
92+
describe('normalizeHeaderToId', function() {
93+
it('should ignore content in the parenthesis', function() {
94+
expect(normalizeHeaderToId('One (more)')).toBe('one');
95+
});
96+
97+
it('should ignore html content', function() {
98+
expect(normalizeHeaderToId('Section <a name="section"></a>')).toBe('section');
4799
});
48100

101+
it('should ignore special characters', function() {
102+
expect(normalizeHeaderToId('Section \'!?')).toBe('section');
103+
});
104+
105+
it('should ignore html entities', function() {
106+
expect(normalizeHeaderToId('angular&#39;s-jqlite')).toBe('angulars-jqlite');
107+
});
49108
});
50109

51110
});

docs/spec/ngdocSpec.js

+28-24
Original file line numberDiff line numberDiff line change
@@ -262,33 +262,37 @@ describe('ngdoc', function() {
262262
expect(docs[0].events).toEqual([eventA, eventB]);
263263
expect(docs[0].properties).toEqual([propA, propB]);
264264
});
265+
});
265266

266267

268+
describe('checkBrokenLinks', function() {
269+
var docs;
267270

268-
describe('links checking', function() {
269-
var docs;
270-
beforeEach(function() {
271-
spyOn(console, 'log');
272-
docs = [new Doc({section: 'api', id: 'fake.id1', links: ['non-existing-link']}),
273-
new Doc({section: 'api', id: 'fake.id2'}),
274-
new Doc({section: 'api', id: 'fake.id3'})];
275-
});
276-
277-
it('should log warning when any link doesn\'t exist', function() {
278-
ngdoc.merge(docs);
279-
expect(console.log).toHaveBeenCalled();
280-
expect(console.log.argsForCall[0][0]).toContain('WARNING:');
281-
});
271+
beforeEach(function() {
272+
spyOn(console, 'log');
273+
docs = [new Doc({section: 'api', id: 'fake.id1', anchors: ['one']}),
274+
new Doc({section: 'api', id: 'fake.id2'}),
275+
new Doc({section: 'api', id: 'fake.id3'})];
276+
});
282277

283-
it('should say which link doesn\'t exist', function() {
284-
ngdoc.merge(docs);
285-
expect(console.log.argsForCall[0][0]).toContain('non-existing-link');
286-
});
278+
it('should log warning when a linked page does not exist', function() {
279+
docs.push(new Doc({section: 'api', id: 'with-broken.link', links: ['non-existing-link']}))
280+
ngdoc.checkBrokenLinks(docs);
281+
expect(console.log).toHaveBeenCalled();
282+
var warningMsg = console.log.argsForCall[0][0]
283+
expect(warningMsg).toContain('WARNING:');
284+
expect(warningMsg).toContain('non-existing-link');
285+
expect(warningMsg).toContain('api/with-broken.link');
286+
});
287287

288-
it('should say where is the non-existing link', function() {
289-
ngdoc.merge(docs);
290-
expect(console.log.argsForCall[0][0]).toContain('api/fake.id1');
291-
});
288+
it('should log warning when a linked anchor does not exist', function() {
289+
docs.push(new Doc({section: 'api', id: 'with-broken.link', links: ['api/fake.id1#non-existing']}))
290+
ngdoc.checkBrokenLinks(docs);
291+
expect(console.log).toHaveBeenCalled();
292+
var warningMsg = console.log.argsForCall[0][0]
293+
expect(warningMsg).toContain('WARNING:');
294+
expect(warningMsg).toContain('non-existing');
295+
expect(warningMsg).toContain('api/with-broken.link');
292296
});
293297
});
294298

@@ -524,7 +528,7 @@ describe('ngdoc', function() {
524528
doc.ngdoc = 'filter';
525529
doc.parse();
526530
expect(doc.html()).toContain(
527-
'<h3 id="Animations">Animations</h3>\n' +
531+
'<h3 id="usage_animations">Animations</h3>\n' +
528532
'<div class="animations">' +
529533
'<ul>' +
530534
'<li>enter - Add text</li>' +
@@ -541,7 +545,7 @@ describe('ngdoc', function() {
541545
var doc = new Doc('@ngdoc overview\n@name angular\n@description\n#heading\ntext');
542546
doc.parse();
543547
expect(doc.html()).toContain('text');
544-
expect(doc.html()).toContain('<h2>heading</h2>');
548+
expect(doc.html()).toContain('<h2 id="heading">heading</h2>');
545549
expect(doc.html()).not.toContain('Description');
546550
});
547551
});

docs/src/dom.js

+56-17
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
exports.DOM = DOM;
66
exports.htmlEscape = htmlEscape;
7+
exports.normalizeHeaderToId = normalizeHeaderToId;
78

89
//////////////////////////////////////////////////////////
910

@@ -16,10 +17,36 @@ function htmlEscape(text){
1617
.replace(/\}\}/g, '<span>}}</span>');
1718
}
1819

20+
function nonEmpty(header) {
21+
return !!header;
22+
}
23+
24+
function idFromCurrentHeaders(headers) {
25+
if (headers.length === 1) return headers[0];
26+
// Do not include the first level title, as that's the title of the page.
27+
return headers.slice(1).filter(nonEmpty).join('_');
28+
}
29+
30+
function normalizeHeaderToId(header) {
31+
if (typeof header !== 'string') {
32+
return '';
33+
}
34+
35+
return header.toLowerCase()
36+
.replace(/<.*>/g, '') // html tags
37+
.replace(/[\!\?\:\.\']/g, '') // special characters
38+
.replace(/&#\d\d;/g, '') // html entities
39+
.replace(/\(.*\)/mg, '') // stuff in parenthesis
40+
.replace(/\s$/, '') // trailing spaces
41+
.replace(/\s+/g, '-'); // replace whitespaces with dashes
42+
}
43+
1944

2045
function DOM() {
2146
this.out = [];
2247
this.headingDepth = 0;
48+
this.currentHeaders = [];
49+
this.anchors = [];
2350
}
2451

2552
var INLINE_TAGS = {
@@ -44,17 +71,28 @@ DOM.prototype = {
4471
},
4572

4673
html: function(html) {
47-
if (html) {
48-
var headingDepth = this.headingDepth;
49-
for ( var i = 10; i > 0; --i) {
50-
html = html
51-
.replace(new RegExp('<h' + i + '(.*?)>([\\s\\S]+)<\/h' + i +'>', 'gm'), function(_, attrs, content){
52-
var tag = 'h' + (i + headingDepth);
53-
return '<' + tag + attrs + '>' + content + '</' + tag + '>';
54-
});
55-
}
56-
this.out.push(html);
57-
}
74+
if (!html) return;
75+
76+
var self = this;
77+
// rewrite header levels, add ids and collect the ids
78+
html = html.replace(/<h(\d)(.*?)>([\s\S]+?)<\/h\1>/gm, function(_, level, attrs, content) {
79+
level = parseInt(level, 10) + self.headingDepth; // change header level based on the context
80+
81+
self.currentHeaders[level - 1] = normalizeHeaderToId(content);
82+
self.currentHeaders.length = level;
83+
84+
var id = idFromCurrentHeaders(self.currentHeaders);
85+
self.anchors.push(id);
86+
return '<h' + level + attrs + ' id="' + id + '">' + content + '</h' + level + '>';
87+
});
88+
89+
// collect anchors
90+
html = html.replace(/<a name="(\w*)">/g, function(match, anchor) {
91+
self.anchors.push(anchor);
92+
return match;
93+
});
94+
95+
this.out.push(html);
5896
},
5997

6098
tag: function(name, attr, text) {
@@ -85,17 +123,18 @@ DOM.prototype = {
85123

86124
h: function(heading, content, fn){
87125
if (content==undefined || (content instanceof Array && content.length == 0)) return;
126+
88127
this.headingDepth++;
128+
this.currentHeaders[this.headingDepth - 1] = normalizeHeaderToId(heading);
129+
this.currentHeaders.length = this.headingDepth;
130+
89131
var className = null,
90132
anchor = null;
91133
if (typeof heading == 'string') {
92-
var id = heading.
93-
replace(/\(.*\)/mg, '').
94-
replace(/[^\d\w\$]/mg, '.').
95-
replace(/-+/gm, '-').
96-
replace(/-*$/gm, '');
134+
var id = idFromCurrentHeaders(this.currentHeaders);
135+
this.anchors.push(id);
97136
anchor = {'id': id};
98-
var classNameValue = id.toLowerCase().replace(/[._]/mg, '-');
137+
var classNameValue = this.currentHeaders[this.headingDepth - 1]
99138
if(classNameValue == 'hide') classNameValue = '';
100139
className = {'class': classNameValue};
101140
}

docs/src/gen-docs.js

+2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ writer.makeDir('build/docs/', true).then(function() {
5555
fileFutures.push(writer.output('partials/' + doc.section + '/' + id + '.html', doc.html()));
5656
});
5757

58+
ngdoc.checkBrokenLinks(docs);
59+
5860
writeTheRest(fileFutures);
5961

6062
return Q.deep(fileFutures);

0 commit comments

Comments
 (0)