From b2d2066d96994501f68a7a782a8da1424a31fd7f Mon Sep 17 00:00:00 2001 From: Thibaut Courouble Date: Sun, 17 Jan 2016 13:30:46 -0500 Subject: [PATCH] Multi-version support Ref #25. --- assets/javascripts/app/app.coffee | 17 ++++- assets/javascripts/app/router.coffee | 6 +- assets/javascripts/collections/docs.coffee | 3 + assets/javascripts/models/doc.coffee | 2 + .../templates/pages/offline_tmpl.coffee | 3 +- assets/javascripts/templates/path_tmpl.coffee | 2 +- .../javascripts/templates/sidebar_tmpl.coffee | 10 +-- lib/app.rb | 23 ++++++- lib/docs.rb | 31 ++++++--- lib/docs/core/doc.rb | 29 +++++++- .../entries.rb => python/entries_v2.rb} | 4 +- .../python/{entries.rb => entries_v3.rb} | 2 +- lib/docs/scrapers/python.rb | 21 ++++-- lib/docs/scrapers/python2.rb | 29 -------- lib/tasks/docs.thor | 42 ++++++------ test/app_test.rb | 67 +++++++++++++------ test/files/docs.json | 2 +- test/lib/docs/core/doc_test.rb | 51 ++++++++++++++ views/app.erb | 2 +- 19 files changed, 244 insertions(+), 102 deletions(-) rename lib/docs/filters/{python2/entries.rb => python/entries_v2.rb} (97%) rename lib/docs/filters/python/{entries.rb => entries_v3.rb} (98%) delete mode 100644 lib/docs/scrapers/python2.rb diff --git a/assets/javascripts/app/app.coffee b/assets/javascripts/app/app.coffee index 00c17495..22845ccf 100644 --- a/assets/javascripts/app/app.coffee +++ b/assets/javascripts/app/app.coffee @@ -75,6 +75,7 @@ docs = @settings.getDocs() for doc in @DOCS (if docs.indexOf(doc.slug) >= 0 then @docs else @disabledDocs).add(doc) + @migrateDocs() @docs.sort() @disabledDocs.sort() @docs.load @start.bind(@), @onBootError.bind(@), readCache: true, writeCache: true @@ -99,6 +100,15 @@ @entries.add doc.entries.all() return + migrateDocs: -> + for slug in @settings.getDocs() when not @docs.findBy('slug', slug) + needsSaving = true + if doc = @disabledDocs.findBy('slug_without_version', slug) + @disabledDocs.remove(doc) + @docs.add(doc) + + @saveDocs() if needsSaving + enableDoc: (doc, _onSuccess, onError) -> return if @docs.contains(doc) onSuccess = => @@ -106,14 +116,17 @@ @docs.add(doc) @docs.sort() @initDoc(doc) - @settings.setDocs(doc.slug for doc in @docs.all()) + @saveDocs() _onSuccess() - @appCache?.updateInBackground() return doc.load onSuccess, onError, writeCache: true return + saveDocs: -> + @settings.setDocs(doc.slug for doc in @docs.all()) + @appCache?.updateInBackground() + welcomeBack: -> visitCount = @settings.get('count') @settings.set 'count', ++visitCount diff --git a/assets/javascripts/app/router.coffee b/assets/javascripts/app/router.coffee index d86b9881..aa1fc6b7 100644 --- a/assets/javascripts/app/router.coffee +++ b/assets/javascripts/app/router.coffee @@ -39,7 +39,7 @@ class app.Router return doc: (context, next) -> - if doc = app.docs.findBy('slug', context.params.doc) or app.disabledDocs.findBy('slug', context.params.doc) + if doc = app.docs.findBySlug(context.params.doc) or app.disabledDocs.findBySlug(context.params.doc) context.doc = doc context.entry = doc.toEntry() @triggerRoute 'entry' @@ -48,7 +48,7 @@ class app.Router return type: (context, next) -> - doc = app.docs.findBy 'slug', context.params.doc + doc = app.docs.findBySlug(context.params.doc) if type = doc?.types.findBy 'slug', context.params.type context.doc = doc @@ -59,7 +59,7 @@ class app.Router return entry: (context, next) -> - doc = app.docs.findBy 'slug', context.params.doc + doc = app.docs.findBySlug(context.params.doc) if entry = doc?.findEntryByPathAndHash(context.params.path, context.hash) context.doc = doc diff --git a/assets/javascripts/collections/docs.coffee b/assets/javascripts/collections/docs.coffee index 30001566..c4b2e368 100644 --- a/assets/javascripts/collections/docs.coffee +++ b/assets/javascripts/collections/docs.coffee @@ -1,6 +1,9 @@ class app.collections.Docs extends app.Collection @model: 'Doc' + findBySlug: (slug) -> + @findBy('slug', slug) or @findBy('slug_without_version', slug) + sort: -> @models.sort (a, b) -> a = a.name.toLowerCase() diff --git a/assets/javascripts/models/doc.coffee b/assets/javascripts/models/doc.coffee index c805d3a2..b7b78b54 100644 --- a/assets/javascripts/models/doc.coffee +++ b/assets/javascripts/models/doc.coffee @@ -4,6 +4,8 @@ class app.models.Doc extends app.Model constructor: -> super @reset @ + [@slug_without_version, @version] = @slug.split('~v') + @icon = @slug_without_version @text = @toEntry().text reset: (data) -> diff --git a/assets/javascripts/templates/pages/offline_tmpl.coffee b/assets/javascripts/templates/pages/offline_tmpl.coffee index 54a6bfa7..bf79a7f7 100644 --- a/assets/javascripts/templates/pages/offline_tmpl.coffee +++ b/assets/javascripts/templates/pages/offline_tmpl.coffee @@ -51,10 +51,11 @@ canICloseTheTab = -> app.templates.offlineDoc = (doc, status) -> outdated = doc.isOutdated(status) + version = if doc.version then " (#{doc.version})" else '' html = """ - #{doc.name} + #{doc.name}#{version} #{Math.ceil(doc.db_size / 100000) / 10} MB """ diff --git a/assets/javascripts/templates/path_tmpl.coffee b/assets/javascripts/templates/path_tmpl.coffee index 43996ee0..cf4bc873 100644 --- a/assets/javascripts/templates/path_tmpl.coffee +++ b/assets/javascripts/templates/path_tmpl.coffee @@ -1,5 +1,5 @@ app.templates.path = (doc, type, entry) -> - html = """#{doc.name}""" + html = """#{doc.name}""" html += """#{type.name}""" if type html += """#{$.escape entry.name}""" if entry html diff --git a/assets/javascripts/templates/sidebar_tmpl.coffee b/assets/javascripts/templates/sidebar_tmpl.coffee index 41c7038e..0b2390c4 100644 --- a/assets/javascripts/templates/sidebar_tmpl.coffee +++ b/assets/javascripts/templates/sidebar_tmpl.coffee @@ -1,7 +1,7 @@ templates = app.templates templates.sidebarDoc = (doc, options = {}) -> - link = """""" if options.disabled @@ -22,7 +22,7 @@ templates.sidebarResult = (entry) -> """Enable""" else """""" - """#{addon}#{$.escape entry.name}""" + """#{addon}#{$.escape entry.name}""" templates.sidebarNoResults = -> html = """
No results.
""" @@ -35,11 +35,13 @@ templates.sidebarPageLink = (count) -> """Show more\u2026 (#{count})""" templates.sidebarLabel = (doc, options = {}) -> - label = """" + label += ">#{doc.name}" + label += " (#{doc.version})" if doc.version + label + "" templates.sidebarDisabledList = (options) -> """
#{templates.render 'sidebarDoc', options.docs, disabled: true}
""" diff --git a/lib/app.rb b/lib/app.rb index a618a243..d85fa49d 100644 --- a/lib/app.rb +++ b/lib/app.rb @@ -115,6 +115,23 @@ class App < Sinatra::Application end end + def find_doc(slug) + settings.docs[slug] || begin + slug = "#{slug}~v" + settings.docs.each do |_slug, _doc| + return _doc if _slug.start_with?(slug) + end + nil + end + end + + def user_has_docs?(slug) + docs.include?(slug) || begin + slug = "#{slug}~v" + docs.any? { |_slug| _slug.start_with?(slug) } + end + end + def doc_index_urls docs.each_with_object [] do |slug, result| if doc = settings.docs[slug] @@ -247,14 +264,14 @@ class App < Sinatra::Application settings.news_feed end - get %r{\A/(\w+)(\-[\w\-]+)?(/.*)?\z} do |doc, type, rest| - return 404 unless @doc = settings.docs[doc] + get %r{\A/([\w~\.]+)(\-[\w\-]+)?(/.*)?\z} do |doc, type, rest| + return 404 unless @doc = find_doc(doc) if rest.nil? redirect "/#{doc}#{type}/#{query_string_for_redirection}" elsif rest.length > 1 && rest.end_with?('/') redirect "/#{doc}#{type}#{rest[0...-1]}#{query_string_for_redirection}" - elsif docs.include?(doc) && supports_js_redirection? + elsif user_has_docs?(doc) && supports_js_redirection? redirect_via_js(request.path) else erb :other diff --git a/lib/docs.rb b/lib/docs.rb index b31dcf7e..046d75a8 100644 --- a/lib/docs.rb +++ b/lib/docs.rb @@ -31,31 +31,44 @@ module Docs Dir["#{root_path}/docs/scrapers/**/*.rb"]. map { |file| File.basename(file, '.rb') }. sort!. - map(&method(:find)). + map { |name| const_get(name.camelize) }. reject(&:abstract) end - def self.find(name) + def self.all_versions + all.flat_map(&:versions) + end + + def self.find(name, version) const = name.camelize - const_get(const) + doc = const_get(const) + + if version.present? + doc = doc.versions.find { |klass| klass.version == version } + raise DocNotFound.new(%(could not find version "#{version}" for doc "#{name}"), name) unless doc + else + doc = doc.versions.first + end + + doc rescue NameError => error if error.name.to_s == const - raise DocNotFound.new("failed to locate doc class '#{name}'", name) + raise DocNotFound.new(%(could not find doc "#{name}"), name) else raise error end end - def self.generate_page(name, page_id) - find(name).store_page(store, page_id) + def self.generate_page(name, version, page_id) + find(name, version).store_page(store, page_id) end - def self.generate(name) - find(name).store_pages(store) + def self.generate(name, version) + find(name, version).store_pages(store) end def self.generate_manifest - Manifest.new(store, all).store + Manifest.new(store, all_versions).store end def self.store diff --git a/lib/docs/core/doc.rb b/lib/docs/core/doc.rb index 23203756..69eb67a0 100644 --- a/lib/docs/core/doc.rb +++ b/lib/docs/core/doc.rb @@ -12,12 +12,39 @@ module Docs subclass.type = type end + def version(version = nil, &block) + return @version if version.nil? + + klass = Class.new(self) + klass.class_exec(&block) + klass.name = name + klass.slug = slug + klass.version = version + klass.links = links + @versions ||= [] + @versions << klass + klass + end + + def version=(value) + @version = value.to_s + end + + def versions + @versions.presence || [self] + end + + def version? + version.present? + end + def name @name || super.try(:demodulize) end def slug - @slug || name.try(:downcase) + slug = @slug || name.try(:downcase) + version? ? "#{slug}~v#{version}" : slug end def path diff --git a/lib/docs/filters/python2/entries.rb b/lib/docs/filters/python/entries_v2.rb similarity index 97% rename from lib/docs/filters/python2/entries.rb rename to lib/docs/filters/python/entries_v2.rb index 453f4d19..c5f7bfc1 100644 --- a/lib/docs/filters/python2/entries.rb +++ b/lib/docs/filters/python/entries_v2.rb @@ -1,6 +1,6 @@ module Docs - class Python2 - class EntriesFilter < Docs::EntriesFilter + class Python + class EntriesV2Filter < Docs::EntriesFilter REPLACE_TYPES = { 'compiler package' => 'Compiler', 'Cryptographic' => 'Cryptography', diff --git a/lib/docs/filters/python/entries.rb b/lib/docs/filters/python/entries_v3.rb similarity index 98% rename from lib/docs/filters/python/entries.rb rename to lib/docs/filters/python/entries_v3.rb index 2f7d6e1f..2addea16 100644 --- a/lib/docs/filters/python/entries.rb +++ b/lib/docs/filters/python/entries_v3.rb @@ -1,6 +1,6 @@ module Docs class Python - class EntriesFilter < Docs::EntriesFilter + class EntriesV3Filter < Docs::EntriesFilter REPLACE_TYPES = { 'Cryptographic' => 'Cryptography', 'Custom Interpreters' => 'Interpreters', diff --git a/lib/docs/scrapers/python.rb b/lib/docs/scrapers/python.rb index 877f4161..51e1586a 100644 --- a/lib/docs/scrapers/python.rb +++ b/lib/docs/scrapers/python.rb @@ -1,13 +1,8 @@ module Docs class Python < FileScraper - self.release = '3.5.1' self.type = 'sphinx' - self.dir = '/Users/Thibaut/DevDocs/Docs/Python' # downloaded from docs.python.org/3/download.html - self.base_url = 'http://docs.python.org/3/' self.root_path = 'library/index.html' - html_filters.push 'python/entries', 'python/clean_html' - options[:only_patterns] = [/\Alibrary\//] options[:skip] = %w( @@ -23,5 +18,21 @@ module Docs © 1990–2015 Python Software Foundation
Licensed under the PSF License. HTML + + version '3.5' do + self.release = '3.5.1' + self.dir = '/Users/Thibaut/DevDocs/Docs/Python35' # docs.python.org/3.5/download.html + self.base_url = 'https://docs.python.org/3.5/' + + html_filters.push 'python/entries_v3', 'python/clean_html' + end + + version '2.7' do + self.release = '2.7.10' + self.dir = '/Users/Thibaut/DevDocs/Docs/Python27' # docs.python.org/2.7/download.html + self.base_url = 'https://docs.python.org/2.7/' + + html_filters.push 'python/entries_v2', 'python/clean_html' + end end end diff --git a/lib/docs/scrapers/python2.rb b/lib/docs/scrapers/python2.rb deleted file mode 100644 index d36452e9..00000000 --- a/lib/docs/scrapers/python2.rb +++ /dev/null @@ -1,29 +0,0 @@ -module Docs - class Python2 < FileScraper - self.name = 'Python 2' - self.slug = 'python2' - self.release = '2.7.10' - self.type = 'sphinx' - self.dir = '/Users/Thibaut/DevDocs/Docs/Python2' # downloaded from docs.python.org/2.7/download.html - self.base_url = 'http://docs.python.org/2.7/' - self.root_path = 'library/index.html' - - html_filters.push 'python2/entries', 'python/clean_html' - - options[:only_patterns] = [/\Alibrary\//] - - options[:skip] = %w( - library/2to3.html - library/formatter.html - library/index.html - library/intro.html - library/undoc.html - library/unittest.mock-examples.html - library/sunau.html) - - options[:attribution] = <<-HTML - © 1990–2015 Python Software Foundation
- Licensed under the PSF License. - HTML - end -end diff --git a/lib/tasks/docs.thor b/lib/tasks/docs.thor index 8a8beb0d..4445b256 100644 --- a/lib/tasks/docs.thor +++ b/lib/tasks/docs.thor @@ -16,11 +16,13 @@ class DocsCLI < Thor max_length = 0 Docs.all. map { |doc| [doc.to_s.demodulize.underscore, doc] }. - each { |pair| max_length = pair.first.length if pair.first.length > max_length }. - each { |pair| puts "#{pair.first.rjust max_length + 1}: #{pair.second.base_url.remove %r{\Ahttps?://}}" } + to_h. + each { |name, doc| max_length = name.length if name.length > max_length }. + each { |name, doc| puts "#{name.rjust max_length + 1}: #{doc.versions.map { |v| v.release || '-' }.join(', ')}" } end - desc 'page [path] [--verbose] [--debug]', 'Generate a page (no indexing)' + desc 'page [path] [--version] [--verbose] [--debug]', 'Generate a page (no indexing)' + option :version, type: :string option :verbose, type: :boolean option :debug, type: :boolean def page(name, path = '') @@ -34,16 +36,17 @@ class DocsCLI < Thor Docs.install_report :filter, :request end - if Docs.generate_page(name, path) + if Docs.generate_page(name, options[:version], path) puts 'Done' else puts "Failed!#{' (try running with --debug for more information)' unless options[:debug]}" end - rescue Docs::DocNotFound - invalid_doc(name) + rescue Docs::DocNotFound => error + handle_doc_not_found_error(error) end - desc 'generate [--verbose] [--debug] [--force] [--package]', 'Generate a documentation' + desc 'generate [--version] [--verbose] [--debug] [--force] [--package]', 'Generate a documentation' + option :version, type: :string option :verbose, type: :boolean option :debug, type: :boolean option :force, type: :boolean @@ -66,18 +69,18 @@ class DocsCLI < Thor return unless yes? 'Proceed? (y/n)' end - if Docs.generate(name) + if Docs.generate(name, options[:version]) generate_manifest if options[:package] require 'unix_utils' - package_doc(Docs.find(name)) + package_doc(Docs.find(name, options[:version])) end puts 'Done' else puts "Failed!#{' (try running with --debug for more information)' unless options[:debug]}" end - rescue Docs::DocNotFound - invalid_doc(name) + rescue Docs::DocNotFound => error + handle_doc_not_found_error(error) end desc 'manifest', 'Create the manifest' @@ -86,7 +89,7 @@ class DocsCLI < Thor puts 'Done' end - desc 'download ( ... | --all)', 'Download documentations' + desc 'download ( ... | --all)', 'Download documentations' option :all, type: :boolean def download(*names) require 'unix_utils' @@ -96,10 +99,10 @@ class DocsCLI < Thor generate_manifest puts 'Done' rescue Docs::DocNotFound => error - invalid_doc(error.name) + handle_doc_not_found_error(error) end - desc 'package ( ... | --all)', 'Package documentations' + desc 'package ( ... | --all)', 'Package documentations' option :all, type: :boolean def package(*names) require 'unix_utils' @@ -108,7 +111,7 @@ class DocsCLI < Thor docs.each(&method(:package_doc)) puts 'Done' rescue Docs::DocNotFound => error - invalid_doc(error.name) + handle_doc_not_found_error(error) end desc 'clean', 'Delete documentation packages' @@ -121,7 +124,8 @@ class DocsCLI < Thor def find_docs(names) names.map do |name| - Docs.find(name) + name, version = name.split('@') + Docs.find(name, version) end end @@ -133,9 +137,9 @@ class DocsCLI < Thor end end - def invalid_doc(name) - puts %(ERROR: invalid doc "#{name}".) - puts 'Run "thor docs:list" to see the list of docs.' + def handle_doc_not_found_error(error) + puts %(ERROR: #{error}.) + puts 'Run "thor docs:list" to see the list of docs and versions.' end def download_docs(docs) diff --git a/test/app_test.rb b/test/app_test.rb index 94512900..db7d7ba1 100644 --- a/test/app_test.rb +++ b/test/app_test.rb @@ -80,11 +80,18 @@ class AppTest < MiniTest::Spec end it "works with cookie" do - set_cookie('docs=css/html') + set_cookie('docs=css/html~v5') get '/manifest.appcache' assert last_response.ok? - assert_includes last_response.body, '/css/index.json' - assert_includes last_response.body, '/html/index.json' + assert_includes last_response.body, '/css/index.json?1420139788' + assert_includes last_response.body, '/html~v5/index.json?1420139791' + end + + it "ignores invalid docs in the cookie" do + set_cookie('docs=foo') + get '/manifest.appcache' + assert last_response.ok? + refute_includes last_response.body, 'foo' end it "has the word 'default' when no 'dark' cookie is set" do @@ -120,13 +127,26 @@ class AppTest < MiniTest::Spec describe "/[doc]" do it "renders when the doc exists and isn't enabled" do - set_cookie('docs=css') - get '/html/', {}, 'HTTP_USER_AGENT' => MODERN_BROWSER + set_cookie('docs=html~v5') + get '/html~v4/', {}, 'HTTP_USER_AGENT' => MODERN_BROWSER assert last_response.ok? end it "redirects via JS cookie when the doc exists and is enabled" do - set_cookie('docs=html') + set_cookie('docs=html~v5') + get '/html~v5/', {}, 'HTTP_USER_AGENT' => MODERN_BROWSER + assert last_response.redirect? + assert_equal 'http://example.org/', last_response['Location'] + assert last_response['Set-Cookie'].start_with?("initial_path=%2Fhtml%7Ev5%2F; path=/; expires=") + end + + it "renders when the doc exists, has no version in the path, and isn't enabled" do + get '/html/', {}, 'HTTP_USER_AGENT' => MODERN_BROWSER + assert last_response.ok? + end + + it "redirects via JS cookie when the doc exists, has no version in the path, and a version is enabled" do + set_cookie('docs=html~v5') get '/html/', {}, 'HTTP_USER_AGENT' => MODERN_BROWSER assert last_response.redirect? assert_equal 'http://example.org/', last_response['Location'] @@ -140,7 +160,7 @@ class AppTest < MiniTest::Spec end it "returns 404 when the doc doesn't exist" do - get '/foo/' + get '/html~v6/' assert last_response.not_found? end @@ -157,42 +177,49 @@ class AppTest < MiniTest::Spec describe "/[doc]-[type]" do it "works when the doc exists" do + get '/html~v4-foo-bar_42/' + assert last_response.ok? + assert_includes last_response.body, 'app.DOC = {"name":"HTML","slug":"html~v4"' + end + + it "works when the doc has no version in the path and a version exists" do get '/html-foo-bar_42/' assert last_response.ok? + assert_includes last_response.body, 'app.DOC = {"name":"HTML","slug":"html~v5"' end it "returns 404 when the type is blank" do - get '/html-/' + get '/css-/' assert last_response.not_found? end it "returns 404 when the type is not alpha-numeric" do - get '/html-foo:bar/' + get '/css-foo:bar/' assert last_response.not_found? end it "returns 404 when the doc doesn't exist" do - get '/foo-bar/' + get '/html~v6-bar/' assert last_response.not_found? end it "redirects with trailing slash" do - get '/html-foo' + get '/css-foo' assert last_response.redirect? - assert_equal 'http://example.org/html-foo/', last_response['Location'] + assert_equal 'http://example.org/css-foo/', last_response['Location'] - get '/html-foo', bar: 'baz' + get '/css-foo', bar: 'baz' assert last_response.redirect? - assert_equal 'http://example.org/html-foo/?bar=baz', last_response['Location'] + assert_equal 'http://example.org/css-foo/?bar=baz', last_response['Location'] end end describe "/[doc+type]/[path]" do it "works when the doc exists" do - get '/html/foo' + get '/css/foo' assert last_response.ok? - get '/html-bar/foo' + get '/css-bar/foo' assert last_response.ok? end @@ -202,13 +229,13 @@ class AppTest < MiniTest::Spec end it "redirects without trailing slash" do - get '/html/foo/' + get '/css/foo/' assert last_response.redirect? - assert_equal 'http://example.org/html/foo', last_response['Location'] + assert_equal 'http://example.org/css/foo', last_response['Location'] - get '/html/foo/', bar: 'baz' + get '/css/foo/', bar: 'baz' assert last_response.redirect? - assert_equal 'http://example.org/html/foo?bar=baz', last_response['Location'] + assert_equal 'http://example.org/css/foo?bar=baz', last_response['Location'] end end diff --git a/test/files/docs.json b/test/files/docs.json index c010fc0b..c70c2056 100644 --- a/test/files/docs.json +++ b/test/files/docs.json @@ -1 +1 @@ -[{"name":"CSS","slug":"css","type":"mdn","release":null,"mtime":1420139788,"db_size":3460507},{"name":"DOM","slug":"dom","type":"mdn","release":null,"mtime":1420139789,"db_size":11399128},{"name":"DOM Events","slug":"dom_events","type":"mdn","release":null,"mtime":1420139790,"db_size":889020},{"name":"HTML","slug":"html","type":"mdn","release":null,"mtime":1420139790,"db_size":1835646},{"name":"HTTP","slug":"http","type":"rfc","release":null,"mtime":1420139790,"db_size":183083},{"name":"JavaScript","slug":"javascript","type":"mdn","release":null,"mtime":1420139791,"db_size":4125477}] +[{"name":"CSS","slug":"css","type":"mdn","release":null,"mtime":1420139788,"db_size":3460507},{"name":"DOM","slug":"dom","type":"mdn","release":null,"mtime":1420139789,"db_size":11399128},{"name":"DOM Events","slug":"dom_events","type":"mdn","release":null,"mtime":1420139790,"db_size":889020},{"name":"HTML","slug":"html~v5","type":"mdn","version":"5","mtime":1420139791,"db_size":1835647},{"name":"HTML","slug":"html~v4","type":"mdn","version":"4","mtime":1420139790,"db_size":1835646},{"name":"HTTP","slug":"http","type":"rfc","release":null,"mtime":1420139790,"db_size":183083},{"name":"JavaScript","slug":"javascript","type":"mdn","release":null,"mtime":1420139791,"db_size":4125477}] diff --git a/test/lib/docs/core/doc_test.rb b/test/lib/docs/core/doc_test.rb index 3e595486..5430a975 100644 --- a/test/lib/docs/core/doc_test.rb +++ b/test/lib/docs/core/doc_test.rb @@ -44,6 +44,17 @@ class DocsDocTest < MiniTest::Spec it "returns 'doc' when the class is Docs::Doc" do assert_equal 'doc', Docs::Doc.slug end + + it "returns 'doc~v42' when the class is Docs::Doc and its #version is '42'" do + stub(Docs::Doc).version { '42' } + assert_equal 'doc~v42', Docs::Doc.slug + end + + it "returns 'foo~v42' when #slug has been set to 'foo' and #version to '42'" do + doc.slug = 'foo' + doc.version = '42' + assert_equal 'foo~v42', doc.slug + end end describe ".slug=" do @@ -53,6 +64,13 @@ class DocsDocTest < MiniTest::Spec end end + describe ".version=" do + it "stores .version as a string" do + doc.version = 4815162342 + assert_equal '4815162342', doc.version + end + end + describe ".release=" do it "stores .release" do doc.release = '1' @@ -297,4 +315,37 @@ class DocsDocTest < MiniTest::Spec end end end + + describe ".versions" do + it "returns [self] if no versions have been created" do + assert_equal [doc], doc.versions + end + end + + describe ".version" do + context "with no args" do + it "returns @version by default" do + doc.version = 'v' + assert_equal 'v', doc.version + end + end + + context "with args" do + it "creates a version subclass" do + version = doc.version('4') { self.release = '8'} + + assert_equal [version], doc.versions + + assert_nil doc.version + assert_nil doc.release + refute doc.version? + + assert version.version? + assert_equal '4', version.version + assert_equal '8', version.release + assert_equal 'name', version.name + assert_equal 'type', version.type + end + end + end end diff --git a/views/app.erb b/views/app.erb index 29e4cab7..adc1b2af 100644 --- a/views/app.erb +++ b/views/app.erb @@ -21,7 +21,7 @@
<% unless @doc %> <% App.docs.each do |slug, doc| %> - <%= doc['name'] %> + <%= doc['name'] %> <% end %> <% end %>