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
@@ -1,5 +1,21 @@
1
1
  import type { RenderBox, Run } from "../../src/pdf/types.js";
2
- export declare function renderRuns(html: string, css?: string): Promise<Run[]>;
3
- export declare function renderTreeForHtml(html: string, css?: string): Promise<import("../../src/pdf/types.js").LayoutTree>;
2
+ declare const DEFAULT_TEST_MARGINS: {
3
+ top: number;
4
+ right: number;
5
+ bottom: number;
6
+ left: number;
7
+ };
8
+ export interface RenderHelperOptions {
9
+ viewportWidth?: number;
10
+ viewportHeight?: number;
11
+ pageWidth?: number;
12
+ pageHeight?: number;
13
+ margins?: Partial<typeof DEFAULT_TEST_MARGINS>;
14
+ pagedBodyMargin?: "auto" | "zero";
15
+ interBlockWhitespace?: "collapse" | "preserve";
16
+ }
17
+ export declare function renderRuns(html: string, css?: string, options?: RenderHelperOptions): Promise<Run[]>;
18
+ export declare function renderTreeForHtml(html: string, css?: string, options?: RenderHelperOptions): Promise<import("../../src/pdf/types.js").LayoutTree>;
4
19
  export declare function collectRuns(box: RenderBox): Run[];
5
20
  export declare function collectBoxes(box: RenderBox): RenderBox[];
21
+ export {};
@@ -1,27 +1,40 @@
1
1
  import { prepareHtmlRender } from "../../src/html-to-pdf.js";
