From 13f92de6246a0af8450fde84b209211a56397fda Mon Sep 17 00:00:00 2001 From: Vojta Jina Date: Tue, 30 Aug 2011 11:47:24 +0200 Subject: [PATCH] feat(docs): use html5 history api for all routing in the docs app - Configure our docs app to use new $location with html5 history api! - Update simple node web server to serve index.html for all links (rewritting). - Update .htaccess file to serve index.html for all links (rewritting). - At runtime determine the base href path and attach it to the DOM. We needed the absolute URL to get all browsers to work well. - Because of the above, we also need to dynamically determine all needed js/css resources and add them to the DOM. This was needed because FF6 would eagerly fetch resources with wrong URL since the base element is added to the dom at runtime. - All content html files were moved to the partials directory, because with the new html5 urls it was impossible to tell if request for http://domain/api/angular.filter.html was an html5 url for the html filter doc page, or an xhr/appcache request for the content html file for the html filter. f --- Rakefile | 29 +++++++-- docs/content/cookbook/deeplinking.ngdoc | 10 +-- docs/spec/ngdocSpec.js | 14 ++-- docs/src/appCache.js | 4 +- docs/src/gen-docs.js | 21 ++---- docs/src/ngdoc.js | 12 ++-- docs/src/templates/.htaccess | 6 +- docs/src/templates/docs.js | 11 ++-- docs/src/templates/index.html | 86 ++++++++++++++++++------- lib/nodeserver/server.js | 12 ++++ src/markups.js | 15 +++-- src/widgets.js | 10 +-- 12 files changed, 150 insertions(+), 80 deletions(-) diff --git a/Rakefile b/Rakefile index f7183c094a47..642ef56767cc 100644 --- a/Rakefile +++ b/Rakefile @@ -209,7 +209,8 @@ task :package => [:clean, :compile, :docs] do text = f.read f.truncate 0 f.rewind - f.write text.sub('angular.min.js', "angular-#{NG_VERSION.full}.min.js") + f.write text.sub('angular.min.js', "angular-#{NG_VERSION.full}.min.js"). + sub('/build/docs/', "/#{NG_VERSION.full}/docs-#{NG_VERSION.full}/") end @@ -217,10 +218,28 @@ task :package => [:clean, :compile, :docs] do text = f.read f.truncate 0 f.rewind - f.write text.sub('angular.min.js', "angular-#{NG_VERSION.full}.min.js") + f.write text.sub('angular.min.js', "angular-#{NG_VERSION.full}.min.js"). + sub('/build/docs/', "/#{NG_VERSION.full}/docs-#{NG_VERSION.full}/") end + File.open("#{pkg_dir}/docs-#{NG_VERSION.full}/index-debug.html", File::RDWR) do |f| + text = f.read + f.truncate 0 + f.rewind + f.write text.sub('../angular.js', "../angular-#{NG_VERSION.full}.js"). + sub('/build/docs/', "/#{NG_VERSION.full}/docs-#{NG_VERSION.full}/") + end + + + File.open("#{pkg_dir}/docs-#{NG_VERSION.full}/index-jq-debug.html", File::RDWR) do |f| + text = f.read + f.truncate 0 + f.rewind + f.write text.sub('../angular.js', "../angular-#{NG_VERSION.full}.js"). + sub('/build/docs/', "/#{NG_VERSION.full}/docs-#{NG_VERSION.full}/") + end + File.open("#{pkg_dir}/docs-#{NG_VERSION.full}/docs-scenario.html", File::RDWR) do |f| text = f.read f.truncate 0 @@ -232,14 +251,16 @@ task :package => [:clean, :compile, :docs] do text = f.read f.truncate 0 f.rewind - f.write text.sub('angular.min.js', "angular-#{NG_VERSION.full}.min.js") + f.write text.sub('angular.min.js', "angular-#{NG_VERSION.full}.min.js"). + sub('/build/docs/', "/#{NG_VERSION.full}/docs-#{NG_VERSION.full}/") end File.open("#{pkg_dir}/docs-#{NG_VERSION.full}/appcache-offline.manifest", File::RDWR) do |f| text = f.read f.truncate 0 f.rewind - f.write text.sub('angular.min.js', "angular-#{NG_VERSION.full}.min.js") + f.write text.sub('angular.min.js', "angular-#{NG_VERSION.full}.min.js"). + sub('/build/docs/', "/#{NG_VERSION.full}/docs-#{NG_VERSION.full}/") end diff --git a/docs/content/cookbook/deeplinking.ngdoc b/docs/content/cookbook/deeplinking.ngdoc index 10c80f21fbb1..6747b55f476e 100644 --- a/docs/content/cookbook/deeplinking.ngdoc +++ b/docs/content/cookbook/deeplinking.ngdoc @@ -28,14 +28,14 @@ controller. In this example we have a simple app which consist of two screens: -* Welcome: url `#` Show the user contact information. -* Settings: url `#/settings` Show an edit screen for user contact information. +* Welcome: url `welcome` Show the user contact information. +* Settings: url `settings` Show an edit screen for user contact information. The two partials are defined in the following URLs: -* {@link ./examples/settings.html} -* {@link ./examples/welcome.html} +* ./examples/settings.html +* ./examples/welcome.html @@ -79,7 +79,7 @@ The two partials are defined in the following URLs:

