pagyra-js 0.0.20 → 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.
Files changed (286) hide show
  1. package/README.md +55 -0
  2. package/dist/assets/fonts/licenses/selawik/SIL Open Font License.txt +43 -0
  3. package/dist/assets/fonts/ttf/arimo/Arimo-Bold.ttf +0 -0
  4. package/dist/assets/fonts/ttf/arimo/Arimo-BoldItalic.ttf +0 -0
  5. package/dist/assets/fonts/ttf/arimo/Arimo-Italic.ttf +0 -0
  6. package/dist/assets/fonts/ttf/arimo/Arimo-Regular.ttf +0 -0
  7. package/dist/assets/fonts/ttf/cinzeldecorative/CinzelDecorative-Black.ttf +0 -0
  8. package/dist/assets/fonts/ttf/cinzeldecorative/CinzelDecorative-Bold.ttf +0 -0
  9. package/dist/assets/fonts/ttf/cinzeldecorative/CinzelDecorative-Regular.ttf +0 -0
  10. package/dist/assets/fonts/ttf/dejavu/DejaVuSans.ttf +0 -0
  11. package/dist/assets/fonts/ttf/firecode/FiraCode-Bold.ttf +0 -0
  12. package/dist/assets/fonts/ttf/firecode/FiraCode-Light.ttf +0 -0
  13. package/dist/assets/fonts/ttf/firecode/FiraCode-Medium.ttf +0 -0
  14. package/dist/assets/fonts/ttf/firecode/FiraCode-Regular.ttf +0 -0
  15. package/dist/assets/fonts/ttf/firecode/FiraCode-SemiBold.ttf +0 -0
  16. package/dist/assets/fonts/ttf/notoemoji/NotoEmoji-Bold.ttf +0 -0
  17. package/dist/assets/fonts/ttf/notoemoji/NotoEmoji-Light.ttf +0 -0
  18. package/dist/assets/fonts/ttf/notoemoji/NotoEmoji-Medium.ttf +0 -0
  19. package/dist/assets/fonts/ttf/notoemoji/NotoEmoji-Regular.ttf +0 -0
  20. package/dist/assets/fonts/ttf/notoemoji/NotoEmoji-SemiBold.ttf +0 -0
  21. package/dist/assets/fonts/ttf/notosans/NotoSans-Regular.ttf +0 -0
  22. package/dist/assets/fonts/ttf/roboto/Roboto-Bold.ttf +0 -0
  23. package/dist/assets/fonts/ttf/roboto/Roboto-BoldItalic.ttf +0 -0
  24. package/dist/assets/fonts/ttf/roboto/Roboto-Italic.ttf +0 -0
  25. package/dist/assets/fonts/ttf/roboto/Roboto-Regular.ttf +0 -0
  26. package/dist/assets/fonts/ttf/selawik/selawk.ttf +0 -0
  27. package/dist/assets/fonts/ttf/selawik/selawkb.ttf +0 -0
  28. package/dist/assets/fonts/ttf/selawik/selawkl.ttf +0 -0
  29. package/dist/assets/fonts/ttf/selawik/selawksb.ttf +0 -0
  30. package/dist/assets/fonts/ttf/selawik/selawksl.ttf +0 -0
  31. package/dist/assets/fonts/ttf/stixtwomath/STIXTwoMath-Regular.ttf +0 -0
  32. package/dist/assets/fonts/ttf/tinos/Tinos-Bold.ttf +0 -0
  33. package/dist/assets/fonts/ttf/tinos/Tinos-BoldItalic.ttf +0 -0
  34. package/dist/assets/fonts/ttf/tinos/Tinos-Italic.ttf +0 -0
  35. package/dist/assets/fonts/ttf/tinos/Tinos-Regular.ttf +0 -0
  36. package/dist/assets/fonts/woff/lato/lato-latin-400-italic.woff +0 -0
  37. package/dist/assets/fonts/woff/lato/lato-latin-400-normal.woff +0 -0
  38. package/dist/assets/fonts/woff/lato/lato-latin-700-italic.woff +0 -0
  39. package/dist/assets/fonts/woff/lato/lato-latin-700-normal.woff +0 -0
  40. package/dist/assets/fonts/woff2/caveat/Caveat-Bold.woff2 +0 -0
  41. package/dist/assets/fonts/woff2/caveat/Caveat-Regular.woff2 +0 -0
  42. package/dist/assets/fonts/woff2/lato/lato-latin-400-italic.woff2 +0 -0
  43. package/dist/assets/fonts/woff2/lato/lato-latin-400-normal.woff2 +0 -0
  44. package/dist/assets/fonts/woff2/lato/lato-latin-700-italic.woff2 +0 -0
  45. package/dist/assets/fonts/woff2/lato/lato-latin-700-normal.woff2 +0 -0
  46. package/dist/browser/pagyra.min.js +34 -34
  47. package/dist/browser/pagyra.min.js.map +4 -4
  48. package/dist/playground/server.js +2 -0
  49. package/dist/src/css/compute-style/base-options.d.ts +7 -0
  50. package/dist/src/css/compute-style/base-options.js +24 -0
  51. package/dist/src/css/compute-style/declarations.d.ts +10 -0
  52. package/dist/src/css/compute-style/declarations.js +77 -0
  53. package/dist/src/css/compute-style/decoration.d.ts +8 -0
  54. package/dist/src/css/compute-style/decoration.js +55 -0
  55. package/dist/src/css/compute-style/defaults.d.ts +3 -0
  56. package/dist/src/css/compute-style/defaults.js +34 -0
  57. package/dist/src/css/compute-style/display.d.ts +3 -0
  58. package/dist/src/css/compute-style/display.js +85 -0
  59. package/dist/src/css/compute-style/float.d.ts +2 -0
  60. package/dist/src/css/compute-style/float.js +13 -0
  61. package/dist/src/css/compute-style/font.d.ts +12 -0
  62. package/dist/src/css/compute-style/font.js +57 -0
  63. package/dist/src/css/compute-style/overrides.d.ts +3 -0
  64. package/dist/src/css/compute-style/overrides.js +241 -0
  65. package/dist/src/css/compute-style.d.ts +2 -0
  66. package/dist/src/css/compute-style.js +34 -487
  67. package/dist/src/css/enums.d.ts +4 -0
  68. package/dist/src/css/enums.js +5 -0
  69. package/dist/src/css/layout-property-resolver.js +30 -18
  70. package/dist/src/css/length.d.ts +26 -2
  71. package/dist/src/css/length.js +48 -0
  72. package/dist/src/css/parsers/background-parser.js +1 -1
  73. package/dist/src/css/parsers/calc-parser.d.ts +2 -0
  74. package/dist/src/css/parsers/calc-parser.js +310 -0
  75. package/dist/src/css/parsers/content-parser.d.ts +2 -1
  76. package/dist/src/css/parsers/content-parser.js +7 -2
  77. package/dist/src/css/parsers/dimension-parser.js +37 -18
  78. package/dist/src/css/parsers/display-flex-parser.d.ts +4 -0
  79. package/dist/src/css/parsers/display-flex-parser.js +97 -0
  80. package/dist/src/css/parsers/filter-parser.d.ts +14 -0
  81. package/dist/src/css/parsers/filter-parser.js +255 -0
  82. package/dist/src/css/parsers/grid-parser-extended.d.ts +1 -0
  83. package/dist/src/css/parsers/grid-parser-extended.js +40 -1
  84. package/dist/src/css/parsers/grid-parser.d.ts +5 -2
  85. package/dist/src/css/parsers/grid-parser.js +71 -7
  86. package/dist/src/css/parsers/length-parser.d.ts +8 -3
  87. package/dist/src/css/parsers/length-parser.js +45 -2
  88. package/dist/src/css/parsers/margin-block-parser.js +3 -3
  89. package/dist/src/css/parsers/margin-parser.js +3 -3
  90. package/dist/src/css/parsers/padding-block-parser.js +3 -3
  91. package/dist/src/css/parsers/padding-inline-parser.js +3 -3
  92. package/dist/src/css/parsers/padding-parser.js +6 -6
  93. package/dist/src/css/parsers/position-parser.js +2 -22
  94. package/dist/src/css/parsers/register-parsers.js +29 -2
  95. package/dist/src/css/parsers/word-break-parser.d.ts +2 -0
  96. package/dist/src/css/parsers/word-break-parser.js +23 -0
  97. package/dist/src/css/properties/grid.d.ts +16 -2
  98. package/dist/src/css/properties/layout.d.ts +3 -1
  99. package/dist/src/css/properties/layout.js +1 -1
  100. package/dist/src/css/properties/misc.d.ts +5 -0
  101. package/dist/src/css/properties/typography.d.ts +3 -0
  102. package/dist/src/css/properties/visual.d.ts +36 -0
  103. package/dist/src/css/shorthands/box-shorthand.d.ts +2 -2
  104. package/dist/src/css/style-inheritance.d.ts +2 -1
  105. package/dist/src/css/style-inheritance.js +1 -0
  106. package/dist/src/css/style.d.ts +30 -10
  107. package/dist/src/css/style.js +8 -1
  108. package/dist/src/css/ua-defaults/base-defaults.d.ts +1 -0
  109. package/dist/src/css/ua-defaults/base-defaults.js +10 -1
  110. package/dist/src/css/ua-defaults/element-defaults.js +0 -2
  111. package/dist/src/html/css/parse-css.d.ts +2 -0
  112. package/dist/src/html/css/parse-css.js +32 -3
  113. package/dist/src/html/dom-converter/background-images.d.ts +3 -0
  114. package/dist/src/html/dom-converter/background-images.js +88 -0
  115. package/dist/src/html/dom-converter/convert-dom-node.d.ts +5 -0
  116. package/dist/src/html/dom-converter/convert-dom-node.js +81 -0
  117. package/dist/src/html/dom-converter/handlers/br-handler.d.ts +2 -0
  118. package/dist/src/html/dom-converter/handlers/br-handler.js +20 -0
  119. package/dist/src/html/dom-converter/handlers/form-control-handler.d.ts +2 -0
  120. package/dist/src/html/dom-converter/handlers/form-control-handler.js +28 -0
  121. package/dist/src/html/dom-converter/handlers/img-handler.d.ts +2 -0
  122. package/dist/src/html/dom-converter/handlers/img-handler.js +4 -0
  123. package/dist/src/html/dom-converter/handlers/index.d.ts +4 -0
  124. package/dist/src/html/dom-converter/handlers/index.js +19 -0
  125. package/dist/src/html/dom-converter/handlers/svg-handler.d.ts +2 -0
  126. package/dist/src/html/dom-converter/handlers/svg-handler.js +32 -0
  127. package/dist/src/html/dom-converter/handlers/types.d.ts +12 -0
  128. package/dist/src/html/dom-converter/handlers/types.js +2 -0
  129. package/dist/src/html/dom-converter/helpers.d.ts +7 -0
  130. package/dist/src/html/dom-converter/helpers.js +35 -0
  131. package/dist/src/html/dom-converter/index.d.ts +1 -0
  132. package/dist/src/html/dom-converter/index.js +1 -0
  133. package/dist/src/html/dom-converter/pseudo-elements.d.ts +6 -0
  134. package/dist/src/html/dom-converter/pseudo-elements.js +48 -0
  135. package/dist/src/html/dom-converter/text.d.ts +15 -0
  136. package/dist/src/html/dom-converter/text.js +170 -0
  137. package/dist/src/html/dom-converter.d.ts +1 -5
  138. package/dist/src/html/dom-converter.js +1 -417
  139. package/dist/src/html/image-converter.d.ts +5 -0
  140. package/dist/src/html-to-pdf/document-css.d.ts +14 -0
  141. package/dist/src/html-to-pdf/document-css.js +45 -0
  142. package/dist/src/html-to-pdf/fonts.d.ts +16 -0
  143. package/dist/src/html-to-pdf/fonts.js +74 -0
  144. package/dist/src/html-to-pdf/header-footer.d.ts +14 -0
  145. package/dist/src/html-to-pdf/header-footer.js +101 -0
  146. package/dist/src/html-to-pdf/html-parser.d.ts +6 -0
  147. package/dist/src/html-to-pdf/html-parser.js +81 -0
  148. package/dist/src/html-to-pdf/index.d.ts +3 -0
  149. package/dist/src/html-to-pdf/index.js +2 -0
  150. package/dist/src/html-to-pdf/layout-build.d.ts +37 -0
  151. package/dist/src/html-to-pdf/layout-build.js +73 -0
  152. package/dist/src/html-to-pdf/prepare-html-render.d.ts +2 -0
  153. package/dist/src/html-to-pdf/prepare-html-render.js +121 -0
  154. package/dist/src/html-to-pdf/render-finalize.d.ts +15 -0
  155. package/dist/src/html-to-pdf/render-finalize.js +27 -0
  156. package/dist/src/html-to-pdf/render-html-to-pdf.d.ts +3 -0
  157. package/dist/src/html-to-pdf/render-html-to-pdf.js +25 -0
  158. package/dist/src/html-to-pdf/resource-loader.d.ts +6 -0
  159. package/dist/src/html-to-pdf/resource-loader.js +120 -0
  160. package/dist/src/html-to-pdf/types.d.ts +38 -0
  161. package/dist/src/html-to-pdf/types.js +2 -0
  162. package/dist/src/html-to-pdf.d.ts +1 -37
  163. package/dist/src/html-to-pdf.js +1 -537
  164. package/dist/src/image/js-png-backend.d.ts +7 -0
  165. package/dist/src/image/js-png-backend.js +9 -0
  166. package/dist/src/image/png-backend.d.ts +5 -0
  167. package/dist/src/image/png-backend.js +1 -0
  168. package/dist/src/image/png-wasm-loader.d.ts +5 -0
  169. package/dist/src/image/png-wasm-loader.js +59 -0
  170. package/dist/src/image/wasm/png_decoder_wasm.d.ts +8 -0
  171. package/dist/src/image/wasm/png_decoder_wasm.js +24 -0
  172. package/dist/src/image/wasm/png_decoder_wasm_bg.js +16 -0
  173. package/dist/src/image/wasm-png-backend.d.ts +6 -0
  174. package/dist/src/image/wasm-png-backend.js +17 -0
  175. package/dist/src/layout/counter.d.ts +1 -2
  176. package/dist/src/layout/counter.js +18 -18
  177. package/dist/src/layout/inline/inline-utils.d.ts +1 -1
  178. package/dist/src/layout/inline/inline-utils.js +8 -7
  179. package/dist/src/layout/inline/layout.js +16 -3
  180. package/dist/src/layout/inline/run-placer.d.ts +1 -0
  181. package/dist/src/layout/inline/run-placer.js +2 -10
  182. package/dist/src/layout/pipeline/out-of-flow-manager.js +25 -1
  183. package/dist/src/layout/strategies/block.js +35 -24
  184. package/dist/src/layout/strategies/flex.js +305 -61
  185. package/dist/src/layout/strategies/form.d.ts +2 -0
  186. package/dist/src/layout/strategies/form.js +38 -13
  187. package/dist/src/layout/strategies/grid.js +166 -29
  188. package/dist/src/layout/strategies/image.js +53 -27
  189. package/dist/src/layout/strategies/inline.js +26 -21
  190. package/dist/src/layout/strategies/table.js +26 -18
  191. package/dist/src/layout/utils/content-measurer.d.ts +1 -1
  192. package/dist/src/layout/utils/content-measurer.js +8 -7
  193. package/dist/src/layout/utils/floats.d.ts +1 -0
  194. package/dist/src/layout/utils/floats.js +14 -12
  195. package/dist/src/layout/utils/margin.d.ts +4 -4
  196. package/dist/src/layout/utils/margin.js +20 -16
  197. package/dist/src/layout/utils/node-math.d.ts +12 -6
  198. package/dist/src/layout/utils/node-math.js +71 -41
  199. package/dist/src/layout/utils/sizing.js +2 -1
  200. package/dist/src/pdf/font-subset/font-registry.d.ts +6 -0
  201. package/dist/src/pdf/font-subset/font-registry.js +30 -2
  202. package/dist/src/pdf/header-footer-renderer.js +12 -1
  203. package/dist/src/pdf/layout-tree-builder.js +5 -1
  204. package/dist/src/pdf/page-painter.js +13 -0
  205. package/dist/src/pdf/pagination.js +2 -2
  206. package/dist/src/pdf/renderer/box-painter.js +28 -3
  207. package/dist/src/pdf/renderer/page-paint.js +11 -3
  208. package/dist/src/pdf/renderers/radius-utils.js +31 -38
  209. package/dist/src/pdf/renderers/shape-renderer.js +1 -1
  210. package/dist/src/pdf/renderers/shape-utils.js +1 -1
  211. package/dist/src/pdf/renderers/text-renderer.d.ts +9 -1
  212. package/dist/src/pdf/renderers/text-renderer.js +36 -2
  213. package/dist/src/pdf/stacking/build-stacking-contexts.js +1 -2
  214. package/dist/src/pdf/stacking/resolve-paint-order.d.ts +5 -6
  215. package/dist/src/pdf/stacking/resolve-paint-order.js +29 -9
  216. package/dist/src/pdf/stacking/types.d.ts +14 -0
  217. package/dist/src/pdf/svg/shape-renderer.js +47 -20
  218. package/dist/src/pdf/types.d.ts +7 -1
  219. package/dist/src/pdf/utils/border-radius-utils.js +31 -38
  220. package/dist/src/pdf/utils/color-utils.js +17 -2
  221. package/dist/src/pdf/utils/filter-utils.d.ts +29 -0
  222. package/dist/src/pdf/utils/filter-utils.js +85 -0
  223. package/dist/src/pdf/utils/node-text-run-factory.js +1 -1
  224. package/dist/src/pdf/utils/text-layout-adjuster.d.ts +0 -8
  225. package/dist/src/pdf/utils/text-layout-adjuster.js +12 -9
  226. package/dist/src/units/units.d.ts +1 -1
  227. package/dist/tests/css/box-sizing.spec.d.ts +1 -0
  228. package/dist/tests/css/box-sizing.spec.js +46 -0
  229. package/dist/tests/css/calc-parser.spec.d.ts +1 -0
  230. package/dist/tests/css/calc-parser.spec.js +68 -0
  231. package/dist/tests/css/container-query-units.spec.d.ts +1 -0
  232. package/dist/tests/css/container-query-units.spec.js +64 -0
  233. package/dist/tests/css/content-parser.spec.js +13 -0
  234. package/dist/tests/css/filter-parser.spec.d.ts +1 -0
  235. package/dist/tests/css/filter-parser.spec.js +116 -0
  236. package/dist/tests/css/flex-shorthand.spec.d.ts +1 -0
  237. package/dist/tests/css/flex-shorthand.spec.js +45 -0
  238. package/dist/tests/css/grid-clamp.spec.d.ts +1 -0
  239. package/dist/tests/css/grid-clamp.spec.js +82 -0
  240. package/dist/tests/css/parse-css-pseudo.spec.d.ts +1 -0
  241. package/dist/tests/css/parse-css-pseudo.spec.js +26 -0
  242. package/dist/tests/helpers/render-utils.d.ts +18 -2
  243. package/dist/tests/helpers/render-utils.js +25 -12
  244. package/dist/tests/html/dom-converter-pseudo-elements.spec.d.ts +1 -0
  245. package/dist/tests/html/dom-converter-pseudo-elements.spec.js +33 -0
  246. package/dist/tests/html/dom-converter-text.spec.d.ts +1 -0
  247. package/dist/tests/html/dom-converter-text.spec.js +67 -0
  248. package/dist/tests/image/png-backend.spec.d.ts +1 -0
  249. package/dist/tests/image/png-backend.spec.js +34 -0
  250. package/dist/tests/layout/box-sizing.spec.d.ts +1 -0
  251. package/dist/tests/layout/box-sizing.spec.js +75 -0
  252. package/dist/tests/layout/calc-padding.spec.d.ts +1 -0
  253. package/dist/tests/layout/calc-padding.spec.js +19 -0
  254. package/dist/tests/layout/container-query-units.spec.d.ts +1 -0
  255. package/dist/tests/layout/container-query-units.spec.js +24 -0
  256. package/dist/tests/layout/flex-auto-height.spec.d.ts +1 -0
  257. package/dist/tests/layout/flex-auto-height.spec.js +35 -0
  258. package/dist/tests/layout/flex-wrap-cards.spec.d.ts +1 -0
  259. package/dist/tests/layout/flex-wrap-cards.spec.js +16 -0
  260. package/dist/tests/layout/flex-wrap-grow-align-content.spec.d.ts +1 -0
  261. package/dist/tests/layout/flex-wrap-grow-align-content.spec.js +20 -0
  262. package/dist/tests/layout/grid-clamp-gap.spec.d.ts +1 -0
  263. package/dist/tests/layout/grid-clamp-gap.spec.js +22 -0
  264. package/dist/tests/layout/inline-fragments.spec.js +38 -0
  265. package/dist/tests/layout/paged-body-margin.spec.d.ts +1 -0
  266. package/dist/tests/layout/paged-body-margin.spec.js +92 -0
  267. package/dist/tests/layout/pseudo-counters-generated-content.spec.d.ts +1 -0
  268. package/dist/tests/layout/pseudo-counters-generated-content.spec.js +51 -0
  269. package/dist/tests/layout/responsive-clamp-grid-parity.spec.d.ts +1 -0
  270. package/dist/tests/layout/responsive-clamp-grid-parity.spec.js +75 -0
  271. package/dist/tests/layout/run-placer-baseline.spec.js +13 -11
  272. package/dist/tests/pdf/backdrop-filter-noop.spec.d.ts +1 -0
  273. package/dist/tests/pdf/backdrop-filter-noop.spec.js +140 -0
  274. package/dist/tests/pdf/filter-drop-shadow.spec.d.ts +1 -0
  275. package/dist/tests/pdf/filter-drop-shadow.spec.js +74 -0
  276. package/dist/tests/pdf/filter-opacity.spec.d.ts +1 -0
  277. package/dist/tests/pdf/filter-opacity.spec.js +30 -0
  278. package/dist/tests/pdf/font-subset-registry-key.spec.d.ts +1 -0
  279. package/dist/tests/pdf/font-subset-registry-key.spec.js +66 -0
  280. package/dist/tests/pdf/selawik-opt-in.spec.d.ts +1 -0
  281. package/dist/tests/pdf/selawik-opt-in.spec.js +106 -0
  282. package/dist/tests/pdf/system-ui-fallback-subset-regression.spec.d.ts +1 -0
  283. package/dist/tests/pdf/system-ui-fallback-subset-regression.spec.js +39 -0
  284. package/dist/tests/pdf/text-renderer-fallback.spec.js +55 -0
  285. package/dist/tests/pdf/text-transform-matrix.spec.js +8 -7
  286. package/package.json +2 -2