2
- export async function renderRuns(html, css = "") {
2
+ const DEFAULT_TEST_PAGE = {
3
+ viewportWidth: 794,
4
+ viewportHeight: 1123,
5
+ pageWidth: 794,
6
+ pageHeight: 1123,
7
+ };
8
+ // Keep legacy fixture behavior stable: these tests historically rendered with 48px
9
+ // effective margins because prepareHtmlRender ignored helper-provided margins.
10
+ const DEFAULT_TEST_MARGINS = { top: 48, right: 48, bottom: 48, left: 48 };
11
+ export async function renderRuns(html, css = "", options = {}) {
3
12
  const wrappedHtml = /<html[\s>]/i.test(html) ? html : `<html><body>${html}</body></html>`;
4
13
  const { renderTree } = await prepareHtmlRender({
5
14
  html: wrappedHtml,
6
15
  css,
7
- viewportWidth: 794,
8
- viewportHeight: 1123,
9
- pageWidth: 794,
10
- pageHeight: 1123,
11
- margins: { top: 96, right: 96, bottom: 96, left: 96 },
16
+ viewportWidth: options.viewportWidth ?? DEFAULT_TEST_PAGE.viewportWidth,
17
+ viewportHeight: options.viewportHeight ?? DEFAULT_TEST_PAGE.viewportHeight,
18
+ pageWidth: options.pageWidth ?? DEFAULT_TEST_PAGE.pageWidth,
19
+ pageHeight: options.pageHeight ?? DEFAULT_TEST_PAGE.pageHeight,
20
+ margins: options.margins ?? DEFAULT_TEST_MARGINS,
21
+ pagedBodyMargin: options.pagedBodyMargin ?? "auto",
22
+ interBlockWhitespace: options.interBlockWhitespace ?? "collapse",
12
23
  });
13
24
  return collectRuns(renderTree.root);
14
25
  }
15
- export async function renderTreeForHtml(html, css = "") {
26
+ export async function renderTreeForHtml(html, css = "", options = {}) {
16
27
  const wrappedHtml = /<html[\s>]/i.test(html) ? html : `<html><body>${html}</body></html>`;
17
28
  const { renderTree } = await prepareHtmlRender({
18
29
  html: wrappedHtml,
19
30
  css,
20
- viewportWidth: 794,
21
- viewportHeight: 1123,
22
- pageWidth: 794,
23
- pageHeight: 1123,
24
- margins: { top: 96, right: 96, bottom: 96, left: 96 },
31
+ viewportWidth: options.viewportWidth ?? DEFAULT_TEST_PAGE.viewportWidth,
32
+ viewportHeight: options.viewportHeight ?? DEFAULT_TEST_PAGE.viewportHeight,
33
+ pageWidth: options.pageWidth ?? DEFAULT_TEST_PAGE.pageWidth,
34
+ pageHeight: options.pageHeight ?? DEFAULT_TEST_PAGE.pageHeight,
35
+ margins: options.margins ?? DEFAULT_TEST_MARGINS,
36
+ pagedBodyMargin: options.pagedBodyMargin ?? "auto",
37
+ interBlockWhitespace: options.interBlockWhitespace ?? "collapse",
25
38
  });
26
39
  return renderTree;
27
40
  }
@@ -0,0 +1,33 @@
1
+ import { parseHTML } from "linkedom";
2
+ import { ComputedStyle } from "../../src/css/style.js";
3
+ import { buildCssRules } from "../../src/html/css/parse-css.js";
4
+ import { synthesizePseudoElement } from "../../src/html/dom-converter/pseudo-elements.js";
5
+ import { makeUnitParsers } from "../../src/units/units.js";
6
+ function createTestContext() {
7
+ return {
8
+ resourceBaseDir: "",
9
+ assetRootDir: "",
10
+ units: makeUnitParsers({ viewport: { width: 800, height: 600 } }),
11
+ rootFontSize: 16,
12
+ };
13
+ }
14
+ describe("dom-converter/pseudo-elements", () => {
15
+ it("synthesizes a pseudo element with generated text content", async () => {
16
+ const { document } = parseHTML(`<div id="host" data-label="Alpha"></div>`);
17
+ const element = document.querySelector("#host");
18
+ const cssRules = buildCssRules(`#host::before { content: "X"; }`).styleRules;
19
+ const result = await synthesizePseudoElement(element, "::before", cssRules, new ComputedStyle(), createTestContext(), null);
20
+ expect(result).not.toBeNull();
21
+ expect(result?.tagName).toBe("::before");
22
+ expect(result?.customData).toMatchObject({ pseudoType: "before" });
23
+ expect(result?.children).toHaveLength(1);
24
+ expect(result?.children[0]?.textContent).toBe("X");
25
+ });
26
+ it("returns null when the pseudo style has no content", async () => {
27
+ const { document } = parseHTML(`<div id="host"></div>`);
28
+ const element = document.querySelector("#host");
29
+ const cssRules = buildCssRules(`#host { color: red; }`).styleRules;
30
+ const result = await synthesizePseudoElement(element, "::after", cssRules, new ComputedStyle(), createTestContext(), null);
31
+ expect(result).toBeNull();
32
+ });
33
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,67 @@
1
+ import { parseHTML } from "linkedom";
2
+ import { Display } from "../../src/css/enums.js";
3
+ import { ComputedStyle } from "../../src/css/style.js";
4
+ import { LayoutNode } from "../../src/dom/node.js";
5
+ import { convertTextDomNode, flushBufferedText } from "../../src/html/dom-converter/text.js";
6
+ describe("dom-converter/text whitespace handling", () => {
7
+ it("preserves a single collapsed space between meaningful siblings", () => {
8
+ const { document } = parseHTML(`<div><span>A</span> <span>B</span></div>`);
9
+ const root = document.querySelector("div");
10
+ const textNode = root?.childNodes[1] ?? null;
11
+ expect(textNode).toBeTruthy();
12
+ const result = convertTextDomNode(textNode, new ComputedStyle());
13
+ expect(result).not.toBeNull();
14
+ expect(result?.textContent).toBe(" ");
15
+ expect(result?.customData).toMatchObject({
16
+ preserveLeadingSpace: true,
17
+ preserveTrailingSpace: true,
18
+ });
19
+ });
20
+ it("drops whitespace-only text at the edge of an element", () => {
21
+ const { document } = parseHTML(`<div> <span>B</span></div>`);
22
+ const root = document.querySelector("div");
23
+ const textNode = root?.childNodes[0] ?? null;
24
+ const result = convertTextDomNode(textNode, new ComputedStyle());
25
+ expect(result).toBeNull();
26
+ });
27
+ it("removes leading collapsed whitespace at block edges while preserving interior separation", () => {
28
+ const { document } = parseHTML(`<div> hello <span>world</span> </div>`);
29
+ const root = document.querySelector("div");
30
+ const leadingTextNode = root?.childNodes[0] ?? null;
31
+ const trailingTextNode = root?.childNodes[2] ?? null;
32
+ const leading = convertTextDomNode(leadingTextNode, new ComputedStyle());
33
+ const trailing = convertTextDomNode(trailingTextNode, new ComputedStyle());
34
+ expect(leading?.textContent).toBe("hello ");
35
+ expect(trailing).toBeNull();
36
+ });
37
+ it("preserves space before trailing text content after an inline sibling", () => {
38
+ const { document } = parseHTML(`<div><span>A</span> tail </div>`);
39
+ const root = document.querySelector("div");
40
+ const trailingTextNode = root?.childNodes[1] ?? null;
41
+ const trailing = convertTextDomNode(trailingTextNode, new ComputedStyle());
42
+ expect(trailing?.textContent).toBe(" tail");
43
+ });
44
+ it("preserves buffered whitespace after inline content but drops it after block content", () => {
45
+ const inlineChildren = [new LayoutNode(new ComputedStyle({ display: Display.Inline }))];
46
+ const blockChildren = [new LayoutNode(new ComputedStyle({ display: Display.Block }))];
47
+ const style = new ComputedStyle();
48
+ flushBufferedText(inlineChildren, " ", style);
49
+ flushBufferedText(blockChildren, " ", style);
50
+ expect(inlineChildren.at(-1)?.textContent).toBe(" ");
51
+ expect(blockChildren).toHaveLength(1);
52
+ });
53
+ it("drops whitespace-only text between block siblings by default", () => {
54
+ const { document } = parseHTML(`<body><p>A</p>\n <p>B</p></body>`);
55
+ const body = document.querySelector("body");
56
+ const textNode = body?.childNodes[1] ?? null;
57
+ const result = convertTextDomNode(textNode, new ComputedStyle({ display: Display.Block }));
58
+ expect(result).toBeNull();
59
+ });
60
+ it("preserves whitespace-only text between block siblings when preserve mode is enabled", () => {
61
+ const { document } = parseHTML(`<body><p>A</p>\n <p>B</p></body>`);
62
+ const body = document.querySelector("body");
63
+ const textNode = body?.childNodes[1] ?? null;
64
+ const result = convertTextDomNode(textNode, new ComputedStyle({ display: Display.Block }), { interBlockWhitespace: "preserve" });
65
+ expect(result?.textContent).toBe(" ");
66
+ });
67
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { ImageService } from '../../src/image/image-service.js';
3
+ import { PngWasmLoader } from '../../src/image/png-wasm-loader.js';
4
+ import { NodeEnvironment } from '../../src/environment/node-environment.js';
5
+ describe('ImageService PNG Backend', () => {
6
+ beforeEach(() => {
7
+ ImageService['instance'] = undefined;
8
+ PngWasmLoader.reset();
9
+ });
10
+ it('should use JS backend when configured', async () => {
11
+ const env = new NodeEnvironment();
12
+ const service = ImageService.getInstance(env);
13
+ service.setPngBackendConfig({ pngBackend: 'js' });
14
+ expect(service.getPngBackendConfig()).toEqual({ pngBackend: 'js' });
15
+ });
16
+ it('should default to auto backend', async () => {
17
+ const env = new NodeEnvironment();
18
+ const service = ImageService.getInstance(env);
19
+ expect(service.getPngBackendConfig()).toEqual({ pngBackend: 'auto' });
20
+ });
21
+ it('should fallback to JS when WASM is not available', async () => {
22
+ const env = new NodeEnvironment();
23
+ const service = ImageService.getInstance(env);
24
+ service.setPngBackendConfig({ pngBackend: 'auto' });
25
+ const isWasmAvailable = await PngWasmLoader.isAvailable();
26
+ expect(isWasmAvailable).toBe(false);
27
+ });
28
+ it('should throw in strict mode when WASM unavailable', async () => {
29
+ const env = new NodeEnvironment();
30
+ const service = ImageService.getInstance(env);
31
+ service.setPngBackendConfig({ pngBackend: 'wasm', wasm: { strict: true } });
32
+ await expect(PngWasmLoader.getOrInit()).rejects.toThrow();
33
+ });
34
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,75 @@
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("layout box-sizing behavior", () => {
10
+ it("uses content-box as default and border-box when specified for width", async () => {
11
+ const html = `
12
+ <article style="width:200px;padding:20px;border:5px solid #000">A</article>
13
+ <aside style="width:200px;padding:20px;border:5px solid #000;box-sizing:border-box">B</aside>
14
+ `;
15
+ const tree = await renderTreeForHtml(html);
16
+ const boxes = collectBoxes(tree.root);
17
+ const contentBox = findByTag(boxes, "article");
18
+ const borderBox = findByTag(boxes, "aside");
19
+ expect(contentBox.contentBox.width).toBeCloseTo(200, 3);
20
+ expect(contentBox.borderBox.width).toBeCloseTo(250, 3);
21
+ expect(borderBox.contentBox.width).toBeCloseTo(150, 3);
22
+ expect(borderBox.borderBox.width).toBeCloseTo(200, 3);
23
+ });
24
+ it("applies max-width in border-box space when box-sizing is border-box", async () => {
25
+ const html = `
26
+ <section style="width:400px;max-width:220px;padding:10px;border:5px solid #000;box-sizing:border-box">X</section>
27
+ `;
28
+ const tree = await renderTreeForHtml(html);
29
+ const boxes = collectBoxes(tree.root);
30
+ const section = findByTag(boxes, "section");
31
+ expect(section.contentBox.width).toBeCloseTo(190, 3);
32
+ expect(section.borderBox.width).toBeCloseTo(220, 3);
33
+ });
34
+ it("applies explicit height in border-box space for block containers", async () => {
35
+ const html = `
36
+ <main style="height:200px;padding:20px;border:5px solid #000;box-sizing:border-box"></main>
37
+ `;
38
+ const tree = await renderTreeForHtml(html);
39
+ const boxes = collectBoxes(tree.root);
40
+ const main = findByTag(boxes, "main");
41
+ expect(main.contentBox.height).toBeCloseTo(150, 3);
42
+ expect(main.borderBox.height).toBeCloseTo(200, 3);
43
+ });
44
+ it("applies explicit height in border-box space for grid and table containers", async () => {
45
+ const html = `
46
+ <div style="display:grid;height:200px;padding:20px;border:5px solid #000;box-sizing:border-box"></div>
47
+ <table style="height:200px;padding:20px;border:5px solid #000;box-sizing:border-box">
48
+ <tr><td>cell</td></tr>
49
+ </table>
50
+ `;
51
+ const tree = await renderTreeForHtml(html);
52
+ const boxes = collectBoxes(tree.root);
53
+ const grid = boxes.find((b) => b.tagName === "div");
54
+ const table = findByTag(boxes, "table");
55
+ if (!grid) {
56
+ throw new Error("Expected to find grid container");
57
+ }
58
+ expect(grid.contentBox.height).toBeCloseTo(150, 3);
59
+ expect(grid.borderBox.height).toBeCloseTo(200, 3);
60
+ expect(table.contentBox.height).toBeCloseTo(150, 3);
61
+ expect(table.borderBox.height).toBeCloseTo(200, 3);
62
+ });
63
+ it("applies border-box sizing to form controls with explicit width and height", async () => {
64
+ const html = `
65
+ <input type="text" style="width:200px;height:60px;padding:10px;border:5px solid #000;box-sizing:border-box" value="ok" />
66
+ `;
67
+ const tree = await renderTreeForHtml(html);
68
+ const boxes = collectBoxes(tree.root);
69
+ const input = findByTag(boxes, "input");
70
+ expect(input.contentBox.width).toBeCloseTo(170, 3);
71
+ expect(input.borderBox.width).toBeCloseTo(200, 3);
72
+ expect(input.contentBox.height).toBeCloseTo(30, 3);
73
+ expect(input.borderBox.height).toBeCloseTo(60, 3);
74
+ });
75
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
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("calc() layout for padding", () => {
10
+ it("resolves calc(px + %) using containing block width", async () => {
11
+ const html = "<!DOCTYPE html><html><body style=\"margin:0;\"><main style=\"width:400px;padding:calc(10px + 2%);height:40px;background:#ddd;\">X</main></body></html>";
12
+ const tree = await renderTreeForHtml(html);
13
+ const boxes = collectBoxes(tree.root);
14
+ const main = findByTag(boxes, "main");
15
+ const expectedPadding = 10 + 0.02 * 400;
16
+ expect(main.contentBox.width).toBeCloseTo(400, 2);
17
+ expect(main.borderBox.width).toBeCloseTo(400 + expectedPadding * 2, 2);
18
+ });
19
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
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("container query unit layout", () => {
10
+ it("resolves cqw/cqh/cqmin/cqmax against containing block dimensions", async () => {
11
+ const html = "<!DOCTYPE html><html><body style=\"margin:0;\"><section style=\"width:400px;height:300px;\"><div id=\"a\" style=\"width:50cqw;height:25cqh;\"></div><div id=\"b\" style=\"width:10cqmin;height:10cqmax;\"></div></section></body></html>";
12
+ const tree = await renderTreeForHtml(html);
13
+ const boxes = collectBoxes(tree.root).filter((box) => box.tagName === "div");
14
+ if (boxes.length !== 2) {
15
+ throw new Error(`Expected exactly 2 divs, got ${boxes.length}`);
16
+ }
17
+ const first = boxes[0];
18
+ const second = boxes[1];
19
+ expect(first.contentBox.width).toBeCloseTo(200, 2);
20
+ expect(first.contentBox.height).toBeCloseTo(75, 2);
21
+ expect(second.contentBox.width).toBeCloseTo(30, 2);
22
+ expect(second.contentBox.height).toBeCloseTo(40, 2);
23
+ });
24
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -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,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
+ });