Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions assets/js/swup.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,32 @@ const maybeMetaRedirect = (visit, {page}) => {

window.addEventListener('DOMContentLoaded', emitExdocLoaded)

// Match links to local .html documentation pages, with or without a fragment.
// Routing non-HTML files (downloads such as .mmd) through SWUP breaks the page
// swap (#2182); the `.html#` branch keeps in-doc anchored links (function
// references like file.html#list_dir/1, sidebar entries) on the SWUP path.
export const LINK_SELECTOR = 'a[href]:not([href^="/"]):not([href^="http"]):is([href$=".html"], [href*=".html#"])'

// SWUP only swaps `#main`, so it must stay within a single ExDoc build: the
// sidebar (navigation + version) is built from per-build <head> scripts SWUP
// does not re-run. ExDoc writes every page of a build as a flat file in one
// folder, so an in-build link is a bare filename and a slash in its path means
// another folder/build (the Erlang/OTP docs flatten one build per application).
// We test the path -- the part before the fragment -- not the whole href, since
// anchors carry slashes too (the arity in file.html#list_dir/1).
export const isWithinBuild = (href) => !href.split('#')[0].includes('/')

if (!isEmbedded && window.location.protocol !== 'file:') {
new Swup({
animationSelector: false,
containers: ['#main'],
ignoreVisit: (url) => {
ignoreVisit: (url, {el} = {}) => {
const path = url.split('#')[0]
return path === window.location.pathname ||
path === window.location.pathname + '.html'
path === window.location.pathname + '.html' ||
!isWithinBuild(el?.getAttribute('href') ?? '')
},
linkSelector: 'a[href]:not([href^="/"]):not([href^="http"])[href$=".html"]',
linkSelector: LINK_SELECTOR,
hooks: {
'page:load': maybeMetaRedirect,
'page:view': emitExdocLoaded
Expand Down
42 changes: 42 additions & 0 deletions assets/test/swup.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { el } from '../js/helpers'
import { LINK_SELECTOR, isWithinBuild } from '../js/swup'

const linkMatches = (href) => el('a', {href}).matches(LINK_SELECTOR)

describe('swup', () => {
describe('LINK_SELECTOR', () => {
it('matches local .html pages, with or without an anchor', () => {
expect(linkMatches('Foo.html')).toBe(true)
expect(linkMatches('Foo.Bar.html')).toBe(true)
expect(linkMatches('file.html#section')).toBe(true)
// Anchors carry slashes (name/arity); the selector must still match them.
expect(linkMatches('file.html#list_dir/1')).toBe(true)
})

it('ignores non-HTML files (#2182), absolute and external links', () => {
expect(linkMatches('ecto_erd.mmd')).toBe(false)
expect(linkMatches('image.png')).toBe(false)
expect(linkMatches('/Foo.html')).toBe(false)
expect(linkMatches('http://example.com/Foo.html')).toBe(false)
expect(linkMatches('https://example.com/Foo.html')).toBe(false)
})

it('does not match bare same-page anchors (scrolled natively, no SWUP)', () => {
expect(linkMatches('#section')).toBe(false)
})
})

describe('isWithinBuild', () => {
it('is true for a same-folder link (bare filename)', () => {
expect(isWithinBuild('Foo.html')).toBe(true)
expect(isWithinBuild('file.html#section')).toBe(true)
// A slash in the fragment (name/arity) does not leave the build.
expect(isWithinBuild('file.html#list_dir/1')).toBe(true)
})

it('is false for a link into another folder/build', () => {
expect(isWithinBuild('apps/kernel/file.html')).toBe(false)
expect(isWithinBuild('../stdlib/lists.html#foo/1')).toBe(false)
})
})
})
Loading
Loading