From 08e807744de17ff8996c80f7dc031c252715e36b Mon Sep 17 00:00:00 2001 From: Eric Dobbs Date: Thu, 23 Dec 2021 09:35:38 -0700 Subject: [PATCH 01/12] add conformance test for current state behavior This is not a complete test of everything state does. We have focused tests around the uses of location.pathname. We will be introducing use of location.hash and want to be sure we know if we brake previous behavior. --- Gruntfile.js | 1 + test/state.coffee | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 test/state.coffee diff --git a/Gruntfile.js b/Gruntfile.js index ebfba6db..486c4555 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -113,6 +113,7 @@ module.exports = function (grunt) { 'test/drop.coffee', 'test/revision.coffee', 'test/resolve.coffee', + 'test/state.coffee', 'test/wiki.coffee' ] } diff --git a/test/state.coffee b/test/state.coffee new file mode 100644 index 00000000..50ba8e48 --- /dev/null +++ b/test/state.coffee @@ -0,0 +1,49 @@ +state = require('../lib/state') +lineup = require('../lib/lineup') +expect = require 'expect.js' + +# here we test manipulations of the lineup and window.location + +describe 'state', -> + tests = [ + # [pathname, locs, pages] + ['/', [], []], + ['/view/welcome-visitors', ['view'], ['welcome-visitors']], + ['/view/foo/view/bar', ['view', 'view'], ['foo', 'bar']], + ['/view/welcome-visitors/fed.wiki.org/featured-sites', + ['view', 'fed.wiki.org'], ['welcome-visitors', 'featured-sites']] + ] + beforeEach -> + global.$ = (el) -> {attr: (key) -> el[key]} + + describe 'urlPages', -> + for [pathname, locs, pages] in tests + it pathname, -> + global.location = {pathname: pathname} + expect(state.urlPages()).to.eql(pages) + + describe 'urlLocs', -> + for [pathname, locs, pages] in tests + it pathname, -> + global.location = {pathname: pathname} + expect(state.urlLocs()).to.eql(locs) + + describe 'setUrl', -> + actual = null + beforeEach -> + actual = null + global.location = {pathname: '/view/welcome-visitors'} + global.document = {title: null} + global.history = + pushState: (x, y, url) -> actual = url + it 'does not push url to history for the same location', -> + state.pagesInDom = -> ['welcome-visitors'] + state.locsInDom = -> ['view'] + state.setUrl() + expect(actual).to.be(null) + it 'pushes url to history when location changes', -> + state.pagesInDom = -> ['welcome-visitors', 'welcome-visitors'] + state.locsInDom = -> ['view', 'fed.wiki.org'] + state.setUrl() + expect(global.document.title).to.be('Wiki') + expect(actual).to.be('/view/welcome-visitors/fed.wiki.org/welcome-visitors') From 9abbfa3831d203a6cb915b06caaf872353f1ccb6 Mon Sep 17 00:00:00 2001 From: Eric Dobbs Date: Fri, 24 Dec 2021 09:52:24 -0700 Subject: [PATCH 02/12] replace use of jquery to get location.pathname --- lib/state.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/state.coffee b/lib/state.coffee index d7306f08..ce0e05f9 100644 --- a/lib/state.coffee +++ b/lib/state.coffee @@ -17,14 +17,14 @@ state.pagesInDom = -> $.makeArray $(".page").map (_, el) -> el.id state.urlPages = -> - (i for i in $(location).attr('pathname').split('/') by 2)[1..] + (i for i in location.pathname.split('/') by 2)[1..] state.locsInDom = -> $.makeArray $(".page").map (_, el) -> $(el).data('site') or 'view' state.urlLocs = -> - (j for j in $(location).attr('pathname').split('/')[1..] by 2) + (j for j in location.pathname.split('/')[1..] by 2) state.setUrl = -> document.title = lineup.bestTitle() @@ -32,7 +32,7 @@ state.setUrl = -> locs = state.locsInDom() pages = state.pagesInDom() url = ("/#{locs?[idx] or 'view'}/#{page}" for page, idx in pages).join('') - unless url is $(location).attr('pathname') + unless url is location.pathname history.pushState(null, null, url) state.debugStates = () -> From 74a6993241308fdaf5a2499a97f84e116406f467 Mon Sep 17 00:00:00 2001 From: Eric Dobbs Date: Fri, 24 Dec 2021 13:03:35 -0700 Subject: [PATCH 03/12] refactor to introduce internal [{site, slug}...] structure We introduce an internal structure to represent the Reader's intent for the lineup. We believe this opens several opportunities. We enables a URL search parameter to provide the intended lineup while preserving support for existing use of URL pathname. Using URL search instead of URL pathname opens the door to statically hosted wikis. (n.b. introducing this additional behavior means this commit is not a pure refactoring) We anticipate this intermediate structure enabling other changes to clarify responsibilities between state and lineup. The {site, slug} invites {site, slug, revision} and could open the door for better support of non-European languages with something like {site, slug?, revision, title} Previous experiments in the DAT/hypercore wiki variant and seran outputs have used the URL hash where this commit chooses to use URL search. One rationale is to avoid collisions in our URL semantics with whatever Google are experimenting with in their recent chrome plugin for links to text fragments. Google use #:~:... to delimit these new fragment links. One other decision hidden in this code is the choice to use the specific search parameter "pathname" to hold the intended lineup. This opens experimenting with other URL structures like the slug@site/slug@site we have in some other experiments. --- lib/state.coffee | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/lib/state.coffee b/lib/state.coffee index ce0e05f9..e137a2c8 100644 --- a/lib/state.coffee +++ b/lib/state.coffee @@ -13,25 +13,48 @@ module.exports = state = {} state.inject = (link_) -> link = link_ +state.fromLocation = (location) -> + # [{site, slug},...] + search = new URLSearchParams(location.search) + pathname = (search.get("pathname") || location.pathname) + .replace(/^\//,'') + toSiteSlug = (acc, item, idx) -> + if idx % 2 == 0 + acc.push({site: item}) + else + acc[acc.length-1].slug = item + acc + pathname.split('/').reduce(toSiteSlug, []) + +state.fromDOM = () -> + # [{site, slug},...] + slugs = state.pagesInDom() + sites = state.locsInDom() + slugs.map (slug, idx) -> + {site: sites[idx], slug: slug} + +state.toURL = (siteSlugs) -> + combine = (url, item) -> "#{url}/#{item.site}/#{item.slug}" + siteSlugs.reduce(combine, "") + state.pagesInDom = -> $.makeArray $(".page").map (_, el) -> el.id state.urlPages = -> - (i for i in location.pathname.split('/') by 2)[1..] + state.fromLocation(location).map((it) -> it.slug) state.locsInDom = -> $.makeArray $(".page").map (_, el) -> $(el).data('site') or 'view' state.urlLocs = -> - (j for j in location.pathname.split('/')[1..] by 2) + state.fromLocation(location).map((it) -> it.site) state.setUrl = -> document.title = lineup.bestTitle() if history and history.pushState - locs = state.locsInDom() - pages = state.pagesInDom() - url = ("/#{locs?[idx] or 'view'}/#{page}" for page, idx in pages).join('') + siteSlugs = state.fromDOM() + url = state.toURL(siteSlugs) unless url is location.pathname history.pushState(null, null, url) @@ -72,4 +95,3 @@ state.first = -> oldPages = state.pagesInDom() for urlPage, idx in firstUrlPages when urlPage not in oldPages link.createPage(urlPage, firstUrlLocs[idx]) unless urlPage is '' - From b545572541b437568c487edd7fd4c66529a0bf81 Mon Sep 17 00:00:00 2001 From: Eric Dobbs Date: Fri, 24 Dec 2021 15:16:25 -0700 Subject: [PATCH 04/12] changed toURL() to a private function & added unchaged() small steps toward making setUrl adapt to both pathname and search --- lib/state.coffee | 14 ++++++++++---- test/state.coffee | 6 +++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/state.coffee b/lib/state.coffee index e137a2c8..4352f69f 100644 --- a/lib/state.coffee +++ b/lib/state.coffee @@ -33,9 +33,15 @@ state.fromDOM = () -> slugs.map (slug, idx) -> {site: sites[idx], slug: slug} -state.toURL = (siteSlugs) -> +toURL = (siteSlugs) -> combine = (url, item) -> "#{url}/#{item.site}/#{item.slug}" - siteSlugs.reduce(combine, "") + new URL(siteSlugs.reduce(combine, ""), location) + +unchanged = (url, location) -> + loc = new URL(location) + url.pathname == loc.pathname and + url.search == loc.search and + url.hash == loc.hash state.pagesInDom = -> $.makeArray $(".page").map (_, el) -> el.id @@ -54,8 +60,8 @@ state.setUrl = -> document.title = lineup.bestTitle() if history and history.pushState siteSlugs = state.fromDOM() - url = state.toURL(siteSlugs) - unless url is location.pathname + url = toURL(siteSlugs) + unless unchanged(url, location) history.pushState(null, null, url) state.debugStates = () -> diff --git a/test/state.coffee b/test/state.coffee index 50ba8e48..2fddbdcc 100644 --- a/test/state.coffee +++ b/test/state.coffee @@ -32,10 +32,10 @@ describe 'state', -> actual = null beforeEach -> actual = null - global.location = {pathname: '/view/welcome-visitors'} + global.location = new URL('https://example.com/view/welcome-visitors') global.document = {title: null} global.history = - pushState: (x, y, url) -> actual = url + pushState: (state, title, url) -> actual = url it 'does not push url to history for the same location', -> state.pagesInDom = -> ['welcome-visitors'] state.locsInDom = -> ['view'] @@ -46,4 +46,4 @@ describe 'state', -> state.locsInDom = -> ['view', 'fed.wiki.org'] state.setUrl() expect(global.document.title).to.be('Wiki') - expect(actual).to.be('/view/welcome-visitors/fed.wiki.org/welcome-visitors') + expect(actual.pathname).to.be('/view/welcome-visitors/fed.wiki.org/welcome-visitors') From 20c1f6142a9cfe68085498417c356c7ae0ac2056 Mon Sep 17 00:00:00 2001 From: Eric Dobbs Date: Fri, 24 Dec 2021 17:07:07 -0700 Subject: [PATCH 05/12] verify same test cases work with URL.search (except setUrl) --- test/state.coffee | 82 ++++++++++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/test/state.coffee b/test/state.coffee index 2fddbdcc..8cd86327 100644 --- a/test/state.coffee +++ b/test/state.coffee @@ -5,6 +5,7 @@ expect = require 'expect.js' # here we test manipulations of the lineup and window.location describe 'state', -> + actual = null tests = [ # [pathname, locs, pages] ['/', [], []], @@ -13,37 +14,60 @@ describe 'state', -> ['/view/welcome-visitors/fed.wiki.org/featured-sites', ['view', 'fed.wiki.org'], ['welcome-visitors', 'featured-sites']] ] - beforeEach -> + before -> global.$ = (el) -> {attr: (key) -> el[key]} + global.history = + pushState: (state, title, url) -> actual = url - describe 'urlPages', -> + context 'using URL.pathname', -> for [pathname, locs, pages] in tests - it pathname, -> - global.location = {pathname: pathname} - expect(state.urlPages()).to.eql(pages) + context pathname, -> + beforeEach -> + global.location = {pathname: pathname} + it "urlPages() is [#{pages}]", -> + expect(state.urlPages()).to.eql(pages) + it "urlLocs() is [#{locs}]", -> + expect(state.urlLocs()).to.eql(locs) + describe 'setUrl', -> + beforeEach -> + actual = null + global.location = new URL('https://example.com/view/welcome-visitors') + global.document = {title: null} + it 'does not push url to history for the same location', -> + state.pagesInDom = -> ['welcome-visitors'] + state.locsInDom = -> ['view'] + state.setUrl() + expect(actual).to.be(null) + it 'pushes url to history when location changes', -> + state.pagesInDom = -> ['welcome-visitors', 'welcome-visitors'] + state.locsInDom = -> ['view', 'fed.wiki.org'] + state.setUrl() + expect(global.document.title).to.be('Wiki') + expect(actual.pathname).to.be('/view/welcome-visitors/fed.wiki.org/welcome-visitors') - describe 'urlLocs', -> + context.skip 'using URL.search', -> for [pathname, locs, pages] in tests - it pathname, -> - global.location = {pathname: pathname} - expect(state.urlLocs()).to.eql(locs) - - describe 'setUrl', -> - actual = null - beforeEach -> - actual = null - global.location = new URL('https://example.com/view/welcome-visitors') - global.document = {title: null} - global.history = - pushState: (state, title, url) -> actual = url - it 'does not push url to history for the same location', -> - state.pagesInDom = -> ['welcome-visitors'] - state.locsInDom = -> ['view'] - state.setUrl() - expect(actual).to.be(null) - it 'pushes url to history when location changes', -> - state.pagesInDom = -> ['welcome-visitors', 'welcome-visitors'] - state.locsInDom = -> ['view', 'fed.wiki.org'] - state.setUrl() - expect(global.document.title).to.be('Wiki') - expect(actual.pathname).to.be('/view/welcome-visitors/fed.wiki.org/welcome-visitors') + context pathname, -> + beforeEach -> + global.location = new URL("https://example.com?pathname=#{pathname}") + it "urlPages() is [#{pages}]", -> + expect(state.urlPages()).to.eql(pages) + it "urlLocs() is [#{locs}]", -> + expect(state.urlLocs()).to.eql(locs) + describe 'setUrl', -> + beforeEach -> + actual = null + global.location = new URL('https://example.com?pathname=view/welcome-visitors') + global.document = {title: null} + it 'does not push url to history for the same location', -> + state.pagesInDom = -> ['welcome-visitors'] + state.locsInDom = -> ['view'] + state.setUrl() + expect(actual).to.be(null) + it 'pushes url to history when location changes', -> + state.pagesInDom = -> ['welcome-visitors', 'welcome-visitors'] + state.locsInDom = -> ['view', 'fed.wiki.org'] + state.setUrl() + expect(global.document.title).to.be('Wiki') + expect(actual.searchParams.get('pathname')) + .to.be('/view/welcome-visitors/fed.wiki.org/welcome-visitors') From 8b9076b8611beadbfc7ed6b67857f5a8caef9782 Mon Sep 17 00:00:00 2001 From: Eric Dobbs Date: Fri, 24 Dec 2021 17:29:45 -0700 Subject: [PATCH 06/12] state.setUrl() now sets URL.search if it is present --- lib/state.coffee | 8 +++++++- test/state.coffee | 6 +++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/state.coffee b/lib/state.coffee index 4352f69f..ee7cbd1d 100644 --- a/lib/state.coffee +++ b/lib/state.coffee @@ -35,7 +35,13 @@ state.fromDOM = () -> toURL = (siteSlugs) -> combine = (url, item) -> "#{url}/#{item.site}/#{item.slug}" - new URL(siteSlugs.reduce(combine, ""), location) + pathname = siteSlugs.reduce(combine, "") + url = new URL(location) + if !!url.search + url.search = "pathname=#{pathname.replace(/^\//,'')}" + else + url.pathname = pathname + url unchanged = (url, location) -> loc = new URL(location) diff --git a/test/state.coffee b/test/state.coffee index 8cd86327..772f0714 100644 --- a/test/state.coffee +++ b/test/state.coffee @@ -23,7 +23,7 @@ describe 'state', -> for [pathname, locs, pages] in tests context pathname, -> beforeEach -> - global.location = {pathname: pathname} + global.location = new URL("https://example.com#{pathname}") it "urlPages() is [#{pages}]", -> expect(state.urlPages()).to.eql(pages) it "urlLocs() is [#{locs}]", -> @@ -45,7 +45,7 @@ describe 'state', -> expect(global.document.title).to.be('Wiki') expect(actual.pathname).to.be('/view/welcome-visitors/fed.wiki.org/welcome-visitors') - context.skip 'using URL.search', -> + context 'using URL.search', -> for [pathname, locs, pages] in tests context pathname, -> beforeEach -> @@ -70,4 +70,4 @@ describe 'state', -> state.setUrl() expect(global.document.title).to.be('Wiki') expect(actual.searchParams.get('pathname')) - .to.be('/view/welcome-visitors/fed.wiki.org/welcome-visitors') + .to.be('view/welcome-visitors/fed.wiki.org/welcome-visitors') From 1546c4759c2f7158e4c85d6dfd438dfbe4fde03f Mon Sep 17 00:00:00 2001 From: Eric Dobbs Date: Fri, 24 Dec 2021 17:39:32 -0700 Subject: [PATCH 07/12] state.setUrl() uses URL.search if URL.pathname is / --- lib/state.coffee | 2 +- test/state.coffee | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/state.coffee b/lib/state.coffee index ee7cbd1d..a8f4376f 100644 --- a/lib/state.coffee +++ b/lib/state.coffee @@ -37,7 +37,7 @@ toURL = (siteSlugs) -> combine = (url, item) -> "#{url}/#{item.site}/#{item.slug}" pathname = siteSlugs.reduce(combine, "") url = new URL(location) - if !!url.search + if !!url.search or url.pathname == '/' url.search = "pathname=#{pathname.replace(/^\//,'')}" else url.pathname = pathname diff --git a/test/state.coffee b/test/state.coffee index 772f0714..b64a17c2 100644 --- a/test/state.coffee +++ b/test/state.coffee @@ -71,3 +71,13 @@ describe 'state', -> expect(global.document.title).to.be('Wiki') expect(actual.searchParams.get('pathname')) .to.be('view/welcome-visitors/fed.wiki.org/welcome-visitors') + + it 'setUrl() defaults to URL.search', -> + actual = null + global.location = new URL('https://example.com') + global.document = {title: null} + state.pagesInDom = -> ['welcome-visitors'] + state.locsInDom = -> ['view'] + state.setUrl() + expect(actual.pathname).to.be('/') + expect(actual.searchParams.get('pathname')).to.be('view/welcome-visitors') From 9e924ba074023ee9d95f621210a8556fc19cbb3e Mon Sep 17 00:00:00 2001 From: Eric Dobbs Date: Fri, 24 Dec 2021 16:47:29 -0700 Subject: [PATCH 08/12] test/state needs to stub lineup.bestTitle() when running all tests --- test/state.coffee | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/state.coffee b/test/state.coffee index b64a17c2..03d944db 100644 --- a/test/state.coffee +++ b/test/state.coffee @@ -6,6 +6,7 @@ expect = require 'expect.js' describe 'state', -> actual = null + title = null tests = [ # [pathname, locs, pages] ['/', [], []], @@ -18,6 +19,7 @@ describe 'state', -> global.$ = (el) -> {attr: (key) -> el[key]} global.history = pushState: (state, title, url) -> actual = url + lineup.bestTitle = () -> title context 'using URL.pathname', -> for [pathname, locs, pages] in tests @@ -31,6 +33,7 @@ describe 'state', -> describe 'setUrl', -> beforeEach -> actual = null + title = 'Welcome Visitors' global.location = new URL('https://example.com/view/welcome-visitors') global.document = {title: null} it 'does not push url to history for the same location', -> @@ -42,7 +45,7 @@ describe 'state', -> state.pagesInDom = -> ['welcome-visitors', 'welcome-visitors'] state.locsInDom = -> ['view', 'fed.wiki.org'] state.setUrl() - expect(global.document.title).to.be('Wiki') + expect(global.document.title).to.be('Welcome Visitors') expect(actual.pathname).to.be('/view/welcome-visitors/fed.wiki.org/welcome-visitors') context 'using URL.search', -> @@ -57,6 +60,7 @@ describe 'state', -> describe 'setUrl', -> beforeEach -> actual = null + title = 'Welcome Visitors' global.location = new URL('https://example.com?pathname=view/welcome-visitors') global.document = {title: null} it 'does not push url to history for the same location', -> @@ -68,7 +72,7 @@ describe 'state', -> state.pagesInDom = -> ['welcome-visitors', 'welcome-visitors'] state.locsInDom = -> ['view', 'fed.wiki.org'] state.setUrl() - expect(global.document.title).to.be('Wiki') + expect(global.document.title).to.be('Welcome Visitors') expect(actual.searchParams.get('pathname')) .to.be('view/welcome-visitors/fed.wiki.org/welcome-visitors') From 5a60bbb2c3500cc1cd835f892a00f0c1c95da95e Mon Sep 17 00:00:00 2001 From: Eric Dobbs Date: Tue, 28 Dec 2021 10:22:22 -0700 Subject: [PATCH 09/12] use location.hash instead of location.search This commit revisits a decision described in commit 4ea7ab1. We previously chose location.search partly to avoid collisions with Google Chrome's experiments with links to text fragments. We have learned that wiki-server replies with 302 redirects. In this commit we choose to use location.hash to allow us to release this functionality without requiring changes to wiki-server. --- lib/state.coffee | 10 +++++----- test/state.coffee | 14 ++++++++------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/state.coffee b/lib/state.coffee index a8f4376f..792b4d4b 100644 --- a/lib/state.coffee +++ b/lib/state.coffee @@ -15,8 +15,8 @@ state.inject = (link_) -> state.fromLocation = (location) -> # [{site, slug},...] - search = new URLSearchParams(location.search) - pathname = (search.get("pathname") || location.pathname) + hash = new URLSearchParams(location.hash.substring(1)) + pathname = (hash.get("pathname") || location.pathname) .replace(/^\//,'') toSiteSlug = (acc, item, idx) -> if idx % 2 == 0 @@ -37,8 +37,8 @@ toURL = (siteSlugs) -> combine = (url, item) -> "#{url}/#{item.site}/#{item.slug}" pathname = siteSlugs.reduce(combine, "") url = new URL(location) - if !!url.search or url.pathname == '/' - url.search = "pathname=#{pathname.replace(/^\//,'')}" + if !!url.hash or url.pathname == '/' + url.hash = "pathname=#{pathname.replace(/^\//,'')}" else url.pathname = pathname url @@ -80,7 +80,7 @@ state.show = (e) -> oldLocs = state.locsInDom() newLocs = state.urlLocs() - return if (!location.pathname or location.pathname is '/') + return if !location.hash and (!location.pathname or location.pathname is '/') matching = true for name, idx in oldPages diff --git a/test/state.coffee b/test/state.coffee index 03d944db..ce089dc6 100644 --- a/test/state.coffee +++ b/test/state.coffee @@ -48,11 +48,11 @@ describe 'state', -> expect(global.document.title).to.be('Welcome Visitors') expect(actual.pathname).to.be('/view/welcome-visitors/fed.wiki.org/welcome-visitors') - context 'using URL.search', -> + context 'using URL.hash', -> for [pathname, locs, pages] in tests context pathname, -> beforeEach -> - global.location = new URL("https://example.com?pathname=#{pathname}") + global.location = new URL("https://example.com#pathname=#{pathname}") it "urlPages() is [#{pages}]", -> expect(state.urlPages()).to.eql(pages) it "urlLocs() is [#{locs}]", -> @@ -61,7 +61,7 @@ describe 'state', -> beforeEach -> actual = null title = 'Welcome Visitors' - global.location = new URL('https://example.com?pathname=view/welcome-visitors') + global.location = new URL('https://example.com#pathname=view/welcome-visitors') global.document = {title: null} it 'does not push url to history for the same location', -> state.pagesInDom = -> ['welcome-visitors'] @@ -73,10 +73,11 @@ describe 'state', -> state.locsInDom = -> ['view', 'fed.wiki.org'] state.setUrl() expect(global.document.title).to.be('Welcome Visitors') - expect(actual.searchParams.get('pathname')) + params = new URLSearchParams(actual.hash.substring(1)) + expect(params.get('pathname')) .to.be('view/welcome-visitors/fed.wiki.org/welcome-visitors') - it 'setUrl() defaults to URL.search', -> + it 'setUrl() defaults to URL.hash', -> actual = null global.location = new URL('https://example.com') global.document = {title: null} @@ -84,4 +85,5 @@ describe 'state', -> state.locsInDom = -> ['view'] state.setUrl() expect(actual.pathname).to.be('/') - expect(actual.searchParams.get('pathname')).to.be('view/welcome-visitors') + params = new URLSearchParams(actual.hash.substring(1)) + expect(params.get('pathname')).to.be('view/welcome-visitors') From 04e4c2943ceed396d1f5816441bafa53f70c9fe7 Mon Sep 17 00:00:00 2001 From: Eric Dobbs Date: Tue, 28 Dec 2021 10:32:03 -0700 Subject: [PATCH 10/12] state.fromLocation() now understands welcome-visitors.html This commit enables wiki-client to support a statically hosted wiki while maintaining its support for dynamically hosted node-based wikis. We hope to find time to follow this commit with some refactoring. In particular, state.syncDomWithLocation() is hacking the DOM of the html page early in the bootstrapping of the client, undoing or redoing some of the work wiki-server does for us. This will unfortunately increase the confusion in the wiki bootstrapping. There are details in the client's bootstrapping that need disentangling between wiki.coffee, legacy.coffee, and state.coffee. Some changes will also be wanted in wiki-server. What we get in exchange for this increase in confusion is the ability for authors to host wikis in github pages or in similar static web sites. We believe this paves the way toward easier wiki hosting. --- lib/legacy.coffee | 2 ++ lib/state.coffee | 17 ++++++++++++++++- test/state.coffee | 4 +++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/legacy.coffee b/lib/legacy.coffee index 494dd3c8..79d9eb41 100644 --- a/lib/legacy.coffee +++ b/lib/legacy.coffee @@ -38,6 +38,8 @@ wiki.origin.get 'system/factories.json', (error, data) -> preLoadEditors data $ -> + state.syncDomWithLocation() + dialog.emit() # FUNCTIONS used by plugins and elsewhere diff --git a/lib/state.coffee b/lib/state.coffee index 792b4d4b..29a0e662 100644 --- a/lib/state.coffee +++ b/lib/state.coffee @@ -24,7 +24,13 @@ state.fromLocation = (location) -> else acc[acc.length-1].slug = item acc - pathname.split('/').reduce(toSiteSlug, []) + parts = pathname.split('/') + if parts.length % 2 == 0 + parts.reduce(toSiteSlug, []) + else if parts.length == 1 and parts[0].endsWith(".html") + [{site: "view", slug: parts[0].replace(/\.html$/,'')}] + else + [{site: "view", slug: "welcome-visitors"}] state.fromDOM = () -> # [{site, slug},...] @@ -107,3 +113,12 @@ state.first = -> oldPages = state.pagesInDom() for urlPage, idx in firstUrlPages when urlPage not in oldPages link.createPage(urlPage, firstUrlLocs[idx]) unless urlPage is '' + +state.syncDomWithLocation = -> + main = "" + for {site, slug} in state.fromLocation(location) + dataSite = "" + if site != "view" + dataSite = """data-site="#{site}" """ + main += """
\n""" + $('section.main').html(main) diff --git a/test/state.coffee b/test/state.coffee index ce089dc6..0812f0d5 100644 --- a/test/state.coffee +++ b/test/state.coffee @@ -13,7 +13,9 @@ describe 'state', -> ['/view/welcome-visitors', ['view'], ['welcome-visitors']], ['/view/foo/view/bar', ['view', 'view'], ['foo', 'bar']], ['/view/welcome-visitors/fed.wiki.org/featured-sites', - ['view', 'fed.wiki.org'], ['welcome-visitors', 'featured-sites']] + ['view', 'fed.wiki.org'], ['welcome-visitors', 'featured-sites']], + ['/welcome-visitors.html', ['view'], ['welcome-visitors']], + ['/how-to-wiki.html', ['view'], ['how-to-wiki']] ] before -> global.$ = (el) -> {attr: (key) -> el[key]} From 76158e2c35ece911648ac807f19f71346ef74275 Mon Sep 17 00:00:00 2001 From: Eric Dobbs Date: Sat, 1 Jan 2022 15:07:41 -0700 Subject: [PATCH 11/12] clarify names: pathname refers only to url.pathname We choose "stat" for the url.hash parameter. It gestures at "static" which matches original intentions which motivate this collection of changes. Because URLs can get bookmarked or included in links in page content, we expect to have to support this name for the long term. We like that it's both short and a bit opaque. --- lib/state.coffee | 10 +++++----- test/state.coffee | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/state.coffee b/lib/state.coffee index 29a0e662..f1aeef3d 100644 --- a/lib/state.coffee +++ b/lib/state.coffee @@ -16,7 +16,7 @@ state.inject = (link_) -> state.fromLocation = (location) -> # [{site, slug},...] hash = new URLSearchParams(location.hash.substring(1)) - pathname = (hash.get("pathname") || location.pathname) + intent = (hash.get("stat") || location.pathname) .replace(/^\//,'') toSiteSlug = (acc, item, idx) -> if idx % 2 == 0 @@ -24,7 +24,7 @@ state.fromLocation = (location) -> else acc[acc.length-1].slug = item acc - parts = pathname.split('/') + parts = intent.split('/') if parts.length % 2 == 0 parts.reduce(toSiteSlug, []) else if parts.length == 1 and parts[0].endsWith(".html") @@ -41,12 +41,12 @@ state.fromDOM = () -> toURL = (siteSlugs) -> combine = (url, item) -> "#{url}/#{item.site}/#{item.slug}" - pathname = siteSlugs.reduce(combine, "") + intent = siteSlugs.reduce(combine, "") url = new URL(location) if !!url.hash or url.pathname == '/' - url.hash = "pathname=#{pathname.replace(/^\//,'')}" + url.hash = "stat=#{intent.replace(/^\//,'')}" else - url.pathname = pathname + url.pathname = intent url unchanged = (url, location) -> diff --git a/test/state.coffee b/test/state.coffee index 0812f0d5..39d08708 100644 --- a/test/state.coffee +++ b/test/state.coffee @@ -51,10 +51,10 @@ describe 'state', -> expect(actual.pathname).to.be('/view/welcome-visitors/fed.wiki.org/welcome-visitors') context 'using URL.hash', -> - for [pathname, locs, pages] in tests - context pathname, -> + for [stat, locs, pages] in tests + context stat, -> beforeEach -> - global.location = new URL("https://example.com#pathname=#{pathname}") + global.location = new URL("https://example.com#stat=#{stat}") it "urlPages() is [#{pages}]", -> expect(state.urlPages()).to.eql(pages) it "urlLocs() is [#{locs}]", -> @@ -63,7 +63,7 @@ describe 'state', -> beforeEach -> actual = null title = 'Welcome Visitors' - global.location = new URL('https://example.com#pathname=view/welcome-visitors') + global.location = new URL('https://example.com#stat=view/welcome-visitors') global.document = {title: null} it 'does not push url to history for the same location', -> state.pagesInDom = -> ['welcome-visitors'] @@ -76,7 +76,7 @@ describe 'state', -> state.setUrl() expect(global.document.title).to.be('Welcome Visitors') params = new URLSearchParams(actual.hash.substring(1)) - expect(params.get('pathname')) + expect(params.get('stat')) .to.be('view/welcome-visitors/fed.wiki.org/welcome-visitors') it 'setUrl() defaults to URL.hash', -> @@ -88,4 +88,4 @@ describe 'state', -> state.setUrl() expect(actual.pathname).to.be('/') params = new URLSearchParams(actual.hash.substring(1)) - expect(params.get('pathname')).to.be('view/welcome-visitors') + expect(params.get('stat')).to.be('view/welcome-visitors') From 41252bf5e08055ad5fb8a3ec9ef2f021d2d33d81 Mon Sep 17 00:00:00 2001 From: Eric Dobbs Date: Sat, 18 Mar 2023 19:53:36 -0600 Subject: [PATCH 12/12] rename parameter from stat to sfw On returning to this pull request over a year later, the name stat seems awkward. We now think SFW, the shorthand for the original Smallest Federated Wiki, is a better name given the origins of this url schema. We have also sorted the methods to put fromLocation() and toURL() next to one another to clarify their inverse relationship. The former consumes a URL and emits an array of siteSlugs. The latter consumes an array of siteSlugs and emits a URL. --- lib/state.coffee | 18 +++++++++--------- test/state.coffee | 12 ++++++------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/state.coffee b/lib/state.coffee index f1aeef3d..62aa2044 100644 --- a/lib/state.coffee +++ b/lib/state.coffee @@ -13,10 +13,17 @@ module.exports = state = {} state.inject = (link_) -> link = link_ +state.fromDOM = () -> + # [{site, slug},...] + slugs = state.pagesInDom() + sites = state.locsInDom() + slugs.map (slug, idx) -> + {site: sites[idx], slug: slug} + state.fromLocation = (location) -> # [{site, slug},...] hash = new URLSearchParams(location.hash.substring(1)) - intent = (hash.get("stat") || location.pathname) + intent = (hash.get("sfw") || location.pathname) .replace(/^\//,'') toSiteSlug = (acc, item, idx) -> if idx % 2 == 0 @@ -32,19 +39,12 @@ state.fromLocation = (location) -> else [{site: "view", slug: "welcome-visitors"}] -state.fromDOM = () -> - # [{site, slug},...] - slugs = state.pagesInDom() - sites = state.locsInDom() - slugs.map (slug, idx) -> - {site: sites[idx], slug: slug} - toURL = (siteSlugs) -> combine = (url, item) -> "#{url}/#{item.site}/#{item.slug}" intent = siteSlugs.reduce(combine, "") url = new URL(location) if !!url.hash or url.pathname == '/' - url.hash = "stat=#{intent.replace(/^\//,'')}" + url.hash = "sfw=#{intent.replace(/^\//,'')}" else url.pathname = intent url diff --git a/test/state.coffee b/test/state.coffee index 39d08708..6f4519c4 100644 --- a/test/state.coffee +++ b/test/state.coffee @@ -51,10 +51,10 @@ describe 'state', -> expect(actual.pathname).to.be('/view/welcome-visitors/fed.wiki.org/welcome-visitors') context 'using URL.hash', -> - for [stat, locs, pages] in tests - context stat, -> + for [sfw, locs, pages] in tests + context sfw, -> beforeEach -> - global.location = new URL("https://example.com#stat=#{stat}") + global.location = new URL("https://example.com#sfw=#{sfw}") it "urlPages() is [#{pages}]", -> expect(state.urlPages()).to.eql(pages) it "urlLocs() is [#{locs}]", -> @@ -63,7 +63,7 @@ describe 'state', -> beforeEach -> actual = null title = 'Welcome Visitors' - global.location = new URL('https://example.com#stat=view/welcome-visitors') + global.location = new URL('https://example.com#sfw=view/welcome-visitors') global.document = {title: null} it 'does not push url to history for the same location', -> state.pagesInDom = -> ['welcome-visitors'] @@ -76,7 +76,7 @@ describe 'state', -> state.setUrl() expect(global.document.title).to.be('Welcome Visitors') params = new URLSearchParams(actual.hash.substring(1)) - expect(params.get('stat')) + expect(params.get('sfw')) .to.be('view/welcome-visitors/fed.wiki.org/welcome-visitors') it 'setUrl() defaults to URL.hash', -> @@ -88,4 +88,4 @@ describe 'state', -> state.setUrl() expect(actual.pathname).to.be('/') params = new URLSearchParams(actual.hash.substring(1)) - expect(params.get('stat')).to.be('view/welcome-visitors') + expect(params.get('sfw')).to.be('view/welcome-visitors')