63 lines
3.4 KiB
JavaScript
63 lines
3.4 KiB
JavaScript
// Read each slide's declared ANCHOR from the deck source — with Slidev's own parser.
|
|
//
|
|
// An anchor lets a link land ON a slide instead of at the top of the deck: the glossary
|
|
// tooltip over `NCL` goes to the Nickel slide, not to the cover. Without one the link
|
|
// cannot be written at all, and until now it could not: the slide ids were assigned by
|
|
// deck.js at runtime, so the served HTML carried none and the browser resolved the hash
|
|
// before they existed.
|
|
//
|
|
// WHY THE ANCHOR LIVES IN THE SLIDE'S FRONTMATTER, and not in the registry that links to it:
|
|
// a `#slide-7` is a POSITIONAL pointer into a document that reorders. Insert one slide and
|
|
// every link points one slide off — at 200, in silence, with nothing to fail. That is the
|
|
// drift this whole projection exists to refuse. Declared in the slide, the anchor travels
|
|
// with it when it moves, and dies loudly with it when it is deleted (see the gate).
|
|
//
|
|
// WHY SLIDEV'S PARSER AND NOT A `---` SPLIT: this file's neighbours say it plainly — the deck
|
|
// is not re-parsed, because Slidev's markdown is not standard markdown. A `---` inside a code
|
|
// fence would silently shift every anchor onto the wrong slide, which is the exact failure the
|
|
// anchors exist to prevent. Slidev already ships the parser that draws these boundaries; the
|
|
// only correct number of implementations of it is one, and it is not this one.
|
|
import { createRequire } from 'node:module'
|
|
import { realpathSync } from 'node:fs'
|
|
import { readFile } from 'node:fs/promises'
|
|
import { resolve } from 'node:path'
|
|
import { PRESENTATION } from './paths.mjs'
|
|
|
|
// pnpm links @slidev/parser under the CLI, never at the top level, and createRequire does not
|
|
// resolve the symlink for you: pointed at the link path it looks for a node_modules that is not
|
|
// there. Realpath first, and resolution happens in the tree that actually has the package.
|
|
const parserPromise = (async () => {
|
|
const cli = realpathSync(resolve(PRESENTATION, 'node_modules/@slidev/cli/package.json'))
|
|
return import(createRequire(cli).resolve('@slidev/parser'))
|
|
})()
|
|
|
|
// The anchor is a URL fragment, so it must survive being one: lowercase, no spaces, no accents.
|
|
// Rejected here rather than normalised — an anchor that is not what the author typed is an
|
|
// anchor the author cannot link to from memory, and they would find out in the browser.
|
|
const SHAPE = /^[a-z0-9][a-z0-9-]*$/
|
|
|
|
/**
|
|
* @param {string} source filename relative to outreach/presentation/
|
|
* @returns {Promise<Array<string|null>>} one entry per slide, index-parallel to the capture
|
|
*/
|
|
export async function readAnchors(source) {
|
|
const { parse } = await parserPromise
|
|
const file = resolve(PRESENTATION, source)
|
|
const data = await parse(await readFile(file, 'utf8'), file)
|
|
|
|
const seen = new Map()
|
|
return data.slides.map((s, i) => {
|
|
const a = s.frontmatter?.anchor
|
|
if (a == null || a === '') return null
|
|
if (typeof a !== 'string' || !SHAPE.test(a)) {
|
|
throw new Error(`${source} slide ${i + 1}: anchor "${a}" no vale como fragmento de URL — minúsculas, dígitos y guiones (empezando por letra o dígito)`)
|
|
}
|
|
// Two slides claiming one anchor is a link with two destinations: the browser takes the
|
|
// first and the author never learns which one they were pointing at.
|
|
if (seen.has(a)) {
|
|
throw new Error(`${source}: el ancla "${a}" está declarada en las slides ${seen.get(a) + 1} y ${i + 1} — un ancla nombra UNA slide`)
|
|
}
|
|
seen.set(a, i)
|
|
return a
|
|
})
|
|
}
|