From 344dbf6351653db0c8816d0e8abe7e13cbe8d63e Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 4 Nov 2017 07:35:37 -0400 Subject: [PATCH] Convert external links to other enabled docs to internal links --- assets/javascripts/app/app.coffee | 2 + assets/javascripts/app/urls.coffee | 31 ++++++++++ assets/javascripts/models/entry.coffee | 2 +- .../views/content/entry_page.coffee | 12 +++- lib/docs/core/models/entry.rb | 14 +++-- lib/docs/filters/core/entries.rb | 12 +++- test/lib/docs/core/doc_test.rb | 4 +- test/lib/docs/core/entry_index_test.rb | 20 +++---- test/lib/docs/core/models/entry_test.rb | 59 +++++++++++++------ test/lib/docs/filters/core/entries_test.rb | 15 ++++- 10 files changed, 131 insertions(+), 40 deletions(-) create mode 100644 assets/javascripts/app/urls.coffee diff --git a/assets/javascripts/app/app.coffee b/assets/javascripts/app/app.coffee index 979ea35362..c337ac481b 100644 --- a/assets/javascripts/app/app.coffee +++ b/assets/javascripts/app/app.coffee @@ -25,6 +25,7 @@ @shortcuts = new app.Shortcuts @document = new app.views.Document @mobile = new app.views.Mobile if @isMobile() + @urls = new app.Urls if document.body.hasAttribute('data-doc') @DOC = JSON.parse(document.body.getAttribute('data-doc')) @@ -112,6 +113,7 @@ initDoc: (doc) -> doc.entries.add type.toEntry() for type in doc.types.all() @entries.add doc.entries.all() + @urls.addDoc doc return migrateDocs: -> diff --git a/assets/javascripts/app/urls.coffee b/assets/javascripts/app/urls.coffee new file mode 100644 index 0000000000..5521680cec --- /dev/null +++ b/assets/javascripts/app/urls.coffee @@ -0,0 +1,31 @@ +class app.Urls + _map: {} + _cached: {} + + get: (url) -> + return unless url + parsed = new URL(url) + @_cached?[parsed.host]?[_keyFor(parsed)] + + addDoc: (doc) -> + @_map[doc.slug] = doc.entries + .all() + .filter (entry) -> entry.url + .map (entry) -> [entry.url, entry.path] + @rebuild() + + removeDoc: (doc) -> + deleted = delete @_map[doc.slug] + @rebuild() if deleted + delted + + rebuild: -> + @_cached = {} + for slug, entries of @_map + for [url, path] in entries + parsed = new URL(url) + @_cached[parsed.host] ?= {} + @_cached[parsed.host][_keyFor(parsed)] = "/#{slug}/#{path}" + + +_keyFor = (parsed) -> (parsed.pathname + parsed.search + parsed.hash).toLowerCase() diff --git a/assets/javascripts/models/entry.coffee b/assets/javascripts/models/entry.coffee index a8b2dd0160..ce9c6dce51 100644 --- a/assets/javascripts/models/entry.coffee +++ b/assets/javascripts/models/entry.coffee @@ -1,7 +1,7 @@ #= require app/searcher class app.models.Entry extends app.Model - # Attributes: name, type, path + # Attributes: name, type, path, url constructor: -> super diff --git a/assets/javascripts/views/content/entry_page.coffee b/assets/javascripts/views/content/entry_page.coffee index beae4d775a..e0b2a38e68 100644 --- a/assets/javascripts/views/content/entry_page.coffee +++ b/assets/javascripts/views/content/entry_page.coffee @@ -34,7 +34,9 @@ class app.views.EntryPage extends app.View $.batchUpdate @el, => @subview.render(content, fromCache) - @addCopyButtons() unless fromCache + unless fromCache + @addCopyButtons() + @internalizeLinks() return if app.disabledDocs.findBy 'slug', @entry.doc.slug @@ -55,6 +57,14 @@ class app.views.EntryPage extends app.View el.appendChild @copyButton.cloneNode(true) for el in @findAllByTag('pre') return + internalizeLinks: -> + for el in @findAllByTag('a') + continue if el.classList.contains '_attribution-link' + + internalUrl = app.urls.get(el.href) + if internalUrl? + el.href = internalUrl + polyfillMathML: -> return unless window.supportsMathML is false and !@polyfilledMathML and @findByTag('math') @polyfilledMathML = true diff --git a/lib/docs/core/models/entry.rb b/lib/docs/core/models/entry.rb index c3f5464c61..2190320a86 100644 --- a/lib/docs/core/models/entry.rb +++ b/lib/docs/core/models/entry.rb @@ -4,22 +4,24 @@ module Docs class Entry class Invalid < StandardError; end - attr_accessor :name, :type, :path + attr_accessor :name, :type, :path, :url - def initialize(name = nil, path = nil, type = nil) + def initialize(name = nil, path = nil, type = nil, url = nil) self.name = name self.path = path self.type = type + self.url = url unless root? raise Invalid, 'missing name' if !name || name.empty? raise Invalid, 'missing path' if !path || path.empty? raise Invalid, 'missing type' if !type || type.empty? + raise Invalid, 'missing url' if !url || url.empty? end end def ==(other) - other.name == name && other.path == path && other.type == type + other.name == name && other.path == path && other.type == type && other.url == url end def name=(value) @@ -30,12 +32,16 @@ def type=(value) @type = value.try :strip end + def url=(value) + @url = value.try :strip + end + def root? path == 'index' end def as_json - { name: name, path: path, type: type } + { name: name, path: path, type: type, url: url } end end end diff --git a/lib/docs/filters/core/entries.rb b/lib/docs/filters/core/entries.rb index 43af4095a6..954ce59a2b 100644 --- a/lib/docs/filters/core/entries.rb +++ b/lib/docs/filters/core/entries.rb @@ -56,8 +56,16 @@ def build_entries(entries) def build_entry(name, frag = nil, type = nil) type ||= self.type - path = frag ? (frag.include?('#') ? frag : "#{self.path}##{frag}") : self.path - Entry.new(name, path, type) + if frag.present? && frag.include?('#') + path = frag + # TODO: What should `url` get changed to? + url = current_url + else + hash_frag = frag ? "##{frag}" : '' + path = "#{self.path}#{hash_frag}" + url = "#{current_url}#{hash_frag}" + end + Entry.new(name, path, type, url) end end end diff --git a/test/lib/docs/core/doc_test.rb b/test/lib/docs/core/doc_test.rb index ebf44664c5..25f7be0e3e 100644 --- a/test/lib/docs/core/doc_test.rb +++ b/test/lib/docs/core/doc_test.rb @@ -14,7 +14,7 @@ class DocsDocTest < MiniTest::Spec end let :entry do - Docs::Entry.new 'name', 'path', 'type' + Docs::Entry.new 'name', 'path', 'type', 'url' end let :store do @@ -274,7 +274,7 @@ class DocsDocTest < MiniTest::Spec mock(store).write('index.json', anything) do |path, json| json = JSON.parse(json) assert_equal pages.length, json['entries'].length - assert_includes json['entries'], Docs::Entry.new('one', 'path', 'type').as_json.stringify_keys + assert_includes json['entries'], Docs::Entry.new('one', 'path', 'type', 'url').as_json.stringify_keys end doc.store_pages(store) end diff --git a/test/lib/docs/core/entry_index_test.rb b/test/lib/docs/core/entry_index_test.rb index 0f8b0b187d..7419b3d6bc 100644 --- a/test/lib/docs/core/entry_index_test.rb +++ b/test/lib/docs/core/entry_index_test.rb @@ -3,7 +3,7 @@ class DocsEntryIndexTest < MiniTest::Spec let :entry do - Docs::Entry.new 'name', 'path', 'type' + Docs::Entry.new 'name', 'path', 'type', 'url' end let :index do @@ -30,8 +30,8 @@ class DocsEntryIndexTest < MiniTest::Spec it "stores an array of entries" do entries = [ - Docs::Entry.new('one', 'path', 'type'), - Docs::Entry.new('two', 'path', 'type') + Docs::Entry.new('one', 'path', 'type', 'url'), + Docs::Entry.new('two', 'path', 'type', 'url') ] index.add(entries) @@ -56,9 +56,9 @@ class DocsEntryIndexTest < MiniTest::Spec end it "creates and indexes the type" do - index.add Docs::Entry.new('one', 'path', 'a') - index.add Docs::Entry.new('two', 'path', 'b') - index.add Docs::Entry.new('three', 'path', 'b') + index.add Docs::Entry.new('one', 'path', 'a', 'url') + index.add Docs::Entry.new('two', 'path', 'b', 'url') + index.add Docs::Entry.new('three', 'path', 'b', 'url') assert_equal ['a', 'b'], index.types.keys assert_instance_of Docs::Type, index.types['a'] end @@ -69,8 +69,8 @@ class DocsEntryIndexTest < MiniTest::Spec end it "increments the type's count" do - index.add Docs::Entry.new('one', 'path', 'type') - index.add Docs::Entry.new('two', 'path', 'type') + index.add Docs::Entry.new('one', 'path', 'type', 'url') + index.add Docs::Entry.new('two', 'path', 'type', 'url') assert_equal 2, index.types['type'].count end end @@ -101,8 +101,8 @@ class DocsEntryIndexTest < MiniTest::Spec end it "includes the json representation of the #entries" do - index.add one = Docs::Entry.new('one', 'path', 'type') - index.add two = Docs::Entry.new('two', 'path', 'type') + index.add one = Docs::Entry.new('one', 'path', 'type', 'url') + index.add two = Docs::Entry.new('two', 'path', 'type', 'url') assert_equal [one.as_json, two.as_json], index.as_json[:entries] end diff --git a/test/lib/docs/core/models/entry_test.rb b/test/lib/docs/core/models/entry_test.rb index 307cdaac43..bf07896790 100644 --- a/test/lib/docs/core/models/entry_test.rb +++ b/test/lib/docs/core/models/entry_test.rb @@ -5,35 +5,38 @@ class DocsEntryTest < MiniTest::Spec Entry = Docs::Entry let :entry do - Entry.new('name', 'path', 'type') + Entry.new('name', 'path', 'type', 'url') end - def build_entry(name = 'name', path = 'path', type = 'type') - Entry.new(name, path, type) + def build_entry(name = 'name', path = 'path', type = 'type', url = 'url') + Entry.new(name, path, type, url) end describe ".new" do - it "stores #name, #path and #type" do - entry = Entry.new('name', 'path', 'type') + it "stores #name, #path, #type, and #url" do + entry = Entry.new('name', 'path', 'type', 'url') assert_equal 'name', entry.name assert_equal 'path', entry.path assert_equal 'type', entry.type + assert_equal 'url', entry.url end - it "raises an error when #name, #path or #type is nil or empty" do - assert_raises(Docs::Entry::Invalid) { Entry.new(nil, 'path', 'type') } - assert_raises(Docs::Entry::Invalid) { Entry.new('', 'path', 'type') } - assert_raises(Docs::Entry::Invalid) { Entry.new('name', nil, 'type') } - assert_raises(Docs::Entry::Invalid) { Entry.new('name', '', 'type') } - assert_raises(Docs::Entry::Invalid) { Entry.new('name', 'path', nil) } - assert_raises(Docs::Entry::Invalid) { Entry.new('name', 'path', '') } + it "raises an error when #name, #path, #type, or #url is nil or empty" do + assert_raises(Docs::Entry::Invalid) { Entry.new(nil, 'path', 'type', 'url') } + assert_raises(Docs::Entry::Invalid) { Entry.new('', 'path', 'type', 'url') } + assert_raises(Docs::Entry::Invalid) { Entry.new('name', nil, 'type', 'url') } + assert_raises(Docs::Entry::Invalid) { Entry.new('name', '', 'type', 'url') } + assert_raises(Docs::Entry::Invalid) { Entry.new('name', 'path', nil, 'url') } + assert_raises(Docs::Entry::Invalid) { Entry.new('name', 'path', '', 'url') } + assert_raises(Docs::Entry::Invalid) { Entry.new('name', 'path', 'type', nil) } + assert_raises(Docs::Entry::Invalid) { Entry.new('name', 'path', 'type', '') } end it "don't raise an error when #path is 'index' and #name or #type is nil or empty" do - Entry.new(nil, 'index', 'type') - Entry.new('', 'index', 'type') - Entry.new('name', 'index', nil) - Entry.new('name', 'index', '') + Entry.new(nil, 'index', 'type', 'url') + Entry.new('', 'index', 'type', 'url') + Entry.new('name', 'index', nil, 'url') + Entry.new('name', 'index', '', 'url') end end @@ -61,6 +64,19 @@ def build_entry(name = 'name', path = 'path', type = 'type') end end + + describe "#url=" do + it "removes surrounding whitespace" do + entry.url = " \n\rurl " + assert_equal 'url', entry.url + end + + it "accepts nil" do + entry.url = nil + assert_nil entry.url + end + end + describe "#==" do it "returns true when the other has the same name, path and type" do assert_equal build_entry, build_entry @@ -80,6 +96,11 @@ def build_entry(name = 'name', path = 'path', type = 'type') entry.type = 'other_type' refute_equal build_entry, entry end + + it "returns false when the other has a different url" do + entry.url = 'other_url' + refute_equal build_entry, entry + end end describe "#root?" do @@ -96,10 +117,10 @@ def build_entry(name = 'name', path = 'path', type = 'type') describe "#as_json" do it "returns a hash with the name, path and type" do - as_json = Entry.new('name', 'path', 'type').as_json + as_json = Entry.new('name', 'path', 'type', 'url').as_json assert_instance_of Hash, as_json - assert_equal [:name, :path, :type], as_json.keys - assert_equal %w(name path type), as_json.values + assert_equal [:name, :path, :type, :url], as_json.keys + assert_equal %w(name path type url), as_json.values end end end diff --git a/test/lib/docs/filters/core/entries_test.rb b/test/lib/docs/filters/core/entries_test.rb index 0fa843a08d..3af8be2a61 100644 --- a/test/lib/docs/filters/core/entries_test.rb +++ b/test/lib/docs/filters/core/entries_test.rb @@ -14,6 +14,7 @@ class EntriesFilterTest < MiniTest::Spec stub(filter).name { 'name' } stub(filter).path { 'path' } stub(filter).type { 'type' } + stub(filter).current_url { 'url' } end let :entries do @@ -41,10 +42,11 @@ class EntriesFilterTest < MiniTest::Spec end describe "the default entry" do - it "has the #name, #path and #type" do + it "has the #name, #path, #type, and #url" do assert_equal 'name', entries.first.name assert_equal 'path', entries.first.path assert_equal 'type', entries.first.type + assert_equal 'url', entries.first.url end end @@ -67,6 +69,7 @@ class EntriesFilterTest < MiniTest::Spec it "has a path with the given fragment" do stub(filter).additional_entries { [['test', 'frag']] } assert_equal 'path#frag', entries.last.path + assert_equal 'url#frag', entries.last.url end it "has a path with the given path" do @@ -88,6 +91,16 @@ class EntriesFilterTest < MiniTest::Spec stub(filter).additional_entries { [['test', nil, nil]] } assert_equal 'type', entries.last.type end + + it "has a url copied from the current_url property" do + stub(filter).additional_entries { [['test', nil, 'test']] } + assert_equal 'url', entries.last.url + end + + it "appends the fragment to #url" do + stub(filter).additional_entries { [['test', 'hash', 'test']] } + assert_equal 'url#hash', entries.last.url + end end end