@@ -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,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 font metrics for baseline calculation when FontEmbedder is provided", () => {
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 calculation:
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 default heuristic when FontEmbedder is not provided", () => {
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
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { extractOpacityMultiplier } from "../../src/pdf/utils/filter-utils.js";
3
+ describe("extractOpacityMultiplier", () => {
4
+ it("returns 1 for empty filter list", () => {
5
+ expect(extractOpacityMultiplier([])).toBe(1);
6
+ });
7
+ it("returns value for single opacity()", () => {
8
+ const filters = [{ kind: "opacity", value: 0.5 }];
9
+ expect(extractOpacityMultiplier(filters)).toBe(0.5);
10
+ });
11
+ it("multiplies multiple opacity() filters", () => {
12
+ const filters = [
13
+ { kind: "opacity", value: 0.5 },
14
+ { kind: "opacity", value: 0.5 },
15
+ ];
16
+ expect(extractOpacityMultiplier(filters)).toBe(0.25);
17
+ });
18
+ it("ignores non-opacity filters", () => {
19
+ const filters = [
20
+ { kind: "blur", value: 5 },
21
+ { kind: "opacity", value: 0.7 },
22
+ { kind: "brightness", value: 1.5 },
23
+ ];
24
+ expect(extractOpacityMultiplier(filters)).toBeCloseTo(0.7);
25
+ });
26
+ it("clamps result to [0, 1]", () => {
27
+ const filters = [{ kind: "opacity", value: 0 }];
28
+ expect(extractOpacityMultiplier(filters)).toBe(0);
29
+ });
30
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,66 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { PdfFontRegistry } from "../../src/pdf/font-subset/font-registry.js";
3
+ function makeFont(family, glyphIdForA, rawAccessor) {
4
+ const unicodeMap = new Map([[65, glyphIdForA]]);
5
+ return {
6
+ metrics: {
7
+ metrics: {
8
+ unitsPerEm: 1000,
9
+ ascender: 800,
10
+ descender: -200,
11
+ lineGap: 0,
12
+ capHeight: 700,
13
+ xHeight: 500,
14
+ },
15
+ glyphMetrics: new Map([
16
+ [0, { advanceWidth: 500, leftSideBearing: 0 }],
17
+ [glyphIdForA, { advanceWidth: 600, leftSideBearing: 0 }],
18
+ ]),
19
+ cmap: {
20
+ getGlyphId: (cp) => unicodeMap.get(cp) ?? 0,
21
+ hasCodePoint: (cp) => unicodeMap.has(cp),
22
+ unicodeMap,
23
+ },
24
+ },
25
+ program: {
26
+ sourceFormat: "ttf",
27
+ unitsPerEm: 1000,
28
+ glyphCount: 2048,
29
+ getRawTableData: rawAccessor,
30
+ },
31
+ css: {
32
+ family,
33
+ weight: 700,
34
+ style: "normal",
35
+ },
36
+ };
37
+ }
38
+ describe("PdfFontRegistry keying", () => {
39
+ it("separates subsets for different actual fonts even with same css family stack", () => {
40
+ const registry = new PdfFontRegistry();
41
+ const familyStack = 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
42
+ const fontA = makeFont(familyStack, 44, () => null);
43
+ const fontB = makeFont(familyStack, 43, () => null);
44
+ const runA = {
45
+ font: fontA,
46
+ glyphIds: [44],
47
+ positions: [{ x: 0, y: 0 }],
48
+ text: "H",
49
+ fontSize: 14,
50
+ };
51
+ const runB = {
52
+ font: fontB,
53
+ glyphIds: [43],
54
+ positions: [{ x: 0, y: 0 }],
55
+ text: "H",
56
+ fontSize: 14,
57
+ };
58
+ registry.registerGlyphRun(runA);
59
+ registry.registerGlyphRun(runB);
60
+ const subsetA = registry.ensureSubsetFor(fontA);
61
+ const subsetB = registry.ensureSubsetFor(fontB);
62
+ expect(subsetA.subset.name).not.toBe(subsetB.subset.name);
63
+ expect(subsetA.subset.encodeGlyph(44)).toBe(44);
64
+ expect(subsetB.subset.encodeGlyph(43)).toBe(43);
65
+ });
66
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,106 @@
1
+ import path from "node:path";
2
+ import { describe, expect, it } from "vitest";
3
+ import { prepareHtmlRender, renderHtmlToPdf } from "../../src/html-to-pdf.js";
4
+ import { loadBuiltinFontConfig } from "../../src/pdf/font/builtin-fonts.js";
5
+ import { NodeEnvironment } from "../../src/environment/node-environment.js";
6
+ import { PdfDocument } from "../../src/pdf/primitives/pdf-document.js";
7
+ import { FontRegistry } from "../../src/pdf/font/font-registry.js";
8
+ import { TextFontResolver } from "../../src/pdf/renderers/text-font-resolver.js";
9
+ const ASSET_FONTS_DIR = path.resolve(process.cwd(), "assets/fonts");
10
+ const STACK = "'Selawik', 'DejaVu Sans', 'Arimo', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif";
11
+ const STRESS_TEXT = "HTML→PDF Stress • sem JS • sem fixed/sticky";
12
+ const SELAWIK_CSS = `
13
+ @font-face {
14
+ font-family: 'Selawik';
15
+ src: url('ttf/selawik/selawkl.ttf') format('truetype');
16
+ font-weight: 300;
17
+ font-style: normal;
18
+ }
19
+ @font-face {
20
+ font-family: 'Selawik';
21
+ src: url('ttf/selawik/selawksl.ttf') format('truetype');
22
+ font-weight: 300;
23
+ font-style: normal;
24
+ }
25
+ @font-face {
26
+ font-family: 'Selawik';
27
+ src: url('ttf/selawik/selawk.ttf') format('truetype');
28
+ font-weight: 400;
29
+ font-style: normal;
30
+ }
31
+ @font-face {
32
+ font-family: 'Selawik';
33
+ src: url('ttf/selawik/selawksb.ttf') format('truetype');
34
+ font-weight: 600;
35
+ font-style: normal;
36
+ }
37
+ @font-face {
38
+ font-family: 'Selawik';
39
+ src: url('ttf/selawik/selawkb.ttf') format('truetype');
40
+ font-weight: 700;
41
+ font-style: normal;
42
+ }
43
+ body {
44
+ font-family: ${STACK};
45
+ }
46
+ `;
47
+ function cloneFontConfig(config) {
48
+ return {
49
+ fontFaceDefs: config.fontFaceDefs.map((face) => ({ ...face })),
50
+ defaultStack: [...(config.defaultStack ?? [])],
51
+ };
52
+ }
53
+ describe("Selawik opt-in integration", () => {
54
+ it("loads Selawik from @font-face and keeps symbol fallback safe", async () => {
55
+ const environment = new NodeEnvironment(ASSET_FONTS_DIR);
56
+ const builtin = await loadBuiltinFontConfig(environment);
57
+ if (!builtin) {
58
+ throw new Error("Builtin font config is required for this test");
59
+ }
60
+ const fontConfig = cloneFontConfig(builtin);
61
+ await prepareHtmlRender({
62
+ html: `<div>${STRESS_TEXT}</div>`,
63
+ css: SELAWIK_CSS,
64
+ pageWidth: 794,
65
+ pageHeight: 1123,
66
+ viewportWidth: 794,
67
+ viewportHeight: 1123,
68
+ margins: { top: 0, right: 0, bottom: 0, left: 0 },
69
+ fontConfig,
70
+ resourceBaseDir: ASSET_FONTS_DIR,
71
+ assetRootDir: ASSET_FONTS_DIR,
72
+ environment,
73
+ });
74
+ const selawikFaces = fontConfig.fontFaceDefs.filter((face) => face.family === "Selawik");
75
+ expect(selawikFaces.length).toBeGreaterThanOrEqual(5);
76
+ expect(selawikFaces.every((face) => face.data instanceof ArrayBuffer)).toBe(true);
77
+ expect(selawikFaces.map((face) => face.weight)).toEqual(expect.arrayContaining([300, 300, 400, 600, 700]));
78
+ const registry = new FontRegistry(new PdfDocument(), { fontFaces: [] });
79
+ await registry.initializeEmbedder(fontConfig);
80
+ const resolver = new TextFontResolver(registry);
81
+ const resolved = await resolver.ensureFontResource({
82
+ fontFamily: STACK,
83
+ fontWeight: 400,
84
+ fontStyle: "normal",
85
+ text: STRESS_TEXT,
86
+ });
87
+ const arrow = "→".codePointAt(0);
88
+ const bullet = "•".codePointAt(0);
89
+ expect(resolved.metrics).toBeDefined();
90
+ expect(resolved.metrics.cmap.getGlyphId(arrow)).toBeGreaterThan(0);
91
+ expect(resolved.metrics.cmap.getGlyphId(bullet)).toBeGreaterThan(0);
92
+ const pdfBytes = await renderHtmlToPdf({
93
+ html: `<div>${STRESS_TEXT}</div>`,
94
+ css: SELAWIK_CSS,
95
+ pageWidth: 794,
96
+ pageHeight: 1123,
97
+ viewportWidth: 794,
98
+ viewportHeight: 1123,
99
+ margins: { top: 0, right: 0, bottom: 0, left: 0 },
100
+ resourceBaseDir: ASSET_FONTS_DIR,
101
+ assetRootDir: ASSET_FONTS_DIR,
102
+ environment,
103
+ });
104
+ expect(pdfBytes.byteLength).toBeGreaterThan(1024);
105
+ });
106
+ });
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { PDFParse } from "pdf-parse";
3
+ import { renderHtmlToPdf } from "../../src/html-to-pdf.js";
4
+ describe("system-ui fallback subset regression", () => {
5
+ it("keeps text readable when fallback font differs from initial stack resolution", async () => {
6
+ const html = `
7
+ <!DOCTYPE html>
8
+ <html lang="pt-BR">
9
+ <head>
10
+ <meta charset="utf-8" />
11
+ <style>
12
+ :root { --body-font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
13
+ body { font-family: var(--body-font); margin: 0; }
14
+ .title { font-weight: 700; font-size: 14pt; }
15
+ </style>
16
+ </head>
17
+ <body>
18
+ <div class="title">HTML→PDF Stress</div>
19
+ <div>Fluxo puro • sem JS • sem fixed/sticky</div>
20
+ </body>
21
+ </html>
22
+ `;
23
+ const pdfBytes = await renderHtmlToPdf({
24
+ html,
25
+ pageWidth: 794,
26
+ pageHeight: 1123,
27
+ viewportWidth: 794,
28
+ viewportHeight: 1123,
29
+ margins: { top: 0, right: 0, bottom: 0, left: 0 },
30
+ });
31
+ const parser = new PDFParse({ data: Buffer.from(pdfBytes) });
32
+ const extracted = await parser.getText();
33
+ await parser.destroy();
34
+ const compact = extracted.text.replace(/\s+/g, " ").trim();
35
+ expect(compact).toContain("HTML→PDF");
36
+ expect(compact.toLowerCase()).toContain("stress");
37
+ expect(compact).not.toContain("GSLK");
38
+ });
39
+ });