Skip to content

Commit

Permalink
Merge pull request #518 from marp-team/bespoke-template-stable-anchor
Browse files Browse the repository at this point in the history
Support stable anchor links in `bespoke` template
  • Loading branch information
yhatt authored Apr 16, 2023
2 parents 6315550 + ce6df0a commit b2fe7c6
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- Stable anchor link support in `bespoke` template ([#518](https://github.com/marp-team/marp-cli/pull/518))
- Support [Puppeteer's new headless mode](https://developer.chrome.com/articles/new-headless/) by `PUPPETEER_HEADLESS_MODE=new` env ([#508](https://github.com/marp-team/marp-cli/pull/508))

### Changed
Expand Down
2 changes: 2 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/* eslint-env jest */
jest.mock('wrap-ansi')

require('css.escape') // Polyfill for CSS.escape
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"chalk": "^5.2.0",
"cheerio": "^1.0.0-rc.12",
"chrome-launcher": "^0.15.1",
"css.escape": "^1.5.1",
"cssnano": "^6.0.0",
"eslint": "^8.38.0",
"eslint-config-prettier": "^8.8.0",
Expand Down
41 changes: 39 additions & 2 deletions src/templates/bespoke/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,45 @@ const bespokeState = (opts: BespokeStateOption = {}) => {
}

const parseState = (opts: any = { fragment: true }) => {
const page = (coerceInt(location.hash.slice(1)) || 1) - 1
const fragment = opts.fragment ? coerceInt(readQuery('f') || '') : null
let fragment = opts.fragment ? coerceInt(readQuery('f') || '') : null

const page = (() => {
if (location.hash) {
// Support text fragments: https://web.dev/text-fragments/
const [hashWithoutDelimiter] = location.hash.slice(1).split(':~:')

const numMatcher = /^\d+$/.test(hashWithoutDelimiter)
if (numMatcher) return (coerceInt(hashWithoutDelimiter) ?? 1) - 1

const anchorTarget =
document.getElementById(hashWithoutDelimiter) ||
document.querySelector(
`a[name="${CSS.escape(hashWithoutDelimiter)}"]`
)

if (anchorTarget) {
const { length } = deck.slides

for (let i = 0; i < length; i += 1) {
if (deck.slides[i].contains(anchorTarget)) {
// Detect the fragmented list in the parent element
const pageFragments = deck.fragments?.[i]
const fragmentElement = anchorTarget.closest(
'[data-marpit-fragment]'
)

if (pageFragments && fragmentElement) {
const fragmentIndex = pageFragments.indexOf(fragmentElement)
if (fragmentIndex >= 0) fragment = fragmentIndex
}

return i
}
}
}
}
return 0
})()

activateSlide(page, fragment)
}
Expand Down
6 changes: 6 additions & 0 deletions test/_browser/viewTransition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ beforeEach(() => {
})
})

afterEach(() => {
if ('startViewTransition' in document) {
delete document.startViewTransition
}
})

export const skipTransition = jest.fn()

export const ViewTransition = Object.seal({ skipTransition })
75 changes: 66 additions & 9 deletions test/templates/bespoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,26 @@ describe("Bespoke template's browser context", () => {
const render = (
md = defaultMarkdown,
targetDocument = document,
{ resetHead = true }: { resetHead?: boolean } = {}
{
html: enableHtml = false,
resetHead = true,
}: { html?: boolean; resetHead?: boolean } = {}
): HTMLElement => {
if (resetHead) targetDocument.head.innerHTML = ''

let { html, comments } = marp.render(md) // eslint-disable-line prefer-const
const ret = marp.render(md, { html: enableHtml })

comments.forEach((c, i) => {
if (c.length > 0)
html = `${html}<div class="bespoke-marp-note" data-index="${i}"><p>${c.join(
ret.comments.forEach((c, i) => {
if (c.length > 0) {
ret.html = `${
ret.html
}<div class="bespoke-marp-note" data-index="${i}"><p>${c.join(
'\n\n'
)}</p></div>`
}
})

targetDocument.body.innerHTML = html
targetDocument.body.innerHTML = ret.html
return targetDocument.getElementById(':$p')! // eslint-disable-line @typescript-eslint/no-non-null-assertion
}

Expand Down Expand Up @@ -1097,7 +1103,7 @@ describe("Bespoke template's browser context", () => {
})

describe('State', () => {
it('activates specified page by hash index', () => {
it('activates specific page by hash with the page number', () => {
history.replaceState(null, document.title, '#2')

render()
Expand All @@ -1106,14 +1112,48 @@ describe("Bespoke template's browser context", () => {

expect(deck.slide()).toBe(1)

// Navigate by anchor
// Navigate by anchor with the page number
history.replaceState(null, document.title, '#3')
window.dispatchEvent(new HashChangeEvent('hashchange'))

expect(deck.slide()).toBe(2)
})

it('activates specified fragment state by "f" query param', () => {
it('activates specific page by hash with the anchor ID', () => {
history.replaceState(null, document.title, '#2')
render('# Page 1\n\n---\n\n# Page 2\n\n---\n\n# Page 3')

const deck = bespoke()
jest.runAllTimers()

expect(deck.slide()).toBe(1)

// Navigate by anchor with the slugified anchor ID
history.replaceState(null, document.title, '#page-3')
window.dispatchEvent(new HashChangeEvent('hashchange'))

expect(deck.slide()).toBe(2)
})

it('activates specific page by hash with the anchor link', () => {
history.replaceState(null, document.title, '')

render('a\n\n---\n\nb\n\n---\n\nc <a name="anchor">named</a>', document, {
html: true,
})

const deck = bespoke()
jest.runAllTimers()
expect(deck.slide()).toBe(0)

// Navigate by anchor with the anchor link
history.replaceState(null, document.title, '#anchor')
window.dispatchEvent(new HashChangeEvent('hashchange'))

expect(deck.slide()).toBe(2)
})

it('activates specific fragment state by "f" query param', () => {
history.replaceState(null, document.title, '?f=1')

render('* a\n* b\n\n---\n\n* a\n* b\n* c')
Expand Down Expand Up @@ -1154,6 +1194,23 @@ describe("Bespoke template's browser context", () => {
)
).toHaveLength(1)
})

it('activates specific fragment state by hash with the anchor ID when the element is included in the fragmented list', () => {
history.replaceState(null, document.title, '?f=0')

render('* # a\n* # b\n* # c')
const deck = bespoke()
jest.runAllTimers()

expect(deck.slide()).toBe(0)
expect(deck.fragmentIndex).toBe(0)

// Navigate by anchor with the slugified anchor ID that is included in the element within the fragmented list
history.replaceState(null, document.title, '#b')
window.dispatchEvent(new HashChangeEvent('hashchange'))

expect(deck.fragmentIndex).toBe(2)
})
})

describe('Sync', () => {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2816,6 +2816,11 @@ css-what@^6.0.1, css-what@^6.1.0:
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==

css.escape@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb"
integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==

cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
Expand Down

0 comments on commit b2fe7c6

Please sign in to comment.