Explorar o código

Multi-version support

Ref #25.
Thibaut Courouble %!s(int64=10) %!d(string=hai) anos
pai
achega
b2d2066d96

+ 15 - 2
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

+ 3 - 3
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

+ 3 - 0
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()

+ 2 - 0
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) ->

+ 2 - 1
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 = """
     <tr data-slug="#{doc.slug}"#{if outdated then ' class="_highlight"' else ''}>
-      <td class="_docs-name _icon-#{doc.slug}">#{doc.name}</td>
+      <td class="_docs-name _icon-#{doc.icon}">#{doc.name}#{version}</td>
       <td class="_docs-size">#{Math.ceil(doc.db_size / 100000) / 10} MB</td>
   """
 

+ 1 - 1
assets/javascripts/templates/path_tmpl.coffee

@@ -1,5 +1,5 @@
 app.templates.path = (doc, type, entry) ->
-  html = """<a href="#{doc.fullPath()}" class="_path-item _icon-#{doc.slug}">#{doc.name}</a>"""
+  html = """<a href="#{doc.fullPath()}" class="_path-item _icon-#{doc.icon}">#{doc.name}</a>"""
   html += """<a href="#{type.fullPath()}" class="_path-item">#{type.name}</a>""" if type
   html += """<span class="_path-item">#{$.escape entry.name}</span>""" if entry
   html

+ 6 - 4
assets/javascripts/templates/sidebar_tmpl.coffee

@@ -1,7 +1,7 @@
 templates = app.templates
 
 templates.sidebarDoc = (doc, options = {}) ->
-  link  = """<a href="#{doc.fullPath()}" class="_list-item _icon-#{doc.slug} """
+  link  = """<a href="#{doc.fullPath()}" class="_list-item _icon-#{doc.icon} """
   link += if options.disabled then '_list-disabled' else '_list-dir'
   link += """" data-slug="#{doc.slug}" title="#{doc.name}">"""
   if options.disabled
@@ -22,7 +22,7 @@ templates.sidebarResult = (entry) ->
     """<span class="_list-enable" data-enable="#{entry.doc.slug}">Enable</span>"""
   else
     """<span class="_list-reveal" data-reset-list title="Reveal in list"></span>"""
-  """<a href="#{entry.fullPath()}" class="_list-item _list-hover _list-result _icon-#{entry.doc.slug}">#{addon}#{$.escape entry.name}</a>"""
+  """<a href="#{entry.fullPath()}" class="_list-item _list-hover _list-result _icon-#{entry.doc.icon}">#{addon}#{$.escape entry.name}</a>"""
 
 templates.sidebarNoResults = ->
   html = """ <div class="_list-note">No results.</div> """
@@ -35,11 +35,13 @@ templates.sidebarPageLink = (count) ->
   """<span class="_list-item _list-pagelink">Show more\u2026 (#{count})</span>"""
 
 templates.sidebarLabel = (doc, options = {}) ->
-  label = """<label class="_list-item _list-label _icon-#{doc.slug}"""
+  label = """<label class="_list-item _list-label _icon-#{doc.icon}"""
   label += ' _list-label-off' unless options.checked
   label += """"><input type="checkbox" name="#{doc.slug}" class="_list-checkbox" """
   label += 'checked' if options.checked
-  label +  ">#{doc.name}</label>"
+  label += ">#{doc.name}"
+  label += " (#{doc.version})" if doc.version
+  label + "</label>"
 
 templates.sidebarDisabledList = (options) ->
   """<div class="_disabled-list">#{templates.render 'sidebarDoc', options.docs, disabled: true}</div>"""

+ 20 - 3
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

+ 22 - 9
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

+ 28 - 1
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

+ 2 - 2
lib/docs/filters/python2/entries.rb → 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',

+ 1 - 1
lib/docs/filters/python/entries.rb → 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',

+ 16 - 5
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
       &copy; 1990&ndash;2015 Python Software Foundation<br>
       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

+ 0 - 29
lib/docs/scrapers/python2.rb

@@ -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
-      &copy; 1990&ndash;2015 Python Software Foundation<br>
-      Licensed under the PSF License.
-    HTML
-  end
-end

+ 23 - 19
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 <doc> [path] [--verbose] [--debug]', 'Generate a page (no indexing)'
+  desc 'page <doc> [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 <doc> [--verbose] [--debug] [--force] [--package]', 'Generate a documentation'
+  desc 'generate <doc> [--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 (<doc> <doc>... | --all)', 'Download documentations'
+  desc 'download (<doc> <doc@version>... | --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 (<doc> <doc>... | --all)', 'Package documentations'
+  desc 'package (<doc> <doc@version>... | --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)

+ 47 - 20
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
 

+ 1 - 1
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}]

+ 51 - 0
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

+ 1 - 1
views/app.erb

@@ -21,7 +21,7 @@
     <div class="_list">
       <% unless @doc %>
       <% App.docs.each do |slug, doc| %>
-      <a href="/<%= slug %>/" class="_list-item _icon-<%= slug %> _list-dir"><span class="_list-arrow"></span><%= doc['name'] %></a>
+      <a href="/<%= slug %>/" class="_list-item"><span class="_list-arrow"></span><%= doc['name'] %></a>
       <% end %>
       <% end %>
     </div>