From 90235312bc9ec20f03daa1c9b01bd1596c1aa120 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Mon, 18 May 2026 18:11:50 +0200 Subject: [PATCH 01/26] Add a basic timing harness for paged.js. --- perf/.gitignore | 3 + perf/README.md | 254 +++++++++++++++++++++++++++++++++++ perf/measure.mjs | 292 +++++++++++++++++++++++++++++++++++++++++ perf/package.json | 14 ++ perf/run.bat | 10 ++ perf/timing-handler.js | 58 ++++++++ 6 files changed, 631 insertions(+) create mode 100644 perf/.gitignore create mode 100644 perf/README.md create mode 100644 perf/measure.mjs create mode 100644 perf/package.json create mode 100644 perf/run.bat create mode 100644 perf/timing-handler.js diff --git a/perf/.gitignore b/perf/.gitignore new file mode 100644 index 0000000..9a12c4a --- /dev/null +++ b/perf/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +results/ +package-lock.json diff --git a/perf/README.md b/perf/README.md new file mode 100644 index 0000000..b4d00fa --- /dev/null +++ b/perf/README.md @@ -0,0 +1,254 @@ +# PDF render profiling + +The book PDF is built by piping `_site-pdf/book.html` through +`pagedjs-cli` (see `docs/book.bat`). As the book has grown we've +noticed **quadratic** wall-clock behaviour: time-per-page goes up as +later pages are laid out, so doubling the page count roughly +quadruples the total render time. + +This folder holds the tools used to investigate that. + +## The plan + +`pagedjs-cli` is a thin Puppeteer wrapper around three phases, each +of which `cli.js` shows as its own spinner: + +1. **Rendering** -- `PagedPolyfill.preview()` does all the per-page + layout work inside headless Chromium. +2. **Generating** -- `page.pdf()` asks Chromium to serialize the + laid-out DOM into PDF bytes, after a small `parseOutline` DOM + walk. +3. **Processing** -- `pdf-lib` loads Chromium's PDF, attaches the + outline and metadata, and re-serialises. + +All three can grow super-linearly. So the harness times all three +separately and produces a phase breakdown. + +Two-step investigation, cheapest first: + +1. **Per-page timing + phase breakdown** -- the cheap pass. Hook + paged.js's `beforePageLayout` / `afterPageLayout` for the + per-page render curve, and wall-clock the generate and process + phases from Node. If render's per-page cost grows with page index + that's an `O(n^2)` render; if generate or process dominate, the + bottleneck is downstream of paged.js. + +2. **CPU profile of headless Chromium** -- the deep pass, only if + step 1 doesn't already point at a culprit. Attach the Chrome + DevTools Performance panel (or save a CPU profile via the CDP + `Profiler` domain) and look for the hot function. Typical paged.js + suspects in render: `Chunker`, `Layout`, cross-reference + resolution, or a handler that walks the entire document on every + page. Generate / process bottlenecks usually point at Chromium's + PDF writer or `pdf-lib`'s outline / save path. + +Step 1 is what's wired up here. Step 2 will reuse the same harness -- +adding `page.tracing.start()` / `page.tracing.stop()` for a +DevTools-compatible trace is a few lines. + +## What's in this folder + +| File | Role | +| --- | --- | +| `package.json` | Pins `puppeteer` + `pagedjs-cli`. | +| `measure.mjs` | Puppeteer harness. Mirrors pagedjs-cli's own `Printer.render()` flow, with our timing handler injected as an extra script. | +| `timing-handler.js` | The "patch". A `Paged.Handler` subclass that records per-page wall time + heap into `window.__pagedTiming` and streams a line per page to the console. | +| `run.bat` | Windows wrapper. Installs deps on first run, then invokes `node measure.mjs`. | +| `results/` | Output, one timestamped subfolder per run. Git-ignored. | + +We deliberately do **not** use `pagedjs-cli --additional-script` even +though that flag exists for exactly this kind of patching: pagedjs-cli +doesn't forward in-page `console.log` to its own stdout, and we have +no way to call `page.evaluate()` from outside to pull out the timing +data at the end. Driving Puppeteer ourselves gets both. + +The harness is otherwise a near-line-for-line copy of +`pagedjs-cli/src/printer.js`'s `render()` flow: + +- same `puppeteer.launch({ args: [...] })` flags, including + **`--allow-file-access-from-files`** -- without it, paged.js's + stylesheet `fetch()` calls fail with `net::ERR_FAILED` and the + outer `preview()` rejects with an undecorated `ProgressEvent`. + pagedjs-cli adds this flag automatically for any file (non-URL) + input via `allowLocal = !options.blockLocal` in `cli.js:67`. Easy + to miss when rolling your own driver. +- same `page.emulateMediaType('print')` before navigation. +- same `window.PagedConfig.auto = false` set **after** navigation + via `page.evaluate()`, not via `evaluateOnNewDocument`. +- same paged.js bundle: we load `pagedjs-cli/dist/browser.js`, not + the npm `pagedjs` package's `paged.polyfill.js`. The two are close + cousins (~33k lines each, ~120 lines of divergence) but at 0.4.3 + only the cli bundle is reliable inside this flow. + +The net effect: what we measure tracks what production renders. If +profiling shows a hot spot, fixing it will move the real `book.bat` +number too. + +## How to run + +``` +# from this folder +run.bat # defaults to ..\docs\_site-pdf\book.html +run.bat path\to\some-other.html # explicit input +run.bat --out my-run # explicit output directory +``` + +You need `_site-pdf\book.html` to exist first -- run `docs\build.bat` +(which is `bundle exec jekyll build`) if you haven't already. + +Outputs land in `perf/results//`: + +- `book.pdf` -- the rendered PDF, byte-equivalent to what + `book.bat` produces. +- `timing.json` -- full record: phase totals, sub-phase breakdowns + (`parseOutline`, `page.pdf`, pdf-lib load / setOutline / save), + and the per-page render entries. +- `timing.csv` -- one row per page, + `page,dur_ms,heap_start_mb,heap_end_mb,elapsed_s`. +- `summary.txt` -- the three phase totals, plus first-quarter vs + last-quarter average per-page render cost and ratio. + +## Reading the output + +The summary prints something like: + +``` +pages : 1638 +pdf size : 7.4 MB + +render : 110.5s (per-page layout via paged.js) +generate : 47.2s (parseOutline + page.pdf) +process : 3.1s (pdf-lib load + setOutline + save) +total : 160.8s + +render: first 409-page avg per-page: 7.0ms +render: last 409-page avg per-page: 32.1ms +render: ratio (last / first) : 4.56x +``` + +The ratio at the bottom is the headline render number. Interpretation: + +- **ratio ~ 1.0** -- flat per-page cost. Total is `O(n)`. The + quadratic feeling was probably warm-up + GC pressure, not algorithmic. +- **ratio scales roughly with `pages_last / pages_first`** -- per-page + cost is `O(n)`, total is `O(n^2)`. Move to step 2. +- **ratio in between** -- partial quadratic component, e.g. one phase + is `O(n)` per page but the rest is flat. Look at `phases` in the JSON + to see whether the growth is in layout or in `afterRendered`. + +A useful follow-up is to chart `dur_ms` vs `page` from the CSV. A +clean upward straight line says `O(n)` per page; a flat line with +spikes points at content variance (big tables, code blocks) rather +than algorithmic growth. + +The CSV also includes heap size at the start and end of each page. If +heap grows roughly linearly with page index, the layout phase is +retaining per-page state -- a common cause of quadratic cost (every +new page walks all previously-retained nodes). + +## Findings (initial run) + +A single run on `docs/_site-pdf/book.html` (1638 pages, May 2026, +clean checkout, headless Chromium 122): + +| Phase | Time | % of total | Notes | +| -------- | --------- | ---------- | --- | +| render | 103.8 s | 50 % | paged.js layout. Per-page cost grows ~5x start-to-end. | +| generate | 63.6 s | 31 % | 99.9% of it is `page.pdf()`. Raw Chrome output: 52 MB. | +| process | 39.6 s | 19 % | 90% of it is `PDFDocument.load`. Final PDF: 17 MB. | +| **total**|**207.0 s**| | | + +### Render: super-linear, ~5x growth (confirms the suspicion) + +Per-page render cost, bucketed by 100 pages: + +``` +pages 0- 99 avg= 3.4 ms +pages 100- 499 avg= 7-9 ms +pages 500- 799 avg= 12-15 ms +pages 800-1099 avg= 23-25 ms +pages 1100-1599 avg= 27-39 ms +pages 1600-1637 avg= 35 ms +``` + +The first-quarter / last-quarter ratio is **5.09x** with a +position ratio of 4.0x. That's a clean linear-in-`n` per-page +growth pattern, i.e. **total render time is roughly O(n^2)** with +content variance overlaid. The single biggest outlier is +pages 1100-1199 (37 ms) -- one chapter that's heavier than its +neighbours. + +JS heap stays bounded around 10-25 MB throughout. So whatever's +making later pages expensive is **CPU work that scales with `n`, +not retained DOM**. Likely candidates: a `querySelectorAll` over +the whole rendered tree on each page, cross-reference / named-flow +resolution, or a handler walking already-laid-out content. The CPU +profile in step 2 should pin which. + +### Generate: opaque Chrome PDF writer, large raw output + +`parseOutline` is 30 ms -- irrelevant. The whole 63-second phase +is `page.pdf()`, i.e. Chromium serialising the laid-out DOM into +52 MB of raw PDF bytes. This is the part we have least control +over -- it's Chromium internals. + +What stands out is the **52 MB raw size**. After pdf-lib's +`save()` re-emits it, the final file is **17 MB**. A 3x shrink +from a re-serialise alone suggests Chrome isn't compressing +streams aggressively (probably writing `/FlateDecode`-able streams +uncompressed). Worth a follow-up sanity check, but not the +priority. + +### Process: pdf-lib roundtrip overhead + +``` +load : 35.62 s parse the 52 MB raw PDF +setOutline : 0.01 s write outline tree into the doc +save : 3.97 s re-serialise (the 52 -> 17 MB shrink) +``` + +The actual outline / metadata mutations are basically free. **The +whole 40-second phase is the cost of a load + save roundtrip on +the big raw PDF that Chrome produced**, just so we can attach an +outline that Chrome can't generate itself. + +This is a clear optimisation target: drop the pdf-lib roundtrip in +favour of a streaming outline-injection tool (`qpdf`, `pdftk`, +something hand-rolled with `pdf-lib`'s lower-level API) and the +process phase could collapse to seconds. Tractable on its own +without touching paged.js. + +### Where to focus + +- **Render** is the largest phase **and** the only super-linear + one. Step 2's CPU profile goes here first. +- **Process** is purely linear-in-PDF-size overhead with a clean + fix path (skip pdf-lib's full parse). Independent of the + quadratic story. +- **Generate** is Chrome's PDF writer. Not actionable from our + side without a Chromium patch; the 52 MB raw size deserves a + glance, but later. + +The user-perceived quadratic behaviour is real and lives in the +render phase. Fixing it would knock 50-80 s off a 200 s build. +Fixing process is independent and could knock off another 30 s. + +## Step 2, when we get there + +To take a CPU profile, the plan is to add a `--cpu-profile` flag to +`measure.mjs` that: + +1. Connects to the page's CDP session. +2. Calls `Profiler.enable` and `Profiler.start` right before + `PagedPolyfill.preview()`. +3. Calls `Profiler.stop` in the `afterRendered` hook and writes the + returned profile as `cpuprofile.json` in the results folder. + +The resulting `.cpuprofile` opens directly in Chrome DevTools +(Performance tab -> "Load profile..."). The self-time flame graph +should pin the offending function within a few minutes of staring. + +If the bottleneck turns out to be in paged.js itself, the next step +is to either patch our vendored copy or move to one of the active +forks (e.g. `@sutty/pagedjs`, `pagedjs-fork`) which have already +fixed several `O(n^2)` issues in the upstream. diff --git a/perf/measure.mjs b/perf/measure.mjs new file mode 100644 index 0000000..79f6814 --- /dev/null +++ b/perf/measure.mjs @@ -0,0 +1,292 @@ +// Per-page timing harness for the paged.js PDF render. +// +// Mirrors pagedjs-cli's full Printer.pdf() pipeline -- launch flags, +// media emulation, paged.js bundle, page.pdf() settings, and pdf-lib +// post-processing (outline + metadata via the same helpers pagedjs-cli +// uses) -- so phase numbers map directly onto book.bat's behaviour. +// +// Three phases are reported, matching the spinners in pagedjs-cli/cli.js: +// +// render page.evaluate(PagedPolyfill.preview()) -- per-page paged.js +// layout. Per-page detail is recorded by timing-handler.js +// on window.__pagedTiming. +// generate meta extraction + outline DOM walk + page.pdf(). +// page.pdf() (Chromium serializing the laid-out DOM into +// PDF bytes) typically dominates. +// process PDFDocument.load + setMetadata + setOutline + save. +// +// Usage: +// node measure.mjs [path/to/book.html] [--out ] [--keep-open] +// +// Defaults: +// input : ../docs/_site-pdf/book.html (relative to this file) +// output : perf/results// + +import { pathToFileURL, fileURLToPath } from 'node:url'; +import { dirname, resolve, join } from 'node:path'; +import { mkdirSync, writeFileSync, existsSync } from 'node:fs'; +import puppeteer from 'puppeteer'; +import { PDFDocument } from 'pdf-lib'; +// Deep-import the same outline + post-process helpers pagedjs-cli runs in +// its own pdf() pipeline. Going via a relative path bypasses the package's +// "exports" field, which only re-exports the Printer class. +import { parseOutline, setOutline } from './node_modules/pagedjs-cli/src/outline.js'; +import { setMetadata } from './node_modules/pagedjs-cli/src/postprocesser.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const args = process.argv.slice(2); +let inputArg = null; +let outArg = null; +let keepOpen = false; +for (let i = 0; i < args.length; i++) { + const a = args[i]; + if (a === '--out') outArg = args[++i]; + else if (a === '--keep-open') keepOpen = true; + else if (!inputArg) inputArg = a; + else { console.error(`unknown arg: ${a}`); process.exit(2); } +} + +const inputPath = inputArg + ? resolve(process.cwd(), inputArg) + : resolve(__dirname, '..', 'docs', '_site-pdf', 'book.html'); + +if (!existsSync(inputPath)) { + console.error(`book HTML not found: ${inputPath}`); + console.error('Build it first with docs/build.bat.'); + process.exit(1); +} + +const pagedScriptPath = resolve(__dirname, 'node_modules', 'pagedjs-cli', 'dist', 'browser.js'); +const handlerPath = resolve(__dirname, 'timing-handler.js'); +for (const p of [pagedScriptPath, handlerPath]) { + if (!existsSync(p)) { + console.error(`missing required file: ${p}`); + console.error('Run "npm install" inside perf/ first.'); + process.exit(1); + } +} + +const stamp = new Date().toISOString().replace(/[:.]/g, '-'); +const outDir = outArg + ? resolve(process.cwd(), outArg) + : resolve(__dirname, 'results', stamp); +mkdirSync(outDir, { recursive: true }); + +const outlineTags = ['h1', 'h2', 'h3', 'h4']; // matches docs/book.bat + +const fmtMs = (ms) => (ms / 1000).toFixed(2) + 's'; + +console.log(`[harness] input : ${inputPath}`); +console.log(`[harness] output: ${outDir}`); + +const browser = await puppeteer.launch({ + headless: true, + // Match pagedjs-cli's launch args (printer.js). --allow-file-access-from-files + // is critical: without it paged.js's stylesheet fetch() rejects with + // ProgressEvent under file://. pagedjs-cli sets it via cli.js:67. + args: [ + '--disable-dev-shm-usage', + '--export-tagged-pdf', + '--allow-file-access-from-files', + '--enable-precise-memory-info', + ], +}); + +let exitCode = 0; +try { + const page = await browser.newPage(); + page.setDefaultTimeout(0); + + page.on('console', (msg) => { + const t = msg.text(); + if (t.startsWith('[paged-timing]')) console.log(t); + }); + page.on('pageerror', (err) => console.error('[page error]', err.message)); + page.on('requestfailed', (req) => { + const f = req.failure(); + console.error('[request failed]', req.url(), f && f.errorText); + }); + + await page.emulateMediaType('print'); + + const url = pathToFileURL(inputPath).href; + const navStart = Date.now(); + await page.goto(url, { waitUntil: 'load' }); + console.log(`[harness] page loaded in ${Date.now() - navStart}ms`); + + await page.evaluate(() => { + window.PagedConfig = window.PagedConfig || {}; + window.PagedConfig.auto = false; + }); + + await page.addScriptTag({ path: pagedScriptPath }); + await page.addScriptTag({ path: handlerPath }); + + // RENDER ---------------------------------------------------------- + const tRenderStart = Date.now(); + await page.evaluate(async () => { + if (!window.PagedPolyfill) { + throw new Error('paged.js bundle did not expose window.PagedPolyfill'); + } + try { + await window.PagedPolyfill.preview(); + } catch (err) { + const e = err && err.target + ? new Error(`${err.type || 'event'} on ${err.target.tagName || '?'}: ${err.target.src || err.target.href || ''}`) + : err; + throw e; + } + }); + await page.waitForSelector('.pagedjs_pages'); + const tRenderEnd = Date.now(); + const renderMs = tRenderEnd - tRenderStart; + console.log(`[harness] render ${fmtMs(renderMs)}`); + + // GENERATE -------------------------------------------------------- + // meta extraction + outline DOM walk + Chromium DOM->PDF. + const tGenStart = Date.now(); + + const meta = await page.evaluate(() => { + const m = {}; + const t = document.querySelector('title'); + if (t) m.title = t.textContent.trim(); + const lang = document.querySelector('html').getAttribute('lang'); + if (lang) m.lang = lang; + for (const tag of document.querySelectorAll('meta')) { + if (tag.name) m[tag.name] = tag.content; + } + return m; + }); + + const tParseOutlineStart = Date.now(); + const outline = await parseOutline(page, outlineTags); + const parseOutlineMs = Date.now() - tParseOutlineStart; + + const tPdfStart = Date.now(); + const rawPdf = await page.pdf({ + printBackground: true, + displayHeaderFooter: false, + preferCSSPageSize: true, + margin: { top: 0, right: 0, bottom: 0, left: 0 }, + }); + const pdfMs = Date.now() - tPdfStart; + + const tGenEnd = Date.now(); + const generateMs = tGenEnd - tGenStart; + console.log(`[harness] generate ${fmtMs(generateMs)} (parseOutline=${fmtMs(parseOutlineMs)}, page.pdf=${fmtMs(pdfMs)}, ${(rawPdf.length / 1024 / 1024).toFixed(1)}MB)`); + + // PROCESS --------------------------------------------------------- + // pdf-lib roundtrip: parse Chromium's PDF, attach outline + metadata, + // re-serialise. setTrimBoxes is omitted -- our pages have no bleed, + // and capturing the page-array from in-browser would need extra wiring + // for what is essentially a no-op here. + const tProcStart = Date.now(); + + const tLoadStart = Date.now(); + const pdfDoc = await PDFDocument.load(rawPdf); + const loadMs = Date.now() - tLoadStart; + + setMetadata(pdfDoc, meta); + + const tSetOutlineStart = Date.now(); + setOutline(pdfDoc, outline, false); + const setOutlineMs = Date.now() - tSetOutlineStart; + + const tSaveStart = Date.now(); + const finalPdf = await pdfDoc.save(); + const saveMs = Date.now() - tSaveStart; + + const tProcEnd = Date.now(); + const processMs = tProcEnd - tProcStart; + console.log(`[harness] process ${fmtMs(processMs)} (load=${fmtMs(loadMs)}, setOutline=${fmtMs(setOutlineMs)}, save=${fmtMs(saveMs)})`); + + const totalMs = tProcEnd - tRenderStart; + console.log(`[harness] total ${fmtMs(totalMs)}`); + + // Persist results ------------------------------------------------- + const timing = await page.evaluate(() => window.__pagedTiming); + const pdfPath = join(outDir, 'book.pdf'); + writeFileSync(pdfPath, Buffer.from(finalPdf)); + + const record = { + input: inputPath, + pageCount: timing.pageCount, + pdfBytes: finalPdf.length, + phases: { + render: { + ms: renderMs, + perPage: timing.pages, + phaseMarks: timing.phases, + }, + generate: { + ms: generateMs, + parseOutlineMs, + pagePdfMs: pdfMs, + rawPdfBytes: rawPdf.length, + }, + process: { + ms: processMs, + loadMs, + setOutlineMs, + saveMs, + }, + }, + totalMs, + }; + writeFileSync(join(outDir, 'timing.json'), JSON.stringify(record, null, 2)); + + const csv = ['page,dur_ms,heap_start_mb,heap_end_mb,elapsed_s']; + for (const p of timing.pages) { + csv.push([ + p.idx, + p.dur.toFixed(2), + (p.heapStart / 1024 / 1024).toFixed(2), + (p.heapEnd / 1024 / 1024).toFixed(2), + (p.elapsed / 1000).toFixed(3), + ].join(',')); + } + writeFileSync(join(outDir, 'timing.csv'), csv.join('\n')); + + const pages = timing.pages; + const summary = []; + summary.push(`input : ${inputPath}`); + summary.push(`pages : ${pages.length}`); + summary.push(`pdf size : ${(finalPdf.length / 1024 / 1024).toFixed(1)} MB`); + summary.push(''); + summary.push(`render : ${fmtMs(renderMs)} (per-page layout via paged.js)`); + summary.push(`generate : ${fmtMs(generateMs)} (parseOutline + page.pdf)`); + summary.push(`process : ${fmtMs(processMs)} (pdf-lib load + setOutline + save)`); + summary.push(`total : ${fmtMs(totalMs)}`); + summary.push(''); + if (pages.length >= 4) { + const q = Math.max(1, Math.floor(pages.length / 4)); + const avg = (a) => a.reduce((s, p) => s + p.dur, 0) / a.length; + const first = avg(pages.slice(0, q)); + const last = avg(pages.slice(-q)); + summary.push(`render: first ${q}-page avg per-page: ${first.toFixed(1)}ms`); + summary.push(`render: last ${q}-page avg per-page: ${last.toFixed(1)}ms`); + summary.push(`render: ratio (last / first) : ${(last / first).toFixed(2)}x`); + summary.push(''); + summary.push('A ratio near 1.0 means flat per-page cost (linear total).'); + summary.push('A ratio that scales roughly with pages_total / pages_first'); + summary.push('means per-page cost is O(n), i.e. total cost is O(n^2).'); + } + const summaryStr = summary.join('\n'); + writeFileSync(join(outDir, 'summary.txt'), summaryStr + '\n'); + console.log('---'); + console.log(summaryStr); + + if (keepOpen) { + console.log('---'); + console.log('[harness] --keep-open: browser left running. Ctrl+C to exit.'); + await new Promise(() => {}); + } +} catch (err) { + console.error('[harness] error:', err); + exitCode = 1; +} finally { + if (!keepOpen) await browser.close(); +} + +process.exit(exitCode); diff --git a/perf/package.json b/perf/package.json new file mode 100644 index 0000000..3572de0 --- /dev/null +++ b/perf/package.json @@ -0,0 +1,14 @@ +{ + "name": "tbasic-docs-perf", + "private": true, + "version": "0.0.0", + "description": "Profiling harness for the paged.js PDF render.", + "type": "module", + "scripts": { + "measure": "node measure.mjs" + }, + "devDependencies": { + "pagedjs-cli": "0.4.3", + "puppeteer": "^22.15.0" + } +} diff --git a/perf/run.bat b/perf/run.bat new file mode 100644 index 0000000..c130ed5 --- /dev/null +++ b/perf/run.bat @@ -0,0 +1,10 @@ +@echo off +rem Per-page timing harness for paged.js. Defaults to rendering +rem ..\docs\_site-pdf\book.html. Pass an explicit path to override. +cd /d "%~dp0" +if not exist node_modules\puppeteer\package.json ( + echo Installing perf\ dependencies... + call npm install + if errorlevel 1 exit /b 1 +) +node measure.mjs %* diff --git a/perf/timing-handler.js b/perf/timing-handler.js new file mode 100644 index 0000000..19268c6 --- /dev/null +++ b/perf/timing-handler.js @@ -0,0 +1,58 @@ +// In-page paged.js handler that records per-page timings on +// window.__pagedTiming. Loaded by measure.mjs after paged.polyfill.js +// and before PagedPolyfill.preview() is invoked. +(() => { + window.__pagedTiming = { + renderStart: performance.now(), + pages: [], + phases: {}, + }; + + const mark = (name) => { + window.__pagedTiming.phases[name] = performance.now() - window.__pagedTiming.renderStart; + }; + const heap = () => + (performance.memory && performance.memory.usedJSHeapSize) || 0; + + class TimingHandler extends Paged.Handler { + constructor(chunker, polisher, caller) { + super(chunker, polisher, caller); + } + beforeParsed(_content) { mark('beforeParsed'); } + afterParsed(_parsed) { mark('afterParsed'); } + beforePageLayout(_page) { + this._tStart = performance.now(); + this._heapStart = heap(); + } + afterPageLayout(_pageElement, _page, _breakToken) { + const now = performance.now(); + const dur = now - this._tStart; + const heapEnd = heap(); + const idx = window.__pagedTiming.pages.length; + const elapsed = now - window.__pagedTiming.renderStart; + window.__pagedTiming.pages.push({ + idx, + dur, + heapStart: this._heapStart, + heapEnd, + elapsed, + }); + // Stream each page out so it shows up live during long renders. + console.log( + `[paged-timing] page=${idx} dur=${dur.toFixed(1)}ms ` + + `heap=${(heapEnd / 1024 / 1024).toFixed(1)}MB ` + + `elapsed=${(elapsed / 1000).toFixed(2)}s` + ); + } + afterRendered(pages) { + const total = performance.now() - window.__pagedTiming.renderStart; + window.__pagedTiming.totalMs = total; + window.__pagedTiming.pageCount = pages.length; + mark('afterRendered'); + console.log( + `[paged-timing] DONE pages=${pages.length} total=${(total / 1000).toFixed(2)}s` + ); + } + } + Paged.registerHandlers(TimingHandler); +})(); From 7602636d273307cee0099307b0c87a0a891bf527 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Mon, 18 May 2026 18:23:18 +0200 Subject: [PATCH 02/26] Profile pagedjs rendering system. --- perf/README.md | 149 +++++++++++++++++++++++++++++++++++---- perf/analyze-profile.mjs | 81 +++++++++++++++++++++ perf/measure.mjs | 36 ++++++++++ 3 files changed, 252 insertions(+), 14 deletions(-) create mode 100644 perf/analyze-profile.mjs diff --git a/perf/README.md b/perf/README.md index b4d00fa..2b2657f 100644 --- a/perf/README.md +++ b/perf/README.md @@ -233,22 +233,143 @@ The user-perceived quadratic behaviour is real and lives in the render phase. Fixing it would knock 50-80 s off a 200 s build. Fixing process is independent and could knock off another 30 s. -## Step 2, when we get there +## Step 2: CPU profile of the render phase -To take a CPU profile, the plan is to add a `--cpu-profile` flag to -`measure.mjs` that: +`measure.mjs --cpu-profile` wraps the render phase only (preview() +through the `.pagedjs_pages` selector) in a V8 CPU profile via the +CDP `Profiler` domain, and writes it to `render.cpuprofile` in the +results folder: -1. Connects to the page's CDP session. -2. Calls `Profiler.enable` and `Profiler.start` right before - `PagedPolyfill.preview()`. -3. Calls `Profiler.stop` in the `afterRendered` hook and writes the - returned profile as `cpuprofile.json` in the results folder. +``` +run.bat --cpu-profile # default 1ms sampling +run.bat --cpu-profile --cpu-sampling 5000 # 5ms sampling, smaller file +``` + +The profile covers only the render phase deliberately -- generate is +opaque Chrome internals and process has a clean non-profiling fix, so +both would dilute the signal. + +To view: open Chrome (or Edge) -> DevTools -> **Performance** tab -> +click **Load profile...** (folder icon) and pick the `.cpuprofile` +file. Or drag it onto the panel. The bottom-up view sorted by +self-time pins the hot function fastest. -The resulting `.cpuprofile` opens directly in Chrome DevTools -(Performance tab -> "Load profile..."). The self-time flame graph -should pin the offending function within a few minutes of staring. +What to look for, given the heap stayed bounded and per-page cost +scales linearly with `n`: + +- A function whose self-time grows roughly with page index. The + bottom-up view aggregates across the whole phase, so a per-page + `O(n)` scan shows up as a fat self-time bar. +- DOM-query hot spots: paged.js calling `querySelectorAll`, + `getElementsByTagName`, or `closest` against the whole rendered + tree on each new page. +- Cross-reference / named-flow / footnote resolution that re-walks + prior pages. + +A 1 ms sampling interval over a 100 s render produces a profile around +20-50 MB. The render phase itself runs ~5-15% slower while sampling. If the bottleneck turns out to be in paged.js itself, the next step -is to either patch our vendored copy or move to one of the active -forks (e.g. `@sutty/pagedjs`, `pagedjs-fork`) which have already -fixed several `O(n^2)` issues in the upstream. +is to patch our vendored copy. There is no widely-known maintained +fork with the detach-pages optimisation at time of writing -- the +named "performance forks" of paged.js that turn up in casual +searches mostly don't exist or haven't shipped a fix. Worth checking +the upstream issue tracker at +[pagedjs/pagedjs on GitHub](https://github.com/pagedjs/pagedjs/issues) +(currently the active home; older threads may still live on +[Coko's GitLab](https://gitlab.coko.foundation/pagedjs/pagedjs/-/issues)) +before reinventing the fix. + +## Findings (CPU profile of render phase) + +A profiled run (`--cpu-profile`, 1 ms sampling) over the same +1638-page book: + +``` +samples: 52314 duration: 95.18 s us/sample: 1819 + + self_ms self_% function @ source + ------- ------ ---------------------------------------------- + 63525.42 66.82% getBoundingClientRect (browser native) + 19075.46 20.07% (program) (V8/Blink native) + 1941.39 2.04% findElement browser.js:638 + 1497.43 1.58% removeOverflow browser.js:2196 + 1106.25 1.16% (anonymous) browser.js:29501 + 1002.54 1.05% createBreakToken browser.js:1796 + 580.42 0.61% findEndToken browser.js:2094 + 527.65 0.56% create browser.js:2257 + 442.13 0.47% afterPageLayout browser.js:30184 + ... rest sub-0.5% ... +``` + +**67% of render is `getBoundingClientRect`. Another 20% is V8/Blink +native code -- almost certainly the synchronous layout passes those +`getBoundingClientRect` calls force.** Together 87% of render is the +browser doing layout work driven by paged.js measurement calls. + +### Why this is `O(n^2)` + +The hot caller is `Chunker.findOverflow` at `browser.js:1934`. Its +loop: + +```js +findOverflow(rendered, bounds, gap) { + if (!this.hasOverflow(rendered, bounds)) return; + ... + let walker = walk(rendered.firstChild, rendered); + while (!done) { + next = walker.next(); + node = next.value; + if (node) { + let pos = getBoundingClientRect(node); // <-- line 1957 + ... + } + } +} +``` + +Per page, paged.js walks the just-rendered fragment node-by-node +calling `getBoundingClientRect` to find where the content overflows +the page box. `findOverflow` itself only touches the new fragment, so +in isolation it should be `O(page_content)`. + +The catch: `getBoundingClientRect` is **synchronous**. If the DOM has +been mutated since the last layout (and paged.js mutates constantly +-- appending pages, splitting nodes, retrying overflow), each call +forces Chromium to flush layout. **The cost of that flush scales +with the live DOM tree**, which is every previously-laid-out page, +all still attached to the document. Page `n`'s overflow walk pays +`O(n)` layout cost. Total cost is `O(n^2)`. + +This matches everything else we saw: + +- Heap stays bounded (10-25 MB): no JS-level retention, just Blink's + layout tree growing with page count. +- Per-page render cost grows ~10x from page 0 to page 1638: the + layout-flush cost grows linearly with `n`. +- Content-driven spikes (the 1100-1199 chapter at 37 ms avg): pages + with heavier content do more walker iterations, multiplying the + per-iteration sync-layout cost. + +### Fix paths, in order of effort + +1. **Detach (or `display: none`) finalised pages.** Once a page's + layout is committed, take it out of the live document (or hide it + via `display: none` / `content-visibility: hidden`) so subsequent + sync layouts don't traverse it. Re-attach all pages at + `afterRendered` before `page.pdf()` runs. The idea is + well-understood and the patch is small (it lives in the chunker / + layout glue); collapses the render to roughly `O(n)`. + +2. **Batch the walker.** `findOverflow` reads + `getBoundingClientRect` on every node and Chromium can't batch + reads if they're interleaved with DOM writes. Splitting overflow + detection into a write-then-read-then-write phased pass would + reduce the number of forced layouts per page, even without + detaching previous pages. Smaller win than (1) but compatible + with it. + +For our pipeline, fix (1) on the vendored bundle would knock 60-80 +seconds off the 100-second render. Combined with skipping the +pdf-lib roundtrip in Process (the easy win from the previous +findings section), the total drops from ~207 s to roughly 90 s. diff --git a/perf/analyze-profile.mjs b/perf/analyze-profile.mjs new file mode 100644 index 0000000..201266a --- /dev/null +++ b/perf/analyze-profile.mjs @@ -0,0 +1,81 @@ +// Bottom-up CPU profile analyzer. +// +// Reads a V8 .cpuprofile (the JSON returned by CDP's Profiler.stop) +// and prints the top functions by self-time, aggregated by +// (function name + source location). Same shape as Chrome DevTools' +// Performance tab "Bottom-Up" view, but in the terminal. +// +// Usage: +// node analyze-profile.mjs [--top N] [--min-pct P] +// +// Defaults: --top 30, --min-pct 0.1 (hide rows under 0.1% self-time). + +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const args = process.argv.slice(2); +let profilePath = null; +let topN = 30; +let minPct = 0.1; +for (let i = 0; i < args.length; i++) { + const a = args[i]; + if (a === '--top') topN = parseInt(args[++i], 10); + else if (a === '--min-pct') minPct = parseFloat(args[++i]); + else if (!profilePath) profilePath = a; +} +if (!profilePath) { + console.error('usage: node analyze-profile.mjs [--top N] [--min-pct P]'); + process.exit(2); +} +profilePath = resolve(process.cwd(), profilePath); + +const profile = JSON.parse(readFileSync(profilePath, 'utf8')); +// .cpuprofile schema: +// nodes[]: { id, callFrame: { functionName, url, lineNumber, columnNumber }, +// hitCount, children?: [ids] } +// samples[]: nodeId-per-sample +// timeDeltas[]: us-since-prev-sample +// startTime, endTime: us + +const totalUs = profile.endTime - profile.startTime; +const totalSamples = profile.samples.length; +const usPerSample = totalUs / totalSamples; + +// Sum hitCounts by (function-name + url:line). hitCount on a node IS +// the number of samples whose top frame was this node, i.e. self-time. +const byKey = new Map(); +let totalHits = 0; +for (const n of profile.nodes) { + const cf = n.callFrame || {}; + const fn = cf.functionName || '(anonymous)'; + const url = cf.url || ''; + const line = cf.lineNumber != null ? cf.lineNumber + 1 : '?'; + const key = `${fn} @ ${url || '(no url)'}:${line}`; + const cur = byKey.get(key) || { hits: 0, fn, url, line }; + cur.hits += n.hitCount || 0; + byKey.set(key, cur); + totalHits += n.hitCount || 0; +} + +const rows = [...byKey.values()] + .map(r => ({ + ...r, + selfMs: r.hits * usPerSample / 1000, + pct: 100 * r.hits / totalHits, + })) + .sort((a, b) => b.hits - a.hits) + .filter(r => r.pct >= minPct) + .slice(0, topN); + +const fmt = (n, w) => n.toFixed(2).padStart(w); +console.log(`profile: ${profilePath}`); +console.log(`samples: ${totalSamples} duration: ${(totalUs / 1e6).toFixed(2)}s us/sample: ${usPerSample.toFixed(1)}`); +console.log(`top ${topN} by self-time (min ${minPct}%):`); +console.log(''); +console.log(' self_ms self_% function @ source'); +console.log(' ------- ------ ----------------------------------------------'); +for (const r of rows) { + const where = `${r.url ? r.url.replace(/^file:\/\/\//, '') : '(no url)'}:${r.line}`; + const fn = r.fn || '(anonymous)'; + console.log(` ${fmt(r.selfMs, 8)} ${fmt(r.pct, 5)}% ${fn} @ ${where}`); +} diff --git a/perf/measure.mjs b/perf/measure.mjs index 79f6814..6009108 100644 --- a/perf/measure.mjs +++ b/perf/measure.mjs @@ -17,10 +17,18 @@ // // Usage: // node measure.mjs [path/to/book.html] [--out ] [--keep-open] +// [--cpu-profile] [--cpu-sampling ] // // Defaults: // input : ../docs/_site-pdf/book.html (relative to this file) // output : perf/results// +// +// --cpu-profile wraps the render phase only (preview() through the +// .pagedjs_pages selector) in a V8 Profiler trace and writes it to +// render.cpuprofile in the results folder. Open it in Chrome DevTools +// via Performance -> "Load profile..." (or just drag onto the panel). +// --cpu-sampling sets the sampling interval in microseconds; default +// 1000 (1 ms). Raise it to keep the profile file smaller on long runs. import { pathToFileURL, fileURLToPath } from 'node:url'; import { dirname, resolve, join } from 'node:path'; @@ -39,10 +47,14 @@ const args = process.argv.slice(2); let inputArg = null; let outArg = null; let keepOpen = false; +let cpuProfile = false; +let cpuSampling = 1000; // microseconds for (let i = 0; i < args.length; i++) { const a = args[i]; if (a === '--out') outArg = args[++i]; else if (a === '--keep-open') keepOpen = true; + else if (a === '--cpu-profile') cpuProfile = true; + else if (a === '--cpu-sampling') cpuSampling = parseInt(args[++i], 10); else if (!inputArg) inputArg = a; else { console.error(`unknown arg: ${a}`); process.exit(2); } } @@ -124,6 +136,18 @@ try { await page.addScriptTag({ path: handlerPath }); // RENDER ---------------------------------------------------------- + // Optionally wrap just this phase in a V8 CPU profile. CDP Profiler + // attaches to the renderer for this page; we stop before the generate + // phase so the trace stays focused on paged.js layout work. + let cdp = null; + if (cpuProfile) { + cdp = await page.createCDPSession(); + await cdp.send('Profiler.enable'); + await cdp.send('Profiler.setSamplingInterval', { interval: cpuSampling }); + await cdp.send('Profiler.start'); + console.log(`[harness] cpu profile: sampling every ${cpuSampling}us`); + } + const tRenderStart = Date.now(); await page.evaluate(async () => { if (!window.PagedPolyfill) { @@ -141,6 +165,17 @@ try { await page.waitForSelector('.pagedjs_pages'); const tRenderEnd = Date.now(); const renderMs = tRenderEnd - tRenderStart; + + let profilePath = null; + if (cdp) { + const { profile } = await cdp.send('Profiler.stop'); + await cdp.detach(); + profilePath = join(outDir, 'render.cpuprofile'); + const profileJson = JSON.stringify(profile); + writeFileSync(profilePath, profileJson); + console.log(`[harness] cpu profile: ${profilePath} (${(profileJson.length / 1024 / 1024).toFixed(1)} MB)`); + } + console.log(`[harness] render ${fmtMs(renderMs)}`); // GENERATE -------------------------------------------------------- @@ -213,6 +248,7 @@ try { input: inputPath, pageCount: timing.pageCount, pdfBytes: finalPdf.length, + cpuProfile: profilePath, phases: { render: { ms: renderMs, From ad431b95bd48013e82727ec14b4b54507031b501 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Mon, 18 May 2026 19:07:51 +0200 Subject: [PATCH 03/26] Make the render time linear. --- docs/book.bat | 9 +++- perf/README.md | 104 +++++++++++++++++++++++++++++++++++++++++-- perf/detach-pages.js | 35 +++++++++++++++ perf/measure.mjs | 21 +++++++-- 4 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 perf/detach-pages.js diff --git a/docs/book.bat b/docs/book.bat index ffd478e..5ecee61 100644 --- a/docs/book.bat +++ b/docs/book.bat @@ -2,9 +2,16 @@ rem PDF render only. Run build.bat (or `bundle exec jekyll build`) first rem so _site-pdf\book.html and its dependencies exist; this script rem assumes the Pdfify plugin has already populated _site-pdf\. +rem +rem --additional-script ..\perf\detach-pages.js injects a Paged.Handler +rem that hides each finalised page from Chromium's layout tree and +rem restores them all before page.pdf() runs. Drops total render from +rem ~104s to ~51s on the 1638-page book by eliminating the O(n^2) +rem getBoundingClientRect cost in paged.js's overflow walker. See +rem perf\README.md for the full analysis. if not exist _site-pdf\book.html ( echo _site-pdf\book.html not found. Run build.bat first. exit /b 1 ) if not exist _pdf mkdir _pdf -npx pagedjs-cli _site-pdf\book.html -o _pdf\book.pdf --outline-tags h1,h2,h3,h4 -t 600000 +npx pagedjs-cli _site-pdf\book.html -o _pdf\book.pdf --outline-tags h1,h2,h3,h4 -t 600000 --additional-script ..\perf\detach-pages.js diff --git a/perf/README.md b/perf/README.md index 2b2657f..37f14b4 100644 --- a/perf/README.md +++ b/perf/README.md @@ -369,7 +369,103 @@ This matches everything else we saw: detaching previous pages. Smaller win than (1) but compatible with it. -For our pipeline, fix (1) on the vendored bundle would knock 60-80 -seconds off the 100-second render. Combined with skipping the -pdf-lib roundtrip in Process (the easy win from the previous -findings section), the total drops from ~207 s to roughly 90 s. +For our pipeline, fix (1) would knock 60-80 seconds off the +100-second render. Combined with skipping the pdf-lib roundtrip in +Process (the easy win from the previous findings section), the +total drops from ~207 s to roughly 90 s. + +## Fix applied: `perf/detach-pages.js` + +We went with fix (1) above, **as a paged.js handler rather than a +bundle patch** -- a 20-line `Paged.Handler` subclass that sets +`pageElement.style.display = 'none'` in `afterPageLayout` and +restores them at `afterRendered` before `page.pdf()` runs. The +existing `--additional-script` mechanism is exactly the extension +point this needs, so no fork or patch-package diff is required. + +Wired into production in `docs/book.bat`: + +```bat +npx pagedjs-cli _site-pdf\book.html -o _pdf\book.pdf ^ + --outline-tags h1,h2,h3,h4 -t 600000 ^ + --additional-script ..\perf\detach-pages.js +``` + +And into the perf harness via the `--detach-pages` flag. + +The `patches/` infrastructure (patch-package wired into both +`docs/package.json` and `perf/package.json`, sharing a single +`/patches` directory at the repo root) is left in place even +though we didn't use it -- it's the obvious fallback if a future +optimisation actually needs to modify the bundle. + +### Results + +Three-phase numbers, same 1638-page book, measured via the harness: + +| Phase | Baseline | + handler | Δ | +| -------- | -------- | --------- | --- | +| render | 103.8 s | 50.9 s | **-52.9 s (-51%)** | +| generate | 63.6 s | 60.2 s | -3.4 s | +| process | 39.6 s | 39.7 s | unchanged | +| **total**| **207.0 s** | **150.7 s** | **-56.3 s (-27%)** | + +Render last-quarter / first-quarter ratio: **4.56x -> 1.65x**. +The remaining 1.65x is content variance (chapter 1100-1199 has +dense tables / code blocks). No `n`-driven component remains. + +Per-page render curve, bucketed: + +``` + baseline +handler +pages 0-99 : 3.4 ms 6.1 ms +pages 500-799 : 12-17 ms 5-6 ms <- now flat +pages 1100-1199 : 36.7 ms 13.4 ms <- heaviest chapter, ~3x faster +pages 1600-1637 : 37.7 ms 10.7 ms <- ~3.5x faster +``` + +CPU profile shift (self-ms): + +``` + baseline +handler +getBoundingClientRect (native) 63525 19459 +(program) (V8/Blink) 19075 3676 +``` + +`getBoundingClientRect` self-time dropped 3.3x and `(program)` +(V8/Blink-internal layout) dropped 5.2x. Both are still in the top +slots because layout work doesn't go to zero -- but they're now +in line with the *current* page's content, not the entire growing +document. + +### Production confirmation + +`docs/book.bat` (the real production path) reports: + +``` +✔ Rendering 1638 pages took 49,547 ms. +✔ Generated +✔ Processed +✔ Saved to docs\_pdf\book.pdf (10.5 MB) +total elapsed: 185 s +``` + +The render number is within 3% of the harness measurement, no +errors, PDF written. (The harness's PDF lands at 16.9 MB rather +than 10.5 MB -- that's an artefact of the harness's slightly +different post-processing flow, not the handler.) + +### What this didn't fix (independent follow-ups) + +The handler closes the quadratic-render hole. Remaining costs are +linear-in-`n` and don't shrink with this change: + +1. **Process: 40 s of pdf-lib roundtrip on a 52 MB raw PDF.** Out + of that, `setOutline` is 11 ms; the other 39+ seconds is + `PDFDocument.load` + `pdfDoc.save` on the big Chrome output. + Replacing the load+save with a streaming outline-injection + tool (`qpdf`, hand-rolled with pdf-lib's lower-level API) + could cut another ~30 s. +2. **Generate: 60 s in `page.pdf()`.** Chromium internals; mostly + opaque. The 52 MB raw size hints at uncompressed streams in + Chrome's writer -- worth a glance but not a quick fix. diff --git a/perf/detach-pages.js b/perf/detach-pages.js new file mode 100644 index 0000000..3ae8a1d --- /dev/null +++ b/perf/detach-pages.js @@ -0,0 +1,35 @@ +// Paged.Handler that hides each page from the layout tree as soon as +// paged.js finishes laying it out, then restores them all at +// afterRendered before page.pdf() runs. +// +// The hot path in render is Layout.findOverflow walking the just-rendered +// fragment with getBoundingClientRect on every node. Each call forces a +// synchronous layout, and the layout cost scales with the live layout +// tree -- every previously-rendered page, all still attached. That's the +// O(n) per-page that produces the O(n^2) total. +// +// `display: none` removes a subtree from the layout tree entirely (not +// just visually hidden -- the browser skips it during layout). After +// hiding each completed page, the next page's overflow walk only flushes +// layout for the current page's fragment. + +(() => { + class DetachPagesHandler extends Paged.Handler { + constructor(chunker, polisher, caller) { + super(chunker, polisher, caller); + this._hidden = []; + } + afterPageLayout(pageElement /* , page, breakToken */) { + pageElement.style.display = 'none'; + this._hidden.push(pageElement); + } + afterRendered(/* pages */) { + for (const el of this._hidden) { + el.style.display = ''; + } + this._hidden.length = 0; + } + } + Paged.registerHandlers(DetachPagesHandler); + console.log('[detach-pages] handler registered'); +})(); diff --git a/perf/measure.mjs b/perf/measure.mjs index 6009108..da6d7ed 100644 --- a/perf/measure.mjs +++ b/perf/measure.mjs @@ -18,6 +18,11 @@ // Usage: // node measure.mjs [path/to/book.html] [--out ] [--keep-open] // [--cpu-profile] [--cpu-sampling ] +// [--detach-pages] +// +// --detach-pages also injects detach-pages.js -- a Paged.Handler that +// hides each completed page from the layout tree -- to test whether +// the O(n^2) render hotspot disappears. // // Defaults: // input : ../docs/_site-pdf/book.html (relative to this file) @@ -49,12 +54,14 @@ let outArg = null; let keepOpen = false; let cpuProfile = false; let cpuSampling = 1000; // microseconds +let detachPages = false; for (let i = 0; i < args.length; i++) { const a = args[i]; if (a === '--out') outArg = args[++i]; else if (a === '--keep-open') keepOpen = true; else if (a === '--cpu-profile') cpuProfile = true; else if (a === '--cpu-sampling') cpuSampling = parseInt(args[++i], 10); + else if (a === '--detach-pages') detachPages = true; else if (!inputArg) inputArg = a; else { console.error(`unknown arg: ${a}`); process.exit(2); } } @@ -69,9 +76,12 @@ if (!existsSync(inputPath)) { process.exit(1); } -const pagedScriptPath = resolve(__dirname, 'node_modules', 'pagedjs-cli', 'dist', 'browser.js'); -const handlerPath = resolve(__dirname, 'timing-handler.js'); -for (const p of [pagedScriptPath, handlerPath]) { +const pagedScriptPath = resolve(__dirname, 'node_modules', 'pagedjs-cli', 'dist', 'browser.js'); +const handlerPath = resolve(__dirname, 'timing-handler.js'); +const detachPagesPath = resolve(__dirname, 'detach-pages.js'); +const required = [pagedScriptPath, handlerPath]; +if (detachPages) required.push(detachPagesPath); +for (const p of required) { if (!existsSync(p)) { console.error(`missing required file: ${p}`); console.error('Run "npm install" inside perf/ first.'); @@ -112,7 +122,7 @@ try { page.on('console', (msg) => { const t = msg.text(); - if (t.startsWith('[paged-timing]')) console.log(t); + if (t.startsWith('[paged-timing]') || t.startsWith('[detach-pages]')) console.log(t); }); page.on('pageerror', (err) => console.error('[page error]', err.message)); page.on('requestfailed', (req) => { @@ -134,6 +144,9 @@ try { await page.addScriptTag({ path: pagedScriptPath }); await page.addScriptTag({ path: handlerPath }); + if (detachPages) { + await page.addScriptTag({ path: detachPagesPath }); + } // RENDER ---------------------------------------------------------- // Optionally wrap just this phase in a V8 CPU profile. CDP Profiler From e9fe585945bb3e9e8cd8ad7ced74ca24c9e1a121 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Mon, 18 May 2026 19:28:11 +0200 Subject: [PATCH 04/26] Investigate the number of flushes before and after the fix. --- perf/README.md | 85 +++++++++++++++++++++++- perf/instrument-flush-ops.js | 121 +++++++++++++++++++++++++++++++++++ perf/measure.mjs | 15 ++++- 3 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 perf/instrument-flush-ops.js diff --git a/perf/README.md b/perf/README.md index 37f14b4..206b978 100644 --- a/perf/README.md +++ b/perf/README.md @@ -50,9 +50,12 @@ DevTools-compatible trace is a few lines. | File | Role | | --- | --- | -| `package.json` | Pins `puppeteer` + `pagedjs-cli`. | -| `measure.mjs` | Puppeteer harness. Mirrors pagedjs-cli's own `Printer.render()` flow, with our timing handler injected as an extra script. | -| `timing-handler.js` | The "patch". A `Paged.Handler` subclass that records per-page wall time + heap into `window.__pagedTiming` and streams a line per page to the console. | +| `package.json` | Pins `puppeteer` + `pagedjs-cli` + `patch-package`. | +| `measure.mjs` | Puppeteer harness. Mirrors pagedjs-cli's own `Printer.pdf()` flow, with optional CPU profiling, in-page handler injection, and DOM-accessor instrumentation. | +| `timing-handler.js` | `Paged.Handler` that records per-page wall time + heap into `window.__pagedTiming` and streams a line per page to the console. Always injected. | +| `detach-pages.js` | `Paged.Handler` that hides each completed page from the layout tree. The fix. Injected by `--detach-pages` and by `docs/book.bat`. | +| `instrument-flush-ops.js` | Wraps `getComputedStyle`, `getBoundingClientRect`, and the `offsetWidth` / `clientWidth` / `scrollWidth` family with counters + per-call timing. Injected by `--instrument`. | +| `analyze-profile.mjs` | Bottom-up self-time analyzer for `.cpuprofile` files. Same shape as DevTools' Performance bottom-up view, in the terminal. | | `run.bat` | Windows wrapper. Installs deps on first run, then invokes `node measure.mjs`. | | `results/` | Output, one timestamped subfolder per run. Git-ignored. | @@ -91,8 +94,15 @@ number too. run.bat # defaults to ..\docs\_site-pdf\book.html run.bat path\to\some-other.html # explicit input run.bat --out my-run # explicit output directory +run.bat --detach-pages # inject the detach-pages fix +run.bat --cpu-profile # CPU-profile the render phase +run.bat --instrument # count + time DOM-accessor calls ``` +Flags compose. The CPU profile lands as `render.cpuprofile` +(loadable in Chrome DevTools -> Performance -> "Load profile..."); +`--instrument` prints a per-op table at end-of-render. + You need `_site-pdf\book.html` to exist first -- run `docs\build.bat` (which is `bundle exec jekyll build`) if you haven't already. @@ -469,3 +479,72 @@ linear-in-`n` and don't shrink with this change: 2. **Generate: 60 s in `page.pdf()`.** Chromium internals; mostly opaque. The 52 MB raw size hints at uncompressed streams in Chrome's writer -- worth a glance but not a quick fix. + +## Confirming the mechanism (instrumentation A/B) + +The CPU profile said `getBoundingClientRect` self-time dropped +3.3x; the wall-clock measurement said render dropped 2x. To +double-check that's actually due to the smaller layout tree (and +not a profile-attribution coincidence, or paged.js silently +skipping work, or new costs appearing elsewhere) the harness has +an `--instrument` flag that wraps every in-page DOM accessor +that *can* force a synchronous layout -- `getComputedStyle`, +`getBoundingClientRect`, the `offsetWidth` / `offsetHeight` / +`offsetTop` / `offsetLeft` family, and the `clientWidth` / +`clientHeight` / `scrollWidth` / `scrollHeight` getters -- with +counters and per-call timing. + +Same wrapper overhead in both runs, so absolute totals are +inflated but the comparison is apples-to-apples. + +Two runs, same content, only difference is `--detach-pages`: + +| op | baseline | + detach | +| --- | --- | --- | +| `getBoundingClientRect` | 260,668 calls, **208 us** avg | 258,940 calls, **70 us** avg | +| `scrollWidth` | 37,911 calls, 1.4 us | 37,047 calls, 1.1 us | +| `scrollHeight` | 37,911 calls, 0.7 us | 37,047 calls, 0.6 us | +| `getComputedStyle` | 9,179 calls, 1.7 us | 9,179 calls, 1.8 us | +| `offset*` / `client*` | **0 calls** | **0 calls** | + +Instrumented render wall-clock: 82.1 s baseline -> 47.7 s with +detach. Same shape as the un-instrumented runs. + +What the numbers say: + +1. **Call counts are essentially identical.** The detach handler + isn't getting paged.js to skip any work -- 260,668 vs 258,940 + `getBoundingClientRect` calls is a rounding error. The fix + makes each call cheaper, not the number of calls smaller. + +2. **`getBoundingClientRect` per-call cost dropped 66 %**, + 208 us -> 70 us. Smaller live layout tree, less to recompute + on each forced flush. Total cost on this op alone: 54.3 s -> + 18.2 s, which is most of the wall-clock render savings. + +3. **`offsetWidth` / `offsetHeight` / `offsetTop` / `offsetLeft` + / `clientWidth` / `clientHeight` are called zero times** on + our content. The auto-width branches inside `finalizePage`'s + margin-box `forEach` (where those accesses live) never fire + on the kind of margin content we have (bottom-right page + number, nothing else). + +That last point matters for an earlier mistaken explanation. The +CPU profile of the post-fix run showed `(anonymous) browser.js:29501` +(the `finalizePage` `["top", "bottom"].forEach` callback) +growing from 1.1 s of self-time in baseline to 13.7 s after the +fix. The plausible-sounding story was "those reads were +free-riding on the chunker's just-flushed layout in baseline, +and now they're paying full price." The instrumentation rules +that out: **the function isn't doing more layout-flushing work, +because it isn't doing any.** Its 13.7 s of profile self-time +is a CPU-profiler attribution artefact -- V8 inlining and +sample distribution shift between runs once the dominant frame +(`getBoundingClientRect`, 67 % of render in baseline) shrinks. +The actual per-page work that closure does -- 8 `querySelector` +calls, 3 `getComputedStyle` calls, a few class checks and style +writes -- didn't change. + +The detach handler has no second-order downside; nothing the +instrumentation can see has shifted besides per-call latency on +the layout-flushing accessors the chunker already used. diff --git a/perf/instrument-flush-ops.js b/perf/instrument-flush-ops.js new file mode 100644 index 0000000..d5cf881 --- /dev/null +++ b/perf/instrument-flush-ops.js @@ -0,0 +1,121 @@ +// Wraps the in-page DOM accessors that can force a synchronous layout +// or style recalculation, so we can count how many times each one is +// called over a render and how long each call takes on average. The +// idea: a single call's wall-clock time tells us whether the call +// actually triggered a recompute (millisecond range) or hit cached +// state (sub-microsecond). +// +// Loaded as an --additional-script BEFORE the paged.js bundle would +// ideally be cleanest, but the harness loads paged.js first; we then +// register a Paged.Handler so we can dump results at afterRendered. +// +// Run with: node measure.mjs --instrument [--detach-pages] +// Compare runs with and without --detach-pages to see whether the +// detach handler changed the count of layout-flushing calls, the +// per-call cost, or both. + +(() => { + const stats = {}; + const props = [ + 'getComputedStyle', + 'getBoundingClientRect', + 'offsetWidth', + 'offsetHeight', + 'offsetTop', + 'offsetLeft', + 'clientWidth', + 'clientHeight', + 'scrollWidth', + 'scrollHeight', + ]; + for (const p of props) stats[p] = { count: 0, totalNs: 0, maxNs: 0 }; + + function record(name, ns) { + const s = stats[name]; + s.count++; + s.totalNs += ns; + if (ns > s.maxNs) s.maxNs = ns; + } + + // performance.now() returns milliseconds with sub-ms precision (Chrome + // clamps to ~5us by default; precise-memory flag also raises clock). + const now = () => performance.now(); + + // window.getComputedStyle + const origGCS = window.getComputedStyle.bind(window); + window.getComputedStyle = function (el, pseudo) { + const t = now(); + const r = origGCS(el, pseudo); + record('getComputedStyle', (now() - t) * 1e6); + return r; + }; + + // Element.prototype.getBoundingClientRect + const origGBCR = Element.prototype.getBoundingClientRect; + Element.prototype.getBoundingClientRect = function () { + const t = now(); + const r = origGBCR.call(this); + record('getBoundingClientRect', (now() - t) * 1e6); + return r; + }; + + // offsetWidth/Height/Top/Left and clientWidth/Height and scrollWidth/Height + // live as getters on HTMLElement.prototype (or Element.prototype for + // some). Wrap each one. + function wrapGetter(proto, prop) { + const desc = Object.getOwnPropertyDescriptor(proto, prop); + if (!desc || !desc.get) return false; + Object.defineProperty(proto, prop, { + configurable: true, + enumerable: desc.enumerable, + get() { + const t = now(); + const r = desc.get.call(this); + record(prop, (now() - t) * 1e6); + return r; + }, + }); + return true; + } + for (const p of ['offsetWidth', 'offsetHeight', 'offsetTop', 'offsetLeft']) { + if (!wrapGetter(HTMLElement.prototype, p)) wrapGetter(Element.prototype, p); + } + for (const p of ['clientWidth', 'clientHeight', 'scrollWidth', 'scrollHeight']) { + if (!wrapGetter(Element.prototype, p)) wrapGetter(HTMLElement.prototype, p); + } + + window.__flushOpStats = stats; + + class InstrumentHandler extends Paged.Handler { + afterRendered(pages) { + const total = pages.length; + const rows = Object.entries(stats) + .map(([name, s]) => ({ + name, + count: s.count, + totalMs: s.totalNs / 1e6, + perPage: s.count / total, + avgUs: s.count ? (s.totalNs / s.count) / 1000 : 0, + maxUs: s.maxNs / 1000, + })) + .sort((a, b) => b.totalMs - a.totalMs); + console.log(`[instrument] flush-op stats over ${total} pages:`); + console.log(' op count total_ms per_page avg_us max_us'); + console.log(' -- ----- -------- -------- ------ ------'); + for (const r of rows) { + console.log( + ' ' + r.name.padEnd(24) + + r.count.toString().padStart(10) + + r.totalMs.toFixed(1).padStart(11) + + r.perPage.toFixed(2).padStart(11) + + r.avgUs.toFixed(2).padStart(9) + + r.maxUs.toFixed(2).padStart(9) + ); + } + // also stash on window in JSON form for the harness to pull + window.__flushOpReport = rows; + } + } + Paged.registerHandlers(InstrumentHandler); + console.log('[instrument] flush-op accessors wrapped'); +})(); diff --git a/perf/measure.mjs b/perf/measure.mjs index da6d7ed..93d6bcc 100644 --- a/perf/measure.mjs +++ b/perf/measure.mjs @@ -55,6 +55,7 @@ let keepOpen = false; let cpuProfile = false; let cpuSampling = 1000; // microseconds let detachPages = false; +let instrument = false; for (let i = 0; i < args.length; i++) { const a = args[i]; if (a === '--out') outArg = args[++i]; @@ -62,6 +63,7 @@ for (let i = 0; i < args.length; i++) { else if (a === '--cpu-profile') cpuProfile = true; else if (a === '--cpu-sampling') cpuSampling = parseInt(args[++i], 10); else if (a === '--detach-pages') detachPages = true; + else if (a === '--instrument') instrument = true; else if (!inputArg) inputArg = a; else { console.error(`unknown arg: ${a}`); process.exit(2); } } @@ -79,8 +81,10 @@ if (!existsSync(inputPath)) { const pagedScriptPath = resolve(__dirname, 'node_modules', 'pagedjs-cli', 'dist', 'browser.js'); const handlerPath = resolve(__dirname, 'timing-handler.js'); const detachPagesPath = resolve(__dirname, 'detach-pages.js'); +const instrumentPath = resolve(__dirname, 'instrument-flush-ops.js'); const required = [pagedScriptPath, handlerPath]; if (detachPages) required.push(detachPagesPath); +if (instrument) required.push(instrumentPath); for (const p of required) { if (!existsSync(p)) { console.error(`missing required file: ${p}`); @@ -122,7 +126,13 @@ try { page.on('console', (msg) => { const t = msg.text(); - if (t.startsWith('[paged-timing]') || t.startsWith('[detach-pages]')) console.log(t); + if (t.startsWith('[paged-timing]') || t.startsWith('[detach-pages]') || + t.startsWith('[instrument]') || t.startsWith(' op ') || + t.startsWith(' --') || t.startsWith(' getComputedStyle') || + t.startsWith(' getBoundingClientRect') || t.startsWith(' offset') || + t.startsWith(' client') || t.startsWith(' scroll')) { + console.log(t); + } }); page.on('pageerror', (err) => console.error('[page error]', err.message)); page.on('requestfailed', (req) => { @@ -147,6 +157,9 @@ try { if (detachPages) { await page.addScriptTag({ path: detachPagesPath }); } + if (instrument) { + await page.addScriptTag({ path: instrumentPath }); + } // RENDER ---------------------------------------------------------- // Optionally wrap just this phase in a V8 CPU profile. CDP Profiler From d4c16f40bebcfab2f29f25c0bac43a70b36038d2 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Mon, 18 May 2026 21:23:19 +0200 Subject: [PATCH 05/26] Test further flush-related optimization opportunities. --- perf/README.md | 61 +++++++++++++++-------- perf/detach-pages.js | 15 +++++- perf/instrument-flush-ops.js | 53 ++++++++++++++++++++ perf/measure.mjs | 12 +++-- perf/time-hooks.js | 97 ++++++++++++++++++++++++++++++++++++ 5 files changed, 212 insertions(+), 26 deletions(-) create mode 100644 perf/time-hooks.js diff --git a/perf/README.md b/perf/README.md index 206b978..68861a5 100644 --- a/perf/README.md +++ b/perf/README.md @@ -53,8 +53,9 @@ DevTools-compatible trace is a few lines. | `package.json` | Pins `puppeteer` + `pagedjs-cli` + `patch-package`. | | `measure.mjs` | Puppeteer harness. Mirrors pagedjs-cli's own `Printer.pdf()` flow, with optional CPU profiling, in-page handler injection, and DOM-accessor instrumentation. | | `timing-handler.js` | `Paged.Handler` that records per-page wall time + heap into `window.__pagedTiming` and streams a line per page to the console. Always injected. | -| `detach-pages.js` | `Paged.Handler` that hides each completed page from the layout tree. The fix. Injected by `--detach-pages` and by `docs/book.bat`. | +| `detach-pages.js` | `Paged.Handler` that hides each completed page from the layout tree (registered against `finalizePage`). The fix. Injected by `--detach-pages` and by `docs/book.bat`. | | `instrument-flush-ops.js` | Wraps `getComputedStyle`, `getBoundingClientRect`, and the `offsetWidth` / `clientWidth` / `scrollWidth` family with counters + per-call timing. Injected by `--instrument`. | +| `time-hooks.js` | Wraps every task registered to `chunker.hooks.*` and `polisher.hooks.*` with a wall-clock timer. Tells you which handler's hook method is eating render time, per page. Injected by `--time-hooks`. | | `analyze-profile.mjs` | Bottom-up self-time analyzer for `.cpuprofile` files. Same shape as DevTools' Performance bottom-up view, in the terminal. | | `run.bat` | Windows wrapper. Installs deps on first run, then invokes `node measure.mjs`. | | `results/` | Output, one timestamped subfolder per run. Git-ignored. | @@ -97,6 +98,7 @@ run.bat --out my-run # explicit output directory run.bat --detach-pages # inject the detach-pages fix run.bat --cpu-profile # CPU-profile the render phase run.bat --instrument # count + time DOM-accessor calls +run.bat --time-hooks # per-task timing of every chunker/polisher hook ``` Flags compose. The CPU profile lands as `render.cpuprofile` @@ -529,22 +531,41 @@ What the numbers say: on the kind of margin content we have (bottom-right page number, nothing else). -That last point matters for an earlier mistaken explanation. The -CPU profile of the post-fix run showed `(anonymous) browser.js:29501` -(the `finalizePage` `["top", "bottom"].forEach` callback) -growing from 1.1 s of self-time in baseline to 13.7 s after the -fix. The plausible-sounding story was "those reads were -free-riding on the chunker's just-flushed layout in baseline, -and now they're paying full price." The instrumentation rules -that out: **the function isn't doing more layout-flushing work, -because it isn't doing any.** Its 13.7 s of profile self-time -is a CPU-profiler attribution artefact -- V8 inlining and -sample distribution shift between runs once the dominant frame -(`getBoundingClientRect`, 67 % of render in baseline) shrinks. -The actual per-page work that closure does -- 8 `querySelector` -calls, 3 `getComputedStyle` calls, a few class checks and style -writes -- didn't change. - -The detach handler has no second-order downside; nothing the -instrumentation can see has shifted besides per-call latency on -the layout-flushing accessors the chunker already used. +## Why detach-pages.js hooks `finalizePage`, not `afterPageLayout` + +The chunker's per-page hook order is: + +``` +beforePageLayout -> afterPageLayout -> finalizePage +``` + +`AtPage.finalizePage` (built into paged.js) reads `getComputedStyle` +on margin-box children and writes `el.style["grid-template-columns"]` +on them. `time-hooks.js` measurements show this method is **11x +slower per call when run on a `display:none` page**: + +| Variant | `chunker.finalizePage::finalizePage` per call | +| --- | --- | +| Baseline (no detach) | 0.82 ms | +| Detach hooked on `afterPageLayout` (hide *before* AtPage) | **9.24 ms** | +| Detach hooked on `finalizePage` (hide *after* AtPage) | 0.67 ms | + +Chromium has fast paths for style reads/writes on visible elements; +on hidden subtrees the same operations re-cascade each call. So +hiding the page before AtPage runs makes AtPage pay a slow path +worth ~8 ms/page over the whole render. + +`detach-pages.js` therefore hooks `finalizePage`, registering after +AtPage so its method runs second. AtPage works on a visible page; +we hide immediately after. The next chunker iteration sees pages +0..N-1 hidden, so the original `getBoundingClientRect` saving in +the chunker is preserved. + +**Wall-clock impact: none measurable.** A 4+4 interleaved A/B +between the two variants showed render medians within ~1 s of +each other (48.70 s vs 49.83 s un-instrumented; 50.78 s vs 50.90 s +with `--time-hooks`), well inside the 3-7 s within-variant noise. +The `finalizePage` hook is the variant we ship because it makes +the CPU profile read honestly (no mystery cost inside AtPage) and +gives AtPage the visible page it expects, not because of a +measurable speedup. diff --git a/perf/detach-pages.js b/perf/detach-pages.js index 3ae8a1d..2c9070c 100644 --- a/perf/detach-pages.js +++ b/perf/detach-pages.js @@ -19,7 +19,18 @@ super(chunker, polisher, caller); this._hidden = []; } - afterPageLayout(pageElement /* , page, breakToken */) { + // Hook into finalizePage rather than afterPageLayout. The chunker + // fires beforePageLayout -> afterPageLayout -> finalizePage per + // page; AtPage's own finalizePage handler does getComputedStyle + // reads and `el.style["grid-template-columns"] = ...` writes on + // the page's margin-box children. Doing that on a display:none + // subtree takes ~8 ms/page in Chromium (no cached layout box, + // style resolution re-cascades). Hiding in finalizePage instead + // means we run *after* AtPage on the same page (because our + // handler registers last via --additional-script), so AtPage + // touches visible elements; the page is hidden immediately after + // for the next chunker.findOverflow. + finalizePage(pageElement /* , page, breakToken, chunker */) { pageElement.style.display = 'none'; this._hidden.push(pageElement); } @@ -31,5 +42,5 @@ } } Paged.registerHandlers(DetachPagesHandler); - console.log('[detach-pages] handler registered'); + console.log('[detach-pages] handler registered (finalizePage variant)'); })(); diff --git a/perf/instrument-flush-ops.js b/perf/instrument-flush-ops.js index d5cf881..282eb43 100644 --- a/perf/instrument-flush-ops.js +++ b/perf/instrument-flush-ops.js @@ -27,6 +27,13 @@ 'clientHeight', 'scrollWidth', 'scrollHeight', + // non-flushing but called a lot from finalizePage; useful for + // accounting where (anonymous) browser.js:29501 spends its time + 'querySelector', + 'querySelectorAll', + 'classList.contains', + 'setProperty', // marginGroup.style.setProperty(...) + 'style.set', // marginGroup.style[...] = ... (CSSStyleDeclaration setter) ]; for (const p of props) stats[p] = { count: 0, totalNs: 0, maxNs: 0 }; @@ -84,6 +91,52 @@ if (!wrapGetter(Element.prototype, p)) wrapGetter(HTMLElement.prototype, p); } + // querySelector / querySelectorAll on Element. These are not + // layout-flushing but they're the dominant non-flush operation + // in the finalizePage forEach we're investigating. + const origQS = Element.prototype.querySelector; + Element.prototype.querySelector = function (sel) { + const t = now(); + const r = origQS.call(this, sel); + record('querySelector', (now() - t) * 1e6); + return r; + }; + const origQSA = Element.prototype.querySelectorAll; + Element.prototype.querySelectorAll = function (sel) { + const t = now(); + const r = origQSA.call(this, sel); + record('querySelectorAll', (now() - t) * 1e6); + return r; + }; + + // DOMTokenList.contains => classList.contains(...) + const origCLC = DOMTokenList.prototype.contains; + DOMTokenList.prototype.contains = function (token) { + const t = now(); + const r = origCLC.call(this, token); + record('classList.contains', (now() - t) * 1e6); + return r; + }; + + // CSSStyleDeclaration writes: cover both .setProperty(name, val) + // and bracket-set `el.style[name] = val` (uses the named setter on + // the proxied CSSStyleDeclaration). + const origSP = CSSStyleDeclaration.prototype.setProperty; + CSSStyleDeclaration.prototype.setProperty = function (n, v, p) { + const t = now(); + const r = origSP.call(this, n, v, p); + record('setProperty', (now() - t) * 1e6); + return r; + }; + // Most assignments like `el.style["grid-template-columns"] = ...` + // go through a Proxy on the CSSStyleDeclaration; wrapping every + // setter would require a Proxy of our own. We count the cheap + // identifier-name setters by reflecting on known property names + // is impractical -- skip and rely on setProperty for explicit + // setProperty calls. (Most paged.js margin-box writes use + // bracket syntax, so this won't catch them; we'll account + // for that in the analysis.) + window.__flushOpStats = stats; class InstrumentHandler extends Paged.Handler { diff --git a/perf/measure.mjs b/perf/measure.mjs index 93d6bcc..45ed556 100644 --- a/perf/measure.mjs +++ b/perf/measure.mjs @@ -56,6 +56,7 @@ let cpuProfile = false; let cpuSampling = 1000; // microseconds let detachPages = false; let instrument = false; +let timeHooks = false; for (let i = 0; i < args.length; i++) { const a = args[i]; if (a === '--out') outArg = args[++i]; @@ -64,6 +65,7 @@ for (let i = 0; i < args.length; i++) { else if (a === '--cpu-sampling') cpuSampling = parseInt(args[++i], 10); else if (a === '--detach-pages') detachPages = true; else if (a === '--instrument') instrument = true; + else if (a === '--time-hooks') timeHooks = true; else if (!inputArg) inputArg = a; else { console.error(`unknown arg: ${a}`); process.exit(2); } } @@ -82,9 +84,11 @@ const pagedScriptPath = resolve(__dirname, 'node_modules', 'pagedjs-cli', 'dist const handlerPath = resolve(__dirname, 'timing-handler.js'); const detachPagesPath = resolve(__dirname, 'detach-pages.js'); const instrumentPath = resolve(__dirname, 'instrument-flush-ops.js'); +const timeHooksPath = resolve(__dirname, 'time-hooks.js'); const required = [pagedScriptPath, handlerPath]; if (detachPages) required.push(detachPagesPath); if (instrument) required.push(instrumentPath); +if (timeHooks) required.push(timeHooksPath); for (const p of required) { if (!existsSync(p)) { console.error(`missing required file: ${p}`); @@ -127,10 +131,7 @@ try { page.on('console', (msg) => { const t = msg.text(); if (t.startsWith('[paged-timing]') || t.startsWith('[detach-pages]') || - t.startsWith('[instrument]') || t.startsWith(' op ') || - t.startsWith(' --') || t.startsWith(' getComputedStyle') || - t.startsWith(' getBoundingClientRect') || t.startsWith(' offset') || - t.startsWith(' client') || t.startsWith(' scroll')) { + t.startsWith('[instrument]') || t.startsWith(' ')) { console.log(t); } }); @@ -160,6 +161,9 @@ try { if (instrument) { await page.addScriptTag({ path: instrumentPath }); } + if (timeHooks) { + await page.addScriptTag({ path: timeHooksPath }); + } // RENDER ---------------------------------------------------------- // Optionally wrap just this phase in a V8 CPU profile. CDP Profiler diff --git a/perf/time-hooks.js b/perf/time-hooks.js new file mode 100644 index 0000000..f035e22 --- /dev/null +++ b/perf/time-hooks.js @@ -0,0 +1,97 @@ +// Wraps chunker.hooks..hooks tasks with per-task wall-clock +// timers. Each registered handler method (e.g. AtPage.prototype.finalizePage) +// gets its own counter and accumulated time. +// +// Loaded by --time-hooks. Registers a Paged.Handler so we can run in +// the constructor after other handlers have already added themselves. +// We register LAST (since additional-script tags load after the +// bundle, and Paged.registerHandlers appends) so by the time our +// constructor runs, every other handler has registered its hooks. + +(() => { + // (hookName, handlerLabel) -> { totalMs, count } + const stats = new Map(); + const labelFor = (fn) => { + // Bound functions: fn.name is typically "bound ". + // Strip the "bound " prefix if present. + let n = (fn && fn.name) || ''; + if (n.startsWith('bound ')) n = n.slice(6); + return n || ''; + }; + + function wrapHook(hookName, hook) { + if (!hook || !Array.isArray(hook.hooks)) return 0; + const orig = hook.hooks; + // If multiple tasks share a label (e.g., two handlers both named + // `finalizePage`), the unmodified key would collide and only the + // last task's stats would be retained. Disambiguate with a + // per-label index. + const seen = new Map(); + hook.hooks = orig.map((task, i) => { + const label = labelFor(task); + const seenN = (seen.get(label) || 0) + 1; + seen.set(label, seenN); + const key = `${hookName}::${label}` + (seenN > 1 ? `#${seenN}` : ''); + const s = { totalMs: 0, count: 0 }; + stats.set(key, s); + return function (...args) { + const t0 = performance.now(); + const r = task.apply(this, args); + if (r && typeof r.then === 'function') { + // async-aware: charge end on resolve + return r.finally(() => { + s.totalMs += performance.now() - t0; + s.count++; + }); + } + s.totalMs += performance.now() - t0; + s.count++; + return r; + }; + }); + return orig.length; + } + + class TimeHooksHandler extends Paged.Handler { + constructor(chunker, polisher, caller) { + super(chunker, polisher, caller); + const ctx = { chunker, polisher, caller }; + let wrapped = 0; + for (const [ctxName, obj] of Object.entries(ctx)) { + if (!obj || !obj.hooks) continue; + for (const [hookName, hook] of Object.entries(obj.hooks)) { + wrapped += wrapHook(`${ctxName}.${hookName}`, hook); + } + } + console.log(`[time-hooks] wrapped ${wrapped} hook tasks across ${stats.size} (hook, handler) pairs`); + } + afterRendered(pages) { + const total = pages.length; + const rows = [...stats.entries()] + .map(([key, s]) => ({ + key, + count: s.count, + totalMs: s.totalMs, + perPageMs: total ? s.totalMs / total : 0, + avgMs: s.count ? s.totalMs / s.count : 0, + })) + .filter(r => r.count > 0) + .sort((a, b) => b.totalMs - a.totalMs); + console.log(`[time-hooks] hook task time over ${total} pages:`); + console.log(' hook::handler count total_ms per_page_ms avg_ms'); + console.log(' ------------- ----- -------- ----------- ------'); + for (const r of rows) { + console.log( + ' ' + r.key.padEnd(45) + + r.count.toString().padStart(8) + + r.totalMs.toFixed(1).padStart(10) + + r.perPageMs.toFixed(3).padStart(13) + + r.avgMs.toFixed(3).padStart(8) + ); + } + window.__hookTimings = rows; + } + } + Paged.registerHandlers(TimeHooksHandler); + console.log('[time-hooks] handler registered'); +})(); From b4acd771ba4a68abe4188591b1caa64a9a3703be Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Mon, 18 May 2026 22:54:10 +0200 Subject: [PATCH 06/26] Track perf/package-lock.json. The perf harness was previously gitignoring its own lockfile, leaving the puppeteer + pagedjs-cli versions reproducible only by faith in npm install. Track the lockfile so the perf numbers in README correspond to a known-pinned dependency set. --- perf/.gitignore | 1 - perf/package-lock.json | 3268 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 3268 insertions(+), 1 deletion(-) create mode 100644 perf/package-lock.json diff --git a/perf/.gitignore b/perf/.gitignore index 9a12c4a..d1f24e0 100644 --- a/perf/.gitignore +++ b/perf/.gitignore @@ -1,3 +1,2 @@ node_modules/ results/ -package-lock.json diff --git a/perf/package-lock.json b/perf/package-lock.json new file mode 100644 index 0000000..e7fbb6f --- /dev/null +++ b/perf/package-lock.json @@ -0,0 +1,3268 @@ +{ + "name": "tbasic-docs-perf", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tbasic-docs-perf", + "version": "0.0.0", + "devDependencies": { + "pagedjs-cli": "0.4.3", + "puppeteer": "^22.15.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/polyfill": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.12.1.tgz", + "integrity": "sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g==", + "deprecated": "🚨 This package has been deprecated in favor of separate inclusion of a polyfill and regenerator-runtime (when needed). See the @babel/polyfill docs (https://babeljs.io/docs/en/babel-polyfill) for more information.", + "dev": true, + "license": "MIT", + "dependencies": { + "core-js": "^2.6.5", + "regenerator-runtime": "^0.13.4" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", + "integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.0.tgz", + "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.3.tgz", + "integrity": "sha512-HdUm8EMQBLaJvGUdidNNbqpA1kYkwNcb+MYxkxCLAPJGQzlv9J0C24h8V65Z4c5GLd/JEALDvpFCQgpLJqc0zw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", + "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", + "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", + "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.3.tgz", + "integrity": "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chromium-bidi": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.3.tgz", + "integrity": "sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/clear-cut": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/clear-cut/-/clear-cut-2.0.2.tgz", + "integrity": "sha512-WVgn/gSejQ+0aoR8ucbKIdo6icduPZW6AbWwyUmAUgxy63rUYjwa5rj/HeoNPhf0/XPrl82X8bO/hwBkSmsFtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "dev": true, + "hasInstallScript": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "dev": true, + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dev": true, + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "dev": true, + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", + "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.0.0", + "is-unicode-supported": "^1.1.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mathjax": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-3.2.2.tgz", + "integrity": "sha512-Bt+SSVU8eBG27zChVewOicYs7Xsdt40qm4+UpHyX7k0/O9NliPc+x77k1/FEsPsjKPZGJvtRZM1vO+geW0OhGw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-fetch/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-6.3.1.tgz", + "integrity": "sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.0.0", + "cli-cursor": "^4.0.0", + "cli-spinners": "^2.6.1", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^1.1.0", + "log-symbols": "^5.1.0", + "stdin-discarder": "^0.1.0", + "strip-ansi": "^7.0.1", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pagedjs": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/pagedjs/-/pagedjs-0.4.3.tgz", + "integrity": "sha512-YtAN9JAjsQw1142gxEjEAwXvOF5nYQuDwnQ67RW2HZDkMLI+b4RsBE37lULZa9gAr6kDAOGBOhXI4wGMoY3raw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/polyfill": "^7.10.1", + "@babel/runtime": "^7.21.0", + "clear-cut": "^2.0.2", + "css-tree": "^1.1.3", + "event-emitter": "^0.3.5" + } + }, + "node_modules/pagedjs-cli": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/pagedjs-cli/-/pagedjs-cli-0.4.3.tgz", + "integrity": "sha512-NwmRwLiQAjXBdfG/tUbbv2iA8sZPiXRovTd4YxQRmYpMV1QQufg9Pqni0u6MMFqGxz6W2aZFdxZ5jPjtWrTqXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^11.0.0", + "express": "^4.18.2", + "html-entities": "^2.4.0", + "katex": "^0.16.8", + "lodash": "^4.17.21", + "mathjax": "^3.2.2", + "node-fetch": "^3.3.1", + "ora": "^6.3.1", + "pagedjs": "^0.4.3", + "pdf-lib": "1.17.1", + "puppeteer": "^20.9.0", + "replace-ext": "^2.0.0" + }, + "bin": { + "pagedjs-cli": "src/cli.js" + } + }, + "node_modules/pagedjs-cli/node_modules/@puppeteer/browsers": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.4.6.tgz", + "integrity": "sha512-x4BEjr2SjOPowNeiguzjozQbsc6h437ovD/wu+JpaenxVLm3jkgzHY2xOslMTp50HoTvQreMjiexiGQw1sqZlQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "progress": "2.0.3", + "proxy-agent": "6.3.0", + "tar-fs": "3.0.4", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.1" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=16.3.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pagedjs-cli/node_modules/chromium-bidi": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.16.tgz", + "integrity": "sha512-7ZbXdWERxRxSwo3txsBjjmc/NLxqb1Bk30mRb0BMS4YIaiV6zvKZqL/UAH+DdqcDYayDWk2n/y8klkBDODrPvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "3.0.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/pagedjs-cli/node_modules/cosmiconfig": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz", + "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/pagedjs-cli/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pagedjs-cli/node_modules/devtools-protocol": { + "version": "0.0.1147663", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1147663.tgz", + "integrity": "sha512-hyWmRrexdhbZ1tcJUGpO95ivbRhWXz++F4Ko+n21AY5PNln2ovoJw+8ZMNDTtip+CNFQfrtLVh/w4009dXO/eQ==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/pagedjs-cli/node_modules/mitt": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.0.tgz", + "integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pagedjs-cli/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pagedjs-cli/node_modules/proxy-agent": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.0.tgz", + "integrity": "sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pagedjs-cli/node_modules/puppeteer": { + "version": "20.9.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-20.9.0.tgz", + "integrity": "sha512-kAglT4VZ9fWEGg3oLc4/de+JcONuEJhlh3J6f5R1TLkrY/EHHIHxWXDOzXvaxQCtedmyVXBwg8M+P8YCO/wZjw==", + "deprecated": "< 24.15.0 is no longer supported", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "1.4.6", + "cosmiconfig": "8.2.0", + "puppeteer-core": "20.9.0" + }, + "engines": { + "node": ">=16.3.0" + } + }, + "node_modules/pagedjs-cli/node_modules/puppeteer-core": { + "version": "20.9.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-20.9.0.tgz", + "integrity": "sha512-H9fYZQzMTRrkboEfPmf7m3CLDN6JvbxXA3qTtS+dFt27tR+CsFHzPsT6pzp6lYL6bJbAPaR0HaPO6uSi+F94Pg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "1.4.6", + "chromium-bidi": "0.4.16", + "cross-fetch": "4.0.0", + "debug": "4.3.4", + "devtools-protocol": "0.0.1147663", + "ws": "8.13.0" + }, + "engines": { + "node": ">=16.3.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pagedjs-cli/node_modules/tar-fs": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", + "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "node_modules/pagedjs-cli/node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/pagedjs-cli/node_modules/yargs": { + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.15.0.tgz", + "integrity": "sha512-XjCY1SiSEi1T7iSYuxS82ft85kwDJUS7wj1Z0eGVXKdtr5g4xnVcbjwxhq5xBnpK/E7x1VZZoJDxpjAOasHT4Q==", + "deprecated": "< 24.15.0 is no longer supported", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.3.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1312386", + "puppeteer-core": "22.15.0" + }, + "bin": { + "puppeteer": "lib/esm/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz", + "integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.3.0", + "chromium-bidi": "0.6.3", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, + "license": "MIT" + }, + "node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stdin-discarder": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", + "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} From be0a6d4c9398adce7a53d08a6e45c508953d3a7c Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Mon, 18 May 2026 22:57:20 +0200 Subject: [PATCH 07/26] Add incremental-update PDF writer to skip the pdf-lib roundtrip. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The process phase of the render pipeline was 40s of `PDFDocument.load` + `pdfDoc.save` on the 52 MB raw Chrome PDF -- just to attach an outline tree and a few `/Info` fields. The incremental writer replaces that with a PDF 1.7 §7.5.6 incremental update: parse only the trailer, xref, Catalog and Info objects (~50ms), build outline objects in a fresh context starting from the original `/Size`, mutate Catalog and Info in place, then append the new objects + a new xref subsection + a trailer with `/Prev` pointing at the original xref. Readers chain backward to resolve everything we didn't touch. Original 52 MB stays byte-identical; we append ~400 KB. Wired into the harness as `--incremental`; on the 1638-page book the process phase drops from 39.7s to 0.25s, total from 150.7s to 110.3s. The tradeoff is file size -- pdf-lib's full save() deflate-compresses content streams as a side effect (17 MB output), the incremental writer preserves Chrome's uncompressed streams (53 MB output). Both documented in perf/README.md alongside the size-reclaim options. --- perf/README.md | 108 +++++++++++++ perf/incremental-pdf.mjs | 326 ++++++++++++++++++++++++++++++++++++++ perf/measure.mjs | 89 +++++++---- perf/test-incremental.mjs | 133 ++++++++++++++++ 4 files changed, 629 insertions(+), 27 deletions(-) create mode 100644 perf/incremental-pdf.mjs create mode 100644 perf/test-incremental.mjs diff --git a/perf/README.md b/perf/README.md index 68861a5..5f983b6 100644 --- a/perf/README.md +++ b/perf/README.md @@ -56,6 +56,8 @@ DevTools-compatible trace is a few lines. | `detach-pages.js` | `Paged.Handler` that hides each completed page from the layout tree (registered against `finalizePage`). The fix. Injected by `--detach-pages` and by `docs/book.bat`. | | `instrument-flush-ops.js` | Wraps `getComputedStyle`, `getBoundingClientRect`, and the `offsetWidth` / `clientWidth` / `scrollWidth` family with counters + per-call timing. Injected by `--instrument`. | | `time-hooks.js` | Wraps every task registered to `chunker.hooks.*` and `polisher.hooks.*` with a wall-clock timer. Tells you which handler's hook method is eating render time, per page. Injected by `--time-hooks`. | +| `incremental-pdf.mjs` | Replaces the pdf-lib load+save roundtrip with a PDF 1.7 §7.5.6 incremental update appended to Chrome's bytes. Used by `--incremental`. | +| `test-incremental.mjs` | Smoke test for `incremental-pdf.mjs`: renders a tiny probe page, runs the writer, verifies the result parses (via pdf-lib re-load) and that outline + metadata land correctly. | | `analyze-profile.mjs` | Bottom-up self-time analyzer for `.cpuprofile` files. Same shape as DevTools' Performance bottom-up view, in the terminal. | | `run.bat` | Windows wrapper. Installs deps on first run, then invokes `node measure.mjs`. | | `results/` | Output, one timestamped subfolder per run. Git-ignored. | @@ -99,6 +101,7 @@ run.bat --detach-pages # inject the detach-pages fix run.bat --cpu-profile # CPU-profile the render phase run.bat --instrument # count + time DOM-accessor calls run.bat --time-hooks # per-task timing of every chunker/polisher hook +run.bat --incremental # process via incremental update instead of pdf-lib roundtrip ``` Flags compose. The CPU profile lands as `render.cpuprofile` @@ -569,3 +572,108 @@ The `finalizePage` hook is the variant we ship because it makes the CPU profile read honestly (no mystery cost inside AtPage) and gives AtPage the visible page it expects, not because of a measurable speedup. + +## Fix applied: `perf/incremental-pdf.mjs` + +The direct follow-up from the previous section's "What this didn't +fix" list: kill the pdf-lib roundtrip that owned the 40 s process +phase. 99 % of that was `PDFDocument.load` + `pdfDoc.save` on the +52 MB raw PDF -- just so we can attach an outline tree and override a +handful of `/Info` fields. + +Approach: a **PDF incremental update** (PDF 1.7 §7.5.6). We never +call `PDFDocument.load`. Instead: + +1. Parse only the trailer, xref, Catalog, and Info objects -- using + `PDFParser` positioned at known byte offsets. Three small dicts, + ~50 ms. +2. Build outline objects in a fresh `PDFContext`, allocating refs + starting from the original `/Size`. +3. Mutate the parsed Catalog (add `/Outlines`, `/Lang`) and Info + (override `/Title`, `/Creator`, dates, ...) **in place**, keeping + their original refs. +4. Append to the original bytes: + - The new and updated indirect objects. + - A new xref section whose subsections cover only those refs. + - A new trailer dict with `/Prev` pointing at the original xref. + - `startxref ` + `%%EOF`. + +Readers chain backward through `/Prev` to resolve any ref we didn't +touch (`/Pages`, `/Dests`, every font / image / content stream). The +original 52 MB stays byte-identical; we just append a few hundred KB. + +The writer is built on pdf-lib's low-level primitives -- `PDFParser` +for the few objects we read, `PDFContext` + `PDFDict` for object +construction, `PDFCrossRefSection` + `PDFTrailerDict` for emitting +the new xref / trailer. The expensive `PDFDocument.load` (which +parses every indirect object in the file) is bypassed entirely. + +### Results + +Same 1638-page book, `--detach-pages` already in effect for both runs: + +| Phase | pdf-lib roundtrip | + incremental | Δ | +| -------- | ----------------- | ------------- | --- | +| render | 50.9 s | 49.2 s | unchanged (noise) | +| generate | 60.2 s | 60.9 s | unchanged (noise) | +| process | 39.7 s | 0.25 s | **-39.4 s (-99%)** | +| **total**| **150.7 s** | **110.3 s** | **-40.4 s (-27%)** | + +Combined with the detach-pages fix, the build is now **110 s vs +207 s baseline (-47 %)**. + +Process-phase breakdown for the incremental path: + +``` +incremental : 250 ms total +appended : ~410 KB (vs 52 MB raw Chrome PDF, untouched) +new objects : 1776 (outline root + 1773 outline items + Catalog + Info) +``` + +The output reparses cleanly under both pdf-lib's full +`PDFDocument.load` and poppler's `pdfinfo` (PDF 1.4, 1638 pages, +A4, all metadata intact). Outline navigation works in the viewer. + +### The size tradeoff + +`pdf-lib`'s `save()` quietly deflate-compresses content streams as a +side effect of full re-emission. That's why the old output was 17 MB +even though Chrome's raw PDF is 52 MB. The incremental writer keeps +Chrome's bytes verbatim, so the final file is essentially "52 MB + +outline": + +| Output mode | Final PDF size | +| --- | --- | +| pdf-lib roundtrip | 16.9 MB | +| incremental | 52.7 MB | + +This is the same uncompressed-streams problem the initial findings +section flagged ("Chrome isn't compressing streams aggressively"). +Two ways to claw the size back without going back to a full parse, +both independent follow-ups: + +1. **qpdf post-pass** -- `qpdf --object-streams=generate + --compress-streams=y in.pdf out.pdf` re-emits the file with deflate + on every stream, without reifying document semantics. C++, + skips object-by-object reconstruction; should be much faster than + pdf-lib's load. Adds a binary dependency. +2. **Deflate inside the writer** -- detect raw streams without + `/Filter` in the parsed objects and rewrite them with + `/Filter /FlateDecode` + a pako-deflated body. Same engineering + shape as qpdf but in JS, and lets the incremental update stay + self-contained. Requires walking the full body of the original + PDF, which puts back some of the cost we just removed. + +The incremental writer ships as-is; pick a size strategy when / +if file size becomes a concern. + +### Production integration + +`measure.mjs --incremental` exercises the writer for measurement. +Wiring it into `docs/book.bat` is a separate step: `pagedjs-cli` +performs its own pdf-lib roundtrip inside `Printer.pdf()`, and +`--additional-script` can't intercept that. The cleanest path is to +replace the `pagedjs-cli` invocation in `book.bat` with a thin Node +driver -- essentially `measure.mjs` minus the timing scaffolding -- +that uses `puppeteer` + `incremental-pdf.mjs` directly. The harness +already proves this is a ~30-line script. diff --git a/perf/incremental-pdf.mjs b/perf/incremental-pdf.mjs new file mode 100644 index 0000000..32b9b74 --- /dev/null +++ b/perf/incremental-pdf.mjs @@ -0,0 +1,326 @@ +// Apply outline + metadata to Chrome's PDF via an incremental update, +// without round-tripping the whole 52 MB body through pdf-lib. +// +// The PDF spec (7.5.6) lets us append: +// +// +// +// +// +// xref +// +// trailer +// < /Info /Prev >> +// startxref +// %%EOF +// +// Readers chain backward via /Prev to the original xref to resolve any +// ref we didn't touch (pages, fonts, images, /Dests, ...). The original +// 52 MB stays byte-identical -- we just append a few KB. +// +// We use pdf-lib's primitives where they help (PDFParser to read just the +// xref + trailer + a couple of objects, PDFContext + PDFDict for object +// construction, PDFCrossRefSection for emitting the new xref) but never +// call PDFDocument.load -- that's the slow path we're eliminating. + +import { + PDFParser, + PDFDict, PDFName, PDFNumber, PDFString, PDFHexString, PDFRef, + PDFCrossRefSection, PDFTrailerDict, +} from 'pdf-lib'; +import { decode as htmlEntitiesDecode } from 'html-entities'; + +// --- outline construction (mirrors pagedjs-cli/src/outline.js setOutline, +// but writes into a caller-supplied context and returns the outline-root +// ref instead of mutating a pdfDoc.catalog) ----------------------------- + +const SANITIZE_XML_RX = /<[^>]+>/g; +function sanitizeOutlineTitle(s) { + if (s.includes('<')) s = s.replace(SANITIZE_XML_RX, ''); + return htmlEntitiesDecode(s); +} + +function setRefsForOutlineItems(layer, context, parentRef) { + for (const item of layer) { + item.ref = context.nextRef(); + item.parentRef = parentRef; + setRefsForOutlineItems(item.children, context, item.ref); + } +} + +function countChildrenOfOutline(layer) { + let n = 0; + for (const item of layer) { + n += 1; + n += countChildrenOfOutline(item.children); + } + return n; +} + +function buildPdfObjectsForOutline(layer, context) { + for (let i = 0; i < layer.length; i++) { + const item = layer[i]; + const prev = layer[i - 1]; + const next = layer[i + 1]; + const entries = new Map([ + [PDFName.of('Title'), PDFHexString.fromText(sanitizeOutlineTitle(item.title))], + [PDFName.of('Dest'), PDFName.of(item.destination)], + [PDFName.of('Parent'), item.parentRef], + ]); + if (prev) entries.set(PDFName.of('Prev'), prev.ref); + if (next) entries.set(PDFName.of('Next'), next.ref); + if (item.children.length) { + entries.set(PDFName.of('First'), item.children[0].ref); + entries.set(PDFName.of('Last'), item.children[item.children.length - 1].ref); + entries.set(PDFName.of('Count'), PDFNumber.of(countChildrenOfOutline(item.children))); + } + context.assign(item.ref, PDFDict.fromMapWithContext(entries, context)); + buildPdfObjectsForOutline(item.children, context); + } +} + +function buildOutline(context, outline) { + if (outline.length === 0) return null; + const outlineRef = context.nextRef(); + setRefsForOutlineItems(outline, context, outlineRef); + buildPdfObjectsForOutline(outline, context); + const rootDict = PDFDict.fromMapWithContext(new Map([ + [PDFName.of('First'), outline[0].ref], + [PDFName.of('Last'), outline[outline.length - 1].ref], + [PDFName.of('Count'), PDFNumber.of(countChildrenOfOutline(outline))], + ]), context); + context.assign(outlineRef, rootDict); + return outlineRef; +} + +// --- metadata merge (mirrors pagedjs-cli/src/postprocesser.js setMetadata, +// but writes into a parsed Info dict instead of via PDFDocument.setX) --- + +function applyMetadataToInfo(infoDict, meta) { + let { creator, producer, creationDate } = meta; + + let keywords = meta.keywords; + if (typeof keywords === 'string') keywords = keywords.split(','); + if (!keywords) keywords = []; + + // Match the existing harness behaviour: always overwrite ModDate, default + // CreationDate to now if missing, and append " + Paged.js" to whatever + // creator Chrome wrote. + if (!(creationDate instanceof Date)) creationDate = new Date(); + const modDate = new Date(); + + // Read existing Creator/Producer directly from the dict. Chrome writes + // them as direct (non-indirect) strings, so we don't need to dereference; + // skipping the lookup also avoids depending on a fully-loaded context. + const decodeIfString = (v) => + (v instanceof PDFString || v instanceof PDFHexString) ? v.decodeText() : null; + if (!creator) { + const existing = decodeIfString(infoDict.get(PDFName.of('Creator'))); + creator = (existing ?? '') + ' + Paged.js'; + } + if (!producer) { + producer = decodeIfString(infoDict.get(PDFName.of('Producer'))) ?? undefined; + } + + if (meta.title) infoDict.set(PDFName.of('Title'), PDFHexString.fromText(meta.title)); + if (meta.subject) infoDict.set(PDFName.of('Subject'), PDFHexString.fromText(meta.subject)); + if (keywords.length) infoDict.set(PDFName.of('Keywords'), PDFHexString.fromText(keywords.join(' '))); + if (meta.author) infoDict.set(PDFName.of('Author'), PDFHexString.fromText(meta.author)); + if (creator) infoDict.set(PDFName.of('Creator'), PDFHexString.fromText(creator)); + if (producer) infoDict.set(PDFName.of('Producer'), PDFHexString.fromText(producer)); + infoDict.set(PDFName.of('CreationDate'), PDFString.fromDate(creationDate)); + infoDict.set(PDFName.of('ModDate'), PDFString.fromDate(modDate)); +} + +// --- raw byte parsing for trailer location ------------------------------ + +function lastIndexOfSeq(buf, needle, fromEnd) { + // Search backwards from buf.length-1 for `needle` within the last + // `fromEnd` bytes. Returns -1 if not found. + const start = Math.max(0, buf.length - fromEnd); + for (let i = buf.length - needle.length; i >= start; i--) { + let ok = true; + for (let j = 0; j < needle.length; j++) { + if (buf[i + j] !== needle[j]) { ok = false; break; } + } + if (ok) return i; + } + return -1; +} + +function findStartxrefOffset(buf) { + // The trailer area is conventionally in the last <1KB. Compliant PDFs + // have `%%EOF` at end; tolerate up to 2KB of trailing junk just in case. + const SEARCH = 2048; + const EOF = Buffer.from('%%EOF'); + const SXR = Buffer.from('startxref'); + const eofIdx = lastIndexOfSeq(buf, EOF, SEARCH); + if (eofIdx < 0) throw new Error('incremental-pdf: no %%EOF in trailing 2KB'); + const sxrIdx = lastIndexOfSeq(buf.subarray(0, eofIdx), SXR, 128); + if (sxrIdx < 0) throw new Error('incremental-pdf: no startxref keyword before %%EOF'); + const between = buf.subarray(sxrIdx + SXR.length, eofIdx).toString('binary').trim(); + const m = between.match(/^(\d+)/); + if (!m) throw new Error('incremental-pdf: could not parse startxref offset'); + return { xrefOffset: parseInt(m[1], 10), startxrefKeywordOffset: sxrIdx }; +} + +// --- main entry point --------------------------------------------------- + +export async function applyOutlineAndMetadataIncremental(rawPdf, outline, meta) { + // page.pdf() can return either a Buffer or a Uint8Array depending on + // puppeteer version. Buffer is a subclass of Uint8Array, so the + // wrapping is cheap when it's already a Buffer. + const buf = Buffer.isBuffer(rawPdf) ? rawPdf : Buffer.from(rawPdf); + + // 1. Find the original xref offset. + const { xrefOffset: oldXrefOffset } = findStartxrefOffset(buf); + + // 2. Parse just the xref + trailer dict using PDFParser positioned at + // the xref. parseHeader() advances bytes past the %PDF-1.x line so + // that subsequent moveTo() calls work in absolute file offsets. + const parser = PDFParser.forBytesWithOptions(buf); + parser.parseHeader(); + parser.bytes.moveTo(oldXrefOffset); + const xrefSection = parser.maybeParseCrossRefSection(); + if (!xrefSection) { + throw new Error('incremental-pdf: classic xref table not found at startxref offset. ' + + 'Chrome\'s PDFs use classic tables; an xref stream here means the input is ' + + 'not from Chrome and is not supported by this writer.'); + } + // maybeParseTrailerDict() throws away /Size and discards the dict + // (it only saves Root/Info/Encrypt/ID onto context.trailerInfo). We + // need /Size too, so consume `trailer` by hand and call parseDict() + // directly. matchKeyword takes a byte sequence and rolls back on + // mismatch, so the error path leaves the cursor where the dict would + // have started -- handy for the message. + parser.skipWhitespaceAndComments(); + if (!parser.matchKeyword(Buffer.from('trailer'))) { + throw new Error(`incremental-pdf: expected 'trailer' keyword after xref at ${oldXrefOffset}`); + } + parser.skipWhitespaceAndComments(); + const trailerDict = parser.parseDict(); + const rootRef = trailerDict.get(PDFName.of('Root')); + const infoRef = trailerDict.get(PDFName.of('Info')); + const sizeNum = trailerDict.get(PDFName.of('Size')); + if (!(rootRef instanceof PDFRef)) throw new Error('incremental-pdf: trailer /Root is not a ref'); + if (!(infoRef instanceof PDFRef)) throw new Error('incremental-pdf: trailer /Info is not a ref (Chrome should always emit one)'); + if (!(sizeNum instanceof PDFNumber)) throw new Error('incremental-pdf: trailer /Size is not a number'); + const oldSize = sizeNum.asNumber(); + + // 3. Find the byte offsets of Catalog and Info in the xref. + const findOffset = (ref) => { + for (const sub of xrefSection.subsections) { + for (const entry of sub) { + if (!entry.deleted && + entry.ref.objectNumber === ref.objectNumber && + entry.ref.generationNumber === ref.generationNumber) { + return entry.offset; + } + } + } + return -1; + }; + const catalogOffset = findOffset(rootRef); + const infoOffset = findOffset(infoRef); + if (catalogOffset < 0) throw new Error(`incremental-pdf: catalog ref ${rootRef.toString()} not in xref`); + if (infoOffset < 0) throw new Error(`incremental-pdf: info ref ${infoRef.toString()} not in xref`); + + // 4. Parse just those two indirect objects into a fresh writing context. + // The parser will set context.indirectObjects[ref] for each. + const writingContext = parser.context; // already populated by parseHeader; reuse. + parser.bytes.moveTo(catalogOffset); + await parser.parseIndirectObject(); + parser.bytes.moveTo(infoOffset); + await parser.parseIndirectObject(); + + const catalogDict = writingContext.lookup(rootRef); + const infoDict = writingContext.lookup(infoRef); + if (!(catalogDict instanceof PDFDict)) throw new Error('incremental-pdf: parsed catalog is not a dict'); + if (!(infoDict instanceof PDFDict)) throw new Error('incremental-pdf: parsed info is not a dict'); + + // 5. Allocate refs for new outline objects starting at oldSize. The + // parser bumped largestObjectNumber while assigning Catalog/Info; reset + // it so nextRef() returns PDFRef.of(oldSize, 0) first. + writingContext.largestObjectNumber = oldSize - 1; + const outlineRootRef = buildOutline(writingContext, outline); + + // 6. Update Catalog + Info in place. Both are now in writingContext, + // keyed by their original refs; serialize() will emit them with those + // refs, overriding the original objects via xref offset. + if (outlineRootRef) catalogDict.set(PDFName.of('Outlines'), outlineRootRef); + if (meta.lang) catalogDict.set(PDFName.of('Lang'), PDFString.of(meta.lang)); + applyMetadataToInfo(infoDict, meta); + + // 7. Serialize each indirect object in ascending object-number order, + // recording absolute byte offsets so we can build the new xref. + const chunks = [buf]; + let offset = buf.length; + // Per PDF 1.7 §7.5.6, an incremental update must begin on a new line. + // Most %%EOF lines end with newline already; if not, add one. + if (buf[buf.length - 1] !== 0x0A) { + const nl = Buffer.from('\n'); + chunks.push(nl); + offset += nl.length; + } + + const xrefEntries = []; + for (const [ref, obj] of writingContext.enumerateIndirectObjects()) { + const header = Buffer.from(`${ref.objectNumber} ${ref.generationNumber} obj\n`); + const body = Buffer.alloc(obj.sizeInBytes()); + obj.copyBytesInto(body, 0); + const tail = Buffer.from('\nendobj\n'); + xrefEntries.push({ ref, offset }); + chunks.push(header, body, tail); + offset += header.length + body.length + tail.length; + } + + // 8. New xref section. PDFCrossRefSection.addEntry auto-groups + // contiguous ascending object numbers into subsections. The subsection + // covering object 0 -- the mandatory "0 65535 f" free entry -- already + // exists in the *original* xref, which readers reach via /Prev. We do + // not repeat it here. + const newXrefOffset = offset; + const xref = PDFCrossRefSection.createEmpty(); + for (const { ref, offset: off } of xrefEntries) { + xref.addEntry(ref, off); + } + const xrefBuf = Buffer.alloc(xref.sizeInBytes()); + xref.copyBytesInto(xrefBuf, 0); + chunks.push(xrefBuf, Buffer.from('\n')); + + // 9. New trailer dict. /Size must cover the highest object number we + // emitted; that's writingContext.largestObjectNumber + 1. /Prev points + // at the original xref so readers chain back through it. Preserve /ID + // from the original trailer when present -- Acrobat warns on its absence + // and some readers use it as a file fingerprint. + const trailerSpec = { + Size: writingContext.largestObjectNumber + 1, + Root: rootRef, + Info: infoRef, + Prev: oldXrefOffset, + }; + const oldId = trailerDict.get(PDFName.of('ID')); + if (oldId) trailerSpec.ID = oldId; + const newTrailerDict = writingContext.obj(trailerSpec); + const trailerWrapper = PDFTrailerDict.of(newTrailerDict); + const trailerBuf = Buffer.alloc(trailerWrapper.sizeInBytes()); + trailerWrapper.copyBytesInto(trailerBuf, 0); + chunks.push(trailerBuf, Buffer.from('\n')); + + // 10. startxref + %%EOF + chunks.push(Buffer.from(`startxref\n${newXrefOffset}\n%%EOF\n`)); + + const out = Buffer.concat(chunks); + return { + bytes: out, + stats: { + originalBytes: buf.length, + appendedBytes: out.length - buf.length, + newObjectCount: xrefEntries.length, + newXrefOffset, + oldXrefOffset, + oldSize, + newSize: writingContext.largestObjectNumber + 1, + }, + }; +} diff --git a/perf/measure.mjs b/perf/measure.mjs index 45ed556..115b832 100644 --- a/perf/measure.mjs +++ b/perf/measure.mjs @@ -13,17 +13,28 @@ // generate meta extraction + outline DOM walk + page.pdf(). // page.pdf() (Chromium serializing the laid-out DOM into // PDF bytes) typically dominates. -// process PDFDocument.load + setMetadata + setOutline + save. +// process default: PDFDocument.load + setMetadata + setOutline + save. +// --incremental: applyOutlineAndMetadataIncremental() -- skip +// the full pdf-lib parse and append an incremental update +// (outline objects + updated Catalog/Info + new xref + +// /Prev pointer) on top of Chrome's bytes. // // Usage: // node measure.mjs [path/to/book.html] [--out ] [--keep-open] // [--cpu-profile] [--cpu-sampling ] -// [--detach-pages] +// [--detach-pages] [--instrument] [--time-hooks] +// [--incremental] // // --detach-pages also injects detach-pages.js -- a Paged.Handler that // hides each completed page from the layout tree -- to test whether // the O(n^2) render hotspot disappears. // +// --incremental switches the process phase from a pdf-lib roundtrip to +// an incremental update against Chrome's bytes. Massively faster (sub- +// second), but the resulting file is the size of Chrome's raw PDF + +// outline (~3x bigger than the pdf-lib output, which deflate-compresses +// content streams during its full re-emit). +// // Defaults: // input : ../docs/_site-pdf/book.html (relative to this file) // output : perf/results// @@ -45,6 +56,7 @@ import { PDFDocument } from 'pdf-lib'; // "exports" field, which only re-exports the Printer class. import { parseOutline, setOutline } from './node_modules/pagedjs-cli/src/outline.js'; import { setMetadata } from './node_modules/pagedjs-cli/src/postprocesser.js'; +import { applyOutlineAndMetadataIncremental } from './incremental-pdf.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -57,6 +69,7 @@ let cpuSampling = 1000; // microseconds let detachPages = false; let instrument = false; let timeHooks = false; +let incremental = false; for (let i = 0; i < args.length; i++) { const a = args[i]; if (a === '--out') outArg = args[++i]; @@ -66,6 +79,7 @@ for (let i = 0; i < args.length; i++) { else if (a === '--detach-pages') detachPages = true; else if (a === '--instrument') instrument = true; else if (a === '--time-hooks') timeHooks = true; + else if (a === '--incremental') incremental = true; else if (!inputArg) inputArg = a; else { console.error(`unknown arg: ${a}`); process.exit(2); } } @@ -242,29 +256,51 @@ try { console.log(`[harness] generate ${fmtMs(generateMs)} (parseOutline=${fmtMs(parseOutlineMs)}, page.pdf=${fmtMs(pdfMs)}, ${(rawPdf.length / 1024 / 1024).toFixed(1)}MB)`); // PROCESS --------------------------------------------------------- - // pdf-lib roundtrip: parse Chromium's PDF, attach outline + metadata, - // re-serialise. setTrimBoxes is omitted -- our pages have no bleed, - // and capturing the page-array from in-browser would need extra wiring - // for what is essentially a no-op here. + // Two paths: + // default : pdf-lib roundtrip -- load + setMetadata + setOutline + // + save. The whole 52 MB Chrome PDF gets parsed and + // re-emitted just so we can attach an outline. + // --incremental : applyOutlineAndMetadataIncremental -- parse only the + // trailer, xref, Catalog and Info objects; append a + // few KB containing the outline tree + updated Catalog + // and Info + a new xref subsection whose /Prev points + // at Chrome's original xref. Original bytes untouched. + // + // Either way we time the full phase plus the meaningful sub-steps so the + // breakdown matches across runs. const tProcStart = Date.now(); - - const tLoadStart = Date.now(); - const pdfDoc = await PDFDocument.load(rawPdf); - const loadMs = Date.now() - tLoadStart; - - setMetadata(pdfDoc, meta); - - const tSetOutlineStart = Date.now(); - setOutline(pdfDoc, outline, false); - const setOutlineMs = Date.now() - tSetOutlineStart; - - const tSaveStart = Date.now(); - const finalPdf = await pdfDoc.save(); - const saveMs = Date.now() - tSaveStart; - - const tProcEnd = Date.now(); + let finalPdf; + let processBreakdown; + if (incremental) { + const tIncStart = Date.now(); + const { bytes, stats } = await applyOutlineAndMetadataIncremental(rawPdf, outline, meta); + const incMs = Date.now() - tIncStart; + finalPdf = bytes; + processBreakdown = { incrementalMs: incMs, ...stats }; + } else { + const tLoadStart = Date.now(); + const pdfDoc = await PDFDocument.load(rawPdf); + const loadMs = Date.now() - tLoadStart; + + setMetadata(pdfDoc, meta); + + const tSetOutlineStart = Date.now(); + setOutline(pdfDoc, outline, false); + const setOutlineMs = Date.now() - tSetOutlineStart; + + const tSaveStart = Date.now(); + finalPdf = await pdfDoc.save(); + const saveMs = Date.now() - tSaveStart; + + processBreakdown = { loadMs, setOutlineMs, saveMs }; + } + const tProcEnd = Date.now(); const processMs = tProcEnd - tProcStart; - console.log(`[harness] process ${fmtMs(processMs)} (load=${fmtMs(loadMs)}, setOutline=${fmtMs(setOutlineMs)}, save=${fmtMs(saveMs)})`); + if (incremental) { + console.log(`[harness] process ${fmtMs(processMs)} (incremental=${fmtMs(processBreakdown.incrementalMs)}, +${processBreakdown.appendedBytes}B, ${processBreakdown.newObjectCount} new objs)`); + } else { + console.log(`[harness] process ${fmtMs(processMs)} (load=${fmtMs(processBreakdown.loadMs)}, setOutline=${fmtMs(processBreakdown.setOutlineMs)}, save=${fmtMs(processBreakdown.saveMs)})`); + } const totalMs = tProcEnd - tRenderStart; console.log(`[harness] total ${fmtMs(totalMs)}`); @@ -293,9 +329,8 @@ try { }, process: { ms: processMs, - loadMs, - setOutlineMs, - saveMs, + mode: incremental ? 'incremental' : 'pdf-lib-roundtrip', + ...processBreakdown, }, }, totalMs, @@ -322,7 +357,7 @@ try { summary.push(''); summary.push(`render : ${fmtMs(renderMs)} (per-page layout via paged.js)`); summary.push(`generate : ${fmtMs(generateMs)} (parseOutline + page.pdf)`); - summary.push(`process : ${fmtMs(processMs)} (pdf-lib load + setOutline + save)`); + summary.push(`process : ${fmtMs(processMs)} (${incremental ? 'incremental update (append outline + updated catalog/info)' : 'pdf-lib load + setOutline + save'})`); summary.push(`total : ${fmtMs(totalMs)}`); summary.push(''); if (pages.length >= 4) { diff --git a/perf/test-incremental.mjs b/perf/test-incremental.mjs new file mode 100644 index 0000000..006f584 --- /dev/null +++ b/perf/test-incremental.mjs @@ -0,0 +1,133 @@ +// Smoke test for incremental-pdf.mjs. +// +// Renders the tiny probe HTML to PDF in headless Chromium, applies a +// synthetic outline + metadata via the incremental writer, writes the +// result, and validates the output by: +// +// 1. Re-parsing it with the existing pdf-lib full-load path (which +// walks the /Prev chain). If the incremental update is malformed +// pdf-lib throws here. +// 2. Confirming the outline tree is reachable from Catalog.Outlines. +// 3. Confirming Title/Author/Producer/CreationDate land in /Info. +// +// This isn't a perf measurement -- just a correctness gate before we +// wire the writer into measure.mjs. + +import { pathToFileURL, fileURLToPath } from 'node:url'; +import { dirname, resolve, join } from 'node:path'; +import { writeFileSync, mkdirSync } from 'node:fs'; +import puppeteer from 'puppeteer'; +import { PDFDocument, PDFName, PDFRef, PDFDict } from 'pdf-lib'; +import { applyOutlineAndMetadataIncremental } from './incremental-pdf.mjs'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const outDir = resolve(__dirname, 'results', 'incremental-smoke'); +mkdirSync(outDir, { recursive: true }); + +const browser = await puppeteer.launch({ + headless: true, + args: ['--disable-dev-shm-usage', '--export-tagged-pdf', '--allow-file-access-from-files'], +}); + +let exit = 0; +try { + const page = await browser.newPage(); + page.setDefaultTimeout(0); + await page.emulateMediaType('print'); + + // The destinations referenced by the synthetic outline must exist as + // named destinations in Chrome's PDF, otherwise the outline entries + // won't navigate anywhere. Chrome creates named destinations from + // `` links in the document, mapping the `id` of the + // target element to a page+coords destination. The hidden link-holder + // trick in pagedjs-cli/src/outline.js does the same thing. + const html = `Probe + + i + c1 +

Intro

+

Body of intro.

+

Chapter 1

+

Body of chapter.

+ `; + await page.setContent(html); + + const raw = await page.pdf({ + printBackground: true, displayHeaderFooter: false, + preferCSSPageSize: true, margin: { top: 0, right: 0, bottom: 0, left: 0 }, + }); + writeFileSync(join(outDir, 'raw.pdf'), Buffer.from(raw)); + console.log(`raw: ${raw.length} bytes`); + + const outline = [ + { title: 'Intro', destination: 'intro', children: [] }, + { title: 'Chapter 1', destination: 'chapter-1', children: [] }, + ]; + const meta = { + title: 'Smoke Test', + author: 'Harness', + subject: 'Incremental writer probe', + keywords: ['probe', 'incremental'], + lang: 'en', + }; + + const { bytes, stats } = await applyOutlineAndMetadataIncremental(raw, outline, meta); + writeFileSync(join(outDir, 'final.pdf'), bytes); + console.log('stats:', stats); + console.log(`growth: +${bytes.length - raw.length} bytes (${((bytes.length - raw.length) / 1024).toFixed(1)} KB)`); + + // 1. Round-trip through pdf-lib. This walks the /Prev chain and + // resolves everything via the incremental update on top of the + // original xref, so it's the strictest correctness check we can run + // without launching a viewer. + // + // updateMetadata: false stops PDFDocument.load from overwriting our + // /Producer with "pdf-lib (https://...)" on its way in. Without it + // the assertion below would read pdf-lib's value, not Chrome's. + const reparsed = await PDFDocument.load(bytes, { updateMetadata: false }); + console.log('reparse: OK'); + + // 2. Outline reachable from catalog + const outlinesRef = reparsed.catalog.get(PDFName.of('Outlines')); + if (!(outlinesRef instanceof PDFRef)) { + throw new Error('Catalog.Outlines is not a ref'); + } + const outlinesDict = reparsed.context.lookup(outlinesRef, PDFDict); + const firstRef = outlinesDict.get(PDFName.of('First')); + const lastRef = outlinesDict.get(PDFName.of('Last')); + const count = outlinesDict.get(PDFName.of('Count')); + console.log(`outline root: First=${firstRef.toString()} Last=${lastRef.toString()} Count=${count.toString()}`); + + // Walk the linked list of top-level entries to make sure prev/next/dest + // are wired up correctly. + let cur = firstRef, idx = 0; + while (cur) { + const node = reparsed.context.lookup(cur, PDFDict); + const title = node.get(PDFName.of('Title')).decodeText(); + const dest = node.get(PDFName.of('Dest')).toString(); + console.log(` [${idx}] ${title} -> ${dest}`); + const next = node.get(PDFName.of('Next')); + cur = next instanceof PDFRef ? next : null; + idx++; + if (idx > 100) throw new Error('outline walk did not terminate'); + } + + // 3. Metadata landed in /Info + console.log(`info.title = ${reparsed.getTitle()}`); + console.log(`info.author = ${reparsed.getAuthor()}`); + console.log(`info.subject = ${reparsed.getSubject()}`); + console.log(`info.keywords = ${reparsed.getKeywords()}`); + console.log(`info.producer = ${reparsed.getProducer()}`); + console.log(`info.creator = ${reparsed.getCreator()}`); + console.log(`info.created = ${reparsed.getCreationDate()?.toISOString()}`); + console.log(`info.modified = ${reparsed.getModificationDate()?.toISOString()}`); + console.log(`catalog.Lang = ${reparsed.catalog.get(PDFName.of('Lang'))?.toString()}`); + + console.log('--- smoke test passed ---'); +} catch (err) { + console.error('smoke test failed:', err); + exit = 1; +} finally { + await browser.close(); +} +process.exit(exit); From 2c31b37f5da37fc7ddb3c17c46eecf3aa6207d47 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Mon, 18 May 2026 22:58:38 +0200 Subject: [PATCH 08/26] Profile pdf-lib roundtrip; survey Chromium PDF knobs. Two findings that aren't behind the incremental writer: 1. pdf-lib's PDFDocument.load defaults to parseSpeed: Slow (100 objects/tick, with await waitForTick() between batches) and save defaults to objectsPerTick: 50. On the 52 MB Chrome PDF that's ~500+1000 cooperative yields per round-trip; a CPU profile shows 79 % of load is V8 idle wait, not actual parsing. The default was designed for browsers (don't lock the main thread for 30 s); in the harness with no concurrent work it's pure overhead. Switching to parseSpeed: Fastest + objectsPerTick: Infinity drops the roundtrip from 40 s to 4.7 s on the same input -- 8x speedup with no behaviour change. Wired into the harness's default roundtrip path. Production book.bat still pays the cost because pagedjs-cli passes no options to load/save; that's a separate follow-up. 2. Chromium gained Page.printToPDF { generateDocumentOutline: true } in M122 (Feb 2024). Puppeteer exposes it as `outline: true` and adds the launch flag automatically since v22. Chrome walks h1..h6 from the accessibility tree and emits /Outlines directly. Wired in as --chrome-outline. Caveats documented in README: - No tag-level filter (h1..h6 unconditional). Per-heading suppression works via role="presentation" / aria-hidden -- 1 attribute, visually unchanged. - Tree depth doesn't equal heading level: Chrome collapses skipped levels (h5 under h3 becomes depth+1, not depth+2). So filtering Chrome's output to "depth <= 3" does not extract h1..h4. - Diff against the injected outline on the real book shows significant structural drift (the injected outline gets 1773 entries, Chrome 6023 unfiltered / 1820 filtered, 10 of those point at different pages by end of book). - pagedjs-cli has never tried this option in any form -- searched github issues/PRs/code, gitlab, web; no mention. Probes and analysis scripts: profile-load.mjs, profile-roundtrip.mjs, probe-chrome-outline.mjs, probe-outline-exclusions.mjs, compare-outlines.mjs. All independently runnable. --- perf/README.md | 313 +++++++++++++++++++++++++++++- perf/compare-outlines.mjs | 169 ++++++++++++++++ perf/measure.mjs | 43 +++- perf/probe-chrome-outline.mjs | 96 +++++++++ perf/probe-outline-exclusions.mjs | 103 ++++++++++ perf/profile-load.mjs | 53 +++++ perf/profile-roundtrip.mjs | 46 +++++ 7 files changed, 809 insertions(+), 14 deletions(-) create mode 100644 perf/compare-outlines.mjs create mode 100644 perf/probe-chrome-outline.mjs create mode 100644 perf/probe-outline-exclusions.mjs create mode 100644 perf/profile-load.mjs create mode 100644 perf/profile-roundtrip.mjs diff --git a/perf/README.md b/perf/README.md index 5f983b6..037e533 100644 --- a/perf/README.md +++ b/perf/README.md @@ -50,7 +50,7 @@ DevTools-compatible trace is a few lines. | File | Role | | --- | --- | -| `package.json` | Pins `puppeteer` + `pagedjs-cli` + `patch-package`. | +| `package.json` | Pins `puppeteer` + `pagedjs-cli`. | | `measure.mjs` | Puppeteer harness. Mirrors pagedjs-cli's own `Printer.pdf()` flow, with optional CPU profiling, in-page handler injection, and DOM-accessor instrumentation. | | `timing-handler.js` | `Paged.Handler` that records per-page wall time + heap into `window.__pagedTiming` and streams a line per page to the console. Always injected. | | `detach-pages.js` | `Paged.Handler` that hides each completed page from the layout tree (registered against `finalizePage`). The fix. Injected by `--detach-pages` and by `docs/book.bat`. | @@ -58,6 +58,11 @@ DevTools-compatible trace is a few lines. | `time-hooks.js` | Wraps every task registered to `chunker.hooks.*` and `polisher.hooks.*` with a wall-clock timer. Tells you which handler's hook method is eating render time, per page. Injected by `--time-hooks`. | | `incremental-pdf.mjs` | Replaces the pdf-lib load+save roundtrip with a PDF 1.7 §7.5.6 incremental update appended to Chrome's bytes. Used by `--incremental`. | | `test-incremental.mjs` | Smoke test for `incremental-pdf.mjs`: renders a tiny probe page, runs the writer, verifies the result parses (via pdf-lib re-load) and that outline + metadata land correctly. | +| `profile-load.mjs` | Standalone profiler for `PDFDocument.load`. Runs the load on a chosen PDF with a chosen `parseSpeed`; intended to be run under `node --cpu-prof`. | +| `profile-roundtrip.mjs` | Times the full pdf-lib `load + save` roundtrip across the three `parseSpeed` / `objectsPerTick` settings on a chosen PDF. | +| `probe-chrome-outline.mjs` | Renders a synthetic multi-level h1..h6 document via Chrome's `outline: true` and dumps the resulting `/Outlines` tree. Quick check that the CDP flag is wired correctly in the local Chromium / puppeteer combo. | +| `compare-outlines.mjs` | Diffs two PDFs' `/Outlines` trees by `(depth, title, target page)`. Used to verify whether Chrome's native outline matches the injected one. | +| `probe-outline-exclusions.mjs` | Tests which per-element attributes / styles (aria-hidden, role=presentation, hidden, display:none, CSS bookmark-level, ...) make Chrome drop a heading from its outline. | | `analyze-profile.mjs` | Bottom-up self-time analyzer for `.cpuprofile` files. Same shape as DevTools' Performance bottom-up view, in the terminal. | | `run.bat` | Windows wrapper. Installs deps on first run, then invokes `node measure.mjs`. | | `results/` | Output, one timestamped subfolder per run. Git-ignored. | @@ -102,6 +107,7 @@ run.bat --cpu-profile # CPU-profile the render phase run.bat --instrument # count + time DOM-accessor calls run.bat --time-hooks # per-task timing of every chunker/polisher hook run.bat --incremental # process via incremental update instead of pdf-lib roundtrip +run.bat --chrome-outline # let Chrome emit /Outlines (skip parseOutline + setOutline) ``` Flags compose. The CPU profile lands as `render.cpuprofile` @@ -396,7 +402,7 @@ bundle patch** -- a 20-line `Paged.Handler` subclass that sets `pageElement.style.display = 'none'` in `afterPageLayout` and restores them at `afterRendered` before `page.pdf()` runs. The existing `--additional-script` mechanism is exactly the extension -point this needs, so no fork or patch-package diff is required. +point this needs, so no fork required. Wired into production in `docs/book.bat`: @@ -408,12 +414,6 @@ npx pagedjs-cli _site-pdf\book.html -o _pdf\book.pdf ^ And into the perf harness via the `--detach-pages` flag. -The `patches/` infrastructure (patch-package wired into both -`docs/package.json` and `perf/package.json`, sharing a single -`/patches` directory at the repo root) is left in place even -though we didn't use it -- it's the obvious fallback if a future -optimisation actually needs to modify the bundle. - ### Results Three-phase numbers, same 1638-page book, measured via the harness: @@ -677,3 +677,300 @@ replace the `pagedjs-cli` invocation in `book.bat` with a thin Node driver -- essentially `measure.mjs` minus the timing scaffolding -- that uses `puppeteer` + `incremental-pdf.mjs` directly. The harness already proves this is a ~30-line script. + +## Profiling pdf-lib's load: 79 % was idle yielding + +The "Fix applied: detach-pages" section above showed the pdf-lib +roundtrip at 39.7 s for the process phase. After profiling, **most +of that wasn't pdf-lib doing work -- it was pdf-lib yielding to the +event loop**. + +`PDFDocument.load` defaults to `parseSpeed: ParseSpeeds.Slow = 100` +objects per tick, with an `await waitForTick()` between batches. +`pdfDoc.save` does the same with `objectsPerTick: 50`. For our +~50k-object PDF that's ~500 yields during load, ~1000 during save, +each costing ~5-10 ms of pure idle on a quiet system. + +A CPU profile of `PDFDocument.load` running standalone on the 52 MB +Chrome output (`node --cpu-prof`, fresh process, no concurrent work): + +``` +samples: 3441 duration: 6.09s us/sample: 1770 + + self_ms self_% function @ source + ------- ------ ---------------------------------------------- + 4766.25 78.92% (idle) (V8 idle wait) + 251.41 4.16% PDFRef.of PDFRef.js:34 + 196.53 3.25% (garbage collector) + 116.85 1.93% (program) + 63.74 1.06% PDFObjectParser.parseString + 46.03 0.76% BaseParser.parseRawInt + 38.95 0.64% BaseParser.parseRawNumber + 35.41 0.59% PDFObjectParser.parseNumberOrRef +``` + +On a 6 s load, **4.77 s is V8 sitting on its hands** between +scheduled batches. Actual parsing self-time is well under a second; +the rest is GC and V8 internals. + +Why such a cautious default? pdf-lib targets the browser too, where +locking the main thread for 30+ s to parse a big PDF would freeze the +page. In Node, with the harness having no other work to do, yielding +is pure overhead. + +### Wins from `parseSpeed: Fastest` (objects/tick = Infinity) + +Three-variant roundtrip on the same 52 MB PDF, fresh process each +time (`profile-roundtrip.mjs`): + +| parseSpeed / objectsPerTick | load | save | total | +| --- | --- | --- | --- | +| **Slow / 50 (default)** | 36.7 s | 3.8 s | 40.5 s | +| Fast / 1500 | 3.0 s | 2.6 s | 5.6 s | +| **Fastest / Infinity** | **2.0 s** | **2.7 s** | **4.7 s** | + +`save` is barely affected by `objectsPerTick` -- its CPU work +dominates the yield overhead -- but `load` collapses by **18x**. + +### Wired into the harness + +`measure.mjs`'s default pdf-lib roundtrip path now passes +`parseSpeed: ParseSpeeds.Fastest` and `objectsPerTick: Infinity`. +End-to-end on the book (`--detach-pages`, default = pdf-lib path, +no `--incremental`): + +| Phase | Old pdf-lib defaults | Fast knobs | Δ | +| -------- | -------------------- | ---------- | --- | +| render | 50.9 s | 45.7 s | noise | +| generate | 60.2 s | 52.4 s | noise (Chrome variance) | +| process | 39.7 s | 7.8 s | **-31.9 s (-80 %)** | +| **total**| **150.7 s** | **105.9 s** | **-44.8 s (-30 %)** | + +Result: the pdf-lib roundtrip is now **competitive with the +incremental writer** (105.9 s vs 110.3 s total) **while still +producing a 17 MB output** (vs 53 MB for incremental, because +`save()` flate-compresses content streams as it re-emits them). + +### What this reinterprets + +The "Fix applied: detach-pages" table is still accurate, but its +39.7 s process column reflects pdf-lib's default tick-yielding, not +its actual work. A reader benchmarking pdf-lib on its merits should +compare against the **7.8 s** number, not 40 s. + +The incremental writer (above) still produces the fastest process +phase by far (0.25 s) and remains useful when sub-second matters +more than file size. But for the common case the single-line +`parseSpeed: Fastest` tweak is the immediate win. + +## Chromium `Page.printToPDF` knob survey + +While we were here, we audited which Chromium / CDP options affect +PDF output. Partly to confirm "is there something Chrome could +compress for us?" (no), partly because one option turned out to be +a real win: `outline: true`. + +Verified against `devtools-protocol@0.0.1312386` and +`puppeteer-core@22.15.0` (both shipped under `perf/node_modules`). + +### `outline: true` -- Chrome can emit /Outlines itself + +CDP's `Page.printToPDF` accepts `generateDocumentOutline: true` since +Chrome M122 (Feb 2024). Puppeteer exposes it as `outline: true` since +v22.x. Behaviour: + +- Chrome walks the rendered DOM's `

..

` once and emits a + /Outlines tree with **page+coords destinations** (`[N 0 R /XYZ x y z]`) + instead of named destinations. +- Implies `tagged: true` (the outline is built from the accessibility + tree). Puppeteer enforces this in `util.ts:395`. +- Requires the launch flag `--generate-pdf-document-outline`. The + harness's `puppeteer.launch` args include it; `pagedjs-cli`'s + default args do *not*, so production integration would need a + launch-arg change too. +- **No tag-level filter**: walks `h1..h6` unconditionally. There is + no equivalent of our `--outline-tags h1,h2,h3,h4` knob. + +Measured cost on the 1638-page book with `--chrome-outline --detach-pages`: + +| Phase | injected outline | Chrome outline | Δ | +| -------- | ---------------- | -------------- | --- | +| generate | 52.4 s | 53.8 s | +1.4 s (Chrome walking the headings) | +| process | 7.8 s | 5.3 s | -2.5 s (no outline objects to save) | +| **total**| **105.9 s** | **107.8 s** | +1.9 s | + +Total is roughly a wash -- one cost shifts to another. The real +benefit is **fewer moving parts**: no `parseOutline`, no +`setOutline`, no incremental-writer outline objects, just metadata. + +### Does Chrome's outline match the injected one? + +We diffed the two outputs on the 1638-page book (`compare-outlines.mjs`): +`results/pdf-lib-fastest/book.pdf` (injected, 1773 entries from +`--outline-tags h1..h4`) versus `results/chrome-outline-on/book.pdf` +(Chrome's, 6023 entries total). + +Naïvely filtering Chrome's tree to "depth ≤ 3" to approximate our +h1..h4 view gives 1820 entries -- close to 1773 in count, but **not +equivalent** structurally. Two reasons: + +1. **Chrome walks all h1..h6 unconditionally.** First concrete + divergence is at the "Alias Types" section: the source + ([book.html:302](docs/_site-pdf/book.html:302)) has + `
Example
` + immediately after the h3 "Alias Types". Our `--outline-tags` + filter correctly drops it; Chrome includes it. Every such + insertion shifts the rest of the pre-order walk. +2. **Chrome's tree depth ≠ HTML heading level.** Chrome collapses + skipped levels: an `
` directly under an `

` becomes + depth+1 (not depth+2). So "filter to depth ≤ 3" does *not* + extract "h1..h4 only" -- it extracts the first four levels of + *nesting*, which can be any mix of h1..h6 depending on context. + +Numerical summary: + +| metric | value | +| --- | --- | +| injected entries | 1773 | +| Chrome entries (h1..h6, all depths) | 6023 | +| Chrome entries filtered to depth ≤ 3 | 1820 | +| pre-order matches (vs injected) | 27 / 1820 | +| same title+depth, different page | 10 | + +The 10 "page-only mismatches" are the smoking gun for structural +drift: same heading title in both outlines but pointing at different +sections of the book. The deltas grow as the walk progresses -- +e.g. "Properties" at A=p956 vs B=p883 (Δ = -73 pages), and similar +near the end of the book. By that point Chrome and our outline are +literally talking about different headings that happen to share a +name (every class in the reference docs has its own "Properties" +sub-heading). + +### Selectively excluding headings from Chrome's outline + +Chrome's outline is built from the accessibility tree (puppeteer +enforces `tagged: true` alongside `outline: true` for this reason). +Anything that hides a heading from a11y excludes it from the outline. +Tested matrix (`probe-outline-exclusions.mjs`): + +**Excluded** -- the heading is dropped from `/Outlines`: + +| attribute on the heading or an ancestor | clean? | notes | +| --- | --- | --- | +| `role="presentation"` | yes | Removes heading semantic only. Visual rendering, DOM, anchor `#id` targets all unchanged. **The cleanest knob.** | +| `role="none"` | yes | Alias of `presentation`. | +| `role="generic"` | yes | Any non-heading role works. | +| `aria-hidden="true"` | - | Excludes the whole subtree from a11y. Heavier -- also affects screen readers. | +| `hidden` attribute | no | Also visually hides. | +| `display: none` | no | Same. | +| `visibility: hidden` | no | Same. | + +**No effect** -- Chrome ignores these: + +| attribute | why | +| --- | --- | +| `bookmark-level: none` (CSS GCPM) | Chrome doesn't implement GCPM. | + +**Reverse direction.** `
Foo
` +*adds* an h3-level entry to Chrome's outline despite not being an +HTML heading. Useful if you ever want an outline entry that doesn't +look like a heading on screen. + +**Implication for our pipeline.** The "Chrome's outline is too +noisy" objection above isn't actually structural -- it's one CSS +selector away from being fixed. A preprocessor step that adds +`role="presentation"` to every `

` and `
` in the Jekyll +build would let Chrome's `outline: true` produce the same h1..h4 +view we want today. We haven't done that step yet, so we still +ship the injected outline -- but the path from "Chrome's outline +works for measurement only" to "Chrome's outline ships in +production" is now ~5 lines of Jekyll plugin code, not a +fundamental redesign. + +### Did pagedjs-cli ever try Chrome's outline? + +No. Searched (`gh api search/issues`, `gh api search/code`, and +web search): + +- `repo:pagedjs/pagedjs-cli outline` -- 2 hits, both unrelated + (TOC page-number bug, rowspan/colspan). +- `org:pagedjs chromium outline` -- 1 hit (the same TOC bug). +- `"pagedjs printToPDF outline"` -- 0 hits. +- `generateDocumentOutline org:pagedjs` (code search) -- 0 hits. +- `"--generate-pdf-document-outline" org:pagedjs` -- 0 hits. + +Timing: Chrome's `generateDocumentOutline` shipped M122 (Feb 2024); +[pagedjs-cli](https://github.com/pagedjs/pagedjs-cli)'s last +meaningful change is May 2024 (Docker hyphenation). The project +is in near-maintenance mode (21 stars). The feature post-dates +active development, and the unfilterable-outline regression +(without the `role="presentation"` workaround above) would have +been a real concern for existing `--outline-tags` users -- so +even a casual look would probably have ended in "we'll keep +injecting for now". Nobody appears to have looked. + +### What's not exposed in CDP (we checked) + +- **No stream-compression flag.** Chromium uses Skia's `SkPDF`, + which writes content streams uncompressed. There's a C++-only + `SkPDF::Metadata::fPDFA` setting; no CDP plumbing for it. This is + *why* `save()` re-emission shrinks 52 MB → 17 MB. +- **No object-streams flag, no font subsetting / image downsampling + knobs, no PDF/A mode.** Skia subsets fonts automatically per face. +- **No parallelism knob.** Generate's 60 s in `page.pdf()` is + single-threaded Skia walking the layout tree. + +### What might still be worth trying + +- **`tagged: false`** -- drops the StructTreeRoot, saving ~10-20 % + of generate time and file size. Loses accessibility *and* the + Chrome outline (tagging is a prerequisite). Probably a no for + our use; documenting for completeness. +- **`pageRanges` sharding** -- run `page.pdf()` N times with + disjoint ranges on parallel browser pages. Each shard serialises + only its slice and they run concurrently. Biggest unused lever + for the 60 s generate phase, but requires a PDF concatenation + post-pass (pdf-lib can do it). +- **`transferMode: 'ReturnAsStream'`** -- puppeteer already + hard-codes it. Without it Chrome buffers + base64-encodes the + whole PDF into one JSON message; very slow and memory-heavy. + +## Where this leaves us + +The full menu of fixes, all measured against the original 207 s +baseline: + +| Configuration | render | generate | process | total | size | +| --- | --- | --- | --- | --- | --- | +| original | 103.8s | 63.6s | 39.6s | 207.0s | 17 MB | +| + detach-pages | 50.9s | 60.2s | 39.7s | 150.7s | 17 MB | +| + detach + **parseSpeed:Fastest** | 45.7s | 52.4s | 7.8s | **105.9s** | **17 MB** | +| + detach + incremental writer | 49.2s | 60.9s | 0.25s | 110.3s | 53 MB | +| + detach + Chrome outline | 48.7s | 53.8s | 5.3s | 107.8s | 17 MB | + +**Practical winner: `+ detach + parseSpeed:Fastest`.** Half the +original wall time, same output size, one-line change. Ship this +first regardless of what else gets layered on top. + +The incremental writer is still the fastest process phase (0.25 s) +and remains the right answer if file size doesn't matter and +sub-second process does. + +Chrome's outline is the simplest *architecture* (no parseOutline, +no setOutline, no incremental outline objects -- just metadata), +and the "unfilterable h1..h6" objection turns out to be a +preprocessor change away from being solved: tag every `
` / +`
` in the Jekyll build with `role="presentation"` and Chrome's +outline collapses to the same h1..h4 view we want today. With that +change, the totals look like: + +| Configuration | render | generate | process | total | size | +| --- | --- | --- | --- | --- | --- | +| + detach + parseSpeed:Fastest *(today)* | 45.7s | 52.4s | 7.8s | 105.9s | 17 MB | +| + detach + parseSpeed:Fastest + Chrome outline | 48.7s | 53.8s | 5.3s | 107.8s | 17 MB | +| *(latter, with role="presentation" on h5/h6 -- pending)* | | | | | | + +The compound win isn't in wall time -- it's in deleting code: +`parseOutline`, `setOutline`, and the entire outline branch of the +incremental writer all go away. Worth it if/when someone wants to +trim the surface area. diff --git a/perf/compare-outlines.mjs b/perf/compare-outlines.mjs new file mode 100644 index 0000000..b555290 --- /dev/null +++ b/perf/compare-outlines.mjs @@ -0,0 +1,169 @@ +// Diff two PDFs' /Outlines trees by (depth, title, target page). +// +// The two inputs should be the same source rendered with each outline +// strategy: +// +// A = injected -- our parseOutline + setOutline path +// (--outline-tags h1,h2,h3,h4, named destinations) +// B = Chrome -- page.pdf({ outline: true }) +// (walks h1..h6 unfiltered, page-coord destinations) +// +// For each entry we record: +// - depth in the outline tree (0 = top-level entry) +// - title (decoded text, trimmed) +// - resolved page index (1-based, for human readability) +// +// We then walk both trees in pre-order and compare. Chrome's outline +// can be deeper because it includes h5/h6; we filter to depth <= 3 +// (h1..h4) before comparing so we're contrasting like-with-like. +// +// Usage: +// node compare-outlines.mjs + +import { readFileSync, writeFileSync } from 'node:fs'; +import { resolve, basename } from 'node:path'; +import { PDFDocument, PDFName, PDFRef, PDFDict, PDFArray, PDFString, PDFHexString, ParseSpeeds } from 'pdf-lib'; + +const [aPath, bPath] = process.argv.slice(2); +if (!aPath || !bPath) { + console.error('usage: node compare-outlines.mjs '); + process.exit(2); +} + +async function loadDoc(p) { + const bytes = readFileSync(resolve(process.cwd(), p)); + return PDFDocument.load(bytes, { updateMetadata: false, parseSpeed: ParseSpeeds.Fastest }); +} + +function decodeTitle(t) { + if (!t) return ''; + if (t instanceof PDFHexString || t instanceof PDFString) return t.decodeText(); + return t.toString(); +} + +// Resolve an outline /Dest (array or name) to its target page ref. +// - Array: [pageRef /XYZ x y z ...] -- take element 0. +// - Name: look up in catalog /Dests dictionary; that resolves to an +// array of the same shape. +function resolveDestPageRef(doc, destObj) { + let arr = destObj; + if (arr instanceof PDFName) { + const destsRef = doc.catalog.get(PDFName.of('Dests')); + const dests = destsRef instanceof PDFRef ? doc.context.lookup(destsRef) : destsRef; + if (!dests) return null; + // /Dests can be a Name Tree (rarely from Chrome) or a flat dict. + // Chrome flat-dicts it for our content, so try .get(name) first. + const entry = dests.get ? dests.get(arr) : null; + arr = entry instanceof PDFRef ? doc.context.lookup(entry) : entry; + } + if (arr instanceof PDFArray && arr.size() > 0) { + const first = arr.get(0); + return first instanceof PDFRef ? first : null; + } + return null; +} + +function buildPageIndex(doc) { + const pages = doc.getPages(); + const m = new Map(); + for (let i = 0; i < pages.length; i++) m.set(pages[i].ref, i + 1); + return m; +} + +function flattenOutline(doc) { + const pageIndex = buildPageIndex(doc); + const outlinesRef = doc.catalog.get(PDFName.of('Outlines')); + if (!(outlinesRef instanceof PDFRef)) return []; + const root = doc.context.lookup(outlinesRef, PDFDict); + const out = []; + function walk(firstRef, depth) { + let cur = firstRef; + while (cur instanceof PDFRef) { + const node = doc.context.lookup(cur, PDFDict); + const title = decodeTitle(node.get(PDFName.of('Title'))).trim(); + const destObj = node.get(PDFName.of('Dest')); + const pageRef = destObj ? resolveDestPageRef(doc, destObj) : null; + const page = pageRef ? pageIndex.get(pageRef) ?? null : null; + out.push({ depth, title, page }); + const child = node.get(PDFName.of('First')); + if (child instanceof PDFRef) walk(child, depth + 1); + cur = node.get(PDFName.of('Next')); + } + } + walk(root.get(PDFName.of('First')), 0); + return out; +} + +const [docA, docB] = await Promise.all([loadDoc(aPath), loadDoc(bPath)]); +const flatA = flattenOutline(docA); +const flatBfull = flattenOutline(docB); +const flatB = flatBfull.filter(e => e.depth <= 3); // h1..h4 only + +console.log(`A (${basename(aPath)}): ${flatA.length} entries (depth 0..${Math.max(...flatA.map(e => e.depth), 0)})`); +console.log(`B (${basename(bPath)}): ${flatBfull.length} entries total, ${flatB.length} after filter to depth<=3 (depths present: ${[...new Set(flatBfull.map(e => e.depth))].sort().join(',')})`); +console.log(''); + +// Pre-order walk diff. Walk both in order and compare entry-by-entry. +// Mismatches: print up to N adjacent ones with context. +const max = Math.max(flatA.length, flatB.length); +const mismatches = []; +for (let i = 0; i < max; i++) { + const a = flatA[i]; + const b = flatB[i]; + if (!a || !b) { + mismatches.push({ i, a, b, kind: 'length' }); + continue; + } + const titleEq = a.title === b.title; + const depthEq = a.depth === b.depth; + const pageEq = a.page === b.page; + if (!titleEq || !depthEq || !pageEq) { + mismatches.push({ i, a, b, kind: 'value', titleEq, depthEq, pageEq }); + } +} + +console.log(`matches: ${max - mismatches.length} / ${max}`); +console.log(`mismatches: ${mismatches.length}`); + +// Page-only mismatch (same title + depth) is the most interesting -- +// tells us if the two paths target different pages for the same heading. +const titleAndDepthOnly = mismatches.filter(m => m.kind === 'value' && m.titleEq && m.depthEq && !m.pageEq); +const titleMismatch = mismatches.filter(m => m.kind === 'value' && !m.titleEq); +const depthMismatch = mismatches.filter(m => m.kind === 'value' && !m.depthEq); +console.log(''); +console.log(` title differs: ${titleMismatch.length}`); +console.log(` depth differs: ${depthMismatch.length}`); +console.log(` only page differs: ${titleAndDepthOnly.length}`); + +console.log(''); +console.log('--- first 25 mismatches ---'); +for (const m of mismatches.slice(0, 25)) { + if (m.kind === 'length') { + console.log(`[${m.i}] LENGTH-ONLY A=${m.a ? `${' '.repeat(m.a.depth)}${m.a.title} (p${m.a.page})` : ''} B=${m.b ? `${' '.repeat(m.b.depth)}${m.b.title} (p${m.b.page})` : ''}`); + } else { + const a = m.a, b = m.b; + console.log(`[${m.i}] A: ${' '.repeat(a.depth)}${a.title} (p${a.page})`); + console.log(` B: ${' '.repeat(b.depth)}${b.title} (p${b.page})`); + } +} + +if (titleAndDepthOnly.length) { + console.log(''); + console.log('--- first 25 page-only mismatches (same title+depth, different page) ---'); + for (const m of titleAndDepthOnly.slice(0, 25)) { + const a = m.a, b = m.b; + const delta = (b.page ?? 0) - (a.page ?? 0); + console.log(`[${m.i}] ${' '.repeat(a.depth)}${a.title} A=p${a.page} B=p${b.page} (Δ=${delta >= 0 ? '+' : ''}${delta})`); + } +} + +// Dump both for offline diff. +const stamp = new Date().toISOString().replace(/[:.]/g, '-'); +const dump = (fname, flat) => { + const lines = flat.map(e => `${e.depth}\t${e.page ?? '-'}\t${e.title}`); + writeFileSync(fname, lines.join('\n') + '\n'); +}; +dump(`results/outline-compare-A-${stamp}.tsv`, flatA); +dump(`results/outline-compare-B-${stamp}.tsv`, flatB); +console.log(''); +console.log(`full dumps: results/outline-compare-{A,B}-${stamp}.tsv`); diff --git a/perf/measure.mjs b/perf/measure.mjs index 115b832..dbe2615 100644 --- a/perf/measure.mjs +++ b/perf/measure.mjs @@ -23,7 +23,7 @@ // node measure.mjs [path/to/book.html] [--out ] [--keep-open] // [--cpu-profile] [--cpu-sampling ] // [--detach-pages] [--instrument] [--time-hooks] -// [--incremental] +// [--incremental] [--chrome-outline] // // --detach-pages also injects detach-pages.js -- a Paged.Handler that // hides each completed page from the layout tree -- to test whether @@ -35,6 +35,14 @@ // outline (~3x bigger than the pdf-lib output, which deflate-compresses // content streams during its full re-emit). // +// --chrome-outline asks Chrome itself to emit the /Outlines tree (CDP's +// generateDocumentOutline, M122+, requires --generate-pdf-document-outline +// at launch -- the harness always passes it). Skips the parseOutline DOM +// walk and the downstream setOutline injection; both pdf-lib and the +// incremental path see outline=[] and write nothing, leaving Chrome's +// outline intact. Chrome walks h1..h6 unconditionally -- no equivalent +// of our --outline-tags h1..h4 filter. +// // Defaults: // input : ../docs/_site-pdf/book.html (relative to this file) // output : perf/results// @@ -50,7 +58,7 @@ import { pathToFileURL, fileURLToPath } from 'node:url'; import { dirname, resolve, join } from 'node:path'; import { mkdirSync, writeFileSync, existsSync } from 'node:fs'; import puppeteer from 'puppeteer'; -import { PDFDocument } from 'pdf-lib'; +import { PDFDocument, ParseSpeeds } from 'pdf-lib'; // Deep-import the same outline + post-process helpers pagedjs-cli runs in // its own pdf() pipeline. Going via a relative path bypasses the package's // "exports" field, which only re-exports the Printer class. @@ -70,6 +78,7 @@ let detachPages = false; let instrument = false; let timeHooks = false; let incremental = false; +let chromeOutline = false; for (let i = 0; i < args.length; i++) { const a = args[i]; if (a === '--out') outArg = args[++i]; @@ -80,6 +89,7 @@ for (let i = 0; i < args.length; i++) { else if (a === '--instrument') instrument = true; else if (a === '--time-hooks') timeHooks = true; else if (a === '--incremental') incremental = true; + else if (a === '--chrome-outline') chromeOutline = true; else if (!inputArg) inputArg = a; else { console.error(`unknown arg: ${a}`); process.exit(2); } } @@ -129,9 +139,13 @@ const browser = await puppeteer.launch({ // Match pagedjs-cli's launch args (printer.js). --allow-file-access-from-files // is critical: without it paged.js's stylesheet fetch() rejects with // ProgressEvent under file://. pagedjs-cli sets it via cli.js:67. + // + // --export-tagged-pdf and --generate-pdf-document-outline are added by + // puppeteer 22+ unconditionally in ChromeLauncher.defaultArgs(), so + // we don't need to repeat them here. --chrome-outline below relies on + // the latter being present at launch. args: [ '--disable-dev-shm-usage', - '--export-tagged-pdf', '--allow-file-access-from-files', '--enable-precise-memory-info', ], @@ -238,8 +252,11 @@ try { return m; }); + // Skip the parseOutline DOM walk when Chrome's about to emit the + // outline itself -- we'd just be doing redundant work whose result + // would get overwritten by Chrome's /Outlines anyway. const tParseOutlineStart = Date.now(); - const outline = await parseOutline(page, outlineTags); + const outline = chromeOutline ? [] : await parseOutline(page, outlineTags); const parseOutlineMs = Date.now() - tParseOutlineStart; const tPdfStart = Date.now(); @@ -248,6 +265,13 @@ try { displayHeaderFooter: false, preferCSSPageSize: true, margin: { top: 0, right: 0, bottom: 0, left: 0 }, + // outline:true makes Chrome walk h1..h6 once and emit a /Outlines + // tree with page-coord destinations. Implies tagged:true (puppeteer + // enforces this) and requires --generate-pdf-document-outline at + // launch (set above). When on we skip the parseOutline+setOutline + // injection below -- that's the whole point of the flag, and leaving + // both on would have our setOutline overwrite Chrome's /Outlines. + ...(chromeOutline ? { outline: true, tagged: true } : {}), }); const pdfMs = Date.now() - tPdfStart; @@ -278,8 +302,15 @@ try { finalPdf = bytes; processBreakdown = { incrementalMs: incMs, ...stats }; } else { + // pdf-lib's defaults are catastrophically slow: parseSpeed=Slow (100 + // objects/tick) and objectsPerTick=50 both yield to the event loop + // between batches, turning a ~2s load into ~36s on a 52 MB PDF (~34s + // pure idle in the cpuprofile). Override to Fastest/Infinity so the + // "baseline" we report reflects the library's actual CPU cost, not + // an artefact of yielding cadence. The harness has no parallel work + // to make space for, so cooperative yielding is pure overhead here. const tLoadStart = Date.now(); - const pdfDoc = await PDFDocument.load(rawPdf); + const pdfDoc = await PDFDocument.load(rawPdf, { parseSpeed: ParseSpeeds.Fastest }); const loadMs = Date.now() - tLoadStart; setMetadata(pdfDoc, meta); @@ -289,7 +320,7 @@ try { const setOutlineMs = Date.now() - tSetOutlineStart; const tSaveStart = Date.now(); - finalPdf = await pdfDoc.save(); + finalPdf = await pdfDoc.save({ objectsPerTick: Infinity }); const saveMs = Date.now() - tSaveStart; processBreakdown = { loadMs, setOutlineMs, saveMs }; diff --git a/perf/probe-chrome-outline.mjs b/perf/probe-chrome-outline.mjs new file mode 100644 index 0000000..bd27660 --- /dev/null +++ b/perf/probe-chrome-outline.mjs @@ -0,0 +1,96 @@ +// Test Chrome's built-in outline generation via Page.printToPDF's +// generateDocumentOutline parameter (Puppeteer's `outline: true`). +// +// Chrome walks the rendered DOM's

...

tree once and emits a +// /Outlines tree directly in the PDF. If this works for our content, +// we can drop the outline-injection step entirely -- no parseOutline, +// no setOutline, no incremental writer outline objects. +// +// Constraints (per the M122+ implementation): +// - Requires --generate-pdf-document-outline launch flag (puppeteer +// adds it for `outline: true`). +// - Implicitly requires generateTaggedPDF (puppeteer sets it). +// - Walks h1-h6 unconditionally; no per-tag opt-out like our +// --outline-tags filter. + +import { pathToFileURL, fileURLToPath } from 'node:url'; +import { dirname, resolve, join } from 'node:path'; +import { mkdirSync, writeFileSync } from 'node:fs'; +import puppeteer from 'puppeteer'; +import { PDFDocument, PDFName, PDFRef, PDFDict } from 'pdf-lib'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const outDir = resolve(__dirname, 'results', 'chrome-outline'); +mkdirSync(outDir, { recursive: true }); + +const browser = await puppeteer.launch({ + headless: true, + args: [ + '--disable-dev-shm-usage', + '--export-tagged-pdf', + '--generate-pdf-document-outline', + '--allow-file-access-from-files', + ], +}); + +try { + const page = await browser.newPage(); + page.setDefaultTimeout(0); + await page.emulateMediaType('print'); + + // Multi-level outline structure to confirm Chrome handles nesting. + const html = `Outline Probe + +

Chapter 1

+

Section 1.1

+

Body.

+

Section 1.2

+

Subsection 1.2.1

+

Chapter 2

+

Section 2.1

+

Deep heading (h4)

+
Deeper (h5 -- might still show)
+
Deepest (h6 -- might still show)
+ `; + await page.setContent(html); + + const pdf = await page.pdf({ + printBackground: true, + displayHeaderFooter: false, + preferCSSPageSize: true, + margin: { top: 0, right: 0, bottom: 0, left: 0 }, + outline: true, // <-- the new flag (puppeteer 22.x+) + tagged: true, // implied by outline:true, but explicit for clarity + }); + writeFileSync(join(outDir, 'probe.pdf'), Buffer.from(pdf)); + console.log(`pdf: ${pdf.length} bytes`); + + // Inspect the resulting outline. Walk the /Outlines tree depth-first + // and print each title + dest at its level so we can see what Chrome + // emitted. + const doc = await PDFDocument.load(pdf, { updateMetadata: false }); + const outlinesRef = doc.catalog.get(PDFName.of('Outlines')); + if (!(outlinesRef instanceof PDFRef)) { + console.log('no /Outlines in catalog -- Chrome did not emit one'); + } else { + const root = doc.context.lookup(outlinesRef, PDFDict); + const count = root.get(PDFName.of('Count')); + console.log(`/Outlines Count = ${count.toString()}`); + + const walk = (firstRef, depth) => { + let cur = firstRef; + while (cur instanceof PDFRef) { + const node = doc.context.lookup(cur, PDFDict); + const title = node.get(PDFName.of('Title'))?.decodeText(); + const dest = node.get(PDFName.of('Dest'))?.toString() ?? node.get(PDFName.of('A'))?.toString(); + console.log(`${' '.repeat(depth)}- ${title} -> ${dest}`); + const childFirst = node.get(PDFName.of('First')); + if (childFirst instanceof PDFRef) walk(childFirst, depth + 1); + cur = node.get(PDFName.of('Next')); + } + }; + walk(root.get(PDFName.of('First')), 0); + } +} finally { + await browser.close(); +} diff --git a/perf/probe-outline-exclusions.mjs b/perf/probe-outline-exclusions.mjs new file mode 100644 index 0000000..6a17568 --- /dev/null +++ b/perf/probe-outline-exclusions.mjs @@ -0,0 +1,103 @@ +// Probe which per-element attributes / styles make Chrome's +// generateDocumentOutline skip a heading. +// +// Chrome's outline is built from the accessibility tree (puppeteer +// enforces tagged:true alongside outline:true for this reason). Anything +// that hides the element from a11y *should* exclude it. We test each +// theory by rendering a doc with labelled headings and checking which +// titles survive into the /Outlines tree. +// +// The labels in each are what we'll look for in the resulting +// outline -- they're unique per row, so if an entry is present we know +// which one it is. + +import { fileURLToPath } from 'node:url'; +import { dirname, resolve, join } from 'node:path'; +import { mkdirSync, writeFileSync } from 'node:fs'; +import puppeteer from 'puppeteer'; +import { PDFDocument, PDFName, PDFRef, PDFDict, PDFArray, PDFString, PDFHexString, ParseSpeeds } from 'pdf-lib'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const outDir = resolve(__dirname, 'results', 'outline-exclusions'); +mkdirSync(outDir, { recursive: true }); + +// Each row has a unique label so we can detect inclusion/exclusion by +// title match. All headings are h3 except where noted -- normalises +// nesting and lets us see whether Chrome's depth-tracking changes when +// some headings are skipped (e.g. does it "see through" an excluded h3 +// and treat the next sibling as the same level?). +const ROWS = [ + { label: 'A baseline plain h3', html: '

A baseline plain h3

' }, + { label: 'B aria-hidden true on h3', html: '' }, + { label: 'C role presentation on h3', html: '

C role presentation on h3

' }, + { label: 'D role none on h3', html: '

D role none on h3

' }, + { label: 'E h3 inside aria-hidden parent', html: '' }, + { label: 'F h3 with hidden attribute', html: '' }, + { label: 'G h3 with display none', html: '

G h3 with display none

' }, + { label: 'H h3 with visibility hidden', html: '

H h3 with visibility hidden

' }, + { label: 'I div role heading aria-level 3', html: '
I div role heading aria-level 3
' }, + { label: 'J h3 with bookmark-level none', html: '

J h3 with bookmark-level none

' }, + { label: 'K h3 inside hidden parent', html: '' }, + { label: 'L h3 with role generic', html: '

L h3 with role generic

' }, + { label: 'M plain h3 trailing baseline', html: '

M plain h3 trailing baseline

' }, +]; + +const body = ROWS.map(r => `${r.html}

${r.label} body

`).join('\n'); +const html = ` + + Outline exclusion probe + +${body}`; + +const browser = await puppeteer.launch({ headless: true, args: ['--disable-dev-shm-usage'] }); +try { + const page = await browser.newPage(); + page.setDefaultTimeout(0); + await page.emulateMediaType('print'); + await page.setContent(html); + + const pdf = await page.pdf({ + printBackground: true, displayHeaderFooter: false, preferCSSPageSize: true, + margin: { top: 0, right: 0, bottom: 0, left: 0 }, + outline: true, tagged: true, + }); + writeFileSync(join(outDir, 'probe.pdf'), Buffer.from(pdf)); + console.log(`pdf: ${pdf.length} bytes`); + + // Collect what Chrome put in /Outlines. + const doc = await PDFDocument.load(pdf, { updateMetadata: false, parseSpeed: ParseSpeeds.Fastest }); + const outlinesRef = doc.catalog.get(PDFName.of('Outlines')); + const found = []; + if (outlinesRef instanceof PDFRef) { + const root = doc.context.lookup(outlinesRef, PDFDict); + const walk = (firstRef, depth) => { + let cur = firstRef; + while (cur instanceof PDFRef) { + const node = doc.context.lookup(cur, PDFDict); + const titleObj = node.get(PDFName.of('Title')); + const title = titleObj instanceof PDFHexString || titleObj instanceof PDFString + ? titleObj.decodeText() : '?'; + found.push({ depth, title }); + const child = node.get(PDFName.of('First')); + if (child instanceof PDFRef) walk(child, depth + 1); + cur = node.get(PDFName.of('Next')); + } + }; + walk(root.get(PDFName.of('First')), 0); + } + + // Report per-row: included or excluded. + console.log(''); + console.log('row included? depth'); + console.log('--- --- --- --- --- --- --- --- --- --- --- --- ---'); + for (const r of ROWS) { + const hit = found.find(f => f.title === r.label); + const status = hit ? ' yes' : 'NO'; + const depth = hit ? `d=${hit.depth}` : ''; + console.log(`${r.label.padEnd(48)} ${status} ${depth}`); + } + console.log(''); + console.log(`total outline entries: ${found.length}`); +} finally { + await browser.close(); +} diff --git a/perf/profile-load.mjs b/perf/profile-load.mjs new file mode 100644 index 0000000..6edc010 --- /dev/null +++ b/perf/profile-load.mjs @@ -0,0 +1,53 @@ +// One-shot: profile PDFDocument.load on a given PDF. +// +// We've measured load at ~35 s on a 52 MB Chrome PDF. That's an order +// of magnitude slower than reading the bytes (~250 ms for 52 MB SSD). +// If most of it is in a single hot path -- string concatenation in +// parseName/parseString, a slow xref scan, repeated context lookups -- +// we'd want to know before deciding whether to push more work onto +// pdf-lib or write our own minimal parser. +// +// Usage: +// node --cpu-prof --cpu-prof-name=load.cpuprofile profile-load.mjs +// +// The .cpuprofile lands in the current directory. Open it in Chrome +// DevTools -> Performance -> Load profile, or run analyze-profile.mjs +// against it for a terminal bottom-up self-time view. + +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { PDFDocument, ParseSpeeds } from 'pdf-lib'; + +const arg = process.argv[2]; +if (!arg) { + console.error('usage: node --cpu-prof profile-load.mjs [--speed slow|medium|fast|fastest]'); + process.exit(2); +} +const speedArg = process.argv[3] === '--speed' ? process.argv[4] : 'slow'; +const speedMap = { + slow: ParseSpeeds.Slow, + medium: ParseSpeeds.Medium, + fast: ParseSpeeds.Fast, + fastest: ParseSpeeds.Fastest, +}; +if (!(speedArg in speedMap)) { + console.error(`unknown --speed: ${speedArg}`); + process.exit(2); +} +const parseSpeed = speedMap[speedArg]; + +const pdfPath = resolve(process.cwd(), arg); +const bytes = readFileSync(pdfPath); +console.log(`input: ${pdfPath} (${(bytes.length / 1024 / 1024).toFixed(1)} MB)`); +console.log(`parseSpeed: ${speedArg} (objects/tick = ${parseSpeed})`); + +// Warm-up read, so the cost of streaming the file off disk doesn't +// dominate the small-PDF case. +const _warm = bytes[0] + bytes[bytes.length - 1]; + +const t0 = process.hrtime.bigint(); +const doc = await PDFDocument.load(bytes, { updateMetadata: false, parseSpeed }); +const t1 = process.hrtime.bigint(); +const ms = Number(t1 - t0) / 1e6; +console.log(`load: ${ms.toFixed(0)} ms`); +console.log(`pages parsed: ${doc.getPageCount()}`); diff --git a/perf/profile-roundtrip.mjs b/perf/profile-roundtrip.mjs new file mode 100644 index 0000000..dfb5a44 --- /dev/null +++ b/perf/profile-roundtrip.mjs @@ -0,0 +1,46 @@ +// Profile the full pdf-lib roundtrip (load + save) with the tick-yield +// knobs cranked to their extremes. The defaults are alarmingly slow: +// +// load: parseSpeed defaults to Slow = 100 objects/tick + await +// waitForTick() between batches. For a ~50k-object book that's +// ~500 yields, each ~10ms of pure idle. +// save: objectsPerTick defaults to 50, with the same yield pattern. +// Roughly 2x as many yields as load. +// +// Both knobs accept Infinity (Fastest) to disable yielding entirely. +// Compare against the harness's 39.7s "process" baseline. +// +// Usage: +// node profile-roundtrip.mjs + +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { PDFDocument, ParseSpeeds } from 'pdf-lib'; + +const pdfPath = resolve(process.cwd(), process.argv[2] || ''); +if (!process.argv[2]) { + console.error('usage: node profile-roundtrip.mjs '); + process.exit(2); +} +const bytes = readFileSync(pdfPath); +console.log(`input: ${pdfPath} (${(bytes.length / 1024 / 1024).toFixed(1)} MB)`); +console.log(''); + +const variants = [ + { name: 'default (Slow / 50)', parseSpeed: ParseSpeeds.Slow, objectsPerTick: 50 }, + { name: 'Fast / 1500', parseSpeed: ParseSpeeds.Fast, objectsPerTick: 1500 }, + { name: 'Fastest / Infinity', parseSpeed: ParseSpeeds.Fastest, objectsPerTick: Infinity }, +]; + +for (const v of variants) { + const tLoad0 = process.hrtime.bigint(); + const doc = await PDFDocument.load(bytes, { updateMetadata: false, parseSpeed: v.parseSpeed }); + const loadMs = Number(process.hrtime.bigint() - tLoad0) / 1e6; + + const tSave0 = process.hrtime.bigint(); + const out = await doc.save({ objectsPerTick: v.objectsPerTick }); + const saveMs = Number(process.hrtime.bigint() - tSave0) / 1e6; + + const outMb = (out.length / 1024 / 1024).toFixed(1); + console.log(`${v.name.padEnd(26)} load=${loadMs.toFixed(0).padStart(6)}ms save=${saveMs.toFixed(0).padStart(6)}ms out=${outMb}MB`); +} From 92a7db8664a3f2a76e4953f6349a4c919605096f Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Mon, 18 May 2026 23:11:00 +0200 Subject: [PATCH 09/26] Drop pagedjs-cli, render via a thin in-tree driver. pagedjs-cli's only behavioural contribution was the paged.js browser bundle and a small set of pdf-lib helpers; the CLI wrapper around them called PDFDocument.load / save with no options and therefore inherited pdf-lib's slow defaults (~32 s of pure idle yielding per build on the 1638-page docs book -- see perf/README.md's "Profiling pdf-lib's load"). Rather than patch upstream just to pass two option objects, vendor what we actually use and drive puppeteer ourselves. docs/lib/paged.browser.js -- pagedjs-cli@0.4.3/dist/browser.js verbatim, MIT, license header preserved. docs/lib/outline.mjs -- src/outline.js, ESM-ified, MIT, attribution. docs/lib/postprocesser.mjs -- src/postprocesser.js, ESM-ified, MIT, attribution. docs/render-book.mjs -- new ~190 LOC production driver. CLI-compatible with the subset of pagedjs-cli flags book.bat actually used (-o, --outline-tags, -t, --additional-script). Calls PDFDocument.load with parseSpeed: Fastest and pdfDoc.save with objectsPerTick: Infinity inline; no plumbing required. docs/book.bat swapped to `node render-book.mjs ...`. Same flags, ~32 s faster end-to-end (process phase drops from 39.7 s to 5.1 s on the same input), one fewer transitive dependency tree. perf/measure.mjs now imports the helpers and the bundle from docs/lib/ so the harness and production share the exact same code path. Both package.json files dropped pagedjs-cli and added puppeteer + pdf-lib + html-entities as direct deps (they were previously transitive via pagedjs-cli). --- docs/book.bat | 17 +- docs/lib/outline.mjs | 181 + docs/lib/paged.browser.js | 33371 +++++++++++++++++++++++++++++++++++ docs/lib/postprocesser.mjs | 97 + docs/package-lock.json | 2206 +-- docs/package.json | 4 +- docs/render-book.mjs | 191 + perf/README.md | 145 +- perf/measure.mjs | 13 +- perf/package-lock.json | 1971 +-- perf/package.json | 3 +- 11 files changed, 34202 insertions(+), 3997 deletions(-) create mode 100644 docs/lib/outline.mjs create mode 100644 docs/lib/paged.browser.js create mode 100644 docs/lib/postprocesser.mjs create mode 100644 docs/render-book.mjs diff --git a/docs/book.bat b/docs/book.bat index 5ecee61..8d8ee78 100644 --- a/docs/book.bat +++ b/docs/book.bat @@ -3,15 +3,26 @@ rem PDF render only. Run build.bat (or `bundle exec jekyll build`) first rem so _site-pdf\book.html and its dependencies exist; this script rem assumes the Pdfify plugin has already populated _site-pdf\. rem +rem render-book.mjs drives puppeteer + paged.js + pdf-lib directly so +rem we control pdf-lib's parseSpeed (the default yields the event loop +rem between every 100 objects on load, adding ~32 s to a 100 s build +rem for no reason in Node -- see perf\README.md "Profiling pdf-lib's +rem load" for the full diagnosis). pagedjs-cli passed no options to +rem load/save and inherited that cost; we don't. +rem rem --additional-script ..\perf\detach-pages.js injects a Paged.Handler rem that hides each finalised page from Chromium's layout tree and rem restores them all before page.pdf() runs. Drops total render from rem ~104s to ~51s on the 1638-page book by eliminating the O(n^2) -rem getBoundingClientRect cost in paged.js's overflow walker. See -rem perf\README.md for the full analysis. +rem getBoundingClientRect cost in paged.js's overflow walker. if not exist _site-pdf\book.html ( echo _site-pdf\book.html not found. Run build.bat first. exit /b 1 ) +if not exist node_modules\puppeteer\package.json ( + echo Installing docs\ dependencies... + call npm install + if errorlevel 1 exit /b 1 +) if not exist _pdf mkdir _pdf -npx pagedjs-cli _site-pdf\book.html -o _pdf\book.pdf --outline-tags h1,h2,h3,h4 -t 600000 --additional-script ..\perf\detach-pages.js +node render-book.mjs _site-pdf\book.html -o _pdf\book.pdf --outline-tags h1,h2,h3,h4 --additional-script ..\perf\detach-pages.js diff --git a/docs/lib/outline.mjs b/docs/lib/outline.mjs new file mode 100644 index 0000000..88ead0f --- /dev/null +++ b/docs/lib/outline.mjs @@ -0,0 +1,181 @@ +// Adapted verbatim from pagedjs-cli 0.4.3 src/outline.js +// (https://github.com/pagedjs/pagedjs-cli) -- MIT, Copyright (c) 2018 +// Adam Hyde. Pulled in directly so we no longer need the pagedjs-cli +// dependency. +// +// Two exports: +// parseOutline(page, tags, enableWarnings) -- runs in the browser +// via page.evaluate. Walks document.querySelectorAll(tags.join(',')) +// to produce a nested outline tree of {title, destination, children}. +// Also creates a hidden link-holder so Chrome +// registers a named destination for each heading -- without that, +// the named-destination Dest entries we write in setOutline would +// point nowhere. +// +// setOutline(pdfDoc, outline, enableWarnings) -- runs in Node on the +// parsed pdf-lib document. Walks the outline tree and writes a +// /Outlines tree of PDF dicts using pdf-lib's low-level API +// (PDFDict.fromMapWithContext, etc.). Each entry's Dest is a name +// that Chrome's /Dests catalog entry resolves to a page+coords. + +import { PDFDict, PDFName, PDFNumber, PDFHexString } from "pdf-lib"; +import { decode as htmlEntitiesDecode } from "html-entities"; + +const SanitizeXMLRx = /<[^>]+>/g; + +function sanitize (string) { + if (string.includes("<")) { + string = string.replace(SanitizeXMLRx, ""); + } + return htmlEntitiesDecode(string); +} + +export async function parseOutline(page, tags, enableWarnings) { + return await page.evaluate((tags) => { + const tagsToProcess = []; + for (const node of document.querySelectorAll(tags.join(","))) { + tagsToProcess.push(node); + } + tagsToProcess.reverse(); + + const root = {children: [], depth: -1}; + let currentOutlineNode = root; + + const linkHolder = document.createElement("div"); + const body = document.querySelector("body"); + linkHolder.style.display = "none"; + body.insertBefore(linkHolder, body.firstChild); + + while (tagsToProcess.length > 0) { + const tag = tagsToProcess.pop(); + const orderDepth = tags.indexOf(tag.tagName.toLowerCase()); + const dest = encodeURIComponent(tag.id).replace(/%/g, "#25"); + + // Add to link holder to register a destination + const hiddenLink = document.createElement("a"); + hiddenLink.href = "#"+dest; + linkHolder.appendChild(hiddenLink); + + if (orderDepth < currentOutlineNode.depth) { + currentOutlineNode = currentOutlineNode.parent; + tagsToProcess.push(tag); + } else { + const newNode = { + title: tag.innerText.trim(), + // encode section ID until https://bugs.chromium.org/p/chromium/issues/detail?id=985254 is fixed + destination: dest, + children: [], + depth: orderDepth, + }; + if (orderDepth == currentOutlineNode.depth) { + if (currentOutlineNode.parent) { + newNode.parent = currentOutlineNode.parent; + currentOutlineNode.parent.children.push(newNode); + } else { + newNode.parent = currentOutlineNode; + currentOutlineNode.children.push(newNode); + } + currentOutlineNode = newNode; + } else if (orderDepth > currentOutlineNode.depth) { + newNode.parent = currentOutlineNode; + currentOutlineNode.children.push(newNode); + currentOutlineNode = newNode; + } + } + } + + const stripParentProperty = (node) => { + node.parent = undefined; + for (const child of node.children) { + stripParentProperty(child); + } + }; + stripParentProperty(root); + return root.children; + }, tags); +} + +function setRefsForOutlineItems (layer, context, parentRef) { + for (const item of layer) { + item.ref = context.nextRef(); + item.parentRef = parentRef; + setRefsForOutlineItems(item.children, context, item.ref); + } +} + +function countChildrenOfOutline (layer) { + let count = 0; + for (const item of layer) { + ++count; + count += countChildrenOfOutline(item.children); + } + return count; +} + +function buildPdfObjectsForOutline (layer, context) { + for (const [i, item] of layer.entries()) { + const prev = layer[i - 1]; + const next = layer[i + 1]; + + const pdfObject = new Map([ + [PDFName.of("Title"), PDFHexString.fromText(sanitize(item.title))], + [PDFName.of("Dest"), PDFName.of(item.destination)], + [PDFName.of("Parent"), item.parentRef] + ]); + if (prev) { + pdfObject.set(PDFName.of("Prev"), prev.ref); + } + if (next) { + pdfObject.set(PDFName.of("Next"), next.ref); + } + if (item.children.length > 0) { + pdfObject.set(PDFName.of("First"), item.children[0].ref); + pdfObject.set(PDFName.of("Last"), item.children[item.children.length - 1].ref); + pdfObject.set(PDFName.of("Count"), PDFNumber.of(countChildrenOfOutline(item.children))); + } + + context.assign(item.ref, PDFDict.fromMapWithContext(pdfObject, context)); + + buildPdfObjectsForOutline(item.children, context); + } +} + +function generateWarningsAboutMissingDestinations (layer, pdfDoc) { + const dests = pdfDoc.context.lookup(pdfDoc.catalog.get(PDFName.of("Dests"))); + // Dests can be undefined if the PDF wasn't successfully generated (for instance if Paged.js threw an exception) + if (dests) { + const validDestinationTargets = dests.entries().map(([key, _]) => key.value()); + for (const item of layer) { + if (item.destination && !validDestinationTargets.includes("/" + item.destination)) { + console.warn(`Unable to find destination "${item.destination}" while generating PDF outline.`); + } + generateWarningsAboutMissingDestinations(item.children, pdfDoc); + } + } +} + +export async function setOutline (pdfDoc, outline, enableWarnings=false) { + const context = pdfDoc.context; + const outlineRef = context.nextRef(); + + if (outline.length === 0) { + return pdfDoc; + } + + if (enableWarnings) { + generateWarningsAboutMissingDestinations(outline, pdfDoc); + } + + setRefsForOutlineItems(outline, context, outlineRef); + buildPdfObjectsForOutline(outline, context); + + const outlineObject = PDFDict.fromMapWithContext(new Map([ + [PDFName.of("First"), outline[0].ref], + [PDFName.of("Last"), outline[outline.length - 1].ref], + [PDFName.of("Count"), PDFNumber.of(countChildrenOfOutline(outline))] + ]), context); + context.assign(outlineRef, outlineObject); + + pdfDoc.catalog.set(PDFName.of("Outlines"), outlineRef); + return pdfDoc; +} diff --git a/docs/lib/paged.browser.js b/docs/lib/paged.browser.js new file mode 100644 index 0000000..df4f9b3 --- /dev/null +++ b/docs/lib/paged.browser.js @@ -0,0 +1,33371 @@ +/** + * @license Paged.js v0.4.3 | MIT | https://pagedjs.org + */ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.PagedPolyfill = factory()); +})(this, (function () { 'use strict'; + + function getBoundingClientRect(element) { + if (!element) { + return; + } + let rect; + if (typeof element.getBoundingClientRect !== "undefined") { + rect = element.getBoundingClientRect(); + } else { + let range = document.createRange(); + range.selectNode(element); + rect = range.getBoundingClientRect(); + } + return rect; + } + + function getClientRects(element) { + if (!element) { + return; + } + let rect; + if (typeof element.getClientRects !== "undefined") { + rect = element.getClientRects(); + } else { + let range = document.createRange(); + range.selectNode(element); + rect = range.getClientRects(); + } + return rect; + } + + /** + * Generates a UUID + * based on: http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript + * @returns {string} uuid + */ + function UUID() { + var d = new Date().getTime(); + if (typeof performance !== "undefined" && typeof performance.now === "function") { + d += performance.now(); //use high-precision timer if available + } + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { + var r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16); + }); + } + + function attr(element, attributes) { + for (var i = 0; i < attributes.length; i++) { + if (element.hasAttribute(attributes[i])) { + return element.getAttribute(attributes[i]); + } + } + } + + /* Based on by https://mths.be/cssescape v1.5.1 by @mathias | MIT license + * Allows # and . + */ + function querySelectorEscape(value) { + if (arguments.length == 0) { + throw new TypeError("`CSS.escape` requires an argument."); + } + var string = String(value); + + var length = string.length; + var index = -1; + var codeUnit; + var result = ""; + var firstCodeUnit = string.charCodeAt(0); + while (++index < length) { + codeUnit = string.charCodeAt(index); + + + + // Note: there’s no need to special-case astral symbols, surrogate + // pairs, or lone surrogates. + + // If the character is NULL (U+0000), then the REPLACEMENT CHARACTER + // (U+FFFD). + if (codeUnit == 0x0000) { + result += "\uFFFD"; + continue; + } + + if ( + // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is + // U+007F, […] + (codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F || + // If the character is the first character and is in the range [0-9] + // (U+0030 to U+0039), […] + (index == 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) || + // If the character is the second character and is in the range [0-9] + // (U+0030 to U+0039) and the first character is a `-` (U+002D), […] + ( + index == 1 && + codeUnit >= 0x0030 && codeUnit <= 0x0039 && + firstCodeUnit == 0x002D + ) + ) { + // https://drafts.csswg.org/cssom/#escape-a-character-as-code-point + result += "\\" + codeUnit.toString(16) + " "; + continue; + } + + if ( + // If the character is the first character and is a `-` (U+002D), and + // there is no second character, […] + index == 0 && + length == 1 && + codeUnit == 0x002D + ) { + result += "\\" + string.charAt(index); + continue; + } + + // support for period character in id + if (codeUnit == 0x002E) { + if (string.charAt(0) == "#") { + result += "\\."; + continue; + } + } + + + // If the character is not handled by one of the above rules and is + // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or + // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to + // U+005A), or [a-z] (U+0061 to U+007A), […] + if ( + codeUnit >= 0x0080 || + codeUnit == 0x002D || + codeUnit == 0x005F || + codeUnit == 35 || // Allow # + codeUnit == 46 || // Allow . + codeUnit >= 0x0030 && codeUnit <= 0x0039 || + codeUnit >= 0x0041 && codeUnit <= 0x005A || + codeUnit >= 0x0061 && codeUnit <= 0x007A + ) { + // the character itself + result += string.charAt(index); + continue; + } + + // Otherwise, the escaped character. + // https://drafts.csswg.org/cssom/#escape-a-character + result += "\\" + string.charAt(index); + + } + return result; + } + + /** + * Creates a new pending promise and provides methods to resolve or reject it. + * From: https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm/Deferred#backwards_forwards_compatible + * @returns {object} defered + */ + function defer() { + this.resolve = null; + + this.reject = null; + + this.id = UUID(); + + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + Object.freeze(this); + } + + const requestIdleCallback = typeof window !== "undefined" && ("requestIdleCallback" in window ? window.requestIdleCallback : window.requestAnimationFrame); + + function CSSValueToString(obj) { + return obj.value + (obj.unit || ""); + } + + function isElement(node) { + return node && node.nodeType === 1; + } + + function isText(node) { + return node && node.nodeType === 3; + } + + function* walk$2(start, limiter) { + let node = start; + + while (node) { + + yield node; + + if (node.childNodes.length) { + node = node.firstChild; + } else if (node.nextSibling) { + if (limiter && node === limiter) { + node = undefined; + break; + } + node = node.nextSibling; + } else { + while (node) { + node = node.parentNode; + if (limiter && node === limiter) { + node = undefined; + break; + } + if (node && node.nextSibling) { + node = node.nextSibling; + break; + } + + } + } + } + } + + function nodeAfter(node, limiter) { + if (limiter && node === limiter) { + return; + } + let significantNode = nextSignificantNode(node); + if (significantNode) { + return significantNode; + } + if (node.parentNode) { + while ((node = node.parentNode)) { + if (limiter && node === limiter) { + return; + } + significantNode = nextSignificantNode(node); + if (significantNode) { + return significantNode; + } + } + } + } + + function nodeBefore(node, limiter) { + if (limiter && node === limiter) { + return; + } + let significantNode = previousSignificantNode(node); + if (significantNode) { + return significantNode; + } + if (node.parentNode) { + while ((node = node.parentNode)) { + if (limiter && node === limiter) { + return; + } + significantNode = previousSignificantNode(node); + if (significantNode) { + return significantNode; + } + } + } + } + + function elementAfter(node, limiter) { + let after = nodeAfter(node, limiter); + + while (after && after.nodeType !== 1) { + after = nodeAfter(after, limiter); + } + + return after; + } + + function elementBefore(node, limiter) { + let before = nodeBefore(node, limiter); + + while (before && before.nodeType !== 1) { + before = nodeBefore(before, limiter); + } + + return before; + } + + function displayedElementAfter(node, limiter) { + let after = elementAfter(node, limiter); + + while (after && after.dataset.undisplayed) { + after = elementAfter(after, limiter); + } + + return after; + } + + function displayedElementBefore(node, limiter) { + let before = elementBefore(node, limiter); + + while (before && before.dataset.undisplayed) { + before = elementBefore(before, limiter); + } + + return before; + } + + function rebuildAncestors(node) { + let parent, ancestor; + let ancestors = []; + let added = []; + + let fragment = document.createDocumentFragment(); + + // Handle rowspan on table + if (node.nodeName === "TR") { + let previousRow = node.previousElementSibling; + let previousRowDistance = 1; + while (previousRow) { + // previous row has more columns, might indicate a rowspan. + if (previousRow.childElementCount > node.childElementCount) { + const initialColumns = Array.from(node.children); + while (node.firstChild) { + node.firstChild.remove(); + } + let k = 0; + for (let j = 0; j < previousRow.children.length; j++) { + let column = previousRow.children[j]; + if (column.rowSpan && column.rowSpan > previousRowDistance) { + const duplicatedColumn = column.cloneNode(true); + // Adjust rowspan value + duplicatedColumn.rowSpan = column.rowSpan - previousRowDistance; + // Add the column to the row + node.appendChild(duplicatedColumn); + } else { + // Fill the gap with the initial columns (if exists) + const initialColumn = initialColumns[k++]; + // The initial column can be undefined if the newly created table has less columns than the original table + if (initialColumn) { + node.appendChild(initialColumn); + } + } + } + } + previousRow = previousRow.previousElementSibling; + previousRowDistance++; + } + } + + // Gather all ancestors + let element = node; + while(element.parentNode && element.parentNode.nodeType === 1) { + ancestors.unshift(element.parentNode); + element = element.parentNode; + } + + for (var i = 0; i < ancestors.length; i++) { + ancestor = ancestors[i]; + parent = ancestor.cloneNode(false); + + parent.setAttribute("data-split-from", parent.getAttribute("data-ref")); + // ancestor.setAttribute("data-split-to", parent.getAttribute("data-ref")); + + if (parent.hasAttribute("id")) { + let dataID = parent.getAttribute("id"); + parent.setAttribute("data-id", dataID); + parent.removeAttribute("id"); + } + + // This is handled by css :not, but also tidied up here + if (parent.hasAttribute("data-break-before")) { + parent.removeAttribute("data-break-before"); + } + + if (parent.hasAttribute("data-previous-break-after")) { + parent.removeAttribute("data-previous-break-after"); + } + + if (added.length) { + let container = added[added.length-1]; + container.appendChild(parent); + } else { + fragment.appendChild(parent); + } + added.push(parent); + + // rebuild table rows + if (parent.nodeName === "TD" && ancestor.parentElement.contains(ancestor)) { + let td = ancestor; + let prev = parent; + while ((td = td.previousElementSibling)) { + let sib = td.cloneNode(false); + parent.parentElement.insertBefore(sib, prev); + prev = sib; + } + + } + } + + added = undefined; + return fragment; + } + /* + export function split(bound, cutElement, breakAfter) { + let needsRemoval = []; + let index = indexOf(cutElement); + + if (!breakAfter && index === 0) { + return; + } + + if (breakAfter && index === (cutElement.parentNode.children.length - 1)) { + return; + } + + // Create a fragment with rebuilt ancestors + let fragment = rebuildAncestors(cutElement); + + // Clone cut + if (!breakAfter) { + let clone = cutElement.cloneNode(true); + let ref = cutElement.parentNode.getAttribute('data-ref'); + let parent = fragment.querySelector("[data-ref='" + ref + "']"); + parent.appendChild(clone); + needsRemoval.push(cutElement); + } + + // Remove all after cut + let next = nodeAfter(cutElement, bound); + while (next) { + let clone = next.cloneNode(true); + let ref = next.parentNode.getAttribute('data-ref'); + let parent = fragment.querySelector("[data-ref='" + ref + "']"); + parent.appendChild(clone); + needsRemoval.push(next); + next = nodeAfter(next, bound); + } + + // Remove originals + needsRemoval.forEach((node) => { + if (node) { + node.remove(); + } + }); + + // Insert after bounds + bound.parentNode.insertBefore(fragment, bound.nextSibling); + return [bound, bound.nextSibling]; + } + */ + + function needsBreakBefore(node) { + if( typeof node !== "undefined" && + typeof node.dataset !== "undefined" && + typeof node.dataset.breakBefore !== "undefined" && + (node.dataset.breakBefore === "always" || + node.dataset.breakBefore === "page" || + node.dataset.breakBefore === "left" || + node.dataset.breakBefore === "right" || + node.dataset.breakBefore === "recto" || + node.dataset.breakBefore === "verso") + ) { + return true; + } + + return false; + } + + function needsPreviousBreakAfter(node) { + if( typeof node !== "undefined" && + typeof node.dataset !== "undefined" && + typeof node.dataset.previousBreakAfter !== "undefined" && + (node.dataset.previousBreakAfter === "always" || + node.dataset.previousBreakAfter === "page" || + node.dataset.previousBreakAfter === "left" || + node.dataset.previousBreakAfter === "right" || + node.dataset.previousBreakAfter === "recto" || + node.dataset.previousBreakAfter === "verso") + ) { + return true; + } + + return false; + } + + function needsPageBreak(node, previousSignificantNode) { + if (typeof node === "undefined" || !previousSignificantNode || isIgnorable(node)) { + return false; + } + if (node.dataset && node.dataset.undisplayed) { + return false; + } + let previousSignificantNodePage = previousSignificantNode.dataset ? previousSignificantNode.dataset.page : undefined; + if (typeof previousSignificantNodePage === "undefined") { + const nodeWithNamedPage = getNodeWithNamedPage(previousSignificantNode); + if (nodeWithNamedPage) { + previousSignificantNodePage = nodeWithNamedPage.dataset.page; + } + } + let currentNodePage = node.dataset ? node.dataset.page : undefined; + if (typeof currentNodePage === "undefined") { + const nodeWithNamedPage = getNodeWithNamedPage(node, previousSignificantNode); + if (nodeWithNamedPage) { + currentNodePage = nodeWithNamedPage.dataset.page; + } + } + return currentNodePage !== previousSignificantNodePage; + } + + function *words(node) { + let currentText = node.nodeValue; + let max = currentText.length; + let currentOffset = 0; + let currentLetter; + + let range; + const significantWhitespaces = node.parentElement && node.parentElement.nodeName === "PRE"; + + while (currentOffset < max) { + currentLetter = currentText[currentOffset]; + if (/^[\S\u202F\u00A0]$/.test(currentLetter) || significantWhitespaces) { + if (!range) { + range = document.createRange(); + range.setStart(node, currentOffset); + } + } else { + if (range) { + range.setEnd(node, currentOffset); + yield range; + range = undefined; + } + } + + currentOffset += 1; + } + + if (range) { + range.setEnd(node, currentOffset); + yield range; + } + } + + function *letters(wordRange) { + let currentText = wordRange.startContainer; + let max = currentText.length; + let currentOffset = wordRange.startOffset; + // let currentLetter; + + let range; + + while(currentOffset < max) { + // currentLetter = currentText[currentOffset]; + range = document.createRange(); + range.setStart(currentText, currentOffset); + range.setEnd(currentText, currentOffset+1); + + yield range; + + currentOffset += 1; + } + } + + function isContainer(node) { + let container; + + if (typeof node.tagName === "undefined") { + return true; + } + + if (node.style && node.style.display === "none") { + return false; + } + + switch (node.tagName) { + // Inline + case "A": + case "ABBR": + case "ACRONYM": + case "B": + case "BDO": + case "BIG": + case "BR": + case "BUTTON": + case "CITE": + case "CODE": + case "DFN": + case "EM": + case "I": + case "IMG": + case "INPUT": + case "KBD": + case "LABEL": + case "MAP": + case "OBJECT": + case "Q": + case "SAMP": + case "SCRIPT": + case "SELECT": + case "SMALL": + case "SPAN": + case "STRONG": + case "SUB": + case "SUP": + case "TEXTAREA": + case "TIME": + case "TT": + case "VAR": + case "P": + case "H1": + case "H2": + case "H3": + case "H4": + case "H5": + case "H6": + case "FIGCAPTION": + case "BLOCKQUOTE": + case "PRE": + case "LI": + case "TD": + case "DT": + case "DD": + case "VIDEO": + case "CANVAS": + container = false; + break; + default: + container = true; + } + + return container; + } + + function cloneNode(n, deep=false) { + return n.cloneNode(deep); + } + + function findElement(node, doc, forceQuery) { + const ref = node.getAttribute("data-ref"); + return findRef(ref, doc, forceQuery); + } + + function findRef(ref, doc, forceQuery) { + if (!forceQuery && doc.indexOfRefs && doc.indexOfRefs[ref]) { + return doc.indexOfRefs[ref]; + } else { + return doc.querySelector(`[data-ref='${ref}']`); + } + } + + function validNode(node) { + if (isText(node)) { + return true; + } + + if (isElement(node) && node.dataset.ref) { + return true; + } + + return false; + } + + function prevValidNode(node) { + while (!validNode(node)) { + if (node.previousSibling) { + node = node.previousSibling; + } else { + node = node.parentNode; + } + + if (!node) { + break; + } + } + + return node; + } + + + function indexOf$2(node) { + let parent = node.parentNode; + if (!parent) { + return 0; + } + return Array.prototype.indexOf.call(parent.childNodes, node); + } + + function child(node, index) { + return node.childNodes[index]; + } + + function hasContent(node) { + if (isElement(node)) { + return true; + } else if (isText(node) && + node.textContent.trim().length) { + return true; + } + return false; + } + + function indexOfTextNode(node, parent) { + if (!isText(node)) { + return -1; + } + let nodeTextContent = node.textContent; + let child; + let index = -1; + for (var i = 0; i < parent.childNodes.length; i++) { + child = parent.childNodes[i]; + if (child.nodeType === 3) { + let text = parent.childNodes[i].textContent; + if (text.includes(nodeTextContent)) { + index = i; + break; + } + } + } + + return index; + } + + + /** + * Throughout, whitespace is defined as one of the characters + * "\t" TAB \u0009 + * "\n" LF \u000A + * "\r" CR \u000D + * " " SPC \u0020 + * + * This does not use Javascript's "\s" because that includes non-breaking + * spaces (and also some other characters). + */ + + /** + * Determine if a node should be ignored by the iterator functions. + * taken from https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace#Whitespace_helper_functions + * + * @param {Node} node An object implementing the DOM1 |Node| interface. + * @return {boolean} true if the node is: + * 1) A |Text| node that is all whitespace + * 2) A |Comment| node + * and otherwise false. + */ + function isIgnorable(node) { + return (node.nodeType === 8) || // A comment node + ((node.nodeType === 3) && isAllWhitespace(node)); // a text node, all whitespace + } + + /** + * Determine whether a node's text content is entirely whitespace. + * + * @param {Node} node A node implementing the |CharacterData| interface (i.e., a |Text|, |Comment|, or |CDATASection| node + * @return {boolean} true if all of the text content of |nod| is whitespace, otherwise false. + */ + function isAllWhitespace(node) { + return !(/[^\t\n\r ]/.test(node.textContent)); + } + + /** + * Version of |previousSibling| that skips nodes that are entirely + * whitespace or comments. (Normally |previousSibling| is a property + * of all DOM nodes that gives the sibling node, the node that is + * a child of the same parent, that occurs immediately before the + * reference node.) + * + * @param {ChildNode} sib The reference node. + * @return {Node|null} Either: + * 1) The closest previous sibling to |sib| that is not ignorable according to |is_ignorable|, or + * 2) null if no such node exists. + */ + function previousSignificantNode(sib) { + while ((sib = sib.previousSibling)) { + if (!isIgnorable(sib)) return sib; + } + return null; + } + + function getNodeWithNamedPage(node, limiter) { + if (node && node.dataset && node.dataset.page) { + return node; + } + if (node.parentNode) { + while ((node = node.parentNode)) { + if (limiter && node === limiter) { + return; + } + if (node.dataset && node.dataset.page) { + return node; + } + } + } + return null; + } + + function breakInsideAvoidParentNode(node) { + while ((node = node.parentNode)) { + if (node && node.dataset && node.dataset.breakInside === "avoid") { + return node; + } + } + return null; + } + + /** + * Find a parent with a given node name. + * @param {Node} node - initial Node + * @param {string} nodeName - node name (eg. "TD", "TABLE", "STRONG"...) + * @param {Node} limiter - go up to the parent until there's no more parent or the current node is equals to the limiter + * @returns {Node|undefined} - Either: + * 1) The closest parent for a the given node name, or + * 2) undefined if no such node exists. + */ + function parentOf(node, nodeName, limiter) { + if (limiter && node === limiter) { + return; + } + if (node.parentNode) { + while ((node = node.parentNode)) { + if (limiter && node === limiter) { + return; + } + if (node.nodeName === nodeName) { + return node; + } + } + } + } + + /** + * Version of |nextSibling| that skips nodes that are entirely + * whitespace or comments. + * + * @param {ChildNode} sib The reference node. + * @return {Node|null} Either: + * 1) The closest next sibling to |sib| that is not ignorable according to |is_ignorable|, or + * 2) null if no such node exists. + */ + function nextSignificantNode(sib) { + while ((sib = sib.nextSibling)) { + if (!isIgnorable(sib)) return sib; + } + return null; + } + + function filterTree(content, func, what) { + const treeWalker = document.createTreeWalker( + content || this.dom, + what || NodeFilter.SHOW_ALL, + func ? { acceptNode: func } : null, + false + ); + + let node; + let current; + node = treeWalker.nextNode(); + while(node) { + current = node; + node = treeWalker.nextNode(); + current.parentNode.removeChild(current); + } + } + + /** + * BreakToken + * @class + */ + class BreakToken { + + constructor(node, offset) { + this.node = node; + this.offset = offset; + } + + equals(otherBreakToken) { + if (!otherBreakToken) { + return false; + } + if (this["node"] && otherBreakToken["node"] && + this["node"] !== otherBreakToken["node"]) { + return false; + } + if (this["offset"] && otherBreakToken["offset"] && + this["offset"] !== otherBreakToken["offset"]) { + return false; + } + return true; + } + + toJSON(hash) { + let node; + let index = 0; + if (!this.node) { + return {}; + } + if (isElement(this.node) && this.node.dataset.ref) { + node = this.node.dataset.ref; + } else if (hash) { + node = this.node.parentElement.dataset.ref; + } + + if (this.node.parentElement) { + const children = Array.from(this.node.parentElement.childNodes); + index = children.indexOf(this.node); + } + + return JSON.stringify({ + "node": node, + "index" : index, + "offset": this.offset + }); + } + + } + + /** + * Render result. + * @class + */ + class RenderResult { + + constructor(breakToken, error) { + this.breakToken = breakToken; + this.error = error; + } + } + + class OverflowContentError extends Error { + constructor(message, items) { + super(message); + this.items = items; + } + } + + function getDefaultExportFromCjs (x) { + return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; + } + + var eventEmitter = {exports: {}}; + + var d$2 = {exports: {}}; + + // ES3 safe + var _undefined$1 = void 0; + + var is$4 = function (value) { return value !== _undefined$1 && value !== null; }; + + var isValue$5 = is$4; + + // prettier-ignore + var possibleTypes = { "object": true, "function": true, "undefined": true /* document.all */ }; + + var is$3 = function (value) { + if (!isValue$5(value)) return false; + return hasOwnProperty.call(possibleTypes, typeof value); + }; + + var isObject$3 = is$3; + + var is$2 = function (value) { + if (!isObject$3(value)) return false; + try { + if (!value.constructor) return false; + return value.constructor.prototype === value; + } catch (error) { + return false; + } + }; + + var isPrototype = is$2; + + var is$1 = function (value) { + if (typeof value !== "function") return false; + + if (!hasOwnProperty.call(value, "length")) return false; + + try { + if (typeof value.length !== "number") return false; + if (typeof value.call !== "function") return false; + if (typeof value.apply !== "function") return false; + } catch (error) { + return false; + } + + return !isPrototype(value); + }; + + var isFunction$1 = is$1; + + var classRe = /^\s*class[\s{/}]/, functionToString = Function.prototype.toString; + + var is = function (value) { + if (!isFunction$1(value)) return false; + if (classRe.test(functionToString.call(value))) return false; + return true; + }; + + var isImplemented$7 = function () { + var assign = Object.assign, obj; + if (typeof assign !== "function") return false; + obj = { foo: "raz" }; + assign(obj, { bar: "dwa" }, { trzy: "trzy" }); + return obj.foo + obj.bar + obj.trzy === "razdwatrzy"; + }; + + var isImplemented$6; + var hasRequiredIsImplemented$2; + + function requireIsImplemented$2 () { + if (hasRequiredIsImplemented$2) return isImplemented$6; + hasRequiredIsImplemented$2 = 1; + + isImplemented$6 = function () { + try { + Object.keys("primitive"); + return true; + } catch (e) { + return false; + } + }; + return isImplemented$6; + } + + // eslint-disable-next-line no-empty-function + var noop$4 = function () {}; + + var _undefined = noop$4(); // Support ES3 engines + + var isValue$4 = function (val) { return val !== _undefined && val !== null; }; + + var shim$5; + var hasRequiredShim$5; + + function requireShim$5 () { + if (hasRequiredShim$5) return shim$5; + hasRequiredShim$5 = 1; + + var isValue = isValue$4; + + var keys = Object.keys; + + shim$5 = function (object) { return keys(isValue(object) ? Object(object) : object); }; + return shim$5; + } + + var keys; + var hasRequiredKeys; + + function requireKeys () { + if (hasRequiredKeys) return keys; + hasRequiredKeys = 1; + + keys = requireIsImplemented$2()() ? Object.keys : requireShim$5(); + return keys; + } + + var isValue$3 = isValue$4; + + var validValue = function (value) { + if (!isValue$3(value)) throw new TypeError("Cannot use null or undefined"); + return value; + }; + + var shim$4; + var hasRequiredShim$4; + + function requireShim$4 () { + if (hasRequiredShim$4) return shim$4; + hasRequiredShim$4 = 1; + + var keys = requireKeys() + , value = validValue + , max = Math.max; + + shim$4 = function (dest, src /*, …srcn*/) { + var error, i, length = max(arguments.length, 2), assign; + dest = Object(value(dest)); + assign = function (key) { + try { + dest[key] = src[key]; + } catch (e) { + if (!error) error = e; + } + }; + for (i = 1; i < length; ++i) { + src = arguments[i]; + keys(src).forEach(assign); + } + if (error !== undefined) throw error; + return dest; + }; + return shim$4; + } + + var assign$2 = isImplemented$7() ? Object.assign : requireShim$4(); + + var isValue$2 = isValue$4; + + var forEach$1 = Array.prototype.forEach, create$5 = Object.create; + + var process = function (src, obj) { + var key; + for (key in src) obj[key] = src[key]; + }; + + // eslint-disable-next-line no-unused-vars + var normalizeOptions = function (opts1 /*, …options*/) { + var result = create$5(null); + forEach$1.call(arguments, function (options) { + if (!isValue$2(options)) return; + process(Object(options), result); + }); + return result; + }; + + var str = "razdwatrzy"; + + var isImplemented$5 = function () { + if (typeof str.contains !== "function") return false; + return str.contains("dwa") === true && str.contains("foo") === false; + }; + + var shim$3; + var hasRequiredShim$3; + + function requireShim$3 () { + if (hasRequiredShim$3) return shim$3; + hasRequiredShim$3 = 1; + + var indexOf = String.prototype.indexOf; + + shim$3 = function (searchString /*, position*/) { + return indexOf.call(this, searchString, arguments[1]) > -1; + }; + return shim$3; + } + + var contains$1 = isImplemented$5() ? String.prototype.contains : requireShim$3(); + + var isValue$1 = is$4 + , isPlainFunction = is + , assign$1 = assign$2 + , normalizeOpts = normalizeOptions + , contains = contains$1; + + var d$1 = (d$2.exports = function (dscr, value/*, options*/) { + var c, e, w, options, desc; + if (arguments.length < 2 || typeof dscr !== "string") { + options = value; + value = dscr; + dscr = null; + } else { + options = arguments[2]; + } + if (isValue$1(dscr)) { + c = contains.call(dscr, "c"); + e = contains.call(dscr, "e"); + w = contains.call(dscr, "w"); + } else { + c = w = true; + e = false; + } + + desc = { value: value, configurable: c, enumerable: e, writable: w }; + return !options ? desc : assign$1(normalizeOpts(options), desc); + }); + + d$1.gs = function (dscr, get, set/*, options*/) { + var c, e, options, desc; + if (typeof dscr !== "string") { + options = set; + set = get; + get = dscr; + dscr = null; + } else { + options = arguments[3]; + } + if (!isValue$1(get)) { + get = undefined; + } else if (!isPlainFunction(get)) { + options = get; + get = set = undefined; + } else if (!isValue$1(set)) { + set = undefined; + } else if (!isPlainFunction(set)) { + options = set; + set = undefined; + } + if (isValue$1(dscr)) { + c = contains.call(dscr, "c"); + e = contains.call(dscr, "e"); + } else { + c = true; + e = false; + } + + desc = { get: get, set: set, configurable: c, enumerable: e }; + return !options ? desc : assign$1(normalizeOpts(options), desc); + }; + + var dExports = d$2.exports; + + var validCallable = function (fn) { + if (typeof fn !== "function") throw new TypeError(fn + " is not a function"); + return fn; + }; + + (function (module, exports) { + + var d = dExports + , callable = validCallable + + , apply = Function.prototype.apply, call = Function.prototype.call + , create = Object.create, defineProperty = Object.defineProperty + , defineProperties = Object.defineProperties + , hasOwnProperty = Object.prototype.hasOwnProperty + , descriptor = { configurable: true, enumerable: false, writable: true } + + , on, once, off, emit, methods, descriptors, base; + + on = function (type, listener) { + var data; + + callable(listener); + + if (!hasOwnProperty.call(this, '__ee__')) { + data = descriptor.value = create(null); + defineProperty(this, '__ee__', descriptor); + descriptor.value = null; + } else { + data = this.__ee__; + } + if (!data[type]) data[type] = listener; + else if (typeof data[type] === 'object') data[type].push(listener); + else data[type] = [data[type], listener]; + + return this; + }; + + once = function (type, listener) { + var once, self; + + callable(listener); + self = this; + on.call(this, type, once = function () { + off.call(self, type, once); + apply.call(listener, this, arguments); + }); + + once.__eeOnceListener__ = listener; + return this; + }; + + off = function (type, listener) { + var data, listeners, candidate, i; + + callable(listener); + + if (!hasOwnProperty.call(this, '__ee__')) return this; + data = this.__ee__; + if (!data[type]) return this; + listeners = data[type]; + + if (typeof listeners === 'object') { + for (i = 0; (candidate = listeners[i]); ++i) { + if ((candidate === listener) || + (candidate.__eeOnceListener__ === listener)) { + if (listeners.length === 2) data[type] = listeners[i ? 0 : 1]; + else listeners.splice(i, 1); + } + } + } else { + if ((listeners === listener) || + (listeners.__eeOnceListener__ === listener)) { + delete data[type]; + } + } + + return this; + }; + + emit = function (type) { + var i, l, listener, listeners, args; + + if (!hasOwnProperty.call(this, '__ee__')) return; + listeners = this.__ee__[type]; + if (!listeners) return; + + if (typeof listeners === 'object') { + l = arguments.length; + args = new Array(l - 1); + for (i = 1; i < l; ++i) args[i - 1] = arguments[i]; + + listeners = listeners.slice(); + for (i = 0; (listener = listeners[i]); ++i) { + apply.call(listener, this, args); + } + } else { + switch (arguments.length) { + case 1: + call.call(listeners, this); + break; + case 2: + call.call(listeners, this, arguments[1]); + break; + case 3: + call.call(listeners, this, arguments[1], arguments[2]); + break; + default: + l = arguments.length; + args = new Array(l - 1); + for (i = 1; i < l; ++i) { + args[i - 1] = arguments[i]; + } + apply.call(listeners, this, args); + } + } + }; + + methods = { + on: on, + once: once, + off: off, + emit: emit + }; + + descriptors = { + on: d(on), + once: d(once), + off: d(off), + emit: d(emit) + }; + + base = defineProperties({}, descriptors); + + module.exports = exports = function (o) { + return (o == null) ? create(base) : defineProperties(Object(o), descriptors); + }; + exports.methods = methods; + } (eventEmitter, eventEmitter.exports)); + + var eventEmitterExports = eventEmitter.exports; + var EventEmitter = /*@__PURE__*/getDefaultExportFromCjs(eventEmitterExports); + + /** + * Hooks allow for injecting functions that must all complete in order before finishing + * They will execute in parallel but all must finish before continuing + * Functions may return a promise if they are asycn. + * From epubjs/src/utils/hooks + * @param {any} context scope of this + * @example this.content = new Hook(this); + */ + class Hook { + constructor(context){ + this.context = context || this; + this.hooks = []; + } + + /** + * Adds a function to be run before a hook completes + * @example this.content.register(function(){...}); + * @return {undefined} void + */ + register(){ + for(var i = 0; i < arguments.length; ++i) { + if (typeof arguments[i] === "function") { + this.hooks.push(arguments[i]); + } else { + // unpack array + for(var j = 0; j < arguments[i].length; ++j) { + this.hooks.push(arguments[i][j]); + } + } + } + } + + /** + * Triggers a hook to run all functions + * @example this.content.trigger(args).then(function(){...}); + * @return {Promise} results + */ + trigger(){ + var args = arguments; + var context = this.context; + var promises = []; + + this.hooks.forEach(function(task) { + var executing = task.apply(context, args); + + if(executing && typeof executing["then"] === "function") { + // Task is a function that returns a promise + promises.push(executing); + } else { + // Otherwise Task resolves immediately, add resolved promise with result + promises.push(new Promise((resolve, reject) => { + resolve(executing); + })); + } + }); + + + return Promise.all(promises); + } + + /** + * Triggers a hook to run all functions synchronously + * @example this.content.trigger(args).then(function(){...}); + * @return {Array} results + */ + triggerSync(){ + var args = arguments; + var context = this.context; + var results = []; + + this.hooks.forEach(function(task) { + var executing = task.apply(context, args); + + results.push(executing); + }); + + + return results; + } + + // Adds a function to be run before a hook completes + list(){ + return this.hooks; + } + + clear(){ + return this.hooks = []; + } + } + + const MAX_CHARS_PER_BREAK = 1500; + + /** + * Layout + * @class + */ + class Layout { + + constructor(element, hooks, options) { + this.element = element; + + this.bounds = this.element.getBoundingClientRect(); + this.parentBounds = this.element.offsetParent.getBoundingClientRect(); + let gap = parseFloat(window.getComputedStyle(this.element).columnGap); + + if (gap) { + let leftMargin = this.bounds.left - this.parentBounds.left; + this.gap = gap - leftMargin; + } else { + this.gap = 0; + } + + if (hooks) { + this.hooks = hooks; + } else { + this.hooks = {}; + this.hooks.onPageLayout = new Hook(); + this.hooks.layout = new Hook(); + this.hooks.renderNode = new Hook(); + this.hooks.layoutNode = new Hook(); + this.hooks.beforeOverflow = new Hook(); + this.hooks.onOverflow = new Hook(); + this.hooks.afterOverflowRemoved = new Hook(); + this.hooks.onBreakToken = new Hook(); + this.hooks.beforeRenderResult = new Hook(); + } + + this.settings = options || {}; + + this.maxChars = this.settings.maxChars || MAX_CHARS_PER_BREAK; + this.forceRenderBreak = false; + } + + async renderTo(wrapper, source, breakToken, bounds = this.bounds) { + let start = this.getStart(source, breakToken); + let walker = walk$2(start, source); + + let node; + let prevNode; + let done; + let next; + + let hasRenderedContent = false; + let newBreakToken; + + let length = 0; + + let prevBreakToken = breakToken || new BreakToken(start); + + this.hooks && this.hooks.onPageLayout.trigger(wrapper, prevBreakToken, this); + + while (!done && !newBreakToken) { + next = walker.next(); + prevNode = node; + node = next.value; + done = next.done; + + if (!node) { + this.hooks && this.hooks.layout.trigger(wrapper, this); + + let imgs = wrapper.querySelectorAll("img"); + if (imgs.length) { + await this.waitForImages(imgs); + } + + newBreakToken = this.findBreakToken(wrapper, source, bounds, prevBreakToken); + + if (newBreakToken && newBreakToken.equals(prevBreakToken)) { + console.warn("Unable to layout item: ", prevNode); + this.hooks && this.hooks.beforeRenderResult.trigger(undefined, wrapper, this); + return new RenderResult(undefined, new OverflowContentError("Unable to layout item", [prevNode])); + } + + this.rebuildTableFromBreakToken(newBreakToken, wrapper); + + this.hooks && this.hooks.beforeRenderResult.trigger(newBreakToken, wrapper, this); + return new RenderResult(newBreakToken); + } + + this.hooks && this.hooks.layoutNode.trigger(node); + + // Check if the rendered element has a break set + if (hasRenderedContent && this.shouldBreak(node, start)) { + this.hooks && this.hooks.layout.trigger(wrapper, this); + + let imgs = wrapper.querySelectorAll("img"); + if (imgs.length) { + await this.waitForImages(imgs); + } + + newBreakToken = this.findBreakToken(wrapper, source, bounds, prevBreakToken); + + if (!newBreakToken) { + newBreakToken = this.breakAt(node); + } else { + this.rebuildTableFromBreakToken(newBreakToken, wrapper); + } + + if (newBreakToken && newBreakToken.equals(prevBreakToken)) { + console.warn("Unable to layout item: ", node); + let after = newBreakToken.node && nodeAfter(newBreakToken.node); + if (after) { + newBreakToken = new BreakToken(after); + } else { + return new RenderResult(undefined, new OverflowContentError("Unable to layout item", [node])); + } + } + + length = 0; + + break; + } + + if (node.dataset && node.dataset.page) { + let named = node.dataset.page; + let page = this.element.closest(".pagedjs_page"); + page.classList.add("pagedjs_named_page"); + page.classList.add("pagedjs_" + named + "_page"); + + if (!node.dataset.splitFrom) { + page.classList.add("pagedjs_" + named + "_first_page"); + } + } + + // Should the Node be a shallow or deep clone + let shallow = isContainer(node); + + let rendered = this.append(node, wrapper, breakToken, shallow); + + length += rendered.textContent.length; + + // Check if layout has content yet + if (!hasRenderedContent) { + hasRenderedContent = hasContent(node); + } + + // Skip to the next node if a deep clone was rendered + if (!shallow) { + walker = walk$2(nodeAfter(node, source), source); + } + + if (this.forceRenderBreak) { + this.hooks && this.hooks.layout.trigger(wrapper, this); + + newBreakToken = this.findBreakToken(wrapper, source, bounds, prevBreakToken); + + if (!newBreakToken) { + newBreakToken = this.breakAt(node); + } else { + this.rebuildTableFromBreakToken(newBreakToken, wrapper); + } + + length = 0; + this.forceRenderBreak = false; + + break; + } + + // Only check x characters + if (length >= this.maxChars) { + + this.hooks && this.hooks.layout.trigger(wrapper, this); + + let imgs = wrapper.querySelectorAll("img"); + if (imgs.length) { + await this.waitForImages(imgs); + } + + newBreakToken = this.findBreakToken(wrapper, source, bounds, prevBreakToken); + + if (newBreakToken) { + length = 0; + this.rebuildTableFromBreakToken(newBreakToken, wrapper); + } + + if (newBreakToken && newBreakToken.equals(prevBreakToken)) { + console.warn("Unable to layout item: ", node); + let after = newBreakToken.node && nodeAfter(newBreakToken.node); + if (after) { + newBreakToken = new BreakToken(after); + } else { + this.hooks && this.hooks.beforeRenderResult.trigger(undefined, wrapper, this); + return new RenderResult(undefined, new OverflowContentError("Unable to layout item", [node])); + } + } + } + + } + + this.hooks && this.hooks.beforeRenderResult.trigger(newBreakToken, wrapper, this); + return new RenderResult(newBreakToken); + } + + breakAt(node, offset = 0) { + let newBreakToken = new BreakToken( + node, + offset + ); + let breakHooks = this.hooks.onBreakToken.triggerSync(newBreakToken, undefined, node, this); + breakHooks.forEach((newToken) => { + if (typeof newToken != "undefined") { + newBreakToken = newToken; + } + }); + + return newBreakToken; + } + + shouldBreak(node, limiter) { + let previousNode = nodeBefore(node, limiter); + let parentNode = node.parentNode; + let parentBreakBefore = needsBreakBefore(node) && parentNode && !previousNode && needsBreakBefore(parentNode); + let doubleBreakBefore; + + if (parentBreakBefore) { + doubleBreakBefore = node.dataset.breakBefore === parentNode.dataset.breakBefore; + } + + return !doubleBreakBefore && needsBreakBefore(node) || needsPreviousBreakAfter(node) || needsPageBreak(node, previousNode); + } + + forceBreak() { + this.forceRenderBreak = true; + } + + getStart(source, breakToken) { + let start; + let node = breakToken && breakToken.node; + + if (node) { + start = node; + } else { + start = source.firstChild; + } + + return start; + } + + append(node, dest, breakToken, shallow = true, rebuild = true) { + + let clone = cloneNode(node, !shallow); + + if (node.parentNode && isElement(node.parentNode)) { + let parent = findElement(node.parentNode, dest); + // Rebuild chain + if (parent) { + parent.appendChild(clone); + } else if (rebuild) { + let fragment = rebuildAncestors(node); + parent = findElement(node.parentNode, fragment); + if (!parent) { + dest.appendChild(clone); + } else if (breakToken && isText(breakToken.node) && breakToken.offset > 0) { + clone.textContent = clone.textContent.substring(breakToken.offset); + parent.appendChild(clone); + } else { + parent.appendChild(clone); + } + + dest.appendChild(fragment); + } else { + dest.appendChild(clone); + } + + + } else { + dest.appendChild(clone); + } + + if (clone.dataset && clone.dataset.ref) { + if (!dest.indexOfRefs) { + dest.indexOfRefs = {}; + } + dest.indexOfRefs[clone.dataset.ref] = clone; + } + + let nodeHooks = this.hooks.renderNode.triggerSync(clone, node, this); + nodeHooks.forEach((newNode) => { + if (typeof newNode != "undefined") { + clone = newNode; + } + }); + + return clone; + } + + rebuildTableFromBreakToken(breakToken, dest) { + if (!breakToken || !breakToken.node) { + return; + } + let node = breakToken.node; + let td = isElement(node) ? node.closest("td") : node.parentElement.closest("td"); + if (td) { + let rendered = findElement(td, dest, true); + if (!rendered) { + return; + } + while ((td = td.nextElementSibling)) { + this.append(td, dest, null, true); + } + } + } + + async waitForImages(imgs) { + let results = Array.from(imgs).map(async (img) => { + return this.awaitImageLoaded(img); + }); + await Promise.all(results); + } + + async awaitImageLoaded(image) { + return new Promise(resolve => { + if (image.complete !== true) { + image.onload = function () { + let {width, height} = window.getComputedStyle(image); + resolve(width, height); + }; + image.onerror = function (e) { + let {width, height} = window.getComputedStyle(image); + resolve(width, height, e); + }; + } else { + let {width, height} = window.getComputedStyle(image); + resolve(width, height); + } + }); + } + + avoidBreakInside(node, limiter) { + let breakNode; + + if (node === limiter) { + return; + } + + while (node.parentNode) { + node = node.parentNode; + + if (node === limiter) { + break; + } + + if (window.getComputedStyle(node)["break-inside"] === "avoid") { + breakNode = node; + break; + } + + } + return breakNode; + } + + createBreakToken(overflow, rendered, source) { + let container = overflow.startContainer; + let offset = overflow.startOffset; + let node, renderedNode, parent, index, temp; + + if (isElement(container)) { + temp = child(container, offset); + + if (isElement(temp)) { + renderedNode = findElement(temp, rendered); + + if (!renderedNode) { + // Find closest element with data-ref + let prevNode = prevValidNode(temp); + if (!isElement(prevNode)) { + prevNode = prevNode.parentElement; + } + renderedNode = findElement(prevNode, rendered); + // Check if temp is the last rendered node at its level. + if (!temp.nextSibling) { + // We need to ensure that the previous sibling of temp is fully rendered. + const renderedNodeFromSource = findElement(renderedNode, source); + const walker = document.createTreeWalker(renderedNodeFromSource, NodeFilter.SHOW_ELEMENT); + const lastChildOfRenderedNodeFromSource = walker.lastChild(); + const lastChildOfRenderedNodeMatchingFromRendered = findElement(lastChildOfRenderedNodeFromSource, rendered); + // Check if we found that the last child in source + if (!lastChildOfRenderedNodeMatchingFromRendered) { + // Pending content to be rendered before virtual break token + return; + } + // Otherwise we will return a break token as per below + } + // renderedNode is actually the last unbroken box that does not overflow. + // Break Token is therefore the next sibling of renderedNode within source node. + node = findElement(renderedNode, source).nextSibling; + offset = 0; + } else { + node = findElement(renderedNode, source); + offset = 0; + } + } else { + renderedNode = findElement(container, rendered); + + if (!renderedNode) { + renderedNode = findElement(prevValidNode(container), rendered); + } + + parent = findElement(renderedNode, source); + index = indexOfTextNode(temp, parent); + // No seperatation for the first textNode of an element + if(index === 0) { + node = parent; + offset = 0; + } else { + node = child(parent, index); + offset = 0; + } + } + } else { + renderedNode = findElement(container.parentNode, rendered); + + if (!renderedNode) { + renderedNode = findElement(prevValidNode(container.parentNode), rendered); + } + + parent = findElement(renderedNode, source); + index = indexOfTextNode(container, parent); + + if (index === -1) { + return; + } + + node = child(parent, index); + + offset += node.textContent.indexOf(container.textContent); + } + + if (!node) { + return; + } + + return new BreakToken( + node, + offset + ); + + } + + findBreakToken(rendered, source, bounds = this.bounds, prevBreakToken, extract = true) { + let overflow = this.findOverflow(rendered, bounds); + let breakToken, breakLetter; + + let overflowHooks = this.hooks.onOverflow.triggerSync(overflow, rendered, bounds, this); + overflowHooks.forEach((newOverflow) => { + if (typeof newOverflow != "undefined") { + overflow = newOverflow; + } + }); + + if (overflow) { + breakToken = this.createBreakToken(overflow, rendered, source); + // breakToken is nullable + let breakHooks = this.hooks.onBreakToken.triggerSync(breakToken, overflow, rendered, this); + breakHooks.forEach((newToken) => { + if (typeof newToken != "undefined") { + breakToken = newToken; + } + }); + + // Stop removal if we are in a loop + if (breakToken && breakToken.equals(prevBreakToken)) { + return breakToken; + } + + if (breakToken && breakToken["node"] && breakToken["offset"] && breakToken["node"].textContent) { + breakLetter = breakToken["node"].textContent.charAt(breakToken["offset"]); + } else { + breakLetter = undefined; + } + + if (breakToken && breakToken.node && extract) { + let removed = this.removeOverflow(overflow, breakLetter); + this.hooks && this.hooks.afterOverflowRemoved.trigger(removed, rendered, this); + } + + } + return breakToken; + } + + hasOverflow(element, bounds = this.bounds) { + let constrainingElement = element && element.parentNode; // this gets the element, instead of the wrapper for the width workaround + let {width, height} = element.getBoundingClientRect(); + let scrollWidth = constrainingElement ? constrainingElement.scrollWidth : 0; + let scrollHeight = constrainingElement ? constrainingElement.scrollHeight : 0; + return Math.max(Math.floor(width), scrollWidth) > Math.round(bounds.width) || + Math.max(Math.floor(height), scrollHeight) > Math.round(bounds.height); + } + + findOverflow(rendered, bounds = this.bounds, gap = this.gap) { + if (!this.hasOverflow(rendered, bounds)) return; + + let start = Math.floor(bounds.left); + let end = Math.round(bounds.right + gap); + let vStart = Math.round(bounds.top); + let vEnd = Math.round(bounds.bottom); + let range; + + let walker = walk$2(rendered.firstChild, rendered); + + // Find Start + let next, done, node, offset, skip, breakAvoid, prev, br; + while (!done) { + next = walker.next(); + done = next.done; + node = next.value; + skip = false; + breakAvoid = false; + prev = undefined; + br = undefined; + + if (node) { + let pos = getBoundingClientRect(node); + let left = Math.round(pos.left); + let right = Math.floor(pos.right); + let top = Math.round(pos.top); + let bottom = Math.floor(pos.bottom); + + if (!range && (left >= end || top >= vEnd)) { + // Check if it is a float + let isFloat = false; + + // Check if the node is inside a break-inside: avoid table cell + const insideTableCell = parentOf(node, "TD", rendered); + if (insideTableCell && window.getComputedStyle(insideTableCell)["break-inside"] === "avoid") { + // breaking inside a table cell produces unexpected result, as a workaround, we forcibly avoid break inside in a cell. + // But we take the whole row, not just the cell that is causing the break. + prev = insideTableCell.parentElement; + } else if (isElement(node)) { + let styles = window.getComputedStyle(node); + isFloat = styles.getPropertyValue("float") !== "none"; + skip = styles.getPropertyValue("break-inside") === "avoid"; + breakAvoid = node.dataset.breakBefore === "avoid" || node.dataset.previousBreakAfter === "avoid"; + prev = breakAvoid && nodeBefore(node, rendered); + br = node.tagName === "BR" || node.tagName === "WBR"; + } + + let tableRow; + if (node.nodeName === "TR") { + tableRow = node; + } else { + tableRow = parentOf(node, "TR", rendered); + } + if (tableRow) { + // honor break-inside="avoid" in parent tbody/thead + let container = tableRow.parentElement; + if (["TBODY", "THEAD"].includes(container.nodeName)) { + let styles = window.getComputedStyle(container); + if (styles.getPropertyValue("break-inside") === "avoid") prev = container; + } + + // Check if the node is inside a row with a rowspan + const table = parentOf(tableRow, "TABLE", rendered); + const rowspan = table.querySelector("[colspan]"); + if (table && rowspan) { + let columnCount = 0; + for (const cell of Array.from(table.rows[0].cells)) { + columnCount += parseInt(cell.getAttribute("colspan") || "1"); + } + if (tableRow.cells.length !== columnCount) { + let previousRow = tableRow.previousElementSibling; + let previousRowColumnCount; + while (previousRow !== null) { + previousRowColumnCount = 0; + for (const cell of Array.from(previousRow.cells)) { + previousRowColumnCount += parseInt(cell.getAttribute("colspan") || "1"); + } + if (previousRowColumnCount === columnCount) { + break; + } + previousRow = previousRow.previousElementSibling; + } + if (previousRowColumnCount === columnCount) { + prev = previousRow; + } + } + } + } + + if (prev) { + range = document.createRange(); + range.selectNode(prev); + break; + } + + if (!br && !isFloat && isElement(node)) { + range = document.createRange(); + range.selectNode(node); + break; + } + + if (isText(node) && node.textContent.trim().length) { + range = document.createRange(); + range.selectNode(node); + break; + } + + } + + if (!range && isText(node) && + node.textContent.trim().length && + !breakInsideAvoidParentNode(node.parentNode)) { + + let rects = getClientRects(node); + let rect; + left = 0; + top = 0; + for (var i = 0; i != rects.length; i++) { + rect = rects[i]; + if (rect.width > 0 && (!left || rect.left > left)) { + left = rect.left; + } + if (rect.height > 0 && (!top || rect.top > top)) { + top = rect.top; + } + } + + if (left >= end || top >= vEnd) { + range = document.createRange(); + offset = this.textBreak(node, start, end, vStart, vEnd); + if (!offset) { + range = undefined; + } else { + range.setStart(node, offset); + } + break; + } + } + + // Skip children + if (skip || (right <= end && bottom <= vEnd)) { + next = nodeAfter(node, rendered); + if (next) { + walker = walk$2(next, rendered); + } + + } + + } + } + + // Find End + if (range) { + range.setEndAfter(rendered.lastChild); + return range; + } + + } + + findEndToken(rendered, source) { + if (rendered.childNodes.length === 0) { + return; + } + + let lastChild = rendered.lastChild; + + let lastNodeIndex; + while (lastChild && lastChild.lastChild) { + if (!validNode(lastChild)) { + // Only get elements with refs + lastChild = lastChild.previousSibling; + } else if (!validNode(lastChild.lastChild)) { + // Deal with invalid dom items + lastChild = prevValidNode(lastChild.lastChild); + break; + } else { + lastChild = lastChild.lastChild; + } + } + + if (isText(lastChild)) { + + if (lastChild.parentNode.dataset.ref) { + lastNodeIndex = indexOf$2(lastChild); + lastChild = lastChild.parentNode; + } else { + lastChild = lastChild.previousSibling; + } + } + + let original = findElement(lastChild, source); + + if (lastNodeIndex) { + original = original.childNodes[lastNodeIndex]; + } + + let after = nodeAfter(original); + + return this.breakAt(after); + } + + textBreak(node, start, end, vStart, vEnd) { + let wordwalker = words(node); + let left = 0; + let right = 0; + let top = 0; + let bottom = 0; + let word, next, done, pos; + let offset; + while (!done) { + next = wordwalker.next(); + word = next.value; + done = next.done; + + if (!word) { + break; + } + + pos = getBoundingClientRect(word); + + left = Math.floor(pos.left); + right = Math.floor(pos.right); + top = Math.floor(pos.top); + bottom = Math.floor(pos.bottom); + + if (left >= end || top >= vEnd) { + offset = word.startOffset; + break; + } + + if (right > end || bottom > vEnd) { + let letterwalker = letters(word); + let letter, nextLetter, doneLetter; + + while (!doneLetter) { + nextLetter = letterwalker.next(); + letter = nextLetter.value; + doneLetter = nextLetter.done; + + if (!letter) { + break; + } + + pos = getBoundingClientRect(letter); + left = Math.floor(pos.left); + top = Math.floor(pos.top); + + if (left >= end || top >= vEnd) { + offset = letter.startOffset; + done = true; + + break; + } + } + } + + } + + return offset; + } + + removeOverflow(overflow, breakLetter) { + let {startContainer} = overflow; + let extracted = overflow.extractContents(); + + this.hyphenateAtBreak(startContainer, breakLetter); + + return extracted; + } + + hyphenateAtBreak(startContainer, breakLetter) { + if (isText(startContainer)) { + let startText = startContainer.textContent; + let prevLetter = startText[startText.length - 1]; + + // Add a hyphen if previous character is a letter or soft hyphen + if ( + (breakLetter && /^\w|\u00AD$/.test(prevLetter) && /^\w|\u00AD$/.test(breakLetter)) || + (!breakLetter && /^\w|\u00AD$/.test(prevLetter)) + ) { + startContainer.parentNode.classList.add("pagedjs_hyphen"); + startContainer.textContent += this.settings.hyphenGlyph || "\u2011"; + } + } + } + + equalTokens(a, b) { + if (!a || !b) { + return false; + } + if (a["node"] && b["node"] && a["node"] !== b["node"]) { + return false; + } + if (a["offset"] && b["offset"] && a["offset"] !== b["offset"]) { + return false; + } + return true; + } + } + + EventEmitter(Layout.prototype); + + /** + * Render a page + * @class + */ + class Page { + constructor(pagesArea, pageTemplate, blank, hooks, options) { + this.pagesArea = pagesArea; + this.pageTemplate = pageTemplate; + this.blank = blank; + + this.width = undefined; + this.height = undefined; + + this.hooks = hooks; + + this.settings = options || {}; + + // this.element = this.create(this.pageTemplate); + } + + create(template, after) { + //let documentFragment = document.createRange().createContextualFragment( TEMPLATE ); + //let page = documentFragment.children[0]; + let clone = document.importNode(this.pageTemplate.content, true); + + let page, index; + if (after) { + this.pagesArea.insertBefore(clone, after.nextElementSibling); + index = Array.prototype.indexOf.call(this.pagesArea.children, after.nextElementSibling); + page = this.pagesArea.children[index]; + } else { + this.pagesArea.appendChild(clone); + page = this.pagesArea.lastChild; + } + + let pagebox = page.querySelector(".pagedjs_pagebox"); + let area = page.querySelector(".pagedjs_page_content"); + let footnotesArea = page.querySelector(".pagedjs_footnote_area"); + + + let size = area.getBoundingClientRect(); + + + area.style.columnWidth = Math.round(size.width) + "px"; + area.style.columnGap = "calc(var(--pagedjs-margin-right) + var(--pagedjs-margin-left) + var(--pagedjs-bleed-right) + var(--pagedjs-bleed-left) + var(--pagedjs-column-gap-offset))"; + // area.style.overflow = "scroll"; + + this.width = Math.round(size.width); + this.height = Math.round(size.height); + + this.element = page; + this.pagebox = pagebox; + this.area = area; + this.footnotesArea = footnotesArea; + + return page; + } + + createWrapper() { + let wrapper = document.createElement("div"); + + this.area.appendChild(wrapper); + + this.wrapper = wrapper; + + return wrapper; + } + + index(pgnum) { + this.position = pgnum; + + let page = this.element; + // let pagebox = this.pagebox; + + let index = pgnum + 1; + + let id = `page-${index}`; + + this.id = id; + + // page.dataset.pageNumber = index; + + page.dataset.pageNumber = index; + page.setAttribute("id", id); + + if (this.name) { + page.classList.add("pagedjs_" + this.name + "_page"); + } + + if (this.blank) { + page.classList.add("pagedjs_blank_page"); + } + + if (pgnum === 0) { + page.classList.add("pagedjs_first_page"); + } + + if (pgnum % 2 !== 1) { + page.classList.remove("pagedjs_left_page"); + page.classList.add("pagedjs_right_page"); + } else { + page.classList.remove("pagedjs_right_page"); + page.classList.add("pagedjs_left_page"); + } + } + + /* + size(width, height) { + if (width === this.width && height === this.height) { + return; + } + this.width = width; + this.height = height; + + this.element.style.width = Math.round(width) + "px"; + this.element.style.height = Math.round(height) + "px"; + this.element.style.columnWidth = Math.round(width) + "px"; + } + */ + + async layout(contents, breakToken, maxChars) { + + this.clear(); + + this.startToken = breakToken; + + let settings = this.settings; + if (!settings.maxChars && maxChars) { + settings.maxChars = maxChars; + } + + this.layoutMethod = new Layout(this.area, this.hooks, settings); + + let renderResult = await this.layoutMethod.renderTo(this.wrapper, contents, breakToken); + let newBreakToken = renderResult.breakToken; + + this.addListeners(contents); + + this.endToken = newBreakToken; + + return newBreakToken; + } + + async append(contents, breakToken) { + + if (!this.layoutMethod) { + return this.layout(contents, breakToken); + } + + let renderResult = await this.layoutMethod.renderTo(this.wrapper, contents, breakToken); + let newBreakToken = renderResult.breakToken; + + this.endToken = newBreakToken; + + return newBreakToken; + } + + getByParent(ref, entries) { + let e; + for (var i = 0; i < entries.length; i++) { + e = entries[i]; + if (e.dataset.ref === ref) { + return e; + } + } + } + + onOverflow(func) { + this._onOverflow = func; + } + + onUnderflow(func) { + this._onUnderflow = func; + } + + clear() { + this.removeListeners(); + this.wrapper && this.wrapper.remove(); + this.createWrapper(); + } + + addListeners(contents) { + if (typeof ResizeObserver !== "undefined") { + this.addResizeObserver(contents); + } else { + this._checkOverflowAfterResize = this.checkOverflowAfterResize.bind(this, contents); + this.element.addEventListener("overflow", this._checkOverflowAfterResize, false); + this.element.addEventListener("underflow", this._checkOverflowAfterResize, false); + } + // TODO: fall back to mutation observer? + + this._onScroll = function () { + if (this.listening) { + this.element.scrollLeft = 0; + } + }.bind(this); + + // Keep scroll left from changing + this.element.addEventListener("scroll", this._onScroll); + + this.listening = true; + + return true; + } + + removeListeners() { + this.listening = false; + + if (typeof ResizeObserver !== "undefined" && this.ro) { + this.ro.disconnect(); + } else if (this.element) { + this.element.removeEventListener("overflow", this._checkOverflowAfterResize, false); + this.element.removeEventListener("underflow", this._checkOverflowAfterResize, false); + } + + this.element && this.element.removeEventListener("scroll", this._onScroll); + + } + + addResizeObserver(contents) { + let wrapper = this.wrapper; + let prevHeight = wrapper.getBoundingClientRect().height; + this.ro = new ResizeObserver(entries => { + + if (!this.listening) { + return; + } + requestAnimationFrame(() => { + for (let entry of entries) { + const cr = entry.contentRect; + + if (cr.height > prevHeight) { + this.checkOverflowAfterResize(contents); + prevHeight = wrapper.getBoundingClientRect().height; + } else if (cr.height < prevHeight) { // TODO: calc line height && (prevHeight - cr.height) >= 22 + this.checkUnderflowAfterResize(contents); + prevHeight = cr.height; + } + } + }); + }); + + this.ro.observe(wrapper); + } + + checkOverflowAfterResize(contents) { + if (!this.listening || !this.layoutMethod) { + return; + } + + let newBreakToken = this.layoutMethod.findBreakToken(this.wrapper, contents, this.startToken); + + if (newBreakToken) { + this.endToken = newBreakToken; + this._onOverflow && this._onOverflow(newBreakToken); + } + } + + checkUnderflowAfterResize(contents) { + if (!this.listening || !this.layoutMethod) { + return; + } + + let endToken = this.layoutMethod.findEndToken(this.wrapper, contents); + + if (endToken) { + this._onUnderflow && this._onUnderflow(endToken); + } + } + + + destroy() { + this.removeListeners(); + + this.element.remove(); + + this.element = undefined; + this.wrapper = undefined; + } + } + + EventEmitter(Page.prototype); + + /** + * Render a flow of text offscreen + * @class + */ + class ContentParser { + + constructor(content, cb) { + if (content && content.nodeType) { + // handle dom + this.dom = this.add(content); + } else if (typeof content === "string") { + this.dom = this.parse(content); + } + + return this.dom; + } + + parse(markup, mime) { + let range = document.createRange(); + let fragment = range.createContextualFragment(markup); + + this.addRefs(fragment); + + return fragment; + } + + add(contents) { + // let fragment = document.createDocumentFragment(); + // + // let children = [...contents.childNodes]; + // for (let child of children) { + // let clone = child.cloneNode(true); + // fragment.appendChild(clone); + // } + + this.addRefs(contents); + + return contents; + } + + addRefs(content) { + var treeWalker = document.createTreeWalker( + content, + NodeFilter.SHOW_ELEMENT, + null, + false + ); + + let node = treeWalker.nextNode(); + while(node) { + + if (!node.hasAttribute("data-ref")) { + let uuid = UUID(); + node.setAttribute("data-ref", uuid); + } + + if (node.id) { + node.setAttribute("data-id", node.id); + } + + // node.setAttribute("data-children", node.childNodes.length); + + // node.setAttribute("data-text", node.textContent.trim().length); + node = treeWalker.nextNode(); + } + } + + find(ref) { + return this.refs[ref]; + } + + destroy() { + this.refs = undefined; + this.dom = undefined; + } + } + + /** + * Queue for handling tasks one at a time + * @class + * @param {scope} context what this will resolve to in the tasks + */ + class Queue { + constructor(context){ + this._q = []; + this.context = context; + this.tick = requestAnimationFrame; + this.running = false; + this.paused = false; + } + + /** + * Add an item to the queue + * @return {Promise} enqueued + */ + enqueue() { + var deferred, promise; + var queued; + var task = [].shift.call(arguments); + var args = arguments; + + // Handle single args without context + // if(args && !Array.isArray(args)) { + // args = [args]; + // } + if(!task) { + throw new Error("No Task Provided"); + } + + if(typeof task === "function"){ + + deferred = new defer(); + promise = deferred.promise; + + queued = { + "task" : task, + "args" : args, + //"context" : context, + "deferred" : deferred, + "promise" : promise + }; + + } else { + // Task is a promise + queued = { + "promise" : task + }; + + } + + this._q.push(queued); + + // Wait to start queue flush + if (this.paused == false && !this.running) { + this.run(); + } + + return queued.promise; + } + + /** + * Run one item + * @return {Promise} dequeued + */ + dequeue(){ + var inwait, task, result; + + if(this._q.length && !this.paused) { + inwait = this._q.shift(); + task = inwait.task; + if(task){ + // console.log(task) + + result = task.apply(this.context, inwait.args); + + if(result && typeof result["then"] === "function") { + // Task is a function that returns a promise + return result.then(function(){ + inwait.deferred.resolve.apply(this.context, arguments); + }.bind(this), function() { + inwait.deferred.reject.apply(this.context, arguments); + }.bind(this)); + } else { + // Task resolves immediately + inwait.deferred.resolve.apply(this.context, result); + return inwait.promise; + } + + + + } else if(inwait.promise) { + // Task is a promise + return inwait.promise; + } + + } else { + inwait = new defer(); + inwait.deferred.resolve(); + return inwait.promise; + } + + } + + // Run All Immediately + dump(){ + while(this._q.length) { + this.dequeue(); + } + } + + /** + * Run all tasks sequentially, at convince + * @return {Promise} all run + */ + run(){ + + if(!this.running){ + this.running = true; + this.defered = new defer(); + } + + this.tick.call(window, () => { + + if(this._q.length) { + + this.dequeue() + .then(function(){ + this.run(); + }.bind(this)); + + } else { + this.defered.resolve(); + this.running = undefined; + } + + }); + + // Unpause + if(this.paused == true) { + this.paused = false; + } + + return this.defered.promise; + } + + /** + * Flush all, as quickly as possible + * @return {Promise} ran + */ + flush(){ + + if(this.running){ + return this.running; + } + + if(this._q.length) { + this.running = this.dequeue() + .then(function(){ + this.running = undefined; + return this.flush(); + }.bind(this)); + + return this.running; + } + + } + + /** + * Clear all items in wait + * @return {void} + */ + clear(){ + this._q = []; + } + + /** + * Get the number of tasks in the queue + * @return {number} tasks + */ + length(){ + return this._q.length; + } + + /** + * Pause a running queue + * @return {void} + */ + pause(){ + this.paused = true; + } + + /** + * End the queue + * @return {void} + */ + stop(){ + this._q = []; + this.running = false; + this.paused = true; + } + } + + const TEMPLATE = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`; + + /** + * Chop up text into flows + * @class + */ + class Chunker { + constructor(content, renderTo, options) { + // this.preview = preview; + + this.settings = options || {}; + + this.hooks = {}; + this.hooks.beforeParsed = new Hook(this); + this.hooks.filter = new Hook(this); + this.hooks.afterParsed = new Hook(this); + this.hooks.beforePageLayout = new Hook(this); + this.hooks.onPageLayout = new Hook(this); + this.hooks.layout = new Hook(this); + this.hooks.renderNode = new Hook(this); + this.hooks.layoutNode = new Hook(this); + this.hooks.onOverflow = new Hook(this); + this.hooks.afterOverflowRemoved = new Hook(this); + this.hooks.onBreakToken = new Hook(); + this.hooks.beforeRenderResult = new Hook(this); + this.hooks.afterPageLayout = new Hook(this); + this.hooks.finalizePage = new Hook(this); + this.hooks.afterRendered = new Hook(this); + + this.pages = []; + this.total = 0; + + this.q = new Queue(this); + this.stopped = false; + this.rendered = false; + + this.content = content; + + this.charsPerBreak = []; + this.maxChars; + + if (content) { + this.flow(content, renderTo); + } + } + + setup(renderTo) { + this.pagesArea = document.createElement("div"); + this.pagesArea.classList.add("pagedjs_pages"); + + if (renderTo) { + renderTo.appendChild(this.pagesArea); + } else { + document.querySelector("body").appendChild(this.pagesArea); + } + + this.pageTemplate = document.createElement("template"); + this.pageTemplate.innerHTML = TEMPLATE; + + } + + async flow(content, renderTo) { + let parsed; + + await this.hooks.beforeParsed.trigger(content, this); + + parsed = new ContentParser(content); + + this.hooks.filter.triggerSync(parsed); + + this.source = parsed; + this.breakToken = undefined; + + if (this.pagesArea && this.pageTemplate) { + this.q.clear(); + this.removePages(); + } else { + this.setup(renderTo); + } + + this.emit("rendering", parsed); + + await this.hooks.afterParsed.trigger(parsed, this); + + await this.loadFonts(); + + let rendered = await this.render(parsed, this.breakToken); + while (rendered.canceled) { + this.start(); + rendered = await this.render(parsed, this.breakToken); + } + + this.rendered = true; + this.pagesArea.style.setProperty("--pagedjs-page-count", this.total); + + await this.hooks.afterRendered.trigger(this.pages, this); + + this.emit("rendered", this.pages); + + + + return this; + } + + // oversetPages() { + // let overset = []; + // for (let i = 0; i < this.pages.length; i++) { + // let page = this.pages[i]; + // if (page.overset) { + // overset.push(page); + // // page.overset = false; + // } + // } + // return overset; + // } + // + // async handleOverset(parsed) { + // let overset = this.oversetPages(); + // if (overset.length) { + // console.log("overset", overset); + // let index = this.pages.indexOf(overset[0]) + 1; + // console.log("INDEX", index); + // + // // Remove pages + // // this.removePages(index); + // + // // await this.render(parsed, overset[0].overset); + // + // // return this.handleOverset(parsed); + // } + // } + + async render(parsed, startAt) { + let renderer = this.layout(parsed, startAt); + + let done = false; + let result; + while (!done) { + result = await this.q.enqueue(() => { return this.renderAsync(renderer); }); + done = result.done; + } + + return result; + } + + start() { + this.rendered = false; + this.stopped = false; + } + + stop() { + this.stopped = true; + // this.q.clear(); + } + + renderOnIdle(renderer) { + return new Promise(resolve => { + requestIdleCallback(async () => { + if (this.stopped) { + return resolve({ done: true, canceled: true }); + } + let result = await renderer.next(); + if (this.stopped) { + resolve({ done: true, canceled: true }); + } else { + resolve(result); + } + }); + }); + } + + async renderAsync(renderer) { + if (this.stopped) { + return { done: true, canceled: true }; + } + let result = await renderer.next(); + if (this.stopped) { + return { done: true, canceled: true }; + } else { + return result; + } + } + + async handleBreaks(node, force) { + let currentPage = this.total + 1; + let currentPosition = currentPage % 2 === 0 ? "left" : "right"; + // TODO: Recto and Verso should reverse for rtl languages + let currentSide = currentPage % 2 === 0 ? "verso" : "recto"; + let previousBreakAfter; + let breakBefore; + let page; + + if (currentPage === 1) { + return; + } + + if (node && + typeof node.dataset !== "undefined" && + typeof node.dataset.previousBreakAfter !== "undefined") { + previousBreakAfter = node.dataset.previousBreakAfter; + } + + if (node && + typeof node.dataset !== "undefined" && + typeof node.dataset.breakBefore !== "undefined") { + breakBefore = node.dataset.breakBefore; + } + + if (force) { + page = this.addPage(true); + } else if( previousBreakAfter && + (previousBreakAfter === "left" || previousBreakAfter === "right") && + previousBreakAfter !== currentPosition) { + page = this.addPage(true); + } else if( previousBreakAfter && + (previousBreakAfter === "verso" || previousBreakAfter === "recto") && + previousBreakAfter !== currentSide) { + page = this.addPage(true); + } else if( breakBefore && + (breakBefore === "left" || breakBefore === "right") && + breakBefore !== currentPosition) { + page = this.addPage(true); + } else if( breakBefore && + (breakBefore === "verso" || breakBefore === "recto") && + breakBefore !== currentSide) { + page = this.addPage(true); + } + + if (page) { + await this.hooks.beforePageLayout.trigger(page, undefined, undefined, this); + this.emit("page", page); + // await this.hooks.layout.trigger(page.element, page, undefined, this); + await this.hooks.afterPageLayout.trigger(page.element, page, undefined, this); + await this.hooks.finalizePage.trigger(page.element, page, undefined, this); + this.emit("renderedPage", page); + } + } + + async *layout(content, startAt) { + let breakToken = startAt || false; + let tokens = []; + + while (breakToken !== undefined && (true)) { + + if (breakToken && breakToken.node) { + await this.handleBreaks(breakToken.node); + } else { + await this.handleBreaks(content.firstChild); + } + + let page = this.addPage(); + + await this.hooks.beforePageLayout.trigger(page, content, breakToken, this); + this.emit("page", page); + + // Layout content in the page, starting from the breakToken + breakToken = await page.layout(content, breakToken, this.maxChars); + + if (breakToken) { + let newToken = breakToken.toJSON(true); + if (tokens.lastIndexOf(newToken) > -1) { + // loop + let err = new OverflowContentError("Layout repeated", [breakToken.node]); + console.error("Layout repeated at: ", breakToken.node); + return err; + } else { + tokens.push(newToken); + } + } + + await this.hooks.afterPageLayout.trigger(page.element, page, breakToken, this); + await this.hooks.finalizePage.trigger(page.element, page, undefined, this); + this.emit("renderedPage", page); + + this.recoredCharLength(page.wrapper.textContent.length); + + yield breakToken; + + // Stop if we get undefined, showing we have reached the end of the content + } + + + } + + recoredCharLength(length) { + if (length === 0) { + return; + } + + this.charsPerBreak.push(length); + + // Keep the length of the last few breaks + if (this.charsPerBreak.length > 4) { + this.charsPerBreak.shift(); + } + + this.maxChars = this.charsPerBreak.reduce((a, b) => a + b, 0) / (this.charsPerBreak.length); + } + + removePages(fromIndex=0) { + + if (fromIndex >= this.pages.length) { + return; + } + + // Remove pages + for (let i = fromIndex; i < this.pages.length; i++) { + this.pages[i].destroy(); + } + + if (fromIndex > 0) { + this.pages.splice(fromIndex); + } else { + this.pages = []; + } + + this.total = this.pages.length; + } + + addPage(blank) { + let lastPage = this.pages[this.pages.length - 1]; + // Create a new page from the template + let page = new Page(this.pagesArea, this.pageTemplate, blank, this.hooks, this.settings); + + this.pages.push(page); + + // Create the pages + page.create(undefined, lastPage && lastPage.element); + + page.index(this.total); + + if (!blank) { + // Listen for page overflow + page.onOverflow((overflowToken) => { + console.warn("overflow on", page.id, overflowToken); + + // Only reflow while rendering + if (this.rendered) { + return; + } + + let index = this.pages.indexOf(page) + 1; + + // Stop the rendering + this.stop(); + + // Set the breakToken to resume at + this.breakToken = overflowToken; + + // Remove pages + this.removePages(index); + + if (this.rendered === true) { + this.rendered = false; + + this.q.enqueue(async () => { + + this.start(); + + await this.render(this.source, this.breakToken); + + this.rendered = true; + + }); + } + + + }); + + page.onUnderflow((overflowToken) => { + // console.log("underflow on", page.id, overflowToken); + + // page.append(this.source, overflowToken); + + }); + } + + this.total = this.pages.length; + + return page; + } + /* + insertPage(index, blank) { + let lastPage = this.pages[index]; + // Create a new page from the template + let page = new Page(this.pagesArea, this.pageTemplate, blank, this.hooks); + + let total = this.pages.splice(index, 0, page); + + // Create the pages + page.create(undefined, lastPage && lastPage.element); + + page.index(index + 1); + + for (let i = index + 2; i < this.pages.length; i++) { + this.pages[i].index(i); + } + + if (!blank) { + // Listen for page overflow + page.onOverflow((overflowToken) => { + if (total < this.pages.length) { + this.pages[total].layout(this.source, overflowToken); + } else { + let newPage = this.addPage(); + newPage.layout(this.source, overflowToken); + } + }); + + page.onUnderflow(() => { + // console.log("underflow on", page.id); + }); + } + + this.total += 1; + + return page; + } + */ + + async clonePage(originalPage) { + let lastPage = this.pages[this.pages.length - 1]; + + let page = new Page(this.pagesArea, this.pageTemplate, false, this.hooks); + + this.pages.push(page); + + // Create the pages + page.create(undefined, lastPage && lastPage.element); + + page.index(this.total); + + await this.hooks.beforePageLayout.trigger(page, undefined, undefined, this); + this.emit("page", page); + + for (const className of originalPage.element.classList) { + if (className !== "pagedjs_left_page" && className !== "pagedjs_right_page") { + page.element.classList.add(className); + } + } + + await this.hooks.afterPageLayout.trigger(page.element, page, undefined, this); + await this.hooks.finalizePage.trigger(page.element, page, undefined, this); + this.emit("renderedPage", page); + } + + loadFonts() { + let fontPromises = []; + (document.fonts || []).forEach((fontFace) => { + if (fontFace.status !== "loaded") { + let fontLoaded = fontFace.load().then((r) => { + return fontFace.family; + }, (r) => { + console.warn("Failed to preload font-family:", fontFace.family); + return fontFace.family; + }); + fontPromises.push(fontLoaded); + } + }); + return Promise.all(fontPromises).catch((err) => { + console.warn(err); + }); + } + + destroy() { + this.pagesArea.remove(); + this.pageTemplate.remove(); + } + + } + + EventEmitter(Chunker.prototype); + + var syntax = {exports: {}}; + + var create$4 = {}; + + // + // list + // ┌──────┐ + // ┌──────────────┼─head │ + // │ │ tail─┼──────────────┐ + // │ └──────┘ │ + // ▼ ▼ + // item item item item + // ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ + // null ◀──┼─prev │◀───┼─prev │◀───┼─prev │◀───┼─prev │ + // │ next─┼───▶│ next─┼───▶│ next─┼───▶│ next─┼──▶ null + // ├──────┤ ├──────┤ ├──────┤ ├──────┤ + // │ data │ │ data │ │ data │ │ data │ + // └──────┘ └──────┘ └──────┘ └──────┘ + // + + function createItem(data) { + return { + prev: null, + next: null, + data: data + }; + } + + function allocateCursor(node, prev, next) { + var cursor; + + if (cursors !== null) { + cursor = cursors; + cursors = cursors.cursor; + cursor.prev = prev; + cursor.next = next; + cursor.cursor = node.cursor; + } else { + cursor = { + prev: prev, + next: next, + cursor: node.cursor + }; + } + + node.cursor = cursor; + + return cursor; + } + + function releaseCursor(node) { + var cursor = node.cursor; + + node.cursor = cursor.cursor; + cursor.prev = null; + cursor.next = null; + cursor.cursor = cursors; + cursors = cursor; + } + + var cursors = null; + var List$6 = function() { + this.cursor = null; + this.head = null; + this.tail = null; + }; + + List$6.createItem = createItem; + List$6.prototype.createItem = createItem; + + List$6.prototype.updateCursors = function(prevOld, prevNew, nextOld, nextNew) { + var cursor = this.cursor; + + while (cursor !== null) { + if (cursor.prev === prevOld) { + cursor.prev = prevNew; + } + + if (cursor.next === nextOld) { + cursor.next = nextNew; + } + + cursor = cursor.cursor; + } + }; + + List$6.prototype.getSize = function() { + var size = 0; + var cursor = this.head; + + while (cursor) { + size++; + cursor = cursor.next; + } + + return size; + }; + + List$6.prototype.fromArray = function(array) { + var cursor = null; + + this.head = null; + + for (var i = 0; i < array.length; i++) { + var item = createItem(array[i]); + + if (cursor !== null) { + cursor.next = item; + } else { + this.head = item; + } + + item.prev = cursor; + cursor = item; + } + + this.tail = cursor; + + return this; + }; + + List$6.prototype.toArray = function() { + var cursor = this.head; + var result = []; + + while (cursor) { + result.push(cursor.data); + cursor = cursor.next; + } + + return result; + }; + + List$6.prototype.toJSON = List$6.prototype.toArray; + + List$6.prototype.isEmpty = function() { + return this.head === null; + }; + + List$6.prototype.first = function() { + return this.head && this.head.data; + }; + + List$6.prototype.last = function() { + return this.tail && this.tail.data; + }; + + List$6.prototype.each = function(fn, context) { + var item; + + if (context === undefined) { + context = this; + } + + // push cursor + var cursor = allocateCursor(this, null, this.head); + + while (cursor.next !== null) { + item = cursor.next; + cursor.next = item.next; + + fn.call(context, item.data, item, this); + } + + // pop cursor + releaseCursor(this); + }; + + List$6.prototype.forEach = List$6.prototype.each; + + List$6.prototype.eachRight = function(fn, context) { + var item; + + if (context === undefined) { + context = this; + } + + // push cursor + var cursor = allocateCursor(this, this.tail, null); + + while (cursor.prev !== null) { + item = cursor.prev; + cursor.prev = item.prev; + + fn.call(context, item.data, item, this); + } + + // pop cursor + releaseCursor(this); + }; + + List$6.prototype.forEachRight = List$6.prototype.eachRight; + + List$6.prototype.reduce = function(fn, initialValue, context) { + var item; + + if (context === undefined) { + context = this; + } + + // push cursor + var cursor = allocateCursor(this, null, this.head); + var acc = initialValue; + + while (cursor.next !== null) { + item = cursor.next; + cursor.next = item.next; + + acc = fn.call(context, acc, item.data, item, this); + } + + // pop cursor + releaseCursor(this); + + return acc; + }; + + List$6.prototype.reduceRight = function(fn, initialValue, context) { + var item; + + if (context === undefined) { + context = this; + } + + // push cursor + var cursor = allocateCursor(this, this.tail, null); + var acc = initialValue; + + while (cursor.prev !== null) { + item = cursor.prev; + cursor.prev = item.prev; + + acc = fn.call(context, acc, item.data, item, this); + } + + // pop cursor + releaseCursor(this); + + return acc; + }; + + List$6.prototype.nextUntil = function(start, fn, context) { + if (start === null) { + return; + } + + var item; + + if (context === undefined) { + context = this; + } + + // push cursor + var cursor = allocateCursor(this, null, start); + + while (cursor.next !== null) { + item = cursor.next; + cursor.next = item.next; + + if (fn.call(context, item.data, item, this)) { + break; + } + } + + // pop cursor + releaseCursor(this); + }; + + List$6.prototype.prevUntil = function(start, fn, context) { + if (start === null) { + return; + } + + var item; + + if (context === undefined) { + context = this; + } + + // push cursor + var cursor = allocateCursor(this, start, null); + + while (cursor.prev !== null) { + item = cursor.prev; + cursor.prev = item.prev; + + if (fn.call(context, item.data, item, this)) { + break; + } + } + + // pop cursor + releaseCursor(this); + }; + + List$6.prototype.some = function(fn, context) { + var cursor = this.head; + + if (context === undefined) { + context = this; + } + + while (cursor !== null) { + if (fn.call(context, cursor.data, cursor, this)) { + return true; + } + + cursor = cursor.next; + } + + return false; + }; + + List$6.prototype.map = function(fn, context) { + var result = new List$6(); + var cursor = this.head; + + if (context === undefined) { + context = this; + } + + while (cursor !== null) { + result.appendData(fn.call(context, cursor.data, cursor, this)); + cursor = cursor.next; + } + + return result; + }; + + List$6.prototype.filter = function(fn, context) { + var result = new List$6(); + var cursor = this.head; + + if (context === undefined) { + context = this; + } + + while (cursor !== null) { + if (fn.call(context, cursor.data, cursor, this)) { + result.appendData(cursor.data); + } + cursor = cursor.next; + } + + return result; + }; + + List$6.prototype.clear = function() { + this.head = null; + this.tail = null; + }; + + List$6.prototype.copy = function() { + var result = new List$6(); + var cursor = this.head; + + while (cursor !== null) { + result.insert(createItem(cursor.data)); + cursor = cursor.next; + } + + return result; + }; + + List$6.prototype.prepend = function(item) { + // head + // ^ + // item + this.updateCursors(null, item, this.head, item); + + // insert to the beginning of the list + if (this.head !== null) { + // new item <- first item + this.head.prev = item; + + // new item -> first item + item.next = this.head; + } else { + // if list has no head, then it also has no tail + // in this case tail points to the new item + this.tail = item; + } + + // head always points to new item + this.head = item; + + return this; + }; + + List$6.prototype.prependData = function(data) { + return this.prepend(createItem(data)); + }; + + List$6.prototype.append = function(item) { + return this.insert(item); + }; + + List$6.prototype.appendData = function(data) { + return this.insert(createItem(data)); + }; + + List$6.prototype.insert = function(item, before) { + if (before !== undefined && before !== null) { + // prev before + // ^ + // item + this.updateCursors(before.prev, item, before, item); + + if (before.prev === null) { + // insert to the beginning of list + if (this.head !== before) { + throw new Error('before doesn\'t belong to list'); + } + + // since head points to before therefore list doesn't empty + // no need to check tail + this.head = item; + before.prev = item; + item.next = before; + + this.updateCursors(null, item); + } else { + + // insert between two items + before.prev.next = item; + item.prev = before.prev; + + before.prev = item; + item.next = before; + } + } else { + // tail + // ^ + // item + this.updateCursors(this.tail, item, null, item); + + // insert to the ending of the list + if (this.tail !== null) { + // last item -> new item + this.tail.next = item; + + // last item <- new item + item.prev = this.tail; + } else { + // if list has no tail, then it also has no head + // in this case head points to new item + this.head = item; + } + + // tail always points to new item + this.tail = item; + } + + return this; + }; + + List$6.prototype.insertData = function(data, before) { + return this.insert(createItem(data), before); + }; + + List$6.prototype.remove = function(item) { + // item + // ^ + // prev next + this.updateCursors(item, item.prev, item, item.next); + + if (item.prev !== null) { + item.prev.next = item.next; + } else { + if (this.head !== item) { + throw new Error('item doesn\'t belong to list'); + } + + this.head = item.next; + } + + if (item.next !== null) { + item.next.prev = item.prev; + } else { + if (this.tail !== item) { + throw new Error('item doesn\'t belong to list'); + } + + this.tail = item.prev; + } + + item.prev = null; + item.next = null; + + return item; + }; + + List$6.prototype.push = function(data) { + this.insert(createItem(data)); + }; + + List$6.prototype.pop = function() { + if (this.tail !== null) { + return this.remove(this.tail); + } + }; + + List$6.prototype.unshift = function(data) { + this.prepend(createItem(data)); + }; + + List$6.prototype.shift = function() { + if (this.head !== null) { + return this.remove(this.head); + } + }; + + List$6.prototype.prependList = function(list) { + return this.insertList(list, this.head); + }; + + List$6.prototype.appendList = function(list) { + return this.insertList(list); + }; + + List$6.prototype.insertList = function(list, before) { + // ignore empty lists + if (list.head === null) { + return this; + } + + if (before !== undefined && before !== null) { + this.updateCursors(before.prev, list.tail, before, list.head); + + // insert in the middle of dist list + if (before.prev !== null) { + // before.prev <-> list.head + before.prev.next = list.head; + list.head.prev = before.prev; + } else { + this.head = list.head; + } + + before.prev = list.tail; + list.tail.next = before; + } else { + this.updateCursors(this.tail, list.tail, null, list.head); + + // insert to end of the list + if (this.tail !== null) { + // if destination list has a tail, then it also has a head, + // but head doesn't change + + // dest tail -> source head + this.tail.next = list.head; + + // dest tail <- source head + list.head.prev = this.tail; + } else { + // if list has no a tail, then it also has no a head + // in this case points head to new item + this.head = list.head; + } + + // tail always start point to new item + this.tail = list.tail; + } + + list.head = null; + list.tail = null; + + return this; + }; + + List$6.prototype.replace = function(oldItem, newItemOrList) { + if ('head' in newItemOrList) { + this.insertList(newItemOrList, oldItem); + } else { + this.insert(newItemOrList, oldItem); + } + + this.remove(oldItem); + }; + + var List_1 = List$6; + + var createCustomError$3 = function createCustomError(name, message) { + // use Object.create(), because some VMs prevent setting line/column otherwise + // (iOS Safari 10 even throws an exception) + var error = Object.create(SyntaxError.prototype); + var errorStack = new Error(); + + error.name = name; + error.message = message; + + Object.defineProperty(error, 'stack', { + get: function() { + return (errorStack.stack || '').replace(/^(.+\n){1,3}/, name + ': ' + message + '\n'); + } + }); + + return error; + }; + + var createCustomError$2 = createCustomError$3; + var MAX_LINE_LENGTH = 100; + var OFFSET_CORRECTION = 60; + var TAB_REPLACEMENT = ' '; + + function sourceFragment(error, extraLines) { + function processLines(start, end) { + return lines.slice(start, end).map(function(line, idx) { + var num = String(start + idx + 1); + + while (num.length < maxNumLength) { + num = ' ' + num; + } + + return num + ' |' + line; + }).join('\n'); + } + + var lines = error.source.split(/\r\n?|\n|\f/); + var line = error.line; + var column = error.column; + var startLine = Math.max(1, line - extraLines) - 1; + var endLine = Math.min(line + extraLines, lines.length + 1); + var maxNumLength = Math.max(4, String(endLine).length) + 1; + var cutLeft = 0; + + // column correction according to replaced tab before column + column += (TAB_REPLACEMENT.length - 1) * (lines[line - 1].substr(0, column - 1).match(/\t/g) || []).length; + + if (column > MAX_LINE_LENGTH) { + cutLeft = column - OFFSET_CORRECTION + 3; + column = OFFSET_CORRECTION - 2; + } + + for (var i = startLine; i <= endLine; i++) { + if (i >= 0 && i < lines.length) { + lines[i] = lines[i].replace(/\t/g, TAB_REPLACEMENT); + lines[i] = + (cutLeft > 0 && lines[i].length > cutLeft ? '\u2026' : '') + + lines[i].substr(cutLeft, MAX_LINE_LENGTH - 2) + + (lines[i].length > cutLeft + MAX_LINE_LENGTH - 1 ? '\u2026' : ''); + } + } + + return [ + processLines(startLine, line), + new Array(column + maxNumLength + 2).join('-') + '^', + processLines(line, endLine) + ].filter(Boolean).join('\n'); + } + + var SyntaxError$4 = function(message, source, offset, line, column) { + var error = createCustomError$2('SyntaxError', message); + + error.source = source; + error.offset = offset; + error.line = line; + error.column = column; + + error.sourceFragment = function(extraLines) { + return sourceFragment(error, isNaN(extraLines) ? 0 : extraLines); + }; + Object.defineProperty(error, 'formattedMessage', { + get: function() { + return ( + 'Parse error: ' + error.message + '\n' + + sourceFragment(error, 2) + ); + } + }); + + // for backward capability + error.parseError = { + offset: offset, + line: line, + column: column + }; + + return error; + }; + + var _SyntaxError$1 = SyntaxError$4; + + // CSS Syntax Module Level 3 + // https://www.w3.org/TR/css-syntax-3/ + var TYPE$H = { + EOF: 0, // + Ident: 1, // + Function: 2, // + AtKeyword: 3, // + Hash: 4, // + String: 5, // + BadString: 6, // + Url: 7, // + BadUrl: 8, // + Delim: 9, // + Number: 10, // + Percentage: 11, // + Dimension: 12, // + WhiteSpace: 13, // + CDO: 14, // + CDC: 15, // + Colon: 16, // : + Semicolon: 17, // ; + Comma: 18, // , + LeftSquareBracket: 19, // <[-token> + RightSquareBracket: 20, // <]-token> + LeftParenthesis: 21, // <(-token> + RightParenthesis: 22, // <)-token> + LeftCurlyBracket: 23, // <{-token> + RightCurlyBracket: 24, // <}-token> + Comment: 25 + }; + + var NAME$3 = Object.keys(TYPE$H).reduce(function(result, key) { + result[TYPE$H[key]] = key; + return result; + }, {}); + + var _const = { + TYPE: TYPE$H, + NAME: NAME$3 + }; + + var EOF$1 = 0; + + // https://drafts.csswg.org/css-syntax-3/ + // § 4.2. Definitions + + // digit + // A code point between U+0030 DIGIT ZERO (0) and U+0039 DIGIT NINE (9). + function isDigit$5(code) { + return code >= 0x0030 && code <= 0x0039; + } + + // hex digit + // A digit, or a code point between U+0041 LATIN CAPITAL LETTER A (A) and U+0046 LATIN CAPITAL LETTER F (F), + // or a code point between U+0061 LATIN SMALL LETTER A (a) and U+0066 LATIN SMALL LETTER F (f). + function isHexDigit$4(code) { + return ( + isDigit$5(code) || // 0 .. 9 + (code >= 0x0041 && code <= 0x0046) || // A .. F + (code >= 0x0061 && code <= 0x0066) // a .. f + ); + } + + // uppercase letter + // A code point between U+0041 LATIN CAPITAL LETTER A (A) and U+005A LATIN CAPITAL LETTER Z (Z). + function isUppercaseLetter$1(code) { + return code >= 0x0041 && code <= 0x005A; + } + + // lowercase letter + // A code point between U+0061 LATIN SMALL LETTER A (a) and U+007A LATIN SMALL LETTER Z (z). + function isLowercaseLetter(code) { + return code >= 0x0061 && code <= 0x007A; + } + + // letter + // An uppercase letter or a lowercase letter. + function isLetter(code) { + return isUppercaseLetter$1(code) || isLowercaseLetter(code); + } + + // non-ASCII code point + // A code point with a value equal to or greater than U+0080 . + function isNonAscii(code) { + return code >= 0x0080; + } + + // name-start code point + // A letter, a non-ASCII code point, or U+005F LOW LINE (_). + function isNameStart(code) { + return isLetter(code) || isNonAscii(code) || code === 0x005F; + } + + // name code point + // A name-start code point, a digit, or U+002D HYPHEN-MINUS (-). + function isName$2(code) { + return isNameStart(code) || isDigit$5(code) || code === 0x002D; + } + + // non-printable code point + // A code point between U+0000 NULL and U+0008 BACKSPACE, or U+000B LINE TABULATION, + // or a code point between U+000E SHIFT OUT and U+001F INFORMATION SEPARATOR ONE, or U+007F DELETE. + function isNonPrintable(code) { + return ( + (code >= 0x0000 && code <= 0x0008) || + (code === 0x000B) || + (code >= 0x000E && code <= 0x001F) || + (code === 0x007F) + ); + } + + // newline + // U+000A LINE FEED. Note that U+000D CARRIAGE RETURN and U+000C FORM FEED are not included in this definition, + // as they are converted to U+000A LINE FEED during preprocessing. + // TODO: we doesn't do a preprocessing, so check a code point for U+000D CARRIAGE RETURN and U+000C FORM FEED + function isNewline$1(code) { + return code === 0x000A || code === 0x000D || code === 0x000C; + } + + // whitespace + // A newline, U+0009 CHARACTER TABULATION, or U+0020 SPACE. + function isWhiteSpace$2(code) { + return isNewline$1(code) || code === 0x0020 || code === 0x0009; + } + + // § 4.3.8. Check if two code points are a valid escape + function isValidEscape$2(first, second) { + // If the first code point is not U+005C REVERSE SOLIDUS (\), return false. + if (first !== 0x005C) { + return false; + } + + // Otherwise, if the second code point is a newline or EOF, return false. + if (isNewline$1(second) || second === EOF$1) { + return false; + } + + // Otherwise, return true. + return true; + } + + // § 4.3.9. Check if three code points would start an identifier + function isIdentifierStart$2(first, second, third) { + // Look at the first code point: + + // U+002D HYPHEN-MINUS + if (first === 0x002D) { + // If the second code point is a name-start code point or a U+002D HYPHEN-MINUS, + // or the second and third code points are a valid escape, return true. Otherwise, return false. + return ( + isNameStart(second) || + second === 0x002D || + isValidEscape$2(second, third) + ); + } + + // name-start code point + if (isNameStart(first)) { + // Return true. + return true; + } + + // U+005C REVERSE SOLIDUS (\) + if (first === 0x005C) { + // If the first and second code points are a valid escape, return true. Otherwise, return false. + return isValidEscape$2(first, second); + } + + // anything else + // Return false. + return false; + } + + // § 4.3.10. Check if three code points would start a number + function isNumberStart$1(first, second, third) { + // Look at the first code point: + + // U+002B PLUS SIGN (+) + // U+002D HYPHEN-MINUS (-) + if (first === 0x002B || first === 0x002D) { + // If the second code point is a digit, return true. + if (isDigit$5(second)) { + return 2; + } + + // Otherwise, if the second code point is a U+002E FULL STOP (.) + // and the third code point is a digit, return true. + // Otherwise, return false. + return second === 0x002E && isDigit$5(third) ? 3 : 0; + } + + // U+002E FULL STOP (.) + if (first === 0x002E) { + // If the second code point is a digit, return true. Otherwise, return false. + return isDigit$5(second) ? 2 : 0; + } + + // digit + if (isDigit$5(first)) { + // Return true. + return 1; + } + + // anything else + // Return false. + return 0; + } + + // + // Misc + // + + // detect BOM (https://en.wikipedia.org/wiki/Byte_order_mark) + function isBOM$2(code) { + // UTF-16BE + if (code === 0xFEFF) { + return 1; + } + + // UTF-16LE + if (code === 0xFFFE) { + return 1; + } + + return 0; + } + + // Fast code category + // + // https://drafts.csswg.org/css-syntax/#tokenizer-definitions + // > non-ASCII code point + // > A code point with a value equal to or greater than U+0080 + // > name-start code point + // > A letter, a non-ASCII code point, or U+005F LOW LINE (_). + // > name code point + // > A name-start code point, a digit, or U+002D HYPHEN-MINUS (-) + // That means only ASCII code points has a special meaning and we define a maps for 0..127 codes only + var CATEGORY = new Array(0x80); + charCodeCategory$1.Eof = 0x80; + charCodeCategory$1.WhiteSpace = 0x82; + charCodeCategory$1.Digit = 0x83; + charCodeCategory$1.NameStart = 0x84; + charCodeCategory$1.NonPrintable = 0x85; + + for (var i = 0; i < CATEGORY.length; i++) { + switch (true) { + case isWhiteSpace$2(i): + CATEGORY[i] = charCodeCategory$1.WhiteSpace; + break; + + case isDigit$5(i): + CATEGORY[i] = charCodeCategory$1.Digit; + break; + + case isNameStart(i): + CATEGORY[i] = charCodeCategory$1.NameStart; + break; + + case isNonPrintable(i): + CATEGORY[i] = charCodeCategory$1.NonPrintable; + break; + + default: + CATEGORY[i] = i || charCodeCategory$1.Eof; + } + } + + function charCodeCategory$1(code) { + return code < 0x80 ? CATEGORY[code] : charCodeCategory$1.NameStart; + } + var charCodeDefinitions$1 = { + isDigit: isDigit$5, + isHexDigit: isHexDigit$4, + isUppercaseLetter: isUppercaseLetter$1, + isLowercaseLetter: isLowercaseLetter, + isLetter: isLetter, + isNonAscii: isNonAscii, + isNameStart: isNameStart, + isName: isName$2, + isNonPrintable: isNonPrintable, + isNewline: isNewline$1, + isWhiteSpace: isWhiteSpace$2, + isValidEscape: isValidEscape$2, + isIdentifierStart: isIdentifierStart$2, + isNumberStart: isNumberStart$1, + + isBOM: isBOM$2, + charCodeCategory: charCodeCategory$1 + }; + + var charCodeDef = charCodeDefinitions$1; + var isDigit$4 = charCodeDef.isDigit; + var isHexDigit$3 = charCodeDef.isHexDigit; + var isUppercaseLetter = charCodeDef.isUppercaseLetter; + var isName$1 = charCodeDef.isName; + var isWhiteSpace$1 = charCodeDef.isWhiteSpace; + var isValidEscape$1 = charCodeDef.isValidEscape; + + function getCharCode(source, offset) { + return offset < source.length ? source.charCodeAt(offset) : 0; + } + + function getNewlineLength$1(source, offset, code) { + if (code === 13 /* \r */ && getCharCode(source, offset + 1) === 10 /* \n */) { + return 2; + } + + return 1; + } + + function cmpChar$5(testStr, offset, referenceCode) { + var code = testStr.charCodeAt(offset); + + // code.toLowerCase() for A..Z + if (isUppercaseLetter(code)) { + code = code | 32; + } + + return code === referenceCode; + } + + function cmpStr$6(testStr, start, end, referenceStr) { + if (end - start !== referenceStr.length) { + return false; + } + + if (start < 0 || end > testStr.length) { + return false; + } + + for (var i = start; i < end; i++) { + var testCode = testStr.charCodeAt(i); + var referenceCode = referenceStr.charCodeAt(i - start); + + // testCode.toLowerCase() for A..Z + if (isUppercaseLetter(testCode)) { + testCode = testCode | 32; + } + + if (testCode !== referenceCode) { + return false; + } + } + + return true; + } + + function findWhiteSpaceStart$1(source, offset) { + for (; offset >= 0; offset--) { + if (!isWhiteSpace$1(source.charCodeAt(offset))) { + break; + } + } + + return offset + 1; + } + + function findWhiteSpaceEnd$1(source, offset) { + for (; offset < source.length; offset++) { + if (!isWhiteSpace$1(source.charCodeAt(offset))) { + break; + } + } + + return offset; + } + + function findDecimalNumberEnd(source, offset) { + for (; offset < source.length; offset++) { + if (!isDigit$4(source.charCodeAt(offset))) { + break; + } + } + + return offset; + } + + // § 4.3.7. Consume an escaped code point + function consumeEscaped$1(source, offset) { + // It assumes that the U+005C REVERSE SOLIDUS (\) has already been consumed and + // that the next input code point has already been verified to be part of a valid escape. + offset += 2; + + // hex digit + if (isHexDigit$3(getCharCode(source, offset - 1))) { + // Consume as many hex digits as possible, but no more than 5. + // Note that this means 1-6 hex digits have been consumed in total. + for (var maxOffset = Math.min(source.length, offset + 5); offset < maxOffset; offset++) { + if (!isHexDigit$3(getCharCode(source, offset))) { + break; + } + } + + // If the next input code point is whitespace, consume it as well. + var code = getCharCode(source, offset); + if (isWhiteSpace$1(code)) { + offset += getNewlineLength$1(source, offset, code); + } + } + + return offset; + } + + // §4.3.11. Consume a name + // Note: This algorithm does not do the verification of the first few code points that are necessary + // to ensure the returned code points would constitute an . If that is the intended use, + // ensure that the stream starts with an identifier before calling this algorithm. + function consumeName$1(source, offset) { + // Let result initially be an empty string. + // Repeatedly consume the next input code point from the stream: + for (; offset < source.length; offset++) { + var code = source.charCodeAt(offset); + + // name code point + if (isName$1(code)) { + // Append the code point to result. + continue; + } + + // the stream starts with a valid escape + if (isValidEscape$1(code, getCharCode(source, offset + 1))) { + // Consume an escaped code point. Append the returned code point to result. + offset = consumeEscaped$1(source, offset) - 1; + continue; + } + + // anything else + // Reconsume the current input code point. Return result. + break; + } + + return offset; + } + + // §4.3.12. Consume a number + function consumeNumber$5(source, offset) { + var code = source.charCodeAt(offset); + + // 2. If the next input code point is U+002B PLUS SIGN (+) or U+002D HYPHEN-MINUS (-), + // consume it and append it to repr. + if (code === 0x002B || code === 0x002D) { + code = source.charCodeAt(offset += 1); + } + + // 3. While the next input code point is a digit, consume it and append it to repr. + if (isDigit$4(code)) { + offset = findDecimalNumberEnd(source, offset + 1); + code = source.charCodeAt(offset); + } + + // 4. If the next 2 input code points are U+002E FULL STOP (.) followed by a digit, then: + if (code === 0x002E && isDigit$4(source.charCodeAt(offset + 1))) { + // 4.1 Consume them. + // 4.2 Append them to repr. + code = source.charCodeAt(offset += 2); + + // 4.3 Set type to "number". + // TODO + + // 4.4 While the next input code point is a digit, consume it and append it to repr. + + offset = findDecimalNumberEnd(source, offset); + } + + // 5. If the next 2 or 3 input code points are U+0045 LATIN CAPITAL LETTER E (E) + // or U+0065 LATIN SMALL LETTER E (e), ... , followed by a digit, then: + if (cmpChar$5(source, offset, 101 /* e */)) { + var sign = 0; + code = source.charCodeAt(offset + 1); + + // ... optionally followed by U+002D HYPHEN-MINUS (-) or U+002B PLUS SIGN (+) ... + if (code === 0x002D || code === 0x002B) { + sign = 1; + code = source.charCodeAt(offset + 2); + } + + // ... followed by a digit + if (isDigit$4(code)) { + // 5.1 Consume them. + // 5.2 Append them to repr. + + // 5.3 Set type to "number". + // TODO + + // 5.4 While the next input code point is a digit, consume it and append it to repr. + offset = findDecimalNumberEnd(source, offset + 1 + sign + 1); + } + } + + return offset; + } + + // § 4.3.14. Consume the remnants of a bad url + // ... its sole use is to consume enough of the input stream to reach a recovery point + // where normal tokenizing can resume. + function consumeBadUrlRemnants$1(source, offset) { + // Repeatedly consume the next input code point from the stream: + for (; offset < source.length; offset++) { + var code = source.charCodeAt(offset); + + // U+0029 RIGHT PARENTHESIS ()) + // EOF + if (code === 0x0029) { + // Return. + offset++; + break; + } + + if (isValidEscape$1(code, getCharCode(source, offset + 1))) { + // Consume an escaped code point. + // Note: This allows an escaped right parenthesis ("\)") to be encountered + // without ending the . This is otherwise identical to + // the "anything else" clause. + offset = consumeEscaped$1(source, offset); + } + } + + return offset; + } + + var utils$2 = { + consumeEscaped: consumeEscaped$1, + consumeName: consumeName$1, + consumeNumber: consumeNumber$5, + consumeBadUrlRemnants: consumeBadUrlRemnants$1, + + cmpChar: cmpChar$5, + cmpStr: cmpStr$6, + + getNewlineLength: getNewlineLength$1, + findWhiteSpaceStart: findWhiteSpaceStart$1, + findWhiteSpaceEnd: findWhiteSpaceEnd$1 + }; + + var constants$2 = _const; + var TYPE$G = constants$2.TYPE; + var NAME$2 = constants$2.NAME; + + var utils$1 = utils$2; + var cmpStr$5 = utils$1.cmpStr; + + var EOF = TYPE$G.EOF; + var WHITESPACE$c = TYPE$G.WhiteSpace; + var COMMENT$a = TYPE$G.Comment; + + var OFFSET_MASK$1 = 0x00FFFFFF; + var TYPE_SHIFT$1 = 24; + + var TokenStream$4 = function() { + this.offsetAndType = null; + this.balance = null; + + this.reset(); + }; + + TokenStream$4.prototype = { + reset: function() { + this.eof = false; + this.tokenIndex = -1; + this.tokenType = 0; + this.tokenStart = this.firstCharOffset; + this.tokenEnd = this.firstCharOffset; + }, + + lookupType: function(offset) { + offset += this.tokenIndex; + + if (offset < this.tokenCount) { + return this.offsetAndType[offset] >> TYPE_SHIFT$1; + } + + return EOF; + }, + lookupOffset: function(offset) { + offset += this.tokenIndex; + + if (offset < this.tokenCount) { + return this.offsetAndType[offset - 1] & OFFSET_MASK$1; + } + + return this.source.length; + }, + lookupValue: function(offset, referenceStr) { + offset += this.tokenIndex; + + if (offset < this.tokenCount) { + return cmpStr$5( + this.source, + this.offsetAndType[offset - 1] & OFFSET_MASK$1, + this.offsetAndType[offset] & OFFSET_MASK$1, + referenceStr + ); + } + + return false; + }, + getTokenStart: function(tokenIndex) { + if (tokenIndex === this.tokenIndex) { + return this.tokenStart; + } + + if (tokenIndex > 0) { + return tokenIndex < this.tokenCount + ? this.offsetAndType[tokenIndex - 1] & OFFSET_MASK$1 + : this.offsetAndType[this.tokenCount] & OFFSET_MASK$1; + } + + return this.firstCharOffset; + }, + + // TODO: -> skipUntilBalanced + getRawLength: function(startToken, mode) { + var cursor = startToken; + var balanceEnd; + var offset = this.offsetAndType[Math.max(cursor - 1, 0)] & OFFSET_MASK$1; + var type; + + loop: + for (; cursor < this.tokenCount; cursor++) { + balanceEnd = this.balance[cursor]; + + // stop scanning on balance edge that points to offset before start token + if (balanceEnd < startToken) { + break loop; + } + + type = this.offsetAndType[cursor] >> TYPE_SHIFT$1; + + // check token is stop type + switch (mode(type, this.source, offset)) { + case 1: + break loop; + + case 2: + cursor++; + break loop; + + default: + // fast forward to the end of balanced block + if (this.balance[balanceEnd] === cursor) { + cursor = balanceEnd; + } + + offset = this.offsetAndType[cursor] & OFFSET_MASK$1; + } + } + + return cursor - this.tokenIndex; + }, + isBalanceEdge: function(pos) { + return this.balance[this.tokenIndex] < pos; + }, + isDelim: function(code, offset) { + if (offset) { + return ( + this.lookupType(offset) === TYPE$G.Delim && + this.source.charCodeAt(this.lookupOffset(offset)) === code + ); + } + + return ( + this.tokenType === TYPE$G.Delim && + this.source.charCodeAt(this.tokenStart) === code + ); + }, + + getTokenValue: function() { + return this.source.substring(this.tokenStart, this.tokenEnd); + }, + getTokenLength: function() { + return this.tokenEnd - this.tokenStart; + }, + substrToCursor: function(start) { + return this.source.substring(start, this.tokenStart); + }, + + skipWS: function() { + for (var i = this.tokenIndex, skipTokenCount = 0; i < this.tokenCount; i++, skipTokenCount++) { + if ((this.offsetAndType[i] >> TYPE_SHIFT$1) !== WHITESPACE$c) { + break; + } + } + + if (skipTokenCount > 0) { + this.skip(skipTokenCount); + } + }, + skipSC: function() { + while (this.tokenType === WHITESPACE$c || this.tokenType === COMMENT$a) { + this.next(); + } + }, + skip: function(tokenCount) { + var next = this.tokenIndex + tokenCount; + + if (next < this.tokenCount) { + this.tokenIndex = next; + this.tokenStart = this.offsetAndType[next - 1] & OFFSET_MASK$1; + next = this.offsetAndType[next]; + this.tokenType = next >> TYPE_SHIFT$1; + this.tokenEnd = next & OFFSET_MASK$1; + } else { + this.tokenIndex = this.tokenCount; + this.next(); + } + }, + next: function() { + var next = this.tokenIndex + 1; + + if (next < this.tokenCount) { + this.tokenIndex = next; + this.tokenStart = this.tokenEnd; + next = this.offsetAndType[next]; + this.tokenType = next >> TYPE_SHIFT$1; + this.tokenEnd = next & OFFSET_MASK$1; + } else { + this.tokenIndex = this.tokenCount; + this.eof = true; + this.tokenType = EOF; + this.tokenStart = this.tokenEnd = this.source.length; + } + }, + + forEachToken(fn) { + for (var i = 0, offset = this.firstCharOffset; i < this.tokenCount; i++) { + var start = offset; + var item = this.offsetAndType[i]; + var end = item & OFFSET_MASK$1; + var type = item >> TYPE_SHIFT$1; + + offset = end; + + fn(type, start, end, i); + } + }, + + dump() { + var tokens = new Array(this.tokenCount); + + this.forEachToken((type, start, end, index) => { + tokens[index] = { + idx: index, + type: NAME$2[type], + chunk: this.source.substring(start, end), + balance: this.balance[index] + }; + }); + + return tokens; + } + }; + + var TokenStream_1 = TokenStream$4; + + function noop$3(value) { + return value; + } + + function generateMultiplier(multiplier) { + if (multiplier.min === 0 && multiplier.max === 0) { + return '*'; + } + + if (multiplier.min === 0 && multiplier.max === 1) { + return '?'; + } + + if (multiplier.min === 1 && multiplier.max === 0) { + return multiplier.comma ? '#' : '+'; + } + + if (multiplier.min === 1 && multiplier.max === 1) { + return ''; + } + + return ( + (multiplier.comma ? '#' : '') + + (multiplier.min === multiplier.max + ? '{' + multiplier.min + '}' + : '{' + multiplier.min + ',' + (multiplier.max !== 0 ? multiplier.max : '') + '}' + ) + ); + } + + function generateTypeOpts(node) { + switch (node.type) { + case 'Range': + return ( + ' [' + + (node.min === null ? '-∞' : node.min) + + ',' + + (node.max === null ? '∞' : node.max) + + ']' + ); + + default: + throw new Error('Unknown node type `' + node.type + '`'); + } + } + + function generateSequence(node, decorate, forceBraces, compact) { + var combinator = node.combinator === ' ' || compact ? node.combinator : ' ' + node.combinator + ' '; + var result = node.terms.map(function(term) { + return generate$2(term, decorate, forceBraces, compact); + }).join(combinator); + + if (node.explicit || forceBraces) { + result = (compact || result[0] === ',' ? '[' : '[ ') + result + (compact ? ']' : ' ]'); + } + + return result; + } + + function generate$2(node, decorate, forceBraces, compact) { + var result; + + switch (node.type) { + case 'Group': + result = + generateSequence(node, decorate, forceBraces, compact) + + (node.disallowEmpty ? '!' : ''); + break; + + case 'Multiplier': + // return since node is a composition + return ( + generate$2(node.term, decorate, forceBraces, compact) + + decorate(generateMultiplier(node), node) + ); + + case 'Type': + result = '<' + node.name + (node.opts ? decorate(generateTypeOpts(node.opts), node.opts) : '') + '>'; + break; + + case 'Property': + result = '<\'' + node.name + '\'>'; + break; + + case 'Keyword': + result = node.name; + break; + + case 'AtKeyword': + result = '@' + node.name; + break; + + case 'Function': + result = node.name + '('; + break; + + case 'String': + case 'Token': + result = node.value; + break; + + case 'Comma': + result = ','; + break; + + default: + throw new Error('Unknown node type `' + node.type + '`'); + } + + return decorate(result, node); + } + + var generate_1 = function(node, options) { + var decorate = noop$3; + var forceBraces = false; + var compact = false; + + if (typeof options === 'function') { + decorate = options; + } else if (options) { + forceBraces = Boolean(options.forceBraces); + compact = Boolean(options.compact); + if (typeof options.decorate === 'function') { + decorate = options.decorate; + } + } + + return generate$2(node, decorate, forceBraces, compact); + }; + + const createCustomError$1 = createCustomError$3; + const generate$1 = generate_1; + const defaultLoc = { offset: 0, line: 1, column: 1 }; + + function locateMismatch(matchResult, node) { + const tokens = matchResult.tokens; + const longestMatch = matchResult.longestMatch; + const mismatchNode = longestMatch < tokens.length ? tokens[longestMatch].node || null : null; + const badNode = mismatchNode !== node ? mismatchNode : null; + let mismatchOffset = 0; + let mismatchLength = 0; + let entries = 0; + let css = ''; + let start; + let end; + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i].value; + + if (i === longestMatch) { + mismatchLength = token.length; + mismatchOffset = css.length; + } + + if (badNode !== null && tokens[i].node === badNode) { + if (i <= longestMatch) { + entries++; + } else { + entries = 0; + } + } + + css += token; + } + + if (longestMatch === tokens.length || entries > 1) { // last + start = fromLoc(badNode || node, 'end') || buildLoc(defaultLoc, css); + end = buildLoc(start); + } else { + start = fromLoc(badNode, 'start') || + buildLoc(fromLoc(node, 'start') || defaultLoc, css.slice(0, mismatchOffset)); + end = fromLoc(badNode, 'end') || + buildLoc(start, css.substr(mismatchOffset, mismatchLength)); + } + + return { + css, + mismatchOffset, + mismatchLength, + start, + end + }; + } + + function fromLoc(node, point) { + const value = node && node.loc && node.loc[point]; + + if (value) { + return 'line' in value ? buildLoc(value) : value; + } + + return null; + } + + function buildLoc({ offset, line, column }, extra) { + const loc = { + offset, + line, + column + }; + + if (extra) { + const lines = extra.split(/\n|\r\n?|\f/); + + loc.offset += extra.length; + loc.line += lines.length - 1; + loc.column = lines.length === 1 ? loc.column + extra.length : lines.pop().length + 1; + } + + return loc; + } + + const SyntaxReferenceError$1 = function(type, referenceName) { + const error = createCustomError$1( + 'SyntaxReferenceError', + type + (referenceName ? ' `' + referenceName + '`' : '') + ); + + error.reference = referenceName; + + return error; + }; + + const SyntaxMatchError$1 = function(message, syntax, node, matchResult) { + const error = createCustomError$1('SyntaxMatchError', message); + const { + css, + mismatchOffset, + mismatchLength, + start, + end + } = locateMismatch(matchResult, node); + + error.rawMessage = message; + error.syntax = syntax ? generate$1(syntax) : ''; + error.css = css; + error.mismatchOffset = mismatchOffset; + error.mismatchLength = mismatchLength; + error.message = message + '\n' + + ' syntax: ' + error.syntax + '\n' + + ' value: ' + (css || '') + '\n' + + ' --------' + new Array(error.mismatchOffset + 1).join('-') + '^'; + + Object.assign(error, start); + error.loc = { + source: (node && node.loc && node.loc.source) || '', + start, + end + }; + + return error; + }; + + var error = { + SyntaxReferenceError: SyntaxReferenceError$1, + SyntaxMatchError: SyntaxMatchError$1 + }; + + var hasOwnProperty$7 = Object.prototype.hasOwnProperty; + var keywords$1 = Object.create(null); + var properties$1 = Object.create(null); + var HYPHENMINUS$5 = 45; // '-'.charCodeAt() + + function isCustomProperty$1(str, offset) { + offset = offset || 0; + + return str.length - offset >= 2 && + str.charCodeAt(offset) === HYPHENMINUS$5 && + str.charCodeAt(offset + 1) === HYPHENMINUS$5; + } + + function getVendorPrefix(str, offset) { + offset = offset || 0; + + // verdor prefix should be at least 3 chars length + if (str.length - offset >= 3) { + // vendor prefix starts with hyper minus following non-hyper minus + if (str.charCodeAt(offset) === HYPHENMINUS$5 && + str.charCodeAt(offset + 1) !== HYPHENMINUS$5) { + // vendor prefix should contain a hyper minus at the ending + var secondDashIndex = str.indexOf('-', offset + 2); + + if (secondDashIndex !== -1) { + return str.substring(offset, secondDashIndex + 1); + } + } + } + + return ''; + } + + function getKeywordDescriptor(keyword) { + if (hasOwnProperty$7.call(keywords$1, keyword)) { + return keywords$1[keyword]; + } + + var name = keyword.toLowerCase(); + + if (hasOwnProperty$7.call(keywords$1, name)) { + return keywords$1[keyword] = keywords$1[name]; + } + + var custom = isCustomProperty$1(name, 0); + var vendor = !custom ? getVendorPrefix(name, 0) : ''; + + return keywords$1[keyword] = Object.freeze({ + basename: name.substr(vendor.length), + name: name, + vendor: vendor, + prefix: vendor, + custom: custom + }); + } + + function getPropertyDescriptor(property) { + if (hasOwnProperty$7.call(properties$1, property)) { + return properties$1[property]; + } + + var name = property; + var hack = property[0]; + + if (hack === '/') { + hack = property[1] === '/' ? '//' : '/'; + } else if (hack !== '_' && + hack !== '*' && + hack !== '$' && + hack !== '#' && + hack !== '+' && + hack !== '&') { + hack = ''; + } + + var custom = isCustomProperty$1(name, hack.length); + + // re-use result when possible (the same as for lower case) + if (!custom) { + name = name.toLowerCase(); + if (hasOwnProperty$7.call(properties$1, name)) { + return properties$1[property] = properties$1[name]; + } + } + + var vendor = !custom ? getVendorPrefix(name, hack.length) : ''; + var prefix = name.substr(0, hack.length + vendor.length); + + return properties$1[property] = Object.freeze({ + basename: name.substr(prefix.length), + name: name.substr(hack.length), + hack: hack, + vendor: vendor, + prefix: prefix, + custom: custom + }); + } + + var names$2 = { + keyword: getKeywordDescriptor, + property: getPropertyDescriptor, + isCustomProperty: isCustomProperty$1, + vendorPrefix: getVendorPrefix + }; + + var MIN_SIZE = 16 * 1024; + var SafeUint32Array = typeof Uint32Array !== 'undefined' ? Uint32Array : Array; // fallback on Array when TypedArray is not supported + + var adoptBuffer$2 = function adoptBuffer(buffer, size) { + if (buffer === null || buffer.length < size) { + return new SafeUint32Array(Math.max(size + 1024, MIN_SIZE)); + } + + return buffer; + }; + + var TokenStream$3 = TokenStream_1; + var adoptBuffer$1 = adoptBuffer$2; + + var constants$1 = _const; + var TYPE$F = constants$1.TYPE; + + var charCodeDefinitions = charCodeDefinitions$1; + var isNewline = charCodeDefinitions.isNewline; + var isName = charCodeDefinitions.isName; + var isValidEscape = charCodeDefinitions.isValidEscape; + var isNumberStart = charCodeDefinitions.isNumberStart; + var isIdentifierStart$1 = charCodeDefinitions.isIdentifierStart; + var charCodeCategory = charCodeDefinitions.charCodeCategory; + var isBOM$1 = charCodeDefinitions.isBOM; + + var utils = utils$2; + var cmpStr$4 = utils.cmpStr; + var getNewlineLength = utils.getNewlineLength; + var findWhiteSpaceEnd = utils.findWhiteSpaceEnd; + var consumeEscaped = utils.consumeEscaped; + var consumeName = utils.consumeName; + var consumeNumber$4 = utils.consumeNumber; + var consumeBadUrlRemnants = utils.consumeBadUrlRemnants; + + var OFFSET_MASK = 0x00FFFFFF; + var TYPE_SHIFT = 24; + + function tokenize$3(source, stream) { + function getCharCode(offset) { + return offset < sourceLength ? source.charCodeAt(offset) : 0; + } + + // § 4.3.3. Consume a numeric token + function consumeNumericToken() { + // Consume a number and let number be the result. + offset = consumeNumber$4(source, offset); + + // If the next 3 input code points would start an identifier, then: + if (isIdentifierStart$1(getCharCode(offset), getCharCode(offset + 1), getCharCode(offset + 2))) { + // Create a with the same value and type flag as number, and a unit set initially to the empty string. + // Consume a name. Set the ’s unit to the returned value. + // Return the . + type = TYPE$F.Dimension; + offset = consumeName(source, offset); + return; + } + + // Otherwise, if the next input code point is U+0025 PERCENTAGE SIGN (%), consume it. + if (getCharCode(offset) === 0x0025) { + // Create a with the same value as number, and return it. + type = TYPE$F.Percentage; + offset++; + return; + } + + // Otherwise, create a with the same value and type flag as number, and return it. + type = TYPE$F.Number; + } + + // § 4.3.4. Consume an ident-like token + function consumeIdentLikeToken() { + const nameStartOffset = offset; + + // Consume a name, and let string be the result. + offset = consumeName(source, offset); + + // If string’s value is an ASCII case-insensitive match for "url", + // and the next input code point is U+0028 LEFT PARENTHESIS ((), consume it. + if (cmpStr$4(source, nameStartOffset, offset, 'url') && getCharCode(offset) === 0x0028) { + // While the next two input code points are whitespace, consume the next input code point. + offset = findWhiteSpaceEnd(source, offset + 1); + + // If the next one or two input code points are U+0022 QUOTATION MARK ("), U+0027 APOSTROPHE ('), + // or whitespace followed by U+0022 QUOTATION MARK (") or U+0027 APOSTROPHE ('), + // then create a with its value set to string and return it. + if (getCharCode(offset) === 0x0022 || + getCharCode(offset) === 0x0027) { + type = TYPE$F.Function; + offset = nameStartOffset + 4; + return; + } + + // Otherwise, consume a url token, and return it. + consumeUrlToken(); + return; + } + + // Otherwise, if the next input code point is U+0028 LEFT PARENTHESIS ((), consume it. + // Create a with its value set to string and return it. + if (getCharCode(offset) === 0x0028) { + type = TYPE$F.Function; + offset++; + return; + } + + // Otherwise, create an with its value set to string and return it. + type = TYPE$F.Ident; + } + + // § 4.3.5. Consume a string token + function consumeStringToken(endingCodePoint) { + // This algorithm may be called with an ending code point, which denotes the code point + // that ends the string. If an ending code point is not specified, + // the current input code point is used. + if (!endingCodePoint) { + endingCodePoint = getCharCode(offset++); + } + + // Initially create a with its value set to the empty string. + type = TYPE$F.String; + + // Repeatedly consume the next input code point from the stream: + for (; offset < source.length; offset++) { + var code = source.charCodeAt(offset); + + switch (charCodeCategory(code)) { + // ending code point + case endingCodePoint: + // Return the . + offset++; + return; + + // EOF + case charCodeCategory.Eof: + // This is a parse error. Return the . + return; + + // newline + case charCodeCategory.WhiteSpace: + if (isNewline(code)) { + // This is a parse error. Reconsume the current input code point, + // create a , and return it. + offset += getNewlineLength(source, offset, code); + type = TYPE$F.BadString; + return; + } + break; + + // U+005C REVERSE SOLIDUS (\) + case 0x005C: + // If the next input code point is EOF, do nothing. + if (offset === source.length - 1) { + break; + } + + var nextCode = getCharCode(offset + 1); + + // Otherwise, if the next input code point is a newline, consume it. + if (isNewline(nextCode)) { + offset += getNewlineLength(source, offset + 1, nextCode); + } else if (isValidEscape(code, nextCode)) { + // Otherwise, (the stream starts with a valid escape) consume + // an escaped code point and append the returned code point to + // the ’s value. + offset = consumeEscaped(source, offset) - 1; + } + break; + + // anything else + // Append the current input code point to the ’s value. + } + } + } + + // § 4.3.6. Consume a url token + // Note: This algorithm assumes that the initial "url(" has already been consumed. + // This algorithm also assumes that it’s being called to consume an "unquoted" value, like url(foo). + // A quoted value, like url("foo"), is parsed as a . Consume an ident-like token + // automatically handles this distinction; this algorithm shouldn’t be called directly otherwise. + function consumeUrlToken() { + // Initially create a with its value set to the empty string. + type = TYPE$F.Url; + + // Consume as much whitespace as possible. + offset = findWhiteSpaceEnd(source, offset); + + // Repeatedly consume the next input code point from the stream: + for (; offset < source.length; offset++) { + var code = source.charCodeAt(offset); + + switch (charCodeCategory(code)) { + // U+0029 RIGHT PARENTHESIS ()) + case 0x0029: + // Return the . + offset++; + return; + + // EOF + case charCodeCategory.Eof: + // This is a parse error. Return the . + return; + + // whitespace + case charCodeCategory.WhiteSpace: + // Consume as much whitespace as possible. + offset = findWhiteSpaceEnd(source, offset); + + // If the next input code point is U+0029 RIGHT PARENTHESIS ()) or EOF, + // consume it and return the + // (if EOF was encountered, this is a parse error); + if (getCharCode(offset) === 0x0029 || offset >= source.length) { + if (offset < source.length) { + offset++; + } + return; + } + + // otherwise, consume the remnants of a bad url, create a , + // and return it. + offset = consumeBadUrlRemnants(source, offset); + type = TYPE$F.BadUrl; + return; + + // U+0022 QUOTATION MARK (") + // U+0027 APOSTROPHE (') + // U+0028 LEFT PARENTHESIS (() + // non-printable code point + case 0x0022: + case 0x0027: + case 0x0028: + case charCodeCategory.NonPrintable: + // This is a parse error. Consume the remnants of a bad url, + // create a , and return it. + offset = consumeBadUrlRemnants(source, offset); + type = TYPE$F.BadUrl; + return; + + // U+005C REVERSE SOLIDUS (\) + case 0x005C: + // If the stream starts with a valid escape, consume an escaped code point and + // append the returned code point to the ’s value. + if (isValidEscape(code, getCharCode(offset + 1))) { + offset = consumeEscaped(source, offset) - 1; + break; + } + + // Otherwise, this is a parse error. Consume the remnants of a bad url, + // create a , and return it. + offset = consumeBadUrlRemnants(source, offset); + type = TYPE$F.BadUrl; + return; + + // anything else + // Append the current input code point to the ’s value. + } + } + } + + if (!stream) { + stream = new TokenStream$3(); + } + + // ensure source is a string + source = String(source || ''); + + var sourceLength = source.length; + var offsetAndType = adoptBuffer$1(stream.offsetAndType, sourceLength + 1); // +1 because of eof-token + var balance = adoptBuffer$1(stream.balance, sourceLength + 1); + var tokenCount = 0; + var start = isBOM$1(getCharCode(0)); + var offset = start; + var balanceCloseType = 0; + var balanceStart = 0; + var balancePrev = 0; + + // https://drafts.csswg.org/css-syntax-3/#consume-token + // § 4.3.1. Consume a token + while (offset < sourceLength) { + var code = source.charCodeAt(offset); + var type = 0; + + balance[tokenCount] = sourceLength; + + switch (charCodeCategory(code)) { + // whitespace + case charCodeCategory.WhiteSpace: + // Consume as much whitespace as possible. Return a . + type = TYPE$F.WhiteSpace; + offset = findWhiteSpaceEnd(source, offset + 1); + break; + + // U+0022 QUOTATION MARK (") + case 0x0022: + // Consume a string token and return it. + consumeStringToken(); + break; + + // U+0023 NUMBER SIGN (#) + case 0x0023: + // If the next input code point is a name code point or the next two input code points are a valid escape, then: + if (isName(getCharCode(offset + 1)) || isValidEscape(getCharCode(offset + 1), getCharCode(offset + 2))) { + // Create a . + type = TYPE$F.Hash; + + // If the next 3 input code points would start an identifier, set the ’s type flag to "id". + // if (isIdentifierStart(getCharCode(offset + 1), getCharCode(offset + 2), getCharCode(offset + 3))) { + // // TODO: set id flag + // } + + // Consume a name, and set the ’s value to the returned string. + offset = consumeName(source, offset + 1); + + // Return the . + } else { + // Otherwise, return a with its value set to the current input code point. + type = TYPE$F.Delim; + offset++; + } + + break; + + // U+0027 APOSTROPHE (') + case 0x0027: + // Consume a string token and return it. + consumeStringToken(); + break; + + // U+0028 LEFT PARENTHESIS (() + case 0x0028: + // Return a <(-token>. + type = TYPE$F.LeftParenthesis; + offset++; + break; + + // U+0029 RIGHT PARENTHESIS ()) + case 0x0029: + // Return a <)-token>. + type = TYPE$F.RightParenthesis; + offset++; + break; + + // U+002B PLUS SIGN (+) + case 0x002B: + // If the input stream starts with a number, ... + if (isNumberStart(code, getCharCode(offset + 1), getCharCode(offset + 2))) { + // ... reconsume the current input code point, consume a numeric token, and return it. + consumeNumericToken(); + } else { + // Otherwise, return a with its value set to the current input code point. + type = TYPE$F.Delim; + offset++; + } + break; + + // U+002C COMMA (,) + case 0x002C: + // Return a . + type = TYPE$F.Comma; + offset++; + break; + + // U+002D HYPHEN-MINUS (-) + case 0x002D: + // If the input stream starts with a number, reconsume the current input code point, consume a numeric token, and return it. + if (isNumberStart(code, getCharCode(offset + 1), getCharCode(offset + 2))) { + consumeNumericToken(); + } else { + // Otherwise, if the next 2 input code points are U+002D HYPHEN-MINUS U+003E GREATER-THAN SIGN (->), consume them and return a . + if (getCharCode(offset + 1) === 0x002D && + getCharCode(offset + 2) === 0x003E) { + type = TYPE$F.CDC; + offset = offset + 3; + } else { + // Otherwise, if the input stream starts with an identifier, ... + if (isIdentifierStart$1(code, getCharCode(offset + 1), getCharCode(offset + 2))) { + // ... reconsume the current input code point, consume an ident-like token, and return it. + consumeIdentLikeToken(); + } else { + // Otherwise, return a with its value set to the current input code point. + type = TYPE$F.Delim; + offset++; + } + } + } + break; + + // U+002E FULL STOP (.) + case 0x002E: + // If the input stream starts with a number, ... + if (isNumberStart(code, getCharCode(offset + 1), getCharCode(offset + 2))) { + // ... reconsume the current input code point, consume a numeric token, and return it. + consumeNumericToken(); + } else { + // Otherwise, return a with its value set to the current input code point. + type = TYPE$F.Delim; + offset++; + } + + break; + + // U+002F SOLIDUS (/) + case 0x002F: + // If the next two input code point are U+002F SOLIDUS (/) followed by a U+002A ASTERISK (*), + if (getCharCode(offset + 1) === 0x002A) { + // ... consume them and all following code points up to and including the first U+002A ASTERISK (*) + // followed by a U+002F SOLIDUS (/), or up to an EOF code point. + type = TYPE$F.Comment; + offset = source.indexOf('*/', offset + 2) + 2; + if (offset === 1) { + offset = source.length; + } + } else { + type = TYPE$F.Delim; + offset++; + } + break; + + // U+003A COLON (:) + case 0x003A: + // Return a . + type = TYPE$F.Colon; + offset++; + break; + + // U+003B SEMICOLON (;) + case 0x003B: + // Return a . + type = TYPE$F.Semicolon; + offset++; + break; + + // U+003C LESS-THAN SIGN (<) + case 0x003C: + // If the next 3 input code points are U+0021 EXCLAMATION MARK U+002D HYPHEN-MINUS U+002D HYPHEN-MINUS (!--), ... + if (getCharCode(offset + 1) === 0x0021 && + getCharCode(offset + 2) === 0x002D && + getCharCode(offset + 3) === 0x002D) { + // ... consume them and return a . + type = TYPE$F.CDO; + offset = offset + 4; + } else { + // Otherwise, return a with its value set to the current input code point. + type = TYPE$F.Delim; + offset++; + } + + break; + + // U+0040 COMMERCIAL AT (@) + case 0x0040: + // If the next 3 input code points would start an identifier, ... + if (isIdentifierStart$1(getCharCode(offset + 1), getCharCode(offset + 2), getCharCode(offset + 3))) { + // ... consume a name, create an with its value set to the returned value, and return it. + type = TYPE$F.AtKeyword; + offset = consumeName(source, offset + 1); + } else { + // Otherwise, return a with its value set to the current input code point. + type = TYPE$F.Delim; + offset++; + } + + break; + + // U+005B LEFT SQUARE BRACKET ([) + case 0x005B: + // Return a <[-token>. + type = TYPE$F.LeftSquareBracket; + offset++; + break; + + // U+005C REVERSE SOLIDUS (\) + case 0x005C: + // If the input stream starts with a valid escape, ... + if (isValidEscape(code, getCharCode(offset + 1))) { + // ... reconsume the current input code point, consume an ident-like token, and return it. + consumeIdentLikeToken(); + } else { + // Otherwise, this is a parse error. Return a with its value set to the current input code point. + type = TYPE$F.Delim; + offset++; + } + break; + + // U+005D RIGHT SQUARE BRACKET (]) + case 0x005D: + // Return a <]-token>. + type = TYPE$F.RightSquareBracket; + offset++; + break; + + // U+007B LEFT CURLY BRACKET ({) + case 0x007B: + // Return a <{-token>. + type = TYPE$F.LeftCurlyBracket; + offset++; + break; + + // U+007D RIGHT CURLY BRACKET (}) + case 0x007D: + // Return a <}-token>. + type = TYPE$F.RightCurlyBracket; + offset++; + break; + + // digit + case charCodeCategory.Digit: + // Reconsume the current input code point, consume a numeric token, and return it. + consumeNumericToken(); + break; + + // name-start code point + case charCodeCategory.NameStart: + // Reconsume the current input code point, consume an ident-like token, and return it. + consumeIdentLikeToken(); + break; + + // EOF + case charCodeCategory.Eof: + // Return an . + break; + + // anything else + default: + // Return a with its value set to the current input code point. + type = TYPE$F.Delim; + offset++; + } + + switch (type) { + case balanceCloseType: + balancePrev = balanceStart & OFFSET_MASK; + balanceStart = balance[balancePrev]; + balanceCloseType = balanceStart >> TYPE_SHIFT; + balance[tokenCount] = balancePrev; + balance[balancePrev++] = tokenCount; + for (; balancePrev < tokenCount; balancePrev++) { + if (balance[balancePrev] === sourceLength) { + balance[balancePrev] = tokenCount; + } + } + break; + + case TYPE$F.LeftParenthesis: + case TYPE$F.Function: + balance[tokenCount] = balanceStart; + balanceCloseType = TYPE$F.RightParenthesis; + balanceStart = (balanceCloseType << TYPE_SHIFT) | tokenCount; + break; + + case TYPE$F.LeftSquareBracket: + balance[tokenCount] = balanceStart; + balanceCloseType = TYPE$F.RightSquareBracket; + balanceStart = (balanceCloseType << TYPE_SHIFT) | tokenCount; + break; + + case TYPE$F.LeftCurlyBracket: + balance[tokenCount] = balanceStart; + balanceCloseType = TYPE$F.RightCurlyBracket; + balanceStart = (balanceCloseType << TYPE_SHIFT) | tokenCount; + break; + } + + offsetAndType[tokenCount++] = (type << TYPE_SHIFT) | offset; + } + + // finalize buffers + offsetAndType[tokenCount] = (TYPE$F.EOF << TYPE_SHIFT) | offset; // + balance[tokenCount] = sourceLength; + balance[sourceLength] = sourceLength; // prevents false positive balance match with any token + while (balanceStart !== 0) { + balancePrev = balanceStart & OFFSET_MASK; + balanceStart = balance[balancePrev]; + balance[balancePrev] = sourceLength; + } + + // update stream + stream.source = source; + stream.firstCharOffset = start; + stream.offsetAndType = offsetAndType; + stream.tokenCount = tokenCount; + stream.balance = balance; + stream.reset(); + stream.next(); + + return stream; + } + + // extend tokenizer with constants + Object.keys(constants$1).forEach(function(key) { + tokenize$3[key] = constants$1[key]; + }); + + // extend tokenizer with static methods from utils + Object.keys(charCodeDefinitions).forEach(function(key) { + tokenize$3[key] = charCodeDefinitions[key]; + }); + Object.keys(utils).forEach(function(key) { + tokenize$3[key] = utils[key]; + }); + + var tokenizer$3 = tokenize$3; + + var isDigit$3 = tokenizer$3.isDigit; + var cmpChar$4 = tokenizer$3.cmpChar; + var TYPE$E = tokenizer$3.TYPE; + + var DELIM$6 = TYPE$E.Delim; + var WHITESPACE$b = TYPE$E.WhiteSpace; + var COMMENT$9 = TYPE$E.Comment; + var IDENT$i = TYPE$E.Ident; + var NUMBER$9 = TYPE$E.Number; + var DIMENSION$7 = TYPE$E.Dimension; + var PLUSSIGN$8 = 0x002B; // U+002B PLUS SIGN (+) + var HYPHENMINUS$4 = 0x002D; // U+002D HYPHEN-MINUS (-) + var N$4 = 0x006E; // U+006E LATIN SMALL LETTER N (n) + var DISALLOW_SIGN$1 = true; + var ALLOW_SIGN$1 = false; + + function isDelim$1(token, code) { + return token !== null && token.type === DELIM$6 && token.value.charCodeAt(0) === code; + } + + function skipSC(token, offset, getNextToken) { + while (token !== null && (token.type === WHITESPACE$b || token.type === COMMENT$9)) { + token = getNextToken(++offset); + } + + return offset; + } + + function checkInteger$1(token, valueOffset, disallowSign, offset) { + if (!token) { + return 0; + } + + var code = token.value.charCodeAt(valueOffset); + + if (code === PLUSSIGN$8 || code === HYPHENMINUS$4) { + if (disallowSign) { + // Number sign is not allowed + return 0; + } + valueOffset++; + } + + for (; valueOffset < token.value.length; valueOffset++) { + if (!isDigit$3(token.value.charCodeAt(valueOffset))) { + // Integer is expected + return 0; + } + } + + return offset + 1; + } + + // ... + // ... ['+' | '-'] + function consumeB$1(token, offset_, getNextToken) { + var sign = false; + var offset = skipSC(token, offset_, getNextToken); + + token = getNextToken(offset); + + if (token === null) { + return offset_; + } + + if (token.type !== NUMBER$9) { + if (isDelim$1(token, PLUSSIGN$8) || isDelim$1(token, HYPHENMINUS$4)) { + sign = true; + offset = skipSC(getNextToken(++offset), offset, getNextToken); + token = getNextToken(offset); + + if (token === null && token.type !== NUMBER$9) { + return 0; + } + } else { + return offset_; + } + } + + if (!sign) { + var code = token.value.charCodeAt(0); + if (code !== PLUSSIGN$8 && code !== HYPHENMINUS$4) { + // Number sign is expected + return 0; + } + } + + return checkInteger$1(token, sign ? 0 : 1, sign, offset); + } + + // An+B microsyntax https://www.w3.org/TR/css-syntax-3/#anb + var genericAnPlusB = function anPlusB(token, getNextToken) { + /* eslint-disable brace-style*/ + var offset = 0; + + if (!token) { + return 0; + } + + // + if (token.type === NUMBER$9) { + return checkInteger$1(token, 0, ALLOW_SIGN$1, offset); // b + } + + // -n + // -n + // -n ['+' | '-'] + // -n- + // + else if (token.type === IDENT$i && token.value.charCodeAt(0) === HYPHENMINUS$4) { + // expect 1st char is N + if (!cmpChar$4(token.value, 1, N$4)) { + return 0; + } + + switch (token.value.length) { + // -n + // -n + // -n ['+' | '-'] + case 2: + return consumeB$1(getNextToken(++offset), offset, getNextToken); + + // -n- + case 3: + if (token.value.charCodeAt(2) !== HYPHENMINUS$4) { + return 0; + } + + offset = skipSC(getNextToken(++offset), offset, getNextToken); + token = getNextToken(offset); + + return checkInteger$1(token, 0, DISALLOW_SIGN$1, offset); + + // + default: + if (token.value.charCodeAt(2) !== HYPHENMINUS$4) { + return 0; + } + + return checkInteger$1(token, 3, DISALLOW_SIGN$1, offset); + } + } + + // '+'? n + // '+'? n + // '+'? n ['+' | '-'] + // '+'? n- + // '+'? + else if (token.type === IDENT$i || (isDelim$1(token, PLUSSIGN$8) && getNextToken(offset + 1).type === IDENT$i)) { + // just ignore a plus + if (token.type !== IDENT$i) { + token = getNextToken(++offset); + } + + if (token === null || !cmpChar$4(token.value, 0, N$4)) { + return 0; + } + + switch (token.value.length) { + // '+'? n + // '+'? n + // '+'? n ['+' | '-'] + case 1: + return consumeB$1(getNextToken(++offset), offset, getNextToken); + + // '+'? n- + case 2: + if (token.value.charCodeAt(1) !== HYPHENMINUS$4) { + return 0; + } + + offset = skipSC(getNextToken(++offset), offset, getNextToken); + token = getNextToken(offset); + + return checkInteger$1(token, 0, DISALLOW_SIGN$1, offset); + + // '+'? + default: + if (token.value.charCodeAt(1) !== HYPHENMINUS$4) { + return 0; + } + + return checkInteger$1(token, 2, DISALLOW_SIGN$1, offset); + } + } + + // + // + // + // + // ['+' | '-'] + else if (token.type === DIMENSION$7) { + var code = token.value.charCodeAt(0); + var sign = code === PLUSSIGN$8 || code === HYPHENMINUS$4 ? 1 : 0; + + for (var i = sign; i < token.value.length; i++) { + if (!isDigit$3(token.value.charCodeAt(i))) { + break; + } + } + + if (i === sign) { + // Integer is expected + return 0; + } + + if (!cmpChar$4(token.value, i, N$4)) { + return 0; + } + + // + // + // ['+' | '-'] + if (i + 1 === token.value.length) { + return consumeB$1(getNextToken(++offset), offset, getNextToken); + } else { + if (token.value.charCodeAt(i + 1) !== HYPHENMINUS$4) { + return 0; + } + + // + if (i + 2 === token.value.length) { + offset = skipSC(getNextToken(++offset), offset, getNextToken); + token = getNextToken(offset); + + return checkInteger$1(token, 0, DISALLOW_SIGN$1, offset); + } + // + else { + return checkInteger$1(token, i + 2, DISALLOW_SIGN$1, offset); + } + } + } + + return 0; + }; + + var isHexDigit$2 = tokenizer$3.isHexDigit; + var cmpChar$3 = tokenizer$3.cmpChar; + var TYPE$D = tokenizer$3.TYPE; + + var IDENT$h = TYPE$D.Ident; + var DELIM$5 = TYPE$D.Delim; + var NUMBER$8 = TYPE$D.Number; + var DIMENSION$6 = TYPE$D.Dimension; + var PLUSSIGN$7 = 0x002B; // U+002B PLUS SIGN (+) + var HYPHENMINUS$3 = 0x002D; // U+002D HYPHEN-MINUS (-) + var QUESTIONMARK$2 = 0x003F; // U+003F QUESTION MARK (?) + var U$2 = 0x0075; // U+0075 LATIN SMALL LETTER U (u) + + function isDelim(token, code) { + return token !== null && token.type === DELIM$5 && token.value.charCodeAt(0) === code; + } + + function startsWith$1(token, code) { + return token.value.charCodeAt(0) === code; + } + + function hexSequence(token, offset, allowDash) { + for (var pos = offset, hexlen = 0; pos < token.value.length; pos++) { + var code = token.value.charCodeAt(pos); + + if (code === HYPHENMINUS$3 && allowDash && hexlen !== 0) { + if (hexSequence(token, offset + hexlen + 1, false) > 0) { + return 6; // dissallow following question marks + } + + return 0; // dash at the ending of a hex sequence is not allowed + } + + if (!isHexDigit$2(code)) { + return 0; // not a hex digit + } + + if (++hexlen > 6) { + return 0; // too many hex digits + } } + + return hexlen; + } + + function withQuestionMarkSequence(consumed, length, getNextToken) { + if (!consumed) { + return 0; // nothing consumed + } + + while (isDelim(getNextToken(length), QUESTIONMARK$2)) { + if (++consumed > 6) { + return 0; // too many question marks + } + + length++; + } + + return length; + } + + // https://drafts.csswg.org/css-syntax/#urange + // Informally, the production has three forms: + // U+0001 + // Defines a range consisting of a single code point, in this case the code point "1". + // U+0001-00ff + // Defines a range of codepoints between the first and the second value, in this case + // the range between "1" and "ff" (255 in decimal) inclusive. + // U+00?? + // Defines a range of codepoints where the "?" characters range over all hex digits, + // in this case defining the same as the value U+0000-00ff. + // In each form, a maximum of 6 digits is allowed for each hexadecimal number (if you treat "?" as a hexadecimal digit). + // + // = + // u '+' '?'* | + // u '?'* | + // u '?'* | + // u | + // u | + // u '+' '?'+ + var genericUrange = function urange(token, getNextToken) { + var length = 0; + + // should start with `u` or `U` + if (token === null || token.type !== IDENT$h || !cmpChar$3(token.value, 0, U$2)) { + return 0; + } + + token = getNextToken(++length); + if (token === null) { + return 0; + } + + // u '+' '?'* + // u '+' '?'+ + if (isDelim(token, PLUSSIGN$7)) { + token = getNextToken(++length); + if (token === null) { + return 0; + } + + if (token.type === IDENT$h) { + // u '+' '?'* + return withQuestionMarkSequence(hexSequence(token, 0, true), ++length, getNextToken); + } + + if (isDelim(token, QUESTIONMARK$2)) { + // u '+' '?'+ + return withQuestionMarkSequence(1, ++length, getNextToken); + } + + // Hex digit or question mark is expected + return 0; + } + + // u '?'* + // u + // u + if (token.type === NUMBER$8) { + if (!startsWith$1(token, PLUSSIGN$7)) { + return 0; + } + + var consumedHexLength = hexSequence(token, 1, true); + if (consumedHexLength === 0) { + return 0; + } + + token = getNextToken(++length); + if (token === null) { + // u + return length; + } + + if (token.type === DIMENSION$6 || token.type === NUMBER$8) { + // u + // u + if (!startsWith$1(token, HYPHENMINUS$3) || !hexSequence(token, 1, false)) { + return 0; + } + + return length + 1; + } + + // u '?'* + return withQuestionMarkSequence(consumedHexLength, length, getNextToken); + } + + // u '?'* + if (token.type === DIMENSION$6) { + if (!startsWith$1(token, PLUSSIGN$7)) { + return 0; + } + + return withQuestionMarkSequence(hexSequence(token, 1, true), ++length, getNextToken); + } + + return 0; + }; + + var tokenizer$2 = tokenizer$3; + var isIdentifierStart = tokenizer$2.isIdentifierStart; + var isHexDigit$1 = tokenizer$2.isHexDigit; + var isDigit$2 = tokenizer$2.isDigit; + var cmpStr$3 = tokenizer$2.cmpStr; + var consumeNumber$3 = tokenizer$2.consumeNumber; + var TYPE$C = tokenizer$2.TYPE; + var anPlusB = genericAnPlusB; + var urange = genericUrange; + + var cssWideKeywords$1 = ['unset', 'initial', 'inherit']; + var calcFunctionNames = ['calc(', '-moz-calc(', '-webkit-calc(']; + + // https://www.w3.org/TR/css-values-3/#lengths + var LENGTH = { + // absolute length units + 'px': true, + 'mm': true, + 'cm': true, + 'in': true, + 'pt': true, + 'pc': true, + 'q': true, + + // relative length units + 'em': true, + 'ex': true, + 'ch': true, + 'rem': true, + + // viewport-percentage lengths + 'vh': true, + 'vw': true, + 'vmin': true, + 'vmax': true, + 'vm': true + }; + + var ANGLE = { + 'deg': true, + 'grad': true, + 'rad': true, + 'turn': true + }; + + var TIME = { + 's': true, + 'ms': true + }; + + var FREQUENCY = { + 'hz': true, + 'khz': true + }; + + // https://www.w3.org/TR/css-values-3/#resolution (https://drafts.csswg.org/css-values/#resolution) + var RESOLUTION = { + 'dpi': true, + 'dpcm': true, + 'dppx': true, + 'x': true // https://github.com/w3c/csswg-drafts/issues/461 + }; + + // https://drafts.csswg.org/css-grid/#fr-unit + var FLEX = { + 'fr': true + }; + + // https://www.w3.org/TR/css3-speech/#mixing-props-voice-volume + var DECIBEL = { + 'db': true + }; + + // https://www.w3.org/TR/css3-speech/#voice-props-voice-pitch + var SEMITONES = { + 'st': true + }; + + // safe char code getter + function charCode(str, index) { + return index < str.length ? str.charCodeAt(index) : 0; + } + + function eqStr(actual, expected) { + return cmpStr$3(actual, 0, actual.length, expected); + } + + function eqStrAny(actual, expected) { + for (var i = 0; i < expected.length; i++) { + if (eqStr(actual, expected[i])) { + return true; + } + } + + return false; + } + + // IE postfix hack, i.e. 123\0 or 123px\9 + function isPostfixIeHack(str, offset) { + if (offset !== str.length - 2) { + return false; + } + + return ( + str.charCodeAt(offset) === 0x005C && // U+005C REVERSE SOLIDUS (\) + isDigit$2(str.charCodeAt(offset + 1)) + ); + } + + function outOfRange(opts, value, numEnd) { + if (opts && opts.type === 'Range') { + var num = Number( + numEnd !== undefined && numEnd !== value.length + ? value.substr(0, numEnd) + : value + ); + + if (isNaN(num)) { + return true; + } + + if (opts.min !== null && num < opts.min) { + return true; + } + + if (opts.max !== null && num > opts.max) { + return true; + } + } + + return false; + } + + function consumeFunction(token, getNextToken) { + var startIdx = token.index; + var length = 0; + + // balanced token consuming + do { + length++; + + if (token.balance <= startIdx) { + break; + } + } while (token = getNextToken(length)); + + return length; + } + + // TODO: implement + // can be used wherever , , ,