diff options
| author | Vojta Jina | 2013-10-17 14:16:32 -0700 |
|---|---|---|
| committer | Igor Minar | 2013-10-18 15:35:41 -0700 |
| commit | e8cc85f733a49ca53e8cda5a96bbaacc9a20ac7e (patch) | |
| tree | f145db33b29ee9cce531492a8332adc537033b70 | |
| parent | c22adbf160f32c1839fbb35382b7a8c6bcec2927 (diff) | |
| download | angular.js-e8cc85f733a49ca53e8cda5a96bbaacc9a20ac7e.tar.bz2 | |
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)
| -rw-r--r-- | docs/spec/domSpec.js | 65 | ||||
| -rw-r--r-- | docs/spec/ngdocSpec.js | 52 | ||||
| -rw-r--r-- | docs/src/dom.js | 73 | ||||
| -rwxr-xr-x | docs/src/gen-docs.js | 2 | ||||
| -rw-r--r-- | docs/src/ngdoc.js | 63 |
5 files changed, 193 insertions, 62 deletions
diff --git a/docs/spec/domSpec.js b/docs/spec/domSpec.js index 7bc6a7f4..d10db9dc 100644 --- a/docs/spec/domSpec.js +++ b/docs/spec/domSpec.js @@ -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'); + }); }); }); diff --git a/docs/spec/ngdocSpec.js b/docs/spec/ngdocSpec.js index 607745d8..15fd0ba4 100644 --- a/docs/spec/ngdocSpec.js +++ b/docs/spec/ngdocSpec.js @@ -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'); }); }); @@ -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'); }); }); diff --git a/docs/src/dom.js b/docs/src/dom.js index 897a1831..e696faf4 100644 --- a/docs/src/dom.js +++ b/docs/src/dom.js @@ -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}; } diff --git a/docs/src/gen-docs.js b/docs/src/gen-docs.js index 61fd3b3a..ffbf0068 100755 --- a/docs/src/gen-docs.js +++ b/docs/src/gen-docs.js @@ -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); diff --git a/docs/src/ngdoc.js b/docs/src/ngdoc.js index 0f94f6eb..24d1aa26 100644 --- a/docs/src/ngdoc.js +++ b/docs/src/ngdoc.js @@ -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]; |