Your App Chrome

- [ Welcome | Settings ] + [ Welcome | Settings ]
Partial: {{$route.current.template}} diff --git a/docs/spec/ngdocSpec.js b/docs/spec/ngdocSpec.js index 44d8dc3c81bf..106fd22b47e8 100644 --- a/docs/spec/ngdocSpec.js +++ b/docs/spec/ngdocSpec.js @@ -327,8 +327,8 @@ describe('ngdoc', function(){ expect(doc.requires).toEqual([ {name:'$service', text:'

for \nA

'}, {name:'$another', text:'

for B

'}]); - expect(doc.html()).toContain('$service'); - expect(doc.html()).toContain('$another'); + expect(doc.html()).toContain('$service'); + expect(doc.html()).toContain('$another'); expect(doc.html()).toContain('

for \nA

'); expect(doc.html()).toContain('

for B

'); }); @@ -429,13 +429,13 @@ describe('ngdoc', function(){ doc.parse(); expect(doc.description). - toContain('foo angular.foo'); + toContain('foo angular.foo'); expect(doc.description). - toContain('da bar foo bar'); + toContain('da bar foo bar'); expect(doc.description). - toContain('dadangular.foo'); + toContain('dadangular.foo'); expect(doc.description). - toContain('ng:foo'); + toContain('ng:foo'); expect(doc.description). toContain('http://angularjs.org'); expect(doc.description). @@ -447,7 +447,7 @@ describe('ngdoc', function(){ '{@link\napi/angular.foo\na\nb}'); doc.parse(); expect(doc.description). - toContain('a b'); + toContain('a b'); }); }); diff --git a/docs/src/appCache.js b/docs/src/appCache.js index ed35eb792ab2..7b21624e5fb5 100644 --- a/docs/src/appCache.js +++ b/docs/src/appCache.js @@ -29,7 +29,7 @@ function appCache(path) { var resultPostfix = ["", "FALLBACK:", - "/offline.html", + "/ /build/docs/index.html", "", "# allow access to google analytics and twitter when we are online", "NETWORK:", @@ -68,7 +68,7 @@ function appCacheTemplate() { "img/yellow_bkgnd.jpg", "", "FALLBACK:", - "/ offline.html", + "/ /build/docs/offline.html", "", "# allow access to google analytics and twitter when we are online", "NETWORK:", diff --git a/docs/src/gen-docs.js b/docs/src/gen-docs.js index e1778bb7c18a..c7b370259a20 100755 --- a/docs/src/gen-docs.js +++ b/docs/src/gen-docs.js @@ -22,7 +22,7 @@ writer.makeDir('build/docs/syntaxhighlighter').then(function() { ngdoc.merge(docs); var fileFutures = []; docs.forEach(function(doc){ - fileFutures.push(writer.output(doc.section + '/' + doc.id + '.html', doc.html())); + fileFutures.push(writer.output('partials/' + doc.section + '/' + doc.id + '.html', doc.html())); }); writeTheRest(fileFutures); @@ -43,28 +43,19 @@ function writeTheRest(writesFuture) { writesFuture.push(writer.copyDir('img')); writesFuture.push(writer.copyDir('examples')); - var manifest = 'manifest="appcache.manifest"', - jq = '', - ngMin = '', - ng = ''; + var manifest = 'manifest="/build/docs/appcache.manifest"'; writesFuture.push(writer.copy('docs/src/templates/index.html', 'build/docs/index.html', - writer.replace, {'doc:manifest': manifest, - '': ngMin})); + writer.replace, {'doc:manifest': manifest})); writesFuture.push(writer.copy('docs/src/templates/index.html', 'build/docs/index-jq.html', - writer.replace, {'doc:manifest': manifest, - '': ngMin, - '': jq})); + writer.replace, {'doc:manifest': manifest})); writesFuture.push(writer.copy('docs/src/templates/index.html', 'build/docs/index-debug.html', - writer.replace, {'doc:manifest': '', - '': ng})); + writer.replace, {'doc:manifest': ''})); writesFuture.push(writer.copy('docs/src/templates/index.html', 'build/docs/index-jq-debug.html', - writer.replace, {'doc:manifest': '', - '': ng, - '': jq})); + writer.replace, {'doc:manifest': ''})); writesFuture.push(writer.copyTpl('offline.html')); writesFuture.push(writer.copyTpl('docs-scenario.html')); diff --git a/docs/src/ngdoc.js b/docs/src/ngdoc.js index 1045d39df2bd..abe5e1d73fc2 100644 --- a/docs/src/ngdoc.js +++ b/docs/src/ngdoc.js @@ -133,7 +133,7 @@ Doc.prototype = { if (!isFullUrl) self.links.push(absUrl); - return '' + return '' + (isAngular ? '' : '') + (title || url).replace(/\n/g, ' ') + (isAngular ? '' : '') @@ -243,7 +243,7 @@ Doc.prototype = { } dom.h('Dependencies', self.requires, function(require){ dom.tag('code', function(){ - dom.tag('a', {href:"#!/api/angular.service." + require.name}, require.name); + dom.tag('a', {href: 'api/angular.service.' + require.name}, require.name); }); dom.html(require.text); }); @@ -570,23 +570,23 @@ function scenarios(docs){ var specs = []; specs.push('describe("angular+jqlite", function() {'); - appendSpecs('index.html'); + appendSpecs(''); specs.push('});'); specs.push(''); specs.push(''); specs.push('describe("angular+jquery", function() {'); - appendSpecs('index-jq.html'); + appendSpecs('index-jq.html#!/'); specs.push('});'); return specs.join('\n'); - function appendSpecs(htmlFile) { + function appendSpecs(urlPrefix) { docs.forEach(function(doc){ specs.push(' describe("' + doc.section + '/' + doc.id + '", function(){'); specs.push(' beforeEach(function(){'); - specs.push(' browser().navigateTo("' + htmlFile + '#!/' + doc.section + '/' + doc.id + '");'); + specs.push(' browser().navigateTo("' + urlPrefix + doc.section + '/' + doc.id + '");'); specs.push(' });'); specs.push(' '); doc.scenarios.forEach(function(scenario){ diff --git a/docs/src/templates/.htaccess b/docs/src/templates/.htaccess index 87487e9e122c..9f9a152cf651 100644 --- a/docs/src/templates/.htaccess +++ b/docs/src/templates/.htaccess @@ -8,4 +8,8 @@ RewriteEngine on RewriteCond %{HTTP_COOKIE} ng-offline="NG_VERSION_FULL" -RewriteRule appcache.manifest appcache-offline.manifest \ No newline at end of file +RewriteRule appcache.manifest appcache-offline.manifest + + +## HTML5 URL Support ## +RewriteRule ^(guide|api|cookbook|misc|tutorial)(/.*)?$ index.html diff --git a/docs/src/templates/docs.js b/docs/src/templates/docs.js index d1069a770dac..505aed60c2ef 100644 --- a/docs/src/templates/docs.js +++ b/docs/src/templates/docs.js @@ -4,7 +4,8 @@ function DocsController($location, $browser, $window, $cookies) { var self = this, OFFLINE_COOKIE_NAME = 'ng-offline', - DOCS_PATH = /^\/(api)|(guide)|(cookbook)|(misc)|(tutorial)/; + DOCS_PATH = /^\/(api)|(guide)|(cookbook)|(misc)|(tutorial)/, + INDEX_PATH = /^(\/|\/index[^\.]*.html)$/; this.$location = $location; @@ -13,7 +14,7 @@ function DocsController($location, $browser, $window, $cookies) { self.subpage = false; self.offlineEnabled = ($cookies[OFFLINE_COOKIE_NAME] == angular.version.full); - if (!$location.path()) { + if (!$location.path() || INDEX_PATH.test($location.path())) { $location.path('/api').replace(); } @@ -40,11 +41,11 @@ function DocsController($location, $browser, $window, $cookies) { }); this.getUrl = function(page){ - return '#!/' + page.section + '/' + page.id; + return page.section + '/' + page.id; }; this.getCurrentPartial = function(){ - return this.partialId ? ('./' + this.sectionId + '/' + this.partialId + '.html') : ''; + return this.partialId ? ('./partials/' + this.sectionId + '/' + this.partialId + '.html') : ''; }; this.getClass = function(page) { @@ -127,7 +128,7 @@ function TutorialInstructionsCtrl($cookieStore) { angular.service('$locationConfig', function() { return { - html5Mode: false, + html5Mode: true, hashPrefix: '!' }; }); diff --git a/docs/src/templates/index.html b/docs/src/templates/index.html index d5cfaed2d290..a2def7a68f07 100644 --- a/docs/src/templates/index.html +++ b/docs/src/templates/index.html @@ -5,23 +5,57 @@ doc:manifest> - AngularJS - - + AngularJS + @@ -45,11 +91,11 @@
- - - - - - diff --git a/lib/nodeserver/server.js b/lib/nodeserver/server.js index 471bba944da6..54ae78fcec26 100644 --- a/lib/nodeserver/server.js +++ b/lib/nodeserver/server.js @@ -91,6 +91,18 @@ StaticServlet.prototype.handleRequest = function(req, res) { var parts = path.split('/'); if (parts[parts.length-1].charAt(0) === '.') return self.sendForbidden_(req, res, path); + + // docs rewriting + var REWRITE = /\/(guide|api|cookbook|misc|tutorial)\/.*$/, + IGNORED = /(\.(css|js|png|jpg)$|partials\/.*\.html$)/, + match; + + if (!IGNORED.test(path) && (match = path.match(REWRITE))) { + path = path.replace(match[0], '/index.html'); + sys.puts('Rewrite to ' + path); + } + // end of docs rewriting + fs.stat(path, function(err, stat) { if (err) return self.sendMissing_(req, res, path); diff --git a/src/markups.js b/src/markups.js index 5c9c14b460a8..1adad3e0cfb6 100644 --- a/src/markups.js +++ b/src/markups.js @@ -166,10 +166,10 @@ angularTextMarkup('option', function(text, textNode, parentElement){
link 1 (link, don't reload)
link 2 (link, don't reload)
- link 3 (link, reload!)
+ link 3 (link, reload!)
anchor (link, don't reload)
anchor (no link)
- link (link, change hash) + link (link, change hash)
it('should execute ng:click but not reload when href without value', function() { @@ -185,10 +185,10 @@ angularTextMarkup('option', function(text, textNode, parentElement){ }); it('should execute ng:click and change url when ng:href specified', function() { + expect(element('#link-3').attr('href')).toBe("/123"); + element('#link-3').click(); - expect(input('value').val()).toEqual('3'); - expect(element('#link-3').attr('href')).toBe("#!/123"); - expect(browser().location().hash()).toEqual('!/123'); + expect(browser().location().path()).toEqual('/123'); }); it('should execute ng:click but not reload when href empty string and name specified', function() { @@ -205,9 +205,10 @@ angularTextMarkup('option', function(text, textNode, parentElement){ it('should only change url when only ng:href', function() { input('value').enter('6'); + expect(element('#link-6').attr('href')).toBe("/6"); + element('#link-6').click(); - expect(browser().location().hash()).toEqual('!/6'); - expect(element('#link-6').attr('href')).toBe("#!/6"); + expect(browser().location().path()).toEqual('/6'); });
diff --git a/src/widgets.js b/src/widgets.js index 1d0217b81595..bd7c3d7f8847 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -1390,10 +1390,10 @@ angularWidget("@ng:non-bindable", noop); function MyCtrl($route) { $route.when('/overview', { controller: OverviewCtrl, - template: 'guide/dev_guide.overview.html'}); + template: 'partials/guide/dev_guide.overview.html'}); $route.when('/bootstrap', { controller: BootstrapCtrl, - template: 'guide/dev_guide.bootstrap.auto_bootstrap.html'}); + template: 'partials/guide/dev_guide.bootstrap.auto_bootstrap.html'}); }; MyCtrl.$inject = ['$route']; @@ -1401,9 +1401,9 @@ angularWidget("@ng:non-bindable", noop); function OverviewCtrl(){}
- overview | - bootstrap | - undefined + overview | + bootstrap | + undefined