diff --git a/.gitignore b/.gitignore index 47e5d943..8ea362a8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,8 @@ CLAUDE.md /.htmltest.yml node_modules/ + +# perf/measure.mjs --out targets at repo root (book.pdf + render.cpuprofile + timing.*) +/before/ +/after-*/ +/findoverflow-baseline/ diff --git a/docs/assets/css/print.css b/docs/assets/css/print.css index 8c4245c1..6cee56dd 100644 --- a/docs/assets/css/print.css +++ b/docs/assets/css/print.css @@ -13,7 +13,14 @@ margin: 22mm 20mm 22mm 20mm; @bottom-right { - content: counter(page); + /* Reads a JS-tracked page number set on each .pagedjs_page wrapper + by the Counters handler in docs/lib/paged.browser.js. Switched off + `counter(page)` because the aggressive-detach render optimization + (perf/detach-pages.js) physically removes finalized pages from the + DOM, which breaks CSS counter accumulation. The Counters handler + honours the same part-divider counter-reset rules as the original + counter(page) did, so part-restarts continue to work. */ + content: var(--page-num); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; font-size: 9pt; color: #555; diff --git a/docs/book.bat b/docs/book.bat index ffd478ef..8d8ee784 100644 --- a/docs/book.bat +++ b/docs/book.bat @@ -2,9 +2,27 @@ 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. 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 +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 00000000..88ead0fc --- /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 00000000..07509811 --- /dev/null +++ b/docs/lib/paged.browser.js @@ -0,0 +1,33616 @@ +/** + * @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; + // Additive backoff for overflow checks. Without this, every + // node appended past maxChars triggers findBreakToken -> gBCR, + // linear-scanning past the actual overflow point. Instead, + // only fire the check once per maxChars of NEW content, which + // reduces hasOverflow gBCR calls from O(nodes-past-maxChars) + // to O(page-chars / maxChars). findBreakToken handles any + // overshoot correctly: it walks the wrapper, extracts the + // excess via removeOverflow, and returns a BreakToken + // pointing at the right source resume position, so the only + // observable difference is fewer gBCR-driven layout flushes. + let lengthAtLastCheck = 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 overflow once per maxChars of new content. + if (length - lengthAtLastCheck >= 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; + lengthAtLastCheck = 0; + this.rebuildTableFromBreakToken(newBreakToken, wrapper); + } else { + lengthAtLastCheck = length; + } + + 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. + // Upstream paged.js wrote [colspan] here, but the variable is + // `rowspan`, the comment says rowspan, and the walk below + // searches back through rows for one with full column coverage + // (the row that started a rowspan group) -- a colspan check + // here meant tables with rowspan but no colspan silently + // skipped the rowspan-aware break logic. + const table = parentOf(tableRow, "TABLE", rendered); + const rowspan = table.querySelector("[rowspan]"); + 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) { + const child = lastChild.lastChild; + if (!child) break; + if (!validNode(lastChild)) { + // Only get elements with refs + lastChild = lastChild.previousSibling; + } else if (!validNode(child)) { + // Deal with invalid dom items + lastChild = prevValidNode(child); + break; + } else { + lastChild = child; + } + } + + 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 || !this._onUnderflow) { + return; + } + + let endToken = this.layoutMethod.findEndToken(this.wrapper, contents); + + if (endToken) { + 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; + + }); + } + + + }); + + } + + 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 , , ,
/..' parts. + * + * Based on code in the Node.js 'path' core module. + * + * @param aPath The path or url to normalize. + */ + function normalize(aPath) { + var path = aPath; + var url = urlParse(aPath); + if (url) { + if (!url.path) { + return aPath; + } + path = url.path; + } + var isAbsolute = exports.isAbsolute(path); + + var parts = path.split(/\/+/); + for (var part, up = 0, i = parts.length - 1; i >= 0; i--) { + part = parts[i]; + if (part === '.') { + parts.splice(i, 1); + } else if (part === '..') { + up++; + } else if (up > 0) { + if (part === '') { + // The first part is blank if the path is absolute. Trying to go + // above the root is a no-op. Therefore we can remove all '..' parts + // directly after the root. + parts.splice(i + 1, up); + up = 0; + } else { + parts.splice(i, 2); + up--; + } + } + } + path = parts.join('/'); + + if (path === '') { + path = isAbsolute ? '/' : '.'; + } + + if (url) { + url.path = path; + return urlGenerate(url); + } + return path; + } + exports.normalize = normalize; + + /** + * Joins two paths/URLs. + * + * @param aRoot The root path or URL. + * @param aPath The path or URL to be joined with the root. + * + * - If aPath is a URL or a data URI, aPath is returned, unless aPath is a + * scheme-relative URL: Then the scheme of aRoot, if any, is prepended + * first. + * - Otherwise aPath is a path. If aRoot is a URL, then its path portion + * is updated with the result and aRoot is returned. Otherwise the result + * is returned. + * - If aPath is absolute, the result is aPath. + * - Otherwise the two paths are joined with a slash. + * - Joining for example 'http://' and 'www.example.com' is also supported. + */ + function join(aRoot, aPath) { + if (aRoot === "") { + aRoot = "."; + } + if (aPath === "") { + aPath = "."; + } + var aPathUrl = urlParse(aPath); + var aRootUrl = urlParse(aRoot); + if (aRootUrl) { + aRoot = aRootUrl.path || '/'; + } + + // `join(foo, '//www.example.org')` + if (aPathUrl && !aPathUrl.scheme) { + if (aRootUrl) { + aPathUrl.scheme = aRootUrl.scheme; + } + return urlGenerate(aPathUrl); + } + + if (aPathUrl || aPath.match(dataUrlRegexp)) { + return aPath; + } + + // `join('http://', 'www.example.com')` + if (aRootUrl && !aRootUrl.host && !aRootUrl.path) { + aRootUrl.host = aPath; + return urlGenerate(aRootUrl); + } + + var joined = aPath.charAt(0) === '/' + ? aPath + : normalize(aRoot.replace(/\/+$/, '') + '/' + aPath); + + if (aRootUrl) { + aRootUrl.path = joined; + return urlGenerate(aRootUrl); + } + return joined; + } + exports.join = join; + + exports.isAbsolute = function (aPath) { + return aPath.charAt(0) === '/' || urlRegexp.test(aPath); + }; + + /** + * Make a path relative to a URL or another path. + * + * @param aRoot The root path or URL. + * @param aPath The path or URL to be made relative to aRoot. + */ + function relative(aRoot, aPath) { + if (aRoot === "") { + aRoot = "."; + } + + aRoot = aRoot.replace(/\/$/, ''); + + // It is possible for the path to be above the root. In this case, simply + // checking whether the root is a prefix of the path won't work. Instead, we + // need to remove components from the root one by one, until either we find + // a prefix that fits, or we run out of components to remove. + var level = 0; + while (aPath.indexOf(aRoot + '/') !== 0) { + var index = aRoot.lastIndexOf("/"); + if (index < 0) { + return aPath; + } + + // If the only part of the root that is left is the scheme (i.e. http://, + // file:///, etc.), one or more slashes (/), or simply nothing at all, we + // have exhausted all components, so the path is not relative to the root. + aRoot = aRoot.slice(0, index); + if (aRoot.match(/^([^\/]+:\/)?\/*$/)) { + return aPath; + } + + ++level; + } + + // Make sure we add a "../" for each component we removed from the root. + return Array(level + 1).join("../") + aPath.substr(aRoot.length + 1); + } + exports.relative = relative; + + var supportsNullProto = (function () { + var obj = Object.create(null); + return !('__proto__' in obj); + }()); + + function identity (s) { + return s; + } + + /** + * Because behavior goes wacky when you set `__proto__` on objects, we + * have to prefix all the strings in our set with an arbitrary character. + * + * See https://github.com/mozilla/source-map/pull/31 and + * https://github.com/mozilla/source-map/issues/30 + * + * @param String aStr + */ + function toSetString(aStr) { + if (isProtoString(aStr)) { + return '$' + aStr; + } + + return aStr; + } + exports.toSetString = supportsNullProto ? identity : toSetString; + + function fromSetString(aStr) { + if (isProtoString(aStr)) { + return aStr.slice(1); + } + + return aStr; + } + exports.fromSetString = supportsNullProto ? identity : fromSetString; + + function isProtoString(s) { + if (!s) { + return false; + } + + var length = s.length; + + if (length < 9 /* "__proto__".length */) { + return false; + } + + if (s.charCodeAt(length - 1) !== 95 /* '_' */ || + s.charCodeAt(length - 2) !== 95 /* '_' */ || + s.charCodeAt(length - 3) !== 111 /* 'o' */ || + s.charCodeAt(length - 4) !== 116 /* 't' */ || + s.charCodeAt(length - 5) !== 111 /* 'o' */ || + s.charCodeAt(length - 6) !== 114 /* 'r' */ || + s.charCodeAt(length - 7) !== 112 /* 'p' */ || + s.charCodeAt(length - 8) !== 95 /* '_' */ || + s.charCodeAt(length - 9) !== 95 /* '_' */) { + return false; + } + + for (var i = length - 10; i >= 0; i--) { + if (s.charCodeAt(i) !== 36 /* '$' */) { + return false; + } + } + + return true; + } + + /** + * Comparator between two mappings where the original positions are compared. + * + * Optionally pass in `true` as `onlyCompareGenerated` to consider two + * mappings with the same original source/line/column, but different generated + * line and column the same. Useful when searching for a mapping with a + * stubbed out mapping. + */ + function compareByOriginalPositions(mappingA, mappingB, onlyCompareOriginal) { + var cmp = strcmp(mappingA.source, mappingB.source); + if (cmp !== 0) { + return cmp; + } + + cmp = mappingA.originalLine - mappingB.originalLine; + if (cmp !== 0) { + return cmp; + } + + cmp = mappingA.originalColumn - mappingB.originalColumn; + if (cmp !== 0 || onlyCompareOriginal) { + return cmp; + } + + cmp = mappingA.generatedColumn - mappingB.generatedColumn; + if (cmp !== 0) { + return cmp; + } + + cmp = mappingA.generatedLine - mappingB.generatedLine; + if (cmp !== 0) { + return cmp; + } + + return strcmp(mappingA.name, mappingB.name); + } + exports.compareByOriginalPositions = compareByOriginalPositions; + + /** + * Comparator between two mappings with deflated source and name indices where + * the generated positions are compared. + * + * Optionally pass in `true` as `onlyCompareGenerated` to consider two + * mappings with the same generated line and column, but different + * source/name/original line and column the same. Useful when searching for a + * mapping with a stubbed out mapping. + */ + function compareByGeneratedPositionsDeflated(mappingA, mappingB, onlyCompareGenerated) { + var cmp = mappingA.generatedLine - mappingB.generatedLine; + if (cmp !== 0) { + return cmp; + } + + cmp = mappingA.generatedColumn - mappingB.generatedColumn; + if (cmp !== 0 || onlyCompareGenerated) { + return cmp; + } + + cmp = strcmp(mappingA.source, mappingB.source); + if (cmp !== 0) { + return cmp; + } + + cmp = mappingA.originalLine - mappingB.originalLine; + if (cmp !== 0) { + return cmp; + } + + cmp = mappingA.originalColumn - mappingB.originalColumn; + if (cmp !== 0) { + return cmp; + } + + return strcmp(mappingA.name, mappingB.name); + } + exports.compareByGeneratedPositionsDeflated = compareByGeneratedPositionsDeflated; + + function strcmp(aStr1, aStr2) { + if (aStr1 === aStr2) { + return 0; + } + + if (aStr1 === null) { + return 1; // aStr2 !== null + } + + if (aStr2 === null) { + return -1; // aStr1 !== null + } + + if (aStr1 > aStr2) { + return 1; + } + + return -1; + } + + /** + * Comparator between two mappings with inflated source and name strings where + * the generated positions are compared. + */ + function compareByGeneratedPositionsInflated(mappingA, mappingB) { + var cmp = mappingA.generatedLine - mappingB.generatedLine; + if (cmp !== 0) { + return cmp; + } + + cmp = mappingA.generatedColumn - mappingB.generatedColumn; + if (cmp !== 0) { + return cmp; + } + + cmp = strcmp(mappingA.source, mappingB.source); + if (cmp !== 0) { + return cmp; + } + + cmp = mappingA.originalLine - mappingB.originalLine; + if (cmp !== 0) { + return cmp; + } + + cmp = mappingA.originalColumn - mappingB.originalColumn; + if (cmp !== 0) { + return cmp; + } + + return strcmp(mappingA.name, mappingB.name); + } + exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflated; + + /** + * Strip any JSON XSSI avoidance prefix from the string (as documented + * in the source maps specification), and then parse the string as + * JSON. + */ + function parseSourceMapInput(str) { + return JSON.parse(str.replace(/^\)]}'[^\n]*\n/, '')); + } + exports.parseSourceMapInput = parseSourceMapInput; + + /** + * Compute the URL of a source given the the source root, the source's + * URL, and the source map's URL. + */ + function computeSourceURL(sourceRoot, sourceURL, sourceMapURL) { + sourceURL = sourceURL || ''; + + if (sourceRoot) { + // This follows what Chrome does. + if (sourceRoot[sourceRoot.length - 1] !== '/' && sourceURL[0] !== '/') { + sourceRoot += '/'; + } + // The spec says: + // Line 4: An optional source root, useful for relocating source + // files on a server or removing repeated values in the + // “sources” entry. This value is prepended to the individual + // entries in the “source” field. + sourceURL = sourceRoot + sourceURL; + } + + // Historically, SourceMapConsumer did not take the sourceMapURL as + // a parameter. This mode is still somewhat supported, which is why + // this code block is conditional. However, it's preferable to pass + // the source map URL to SourceMapConsumer, so that this function + // can implement the source URL resolution algorithm as outlined in + // the spec. This block is basically the equivalent of: + // new URL(sourceURL, sourceMapURL).toString() + // ... except it avoids using URL, which wasn't available in the + // older releases of node still supported by this library. + // + // The spec says: + // If the sources are not absolute URLs after prepending of the + // “sourceRoot”, the sources are resolved relative to the + // SourceMap (like resolving script src in a html document). + if (sourceMapURL) { + var parsed = urlParse(sourceMapURL); + if (!parsed) { + throw new Error("sourceMapURL could not be parsed"); + } + if (parsed.path) { + // Strip the last path component, but keep the "/". + var index = parsed.path.lastIndexOf('/'); + if (index >= 0) { + parsed.path = parsed.path.substring(0, index + 1); + } + } + sourceURL = join(urlGenerate(parsed), sourceURL); + } + + return normalize(sourceURL); + } + exports.computeSourceURL = computeSourceURL; + } (util$3)); + + var arraySet = {}; + + /* -*- Mode: js; js-indent-level: 2; -*- */ + + /* + * Copyright 2011 Mozilla Foundation and contributors + * Licensed under the New BSD license. See LICENSE or: + * http://opensource.org/licenses/BSD-3-Clause + */ + + var util$2 = util$3; + var has$1 = Object.prototype.hasOwnProperty; + var hasNativeMap = typeof Map !== "undefined"; + + /** + * A data structure which is a combination of an array and a set. Adding a new + * member is O(1), testing for membership is O(1), and finding the index of an + * element is O(1). Removing elements from the set is not supported. Only + * strings are supported for membership. + */ + function ArraySet$1() { + this._array = []; + this._set = hasNativeMap ? new Map() : Object.create(null); + } + + /** + * Static method for creating ArraySet instances from an existing array. + */ + ArraySet$1.fromArray = function ArraySet_fromArray(aArray, aAllowDuplicates) { + var set = new ArraySet$1(); + for (var i = 0, len = aArray.length; i < len; i++) { + set.add(aArray[i], aAllowDuplicates); + } + return set; + }; + + /** + * Return how many unique items are in this ArraySet. If duplicates have been + * added, than those do not count towards the size. + * + * @returns Number + */ + ArraySet$1.prototype.size = function ArraySet_size() { + return hasNativeMap ? this._set.size : Object.getOwnPropertyNames(this._set).length; + }; + + /** + * Add the given string to this set. + * + * @param String aStr + */ + ArraySet$1.prototype.add = function ArraySet_add(aStr, aAllowDuplicates) { + var sStr = hasNativeMap ? aStr : util$2.toSetString(aStr); + var isDuplicate = hasNativeMap ? this.has(aStr) : has$1.call(this._set, sStr); + var idx = this._array.length; + if (!isDuplicate || aAllowDuplicates) { + this._array.push(aStr); + } + if (!isDuplicate) { + if (hasNativeMap) { + this._set.set(aStr, idx); + } else { + this._set[sStr] = idx; + } + } + }; + + /** + * Is the given string a member of this set? + * + * @param String aStr + */ + ArraySet$1.prototype.has = function ArraySet_has(aStr) { + if (hasNativeMap) { + return this._set.has(aStr); + } else { + var sStr = util$2.toSetString(aStr); + return has$1.call(this._set, sStr); + } + }; + + /** + * What is the index of the given string in the array? + * + * @param String aStr + */ + ArraySet$1.prototype.indexOf = function ArraySet_indexOf(aStr) { + if (hasNativeMap) { + var idx = this._set.get(aStr); + if (idx >= 0) { + return idx; + } + } else { + var sStr = util$2.toSetString(aStr); + if (has$1.call(this._set, sStr)) { + return this._set[sStr]; + } + } + + throw new Error('"' + aStr + '" is not in the set.'); + }; + + /** + * What is the element at the given index? + * + * @param Number aIdx + */ + ArraySet$1.prototype.at = function ArraySet_at(aIdx) { + if (aIdx >= 0 && aIdx < this._array.length) { + return this._array[aIdx]; + } + throw new Error('No element indexed by ' + aIdx); + }; + + /** + * Returns the array representation of this set (which has the proper indices + * indicated by indexOf). Note that this is a copy of the internal array used + * for storing the members so that no one can mess with internal state. + */ + ArraySet$1.prototype.toArray = function ArraySet_toArray() { + return this._array.slice(); + }; + + arraySet.ArraySet = ArraySet$1; + + var mappingList = {}; + + /* -*- Mode: js; js-indent-level: 2; -*- */ + + /* + * Copyright 2014 Mozilla Foundation and contributors + * Licensed under the New BSD license. See LICENSE or: + * http://opensource.org/licenses/BSD-3-Clause + */ + + var util$1 = util$3; + + /** + * Determine whether mappingB is after mappingA with respect to generated + * position. + */ + function generatedPositionAfter(mappingA, mappingB) { + // Optimized for most common case + var lineA = mappingA.generatedLine; + var lineB = mappingB.generatedLine; + var columnA = mappingA.generatedColumn; + var columnB = mappingB.generatedColumn; + return lineB > lineA || lineB == lineA && columnB >= columnA || + util$1.compareByGeneratedPositionsInflated(mappingA, mappingB) <= 0; + } + + /** + * A data structure to provide a sorted view of accumulated mappings in a + * performance conscious manner. It trades a neglibable overhead in general + * case for a large speedup in case of mappings being added in order. + */ + function MappingList$1() { + this._array = []; + this._sorted = true; + // Serves as infimum + this._last = {generatedLine: -1, generatedColumn: 0}; + } + + /** + * Iterate through internal items. This method takes the same arguments that + * `Array.prototype.forEach` takes. + * + * NOTE: The order of the mappings is NOT guaranteed. + */ + MappingList$1.prototype.unsortedForEach = + function MappingList_forEach(aCallback, aThisArg) { + this._array.forEach(aCallback, aThisArg); + }; + + /** + * Add the given source mapping. + * + * @param Object aMapping + */ + MappingList$1.prototype.add = function MappingList_add(aMapping) { + if (generatedPositionAfter(this._last, aMapping)) { + this._last = aMapping; + this._array.push(aMapping); + } else { + this._sorted = false; + this._array.push(aMapping); + } + }; + + /** + * Returns the flat, sorted array of mappings. The mappings are sorted by + * generated position. + * + * WARNING: This method returns internal data without copying, for + * performance. The return value must NOT be mutated, and should be treated as + * an immutable borrow. If you want to take ownership, you must make your own + * copy. + */ + MappingList$1.prototype.toArray = function MappingList_toArray() { + if (!this._sorted) { + this._array.sort(util$1.compareByGeneratedPositionsInflated); + this._sorted = true; + } + return this._array; + }; + + mappingList.MappingList = MappingList$1; + + /* -*- Mode: js; js-indent-level: 2; -*- */ + + /* + * Copyright 2011 Mozilla Foundation and contributors + * Licensed under the New BSD license. See LICENSE or: + * http://opensource.org/licenses/BSD-3-Clause + */ + + var base64VLQ = base64Vlq; + var util = util$3; + var ArraySet = arraySet.ArraySet; + var MappingList = mappingList.MappingList; + + /** + * An instance of the SourceMapGenerator represents a source map which is + * being built incrementally. You may pass an object with the following + * properties: + * + * - file: The filename of the generated source. + * - sourceRoot: A root for all relative URLs in this source map. + */ + function SourceMapGenerator$1(aArgs) { + if (!aArgs) { + aArgs = {}; + } + this._file = util.getArg(aArgs, 'file', null); + this._sourceRoot = util.getArg(aArgs, 'sourceRoot', null); + this._skipValidation = util.getArg(aArgs, 'skipValidation', false); + this._sources = new ArraySet(); + this._names = new ArraySet(); + this._mappings = new MappingList(); + this._sourcesContents = null; + } + + SourceMapGenerator$1.prototype._version = 3; + + /** + * Creates a new SourceMapGenerator based on a SourceMapConsumer + * + * @param aSourceMapConsumer The SourceMap. + */ + SourceMapGenerator$1.fromSourceMap = + function SourceMapGenerator_fromSourceMap(aSourceMapConsumer) { + var sourceRoot = aSourceMapConsumer.sourceRoot; + var generator = new SourceMapGenerator$1({ + file: aSourceMapConsumer.file, + sourceRoot: sourceRoot + }); + aSourceMapConsumer.eachMapping(function (mapping) { + var newMapping = { + generated: { + line: mapping.generatedLine, + column: mapping.generatedColumn + } + }; + + if (mapping.source != null) { + newMapping.source = mapping.source; + if (sourceRoot != null) { + newMapping.source = util.relative(sourceRoot, newMapping.source); + } + + newMapping.original = { + line: mapping.originalLine, + column: mapping.originalColumn + }; + + if (mapping.name != null) { + newMapping.name = mapping.name; + } + } + + generator.addMapping(newMapping); + }); + aSourceMapConsumer.sources.forEach(function (sourceFile) { + var sourceRelative = sourceFile; + if (sourceRoot !== null) { + sourceRelative = util.relative(sourceRoot, sourceFile); + } + + if (!generator._sources.has(sourceRelative)) { + generator._sources.add(sourceRelative); + } + + var content = aSourceMapConsumer.sourceContentFor(sourceFile); + if (content != null) { + generator.setSourceContent(sourceFile, content); + } + }); + return generator; + }; + + /** + * Add a single mapping from original source line and column to the generated + * source's line and column for this source map being created. The mapping + * object should have the following properties: + * + * - generated: An object with the generated line and column positions. + * - original: An object with the original line and column positions. + * - source: The original source file (relative to the sourceRoot). + * - name: An optional original token name for this mapping. + */ + SourceMapGenerator$1.prototype.addMapping = + function SourceMapGenerator_addMapping(aArgs) { + var generated = util.getArg(aArgs, 'generated'); + var original = util.getArg(aArgs, 'original', null); + var source = util.getArg(aArgs, 'source', null); + var name = util.getArg(aArgs, 'name', null); + + if (!this._skipValidation) { + this._validateMapping(generated, original, source, name); + } + + if (source != null) { + source = String(source); + if (!this._sources.has(source)) { + this._sources.add(source); + } + } + + if (name != null) { + name = String(name); + if (!this._names.has(name)) { + this._names.add(name); + } + } + + this._mappings.add({ + generatedLine: generated.line, + generatedColumn: generated.column, + originalLine: original != null && original.line, + originalColumn: original != null && original.column, + source: source, + name: name + }); + }; + + /** + * Set the source content for a source file. + */ + SourceMapGenerator$1.prototype.setSourceContent = + function SourceMapGenerator_setSourceContent(aSourceFile, aSourceContent) { + var source = aSourceFile; + if (this._sourceRoot != null) { + source = util.relative(this._sourceRoot, source); + } + + if (aSourceContent != null) { + // Add the source content to the _sourcesContents map. + // Create a new _sourcesContents map if the property is null. + if (!this._sourcesContents) { + this._sourcesContents = Object.create(null); + } + this._sourcesContents[util.toSetString(source)] = aSourceContent; + } else if (this._sourcesContents) { + // Remove the source file from the _sourcesContents map. + // If the _sourcesContents map is empty, set the property to null. + delete this._sourcesContents[util.toSetString(source)]; + if (Object.keys(this._sourcesContents).length === 0) { + this._sourcesContents = null; + } + } + }; + + /** + * Applies the mappings of a sub-source-map for a specific source file to the + * source map being generated. Each mapping to the supplied source file is + * rewritten using the supplied source map. Note: The resolution for the + * resulting mappings is the minimium of this map and the supplied map. + * + * @param aSourceMapConsumer The source map to be applied. + * @param aSourceFile Optional. The filename of the source file. + * If omitted, SourceMapConsumer's file property will be used. + * @param aSourceMapPath Optional. The dirname of the path to the source map + * to be applied. If relative, it is relative to the SourceMapConsumer. + * This parameter is needed when the two source maps aren't in the same + * directory, and the source map to be applied contains relative source + * paths. If so, those relative source paths need to be rewritten + * relative to the SourceMapGenerator. + */ + SourceMapGenerator$1.prototype.applySourceMap = + function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile, aSourceMapPath) { + var sourceFile = aSourceFile; + // If aSourceFile is omitted, we will use the file property of the SourceMap + if (aSourceFile == null) { + if (aSourceMapConsumer.file == null) { + throw new Error( + 'SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, ' + + 'or the source map\'s "file" property. Both were omitted.' + ); + } + sourceFile = aSourceMapConsumer.file; + } + var sourceRoot = this._sourceRoot; + // Make "sourceFile" relative if an absolute Url is passed. + if (sourceRoot != null) { + sourceFile = util.relative(sourceRoot, sourceFile); + } + // Applying the SourceMap can add and remove items from the sources and + // the names array. + var newSources = new ArraySet(); + var newNames = new ArraySet(); + + // Find mappings for the "sourceFile" + this._mappings.unsortedForEach(function (mapping) { + if (mapping.source === sourceFile && mapping.originalLine != null) { + // Check if it can be mapped by the source map, then update the mapping. + var original = aSourceMapConsumer.originalPositionFor({ + line: mapping.originalLine, + column: mapping.originalColumn + }); + if (original.source != null) { + // Copy mapping + mapping.source = original.source; + if (aSourceMapPath != null) { + mapping.source = util.join(aSourceMapPath, mapping.source); + } + if (sourceRoot != null) { + mapping.source = util.relative(sourceRoot, mapping.source); + } + mapping.originalLine = original.line; + mapping.originalColumn = original.column; + if (original.name != null) { + mapping.name = original.name; + } + } + } + + var source = mapping.source; + if (source != null && !newSources.has(source)) { + newSources.add(source); + } + + var name = mapping.name; + if (name != null && !newNames.has(name)) { + newNames.add(name); + } + + }, this); + this._sources = newSources; + this._names = newNames; + + // Copy sourcesContents of applied map. + aSourceMapConsumer.sources.forEach(function (sourceFile) { + var content = aSourceMapConsumer.sourceContentFor(sourceFile); + if (content != null) { + if (aSourceMapPath != null) { + sourceFile = util.join(aSourceMapPath, sourceFile); + } + if (sourceRoot != null) { + sourceFile = util.relative(sourceRoot, sourceFile); + } + this.setSourceContent(sourceFile, content); + } + }, this); + }; + + /** + * A mapping can have one of the three levels of data: + * + * 1. Just the generated position. + * 2. The Generated position, original position, and original source. + * 3. Generated and original position, original source, as well as a name + * token. + * + * To maintain consistency, we validate that any new mapping being added falls + * in to one of these categories. + */ + SourceMapGenerator$1.prototype._validateMapping = + function SourceMapGenerator_validateMapping(aGenerated, aOriginal, aSource, + aName) { + // When aOriginal is truthy but has empty values for .line and .column, + // it is most likely a programmer error. In this case we throw a very + // specific error message to try to guide them the right way. + // For example: https://github.com/Polymer/polymer-bundler/pull/519 + if (aOriginal && typeof aOriginal.line !== 'number' && typeof aOriginal.column !== 'number') { + throw new Error( + 'original.line and original.column are not numbers -- you probably meant to omit ' + + 'the original mapping entirely and only map the generated position. If so, pass ' + + 'null for the original mapping instead of an object with empty or null values.' + ); + } + + if (aGenerated && 'line' in aGenerated && 'column' in aGenerated + && aGenerated.line > 0 && aGenerated.column >= 0 + && !aOriginal && !aSource && !aName) { + // Case 1. + return; + } + else if (aGenerated && 'line' in aGenerated && 'column' in aGenerated + && aOriginal && 'line' in aOriginal && 'column' in aOriginal + && aGenerated.line > 0 && aGenerated.column >= 0 + && aOriginal.line > 0 && aOriginal.column >= 0 + && aSource) { + // Cases 2 and 3. + return; + } + else { + throw new Error('Invalid mapping: ' + JSON.stringify({ + generated: aGenerated, + source: aSource, + original: aOriginal, + name: aName + })); + } + }; + + /** + * Serialize the accumulated mappings in to the stream of base 64 VLQs + * specified by the source map format. + */ + SourceMapGenerator$1.prototype._serializeMappings = + function SourceMapGenerator_serializeMappings() { + var previousGeneratedColumn = 0; + var previousGeneratedLine = 1; + var previousOriginalColumn = 0; + var previousOriginalLine = 0; + var previousName = 0; + var previousSource = 0; + var result = ''; + var next; + var mapping; + var nameIdx; + var sourceIdx; + + var mappings = this._mappings.toArray(); + for (var i = 0, len = mappings.length; i < len; i++) { + mapping = mappings[i]; + next = ''; + + if (mapping.generatedLine !== previousGeneratedLine) { + previousGeneratedColumn = 0; + while (mapping.generatedLine !== previousGeneratedLine) { + next += ';'; + previousGeneratedLine++; + } + } + else { + if (i > 0) { + if (!util.compareByGeneratedPositionsInflated(mapping, mappings[i - 1])) { + continue; + } + next += ','; + } + } + + next += base64VLQ.encode(mapping.generatedColumn + - previousGeneratedColumn); + previousGeneratedColumn = mapping.generatedColumn; + + if (mapping.source != null) { + sourceIdx = this._sources.indexOf(mapping.source); + next += base64VLQ.encode(sourceIdx - previousSource); + previousSource = sourceIdx; + + // lines are stored 0-based in SourceMap spec version 3 + next += base64VLQ.encode(mapping.originalLine - 1 + - previousOriginalLine); + previousOriginalLine = mapping.originalLine - 1; + + next += base64VLQ.encode(mapping.originalColumn + - previousOriginalColumn); + previousOriginalColumn = mapping.originalColumn; + + if (mapping.name != null) { + nameIdx = this._names.indexOf(mapping.name); + next += base64VLQ.encode(nameIdx - previousName); + previousName = nameIdx; + } + } + + result += next; + } + + return result; + }; + + SourceMapGenerator$1.prototype._generateSourcesContent = + function SourceMapGenerator_generateSourcesContent(aSources, aSourceRoot) { + return aSources.map(function (source) { + if (!this._sourcesContents) { + return null; + } + if (aSourceRoot != null) { + source = util.relative(aSourceRoot, source); + } + var key = util.toSetString(source); + return Object.prototype.hasOwnProperty.call(this._sourcesContents, key) + ? this._sourcesContents[key] + : null; + }, this); + }; + + /** + * Externalize the source map. + */ + SourceMapGenerator$1.prototype.toJSON = + function SourceMapGenerator_toJSON() { + var map = { + version: this._version, + sources: this._sources.toArray(), + names: this._names.toArray(), + mappings: this._serializeMappings() + }; + if (this._file != null) { + map.file = this._file; + } + if (this._sourceRoot != null) { + map.sourceRoot = this._sourceRoot; + } + if (this._sourcesContents) { + map.sourcesContent = this._generateSourcesContent(map.sources, map.sourceRoot); + } + + return map; + }; + + /** + * Render the source map being generated to a string. + */ + SourceMapGenerator$1.prototype.toString = + function SourceMapGenerator_toString() { + return JSON.stringify(this.toJSON()); + }; + + sourceMapGenerator.SourceMapGenerator = SourceMapGenerator$1; + + var SourceMapGenerator = sourceMapGenerator.SourceMapGenerator; + var trackNodes = { + Atrule: true, + Selector: true, + Declaration: true + }; + + var sourceMap$1 = function generateSourceMap(handlers) { + var map = new SourceMapGenerator(); + var line = 1; + var column = 0; + var generated = { + line: 1, + column: 0 + }; + var original = { + line: 0, // should be zero to add first mapping + column: 0 + }; + var sourceMappingActive = false; + var activatedGenerated = { + line: 1, + column: 0 + }; + var activatedMapping = { + generated: activatedGenerated + }; + + var handlersNode = handlers.node; + handlers.node = function(node) { + if (node.loc && node.loc.start && trackNodes.hasOwnProperty(node.type)) { + var nodeLine = node.loc.start.line; + var nodeColumn = node.loc.start.column - 1; + + if (original.line !== nodeLine || + original.column !== nodeColumn) { + original.line = nodeLine; + original.column = nodeColumn; + + generated.line = line; + generated.column = column; + + if (sourceMappingActive) { + sourceMappingActive = false; + if (generated.line !== activatedGenerated.line || + generated.column !== activatedGenerated.column) { + map.addMapping(activatedMapping); + } + } + + sourceMappingActive = true; + map.addMapping({ + source: node.loc.source, + original: original, + generated: generated + }); + } + } + + handlersNode.call(this, node); + + if (sourceMappingActive && trackNodes.hasOwnProperty(node.type)) { + activatedGenerated.line = line; + activatedGenerated.column = column; + } + }; + + var handlersChunk = handlers.chunk; + handlers.chunk = function(chunk) { + for (var i = 0; i < chunk.length; i++) { + if (chunk.charCodeAt(i) === 10) { // \n + line++; + column = 0; + } else { + column++; + } + } + + handlersChunk(chunk); + }; + + var handlersResult = handlers.result; + handlers.result = function() { + if (sourceMappingActive) { + map.addMapping(activatedMapping); + } + + return { + css: handlersResult(), + map: map + }; + }; + + return handlers; + }; + + var sourceMap = sourceMap$1; + var hasOwnProperty$4 = Object.prototype.hasOwnProperty; + + function processChildren(node, delimeter) { + var list = node.children; + var prev = null; + + if (typeof delimeter !== 'function') { + list.forEach(this.node, this); + } else { + list.forEach(function(node) { + if (prev !== null) { + delimeter.call(this, prev); + } + + this.node(node); + prev = node; + }, this); + } + } + + var create$2 = function createGenerator(config) { + function processNode(node) { + if (hasOwnProperty$4.call(types, node.type)) { + types[node.type].call(this, node); + } else { + throw new Error('Unknown node type: ' + node.type); + } + } + + var types = {}; + + if (config.node) { + for (var name in config.node) { + types[name] = config.node[name].generate; + } + } + + return function(node, options) { + var buffer = ''; + var handlers = { + children: processChildren, + node: processNode, + chunk: function(chunk) { + buffer += chunk; + }, + result: function() { + return buffer; + } + }; + + if (options) { + if (typeof options.decorator === 'function') { + handlers = options.decorator(handlers); + } + + if (options.sourceMap) { + handlers = sourceMap(handlers); + } + } + + handlers.node(node); + + return handlers.result(); + }; + }; + + var List$2 = List_1; + + var create$1 = function createConvertors(walk) { + return { + fromPlainObject: function(ast) { + walk(ast, { + enter: function(node) { + if (node.children && node.children instanceof List$2 === false) { + node.children = new List$2().fromArray(node.children); + } + } + }); + + return ast; + }, + toPlainObject: function(ast) { + walk(ast, { + leave: function(node) { + if (node.children && node.children instanceof List$2) { + node.children = node.children.toArray(); + } + } + }); + + return ast; + } + }; + }; + + var hasOwnProperty$3 = Object.prototype.hasOwnProperty; + var noop = function() {}; + + function ensureFunction(value) { + return typeof value === 'function' ? value : noop; + } + + function invokeForType(fn, type) { + return function(node, item, list) { + if (node.type === type) { + fn.call(this, node, item, list); + } + }; + } + + function getWalkersFromStructure(name, nodeType) { + var structure = nodeType.structure; + var walkers = []; + + for (var key in structure) { + if (hasOwnProperty$3.call(structure, key) === false) { + continue; + } + + var fieldTypes = structure[key]; + var walker = { + name: key, + type: false, + nullable: false + }; + + if (!Array.isArray(structure[key])) { + fieldTypes = [structure[key]]; + } + + for (var i = 0; i < fieldTypes.length; i++) { + var fieldType = fieldTypes[i]; + if (fieldType === null) { + walker.nullable = true; + } else if (typeof fieldType === 'string') { + walker.type = 'node'; + } else if (Array.isArray(fieldType)) { + walker.type = 'list'; + } + } + + if (walker.type) { + walkers.push(walker); + } + } + + if (walkers.length) { + return { + context: nodeType.walkContext, + fields: walkers + }; + } + + return null; + } + + function getTypesFromConfig(config) { + var types = {}; + + for (var name in config.node) { + if (hasOwnProperty$3.call(config.node, name)) { + var nodeType = config.node[name]; + + if (!nodeType.structure) { + throw new Error('Missed `structure` field in `' + name + '` node type definition'); + } + + types[name] = getWalkersFromStructure(name, nodeType); + } + } + + return types; + } + + function createTypeIterator(config, reverse) { + var fields = config.fields.slice(); + var contextName = config.context; + var useContext = typeof contextName === 'string'; + + if (reverse) { + fields.reverse(); + } + + return function(node, context, walk, walkReducer) { + var prevContextValue; + + if (useContext) { + prevContextValue = context[contextName]; + context[contextName] = node; + } + + for (var i = 0; i < fields.length; i++) { + var field = fields[i]; + var ref = node[field.name]; + + if (!field.nullable || ref) { + if (field.type === 'list') { + var breakWalk = reverse + ? ref.reduceRight(walkReducer, false) + : ref.reduce(walkReducer, false); + + if (breakWalk) { + return true; + } + } else if (walk(ref)) { + return true; + } + } + } + + if (useContext) { + context[contextName] = prevContextValue; + } + }; + } + + function createFastTraveralMap(iterators) { + return { + Atrule: { + StyleSheet: iterators.StyleSheet, + Atrule: iterators.Atrule, + Rule: iterators.Rule, + Block: iterators.Block + }, + Rule: { + StyleSheet: iterators.StyleSheet, + Atrule: iterators.Atrule, + Rule: iterators.Rule, + Block: iterators.Block + }, + Declaration: { + StyleSheet: iterators.StyleSheet, + Atrule: iterators.Atrule, + Rule: iterators.Rule, + Block: iterators.Block, + DeclarationList: iterators.DeclarationList + } + }; + } + + var create = function createWalker(config) { + var types = getTypesFromConfig(config); + var iteratorsNatural = {}; + var iteratorsReverse = {}; + var breakWalk = Symbol('break-walk'); + var skipNode = Symbol('skip-node'); + + for (var name in types) { + if (hasOwnProperty$3.call(types, name) && types[name] !== null) { + iteratorsNatural[name] = createTypeIterator(types[name], false); + iteratorsReverse[name] = createTypeIterator(types[name], true); + } + } + + var fastTraversalIteratorsNatural = createFastTraveralMap(iteratorsNatural); + var fastTraversalIteratorsReverse = createFastTraveralMap(iteratorsReverse); + + var walk = function(root, options) { + function walkNode(node, item, list) { + var enterRet = enter.call(context, node, item, list); + + if (enterRet === breakWalk) { + debugger; + return true; + } + + if (enterRet === skipNode) { + return false; + } + + if (iterators.hasOwnProperty(node.type)) { + if (iterators[node.type](node, context, walkNode, walkReducer)) { + return true; + } + } + + if (leave.call(context, node, item, list) === breakWalk) { + return true; + } + + return false; + } + + var walkReducer = (ret, data, item, list) => ret || walkNode(data, item, list); + var enter = noop; + var leave = noop; + var iterators = iteratorsNatural; + var context = { + break: breakWalk, + skip: skipNode, + + root: root, + stylesheet: null, + atrule: null, + atrulePrelude: null, + rule: null, + selector: null, + block: null, + declaration: null, + function: null + }; + + if (typeof options === 'function') { + enter = options; + } else if (options) { + enter = ensureFunction(options.enter); + leave = ensureFunction(options.leave); + + if (options.reverse) { + iterators = iteratorsReverse; + } + + if (options.visit) { + if (fastTraversalIteratorsNatural.hasOwnProperty(options.visit)) { + iterators = options.reverse + ? fastTraversalIteratorsReverse[options.visit] + : fastTraversalIteratorsNatural[options.visit]; + } else if (!types.hasOwnProperty(options.visit)) { + throw new Error('Bad value `' + options.visit + '` for `visit` option (should be: ' + Object.keys(types).join(', ') + ')'); + } + + enter = invokeForType(enter, options.visit); + leave = invokeForType(leave, options.visit); + } + } + + if (enter === noop && leave === noop) { + throw new Error('Neither `enter` nor `leave` walker handler is set or both aren\'t a function'); + } + + walkNode(root); + }; + + walk.break = breakWalk; + walk.skip = skipNode; + + walk.find = function(ast, fn) { + var found = null; + + walk(ast, function(node, item, list) { + if (fn.call(this, node, item, list)) { + found = node; + return breakWalk; + } + }); + + return found; + }; + + walk.findLast = function(ast, fn) { + var found = null; + + walk(ast, { + reverse: true, + enter: function(node, item, list) { + if (fn.call(this, node, item, list)) { + found = node; + return breakWalk; + } + } + }); + + return found; + }; + + walk.findAll = function(ast, fn) { + var found = []; + + walk(ast, function(node, item, list) { + if (fn.call(this, node, item, list)) { + found.push(node); + } + }); + + return found; + }; + + return walk; + }; + + var List$1 = List_1; + + var clone$1 = function clone(node) { + var result = {}; + + for (var key in node) { + var value = node[key]; + + if (value) { + if (Array.isArray(value) || value instanceof List$1) { + value = value.map(clone); + } else if (value.constructor === Object) { + value = clone(value); + } + } + + result[key] = value; + } + + return result; + }; + + const hasOwnProperty$2 = Object.prototype.hasOwnProperty; + const shape$1 = { + generic: true, + types: appendOrAssign, + atrules: { + prelude: appendOrAssignOrNull, + descriptors: appendOrAssignOrNull + }, + properties: appendOrAssign, + parseContext: assign, + scope: deepAssign, + atrule: ['parse'], + pseudo: ['parse'], + node: ['name', 'structure', 'parse', 'generate', 'walkContext'] + }; + + function isObject$2(value) { + return value && value.constructor === Object; + } + + function copy(value) { + return isObject$2(value) + ? Object.assign({}, value) + : value; + } + + function assign(dest, src) { + return Object.assign(dest, src); + } + + function deepAssign(dest, src) { + for (const key in src) { + if (hasOwnProperty$2.call(src, key)) { + if (isObject$2(dest[key])) { + deepAssign(dest[key], copy(src[key])); + } else { + dest[key] = copy(src[key]); + } + } + } + + return dest; + } + + function append(a, b) { + if (typeof b === 'string' && /^\s*\|/.test(b)) { + return typeof a === 'string' + ? a + b + : b.replace(/^\s*\|\s*/, ''); + } + + return b || null; + } + + function appendOrAssign(a, b) { + if (typeof b === 'string') { + return append(a, b); + } + + const result = Object.assign({}, a); + for (let key in b) { + if (hasOwnProperty$2.call(b, key)) { + result[key] = append(hasOwnProperty$2.call(a, key) ? a[key] : undefined, b[key]); + } + } + + return result; + } + + function appendOrAssignOrNull(a, b) { + const result = appendOrAssign(a, b); + + return !isObject$2(result) || Object.keys(result).length + ? result + : null; + } + + function mix$1(dest, src, shape) { + for (const key in shape) { + if (hasOwnProperty$2.call(shape, key) === false) { + continue; + } + + if (shape[key] === true) { + if (key in src) { + if (hasOwnProperty$2.call(src, key)) { + dest[key] = copy(src[key]); + } + } + } else if (shape[key]) { + if (typeof shape[key] === 'function') { + const fn = shape[key]; + dest[key] = fn({}, dest[key]); + dest[key] = fn(dest[key] || {}, src[key]); + } else if (isObject$2(shape[key])) { + const result = {}; + + for (let name in dest[key]) { + result[name] = mix$1({}, dest[key][name], shape[key]); + } + + for (let name in src[key]) { + result[name] = mix$1(result[name] || {}, src[key][name], shape[key]); + } + + dest[key] = result; + } else if (Array.isArray(shape[key])) { + const res = {}; + const innerShape = shape[key].reduce(function(s, k) { + s[k] = true; + return s; + }, {}); + + for (const [name, value] of Object.entries(dest[key] || {})) { + res[name] = {}; + if (value) { + mix$1(res[name], value, innerShape); + } + } + + for (const name in src[key]) { + if (hasOwnProperty$2.call(src[key], name)) { + if (!res[name]) { + res[name] = {}; + } + + if (src[key] && src[key][name]) { + mix$1(res[name], src[key][name], innerShape); + } + } + } + + dest[key] = res; + } + } + } + return dest; + } + + var mix_1 = (dest, src) => mix$1(dest, src, shape$1); + + var List = List_1; + var SyntaxError$1 = _SyntaxError$1; + var TokenStream = TokenStream_1; + var Lexer = Lexer_1; + var definitionSyntax = definitionSyntax$1; + var tokenize = tokenizer$3; + var createParser = create$3; + var createGenerator = create$2; + var createConvertor = create$1; + var createWalker = create; + var clone = clone$1; + var names = names$2; + var mix = mix_1; + + function createSyntax(config) { + var parse = createParser(config); + var walk = createWalker(config); + var generate = createGenerator(config); + var convert = createConvertor(walk); + + var syntax = { + List: List, + SyntaxError: SyntaxError$1, + TokenStream: TokenStream, + Lexer: Lexer, + + vendorPrefix: names.vendorPrefix, + keyword: names.keyword, + property: names.property, + isCustomProperty: names.isCustomProperty, + + definitionSyntax: definitionSyntax, + lexer: null, + createLexer: function(config) { + return new Lexer(config, syntax, syntax.lexer.structure); + }, + + tokenize: tokenize, + parse: parse, + walk: walk, + generate: generate, + + find: walk.find, + findLast: walk.findLast, + findAll: walk.findAll, + + clone: clone, + fromPlainObject: convert.fromPlainObject, + toPlainObject: convert.toPlainObject, + + createSyntax: function(config) { + return createSyntax(mix({}, config)); + }, + fork: function(extension) { + var base = mix({}, config); // copy of config + return createSyntax( + typeof extension === 'function' + ? extension(base, Object.assign) + : mix(base, extension) + ); + } + }; + + syntax.lexer = new Lexer({ + generic: true, + types: config.types, + atrules: config.atrules, + properties: config.properties, + node: config.node + }, syntax); + + return syntax; + } + create$4.create = function(config) { + return createSyntax(mix({}, config)); + }; + + var require$$0 = { + "@charset": { + syntax: "@charset \"\";", + groups: [ + "CSS Charsets" + ], + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/@charset" + }, + "@counter-style": { + syntax: "@counter-style {\n [ system: ; ] ||\n [ symbols: ; ] ||\n [ additive-symbols: ; ] ||\n [ negative: ; ] ||\n [ prefix: ; ] ||\n [ suffix: ; ] ||\n [ range: ; ] ||\n [ pad: ; ] ||\n [ speak-as: ; ] ||\n [ fallback: ; ]\n}", + interfaces: [ + "CSSCounterStyleRule" + ], + groups: [ + "CSS Counter Styles" + ], + descriptors: { + "additive-symbols": { + syntax: "[ && ]#", + media: "all", + initial: "n/a (required)", + percentages: "no", + computed: "asSpecified", + order: "orderOfAppearance", + status: "standard" + }, + fallback: { + syntax: "", + media: "all", + initial: "decimal", + percentages: "no", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard" + }, + negative: { + syntax: " ?", + media: "all", + initial: "\"-\" hyphen-minus", + percentages: "no", + computed: "asSpecified", + order: "orderOfAppearance", + status: "standard" + }, + pad: { + syntax: " && ", + media: "all", + initial: "0 \"\"", + percentages: "no", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard" + }, + prefix: { + syntax: "", + media: "all", + initial: "\"\"", + percentages: "no", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard" + }, + range: { + syntax: "[ [ | infinite ]{2} ]# | auto", + media: "all", + initial: "auto", + percentages: "no", + computed: "asSpecified", + order: "orderOfAppearance", + status: "standard" + }, + "speak-as": { + syntax: "auto | bullets | numbers | words | spell-out | ", + media: "all", + initial: "auto", + percentages: "no", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard" + }, + suffix: { + syntax: "", + media: "all", + initial: "\". \"", + percentages: "no", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard" + }, + symbols: { + syntax: "+", + media: "all", + initial: "n/a (required)", + percentages: "no", + computed: "asSpecified", + order: "orderOfAppearance", + status: "standard" + }, + system: { + syntax: "cyclic | numeric | alphabetic | symbolic | additive | [ fixed ? ] | [ extends ]", + media: "all", + initial: "symbolic", + percentages: "no", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard" + } + }, + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/@counter-style" + }, + "@document": { + syntax: "@document [ | url-prefix() | domain() | media-document() | regexp() ]# {\n \n}", + interfaces: [ + "CSSGroupingRule", + "CSSConditionRule" + ], + groups: [ + "CSS Conditional Rules" + ], + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/@document" + }, + "@font-face": { + syntax: "@font-face {\n [ font-family: ; ] ||\n [ src: ; ] ||\n [ unicode-range: ; ] ||\n [ font-variant: ; ] ||\n [ font-feature-settings: ; ] ||\n [ font-variation-settings: ; ] ||\n [ font-stretch: ; ] ||\n [ font-weight: ; ] ||\n [ font-style: ; ]\n}", + interfaces: [ + "CSSFontFaceRule" + ], + groups: [ + "CSS Fonts" + ], + descriptors: { + "font-display": { + syntax: "[ auto | block | swap | fallback | optional ]", + media: "visual", + percentages: "no", + initial: "auto", + computed: "asSpecified", + order: "uniqueOrder", + status: "experimental" + }, + "font-family": { + syntax: "", + media: "all", + initial: "n/a (required)", + percentages: "no", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard" + }, + "font-feature-settings": { + syntax: "normal | #", + media: "all", + initial: "normal", + percentages: "no", + computed: "asSpecified", + order: "orderOfAppearance", + status: "standard" + }, + "font-variation-settings": { + syntax: "normal | [ ]#", + media: "all", + initial: "normal", + percentages: "no", + computed: "asSpecified", + order: "orderOfAppearance", + status: "standard" + }, + "font-stretch": { + syntax: "{1,2}", + media: "all", + initial: "normal", + percentages: "no", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard" + }, + "font-style": { + syntax: "normal | italic | oblique {0,2}", + media: "all", + initial: "normal", + percentages: "no", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard" + }, + "font-weight": { + syntax: "{1,2}", + media: "all", + initial: "normal", + percentages: "no", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard" + }, + "font-variant": { + syntax: "normal | none | [ || || || || stylistic() || historical-forms || styleset(#) || character-variant(#) || swash() || ornaments() || annotation() || [ small-caps | all-small-caps | petite-caps | all-petite-caps | unicase | titling-caps ] || || || || ordinal || slashed-zero || || || ruby ]", + media: "all", + initial: "normal", + percentages: "no", + computed: "asSpecified", + order: "orderOfAppearance", + status: "standard" + }, + src: { + syntax: "[ [ format( # ) ]? | local( ) ]#", + media: "all", + initial: "n/a (required)", + percentages: "no", + computed: "asSpecified", + order: "orderOfAppearance", + status: "standard" + }, + "unicode-range": { + syntax: "#", + media: "all", + initial: "U+0-10FFFF", + percentages: "no", + computed: "asSpecified", + order: "orderOfAppearance", + status: "standard" + } + }, + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/@font-face" + }, + "@font-feature-values": { + syntax: "@font-feature-values # {\n \n}", + interfaces: [ + "CSSFontFeatureValuesRule" + ], + groups: [ + "CSS Fonts" + ], + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/@font-feature-values" + }, + "@import": { + syntax: "@import [ | ] [ ]?;", + groups: [ + "Media Queries" + ], + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/@import" + }, + "@keyframes": { + syntax: "@keyframes {\n \n}", + interfaces: [ + "CSSKeyframeRule", + "CSSKeyframesRule" + ], + groups: [ + "CSS Animations" + ], + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/@keyframes" + }, + "@media": { + syntax: "@media {\n \n}", + interfaces: [ + "CSSGroupingRule", + "CSSConditionRule", + "CSSMediaRule", + "CSSCustomMediaRule" + ], + groups: [ + "CSS Conditional Rules", + "Media Queries" + ], + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/@media" + }, + "@namespace": { + syntax: "@namespace ? [ | ];", + groups: [ + "CSS Namespaces" + ], + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/@namespace" + }, + "@page": { + syntax: "@page {\n \n}", + interfaces: [ + "CSSPageRule" + ], + groups: [ + "CSS Pages" + ], + descriptors: { + bleed: { + syntax: "auto | ", + media: [ + "visual", + "paged" + ], + initial: "auto", + percentages: "no", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard" + }, + marks: { + syntax: "none | [ crop || cross ]", + media: [ + "visual", + "paged" + ], + initial: "none", + percentages: "no", + computed: "asSpecified", + order: "orderOfAppearance", + status: "standard" + }, + size: { + syntax: "{1,2} | auto | [ || [ portrait | landscape ] ]", + media: [ + "visual", + "paged" + ], + initial: "auto", + percentages: "no", + computed: "asSpecifiedRelativeToAbsoluteLengths", + order: "orderOfAppearance", + status: "standard" + } + }, + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/@page" + }, + "@property": { + syntax: "@property {\n \n}", + interfaces: [ + "CSS", + "CSSPropertyRule" + ], + groups: [ + "CSS Houdini" + ], + descriptors: { + syntax: { + syntax: "", + media: "all", + percentages: "no", + initial: "n/a (required)", + computed: "asSpecified", + order: "uniqueOrder", + status: "experimental" + }, + inherits: { + syntax: "true | false", + media: "all", + percentages: "no", + initial: "auto", + computed: "asSpecified", + order: "uniqueOrder", + status: "experimental" + }, + "initial-value": { + syntax: "", + media: "all", + initial: "n/a (required)", + percentages: "no", + computed: "asSpecified", + order: "uniqueOrder", + status: "experimental" + } + }, + status: "experimental", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/@property" + }, + "@supports": { + syntax: "@supports {\n \n}", + interfaces: [ + "CSSGroupingRule", + "CSSConditionRule", + "CSSSupportsRule" + ], + groups: [ + "CSS Conditional Rules" + ], + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/@supports" + }, + "@viewport": { + syntax: "@viewport {\n \n}", + interfaces: [ + "CSSViewportRule" + ], + groups: [ + "CSS Device Adaptation" + ], + descriptors: { + height: { + syntax: "{1,2}", + media: [ + "visual", + "continuous" + ], + initial: [ + "min-height", + "max-height" + ], + percentages: [ + "min-height", + "max-height" + ], + computed: [ + "min-height", + "max-height" + ], + order: "orderOfAppearance", + status: "standard" + }, + "max-height": { + syntax: "", + media: [ + "visual", + "continuous" + ], + initial: "auto", + percentages: "referToHeightOfInitialViewport", + computed: "lengthAbsolutePercentageAsSpecifiedOtherwiseAuto", + order: "uniqueOrder", + status: "standard" + }, + "max-width": { + syntax: "", + media: [ + "visual", + "continuous" + ], + initial: "auto", + percentages: "referToWidthOfInitialViewport", + computed: "lengthAbsolutePercentageAsSpecifiedOtherwiseAuto", + order: "uniqueOrder", + status: "standard" + }, + "max-zoom": { + syntax: "auto | | ", + media: [ + "visual", + "continuous" + ], + initial: "auto", + percentages: "the zoom factor itself", + computed: "autoNonNegativeOrPercentage", + order: "uniqueOrder", + status: "standard" + }, + "min-height": { + syntax: "", + media: [ + "visual", + "continuous" + ], + initial: "auto", + percentages: "referToHeightOfInitialViewport", + computed: "lengthAbsolutePercentageAsSpecifiedOtherwiseAuto", + order: "uniqueOrder", + status: "standard" + }, + "min-width": { + syntax: "", + media: [ + "visual", + "continuous" + ], + initial: "auto", + percentages: "referToWidthOfInitialViewport", + computed: "lengthAbsolutePercentageAsSpecifiedOtherwiseAuto", + order: "uniqueOrder", + status: "standard" + }, + "min-zoom": { + syntax: "auto | | ", + media: [ + "visual", + "continuous" + ], + initial: "auto", + percentages: "the zoom factor itself", + computed: "autoNonNegativeOrPercentage", + order: "uniqueOrder", + status: "standard" + }, + orientation: { + syntax: "auto | portrait | landscape", + media: [ + "visual", + "continuous" + ], + initial: "auto", + percentages: "referToSizeOfBoundingBox", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard" + }, + "user-zoom": { + syntax: "zoom | fixed", + media: [ + "visual", + "continuous" + ], + initial: "zoom", + percentages: "referToSizeOfBoundingBox", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard" + }, + "viewport-fit": { + syntax: "auto | contain | cover", + media: [ + "visual", + "continuous" + ], + initial: "auto", + percentages: "no", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard" + }, + width: { + syntax: "{1,2}", + media: [ + "visual", + "continuous" + ], + initial: [ + "min-width", + "max-width" + ], + percentages: [ + "min-width", + "max-width" + ], + computed: [ + "min-width", + "max-width" + ], + order: "orderOfAppearance", + status: "standard" + }, + zoom: { + syntax: "auto | | ", + media: [ + "visual", + "continuous" + ], + initial: "auto", + percentages: "the zoom factor itself", + computed: "autoNonNegativeOrPercentage", + order: "uniqueOrder", + status: "standard" + } + }, + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/@viewport" + } + }; + + var all = { + syntax: "initial | inherit | unset | revert", + media: "noPracticalMedia", + inherited: false, + animationType: "eachOfShorthandPropertiesExceptUnicodeBiDiAndDirection", + percentages: "no", + groups: [ + "CSS Miscellaneous" + ], + initial: "noPracticalInitialValue", + appliesto: "allElements", + computed: "asSpecifiedAppliesToEachProperty", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/all" + }; + var animation = { + syntax: "#", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Animations" + ], + initial: [ + "animation-name", + "animation-duration", + "animation-timing-function", + "animation-delay", + "animation-iteration-count", + "animation-direction", + "animation-fill-mode", + "animation-play-state" + ], + appliesto: "allElementsAndPseudos", + computed: [ + "animation-name", + "animation-duration", + "animation-timing-function", + "animation-delay", + "animation-direction", + "animation-iteration-count", + "animation-fill-mode", + "animation-play-state" + ], + order: "orderOfAppearance", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/animation" + }; + var appearance = { + syntax: "none | auto | textfield | menulist-button | ", + media: "all", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Basic User Interface" + ], + initial: "auto", + appliesto: "allElements", + computed: "asSpecified", + order: "perGrammar", + status: "experimental", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/appearance" + }; + var azimuth = { + syntax: " | [ [ left-side | far-left | left | center-left | center | center-right | right | far-right | right-side ] || behind ] | leftwards | rightwards", + media: "aural", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Speech" + ], + initial: "center", + appliesto: "allElements", + computed: "normalizedAngle", + order: "orderOfAppearance", + status: "obsolete", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/azimuth" + }; + var background = { + syntax: "[ , ]* ", + media: "visual", + inherited: false, + animationType: [ + "background-color", + "background-image", + "background-clip", + "background-position", + "background-size", + "background-repeat", + "background-attachment" + ], + percentages: [ + "background-position", + "background-size" + ], + groups: [ + "CSS Backgrounds and Borders" + ], + initial: [ + "background-image", + "background-position", + "background-size", + "background-repeat", + "background-origin", + "background-clip", + "background-attachment", + "background-color" + ], + appliesto: "allElements", + computed: [ + "background-image", + "background-position", + "background-size", + "background-repeat", + "background-origin", + "background-clip", + "background-attachment", + "background-color" + ], + order: "orderOfAppearance", + alsoAppliesTo: [ + "::first-letter", + "::first-line", + "::placeholder" + ], + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/background" + }; + var border = { + syntax: " || || ", + media: "visual", + inherited: false, + animationType: [ + "border-color", + "border-style", + "border-width" + ], + percentages: "no", + groups: [ + "CSS Backgrounds and Borders" + ], + initial: [ + "border-width", + "border-style", + "border-color" + ], + appliesto: "allElements", + computed: [ + "border-width", + "border-style", + "border-color" + ], + order: "orderOfAppearance", + alsoAppliesTo: [ + "::first-letter" + ], + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/border" + }; + var bottom = { + syntax: " | | auto", + media: "visual", + inherited: false, + animationType: "lpc", + percentages: "referToContainingBlockHeight", + groups: [ + "CSS Positioning" + ], + initial: "auto", + appliesto: "positionedElements", + computed: "lengthAbsolutePercentageAsSpecifiedOtherwiseAuto", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/bottom" + }; + var clear = { + syntax: "none | left | right | both | inline-start | inline-end", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Positioning" + ], + initial: "none", + appliesto: "blockLevelElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/clear" + }; + var clip = { + syntax: " | auto", + media: "visual", + inherited: false, + animationType: "rectangle", + percentages: "no", + groups: [ + "CSS Masking" + ], + initial: "auto", + appliesto: "absolutelyPositionedElements", + computed: "autoOrRectangle", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/clip" + }; + var color$1 = { + syntax: "", + media: "visual", + inherited: true, + animationType: "color", + percentages: "no", + groups: [ + "CSS Color" + ], + initial: "variesFromBrowserToBrowser", + appliesto: "allElements", + computed: "translucentValuesRGBAOtherwiseRGB", + order: "uniqueOrder", + alsoAppliesTo: [ + "::first-letter", + "::first-line", + "::placeholder" + ], + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/color" + }; + var columns = { + syntax: "<'column-width'> || <'column-count'>", + media: "visual", + inherited: false, + animationType: [ + "column-width", + "column-count" + ], + percentages: "no", + groups: [ + "CSS Columns" + ], + initial: [ + "column-width", + "column-count" + ], + appliesto: "blockContainersExceptTableWrappers", + computed: [ + "column-width", + "column-count" + ], + order: "perGrammar", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/columns" + }; + var contain = { + syntax: "none | strict | content | [ size || layout || style || paint ]", + media: "all", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Containment" + ], + initial: "none", + appliesto: "allElements", + computed: "asSpecified", + order: "perGrammar", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/contain" + }; + var content = { + syntax: "normal | none | [ | ] [/ ]?", + media: "all", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Generated Content" + ], + initial: "normal", + appliesto: "beforeAndAfterPseudos", + computed: "normalOnElementsForPseudosNoneAbsoluteURIStringOrAsSpecified", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/content" + }; + var cursor = { + syntax: "[ [ [ ]? , ]* [ auto | default | none | context-menu | help | pointer | progress | wait | cell | crosshair | text | vertical-text | alias | copy | move | no-drop | not-allowed | e-resize | n-resize | ne-resize | nw-resize | s-resize | se-resize | sw-resize | w-resize | ew-resize | ns-resize | nesw-resize | nwse-resize | col-resize | row-resize | all-scroll | zoom-in | zoom-out | grab | grabbing ] ]", + media: [ + "visual", + "interactive" + ], + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Basic User Interface" + ], + initial: "auto", + appliesto: "allElements", + computed: "asSpecifiedURLsAbsolute", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/cursor" + }; + var direction = { + syntax: "ltr | rtl", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Writing Modes" + ], + initial: "ltr", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/direction" + }; + var display = { + syntax: "[ || ] | | | | ", + media: "all", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Display" + ], + initial: "inline", + appliesto: "allElements", + computed: "asSpecifiedExceptPositionedFloatingAndRootElementsKeywordMaybeDifferent", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/display" + }; + var filter = { + syntax: "none | ", + media: "visual", + inherited: false, + animationType: "filterList", + percentages: "no", + groups: [ + "Filter Effects" + ], + initial: "none", + appliesto: "allElementsSVGContainerElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/filter" + }; + var flex = { + syntax: "none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]", + media: "visual", + inherited: false, + animationType: [ + "flex-grow", + "flex-shrink", + "flex-basis" + ], + percentages: "no", + groups: [ + "CSS Flexible Box Layout" + ], + initial: [ + "flex-grow", + "flex-shrink", + "flex-basis" + ], + appliesto: "flexItemsAndInFlowPseudos", + computed: [ + "flex-grow", + "flex-shrink", + "flex-basis" + ], + order: "orderOfAppearance", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/flex" + }; + var float = { + syntax: "left | right | none | inline-start | inline-end", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Positioning" + ], + initial: "none", + appliesto: "allElementsNoEffectIfDisplayNone", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/float" + }; + var font = { + syntax: "[ [ <'font-style'> || || <'font-weight'> || <'font-stretch'> ]? <'font-size'> [ / <'line-height'> ]? <'font-family'> ] | caption | icon | menu | message-box | small-caption | status-bar", + media: "visual", + inherited: true, + animationType: [ + "font-style", + "font-variant", + "font-weight", + "font-stretch", + "font-size", + "line-height", + "font-family" + ], + percentages: [ + "font-size", + "line-height" + ], + groups: [ + "CSS Fonts" + ], + initial: [ + "font-style", + "font-variant", + "font-weight", + "font-stretch", + "font-size", + "line-height", + "font-family" + ], + appliesto: "allElements", + computed: [ + "font-style", + "font-variant", + "font-weight", + "font-stretch", + "font-size", + "line-height", + "font-family" + ], + order: "orderOfAppearance", + alsoAppliesTo: [ + "::first-letter", + "::first-line", + "::placeholder" + ], + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/font" + }; + var gap = { + syntax: "<'row-gap'> <'column-gap'>?", + media: "visual", + inherited: false, + animationType: [ + "row-gap", + "column-gap" + ], + percentages: "no", + groups: [ + "CSS Box Alignment" + ], + initial: [ + "row-gap", + "column-gap" + ], + appliesto: "multiColumnElementsFlexContainersGridContainers", + computed: [ + "row-gap", + "column-gap" + ], + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/gap" + }; + var grid = { + syntax: "<'grid-template'> | <'grid-template-rows'> / [ auto-flow && dense? ] <'grid-auto-columns'>? | [ auto-flow && dense? ] <'grid-auto-rows'>? / <'grid-template-columns'>", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: [ + "grid-template-rows", + "grid-template-columns", + "grid-auto-rows", + "grid-auto-columns" + ], + groups: [ + "CSS Grid Layout" + ], + initial: [ + "grid-template-rows", + "grid-template-columns", + "grid-template-areas", + "grid-auto-rows", + "grid-auto-columns", + "grid-auto-flow", + "grid-column-gap", + "grid-row-gap", + "column-gap", + "row-gap" + ], + appliesto: "gridContainers", + computed: [ + "grid-template-rows", + "grid-template-columns", + "grid-template-areas", + "grid-auto-rows", + "grid-auto-columns", + "grid-auto-flow", + "grid-column-gap", + "grid-row-gap", + "column-gap", + "row-gap" + ], + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/grid" + }; + var height = { + syntax: "auto | | | min-content | max-content | fit-content()", + media: "visual", + inherited: false, + animationType: "lpc", + percentages: "regardingHeightOfGeneratedBoxContainingBlockPercentagesRelativeToContainingBlock", + groups: [ + "CSS Box Model" + ], + initial: "auto", + appliesto: "allElementsButNonReplacedAndTableColumns", + computed: "percentageAutoOrAbsoluteLength", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/height" + }; + var hyphens = { + syntax: "none | manual | auto", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Text" + ], + initial: "manual", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/hyphens" + }; + var inset = { + syntax: "<'top'>{1,4}", + media: "visual", + inherited: false, + animationType: "lpc", + percentages: "logicalHeightOfContainingBlock", + groups: [ + "CSS Logical Properties" + ], + initial: "auto", + appliesto: "positionedElements", + computed: "sameAsBoxOffsets", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/inset" + }; + var isolation = { + syntax: "auto | isolate", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Compositing and Blending" + ], + initial: "auto", + appliesto: "allElementsSVGContainerGraphicsAndGraphicsReferencingElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/isolation" + }; + var left = { + syntax: " | | auto", + media: "visual", + inherited: false, + animationType: "lpc", + percentages: "referToWidthOfContainingBlock", + groups: [ + "CSS Positioning" + ], + initial: "auto", + appliesto: "positionedElements", + computed: "lengthAbsolutePercentageAsSpecifiedOtherwiseAuto", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/left" + }; + var margin = { + syntax: "[ | | auto ]{1,4}", + media: "visual", + inherited: false, + animationType: "length", + percentages: "referToWidthOfContainingBlock", + groups: [ + "CSS Box Model" + ], + initial: [ + "margin-bottom", + "margin-left", + "margin-right", + "margin-top" + ], + appliesto: "allElementsExceptTableDisplayTypes", + computed: [ + "margin-bottom", + "margin-left", + "margin-right", + "margin-top" + ], + order: "uniqueOrder", + alsoAppliesTo: [ + "::first-letter", + "::first-line" + ], + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/margin" + }; + var mask = { + syntax: "#", + media: "visual", + inherited: false, + animationType: [ + "mask-image", + "mask-mode", + "mask-repeat", + "mask-position", + "mask-clip", + "mask-origin", + "mask-size", + "mask-composite" + ], + percentages: [ + "mask-position" + ], + groups: [ + "CSS Masking" + ], + initial: [ + "mask-image", + "mask-mode", + "mask-repeat", + "mask-position", + "mask-clip", + "mask-origin", + "mask-size", + "mask-composite" + ], + appliesto: "allElementsSVGContainerElements", + computed: [ + "mask-image", + "mask-mode", + "mask-repeat", + "mask-position", + "mask-clip", + "mask-origin", + "mask-size", + "mask-composite" + ], + order: "perGrammar", + stacking: true, + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/mask" + }; + var offset = { + syntax: "[ <'offset-position'>? [ <'offset-path'> [ <'offset-distance'> || <'offset-rotate'> ]? ]? ]! [ / <'offset-anchor'> ]?", + media: "visual", + inherited: false, + animationType: [ + "offset-position", + "offset-path", + "offset-distance", + "offset-anchor", + "offset-rotate" + ], + percentages: [ + "offset-position", + "offset-distance", + "offset-anchor" + ], + groups: [ + "CSS Motion Path" + ], + initial: [ + "offset-position", + "offset-path", + "offset-distance", + "offset-anchor", + "offset-rotate" + ], + appliesto: "transformableElements", + computed: [ + "offset-position", + "offset-path", + "offset-distance", + "offset-anchor", + "offset-rotate" + ], + order: "perGrammar", + stacking: true, + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/offset" + }; + var opacity = { + syntax: "", + media: "visual", + inherited: false, + animationType: "number", + percentages: "no", + groups: [ + "CSS Color" + ], + initial: "1.0", + appliesto: "allElements", + computed: "specifiedValueClipped0To1", + order: "uniqueOrder", + alsoAppliesTo: [ + "::placeholder" + ], + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/opacity" + }; + var order = { + syntax: "", + media: "visual", + inherited: false, + animationType: "integer", + percentages: "no", + groups: [ + "CSS Flexible Box Layout" + ], + initial: "0", + appliesto: "flexItemsGridItemsAbsolutelyPositionedContainerChildren", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/order" + }; + var orphans = { + syntax: "", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Fragmentation" + ], + initial: "2", + appliesto: "blockContainerElements", + computed: "asSpecified", + order: "perGrammar", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/orphans" + }; + var outline = { + syntax: "[ <'outline-color'> || <'outline-style'> || <'outline-width'> ]", + media: [ + "visual", + "interactive" + ], + inherited: false, + animationType: [ + "outline-color", + "outline-width", + "outline-style" + ], + percentages: "no", + groups: [ + "CSS Basic User Interface" + ], + initial: [ + "outline-color", + "outline-style", + "outline-width" + ], + appliesto: "allElements", + computed: [ + "outline-color", + "outline-width", + "outline-style" + ], + order: "orderOfAppearance", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/outline" + }; + var overflow = { + syntax: "[ visible | hidden | clip | scroll | auto ]{1,2}", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Overflow" + ], + initial: "visible", + appliesto: "blockContainersFlexContainersGridContainers", + computed: [ + "overflow-x", + "overflow-y" + ], + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/overflow" + }; + var padding = { + syntax: "[ | ]{1,4}", + media: "visual", + inherited: false, + animationType: "length", + percentages: "referToWidthOfContainingBlock", + groups: [ + "CSS Box Model" + ], + initial: [ + "padding-bottom", + "padding-left", + "padding-right", + "padding-top" + ], + appliesto: "allElementsExceptInternalTableDisplayTypes", + computed: [ + "padding-bottom", + "padding-left", + "padding-right", + "padding-top" + ], + order: "uniqueOrder", + alsoAppliesTo: [ + "::first-letter", + "::first-line" + ], + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/padding" + }; + var perspective = { + syntax: "none | ", + media: "visual", + inherited: false, + animationType: "length", + percentages: "no", + groups: [ + "CSS Transforms" + ], + initial: "none", + appliesto: "transformableElements", + computed: "absoluteLengthOrNone", + order: "uniqueOrder", + stacking: true, + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/perspective" + }; + var position$1 = { + syntax: "static | relative | absolute | sticky | fixed", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Positioning" + ], + initial: "static", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + stacking: true, + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/position" + }; + var quotes = { + syntax: "none | auto | [ ]+", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Generated Content" + ], + initial: "dependsOnUserAgent", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/quotes" + }; + var resize = { + syntax: "none | both | horizontal | vertical | block | inline", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Basic User Interface" + ], + initial: "none", + appliesto: "elementsWithOverflowNotVisibleAndReplacedElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/resize" + }; + var right = { + syntax: " | | auto", + media: "visual", + inherited: false, + animationType: "lpc", + percentages: "referToWidthOfContainingBlock", + groups: [ + "CSS Positioning" + ], + initial: "auto", + appliesto: "positionedElements", + computed: "lengthAbsolutePercentageAsSpecifiedOtherwiseAuto", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/right" + }; + var rotate = { + syntax: "none | | [ x | y | z | {3} ] && ", + media: "visual", + inherited: false, + animationType: "transform", + percentages: "no", + groups: [ + "CSS Transforms" + ], + initial: "none", + appliesto: "transformableElements", + computed: "asSpecified", + order: "perGrammar", + stacking: true, + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/rotate" + }; + var scale = { + syntax: "none | {1,3}", + media: "visual", + inherited: false, + animationType: "transform", + percentages: "no", + groups: [ + "CSS Transforms" + ], + initial: "none", + appliesto: "transformableElements", + computed: "asSpecified", + order: "perGrammar", + stacking: true, + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/scale" + }; + var top = { + syntax: " | | auto", + media: "visual", + inherited: false, + animationType: "lpc", + percentages: "referToContainingBlockHeight", + groups: [ + "CSS Positioning" + ], + initial: "auto", + appliesto: "positionedElements", + computed: "lengthAbsolutePercentageAsSpecifiedOtherwiseAuto", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/top" + }; + var transform = { + syntax: "none | ", + media: "visual", + inherited: false, + animationType: "transform", + percentages: "referToSizeOfBoundingBox", + groups: [ + "CSS Transforms" + ], + initial: "none", + appliesto: "transformableElements", + computed: "asSpecifiedRelativeToAbsoluteLengths", + order: "uniqueOrder", + stacking: true, + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/transform" + }; + var transition = { + syntax: "#", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Transitions" + ], + initial: [ + "transition-delay", + "transition-duration", + "transition-property", + "transition-timing-function" + ], + appliesto: "allElementsAndPseudos", + computed: [ + "transition-delay", + "transition-duration", + "transition-property", + "transition-timing-function" + ], + order: "orderOfAppearance", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/transition" + }; + var translate = { + syntax: "none | [ ? ]?", + media: "visual", + inherited: false, + animationType: "transform", + percentages: "referToSizeOfBoundingBox", + groups: [ + "CSS Transforms" + ], + initial: "none", + appliesto: "transformableElements", + computed: "asSpecifiedRelativeToAbsoluteLengths", + order: "perGrammar", + stacking: true, + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/translate" + }; + var visibility = { + syntax: "visible | hidden | collapse", + media: "visual", + inherited: true, + animationType: "visibility", + percentages: "no", + groups: [ + "CSS Box Model" + ], + initial: "visible", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/visibility" + }; + var widows = { + syntax: "", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Fragmentation" + ], + initial: "2", + appliesto: "blockContainerElements", + computed: "asSpecified", + order: "perGrammar", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/widows" + }; + var width = { + syntax: "auto | | | min-content | max-content | fit-content()", + media: "visual", + inherited: false, + animationType: "lpc", + percentages: "referToWidthOfContainingBlock", + groups: [ + "CSS Box Model" + ], + initial: "auto", + appliesto: "allElementsButNonReplacedAndTableRows", + computed: "percentageAutoOrAbsoluteLength", + order: "lengthOrPercentageBeforeKeywordIfBothPresent", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/width" + }; + var zoom = { + syntax: "normal | reset | | ", + media: "visual", + inherited: false, + animationType: "integer", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "normal", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/zoom" + }; + var require$$1 = { + "--*": { + syntax: "", + media: "all", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Variables" + ], + initial: "seeProse", + appliesto: "allElements", + computed: "asSpecifiedWithVarsSubstituted", + order: "perGrammar", + status: "experimental", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/--*" + }, + "-ms-accelerator": { + syntax: "false | true", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "false", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-accelerator" + }, + "-ms-block-progression": { + syntax: "tb | rl | bt | lr", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "tb", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-block-progression" + }, + "-ms-content-zoom-chaining": { + syntax: "none | chained", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "none", + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-content-zoom-chaining" + }, + "-ms-content-zooming": { + syntax: "none | zoom", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "zoomForTheTopLevelNoneForTheRest", + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-content-zooming" + }, + "-ms-content-zoom-limit": { + syntax: "<'-ms-content-zoom-limit-min'> <'-ms-content-zoom-limit-max'>", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: [ + "-ms-content-zoom-limit-max", + "-ms-content-zoom-limit-min" + ], + groups: [ + "Microsoft Extensions" + ], + initial: [ + "-ms-content-zoom-limit-max", + "-ms-content-zoom-limit-min" + ], + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: [ + "-ms-content-zoom-limit-max", + "-ms-content-zoom-limit-min" + ], + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-content-zoom-limit" + }, + "-ms-content-zoom-limit-max": { + syntax: "", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "maxZoomFactor", + groups: [ + "Microsoft Extensions" + ], + initial: "400%", + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-content-zoom-limit-max" + }, + "-ms-content-zoom-limit-min": { + syntax: "", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "minZoomFactor", + groups: [ + "Microsoft Extensions" + ], + initial: "100%", + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-content-zoom-limit-min" + }, + "-ms-content-zoom-snap": { + syntax: "<'-ms-content-zoom-snap-type'> || <'-ms-content-zoom-snap-points'>", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: [ + "-ms-content-zoom-snap-type", + "-ms-content-zoom-snap-points" + ], + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: [ + "-ms-content-zoom-snap-type", + "-ms-content-zoom-snap-points" + ], + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-content-zoom-snap" + }, + "-ms-content-zoom-snap-points": { + syntax: "snapInterval( , ) | snapList( # )", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "snapInterval(0%, 100%)", + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-content-zoom-snap-points" + }, + "-ms-content-zoom-snap-type": { + syntax: "none | proximity | mandatory", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "none", + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-content-zoom-snap-type" + }, + "-ms-filter": { + syntax: "", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "\"\"", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-filter" + }, + "-ms-flow-from": { + syntax: "[ none | ]#", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "none", + appliesto: "nonReplacedElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-flow-from" + }, + "-ms-flow-into": { + syntax: "[ none | ]#", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "none", + appliesto: "iframeElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-flow-into" + }, + "-ms-grid-columns": { + syntax: "none | | ", + media: "visual", + inherited: false, + animationType: "simpleListOfLpcDifferenceLpc", + percentages: "referToDimensionOfContentArea", + groups: [ + "CSS Grid Layout" + ], + initial: "none", + appliesto: "gridContainers", + computed: "asSpecifiedRelativeToAbsoluteLengths", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-grid-columns" + }, + "-ms-grid-rows": { + syntax: "none | | ", + media: "visual", + inherited: false, + animationType: "simpleListOfLpcDifferenceLpc", + percentages: "referToDimensionOfContentArea", + groups: [ + "CSS Grid Layout" + ], + initial: "none", + appliesto: "gridContainers", + computed: "asSpecifiedRelativeToAbsoluteLengths", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-grid-rows" + }, + "-ms-high-contrast-adjust": { + syntax: "auto | none", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "auto", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-high-contrast-adjust" + }, + "-ms-hyphenate-limit-chars": { + syntax: "auto | {1,3}", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "auto", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-hyphenate-limit-chars" + }, + "-ms-hyphenate-limit-lines": { + syntax: "no-limit | ", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "no-limit", + appliesto: "blockContainerElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-hyphenate-limit-lines" + }, + "-ms-hyphenate-limit-zone": { + syntax: " | ", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "referToLineBoxWidth", + groups: [ + "Microsoft Extensions" + ], + initial: "0", + appliesto: "blockContainerElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-hyphenate-limit-zone" + }, + "-ms-ime-align": { + syntax: "auto | after", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "auto", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-ime-align" + }, + "-ms-overflow-style": { + syntax: "auto | none | scrollbar | -ms-autohiding-scrollbar", + media: "interactive", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "auto", + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-overflow-style" + }, + "-ms-scrollbar-3dlight-color": { + syntax: "", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "dependsOnUserAgent", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scrollbar-3dlight-color" + }, + "-ms-scrollbar-arrow-color": { + syntax: "", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "ButtonText", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scrollbar-arrow-color" + }, + "-ms-scrollbar-base-color": { + syntax: "", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "dependsOnUserAgent", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scrollbar-base-color" + }, + "-ms-scrollbar-darkshadow-color": { + syntax: "", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "ThreeDDarkShadow", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scrollbar-darkshadow-color" + }, + "-ms-scrollbar-face-color": { + syntax: "", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "ThreeDFace", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scrollbar-face-color" + }, + "-ms-scrollbar-highlight-color": { + syntax: "", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "ThreeDHighlight", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scrollbar-highlight-color" + }, + "-ms-scrollbar-shadow-color": { + syntax: "", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "ThreeDDarkShadow", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scrollbar-shadow-color" + }, + "-ms-scrollbar-track-color": { + syntax: "", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "Scrollbar", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scrollbar-track-color" + }, + "-ms-scroll-chaining": { + syntax: "chained | none", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "chained", + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scroll-chaining" + }, + "-ms-scroll-limit": { + syntax: "<'-ms-scroll-limit-x-min'> <'-ms-scroll-limit-y-min'> <'-ms-scroll-limit-x-max'> <'-ms-scroll-limit-y-max'>", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: [ + "-ms-scroll-limit-x-min", + "-ms-scroll-limit-y-min", + "-ms-scroll-limit-x-max", + "-ms-scroll-limit-y-max" + ], + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: [ + "-ms-scroll-limit-x-min", + "-ms-scroll-limit-y-min", + "-ms-scroll-limit-x-max", + "-ms-scroll-limit-y-max" + ], + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scroll-limit" + }, + "-ms-scroll-limit-x-max": { + syntax: "auto | ", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "auto", + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scroll-limit-x-max" + }, + "-ms-scroll-limit-x-min": { + syntax: "", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "0", + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scroll-limit-x-min" + }, + "-ms-scroll-limit-y-max": { + syntax: "auto | ", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "auto", + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scroll-limit-y-max" + }, + "-ms-scroll-limit-y-min": { + syntax: "", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "0", + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scroll-limit-y-min" + }, + "-ms-scroll-rails": { + syntax: "none | railed", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "railed", + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scroll-rails" + }, + "-ms-scroll-snap-points-x": { + syntax: "snapInterval( , ) | snapList( # )", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "snapInterval(0px, 100%)", + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scroll-snap-points-x" + }, + "-ms-scroll-snap-points-y": { + syntax: "snapInterval( , ) | snapList( # )", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "snapInterval(0px, 100%)", + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scroll-snap-points-y" + }, + "-ms-scroll-snap-type": { + syntax: "none | proximity | mandatory", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "none", + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scroll-snap-type" + }, + "-ms-scroll-snap-x": { + syntax: "<'-ms-scroll-snap-type'> <'-ms-scroll-snap-points-x'>", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: [ + "-ms-scroll-snap-type", + "-ms-scroll-snap-points-x" + ], + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: [ + "-ms-scroll-snap-type", + "-ms-scroll-snap-points-x" + ], + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scroll-snap-x" + }, + "-ms-scroll-snap-y": { + syntax: "<'-ms-scroll-snap-type'> <'-ms-scroll-snap-points-y'>", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: [ + "-ms-scroll-snap-type", + "-ms-scroll-snap-points-y" + ], + appliesto: "nonReplacedBlockAndInlineBlockElements", + computed: [ + "-ms-scroll-snap-type", + "-ms-scroll-snap-points-y" + ], + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scroll-snap-y" + }, + "-ms-scroll-translation": { + syntax: "none | vertical-to-horizontal", + media: "interactive", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "none", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-scroll-translation" + }, + "-ms-text-autospace": { + syntax: "none | ideograph-alpha | ideograph-numeric | ideograph-parenthesis | ideograph-space", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "none", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-text-autospace" + }, + "-ms-touch-select": { + syntax: "grippers | none", + media: "interactive", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "grippers", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-touch-select" + }, + "-ms-user-select": { + syntax: "none | element | text", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "text", + appliesto: "nonReplacedElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-user-select" + }, + "-ms-wrap-flow": { + syntax: "auto | both | start | end | maximum | clear", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "auto", + appliesto: "blockLevelElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-wrap-flow" + }, + "-ms-wrap-margin": { + syntax: "", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "0", + appliesto: "exclusionElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-wrap-margin" + }, + "-ms-wrap-through": { + syntax: "wrap | none", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Microsoft Extensions" + ], + initial: "wrap", + appliesto: "blockLevelElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-ms-wrap-through" + }, + "-moz-appearance": { + syntax: "none | button | button-arrow-down | button-arrow-next | button-arrow-previous | button-arrow-up | button-bevel | button-focus | caret | checkbox | checkbox-container | checkbox-label | checkmenuitem | dualbutton | groupbox | listbox | listitem | menuarrow | menubar | menucheckbox | menuimage | menuitem | menuitemtext | menulist | menulist-button | menulist-text | menulist-textfield | menupopup | menuradio | menuseparator | meterbar | meterchunk | progressbar | progressbar-vertical | progresschunk | progresschunk-vertical | radio | radio-container | radio-label | radiomenuitem | range | range-thumb | resizer | resizerpanel | scale-horizontal | scalethumbend | scalethumb-horizontal | scalethumbstart | scalethumbtick | scalethumb-vertical | scale-vertical | scrollbarbutton-down | scrollbarbutton-left | scrollbarbutton-right | scrollbarbutton-up | scrollbarthumb-horizontal | scrollbarthumb-vertical | scrollbartrack-horizontal | scrollbartrack-vertical | searchfield | separator | sheet | spinner | spinner-downbutton | spinner-textfield | spinner-upbutton | splitter | statusbar | statusbarpanel | tab | tabpanel | tabpanels | tab-scroll-arrow-back | tab-scroll-arrow-forward | textfield | textfield-multiline | toolbar | toolbarbutton | toolbarbutton-dropdown | toolbargripper | toolbox | tooltip | treeheader | treeheadercell | treeheadersortarrow | treeitem | treeline | treetwisty | treetwistyopen | treeview | -moz-mac-unified-toolbar | -moz-win-borderless-glass | -moz-win-browsertabbar-toolbox | -moz-win-communicationstext | -moz-win-communications-toolbox | -moz-win-exclude-glass | -moz-win-glass | -moz-win-mediatext | -moz-win-media-toolbox | -moz-window-button-box | -moz-window-button-box-maximized | -moz-window-button-close | -moz-window-button-maximize | -moz-window-button-minimize | -moz-window-button-restore | -moz-window-frame-bottom | -moz-window-frame-left | -moz-window-frame-right | -moz-window-titlebar | -moz-window-titlebar-maximized", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Mozilla Extensions", + "WebKit Extensions" + ], + initial: "noneButOverriddenInUserAgentCSS", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/appearance" + }, + "-moz-binding": { + syntax: " | none", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Mozilla Extensions" + ], + initial: "none", + appliesto: "allElementsExceptGeneratedContentOrPseudoElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-binding" + }, + "-moz-border-bottom-colors": { + syntax: "+ | none", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Mozilla Extensions" + ], + initial: "none", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-border-bottom-colors" + }, + "-moz-border-left-colors": { + syntax: "+ | none", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Mozilla Extensions" + ], + initial: "none", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-border-left-colors" + }, + "-moz-border-right-colors": { + syntax: "+ | none", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Mozilla Extensions" + ], + initial: "none", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-border-right-colors" + }, + "-moz-border-top-colors": { + syntax: "+ | none", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Mozilla Extensions" + ], + initial: "none", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-border-top-colors" + }, + "-moz-context-properties": { + syntax: "none | [ fill | fill-opacity | stroke | stroke-opacity ]#", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "Mozilla Extensions" + ], + initial: "none", + appliesto: "allElementsThatCanReferenceImages", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-context-properties" + }, + "-moz-float-edge": { + syntax: "border-box | content-box | margin-box | padding-box", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Mozilla Extensions" + ], + initial: "content-box", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-float-edge" + }, + "-moz-force-broken-image-icon": { + syntax: "", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Mozilla Extensions" + ], + initial: "0", + appliesto: "images", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-force-broken-image-icon" + }, + "-moz-image-region": { + syntax: " | auto", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "Mozilla Extensions" + ], + initial: "auto", + appliesto: "xulImageElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-image-region" + }, + "-moz-orient": { + syntax: "inline | block | horizontal | vertical", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Mozilla Extensions" + ], + initial: "inline", + appliesto: "anyElementEffectOnProgressAndMeter", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-orient" + }, + "-moz-outline-radius": { + syntax: "{1,4} [ / {1,4} ]?", + media: "visual", + inherited: false, + animationType: [ + "-moz-outline-radius-topleft", + "-moz-outline-radius-topright", + "-moz-outline-radius-bottomright", + "-moz-outline-radius-bottomleft" + ], + percentages: [ + "-moz-outline-radius-topleft", + "-moz-outline-radius-topright", + "-moz-outline-radius-bottomright", + "-moz-outline-radius-bottomleft" + ], + groups: [ + "Mozilla Extensions" + ], + initial: [ + "-moz-outline-radius-topleft", + "-moz-outline-radius-topright", + "-moz-outline-radius-bottomright", + "-moz-outline-radius-bottomleft" + ], + appliesto: "allElements", + computed: [ + "-moz-outline-radius-topleft", + "-moz-outline-radius-topright", + "-moz-outline-radius-bottomright", + "-moz-outline-radius-bottomleft" + ], + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-outline-radius" + }, + "-moz-outline-radius-bottomleft": { + syntax: "", + media: "visual", + inherited: false, + animationType: "lpc", + percentages: "referToDimensionOfBorderBox", + groups: [ + "Mozilla Extensions" + ], + initial: "0", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-outline-radius-bottomleft" + }, + "-moz-outline-radius-bottomright": { + syntax: "", + media: "visual", + inherited: false, + animationType: "lpc", + percentages: "referToDimensionOfBorderBox", + groups: [ + "Mozilla Extensions" + ], + initial: "0", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-outline-radius-bottomright" + }, + "-moz-outline-radius-topleft": { + syntax: "", + media: "visual", + inherited: false, + animationType: "lpc", + percentages: "referToDimensionOfBorderBox", + groups: [ + "Mozilla Extensions" + ], + initial: "0", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-outline-radius-topleft" + }, + "-moz-outline-radius-topright": { + syntax: "", + media: "visual", + inherited: false, + animationType: "lpc", + percentages: "referToDimensionOfBorderBox", + groups: [ + "Mozilla Extensions" + ], + initial: "0", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-outline-radius-topright" + }, + "-moz-stack-sizing": { + syntax: "ignore | stretch-to-fit", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "Mozilla Extensions" + ], + initial: "stretch-to-fit", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-stack-sizing" + }, + "-moz-text-blink": { + syntax: "none | blink", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Mozilla Extensions" + ], + initial: "none", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-text-blink" + }, + "-moz-user-focus": { + syntax: "ignore | normal | select-after | select-before | select-menu | select-same | select-all | none", + media: "interactive", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Mozilla Extensions" + ], + initial: "none", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-user-focus" + }, + "-moz-user-input": { + syntax: "auto | none | enabled | disabled", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "Mozilla Extensions" + ], + initial: "auto", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-user-input" + }, + "-moz-user-modify": { + syntax: "read-only | read-write | write-only", + media: "interactive", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "Mozilla Extensions" + ], + initial: "read-only", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-user-modify" + }, + "-moz-window-dragging": { + syntax: "drag | no-drag", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Mozilla Extensions" + ], + initial: "drag", + appliesto: "allElementsCreatingNativeWindows", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-window-dragging" + }, + "-moz-window-shadow": { + syntax: "default | menu | tooltip | sheet | none", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "Mozilla Extensions" + ], + initial: "default", + appliesto: "allElementsCreatingNativeWindows", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-moz-window-shadow" + }, + "-webkit-appearance": { + syntax: "none | button | button-bevel | caret | checkbox | default-button | inner-spin-button | listbox | listitem | media-controls-background | media-controls-fullscreen-background | media-current-time-display | media-enter-fullscreen-button | media-exit-fullscreen-button | media-fullscreen-button | media-mute-button | media-overlay-play-button | media-play-button | media-seek-back-button | media-seek-forward-button | media-slider | media-sliderthumb | media-time-remaining-display | media-toggle-closed-captions-button | media-volume-slider | media-volume-slider-container | media-volume-sliderthumb | menulist | menulist-button | menulist-text | menulist-textfield | meter | progress-bar | progress-bar-value | push-button | radio | searchfield | searchfield-cancel-button | searchfield-decoration | searchfield-results-button | searchfield-results-decoration | slider-horizontal | slider-vertical | sliderthumb-horizontal | sliderthumb-vertical | square-button | textarea | textfield | -apple-pay-button", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: "noneButOverriddenInUserAgentCSS", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/appearance" + }, + "-webkit-border-before": { + syntax: "<'border-width'> || <'border-style'> || <'color'>", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: [ + "-webkit-border-before-width" + ], + groups: [ + "WebKit Extensions" + ], + initial: [ + "border-width", + "border-style", + "color" + ], + appliesto: "allElements", + computed: [ + "border-width", + "border-style", + "color" + ], + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-webkit-border-before" + }, + "-webkit-border-before-color": { + syntax: "<'color'>", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: "currentcolor", + appliesto: "allElements", + computed: "computedColor", + order: "uniqueOrder", + status: "nonstandard" + }, + "-webkit-border-before-style": { + syntax: "<'border-style'>", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: "none", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard" + }, + "-webkit-border-before-width": { + syntax: "<'border-width'>", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "logicalWidthOfContainingBlock", + groups: [ + "WebKit Extensions" + ], + initial: "medium", + appliesto: "allElements", + computed: "absoluteLengthZeroIfBorderStyleNoneOrHidden", + order: "uniqueOrder", + status: "nonstandard" + }, + "-webkit-box-reflect": { + syntax: "[ above | below | right | left ]? ? ?", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: "none", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-webkit-box-reflect" + }, + "-webkit-line-clamp": { + syntax: "none | ", + media: "visual", + inherited: false, + animationType: "byComputedValueType", + percentages: "no", + groups: [ + "WebKit Extensions", + "CSS Overflow" + ], + initial: "none", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-webkit-line-clamp" + }, + "-webkit-mask": { + syntax: "[ || [ / ]? || || [ | border | padding | content | text ] || [ | border | padding | content ] ]#", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: [ + "-webkit-mask-image", + "-webkit-mask-repeat", + "-webkit-mask-attachment", + "-webkit-mask-position", + "-webkit-mask-origin", + "-webkit-mask-clip" + ], + appliesto: "allElements", + computed: [ + "-webkit-mask-image", + "-webkit-mask-repeat", + "-webkit-mask-attachment", + "-webkit-mask-position", + "-webkit-mask-origin", + "-webkit-mask-clip" + ], + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/mask" + }, + "-webkit-mask-attachment": { + syntax: "#", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: "scroll", + appliesto: "allElements", + computed: "asSpecified", + order: "orderOfAppearance", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-webkit-mask-attachment" + }, + "-webkit-mask-clip": { + syntax: "[ | border | padding | content | text ]#", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: "border", + appliesto: "allElements", + computed: "asSpecified", + order: "orderOfAppearance", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/mask-clip" + }, + "-webkit-mask-composite": { + syntax: "#", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: "source-over", + appliesto: "allElements", + computed: "asSpecified", + order: "orderOfAppearance", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-webkit-mask-composite" + }, + "-webkit-mask-image": { + syntax: "#", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: "none", + appliesto: "allElements", + computed: "absoluteURIOrNone", + order: "orderOfAppearance", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/mask-image" + }, + "-webkit-mask-origin": { + syntax: "[ | border | padding | content ]#", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: "padding", + appliesto: "allElements", + computed: "asSpecified", + order: "orderOfAppearance", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/mask-origin" + }, + "-webkit-mask-position": { + syntax: "#", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "referToSizeOfElement", + groups: [ + "WebKit Extensions" + ], + initial: "0% 0%", + appliesto: "allElements", + computed: "absoluteLengthOrPercentage", + order: "orderOfAppearance", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/mask-position" + }, + "-webkit-mask-position-x": { + syntax: "[ | left | center | right ]#", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "referToSizeOfElement", + groups: [ + "WebKit Extensions" + ], + initial: "0%", + appliesto: "allElements", + computed: "absoluteLengthOrPercentage", + order: "orderOfAppearance", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-webkit-mask-position-x" + }, + "-webkit-mask-position-y": { + syntax: "[ | top | center | bottom ]#", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "referToSizeOfElement", + groups: [ + "WebKit Extensions" + ], + initial: "0%", + appliesto: "allElements", + computed: "absoluteLengthOrPercentage", + order: "orderOfAppearance", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-webkit-mask-position-y" + }, + "-webkit-mask-repeat": { + syntax: "#", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: "repeat", + appliesto: "allElements", + computed: "asSpecified", + order: "orderOfAppearance", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/mask-repeat" + }, + "-webkit-mask-repeat-x": { + syntax: "repeat | no-repeat | space | round", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: "repeat", + appliesto: "allElements", + computed: "asSpecified", + order: "orderOfAppearance", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-webkit-mask-repeat-x" + }, + "-webkit-mask-repeat-y": { + syntax: "repeat | no-repeat | space | round", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: "repeat", + appliesto: "allElements", + computed: "absoluteLengthOrPercentage", + order: "orderOfAppearance", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-webkit-mask-repeat-y" + }, + "-webkit-mask-size": { + syntax: "#", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "relativeToBackgroundPositioningArea", + groups: [ + "WebKit Extensions" + ], + initial: "auto auto", + appliesto: "allElements", + computed: "asSpecified", + order: "orderOfAppearance", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/mask-size" + }, + "-webkit-overflow-scrolling": { + syntax: "auto | touch", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: "auto", + appliesto: "scrollingBoxes", + computed: "asSpecified", + order: "orderOfAppearance", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-webkit-overflow-scrolling" + }, + "-webkit-tap-highlight-color": { + syntax: "", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: "black", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-webkit-tap-highlight-color" + }, + "-webkit-text-fill-color": { + syntax: "", + media: "visual", + inherited: true, + animationType: "color", + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: "currentcolor", + appliesto: "allElements", + computed: "computedColor", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-webkit-text-fill-color" + }, + "-webkit-text-stroke": { + syntax: " || ", + media: "visual", + inherited: true, + animationType: [ + "-webkit-text-stroke-width", + "-webkit-text-stroke-color" + ], + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: [ + "-webkit-text-stroke-width", + "-webkit-text-stroke-color" + ], + appliesto: "allElements", + computed: [ + "-webkit-text-stroke-width", + "-webkit-text-stroke-color" + ], + order: "canonicalOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-webkit-text-stroke" + }, + "-webkit-text-stroke-color": { + syntax: "", + media: "visual", + inherited: true, + animationType: "color", + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: "currentcolor", + appliesto: "allElements", + computed: "computedColor", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-webkit-text-stroke-color" + }, + "-webkit-text-stroke-width": { + syntax: "", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: "0", + appliesto: "allElements", + computed: "absoluteLength", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-webkit-text-stroke-width" + }, + "-webkit-touch-callout": { + syntax: "default | none", + media: "visual", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: "default", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/-webkit-touch-callout" + }, + "-webkit-user-modify": { + syntax: "read-only | read-write | read-write-plaintext-only", + media: "interactive", + inherited: true, + animationType: "discrete", + percentages: "no", + groups: [ + "WebKit Extensions" + ], + initial: "read-only", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "nonstandard" + }, + "align-content": { + syntax: "normal | | | ? ", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Box Alignment" + ], + initial: "normal", + appliesto: "multilineFlexContainers", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/align-content" + }, + "align-items": { + syntax: "normal | stretch | | [ ? ]", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Box Alignment" + ], + initial: "normal", + appliesto: "allElements", + computed: "asSpecified", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/align-items" + }, + "align-self": { + syntax: "auto | normal | stretch | | ? ", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Box Alignment" + ], + initial: "auto", + appliesto: "flexItemsGridItemsAndAbsolutelyPositionedBoxes", + computed: "autoOnAbsolutelyPositionedElementsValueOfAlignItemsOnParent", + order: "uniqueOrder", + status: "standard", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/align-self" + }, + "align-tracks": { + syntax: "[ normal | | | ? ]#", + media: "visual", + inherited: false, + animationType: "discrete", + percentages: "no", + groups: [ + "CSS Grid Layout" + ], + initial: "normal", + appliesto: "gridContainersWithMasonryLayoutInTheirBlockAxis", + computed: "asSpecified", + order: "uniqueOrder", + status: "experimental", + mdn_url: "https://developer.mozilla.org/docs/Web/CSS/align-tracks" + }, + all: all, + animation: animation, + "animation-delay": { + syntax: "
` 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. + +## Dropping `pagedjs-cli` + +`pagedjs-cli` did three useful things for us and one harmful one. On +the useful side: it shipped the paged.js browser bundle in +`dist/browser.js`, the outline + metadata helpers in +`src/outline.js` and `src/postprocesser.js` (~250 LOC total), and a +CLI wrapper for the pdf pipeline. On the harmful side, the wrapper +calls `PDFDocument.load(pdf)` and `pdfDoc.save()` with no options +and therefore inherits the slow defaults that wasted ~32 s per build +(see "Profiling pdf-lib's load" above). Patching upstream to fix +that is plumbing for plumbing's sake; the rest of pagedjs-cli is +already mostly duplicated by our harness. + +So we vendored what we needed and dropped the dep: + +- `docs/lib/paged.browser.js` -- `pagedjs-cli@0.4.3/dist/browser.js`, + byte-for-byte. MIT-licensed; license header preserved at top of file. +- `docs/lib/outline.mjs` -- `src/outline.js`, ESM-ified, attribution + in the file header. +- `docs/lib/postprocesser.mjs` -- `src/postprocesser.js`, same. +- `docs/render-book.mjs` -- the production driver. Argv-compatible + with the subset of `pagedjs-cli` flags `book.bat` actually used + (`-o`, `--outline-tags`, `-t`, `--additional-script`). Calls + pdf-lib with `parseSpeed: Fastest` + `objectsPerTick: Infinity` + inline, no patching required. +- `docs/book.bat` -- swapped `npx pagedjs-cli ...` for + `node render-book.mjs ...`. Same CLI, ~32 s faster (pdf-lib idle + yielding gone), one fewer transitive dependency tree. + +Both `docs/package.json` and `perf/package.json` now depend directly +on `puppeteer` + `pdf-lib` + `html-entities` instead of inheriting +them via `pagedjs-cli`. `perf/measure.mjs` imports from `docs/lib/` +so the harness and production share the exact same code path through +the helpers and bundle -- whatever production renders, the harness +measures. + +End-to-end on the 1638-page book through the new driver: + +``` +render: 53.5s (1638 pages) +generate: 68.8s (raw 52.3 MB) +process: 5.1s +saved: docs\_pdf\book.pdf (16.9 MB) +total: 130.4s +``` + +(The total includes puppeteer launch + page nav overhead the +harness elides, so it reads a few seconds higher than the harness's +105 s headline.) + +## Restoring live progress + +Dropping `pagedjs-cli` (above) quietly dropped its ora spinners +along with the rest of the CLI. The terminal goes silent for the +~50 s render and ~60 s generate phases -- on a 130 s build, most +of the wall time looks like the process is hung. + +Render phase: restored via `docs/lib/progress-handler.js`, a small +`Paged.Handler` subclass that emits a `[render-progress] page=N +elapsed=Ns` line from `afterPageLayout`. `render-book.mjs` listens +on `page.on('console')` and re-renders the line as a +`\r`-overwritten TTY status (`rendering: 234 pages (12.4s)`), or +every 100 pages on its own line when stdout is piped (CI / log +files). The live line is cleared just before the final +`render: 53.5s (1638 pages)` summary is printed. + +The handler is a separate in-page script rather than inlined into +`render-book.mjs` because `addScriptTag({ path })` loads it via +file:// into the headless page -- it has to be a real file. It's +structurally parallel to `perf/timing-handler.js`, which uses the +same hook but additionally retains per-page detail on +`window.__pagedTiming` for offline analysis. The production version +stays minimal -- just the log line. + +Generate phase: a 500 ms wall-clock heartbeat in `render-book.mjs` +writes `generating: 23.4s` to a `\r`-overwritten TTY line during +the `page.pdf()` wait. Elapsed time only; no byte- or page-count +signal. The line is cleared before the final +`generate: 68.8s (raw 52.3 MB)` summary, same shape as the render +phase. + +We initially tried byte-level progress -- drive `page.pdf()` at the +CDP level with `transferMode: 'ReturnAsStream'` + chunked `IO.read`. +On the Chromium we ship with, the bytes don't actually stream: +Chrome's SkPDF writer buffers the whole document internally and +emits all 52 MB in one tick at the end. The wrapper showed `0.0 MB` +for ~50 s then flickered `52 MB` for one frame before the summary +-- the heartbeat was doing all the visible work. Dropped the CDP +code; the buffer-then-dump finding is preserved in a comment above +the heartbeat so the next person doesn't re-investigate. + +The process phase stays silent. At ~5 s with the fast pdf-lib knobs +(`parseSpeed: Fastest`) it's not worth a progress signal of its own. + +## Revisiting `AtPage.finalizePage` + +The post-detach CPU profile in the "Fix applied: `perf/detach-pages.js`" +section above showed an `(anonymous) @ browser.js:29501` row at **13.7 s +self-time** -- the `["top","bottom"].forEach(...)` lambda inside +`AtPage.finalizePage`. That looked like a fat target. + +It wasn't, for two reasons: + +1. **The 13.7 s number was stale.** It came from the *first* + detach-pages.js variant, which hooked `afterPageLayout` and hid the + page *before* AtPage ran -- so AtPage paid Chromium's slow style- + cascade path on a `display:none` subtree (~9 ms/page). The shipping + variant hooks `finalizePage` and hides *after* AtPage, so AtPage + sees a visible page and the same lambda is **~0.7 ms/page = ~1.1 s + total render**. Re-measured on a fresh profile, the lambda is + ~1.0 s self-time, not 14 s. The original number is correct for the + variant it was measured on, but doesn't reflect current ship. +2. **Most of that ~1 s isn't query CPU.** Per-page the method does + ~17 `querySelector` calls plus a few `getComputedStyle` reads. + Native query self-time across the whole render is ~340 ms + (`querySelector` ~155 ms + `querySelectorAll` ~185 ms in the + unpatched baseline). The rest of the lambda's ~1 s is the + downstream layout flush triggered by `getComputedStyle` and the + style writes -- unaffected by query consolidation. + +We patched it anyway, as a cleanup. `docs/lib/paged.browser.js`'s +`finalizePage` now builds a `__mLookup` table once per page via a +single `querySelectorAll` over all 16 known margin-cell + margin- +group class selectors, then the two forEach loops index that table +instead of calling `page.element.querySelector(...)` 4× per +iteration. The patch is marked `// PATCH: consolidate` at each of +the three touch points so a future re-vendoring of the bundle can +grep for it. + +### A/B results + +Interleaved 3+3 (A1 B1 A2 B2 A3 B3), `--detach-pages --cpu-profile`, +same 1638-page book each run: + +| metric | A (patched) | B (unpatched) | Δ | +| --- | --- | --- | --- | +| render wall-clock, mean | 49.45 s | 49.91 s | -0.46 s (noise; within-variant range 4-13 s) | +| `querySelector` self-time | <50 ms | 155 ms | -155 ms | +| `querySelectorAll` self-time | 247 ms | 183 ms | +64 ms | +| **query CPU total** | **~247 ms** | **~338 ms** | **-91 ms (-27 %)** | +| finalizePage lambda self-time | 1033 ms | 1025 ms | unchanged | + +The patch does what it says on the tin: ~91 ms shifts out of native +`querySelector` and into a single `querySelectorAll`. Wall-clock +delta is in the noise; the within-variant spread (3-13 s across runs +of the same variant) drowns it out. + +The lambda's self-time being unchanged is the load-bearing +observation: query consolidation doesn't reduce the layout-flush +component, which is most of the 1 s. The next lever in this method +would be **read/write batching** -- hoist all `getComputedStyle` +reads to the top of `finalizePage` before any style writes, so the +write-then-read pattern stops forcing a flush mid-method. + +### Read/write batching + +We applied the hoist anyway, as a follow-up cleanup. After the +`__mLookup` block above, `finalizePage` now reads every relevant +`max-width` / `max-height` value into two `Map`s (`__maxW`, `__maxH`) +in a single batch -- gated by the same `.hasContent` check the +original conditionals used. The two forEach loops then consume +those cached values instead of calling `getComputedStyle` inline. +Marked `// PATCH: max-width reads hoisted` / `max-height reads +hoisted` at each touch point. + +**For this book**, the hoist is a no-op behaviourally. Our @page CSS +sets content on exactly one corner (bottom-right page number), so +only one `.hasContent` cell exists per page; the original code did +exactly one `getComputedStyle` per page and therefore one forced +flush. The hoisted version does the same. + +Smoke test, single render with `--detach-pages` (no profiling): 1638 +pages, 16.9 MB output, render 47.98 s, ratio 1.69x. All in the noise +band from the consolidate-querySelector A/B. + +**For docs with multi-cell marginalia** (running headers + footers + +page numbers across several corners) the hoist collapses N forced +flushes -- one per cell that hits the `if (xContent)` branch in the +original -- down to 1. The win scales with marginalia density. + +### Cross-page memoization + +The next layer of duplicate work: `finalizePage`'s computation is a +pure function of `(page.element.className, this.marginalia, CSS +@page rules)`. The marginalia map and CSS are static; only the +className varies. **Two pages with the same className get the same +four `grid-template-columns` / `grid-template-rows` values.** So we +cache the result. + +Implementation: a `this.__finalizeCache: Map` on the AtPage instance, keyed by +`page.element.className`. The cache check sits between the +`__mLookup` build and the GCS hoist. On a hit we apply the cached +values via `__mLookup` and `return` -- Phases B and C never run. On +a miss the existing code runs and the result is recorded at the end +of the method by reading back the just-written `.style.grid- +template-*` values. + +Phase A's marginalia `.hasContent` classifier still runs on every +page (the class has to be added to *this* page's elements so the +@page-margin CSS rules apply). Only the grid-template +computation is skipped. + +**Assumption.** Cache key is `page.element.className`. Sound as long +as @page rules don't use position-dependent selectors (e.g. +`:nth-of-type`) that pick different rules on pages that share a +className. Common case, true for this book; comment in the bundle +flags the caveat. + +Smoke render (`--detach-pages`, no profile): 1638 pages, **16.9 MB +output (byte-equivalent to the pre-patch run)**, render 48.27 s. +Wall-clock impact still in the noise -- same reason as the hoist: +the flush we skip in `finalizePage` is just deferred to the next +chunker iteration's `findOverflow`. Total layout work the document +demands doesn't shrink. What does shrink is the JS-side work -- +~1633 of 1638 pages now skip ~17 `querySelector` lookups, 6 +`classList.contains` reads, and the GCS pass entirely -- but that's +sub-millisecond per page and disappears into the noise band. + +We're not going to keep iterating on `finalizePage`: budget is ~1 s +total render even when every flush triggers, so further work here is +cleanup-only. + +### Hoisting grid-template emission to parse time + +The cleanup payoff. Three patches in a row -- `__mLookup`, GCS hoist, +cross-page memoization -- had whittled `finalizePage`'s per-page +work to ~30 sub-ms ops, then to one Map lookup. The architectural +move was to **delete the hot spot rather than keep optimizing +around it**: hoist the grid-template computation out of the +per-page JS path and into the polisher's @page CSS emission, so the +rules are emitted once at parse time and the browser applies them +via cascade for every matching page. + +The decision tree's inputs are static at parse time: + +- **hasContent** per `(page-class, margin-cell)` -- already recorded + in `this.marginalia[sel]` by `addMarginaliaStyles` for Phase A's + classifier, and invariant per page-class regardless of page index. +- **max-width / max-height** per cell -- created by the same walker + that copies `width`/`height` declarations to `max-width` / + `max-height` on corner cells. The runtime + `getComputedStyle(el)["max-width"]` reads return the CSS-cascade + result of those rules, which is the value the parser saw. We + capture the string at parse time on the marginalia entry, + defaulting to `"none"` when no declaration exists. + +`AtPage.afterTreeWalk` already runs `addPageClasses`, which +populates `this.marginalia` and emits the per-cell margin-styling +rules. We extended it with `emitMarginGridTemplates`: for each page +entry in `this.pages`, build the effective per-cell `hasContent + +maxWidth + maxHeight` by unioning across every marginalia entry +whose page-selector is a subset of the page's class signature +(matching the runtime Phase A OR-cascade; `maxWidth` follows CSS +cascade and takes the most-specific declared value). Run the same +decision tree the runtime did on that snapshot. Emit one rule per +margin group with `selectorsForPage(page)` as the selector and the +computed `grid-template-columns` / `-rows` as a Raw value +declaration. Skip emission for the four offset-fallback branches +that need `offsetWidth` measurement (they can't be pre-computed -- +they read live layout). + +For this book that produces 24 rules total -- 6 page-class +signatures (`*`, `:first`, `divider`, `front-matter`, +`part-foreword`, `chapter-divider`) × 4 margin groups -- all with +the same `0 0 1fr` value (the static branch the decision tree +produces when only one corner has content and no widths are +declared): + +```css +.pagedjs_page .pagedjs_margin-top { grid-template-columns: 0 0 1fr; } +.pagedjs_page .pagedjs_margin-bottom { grid-template-columns: 0 0 1fr; } +.pagedjs_page .pagedjs_margin-left { grid-template-rows: 0 0 1fr; } +.pagedjs_page .pagedjs_margin-right { grid-template-rows: 0 0 1fr; } +... (5 more page-class signatures) ... +``` + +`finalizePage` collapses to **Phase A + an offset-only Phase B**: + +- **Phase A** unchanged. Per-page DOM, can't be hoisted -- it has to + add `.hasContent` to the freshly created margin cells so the + base-style `.pagedjs_margin:not(.hasContent) { visibility: hidden + }` rule unhides the right ones. +- **Phase B offset fallbacks.** The four branches in the upstream + Phase B that compute `minmax(%, ...)` templates from `offsetWidth` + measurements stay -- they read live layout and can't be + pre-computed. The forEach loop early-exits via a `couldFire` check + (two-or-more cells have content) before any `getComputedStyle` or + `querySelector` on the margin group; for this book that gate fails + on every page so the forEach is dominated by three `querySelector` + calls + three `classList.contains` reads per group. +- **Phase C** disappears entirely. Every branch in the upstream + Phase C (left/right vertical groups) is static at parse time -- + the upstream code has no offset measurement in those paths. +- All three prior PATCH blocks come out: `__mLookup` and + cross-page memoization had no callers left, and only the GCS + hoist stays (preserved as an inline batched read of `max-width` + inside the `couldFire` gate, for documents whose marginalia would + reach the offset fallbacks). + +### Verifying it + +Instrumented A/B on the same 1638-page book: + +| op | pre-emit (3 patches) | post-emit | Δ | +| --- | --- | --- | --- | +| `getComputedStyle` | 9,179 calls | **5,903 calls** | **-3,276 (-36%)** | +| `getBoundingClientRect` | 258,940 | 258,940 | unchanged (different code path) | +| `offsetWidth` | 0 | 0 | unchanged (gate never fires) | +| render wall-clock | 47.6 s | 46.0-47.0 s | noise | +| pdf size | 16.9 MB | 16.9 MB | unchanged (±27-bytes timestamp variance) | + +The -3,276 GCS drop is exactly two reads per page eliminated -- the +prior GCS hoist batched the per-cell `max-width` reads on +`.hasContent` cells (one per `top-right`, one per `bottom-right` +per page). The new `couldFire` early-exit skips them entirely. + +Wall-clock is in the noise, as predicted in the patch brief: this +moves work from runtime JS to parse-time CSS but the browser still +does the same cascade + layout work. The value here is **deleting +the hot spot from the bundle**, not shaving milliseconds. + +Smoke render of `book.bat`: 1638 pages, 16.9 MB output (within 54 +bytes of the pre-patch run -- ±27 bytes is the normal run-to-run +variance from Chrome's `/CreationDate` / `/ModDate` encoding), +render 45.8 s. + +### What's left in `finalizePage` + +Two phases, both with clear single-purpose justifications: + +``` +Phase A classify .hasContent per margin cell (per-page DOM) +Phase B' offset-fallback for auto-width minmax(%) templates + (dead code in this book; live for paged.js compatibility) +``` + +For our content Phase B' is dominated by an early `couldFire` +short-circuit. The method now reads top-to-bottom as "what does the +runtime *have* to do per page", with all the layered optimizations +unwound. There's nothing left to hoist. + +## Looking past `finalizePage`: where render time goes now + +With the `finalizePage` work landed, a fresh `--detach-pages +--time-hooks --cpu-profile` run on 1638 pages (2026-05-19) shows the +named handlers we hook -- the surface we own -- now account for +**under 1 ms/page combined**. Per-page handler costs, top of table: + +``` +hook::handler count total_ms per_page_ms +chunker.afterPageLayout (detach-pages) 1638 788.5 0.481 +chunker.afterPageLayout (#10) 1638 249.0 0.152 +chunker.renderNode 44365 185.6 0.113 +chunker.afterPageLayout (#6) 1638 100.9 0.062 +chunker.finalizePage 1638 71.8 0.044 +chunker.beforePageLayout 1638 68.6 0.042 +``` + +Render is ~49 s on this hardware (~30 ms/page average). Subtracting +the ~1 ms/page of handler work leaves ~29 ms/page of **paged.js +core**: chunking, layout probing, overflow detection, and the +text-break split. That's what the CPU profile attributes to: + +``` +self_ms self_% function source +22855 33.0 % getBoundingClientRect (native, called from JS) +19332 27.9 % (program) V8 overhead / idle + 9931 14.4 % removeOverflow paged.browser.js:2196 + 4280 6.2 % findEndToken paged.browser.js:2094 + 2364 3.4 % findElement paged.browser.js:638 (cache hit; cheap) + 1456 2.1 % insertBefore native + 1228 1.8 % createBreakToken paged.browser.js:1796 + 580 0.8 % afterPageLayout (paged.js) paged.browser.js:30381 +``` + +(Counter-check on the ratio: this run reads **5.59 x** rather than +the usual ~1.6 x. That's instrumentation skew -- both `--time-hooks` +and `--cpu-profile` wrap hot paths, and the sampling overhead is +proportionally larger on later pages. The handler totals and +self-time table are still accurate; the per-page growth curve isn't +trustworthy on instrumented runs.) + +So 33 % of render is `getBoundingClientRect` and another ~20 % is +inside `removeOverflow` + `findEndToken` -- paged.js's per-page +overflow-find + text-split path. That work isn't redundant: each +page genuinely has to decide where its content ends. The remaining +opportunities aren't *eliminating* work, they're *replacing the +algorithm* with something the browser can answer in one call. + +### Three places non-redundant work could be made simpler + +**1. `Layout.textBreak` -- replace per-word `gBCR` loop with a +single native call.** [paged.browser.js:2136](docs/lib/paged.browser.js:2136) +walks an overflowing Range word-by-word, calling +`getBoundingClientRect` on each `Range` to find which word crosses +the page boundary; if a word straddles it, it descends letter-by- +letter doing the same. On a long text node that's dozens to +hundreds of gBCR calls -- and `textBreak` is the inner loop of +`findOverflow`, so it fires on every page that overflows. + +A single `document.caretPositionFromPoint(x, vEnd)` (or +`caretRangeFromPoint` on Chromium) returns the exact text node + +offset at the boundary in **one** browser call. Equivalently, +`range.getClientRects()` returns every line box of the range in one +call, after which the crossing line is a simple `.find()`. Either +replaces an `O(words-in-overflow)` scan with `O(1)`. + +This is the highest-leverage candidate: even if it cuts only half +of the `gBCR` time, that's ~10 s off render. The risk is fidelity +-- we'd need to verify the substitute gives the *same* split point +as the word-walk on edge cases (RTL, hyphenated words, +`white-space: pre`, soft hyphens). Worth a prototype + diff against +the current bundle's output PDF. + +**2. `findOverflow` -- collapse three ancestor walks into one.** +Inside the per-node loop in +[paged.browser.js:1934](docs/lib/paged.browser.js:1934): + +```js +const insideTableCell = parentOf(node, "TD", rendered); +// ... +tableRow = parentOf(node, "TR", rendered); +// ... +const table = parentOf(tableRow, "TABLE", rendered); +``` + +Three separate ancestor traversals per node visited, each climbing +from `node` to `rendered`. One walk that emits the nearest TD/TR/ +TABLE together is ~10 lines and visits each ancestor once. Won't +match #1 for raw savings (this is in the same loop that's already +calling `getComputedStyle`, so a single-digit % gain at best) but +it's the easy follow-up. + +**3. Cache `getComputedStyle` per page.** Same loop, +[paged.browser.js:1969, 1974, 1992](docs/lib/paged.browser.js:1969): +up to four `getComputedStyle` calls per node visited (on the node, +its TD ancestor, and the parent TBODY/THEAD). The walker revisits +the same ancestors across many child nodes; a `WeakMap` populated lazily per page would dedupe. + +This one *is* deduplication-shaped, but it's the cheapest of the +three to land (no algorithmic change, no fidelity risk) and a clean +follow-up if #1 lands. + +### Probable bug worth surfacing separately + +[paged.browser.js:1998](docs/lib/paged.browser.js:1998): + +```js +const table = parentOf(tableRow, "TABLE", rendered); +const rowspan = table.querySelector("[colspan]"); +``` + +The local is named `rowspan` and the surrounding comment is about +rowspan-aware break handling, but the selector matches `colspan`. +Looks like a typo that's silently broken the rowspan path since the +bundle was vendored. Not a perf issue per se, but worth a separate +fix. + +### Strategic note + +Render and generate are now within ~20 s of each other (49 s vs +70 s on this run). Each second shaved off render moves total by +less than it used to, because `page.pdf()` is now the larger phase. +Item 1 above is the only remaining render change that plausibly +returns 10+ s; items 2 and 3 are <5 s each. + +After item 1 the remaining levers all live outside render. The +Chrome-outline experiment above shows generate isn't moved by +shifting outline work around (Chrome walking `h1..h6` itself costs +about what `parseOutline` + `setOutline` save -- net was +1.9 s). +The one generate-side lever we haven't tried is **`pageRanges` +sharding** -- run `page.pdf()` N times with disjoint page ranges on +parallel browser pages and concatenate with pdf-lib. Each shard +serialises only its slice and they run concurrently, so generate +collapses to roughly `60 s / N` plus a small concat pass. Listed +under "What might still be worth trying" above; it's the biggest +untried knob in the pipeline. + +## What happened when we tried item 1 + +The strategic note above was wrong about item 1 -- the binary-search +replacement for `textBreak` saves nothing, and the reason it saves +nothing reveals the actual structure of the remaining render cost. + +### Attempt A: binary-search `textBreak` + +Replaced the per-word-then-per-letter gBCR cascade in +[`Layout.textBreak`](docs/lib/paged.browser.js:2136) with a binary +search over offsets using a single-character probe `Range`. +Semantically equivalent (both return the smallest offset whose +character satisfies `left >= end || top >= vEnd`), should reduce +gBCR call count from O(words) to O(log nodeLength). + +Paired runs with `--detach-pages`: + +| run | baseline | binsearch | +| ---------- | -------- | --------- | +| render (1) | 47.73 s | 51.43 s | +| render (2) | 47.10 s | 47.12 s | +| **avg** | **47.4** | **49.3** | + +Wash, possibly small regression. PDF byte size and page count +identical. Reverted. + +### Attempt B: memoize `Page.create`'s `area.getBoundingClientRect` + +The CPU profile of attempt A's baseline pointed at a much bigger +target. Tracing gBCR's native frames up to their JS callers in the +profile graph: + +``` +caller gBCR time +create:2257 12,947 ms (69 %) +hasOverflow:1925 4,419 ms (24 %) +Layout:1443 586 ms +... +total native gBCR 18,424 ms +``` + +[`Page.create`](docs/lib/paged.browser.js:2257) does one +`area.getBoundingClientRect()` per page, right after the fresh +`insertBefore` / `appendChild` of the page DOM -- so each call +forces a synchronous layout pass. The `area`'s size is CSS-driven +and constant per template, so the gBCR should be cacheable. + +Memoized the result on the `pageTemplate` node (first page pays, +all subsequent same-template pages reuse). + +Profile diff (same `--detach-pages --cpu-profile` flags, paired): + +| caller | PRE | POST | Δ | +| ----------------- | --------- | --------- | ---------- | +| `create:2257` | 12,947 ms | 2 ms | **-12,945** | +| `Layout:1443` | 586 ms | 13,567 ms | **+12,981** | +| `hasOverflow:1925`| 4,419 ms | 4,533 ms | +114 | +| **total** | 18,424 ms | 18,554 ms | +130 | + +The cost moved, it didn't disappear. The memoization successfully +eliminated the gBCR at `create:2257` (from 12,947 ms to 2 ms), but +the layout flush that gBCR was driving still had to happen +somewhere -- it migrated to the next call in the per-page sequence, +[`Layout`'s constructor](docs/lib/paged.browser.js:1443): + +```js +this.bounds = this.element.getBoundingClientRect(); +this.parentBounds = this.element.offsetParent.getBoundingClientRect(); +``` + +Total gBCR self-time barely changed (+130 ms). Per-page ratio got +worse (1.77x -> 3.07x), probably because the deferred flush +accumulated more pending mutations before firing. Reverted. + +### The lesson + +**gBCR self-time in the profile is layout-flush attribution, not +JS call overhead.** Reducing the *number* of gBCR calls in a hot +path saves ~nothing if the layout flush they trigger has to fire +anyway. The cost lives in the flush itself, which is paged.js +measuring the live layout tree to decide where to break. + +Where the residual per-page layout cost actually comes from, after +`--detach-pages` has already trimmed completed pages out of the +layout tree, is probably one of: + +- **CSS counters** at + [`.pagedjs_pages`](docs/lib/paged.browser.js:27213) + (`counter-reset: pages ... footnote ...`). Counter resolution + walks the document, and counter-affecting elements per page + accumulate even when `display: none`. +- **`offsetParent` lookup** in `Layout`'s constructor. That's a + layout-tree walk to find the nearest positioned ancestor; cost + can grow with sibling count even when most siblings are + display:none. + +Neither is fixable by dedup-shaped optimizations in our bundle. + +The remaining `findOverflow` opportunities (items 2 and 3 in the +strategic note above -- collapsing ancestor walks, caching +`getComputedStyle`) might still be worth doing on their own +merits, but they're not where the gBCR time lives. + +### Methodology: compare profiles, not wall-clock + +Both attempts above showed wall-clock results that looked like +noise (47.7 vs 47.1 vs 51.4 s -- inside the run-to-run jitter band +on a busy dev machine). The actual structural change was only +visible by **diffing the bottom-up gBCR-caller breakdown across +two CPU profiles**. The `+12,981 ms` move from `create:2257` to +`Layout:1443` would have been invisible in a wall-clock A/B. + +For any future render-stage optimization work, the rule is: + +1. Run with `--cpu-profile` (paired pre/post, same flags). +2. Compare bottom-up self-time tables ([`analyze-profile.mjs`](perf/analyze-profile.mjs)) + and caller breakdowns ([`find-callers.mjs`](perf/find-callers.mjs); + point it at a profile + a callee name to see which frames are + paying for that callee's time -- essential for spotting gBCR + migration between callers). +3. Treat the wall-clock totals as a sanity check only -- they + confirm "did anything change" but not "where". + +This matters because: + +- **Render's per-page CPU work is dominated by native (layout, + DOM) frames.** V8 self-time deltas from JS-level dedup are + small compared to the layout flushes those calls trigger. +- **CPU sample percentages are stable across machine load.** A + busy machine slows the absolute wall-clock but the proportional + breakdown (gBCR = ~38 % of render samples) stays the same. +- **Migrations between attribution sites are common.** Moving a + gBCR off one call site usually re-attributes its layout cost to + the next caller in the sequence, not to nothing. + +For `generate` and `process` the picture is different (Chromium +internals and pdf-lib parse cost respectively); CPU profiles of +those phases are less informative because the work happens +outside the JS we can see, and wall-clock can be a fine +single-signal A/B. But anything inside paged.js's +render loop wants a profile diff, not a stopwatch. + +## Finding the residual O(n): it's not counters, it's siblings + +After the methodology shift to profile-diffing, two more A/Bs +finally pinned down where the residual per-page layout cost comes +from. Spoiler: it's not what we expected, and the fix is large. + +### Hypothesis 1: CSS counters + +The book uses `@bottom-right { content: counter(page); }` for page +numbers and `article.part-divider { counter-reset: page 0; }` for +per-part renumbering. paged.js's bundle puts +`counter-increment: page var(--pagedjs-page-counter-increment);` +on every `.pagedjs_page`. So on each new page's `@bottom-right`, +Chromium has to resolve `counter(page)` by walking preceding +`counter-increment: page` elements. + +Per CSS spec (`display: none` elements don't increment counters), +`--detach-pages`'s `display: none` strategy should already make +this O(1). But Chromium implementations have historically been +liberal about which display states still contribute. So: A/B by +commenting out the `counter-increment: page` rule entirely +([paged.browser.js:27198](docs/lib/paged.browser.js:27198)) and +diffing the profile. + +Result: + +| variant | render | total gBCR | gBCR %/render | ratio | +| ----------------------- | -------- | ---------- | ------------- | ----- | +| baseline (counters on) | 48.51 s | 18,424 ms | 38 % | 1.77x | +| counters disabled | 44.72 s | 21,514 ms | 48 % | 2.44x | + +Disabling counters did **not** reduce gBCR; it grew. The +wall-clock drop is run-to-run noise (counter resolution is genuinely +cheap on `display: none` siblings); the proportional growth means +removing counter-increment didn't save anything and may have shifted +work elsewhere. **Counter resolution is not the residual O(n).** + +### Hypothesis 2: sibling sweeps over `display: none` pages + +Re-reading the README on `--detach-pages`: the claim has always +been that `display: none` "removes a subtree from the layout tree +entirely". That's true for *layout* -- but Chromium's per-page +work also includes **style/selector resolution and rule matching**, +which walks the sibling list regardless of display state. With +1638 `.pagedjs_page` siblings under `.pagedjs_pages`, any per-page +selector evaluation is O(n). + +A/B: physically `removeChild` finalized pages instead of just +`display: none`, then re-append all at `afterRendered` so +`page.pdf()` sees them. The chunker passes `lastPage.element` to +`Page.create()` for ordered insertion, so the most recent finalized +page has to stay in the DOM -- detach one page behind. DOM holds +at most 2 pages at any moment: the in-flight one being laid out +plus the most recent finalized one. + +Probe modification (in [perf/detach-pages.js](perf/detach-pages.js)), +not shipped; page numbers come out wrong because `counter(page)` +doesn't accumulate, but the profile signal is clean. + +Result: + +| metric | display:none | removeChild | Δ | +| ------------------- | ------------ | ----------- | ------------ | +| **render** | **48.5 s** | **28.0 s** | **-20.5 s (-42 %)** | +| total native gBCR | 18,424 ms | 7,320 ms | -11,104 ms | +| `create:2257` gBCR | 12,947 ms | 1,073 ms | **-11,874 ms (12x)** | +| `hasOverflow:1925` | 4,419 ms | 5,119 ms | +700 ms | +| `Layout:1443` | 586 ms | 562 ms | flat | +| per-page ratio | 1.77x | 1.43x | flatter | + +`Page.create`'s layout flush -- the dominant per-page cost in +every profile we've seen -- went from 12.9 s to 1.1 s. That's the +work Chromium does to maintain style/selector state across the +sibling list, and it's now nearly constant per page. `hasOverflow` +still has a small residual growth but it's an order of magnitude +smaller and bounds the next plausible optimization target. + +**This is the largest single render-stage win we've found in this +investigation.** 20+ seconds off render, dropping render from the +larger phase to the smaller one (vs generate's ~60-70 s). + +### Shipping it + +The probe rendered the right number of pages but the output PDF +was incorrect in two ways: `counter(page)` doesn't accumulate +across detached siblings, and the re-attach loop appended pages +at the end instead of in original order. Both fixable; the +question was whether named strings (`string(chapter-title)`) +would survive detach. Verified empirically: they do. + +Final shipped change set: + +1. **[perf/detach-pages.js](perf/detach-pages.js)** -- rewrite + from `display:none` to physical `removeChild`. Keep the most + recent finalized page in the DOM (the chunker passes + `lastPage.element` to `Page.create` for ordered insertion); + detach one page behind. At `afterRendered`, detach the keeper + and re-append all in finalize order (which is document order). + +2. **[docs/lib/paged.browser.js](docs/lib/paged.browser.js) -- Counters handler.** + Track a running display-page counter on the handler instance, + increment per page during `afterPageLayout`, and write the + value as `--page-num: "N"` on the page wrapper's inline style. + On pages with `[data-counter-page-reset]` (the part dividers), + skip the increment -- mirrors the shipping behaviour of the + pre-existing CSS, where the injected per-page rule's + `counter-increment: none` takes effect but the + `counter-reset: page N` part doesn't (cascade/specificity + issue, not yet diagnosed; behaviour-preserving fix here, the + "intended" part-restart numbering would be a separate change). + +3. **[docs/assets/css/print.css](docs/assets/css/print.css) + + [_site-pdf copy](docs/_site-pdf/assets/css/print.css)** -- + replace `content: counter(page)` in `@bottom-right` with + `content: var(--page-num)`. The CSS custom property approach + keeps the existing cascade (suppression on `@page :first` and + `@page divider` still works, since those rules override the + `content` declaration entirely). + +Verification (1638-page book, all sample pages spot-checked +against the pre-detach output): + +- Page count matches (1638). +- `@bottom-right` page numbers byte-equivalent on every sampled + page (1, 2, 5, 6, 10, 100, 500, 1000, 1500, 1638). +- `@top-right` chapter titles byte-equivalent on every sampled + page -- named strings persist through detach. + +### Shipped numbers + +Profile diff (paired `--detach-pages --cpu-profile` runs): + +| metric | pre (display:none) | post (removeChild) | Δ | +| ------------------- | ------------------ | ------------------ | -------------------- | +| **render** | **48.5 s** | **26.3 s** | **-22.2 s (-46 %)** | +| total native gBCR | 18,424 ms | 7,455 ms | -10,969 ms (-60 %) | +| gBCR % / render | 38 % | 28 % | flatter | +| `create:2257` gBCR | 12,947 ms | **877 ms** | **-12,070 ms (15x)** | +| `hasOverflow:1925` | 4,419 ms | 4,590 ms | flat | +| `Layout:1443` | 586 ms | 463 ms | flat | +| per-page ratio | 1.77x | 1.18x | nearly flat | + +`Page.create`'s layout flush -- the largest single per-page cost +in every profile we'd seen -- went from 12.9 s to 0.9 s. The +remaining gBCR work in `hasOverflow` is now the largest layout +flush, but it's an order of magnitude smaller and only marginally +super-linear. + +### Where this leaves the picture + +The full menu of fixes against the original 207 s baseline: + +| fix | render saved | total saved | shipped | +| ----------------------------------- | ------------ | ----------- | ------- | +| `--detach-pages` (display:none) | ~55 s | ~55 s | yes | +| `--incremental` PDF update | - | ~32 s | yes | +| pdf-lib `parseSpeed: Fastest` | - | ~3 s | yes | +| `finalizePage` micro-optimizations | ~3 s | ~3 s | yes | +| **aggressive detach (removeChild)** | **~22 s** | **~22 s** | **yes** | +| **skip dead `findEndToken` path** | **~3.5 s** | **~3.5 s** | **yes** | +| **renderTo additive backoff** | **~4.25 s** | **~4.25 s** | **yes** | +| pageRanges sharding (generate) | - | 10-40 s | no | + +Render is now ~19 s on a 1638-page book, down from ~104 s in the +original baseline. The next bottleneck is unambiguously +`page.pdf()` -- ~60-70 s of Chromium-internal PDF serialisation +that's only addressable via the `pageRanges` sharding approach +(run multiple `page.pdf()` calls on disjoint page ranges in +parallel browsers, concatenate with pdf-lib). + +## What happened when we tried `createBreakToken` dedup + +With render down to ~26 s, the bottom-up profile points at three +JS bodies still worth looking at: + +``` +findEndToken self 3270 ms (12.4 %) +findElement self 1924 ms ( 7.3 %) +createBreakToken self 996 ms ( 3.8 %) +``` + +### Attempt A: cache `lastChild.lastChild` in `findEndToken` + +The descend-to-deepest-valid-descendant loop in +[`findEndToken`](docs/lib/paged.browser.js:2100) reads +`lastChild.lastChild` up to three times per iteration (while +condition, `validNode` check, assignment). Cache once. + +Profile diff (paired `--detach-pages --cpu-profile`): + +| function | PRE | POST | Δ | +| ---------------- | --------- | --------- | -------- | +| `findEndToken` | 3269.9 ms | 3108.0 ms | **-162** | +| `createBreakToken` | 995.8 ms | 964.9 ms | -31 | +| `findElement` | 1924.0 ms | 1767.2 ms | -157 | + +Real, modest win on `findEndToken` self-time. Plausibly the `-157` +on `findElement` is jitter (`findEndToken` doesn't call it), but +the `findEndToken` self drop is the only one we'd hang our hat on. +PDF byte-equivalent on all sampled pages. Shipped. + +### Attempt B: dedup `findElement(renderedNode, source)` in `createBreakToken` + +In the `!renderedNode` branch of +[`createBreakToken`](docs/lib/paged.browser.js:1796), +`findElement(renderedNode, source)` is called once at line 1817 +(inside `if (!temp.nextSibling)`) and again unconditionally at +line 1830. Hoist + reuse: at most one call per invocation that +takes this branch. + +Profile diff vs the post-Attempt-A baseline: + +| edge | PRE | POST | Δ | +| ----------------------------------- | --------- | --------- | ------ | +| `findElement` self | 1767 ms | 1892 ms | +125 | +| `findElement` <- `createBreakToken` | 1232 ms | 1308 ms | +76 | +| `findElement` <- `findEndToken` | 537 ms | 580 ms | +43 | + +The change cannot regress (it only ever removes one call), so the +deltas are jitter, not real cost. The give-away is the +`findElement <- findEndToken` edge: `findEndToken` wasn't touched +between the two runs, yet its attributed `findElement` total still +moved by +43 ms. That fixes the per-edge noise floor at ~40-80 ms +on this machine, which swallows whatever savings the dedup +produces. + +Read the other way: the `!renderedNode + !temp.nextSibling` branch +must fire rarely enough that removing one of its two `findElement` +calls doesn't register above this noise. We don't have call-count +instrumentation in the cpuprofile to confirm directly (`hitCount` +is samples-on-stack, not invocations), but a savings below +noise is functionally indistinguishable from no savings. + +Reverted. The lesson echoes Attempt A above (textBreak): if the +target branch fires rarely, the dedup's correctness is undeniable +but its effect is unmeasurable. + +### Attempt C: skip `findEndToken` when nobody reads its result + +`findEndToken` (3.1 s self) was the top remaining JS-body in the +post-A profile. Both Attempt A (cache the `.lastChild` access) and +the speculative validNode-caching extension above tried to make +it *faster*. Wrong question. The bottom-up profile shows where +cost lives, but a caller breakdown shows *why* it lives there: + +``` +findEndToken: self=3108 ms, total=3652 ms +callers (attributed total ms): + 3652.19 ms checkUnderflowAfterResize@paged.browser.js:2502 +``` + +`findEndToken` is called from exactly one place: +[`Page.checkUnderflowAfterResize`](docs/lib/paged.browser.js:2503), +which fires from a `ResizeObserver` whenever the page wrapper +*shrinks*. That happens on every overflow extraction during +normal render. The handler computes an `endToken` and hands it to +`this._onUnderflow(endToken)`. The only live registration of +`onUnderflow` in the bundle was an empty callback in +[`Chunker.addPage`](docs/lib/paged.browser.js:3251) with +commented-out intent (`// page.append(this.source, overflowToken);`). +The computed endToken was discarded every time. + +The fix is subtraction, not optimization: delete the no-op +registration so `_onUnderflow` stays `undefined` by default, and +add an early bail in `checkUnderflowAfterResize` so `findEndToken` +doesn't run when nobody can consume its result. A future caller +that wants the path back just calls `page.onUnderflow(realFn)` -- +the presence of a non-default handler is itself the activation +signal, no flag plumbing required. + +Profile diff (paired `--detach-pages --cpu-profile`): + +| function | PRE | POST | Δ | +| -------------- | --------- | --------- | ---------- | +| `findEndToken` | 3108.0 ms | 0.0 ms | **-3108** | +| `findElement` | 1767.2 ms | 1313.8 ms | **-453** | +| **render** | **25.75 s** | **22.26 s** | **-3.49 s (-14%)** | + +The `findElement` drop matches the previously-attributed +`findEndToken → findElement` total-time edge (~537 ms) within +noise; rest is jitter. PDF byte-equivalent on all sampled pages. +Shipped. + +### Attempt D: skip `Footnotes.afterPageLayout` when no `float: footnote` + +After Attempt C the next gBCR caller worth looking at was +[`Footnotes.afterPageLayout`](docs/lib/paged.browser.js:31477) at +~1114 ms attributed gBCR. The handler implements the CSS +`float: footnote` / `@footnote`-margin-box feature; the per-page +work begins with `noteContent.getBoundingClientRect()`, then +sets the inner content's `columnWidth`, then constructs a `Layout` +and runs `findOverflow` on the (for our document, empty) +`pagedjs_footnote_inner_content`. + +Our stylesheet declares `float: footnote` nowhere +(`grep -r "float: footnote" docs/_site-pdf/`), so the handler's +`this.footnotes` dict stays `{}` for the whole render and the +per-page work is in service of nothing. Same shape as Attempt C: +gate at the top with `if (Object.keys(this.footnotes).length === 0) return;`. + +Profile diff (paired `--detach-pages --cpu-profile`): + +| metric | PRE | POST | Δ | +| ------------------------------- | --------- | --------- | ---------- | +| total gBCR (attribution) | 7925 ms | 7756 ms | **-169** | +| ↳ Footnotes `afterPageLayout` | 1114 ms | 0 ms | -1114 | +| ↳ `hasOverflow` | 4687 ms | 4961 ms | **+274** | +| ↳ `create` | 913 ms | 1019 ms | **+106** | +| ↳ `Layout` | 446 ms | 543 ms | **+97** | +| ↳ next-page `afterPageLayout` | 0 ms | 431 ms | **+431** | +| **render wall-clock** | **22.26 s** | **23.14 s** | **+880 ms** | +| **per-page ratio (last/first)** | **1.50x** | **1.75x** | **worse** | + +Net gBCR reduction is only ~170 ms even though we eliminated 1114 ms +of attributed gBCR at the Footnotes call site. The missing ~944 ms +re-attributed to the next gBCR callers in the per-page sequence +(`hasOverflow`, `create`, `Layout`, and a previously-invisible +`afterPageLayout` at line 31986). And the per-page ratio went from +1.50x to 1.75x -- the late pages got *more* expensive, not less. + +That ratio regression is the give-away. The Footnotes' small +gBCR was apparently absorbing pending DOM mutations that, when +not flushed there, accumulated until the next gBCR (typically a +larger one) had to flush more state at once. This is the same +shape as the Page.create memoize trap documented above: removing +a layout flush at point A makes the flush at point B more +expensive, and the cost is super-linear in the deferred mutation +count. + +Reverted. + +### Attempt E: additive backoff on `renderTo`'s overflow check + +After Attempt D the lesson seemed to be "gBCR self-time is +layout-flush attribution; you can't skip a gBCR without the flush +migrating." Then re-reading the per-page render loop turned up a +case the migration framing doesn't actually cover. + +[`Layout.renderTo`](docs/lib/paged.browser.js:1478) calls +`findBreakToken` (→ `findOverflow` → `hasOverflow` → gBCR) when +the cumulative text length of appended nodes crosses `maxChars` +(default 1500). The gate looks like batching, but the reset is +asymmetric: + +```js +if (length >= this.maxChars) { + // ... layout hook, await images ... + newBreakToken = this.findBreakToken(wrapper, source, bounds, prevBreakToken); + if (newBreakToken) { + length = 0; // only reset on overflow found + this.rebuildTableFromBreakToken(newBreakToken, wrapper); + } +} +``` + +When no overflow is found, `length` doesn't reset -- it stays +above `maxChars` and the very next iteration's appended node +triggers another `findBreakToken`. The check fires *every +iteration past `maxChars`* until overflow trips. On a typical +~3000-char page that's ~30+ findBreakToken calls (each one a +hasOverflow gBCR = layout flush) before the actual break point. + +Replace with **additive backoff**: track a moving baseline +`lengthAtLastCheck` and only fire the check when `length - +lengthAtLastCheck >= maxChars`. Advance the baseline when no +overflow yet; reset both on overflow. Per-page check count drops +from O(nodes-past-maxChars) to O(page-chars / maxChars), typically +2-3 instead of 30+. + +Correctness rests on findBreakToken handling arbitrary overshoot: +`findOverflow` walks the wrapper to identify the overflowing +Range regardless of how much excess was appended past it, +`removeOverflow` extracts the excess via `extractContents`, and +`createBreakToken` returns a BreakToken at the right source +position. The chunker builds a fresh walker from `breakToken.node` +on the next page, so the trimmed content gets re-laid-out from +its correct source position. (The `break-inside: avoid` worry -- +that containers with extra trailing content might make different +break decisions -- turned out to be empirically unfounded.) + +Profile diff (paired `--detach-pages --cpu-profile`): + +| metric | PRE | POST | Δ | +| --------------------------- | --------- | --------- | ---------------- | +| **render wall-clock** | **23.73 s** | **19.48 s** | **-4.25 s (-18 %)** | +| total gBCR (attribution) | 8024 ms | 5705 ms | -2319 (-29 %) | +| ↳ `hasOverflow` gBCR | 4837 ms | 2725 ms | **-2112 (-44 %)** | +| ↳ `findOverflow` per-node | 438 ms | 166 ms | -272 | +| ↳ `create` / `Layout` / Footn. | unchanged within jitter | +| `removeOverflow` self | 457 ms | 370 ms | **-87 (improved)** | +| per-page ratio (last/first) | 1.64x | 1.60x | improved | + +No migration: Footnotes (1127 ms), create (955), Layout (534) +all flat. `removeOverflow` *dropped* despite the over-append +overshoot concern, because fewer findBreakToken invocations means +fewer extractContents passes, not larger ones -- the per-call +overshoot is bounded by maxChars (~1500 chars), small relative to +page capacity. + +Full pdftotext-MD5 match on pages 6, 100, 500, 1000, 1500, 1638. +Page count 1638. PDF byte size 126 bytes apart (metadata). + +Shipped. + +### The deeper lesson (a third pattern) + +Attempts B and D taught that you can't elide a *single* gBCR +because the layout flush migrates to the next caller. Attempt E +shows the framing was too narrow: you can't elide one flush, but +you can do *fewer total flushes* if you batch observations across +mutations. + +The three working patterns for render perf, distinguished: + +- **Reduce per-flush cost**: aggressive-detach (-22 s). Shrink the + layout tree by physically removing finalized pages so each + remaining flush has less style/selector state to maintain. + +- **Reduce flush count**: renderTo additive backoff (-4.25 s). + When mutations between observations don't independently need + observing, query once per batch instead of per-mutation. The + per-flush cost grows slightly with deferred mutations but + amortizes well below the linear scan. + +- **Delete dead JS**: skip-findEndToken (-3.5 s), Page.create + hoisted CSS, etc. Walk up the call chain; if the consumer + doesn't read the value, delete the production. Works whenever + the JS self-time is genuinely JS, not flush attribution. + +What *doesn't* work: try to elide one specific gBCR while +preserving the mutation pattern around it (Attempts B and D). The +flush re-attributes to the next gBCR in the per-page sequence, +which then has to flush a larger backlog -- net wash or +regression. + +The diagnostic question to tell these apart: *what does the +mutation rhythm look like between consecutive gBCR calls?* If it's +"mutation, gBCR, mutation, gBCR, ..." (renderTo's per-iteration +check), batching wins. If it's "one mutation, multiple gBCRs" +(Page.create memoize, Footnotes skip), each gBCR is on the same +mutation state and the flush has to happen for the *next* +mutation regardless of which JS asks. + +### Where this leaves the picture + +Render is now ~19 s on a 1638-page book, down from ~104 s in the +original baseline. The JS-body profile after Attempt E: + +``` +findElement self 1373 ms ( 7.1 %) +createBreakToken self 1027 ms ( 5.3 %) +removeOverflow self 370 ms ( 1.9 %) +afterPageLayout self 239 ms ( 1.2 %) +``` + +None of these are individually addressable -- they're load-bearing +work in the per-page break loop. `findElement` already takes the +dictionary fast path. `pageRanges` sharding of `generate` (~60-70 s +of `page.pdf()`) is the only remaining knob with a profile target +large enough to move the wall-clock total meaningfully, and it's +single-threaded-inaddressable (requires multiple Chromium +processes + pdf-lib concatenation). diff --git a/perf/analyze-profile.mjs b/perf/analyze-profile.mjs new file mode 100644 index 00000000..201266a5 --- /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/compare-outlines.mjs b/perf/compare-outlines.mjs new file mode 100644 index 00000000..b5552904 --- /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/detach-pages.js b/perf/detach-pages.js new file mode 100644 index 00000000..0a6689b8 --- /dev/null +++ b/perf/detach-pages.js @@ -0,0 +1,79 @@ +// Paged.Handler that physically removes each finalized page from the +// layout tree as soon as paged.js finishes laying it out, then restores +// them in original order at afterRendered before page.pdf() runs. +// +// The hot path in render is Page.create's getBoundingClientRect on the +// freshly-inserted page area. That call forces a synchronous layout +// flush which, even with display:none on previous pages, scales with +// the .pagedjs_pages sibling count because Chromium's per-page +// style/selector resolution walks the sibling list regardless of +// display state. On a 1638-page book this is the dominant O(n) +// per-page cost. +// +// `display: none` removes the subtree from layout but NOT from style +// resolution. Physically detaching the previous pages from the DOM +// collapses the layout flush from ~8 ms/page (growing) to ~0.7 ms/page +// (flat). See perf/README.md "Finding the residual O(n)" for the +// supporting profile diff. +// +// Implementation notes: +// - The chunker passes `lastPage.element` to Page.create for ordered +// insertion (paged.browser.js:3201). That element has to stay in +// the DOM, so we detach one page behind: when page N+1 finalizes, +// we remove page N (the previous _pendingDetach) and stash page N+1 +// as the new pending one. +// - At afterRendered we also detach the last-in-DOM page, then +// re-append everyone in original index order so page.pdf() reads a +// correctly-ordered document. +// - CSS counters do not accumulate across detached pages, so the +// book's @bottom-right page numbers can no longer use +// `content: counter(page)`. We piggyback on the Counters handler +// in the vendored paged.js bundle: it writes a `--page-num` +// custom property to each page wrapper as part of afterPageLayout, +// and print.css reads it via `content: var(--page-num)`. +// - Named strings (string-set / string()) survive because Chromium +// tracks named-string state at the chunker level, not by re-walking +// the DOM. Verified empirically -- chapter titles in @top-right +// render correctly after detach. + +(() => { + class DetachPagesHandler extends Paged.Handler { + constructor(chunker, polisher, caller) { + super(chunker, polisher, caller); + this._detached = []; // pages removed from DOM, in finalize order + this._pendingDetach = null; // most recent finalized page, still in DOM + this._pagesArea = null; // captured at first finalizePage + } + finalizePage(pageElement /* , page, breakToken, chunker */) { + if (this._pendingDetach) { + const page = this._pendingDetach; + if (page.parentNode) { + page.parentNode.removeChild(page); + this._detached.push(page); + } + } + this._pendingDetach = pageElement; + if (!this._pagesArea) { + this._pagesArea = pageElement.parentNode; + } + } + afterRendered(/* pages */) { + // Detach the last keeper too so we can rebuild order from scratch. + if (this._pendingDetach && this._pendingDetach.parentNode) { + this._detached.push(this._pendingDetach); + this._pendingDetach.parentNode.removeChild(this._pendingDetach); + } + // Re-append in finalize order, which is document order. + if (this._pagesArea) { + for (const page of this._detached) { + this._pagesArea.appendChild(page); + } + } + this._detached.length = 0; + this._pendingDetach = null; + this._pagesArea = null; + } + } + Paged.registerHandlers(DetachPagesHandler); + console.log('[detach-pages] handler registered (aggressive-detach variant)'); +})(); diff --git a/perf/find-callers.mjs b/perf/find-callers.mjs new file mode 100644 index 00000000..a93603e0 --- /dev/null +++ b/perf/find-callers.mjs @@ -0,0 +1,82 @@ +// Attribute a callee's self+descendant time to each direct caller. +// +// Reads a V8 .cpuprofile and, for every node whose `callFrame.functionName` +// matches the given target, walks down its subtree to sum self-time + +// descendant samples, then attributes that total to each parent frame. +// +// Companion to analyze-profile.mjs: that one answers "where is cost", +// this one answers "who is paying for the cost". Used throughout the +// perf README's post-mortems to detect gBCR migration between callers +// (Page.create memoize, Footnotes-handler skip) and dead-call patterns +// (findEndToken -> checkUnderflowAfterResize -> empty onUnderflow). +// +// Usage: +// node perf/find-callers.mjs +// +// Example: +// node perf/find-callers.mjs after/render.cpuprofile getBoundingClientRect +// node perf/find-callers.mjs after/render.cpuprofile findEndToken +// +// Caveats: hitCount is samples-on-stack, not invocations -- this script +// reports time, not call counts. For call counts use --instrument with +// perf/instrument-flush-ops.js. + +import { readFileSync } from 'node:fs'; + +const [profilePath, targetName] = process.argv.slice(2); +if (!profilePath || !targetName) { + console.error('usage: node find-callers.mjs '); + process.exit(2); +} + +const profile = JSON.parse(readFileSync(profilePath, 'utf8')); +const totalUs = profile.endTime - profile.startTime; +const usPerSample = totalUs / profile.samples.length; + +const byId = new Map(); +for (const n of profile.nodes) byId.set(n.id, n); + +const parentOf = new Map(); +for (const n of profile.nodes) { + for (const c of n.children || []) { + if (!parentOf.has(c)) parentOf.set(c, []); + parentOf.get(c).push(n.id); + } +} + +const callerHits = new Map(); +let targetSelfHits = 0; +let targetTotalHits = 0; +for (const n of profile.nodes) { + const fn = n.callFrame?.functionName || ''; + if (fn !== targetName) continue; + targetSelfHits += n.hitCount || 0; + // total = self + all descendants + const stack = [n.id]; + let totalHits = 0; + const seen = new Set(); + while (stack.length) { + const id = stack.pop(); + if (seen.has(id)) continue; + seen.add(id); + const node = byId.get(id); + totalHits += node.hitCount || 0; + for (const c of node.children || []) stack.push(c); + } + targetTotalHits += totalHits; + const parents = parentOf.get(n.id) || []; + for (const pid of parents) { + const p = byId.get(pid); + const pkey = `${p.callFrame?.functionName || '(anon)'}@${p.callFrame?.url || ''}:${p.callFrame?.lineNumber ?? '?'}`; + callerHits.set(pkey, (callerHits.get(pkey) || 0) + totalHits); + } +} + +console.log(`${targetName}: self=${(targetSelfHits * usPerSample / 1000).toFixed(2)}ms, total=${(targetTotalHits * usPerSample / 1000).toFixed(2)}ms`); +console.log('callers (attributed total ms):'); +const rows = [...callerHits.entries()].sort((a, b) => b[1] - a[1]); +for (const [k, hits] of rows) { + const ms = hits * usPerSample / 1000; + if (ms < 1) continue; + console.log(` ${ms.toFixed(2).padStart(8)} ms ${k}`); +} diff --git a/perf/incremental-pdf.mjs b/perf/incremental-pdf.mjs new file mode 100644 index 00000000..32b9b741 --- /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/instrument-flush-ops.js b/perf/instrument-flush-ops.js new file mode 100644 index 00000000..282eb434 --- /dev/null +++ b/perf/instrument-flush-ops.js @@ -0,0 +1,174 @@ +// 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', + // 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 }; + + 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); + } + + // 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 { + 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 new file mode 100644 index 00000000..94eedcef --- /dev/null +++ b/perf/measure.mjs @@ -0,0 +1,425 @@ +// 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 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] [--instrument] [--time-hooks] +// [--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 +// 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). +// +// --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// +// +// --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'; +import { mkdirSync, writeFileSync, existsSync } from 'node:fs'; +import puppeteer from 'puppeteer'; +import { PDFDocument, ParseSpeeds } from 'pdf-lib'; +// Shared with docs/render-book.mjs -- the helpers and the paged.js +// bundle live under docs/lib/ now that we've dropped the pagedjs-cli +// dependency. Importing from there guarantees the harness measures the +// same code that production runs. +import { parseOutline, setOutline } from '../docs/lib/outline.mjs'; +import { setMetadata } from '../docs/lib/postprocesser.mjs'; +import { applyOutlineAndMetadataIncremental } from './incremental-pdf.mjs'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const args = process.argv.slice(2); +let inputArg = null; +let outArg = null; +let keepOpen = false; +let cpuProfile = false; +let cpuSampling = 1000; // microseconds +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]; + 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 (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); } +} + +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, '..', 'docs', 'lib', 'paged.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 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}`); + 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. + // + // --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', + '--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]') || t.startsWith('[detach-pages]') || + t.startsWith('[instrument]') || t.startsWith(' ')) { + 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 }); + if (detachPages) { + await page.addScriptTag({ path: detachPagesPath }); + } + 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 + // 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) { + 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; + + 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 -------------------------------------------------------- + // 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; + }); + + // 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 = chromeOutline ? [] : 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 }, + // 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; + + 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 --------------------------------------------------------- + // 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(); + 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 { + // 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, { parseSpeed: ParseSpeeds.Fastest }); + 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({ objectsPerTick: Infinity }); + const saveMs = Date.now() - tSaveStart; + + processBreakdown = { loadMs, setOutlineMs, saveMs }; + } + const tProcEnd = Date.now(); + const processMs = tProcEnd - tProcStart; + 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)}`); + + // 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, + cpuProfile: profilePath, + phases: { + render: { + ms: renderMs, + perPage: timing.pages, + phaseMarks: timing.phases, + }, + generate: { + ms: generateMs, + parseOutlineMs, + pagePdfMs: pdfMs, + rawPdfBytes: rawPdf.length, + }, + process: { + ms: processMs, + mode: incremental ? 'incremental' : 'pdf-lib-roundtrip', + ...processBreakdown, + }, + }, + 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)} (${incremental ? 'incremental update (append outline + updated catalog/info)' : '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-lock.json b/perf/package-lock.json new file mode 100644 index 00000000..8fdc6810 --- /dev/null +++ b/perf/package-lock.json @@ -0,0 +1,1379 @@ +{ + "name": "tbasic-docs-perf", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tbasic-docs-perf", + "version": "0.0.0", + "devDependencies": { + "html-entities": "^2.5.2", + "pdf-lib": "^1.17.1", + "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/@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/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/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/ast-types/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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-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/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-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/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/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/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/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/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/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/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/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/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/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/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/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/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/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-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/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/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/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", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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-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/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/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/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/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" + } + } + } +} diff --git a/perf/package.json b/perf/package.json new file mode 100644 index 00000000..5229b456 --- /dev/null +++ b/perf/package.json @@ -0,0 +1,15 @@ +{ + "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": { + "html-entities": "^2.5.2", + "pdf-lib": "^1.17.1", + "puppeteer": "^22.15.0" + } +} diff --git a/perf/probe-chrome-outline.mjs b/perf/probe-chrome-outline.mjs new file mode 100644 index 00000000..bd27660e --- /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 00000000..6a17568c --- /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 00000000..6edc010d --- /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 00000000..dfb5a44e --- /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`); +} diff --git a/perf/run.bat b/perf/run.bat new file mode 100644 index 00000000..c130ed5e --- /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/test-incremental.mjs b/perf/test-incremental.mjs new file mode 100644 index 00000000..006f584e --- /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); diff --git a/perf/time-hooks.js b/perf/time-hooks.js new file mode 100644 index 00000000..f035e226 --- /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'); +})(); diff --git a/perf/timing-handler.js b/perf/timing-handler.js new file mode 100644 index 00000000..19268c6c --- /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); +})();