pagyra-js 0.0.19 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/dist/assets/fonts/licenses/selawik/SIL Open Font License.txt +43 -0
- package/dist/assets/fonts/ttf/arimo/Arimo-Bold.ttf +0 -0
- package/dist/assets/fonts/ttf/arimo/Arimo-BoldItalic.ttf +0 -0
- package/dist/assets/fonts/ttf/arimo/Arimo-Italic.ttf +0 -0
- package/dist/assets/fonts/ttf/arimo/Arimo-Regular.ttf +0 -0
- package/dist/assets/fonts/ttf/cinzeldecorative/CinzelDecorative-Black.ttf +0 -0
- package/dist/assets/fonts/ttf/cinzeldecorative/CinzelDecorative-Bold.ttf +0 -0
- package/dist/assets/fonts/ttf/cinzeldecorative/CinzelDecorative-Regular.ttf +0 -0
- package/dist/assets/fonts/ttf/dejavu/DejaVuSans.ttf +0 -0
- package/dist/assets/fonts/ttf/firecode/FiraCode-Bold.ttf +0 -0
- package/dist/assets/fonts/ttf/firecode/FiraCode-Light.ttf +0 -0
- package/dist/assets/fonts/ttf/firecode/FiraCode-Medium.ttf +0 -0
- package/dist/assets/fonts/ttf/firecode/FiraCode-Regular.ttf +0 -0
- package/dist/assets/fonts/ttf/firecode/FiraCode-SemiBold.ttf +0 -0
- package/dist/assets/fonts/ttf/notoemoji/NotoEmoji-Bold.ttf +0 -0
- package/dist/assets/fonts/ttf/notoemoji/NotoEmoji-Light.ttf +0 -0
- package/dist/assets/fonts/ttf/notoemoji/NotoEmoji-Medium.ttf +0 -0
- package/dist/assets/fonts/ttf/notoemoji/NotoEmoji-Regular.ttf +0 -0
- package/dist/assets/fonts/ttf/notoemoji/NotoEmoji-SemiBold.ttf +0 -0
- package/dist/assets/fonts/ttf/notosans/NotoSans-Regular.ttf +0 -0
- package/dist/assets/fonts/ttf/roboto/Roboto-Bold.ttf +0 -0
- package/dist/assets/fonts/ttf/roboto/Roboto-BoldItalic.ttf +0 -0
- package/dist/assets/fonts/ttf/roboto/Roboto-Italic.ttf +0 -0
- package/dist/assets/fonts/ttf/roboto/Roboto-Regular.ttf +0 -0
- package/dist/assets/fonts/ttf/selawik/selawk.ttf +0 -0
- package/dist/assets/fonts/ttf/selawik/selawkb.ttf +0 -0
- package/dist/assets/fonts/ttf/selawik/selawkl.ttf +0 -0
- package/dist/assets/fonts/ttf/selawik/selawksb.ttf +0 -0
- package/dist/assets/fonts/ttf/selawik/selawksl.ttf +0 -0
- package/dist/assets/fonts/ttf/stixtwomath/STIXTwoMath-Regular.ttf +0 -0
- package/dist/assets/fonts/ttf/tinos/Tinos-Bold.ttf +0 -0
- package/dist/assets/fonts/ttf/tinos/Tinos-BoldItalic.ttf +0 -0
- package/dist/assets/fonts/ttf/tinos/Tinos-Italic.ttf +0 -0
- package/dist/assets/fonts/ttf/tinos/Tinos-Regular.ttf +0 -0
- package/dist/assets/fonts/woff/lato/lato-latin-400-italic.woff +0 -0
- package/dist/assets/fonts/woff/lato/lato-latin-400-normal.woff +0 -0
- package/dist/assets/fonts/woff/lato/lato-latin-700-italic.woff +0 -0
- package/dist/assets/fonts/woff/lato/lato-latin-700-normal.woff +0 -0
- package/dist/assets/fonts/woff2/caveat/Caveat-Bold.woff2 +0 -0
- package/dist/assets/fonts/woff2/caveat/Caveat-Regular.woff2 +0 -0
- package/dist/assets/fonts/woff2/lato/lato-latin-400-italic.woff2 +0 -0
- package/dist/assets/fonts/woff2/lato/lato-latin-400-normal.woff2 +0 -0
- package/dist/assets/fonts/woff2/lato/lato-latin-700-italic.woff2 +0 -0
- package/dist/assets/fonts/woff2/lato/lato-latin-700-normal.woff2 +0 -0
- package/dist/browser/pagyra.min.js +34 -34
- package/dist/browser/pagyra.min.js.map +4 -4
- package/dist/playground/server.js +2 -0
- package/dist/src/css/compute-style/base-options.d.ts +7 -0
- package/dist/src/css/compute-style/base-options.js +24 -0
- package/dist/src/css/compute-style/declarations.d.ts +10 -0
- package/dist/src/css/compute-style/declarations.js +77 -0
- package/dist/src/css/compute-style/decoration.d.ts +8 -0
- package/dist/src/css/compute-style/decoration.js +55 -0
- package/dist/src/css/compute-style/defaults.d.ts +3 -0
- package/dist/src/css/compute-style/defaults.js +34 -0
- package/dist/src/css/compute-style/display.d.ts +3 -0
- package/dist/src/css/compute-style/display.js +85 -0
- package/dist/src/css/compute-style/float.d.ts +2 -0
- package/dist/src/css/compute-style/float.js +13 -0
- package/dist/src/css/compute-style/font.d.ts +12 -0
- package/dist/src/css/compute-style/font.js +57 -0
- package/dist/src/css/compute-style/overrides.d.ts +3 -0
- package/dist/src/css/compute-style/overrides.js +241 -0
- package/dist/src/css/compute-style.d.ts +2 -0
- package/dist/src/css/compute-style.js +34 -487
- package/dist/src/css/enums.d.ts +4 -0
- package/dist/src/css/enums.js +5 -0
- package/dist/src/css/layout-property-resolver.js +30 -18
- package/dist/src/css/length.d.ts +26 -2
- package/dist/src/css/length.js +48 -0
- package/dist/src/css/parsers/background-parser.js +1 -1
- package/dist/src/css/parsers/calc-parser.d.ts +2 -0
- package/dist/src/css/parsers/calc-parser.js +310 -0
- package/dist/src/css/parsers/content-parser.d.ts +2 -1
- package/dist/src/css/parsers/content-parser.js +7 -2
- package/dist/src/css/parsers/dimension-parser.js +37 -18
- package/dist/src/css/parsers/display-flex-parser.d.ts +4 -0
- package/dist/src/css/parsers/display-flex-parser.js +97 -0
- package/dist/src/css/parsers/filter-parser.d.ts +14 -0
- package/dist/src/css/parsers/filter-parser.js +255 -0
- package/dist/src/css/parsers/grid-parser-extended.d.ts +1 -0
- package/dist/src/css/parsers/grid-parser-extended.js +40 -1
- package/dist/src/css/parsers/grid-parser.d.ts +5 -2
- package/dist/src/css/parsers/grid-parser.js +71 -7
- package/dist/src/css/parsers/length-parser.d.ts +8 -3
- package/dist/src/css/parsers/length-parser.js +45 -2
- package/dist/src/css/parsers/margin-block-parser.js +3 -3
- package/dist/src/css/parsers/margin-parser.js +3 -3
- package/dist/src/css/parsers/padding-block-parser.js +3 -3
- package/dist/src/css/parsers/padding-inline-parser.js +3 -3
- package/dist/src/css/parsers/padding-parser.js +6 -6
- package/dist/src/css/parsers/position-parser.js +2 -22
- package/dist/src/css/parsers/register-parsers.js +29 -2
- package/dist/src/css/parsers/word-break-parser.d.ts +2 -0
- package/dist/src/css/parsers/word-break-parser.js +23 -0
- package/dist/src/css/properties/grid.d.ts +16 -2
- package/dist/src/css/properties/layout.d.ts +3 -1
- package/dist/src/css/properties/layout.js +1 -1
- package/dist/src/css/properties/misc.d.ts +5 -0
- package/dist/src/css/properties/typography.d.ts +3 -0
- package/dist/src/css/properties/visual.d.ts +36 -0
- package/dist/src/css/shorthands/box-shorthand.d.ts +2 -2
- package/dist/src/css/style-inheritance.d.ts +2 -1
- package/dist/src/css/style-inheritance.js +1 -0
- package/dist/src/css/style.d.ts +30 -10
- package/dist/src/css/style.js +8 -1
- package/dist/src/css/ua-defaults/base-defaults.d.ts +1 -0
- package/dist/src/css/ua-defaults/base-defaults.js +10 -1
- package/dist/src/css/ua-defaults/element-defaults.js +0 -2
- package/dist/src/html/css/parse-css.d.ts +2 -0
- package/dist/src/html/css/parse-css.js +32 -3
- package/dist/src/html/dom-converter/background-images.d.ts +3 -0
- package/dist/src/html/dom-converter/background-images.js +88 -0
- package/dist/src/html/dom-converter/convert-dom-node.d.ts +5 -0
- package/dist/src/html/dom-converter/convert-dom-node.js +81 -0
- package/dist/src/html/dom-converter/handlers/br-handler.d.ts +2 -0
- package/dist/src/html/dom-converter/handlers/br-handler.js +20 -0
- package/dist/src/html/dom-converter/handlers/form-control-handler.d.ts +2 -0
- package/dist/src/html/dom-converter/handlers/form-control-handler.js +28 -0
- package/dist/src/html/dom-converter/handlers/img-handler.d.ts +2 -0
- package/dist/src/html/dom-converter/handlers/img-handler.js +4 -0
- package/dist/src/html/dom-converter/handlers/index.d.ts +4 -0
- package/dist/src/html/dom-converter/handlers/index.js +19 -0
- package/dist/src/html/dom-converter/handlers/svg-handler.d.ts +2 -0
- package/dist/src/html/dom-converter/handlers/svg-handler.js +32 -0
- package/dist/src/html/dom-converter/handlers/types.d.ts +12 -0
- package/dist/src/html/dom-converter/handlers/types.js +2 -0
- package/dist/src/html/dom-converter/helpers.d.ts +7 -0
- package/dist/src/html/dom-converter/helpers.js +35 -0
- package/dist/src/html/dom-converter/index.d.ts +1 -0
- package/dist/src/html/dom-converter/index.js +1 -0
- package/dist/src/html/dom-converter/pseudo-elements.d.ts +6 -0
- package/dist/src/html/dom-converter/pseudo-elements.js +48 -0
- package/dist/src/html/dom-converter/text.d.ts +15 -0
- package/dist/src/html/dom-converter/text.js +170 -0
- package/dist/src/html/dom-converter.d.ts +1 -5
- package/dist/src/html/dom-converter.js +1 -412
- package/dist/src/html/image-converter.d.ts +5 -0
- package/dist/src/html/image-converter.js +8 -3
- package/dist/src/html-to-pdf/document-css.d.ts +14 -0
- package/dist/src/html-to-pdf/document-css.js +45 -0
- package/dist/src/html-to-pdf/fonts.d.ts +16 -0
- package/dist/src/html-to-pdf/fonts.js +74 -0
- package/dist/src/html-to-pdf/header-footer.d.ts +14 -0
- package/dist/src/html-to-pdf/header-footer.js +101 -0
- package/dist/src/html-to-pdf/html-parser.d.ts +6 -0
- package/dist/src/html-to-pdf/html-parser.js +81 -0
- package/dist/src/html-to-pdf/index.d.ts +3 -0
- package/dist/src/html-to-pdf/index.js +2 -0
- package/dist/src/html-to-pdf/layout-build.d.ts +37 -0
- package/dist/src/html-to-pdf/layout-build.js +73 -0
- package/dist/src/html-to-pdf/prepare-html-render.d.ts +2 -0
- package/dist/src/html-to-pdf/prepare-html-render.js +121 -0
- package/dist/src/html-to-pdf/render-finalize.d.ts +15 -0
- package/dist/src/html-to-pdf/render-finalize.js +27 -0
- package/dist/src/html-to-pdf/render-html-to-pdf.d.ts +3 -0
- package/dist/src/html-to-pdf/render-html-to-pdf.js +25 -0
- package/dist/src/html-to-pdf/resource-loader.d.ts +6 -0
- package/dist/src/html-to-pdf/resource-loader.js +120 -0
- package/dist/src/html-to-pdf/types.d.ts +38 -0
- package/dist/src/html-to-pdf/types.js +2 -0
- package/dist/src/html-to-pdf.d.ts +1 -37
- package/dist/src/html-to-pdf.js +1 -537
- package/dist/src/image/js-png-backend.d.ts +7 -0
- package/dist/src/image/js-png-backend.js +9 -0
- package/dist/src/image/png-backend.d.ts +5 -0
- package/dist/src/image/png-backend.js +1 -0
- package/dist/src/image/png-wasm-loader.d.ts +5 -0
- package/dist/src/image/png-wasm-loader.js +59 -0
- package/dist/src/image/wasm/png_decoder_wasm.d.ts +8 -0
- package/dist/src/image/wasm/png_decoder_wasm.js +24 -0
- package/dist/src/image/wasm/png_decoder_wasm_bg.js +16 -0
- package/dist/src/image/wasm-png-backend.d.ts +6 -0
- package/dist/src/image/wasm-png-backend.js +17 -0
- package/dist/src/layout/counter.d.ts +1 -2
- package/dist/src/layout/counter.js +18 -18
- package/dist/src/layout/inline/inline-utils.d.ts +1 -1
- package/dist/src/layout/inline/inline-utils.js +8 -7
- package/dist/src/layout/inline/layout.js +16 -3
- package/dist/src/layout/inline/run-placer.d.ts +1 -0
- package/dist/src/layout/inline/run-placer.js +2 -10
- package/dist/src/layout/pipeline/out-of-flow-manager.js +25 -1
- package/dist/src/layout/strategies/block.js +35 -24
- package/dist/src/layout/strategies/flex.js +305 -61
- package/dist/src/layout/strategies/form.d.ts +2 -0
- package/dist/src/layout/strategies/form.js +38 -13
- package/dist/src/layout/strategies/grid.js +166 -29
- package/dist/src/layout/strategies/image.js +53 -27
- package/dist/src/layout/strategies/inline.js +26 -21
- package/dist/src/layout/strategies/table.js +26 -18
- package/dist/src/layout/utils/content-measurer.d.ts +1 -1
- package/dist/src/layout/utils/content-measurer.js +8 -7
- package/dist/src/layout/utils/floats.d.ts +1 -0
- package/dist/src/layout/utils/floats.js +14 -12
- package/dist/src/layout/utils/margin.d.ts +4 -4
- package/dist/src/layout/utils/margin.js +20 -16
- package/dist/src/layout/utils/node-math.d.ts +12 -6
- package/dist/src/layout/utils/node-math.js +71 -41
- package/dist/src/layout/utils/sizing.js +2 -1
- package/dist/src/pdf/font-subset/font-registry.d.ts +6 -0
- package/dist/src/pdf/font-subset/font-registry.js +30 -2
- package/dist/src/pdf/header-footer-painter.d.ts +2 -0
- package/dist/src/pdf/header-footer-painter.js +52 -4
- package/dist/src/pdf/header-footer-renderer.js +12 -1
- package/dist/src/pdf/layout-tree-builder.js +5 -1
- package/dist/src/pdf/page-painter.js +13 -0
- package/dist/src/pdf/pagination.js +2 -2
- package/dist/src/pdf/renderer/box-painter.js +28 -3
- package/dist/src/pdf/renderer/page-paint.js +12 -3
- package/dist/src/pdf/renderers/radius-utils.js +31 -38
- package/dist/src/pdf/renderers/shape-renderer.js +1 -1
- package/dist/src/pdf/renderers/shape-utils.js +1 -1
- package/dist/src/pdf/renderers/text-renderer.d.ts +9 -1
- package/dist/src/pdf/renderers/text-renderer.js +36 -2
- package/dist/src/pdf/stacking/build-stacking-contexts.js +1 -2
- package/dist/src/pdf/stacking/resolve-paint-order.d.ts +5 -6
- package/dist/src/pdf/stacking/resolve-paint-order.js +29 -9
- package/dist/src/pdf/stacking/types.d.ts +14 -0
- package/dist/src/pdf/svg/shape-renderer.js +47 -20
- package/dist/src/pdf/types.d.ts +7 -1
- package/dist/src/pdf/utils/border-radius-utils.js +31 -38
- package/dist/src/pdf/utils/color-utils.js +17 -2
- package/dist/src/pdf/utils/filter-utils.d.ts +29 -0
- package/dist/src/pdf/utils/filter-utils.js +85 -0
- package/dist/src/pdf/utils/node-text-run-factory.js +1 -1
- package/dist/src/pdf/utils/text-layout-adjuster.d.ts +0 -8
- package/dist/src/pdf/utils/text-layout-adjuster.js +12 -9
- package/dist/src/shim/css-browser.d.ts +14 -9
- package/dist/src/shim/css-browser.js +50 -39
- package/dist/src/units/units.d.ts +1 -1
- package/dist/tests/css/box-sizing.spec.d.ts +1 -0
- package/dist/tests/css/box-sizing.spec.js +46 -0
- package/dist/tests/css/calc-parser.spec.d.ts +1 -0
- package/dist/tests/css/calc-parser.spec.js +68 -0
- package/dist/tests/css/container-query-units.spec.d.ts +1 -0
- package/dist/tests/css/container-query-units.spec.js +64 -0
- package/dist/tests/css/content-parser.spec.js +13 -0
- package/dist/tests/css/filter-parser.spec.d.ts +1 -0
- package/dist/tests/css/filter-parser.spec.js +116 -0
- package/dist/tests/css/flex-shorthand.spec.d.ts +1 -0
- package/dist/tests/css/flex-shorthand.spec.js +45 -0
- package/dist/tests/css/grid-clamp.spec.d.ts +1 -0
- package/dist/tests/css/grid-clamp.spec.js +82 -0
- package/dist/tests/css/parse-css-pseudo.spec.d.ts +1 -0
- package/dist/tests/css/parse-css-pseudo.spec.js +26 -0
- package/dist/tests/helpers/render-utils.d.ts +18 -2
- package/dist/tests/helpers/render-utils.js +25 -12
- package/dist/tests/html/dom-converter-pseudo-elements.spec.d.ts +1 -0
- package/dist/tests/html/dom-converter-pseudo-elements.spec.js +33 -0
- package/dist/tests/html/dom-converter-text.spec.d.ts +1 -0
- package/dist/tests/html/dom-converter-text.spec.js +67 -0
- package/dist/tests/image/png-backend.spec.d.ts +1 -0
- package/dist/tests/image/png-backend.spec.js +34 -0
- package/dist/tests/layout/box-sizing.spec.d.ts +1 -0
- package/dist/tests/layout/box-sizing.spec.js +75 -0
- package/dist/tests/layout/calc-padding.spec.d.ts +1 -0
- package/dist/tests/layout/calc-padding.spec.js +19 -0
- package/dist/tests/layout/container-query-units.spec.d.ts +1 -0
- package/dist/tests/layout/container-query-units.spec.js +24 -0
- package/dist/tests/layout/flex-auto-height.spec.d.ts +1 -0
- package/dist/tests/layout/flex-auto-height.spec.js +35 -0
- package/dist/tests/layout/flex-wrap-cards.spec.d.ts +1 -0
- package/dist/tests/layout/flex-wrap-cards.spec.js +16 -0
- package/dist/tests/layout/flex-wrap-grow-align-content.spec.d.ts +1 -0
- package/dist/tests/layout/flex-wrap-grow-align-content.spec.js +20 -0
- package/dist/tests/layout/grid-clamp-gap.spec.d.ts +1 -0
- package/dist/tests/layout/grid-clamp-gap.spec.js +22 -0
- package/dist/tests/layout/inline-fragments.spec.js +38 -0
- package/dist/tests/layout/paged-body-margin.spec.d.ts +1 -0
- package/dist/tests/layout/paged-body-margin.spec.js +92 -0
- package/dist/tests/layout/pseudo-counters-generated-content.spec.d.ts +1 -0
- package/dist/tests/layout/pseudo-counters-generated-content.spec.js +51 -0
- package/dist/tests/layout/responsive-clamp-grid-parity.spec.d.ts +1 -0
- package/dist/tests/layout/responsive-clamp-grid-parity.spec.js +75 -0
- package/dist/tests/layout/run-placer-baseline.spec.js +13 -11
- package/dist/tests/pdf/backdrop-filter-noop.spec.d.ts +1 -0
- package/dist/tests/pdf/backdrop-filter-noop.spec.js +140 -0
- package/dist/tests/pdf/filter-drop-shadow.spec.d.ts +1 -0
- package/dist/tests/pdf/filter-drop-shadow.spec.js +74 -0
- package/dist/tests/pdf/filter-opacity.spec.d.ts +1 -0
- package/dist/tests/pdf/filter-opacity.spec.js +30 -0
- package/dist/tests/pdf/font-subset-registry-key.spec.d.ts +1 -0
- package/dist/tests/pdf/font-subset-registry-key.spec.js +66 -0
- package/dist/tests/pdf/header-footer-clip-overflow.spec.d.ts +1 -0
- package/dist/tests/pdf/header-footer-clip-overflow.spec.js +45 -0
- package/dist/tests/pdf/selawik-opt-in.spec.d.ts +1 -0
- package/dist/tests/pdf/selawik-opt-in.spec.js +106 -0
- package/dist/tests/pdf/system-ui-fallback-subset-regression.spec.d.ts +1 -0
- package/dist/tests/pdf/system-ui-fallback-subset-regression.spec.js +39 -0
- package/dist/tests/pdf/text-renderer-fallback.spec.js +55 -0
- package/dist/tests/pdf/text-transform-matrix.spec.js +8 -7
- package/package.json +2 -2
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { collectBoxes, renderTreeForHtml } from "../helpers/render-utils.js";
|
|
2
|
+
function findByTag(boxes, tagName) {
|
|
3
|
+
const found = boxes.find((b) => b.tagName === tagName);
|
|
4
|
+
if (!found) {
|
|
5
|
+
throw new Error(`Expected to find <${tagName}> in render tree`);
|
|
6
|
+
}
|
|
7
|
+
return found;
|
|
8
|
+
}
|
|
9
|
+
describe("flex auto-height behavior", () => {
|
|
10
|
+
it("does not stretch column flex containers with auto height to viewport height", async () => {
|
|
11
|
+
const html = `
|
|
12
|
+
<header style="display:flex;flex-direction:column;padding:16px;border:1px solid #000;gap:8px">
|
|
13
|
+
<div style="font-size:16px">Titulo</div>
|
|
14
|
+
<p style="margin:0;font-size:14px;line-height:1.4">Subtitulo</p>
|
|
15
|
+
</header>
|
|
16
|
+
`;
|
|
17
|
+
const tree = await renderTreeForHtml(html);
|
|
18
|
+
const boxes = collectBoxes(tree.root);
|
|
19
|
+
const header = findByTag(boxes, "header");
|
|
20
|
+
expect(header.borderBox.height).toBeLessThan(300);
|
|
21
|
+
expect(header.borderBox.height).toBeGreaterThan(40);
|
|
22
|
+
});
|
|
23
|
+
it("keeps honoring explicit height for column flex containers", async () => {
|
|
24
|
+
const html = `
|
|
25
|
+
<header style="display:flex;flex-direction:column;height:260px;box-sizing:border-box;padding:16px;border:1px solid #000;gap:8px">
|
|
26
|
+
<div style="font-size:16px">Titulo</div>
|
|
27
|
+
<p style="margin:0;font-size:14px;line-height:1.4">Subtitulo</p>
|
|
28
|
+
</header>
|
|
29
|
+
`;
|
|
30
|
+
const tree = await renderTreeForHtml(html);
|
|
31
|
+
const boxes = collectBoxes(tree.root);
|
|
32
|
+
const header = findByTag(boxes, "header");
|
|
33
|
+
expect(header.borderBox.height).toBeCloseTo(260, 3);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { collectBoxes, renderTreeForHtml } from "../helpers/render-utils.js";
|
|
2
|
+
describe("flex wrap card layout", () => {
|
|
3
|
+
it("wraps items to a new line when flex-basis exceeds available width", async () => {
|
|
4
|
+
const html = "<!DOCTYPE html><html><body style=\"margin:0;\"><main style=\"display:flex;flex-wrap:wrap;gap:20px;width:600px;\"><article style=\"flex:1 1 200px;height:100px;\">A</article><article style=\"flex:1 1 200px;height:100px;\">B</article><article style=\"flex:1 1 200px;height:100px;\">C</article></main></body></html>";
|
|
5
|
+
const tree = await renderTreeForHtml(html);
|
|
6
|
+
const boxes = collectBoxes(tree.root);
|
|
7
|
+
const cards = boxes
|
|
8
|
+
.filter((box) => box.tagName === "article")
|
|
9
|
+
.sort((a, b) => (a.contentBox.y - b.contentBox.y) || (a.contentBox.x - b.contentBox.x));
|
|
10
|
+
expect(cards).toHaveLength(3);
|
|
11
|
+
expect(Math.abs(cards[0].contentBox.y - cards[1].contentBox.y)).toBeLessThan(1);
|
|
12
|
+
expect(cards[2].contentBox.y).toBeGreaterThan(cards[0].contentBox.y + 100);
|
|
13
|
+
expect(cards[0].contentBox.width).toBeGreaterThan(180);
|
|
14
|
+
expect(cards[0].contentBox.width).toBeLessThan(320);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { collectBoxes, renderTreeForHtml } from "../helpers/render-utils.js";
|
|
2
|
+
function articlesByPosition(boxes) {
|
|
3
|
+
return boxes
|
|
4
|
+
.filter((box) => box.tagName === "article")
|
|
5
|
+
.sort((a, b) => (a.contentBox.y - b.contentBox.y) || (a.contentBox.x - b.contentBox.x));
|
|
6
|
+
}
|
|
7
|
+
describe("flex wrap grow and align-content", () => {
|
|
8
|
+
it("grows items per line and stretches wrapped lines across explicit height", async () => {
|
|
9
|
+
const html = "<!DOCTYPE html><html><body style=\"margin:0;\"><main style=\"display:flex;flex-wrap:wrap;gap:20px;width:600px;height:500px;align-content:stretch;\"><article style=\"flex:1 1 200px;height:100px;\">A</article><article style=\"flex:1 1 200px;height:100px;\">B</article><article style=\"flex:1 1 200px;height:100px;\">C</article></main></body></html>";
|
|
10
|
+
const tree = await renderTreeForHtml(html);
|
|
11
|
+
const boxes = collectBoxes(tree.root);
|
|
12
|
+
const cards = articlesByPosition(boxes);
|
|
13
|
+
expect(cards).toHaveLength(3);
|
|
14
|
+
expect(Math.abs(cards[0].contentBox.y - cards[1].contentBox.y)).toBeLessThan(1);
|
|
15
|
+
expect(cards[2].contentBox.y).toBeGreaterThan(220);
|
|
16
|
+
expect(cards[0].contentBox.width).toBeCloseTo(cards[1].contentBox.width, 1);
|
|
17
|
+
expect(cards[0].contentBox.width).toBeGreaterThan(280);
|
|
18
|
+
expect(cards[2].contentBox.width).toBeGreaterThan(590);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { collectBoxes, renderTreeForHtml } from "../helpers/render-utils.js";
|
|
2
|
+
function findByTag(boxes, tagName) {
|
|
3
|
+
const found = boxes.find((b) => b.tagName === tagName);
|
|
4
|
+
if (!found) {
|
|
5
|
+
throw new Error(`Expected to find <${tagName}> in render tree`);
|
|
6
|
+
}
|
|
7
|
+
return found;
|
|
8
|
+
}
|
|
9
|
+
describe("grid clamp track and gap layout", () => {
|
|
10
|
+
it("applies clamp() for grid-template-columns and gap", async () => {
|
|
11
|
+
const html = "<!DOCTYPE html><html><body style=\"margin:0;display:grid;grid-template-columns:clamp(150px,20vw,300px) 1fr;gap:clamp(10px,5vw,40px);width:600px;\"><aside style=\"height:20px;\">A</aside><main style=\"height:20px;\">B</main></body></html>";
|
|
12
|
+
const tree = await renderTreeForHtml(html);
|
|
13
|
+
const boxes = collectBoxes(tree.root);
|
|
14
|
+
const aside = findByTag(boxes, "aside");
|
|
15
|
+
const main = findByTag(boxes, "main");
|
|
16
|
+
const viewportWidth = 794 - 48 - 48;
|
|
17
|
+
const expectedTrack = Math.min(Math.max(0.2 * viewportWidth, 150), 300);
|
|
18
|
+
const expectedGap = Math.min(Math.max(0.05 * viewportWidth, 10), 40);
|
|
19
|
+
expect(aside.contentBox.width).toBeCloseTo(expectedTrack, 1);
|
|
20
|
+
expect(main.contentBox.x - aside.contentBox.x - aside.contentBox.width).toBeCloseTo(expectedGap, 1);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -17,4 +17,42 @@ describe("inline fragment layout", () => {
|
|
|
17
17
|
expect(xPositions[i]).toBeGreaterThan(xPositions[i - 1]);
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
|
+
it("does not emit leading or trailing whitespace-only runs in justified paragraph lines", async () => {
|
|
21
|
+
const html = `
|
|
22
|
+
<p style="text-align: justify; width: 220px;">
|
|
23
|
+
This short sample demonstrates how justified paragraphs spread the remaining space
|
|
24
|
+
across each line and keeps line edges aligned.
|
|
25
|
+
</p>
|
|
26
|
+
`;
|
|
27
|
+
const renderTree = await renderTreeForHtml(html, "", { pagedBodyMargin: "zero" });
|
|
28
|
+
const runs = collectRuns(renderTree.root).filter((r) => typeof r.lineIndex === "number");
|
|
29
|
+
const byLine = new Map();
|
|
30
|
+
for (const run of runs) {
|
|
31
|
+
const idx = run.lineIndex;
|
|
32
|
+
const lineRuns = byLine.get(idx) ?? [];
|
|
33
|
+
lineRuns.push(run);
|
|
34
|
+
byLine.set(idx, lineRuns);
|
|
35
|
+
}
|
|
36
|
+
const lineIndexes = [...byLine.keys()].sort((a, b) => a - b);
|
|
37
|
+
const firstLineRuns = [...(byLine.get(lineIndexes[0]) ?? [])].sort((a, b) => (a.lineMatrix?.e ?? 0) - (b.lineMatrix?.e ?? 0));
|
|
38
|
+
const lastLineRuns = [...(byLine.get(lineIndexes[lineIndexes.length - 1]) ?? [])].sort((a, b) => (a.lineMatrix?.e ?? 0) - (b.lineMatrix?.e ?? 0));
|
|
39
|
+
expect(firstLineRuns[0].text.trim().length).toBeGreaterThan(0);
|
|
40
|
+
expect(lastLineRuns[lastLineRuns.length - 1].text.trim().length).toBeGreaterThan(0);
|
|
41
|
+
});
|
|
42
|
+
it("inherits font size and baseline for strong/em/a from parent text", async () => {
|
|
43
|
+
const html = '<p style="font-size: 20px; line-height: 1.5;">base <strong>forte</strong> <em>italico</em> <a href="https://pagyra.dev">link</a></p>';
|
|
44
|
+
const renderTree = await renderTreeForHtml(html);
|
|
45
|
+
const runs = collectRuns(renderTree.root).filter((r) => ["base", "forte", "italico", "link"].includes(r.text));
|
|
46
|
+
expect(runs).toHaveLength(4);
|
|
47
|
+
const baseRun = runs.find((r) => r.text === "base");
|
|
48
|
+
expect(baseRun).toBeDefined();
|
|
49
|
+
const baseFontSize = baseRun?.fontSize ?? 0;
|
|
50
|
+
const baseBaseline = baseRun?.lineMatrix?.f ?? 0;
|
|
51
|
+
for (const text of ["forte", "italico", "link"]) {
|
|
52
|
+
const run = runs.find((r) => r.text === text);
|
|
53
|
+
expect(run).toBeDefined();
|
|
54
|
+
expect(run?.fontSize).toBeCloseTo(baseFontSize, 4);
|
|
55
|
+
expect(run?.lineMatrix?.f).toBeCloseTo(baseBaseline, 4);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
20
58
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { renderRuns } from "../helpers/render-utils.js";
|
|
2
|
+
function countLinesWithText(runs) {
|
|
3
|
+
const lines = new Set();
|
|
4
|
+
for (const run of runs) {
|
|
5
|
+
if (typeof run.lineIndex !== "number") {
|
|
6
|
+
continue;
|
|
7
|
+
}
|
|
8
|
+
if (run.text.trim().length === 0) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
lines.add(run.lineIndex);
|
|
12
|
+
}
|
|
13
|
+
return lines.size;
|
|
14
|
+
}
|
|
15
|
+
function firstContentRunX(runs) {
|
|
16
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
17
|
+
for (const run of runs) {
|
|
18
|
+
if (run.text.trim().length === 0) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const x = run.lineMatrix?.e ?? Number.POSITIVE_INFINITY;
|
|
22
|
+
minX = Math.min(minX, x);
|
|
23
|
+
}
|
|
24
|
+
return minX;
|
|
25
|
+
}
|
|
26
|
+
describe("paged body margin behavior", () => {
|
|
27
|
+
it("packs justified lines wider when pagedBodyMargin is zero", async () => {
|
|
28
|
+
const html = `
|
|
29
|
+
<p style="text-align: justify; font-family: 'Times New Roman', Times, serif;">
|
|
30
|
+
This short sample demonstrates how justified paragraphs spread the remaining space
|
|
31
|
+
across each line. When rendered as PDF the left and right edges are aligned,
|
|
32
|
+
creating a tidy block of text similar to browsers.
|
|
33
|
+
</p>
|
|
34
|
+
`;
|
|
35
|
+
const baseOptions = {
|
|
36
|
+
viewportWidth: 698,
|
|
37
|
+
viewportHeight: 1026,
|
|
38
|
+
pageWidth: 794,
|
|
39
|
+
pageHeight: 1123,
|
|
40
|
+
margins: { top: 48, right: 48, bottom: 48, left: 48 },
|
|
41
|
+
};
|
|
42
|
+
const autoRuns = await renderRuns(html, "", {
|
|
43
|
+
...baseOptions,
|
|
44
|
+
pagedBodyMargin: "auto",
|
|
45
|
+
});
|
|
46
|
+
const zeroRuns = await renderRuns(html, "", {
|
|
47
|
+
...baseOptions,
|
|
48
|
+
pagedBodyMargin: "zero",
|
|
49
|
+
});
|
|
50
|
+
expect(countLinesWithText(autoRuns)).toBeGreaterThan(countLinesWithText(zeroRuns));
|
|
51
|
+
});
|
|
52
|
+
it("honors explicit render margins when computing text start positions", async () => {
|
|
53
|
+
const html = `<p>Margin probe text for verifying left offset changes across render options.</p>`;
|
|
54
|
+
const common = {
|
|
55
|
+
viewportWidth: 698,
|
|
56
|
+
viewportHeight: 1026,
|
|
57
|
+
pageWidth: 794,
|
|
58
|
+
pageHeight: 1123,
|
|
59
|
+
pagedBodyMargin: "zero",
|
|
60
|
+
};
|
|
61
|
+
const narrowMarginRuns = await renderRuns(html, "", {
|
|
62
|
+
...common,
|
|
63
|
+
margins: { top: 48, right: 40, bottom: 48, left: 40 },
|
|
64
|
+
});
|
|
65
|
+
const wideMarginRuns = await renderRuns(html, "", {
|
|
66
|
+
...common,
|
|
67
|
+
margins: { top: 48, right: 40, bottom: 48, left: 120 },
|
|
68
|
+
});
|
|
69
|
+
const narrowX = firstContentRunX(narrowMarginRuns);
|
|
70
|
+
const wideX = firstContentRunX(wideMarginRuns);
|
|
71
|
+
expect(wideX).toBeGreaterThan(narrowX);
|
|
72
|
+
expect(wideX - narrowX).toBeGreaterThan(60);
|
|
73
|
+
});
|
|
74
|
+
it("collapses whitespace-only runs between block siblings by default, with preserve escape hatch", async () => {
|
|
75
|
+
const html = `
|
|
76
|
+
<p>First paragraph.</p>
|
|
77
|
+
|
|
78
|
+
<p>Second paragraph.</p>
|
|
79
|
+
`;
|
|
80
|
+
const collapsedRuns = await renderRuns(html, "", {
|
|
81
|
+
pagedBodyMargin: "zero",
|
|
82
|
+
interBlockWhitespace: "collapse",
|
|
83
|
+
});
|
|
84
|
+
const preservedRuns = await renderRuns(html, "", {
|
|
85
|
+
pagedBodyMargin: "zero",
|
|
86
|
+
interBlockWhitespace: "preserve",
|
|
87
|
+
});
|
|
88
|
+
const collapsedWhitespaceOnly = collapsedRuns.filter((r) => r.text.trim().length === 0);
|
|
89
|
+
const preservedWhitespaceOnly = preservedRuns.filter((r) => r.text.trim().length === 0);
|
|
90
|
+
expect(collapsedWhitespaceOnly.length).toBeLessThan(preservedWhitespaceOnly.length);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { collectBoxes, collectRuns, renderTreeForHtml } from "../helpers/render-utils.js";
|
|
2
|
+
describe("pseudo-elements with generated content and counters", () => {
|
|
3
|
+
it("renders ::before counter() values with decimal-leading-zero", async () => {
|
|
4
|
+
const html = `
|
|
5
|
+
<div class="counter-demo" style="counter-reset: steps 0;">
|
|
6
|
+
<div class="item">Alpha</div>
|
|
7
|
+
<div class="item">Beta</div>
|
|
8
|
+
<div class="item">Gamma</div>
|
|
9
|
+
</div>
|
|
10
|
+
`;
|
|
11
|
+
const css = `
|
|
12
|
+
.item { position: relative; counter-increment: steps 1; padding-left: 24px; }
|
|
13
|
+
.item::before { content: counter(steps, decimal-leading-zero); position: absolute; left: -12px; top: 0; }
|
|
14
|
+
`;
|
|
15
|
+
const renderTree = await renderTreeForHtml(html, css);
|
|
16
|
+
const runs = collectRuns(renderTree.root).map((r) => r.text);
|
|
17
|
+
expect(runs).toContain("01");
|
|
18
|
+
expect(runs).toContain("02");
|
|
19
|
+
expect(runs).toContain("03");
|
|
20
|
+
});
|
|
21
|
+
it("renders ::after generated text after host text", async () => {
|
|
22
|
+
const html = `<p class="tag">Alpha</p>`;
|
|
23
|
+
const css = `.tag::after { content: "X"; }`;
|
|
24
|
+
const renderTree = await renderTreeForHtml(html, css);
|
|
25
|
+
const runs = collectRuns(renderTree.root).filter((r) => r.text.length > 0);
|
|
26
|
+
const joined = runs.map((r) => r.text).join("");
|
|
27
|
+
expect(joined).toContain("AlphaX");
|
|
28
|
+
});
|
|
29
|
+
it("creates an absolute positioned render box for pseudo-elements", async () => {
|
|
30
|
+
const html = `<div id="host" class="item">Alpha</div>`;
|
|
31
|
+
const css = `
|
|
32
|
+
.item { position: relative; counter-reset: s 0; counter-increment: s 1; padding-left: 24px; }
|
|
33
|
+
.item::before {
|
|
34
|
+
content: counter(s, decimal-leading-zero);
|
|
35
|
+
position: absolute;
|
|
36
|
+
left: -12px;
|
|
37
|
+
top: 0;
|
|
38
|
+
background: #0ea5e9;
|
|
39
|
+
padding: 2px 4px;
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
const renderTree = await renderTreeForHtml(html, css);
|
|
43
|
+
const boxes = collectBoxes(renderTree.root);
|
|
44
|
+
const hostBox = boxes.find((box) => box.customData?.id === "host");
|
|
45
|
+
const pseudoBox = boxes.find((box) => box.customData?.pseudoType === "before");
|
|
46
|
+
expect(hostBox).toBeDefined();
|
|
47
|
+
expect(pseudoBox).toBeDefined();
|
|
48
|
+
expect(pseudoBox?.positioning.type).toBe("absolute");
|
|
49
|
+
expect((pseudoBox?.borderBox.x ?? 0)).toBeLessThan(hostBox?.borderBox.x ?? 0);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { collectBoxes, renderTreeForHtml } from "../helpers/render-utils.js";
|
|
2
|
+
function firstByTag(boxes, tagName) {
|
|
3
|
+
const found = boxes.find((box) => box.tagName === tagName);
|
|
4
|
+
if (!found) {
|
|
5
|
+
throw new Error(`Expected <${tagName}> in render tree`);
|
|
6
|
+
}
|
|
7
|
+
return found;
|
|
8
|
+
}
|
|
9
|
+
describe("responsive clamp grid parity", () => {
|
|
10
|
+
it("matches browser-like grid stretch and wrapped flex growth for the repro", async () => {
|
|
11
|
+
const html = `<!DOCTYPE html>
|
|
12
|
+
<html lang="pt">
|
|
13
|
+
<head>
|
|
14
|
+
<meta charset="UTF-8">
|
|
15
|
+
<title>Responsive Grid com clamp()</title>
|
|
16
|
+
<style>
|
|
17
|
+
body {
|
|
18
|
+
margin: 0;
|
|
19
|
+
display: grid;
|
|
20
|
+
grid-template-columns: clamp(150px, 20vw, 300px) 1fr;
|
|
21
|
+
height: 100vh;
|
|
22
|
+
font-family: sans-serif;
|
|
23
|
+
}
|
|
24
|
+
aside {
|
|
25
|
+
background: #333;
|
|
26
|
+
color: white;
|
|
27
|
+
padding: 20px;
|
|
28
|
+
}
|
|
29
|
+
main {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-wrap: wrap;
|
|
32
|
+
gap: clamp(10px, 5vw, 40px);
|
|
33
|
+
padding: calc(10px + 2%);
|
|
34
|
+
}
|
|
35
|
+
div {
|
|
36
|
+
flex: 1 1 200px;
|
|
37
|
+
height: 150px;
|
|
38
|
+
background: #6c5ce7;
|
|
39
|
+
border-radius: 8px;
|
|
40
|
+
font-size: clamp(1rem, 3vw, 2rem);
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
color: white;
|
|
45
|
+
}
|
|
46
|
+
</style>
|
|
47
|
+
</head>
|
|
48
|
+
<body>
|
|
49
|
+
<aside>Menu</aside>
|
|
50
|
+
<main>
|
|
51
|
+
<div>Card 1</div>
|
|
52
|
+
<div>Card 2</div>
|
|
53
|
+
<div>Card 3</div>
|
|
54
|
+
</main>
|
|
55
|
+
</body>
|
|
56
|
+
</html>`;
|
|
57
|
+
const tree = await renderTreeForHtml(html);
|
|
58
|
+
const boxes = collectBoxes(tree.root);
|
|
59
|
+
const body = firstByTag(boxes, "body");
|
|
60
|
+
const aside = firstByTag(boxes, "aside");
|
|
61
|
+
const main = firstByTag(boxes, "main");
|
|
62
|
+
const cards = boxes
|
|
63
|
+
.filter((box) => box.tagName === "div")
|
|
64
|
+
.sort((a, b) => (a.contentBox.y - b.contentBox.y) || (a.contentBox.x - b.contentBox.x));
|
|
65
|
+
expect(cards).toHaveLength(3);
|
|
66
|
+
// 1123 viewport height minus 48 top/bottom default page margins in test helper.
|
|
67
|
+
expect(body.contentBox.height).toBeCloseTo(1123 - 48 - 48, 1);
|
|
68
|
+
expect(aside.borderBox.height).toBeCloseTo(main.borderBox.height, 1);
|
|
69
|
+
expect(main.borderBox.height).toBeGreaterThan(900);
|
|
70
|
+
expect(cards[0].contentBox.width).toBeCloseTo(cards[1].contentBox.width, 1);
|
|
71
|
+
expect(cards[0].contentBox.width).toBeGreaterThan(220);
|
|
72
|
+
expect(cards[2].contentBox.width).toBeGreaterThan(main.contentBox.width - 2);
|
|
73
|
+
expect(cards[2].contentBox.y).toBeGreaterThan(cards[0].contentBox.y + 400);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { RunPlacer } from "../../src/layout/inline/run-placer.js";
|
|
2
2
|
import { LayoutNode } from "../../src/dom/node.js";
|
|
3
3
|
import { ComputedStyle } from "../../src/css/style.js";
|
|
4
|
+
import { calculateBaseline } from "../../src/layout/inline/font-baseline-calculator.js";
|
|
4
5
|
describe("RunPlacer", () => {
|
|
5
|
-
it("uses
|
|
6
|
+
it("uses the shared lineBaseline from LineContext for all runs", () => {
|
|
6
7
|
const mockFontEmbedder = {
|
|
7
8
|
getMetrics: vi.fn().mockReturnValue({
|
|
8
9
|
metrics: {
|
|
@@ -28,6 +29,10 @@ describe("RunPlacer", () => {
|
|
|
28
29
|
style: style,
|
|
29
30
|
text: "Test",
|
|
30
31
|
};
|
|
32
|
+
// Pre-compute the line baseline (as layout.ts would)
|
|
33
|
+
const lineBaseline = calculateBaseline(100, 20, 20, {
|
|
34
|
+
metrics: { unitsPerEm: 1000, ascender: 800, descender: -200 }
|
|
35
|
+
});
|
|
31
36
|
const lineContext = {
|
|
32
37
|
lineTop: 100,
|
|
33
38
|
lineHeight: 20,
|
|
@@ -38,20 +43,16 @@ describe("RunPlacer", () => {
|
|
|
38
43
|
isLastLine: false,
|
|
39
44
|
contentX: 0,
|
|
40
45
|
inlineOffsetStart: 0,
|
|
46
|
+
lineBaseline,
|
|
41
47
|
};
|
|
42
48
|
placer.placeRunsForLine([{ item, offset: 0 }], lineContext);
|
|
43
|
-
expect(mockFontEmbedder.getMetrics).toHaveBeenCalledWith("TestFont", 400, "normal");
|
|
44
49
|
const runs = placer.getNodeRuns().get(node);
|
|
45
50
|
expect(runs).toBeDefined();
|
|
46
51
|
expect(runs.length).toBe(1);
|
|
47
|
-
// Baseline
|
|
48
|
-
// ascent = (800 / 1000) * 20 = 16
|
|
49
|
-
// leading = 20 - 20 = 0
|
|
50
|
-
// halfLeading = 0
|
|
51
|
-
// baseline = 100 + 0 + 16 = 116
|
|
52
|
+
// Baseline should match the shared lineBaseline = 100 + 0 + 16 = 116
|
|
52
53
|
expect(runs[0].baseline).toBe(116);
|
|
53
54
|
});
|
|
54
|
-
it("uses
|
|
55
|
+
it("uses shared lineBaseline even without FontEmbedder", () => {
|
|
55
56
|
const placer = new RunPlacer(null);
|
|
56
57
|
const style = new ComputedStyle({
|
|
57
58
|
fontSize: 20,
|
|
@@ -67,6 +68,8 @@ describe("RunPlacer", () => {
|
|
|
67
68
|
style: style,
|
|
68
69
|
text: "Test",
|
|
69
70
|
};
|
|
71
|
+
// Pre-compute the line baseline using default heuristic (no font metrics)
|
|
72
|
+
const lineBaseline = calculateBaseline(100, 20, 20, null);
|
|
70
73
|
const lineContext = {
|
|
71
74
|
lineTop: 100,
|
|
72
75
|
lineHeight: 20,
|
|
@@ -77,13 +80,12 @@ describe("RunPlacer", () => {
|
|
|
77
80
|
isLastLine: false,
|
|
78
81
|
contentX: 0,
|
|
79
82
|
inlineOffsetStart: 0,
|
|
83
|
+
lineBaseline,
|
|
80
84
|
};
|
|
81
85
|
placer.placeRunsForLine([{ item, offset: 0 }], lineContext);
|
|
82
86
|
const runs = placer.getNodeRuns().get(node);
|
|
83
87
|
expect(runs).toBeDefined();
|
|
84
|
-
// Default heuristic
|
|
85
|
-
// ascent = 20 * 0.75 = 15
|
|
86
|
-
// baseline = 100 + 15 = 115
|
|
88
|
+
// Default heuristic baseline = 100 + 0 + (20*0.75) = 115
|
|
87
89
|
expect(runs[0].baseline).toBe(115);
|
|
88
90
|
});
|
|
89
91
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { renderHtmlToPdf } from "../../src/html-to-pdf.js";
|
|
3
|
+
describe("backdrop-filter no-op", () => {
|
|
4
|
+
it("renders without error when backdrop-filter is present", async () => {
|
|
5
|
+
const html = `
|
|
6
|
+
<!DOCTYPE html>
|
|
7
|
+
<html>
|
|
8
|
+
<head>
|
|
9
|
+
<style>
|
|
10
|
+
.glass {
|
|
11
|
+
backdrop-filter: blur(10px) brightness(1.2);
|
|
12
|
+
background: rgba(255, 255, 255, 0.3);
|
|
13
|
+
padding: 20px;
|
|
14
|
+
border-radius: 10px;
|
|
15
|
+
}
|
|
16
|
+
body {
|
|
17
|
+
font-family: Arial, sans-serif;
|
|
18
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
19
|
+
padding: 40px;
|
|
20
|
+
}
|
|
21
|
+
</style>
|
|
22
|
+
</head>
|
|
23
|
+
<body>
|
|
24
|
+
<div class="glass">
|
|
25
|
+
<h1>Glassmorphism Effect</h1>
|
|
26
|
+
<p>This box has backdrop-filter but it will be ignored in PDF (with warning).</p>
|
|
27
|
+
</div>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
30
|
+
`;
|
|
31
|
+
// Should not throw any errors
|
|
32
|
+
await expect(renderHtmlToPdf({ html })).resolves.toBeDefined();
|
|
33
|
+
});
|
|
34
|
+
it("renders without error when filter has unsupported functions", async () => {
|
|
35
|
+
const html = `
|
|
36
|
+
<!DOCTYPE html>
|
|
37
|
+
<html>
|
|
38
|
+
<head>
|
|
39
|
+
<style>
|
|
40
|
+
.filtered {
|
|
41
|
+
filter: blur(5px) grayscale(0.5) brightness(1.2);
|
|
42
|
+
padding: 20px;
|
|
43
|
+
}
|
|
44
|
+
body {
|
|
45
|
+
font-family: Arial, sans-serif;
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
48
|
+
</head>
|
|
49
|
+
<body>
|
|
50
|
+
<div class="filtered">
|
|
51
|
+
<p>This box has unsupported filters (blur, grayscale, brightness) that will be ignored.</p>
|
|
52
|
+
</div>
|
|
53
|
+
</body>
|
|
54
|
+
</html>
|
|
55
|
+
`;
|
|
56
|
+
// Should not throw any errors
|
|
57
|
+
await expect(renderHtmlToPdf({ html })).resolves.toBeDefined();
|
|
58
|
+
});
|
|
59
|
+
it("applies opacity from filter correctly", async () => {
|
|
60
|
+
const html = `
|
|
61
|
+
<!DOCTYPE html>
|
|
62
|
+
<html>
|
|
63
|
+
<head>
|
|
64
|
+
<style>
|
|
65
|
+
.semi-transparent {
|
|
66
|
+
filter: opacity(0.5);
|
|
67
|
+
background: red;
|
|
68
|
+
padding: 20px;
|
|
69
|
+
}
|
|
70
|
+
body {
|
|
71
|
+
font-family: Arial, sans-serif;
|
|
72
|
+
}
|
|
73
|
+
</style>
|
|
74
|
+
</head>
|
|
75
|
+
<body>
|
|
76
|
+
<div class="semi-transparent">
|
|
77
|
+
<p>This should be 50% opaque.</p>
|
|
78
|
+
</div>
|
|
79
|
+
</body>
|
|
80
|
+
</html>
|
|
81
|
+
`;
|
|
82
|
+
const result = await renderHtmlToPdf({ html });
|
|
83
|
+
expect(result).toBeDefined();
|
|
84
|
+
});
|
|
85
|
+
it("composes filter opacity with element opacity", async () => {
|
|
86
|
+
const html = `
|
|
87
|
+
<!DOCTYPE html>
|
|
88
|
+
<html>
|
|
89
|
+
<head>
|
|
90
|
+
<style>
|
|
91
|
+
.double-transparent {
|
|
92
|
+
opacity: 0.8;
|
|
93
|
+
filter: opacity(0.5);
|
|
94
|
+
background: blue;
|
|
95
|
+
padding: 20px;
|
|
96
|
+
}
|
|
97
|
+
body {
|
|
98
|
+
font-family: Arial, sans-serif;
|
|
99
|
+
}
|
|
100
|
+
</style>
|
|
101
|
+
</head>
|
|
102
|
+
<body>
|
|
103
|
+
<div class="double-transparent">
|
|
104
|
+
<p>This should be 40% opaque (0.8 * 0.5).</p>
|
|
105
|
+
</div>
|
|
106
|
+
</body>
|
|
107
|
+
</html>
|
|
108
|
+
`;
|
|
109
|
+
const result = await renderHtmlToPdf({ html });
|
|
110
|
+
expect(result).toBeDefined();
|
|
111
|
+
});
|
|
112
|
+
it("renders drop-shadow filter", async () => {
|
|
113
|
+
const html = `
|
|
114
|
+
<!DOCTYPE html>
|
|
115
|
+
<html>
|
|
116
|
+
<head>
|
|
117
|
+
<style>
|
|
118
|
+
.shadowed {
|
|
119
|
+
filter: drop-shadow(5px 5px 10px rgba(0, 0, 0, 0.5));
|
|
120
|
+
background: yellow;
|
|
121
|
+
padding: 20px;
|
|
122
|
+
display: inline-block;
|
|
123
|
+
}
|
|
124
|
+
body {
|
|
125
|
+
font-family: Arial, sans-serif;
|
|
126
|
+
padding: 40px;
|
|
127
|
+
}
|
|
128
|
+
</style>
|
|
129
|
+
</head>
|
|
130
|
+
<body>
|
|
131
|
+
<div class="shadowed">
|
|
132
|
+
<p>This box has a drop-shadow filter.</p>
|
|
133
|
+
</div>
|
|
134
|
+
</body>
|
|
135
|
+
</html>
|
|
136
|
+
`;
|
|
137
|
+
const result = await renderHtmlToPdf({ html });
|
|
138
|
+
expect(result).toBeDefined();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { extractDropShadowLayers } from "../../src/pdf/utils/filter-utils.js";
|
|
3
|
+
describe("extractDropShadowLayers", () => {
|
|
4
|
+
const fallbackColor = { r: 0, g: 0, b: 0, a: 1 };
|
|
5
|
+
it("returns empty array for filters without drop-shadow", () => {
|
|
6
|
+
const filters = [
|
|
7
|
+
{ kind: "blur", value: 5 },
|
|
8
|
+
{ kind: "opacity", value: 0.5 },
|
|
9
|
+
];
|
|
10
|
+
expect(extractDropShadowLayers(filters, fallbackColor)).toEqual([]);
|
|
11
|
+
});
|
|
12
|
+
it("extracts drop-shadow with all parameters", () => {
|
|
13
|
+
const filters = [{
|
|
14
|
+
kind: "drop-shadow",
|
|
15
|
+
offsetX: 2,
|
|
16
|
+
offsetY: 4,
|
|
17
|
+
blurRadius: 6,
|
|
18
|
+
color: "red",
|
|
19
|
+
}];
|
|
20
|
+
const result = extractDropShadowLayers(filters, fallbackColor);
|
|
21
|
+
expect(result).toHaveLength(1);
|
|
22
|
+
expect(result[0]).toEqual({
|
|
23
|
+
inset: false,
|
|
24
|
+
offsetX: 2,
|
|
25
|
+
offsetY: 4,
|
|
26
|
+
blur: 6,
|
|
27
|
+
spread: 0,
|
|
28
|
+
color: { r: 255, g: 0, b: 0, a: 1 },
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
it("uses fallback color when drop-shadow has no color", () => {
|
|
32
|
+
const filters = [{
|
|
33
|
+
kind: "drop-shadow",
|
|
34
|
+
offsetX: 3,
|
|
35
|
+
offsetY: 3,
|
|
36
|
+
blurRadius: 5,
|
|
37
|
+
color: undefined,
|
|
38
|
+
}];
|
|
39
|
+
const customFallback = { r: 0.5, g: 0.5, b: 0.5, a: 1 };
|
|
40
|
+
const result = extractDropShadowLayers(filters, customFallback);
|
|
41
|
+
expect(result[0].color).toEqual(customFallback);
|
|
42
|
+
});
|
|
43
|
+
it("extracts multiple drop-shadow functions", () => {
|
|
44
|
+
const filters = [
|
|
45
|
+
{
|
|
46
|
+
kind: "drop-shadow",
|
|
47
|
+
offsetX: 2,
|
|
48
|
+
offsetY: 2,
|
|
49
|
+
blurRadius: 4,
|
|
50
|
+
color: "black",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
kind: "drop-shadow",
|
|
54
|
+
offsetX: 4,
|
|
55
|
+
offsetY: 4,
|
|
56
|
+
blurRadius: 8,
|
|
57
|
+
color: "red",
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
const result = extractDropShadowLayers(filters, fallbackColor);
|
|
61
|
+
expect(result).toHaveLength(2);
|
|
62
|
+
});
|
|
63
|
+
it("clamps negative blur to 0", () => {
|
|
64
|
+
const filters = [{
|
|
65
|
+
kind: "drop-shadow",
|
|
66
|
+
offsetX: 2,
|
|
67
|
+
offsetY: 2,
|
|
68
|
+
blurRadius: -5,
|
|
69
|
+
color: undefined,
|
|
70
|
+
}];
|
|
71
|
+
const result = extractDropShadowLayers(filters, fallbackColor);
|
|
72
|
+
expect(result[0].blur).toBe(0);
|
|
73
|
+
});
|
|
74
|
+
});
|