ranger-compiler 3.5.0 → 3.5.2

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 (338) hide show
  1. package/CHANGELOG.md +1160 -19
  2. package/LICENSE +3 -1
  3. package/README.md +124 -29
  4. package/dist/Lang.rgr +1188 -290
  5. package/dist/api.d.ts +2343 -879
  6. package/dist/api.js +79924 -56922
  7. package/dist/lib/JSON.rgr +102 -91
  8. package/dist/lib/Shell.rgr +4 -4
  9. package/dist/lib/apple/AppleToolchain.rgr +4 -4
  10. package/dist/lib/apple/README.md +5 -4
  11. package/dist/lib/apple/apple_test.rgr +1 -1
  12. package/dist/lib/core/README.md +1 -1
  13. package/dist/lib/evg/EVG.rgr +12 -0
  14. package/dist/lib/evg/EVGA11yFromTree.rgr +302 -0
  15. package/dist/lib/evg/EVGA11yTree.rgr +894 -0
  16. package/dist/lib/evg/EVGBox.rgr +267 -0
  17. package/dist/lib/evg/EVGBoxShorthandTest.rgr +220 -0
  18. package/dist/lib/evg/EVGCodepoint.rgr +316 -0
  19. package/dist/lib/evg/EVGColor.rgr +700 -0
  20. package/dist/lib/evg/EVGCommands.rgr +177 -0
  21. package/dist/lib/evg/EVGComponent.rgr +331 -0
  22. package/dist/lib/evg/EVGComponentTest.rgr +387 -0
  23. package/dist/lib/evg/EVGConnector.rgr +541 -0
  24. package/dist/lib/evg/EVGConnectorTest.rgr +306 -0
  25. package/dist/lib/evg/EVGDisplayList.rgr +4186 -0
  26. package/dist/lib/evg/EVGEasing.rgr +370 -0
  27. package/dist/lib/evg/EVGEffectTest.rgr +253 -0
  28. package/dist/lib/evg/EVGElement.rgr +4914 -0
  29. package/dist/lib/evg/EVGFixedTest.rgr +289 -0
  30. package/dist/lib/evg/EVGFlexRulesTest.rgr +317 -0
  31. package/dist/lib/evg/EVGFlexWrapTest.rgr +246 -0
  32. package/dist/lib/evg/EVGFling.rgr +244 -0
  33. package/dist/lib/evg/EVGFocus.rgr +400 -0
  34. package/dist/lib/evg/EVGFocusTest.rgr +399 -0
  35. package/dist/lib/evg/EVGGradient.rgr +322 -0
  36. package/dist/lib/evg/EVGGrapheme.rgr +190 -0
  37. package/dist/lib/evg/EVGGrid.rgr +975 -0
  38. package/dist/lib/evg/EVGHitTest.rgr +208 -0
  39. package/dist/lib/evg/EVGHoles.rgr +294 -0
  40. package/dist/lib/evg/EVGHostMeasurerTest.rgr +281 -0
  41. package/dist/lib/evg/EVGHostTextMeasurer.rgr +331 -0
  42. package/dist/lib/evg/EVGHostTree.rgr +834 -0
  43. package/dist/lib/evg/EVGHostTreeTest.rgr +447 -0
  44. package/dist/lib/evg/EVGImageDecode.rgr +117 -0
  45. package/dist/lib/evg/EVGImageMeasurer.rgr +86 -0
  46. package/dist/lib/evg/EVGInspect.rgr +883 -0
  47. package/dist/lib/evg/EVGInvalidateTest.rgr +432 -0
  48. package/dist/lib/evg/EVGJsonTest.rgr +535 -0
  49. package/dist/lib/evg/EVGLayout.rgr +4220 -0
  50. package/dist/lib/evg/EVGMeasure.rgr +1267 -0
  51. package/dist/lib/evg/EVGOverlayTest.rgr +626 -0
  52. package/dist/lib/evg/EVGPatch.rgr +1721 -0
  53. package/dist/lib/evg/EVGPatchTest.rgr +692 -0
  54. package/dist/lib/evg/EVGPopoverTest.rgr +395 -0
  55. package/dist/lib/evg/EVGReconcile.rgr +226 -0
  56. package/dist/lib/evg/EVGReconcileTest.rgr +421 -0
  57. package/dist/lib/evg/EVGReject.rgr +126 -0
  58. package/dist/lib/evg/EVGRelayoutTest.rgr +375 -0
  59. package/dist/lib/evg/EVGRtlLayoutTest.rgr +319 -0
  60. package/dist/lib/evg/EVGRuler.rgr +232 -0
  61. package/dist/lib/evg/EVGRulerTest.rgr +172 -0
  62. package/dist/lib/evg/EVGSelectChrome.rgr +178 -0
  63. package/dist/lib/evg/EVGStyleCacheTest.rgr +419 -0
  64. package/dist/lib/evg/EVGStyleSheet.rgr +2148 -0
  65. package/dist/lib/evg/EVGStyleStateTest.rgr +407 -0
  66. package/dist/lib/evg/EVGStyleVarTest.rgr +471 -0
  67. package/dist/lib/evg/EVGText.rgr +105 -0
  68. package/dist/lib/evg/EVGTextEngine.rgr +536 -0
  69. package/dist/lib/evg/EVGTextMeasurer.rgr +720 -0
  70. package/dist/lib/evg/EVGTimingTest.rgr +1417 -0
  71. package/dist/lib/evg/EVGToolbar.rgr +1369 -0
  72. package/dist/lib/evg/EVGTransition.rgr +728 -0
  73. package/dist/lib/evg/EVGTreeJson.rgr +721 -0
  74. package/dist/lib/evg/EVGUnit.rgr +554 -0
  75. package/dist/lib/evg/EVGViewportUnitTest.rgr +236 -0
  76. package/dist/lib/evg/EvgApp.rgr +189 -0
  77. package/dist/lib/evg/EvgBitmapTracer.rgr +4277 -0
  78. package/dist/lib/evg/EvgBitmapTracerTest.rgr +2119 -0
  79. package/dist/lib/evg/EvgHost.rgr +451 -0
  80. package/dist/lib/evg/EvgTest.rgr +91 -0
  81. package/dist/lib/evg/EvgTraceColor.rgr +236 -0
  82. package/dist/lib/evg/EvgTraceCurve.rgr +648 -0
  83. package/dist/lib/evg/EvgTraceFit.rgr +767 -0
  84. package/dist/lib/evg/EvgTracePath.rgr +435 -0
  85. package/dist/lib/evg/EvgTraceTypes.rgr +559 -0
  86. package/dist/lib/evg/EvgViewport.rgr +313 -0
  87. package/dist/lib/evg/FxDemoDoc.rgr +185 -0
  88. package/dist/lib/evg/HOSTS.md +227 -0
  89. package/dist/lib/evg/ISSUES.md +835 -0
  90. package/dist/lib/evg/PLAN_ACCESSIBILITY.md +650 -0
  91. package/dist/lib/evg/PLAN_CSS_LAYOUT_AND_FONTS.md +1124 -0
  92. package/dist/lib/evg/PLAN_EFFECTS.md +431 -0
  93. package/dist/lib/evg/PLAN_EVG.md +518 -0
  94. package/dist/lib/evg/PLAN_EVG_RENDERER.md +1674 -0
  95. package/dist/lib/evg/PLAN_INSPECTOR.md +788 -0
  96. package/dist/lib/evg/PLAN_LINKS_AND_FORMS.md +157 -0
  97. package/dist/lib/evg/PLAN_NATIVE_HOSTS.md +671 -0
  98. package/dist/lib/evg/PLAN_VECTOR_IR.md +664 -0
  99. package/dist/lib/evg/PLAN_VIEW_TRANSFORM.md +322 -0
  100. package/dist/lib/evg/PathBuilder.rgr +333 -0
  101. package/dist/lib/evg/README.md +1236 -0
  102. package/dist/lib/evg/SPEC.md +1252 -0
  103. package/dist/lib/evg/SVGPathParser.rgr +1199 -0
  104. package/dist/lib/evg/SvgParser.rgr +1736 -0
  105. package/dist/lib/evg/TODO_EVG_RENDERER.md +595 -0
  106. package/dist/lib/evg/VectorShapes.rgr +331 -0
  107. package/dist/lib/evg/VectorStroke.rgr +107 -0
  108. package/dist/lib/evg/VectorViewBox.rgr +380 -0
  109. package/dist/lib/evg/agent/README.md +290 -0
  110. package/dist/lib/evg/agent/evg_agent.rgr +899 -0
  111. package/dist/lib/evg/agent/fixtures/broken.evg.json +7 -0
  112. package/dist/lib/evg/agent/fixtures/card.evg.json +9 -0
  113. package/dist/lib/evg/agent/fixtures/connector.css +63 -0
  114. package/dist/lib/evg/agent/fixtures/connector.evg.json +14 -0
  115. package/dist/lib/evg/agent/fixtures/drawn.evg.json +129 -0
  116. package/dist/lib/evg/agent/fixtures/gradient.evg.json +4 -0
  117. package/dist/lib/evg/agent/fixtures/popover.css +99 -0
  118. package/dist/lib/evg/agent/fixtures/popover.evg.json +30 -0
  119. package/dist/lib/evg/agent/roundtrip.sh +74 -0
  120. package/dist/lib/evg/agent/smoke.sh +230 -0
  121. package/dist/lib/evg/android/README.md +97 -0
  122. package/dist/lib/evg/android/androidstubs/AndroidStubs.kt +175 -0
  123. package/dist/lib/evg/android/androidstubs/Annotation.kt +10 -0
  124. package/dist/lib/evg/android/androidstubs/App.kt +30 -0
  125. package/dist/lib/evg/android/androidstubs/Content.kt +41 -0
  126. package/dist/lib/evg/android/androidstubs/ContentRes.kt +12 -0
  127. package/dist/lib/evg/android/androidstubs/InputMethod.kt +46 -0
  128. package/dist/lib/evg/android/androidstubs/Net.kt +6 -0
  129. package/dist/lib/evg/android/androidstubs/Os.kt +30 -0
  130. package/dist/lib/evg/android/androidstubs/Util.kt +15 -0
  131. package/dist/lib/evg/android/androidstubs/View.kt +113 -0
  132. package/dist/lib/evg/android/androidstubs/Widget.kt +16 -0
  133. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidEvgSurface.kt +323 -0
  134. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidTextMeasurer.kt +56 -0
  135. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/RippleEffect.kt +262 -0
  136. package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtEvgSurface.kt +238 -0
  137. package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtTextMeasurer.kt +85 -0
  138. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgEngineThread.kt +115 -0
  139. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgPainter.kt +231 -0
  140. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgSurface.kt +117 -0
  141. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/RecordingSurface.kt +94 -0
  142. package/dist/lib/evg/apple/README.md +128 -0
  143. package/dist/lib/evg/apple/Sources/CoreGraphicsEvgSurface.swift +329 -0
  144. package/dist/lib/evg/apple/Sources/CoreTextMeasurer.swift +70 -0
  145. package/dist/lib/evg/apple/Sources/EvgEngineQueue.swift +105 -0
  146. package/dist/lib/evg/apple/Sources/EvgPainter.swift +225 -0
  147. package/dist/lib/evg/apple/Sources/EvgSurface.swift +163 -0
  148. package/dist/lib/evg/apple/Sources/RecordingSurface.swift +110 -0
  149. package/dist/lib/evg/bench/EvgLayoutBench.rgr +34 -0
  150. package/dist/lib/evg/bench/README.md +64 -0
  151. package/dist/lib/evg/bench/layout-bench.mjs +316 -0
  152. package/dist/lib/evg/bench/layout-cases.mjs +510 -0
  153. package/dist/lib/evg/bench/layout-conformance.mjs +254 -0
  154. package/dist/lib/evg/bin/.gitignore +17 -0
  155. package/dist/lib/evg/evg_test.rgr +313 -0
  156. package/dist/lib/evg/gl/README.md +198 -0
  157. package/dist/lib/evg/gl/a11y-paint-check.mjs +73 -0
  158. package/dist/lib/evg/gl/blur-check.mjs +399 -0
  159. package/dist/lib/evg/gl/boxmodel.json +1 -0
  160. package/dist/lib/evg/gl/demo.html +46 -0
  161. package/dist/lib/evg/gl/effect-presets.css +285 -0
  162. package/dist/lib/evg/gl/effect-presets.js +63 -0
  163. package/dist/lib/evg/gl/effect-shots.mjs +135 -0
  164. package/dist/lib/evg/gl/evg-a11y.js +566 -0
  165. package/dist/lib/evg/gl/evg-binary.js +160 -0
  166. package/dist/lib/evg/gl/evg-engine.js +296 -0
  167. package/dist/lib/evg/gl/evg-fx.js +237 -0
  168. package/dist/lib/evg/gl/evg-gestures.js +209 -0
  169. package/dist/lib/evg/gl/evg-list.js +167 -0
  170. package/dist/lib/evg/gl/evg-measure.js +186 -0
  171. package/dist/lib/evg/gl/evg-textinput.js +303 -0
  172. package/dist/lib/evg/gl/evg-view.js +144 -0
  173. package/dist/lib/evg/gl/evg-webgl.js +3942 -0
  174. package/dist/lib/evg/gl/fx-check.mjs +998 -0
  175. package/dist/lib/evg/gl/fx-demo.html +166 -0
  176. package/dist/lib/evg/gl/fx-demo.js +2 -0
  177. package/dist/lib/evg/gl/fx-serve.mjs +47 -0
  178. package/dist/lib/evg/gl/gestures-check.mjs +189 -0
  179. package/dist/lib/evg/gl/list-binary-check.mjs +152 -0
  180. package/dist/lib/evg/gl/measure-check.mjs +135 -0
  181. package/dist/lib/evg/gl/rotation-check.mjs +207 -0
  182. package/dist/lib/evg/gl/shift-check.mjs +113 -0
  183. package/dist/lib/evg/gl/stroke-check.mjs +168 -0
  184. package/dist/lib/evg/gl/text-snap-check.mjs +139 -0
  185. package/dist/lib/evg/gl/view-check.mjs +343 -0
  186. package/dist/lib/evg/gl/view-policy-check.mjs +184 -0
  187. package/dist/lib/evg/html/evg-dom.js +318 -0
  188. package/dist/lib/evg/html/evg-html.js +600 -0
  189. package/dist/lib/evg/inspect/README.md +415 -0
  190. package/dist/lib/evg/inspect/browser-smoke.mjs +115 -0
  191. package/dist/lib/evg/inspect/evg-inspect.js +947 -0
  192. package/dist/lib/evg/inspect/shots/css.png +0 -0
  193. package/dist/lib/evg/inspect/shots/dashboard.png +0 -0
  194. package/dist/lib/evg/inspect/shots/pptx-slide.png +0 -0
  195. package/dist/lib/evg/inspect/shots/state.png +0 -0
  196. package/dist/lib/evg/inspect/shots.mjs +226 -0
  197. package/dist/lib/evg/oracle/css-blur.json +576 -0
  198. package/dist/lib/evg/oracle/css-box.json +157 -0
  199. package/dist/lib/evg/oracle/css-timing.json +709 -0
  200. package/dist/lib/evg/oracle/css_blur_oracle.mjs +529 -0
  201. package/dist/lib/evg/oracle/css_box_oracle.mjs +106 -0
  202. package/dist/lib/evg/oracle/css_timing_oracle.mjs +389 -0
  203. package/dist/lib/evg/original/EVGColor.clj +310 -0
  204. package/dist/lib/evg/original/EVGColorContext.rgr +178 -0
  205. package/dist/lib/evg/original/SVGPath.rgr +627 -0
  206. package/dist/lib/evg/original/Vec2.crgr +103 -0
  207. package/dist/lib/evg/ranger.json +9 -0
  208. package/dist/lib/evg/showcase/README.md +246 -0
  209. package/dist/lib/evg/showcase/assets/emblem.svg +30 -0
  210. package/dist/lib/evg/showcase/assets/rosette.svg +22 -0
  211. package/dist/lib/evg/showcase/build.mjs +651 -0
  212. package/dist/lib/evg/showcase/pages/album.tsx +30 -0
  213. package/dist/lib/evg/showcase/pages/boxmodel.tsx +37 -0
  214. package/dist/lib/evg/showcase/pages/cards.tsx +41 -0
  215. package/dist/lib/evg/showcase/pages/chart_api.tsx +170 -0
  216. package/dist/lib/evg/showcase/pages/charts.tsx +219 -0
  217. package/dist/lib/evg/showcase/pages/drawing.tsx +160 -0
  218. package/dist/lib/evg/showcase/pages/emoji.tsx +91 -0
  219. package/dist/lib/evg/showcase/pages/flex.tsx +46 -0
  220. package/dist/lib/evg/showcase/pages/more.tsx +332 -0
  221. package/dist/lib/evg/showcase/pages/plots.tsx +262 -0
  222. package/dist/lib/evg/showcase/pages/svg.tsx +63 -0
  223. package/dist/lib/evg/showcase/pages/tables.tsx +209 -0
  224. package/dist/lib/evg/showcase/pages/typography.tsx +43 -0
  225. package/dist/lib/evg/showcase/pages/units.tsx +35 -0
  226. package/dist/lib/evg/showcase/pages/variants.tsx +233 -0
  227. package/dist/lib/evg/showcase/pages/vector.tsx +75 -0
  228. package/dist/lib/evg/showcase/pages/views.tsx +223 -0
  229. package/dist/lib/evg/showcase/tests/chart_api_smoke.mjs +339 -0
  230. package/dist/lib/evg/showcase/tests/gl_smoke.mjs +125 -0
  231. package/dist/lib/evg/showcase/themes/chart_api-default.css +15 -0
  232. package/dist/lib/evg/showcase/themes/charts-default.css +29 -0
  233. package/dist/lib/evg/showcase/themes/drawing-default.css +35 -0
  234. package/dist/lib/evg/showcase/themes/more-default.css +128 -0
  235. package/dist/lib/evg/showcase/themes/plots-default.css +18 -0
  236. package/dist/lib/evg/showcase/themes/showcase.css +508 -0
  237. package/dist/lib/evg/showcase/themes/tables-default.css +123 -0
  238. package/dist/lib/evg/showcase/themes/variants-default.css +15 -0
  239. package/dist/lib/evg/showcase/themes/views-default.css +15 -0
  240. package/dist/lib/evg/tools/bench_vs_potrace.mjs +290 -0
  241. package/dist/lib/evg/tools/evg_image_tool.rgr +488 -0
  242. package/dist/lib/evg/tools/evg_trace_bench.rgr +114 -0
  243. package/dist/lib/evg/tools/evg_trace_cli.rgr +662 -0
  244. package/dist/lib/evg/tools/evg_trace_cpp_bench.rgr +83 -0
  245. package/dist/lib/evg/tools/run_trace_bench.sh +74 -0
  246. package/dist/lib/evg/tools/run_trace_cli_smoke.sh +128 -0
  247. package/dist/lib/evg/web/responsive/EvgResponsiveCheck.rgr +271 -0
  248. package/dist/lib/evg/web/responsive/EvgResponsiveDemo.rgr +531 -0
  249. package/dist/lib/evg/web/responsive/README.md +97 -0
  250. package/dist/lib/evg/web/responsive/build.mjs +90 -0
  251. package/dist/lib/evg/web/responsive/dom-check.mjs +169 -0
  252. package/dist/lib/evg/web/responsive/index.html +177 -0
  253. package/dist/lib/evg/web/responsive/smoke.mjs +221 -0
  254. package/dist/lib/evg/web/tools/assets-client.mjs +95 -0
  255. package/dist/lib/evg/web/tools/boot-bench.mjs +189 -0
  256. package/dist/lib/evg/web/tools/inline-assets.mjs +119 -0
  257. package/dist/lib/evg/web/tools/minify.mjs +60 -0
  258. package/dist/lib/evg/web/tracer/build.mjs +87 -0
  259. package/dist/lib/evg/web/tracer/index.html +2805 -0
  260. package/dist/lib/evg/web/tracer/sample.png +0 -0
  261. package/dist/lib/evg/web/tracer/smoke.mjs +1162 -0
  262. package/dist/lib/evgr/Cargo.lock +21 -0
  263. package/dist/lib/evgr/Cargo.toml +19 -0
  264. package/dist/lib/evgr/README.md +140 -0
  265. package/dist/lib/evgr/bench/NativeBench.rgr +246 -0
  266. package/dist/lib/evgr/bench/compare.mjs +252 -0
  267. package/dist/lib/evgr/bench/speed.mjs +277 -0
  268. package/dist/lib/evgr/bin/.gitignore +3 -0
  269. package/dist/lib/evgr/src/bin/bench.rs +106 -0
  270. package/dist/lib/evgr/src/bin/smoke.rs +13 -0
  271. package/dist/lib/evgr/src/grid.rs +730 -0
  272. package/dist/lib/evgr/src/lib.rs +1003 -0
  273. package/dist/lib/evgr/src/style.rs +468 -0
  274. package/dist/lib/evgr/src/text.rs +83 -0
  275. package/dist/lib/image/BitReader.rgr +171 -0
  276. package/dist/lib/image/Buffer.rgr +173 -0
  277. package/dist/lib/image/DCT.rgr +283 -0
  278. package/dist/lib/image/Deflate.rgr +339 -0
  279. package/dist/lib/image/HuffmanDecoder.rgr +181 -0
  280. package/dist/lib/image/ImageBuffer.rgr +706 -0
  281. package/dist/lib/image/JPEGDecoder.rgr +808 -0
  282. package/dist/lib/image/PNGDecoder.rgr +544 -0
  283. package/dist/lib/image/PNGEncoder.rgr +388 -0
  284. package/dist/lib/image/PPMImage.rgr +224 -0
  285. package/dist/lib/image/ProgressiveJPEGDecoder.rgr +1154 -0
  286. package/dist/lib/image/README.md +33 -0
  287. package/dist/lib/image/RasterBuffer.rgr +294 -0
  288. package/dist/lib/image/VP8BoolDecoder.rgr +163 -0
  289. package/dist/lib/image/WebPDecoder.rgr +433 -0
  290. package/dist/lib/image/WebPLossless.rgr +1035 -0
  291. package/dist/lib/image/WebPLossy.rgr +1929 -0
  292. package/dist/lib/image/ranger.json +9 -0
  293. package/dist/lib/image/testdata/webp/anim_2f_24x16.webp +0 -0
  294. package/dist/lib/image/testdata/webp/fixtures.txt +33 -0
  295. package/dist/lib/image/testdata/webp/gen_fixtures.py +261 -0
  296. package/dist/lib/image/testdata/webp/ll_1x1.webp +0 -0
  297. package/dist/lib/image/testdata/webp/ll_alpha_29x21.webp +0 -0
  298. package/dist/lib/image/testdata/webp/ll_gradient_64x64.webp +0 -0
  299. package/dist/lib/image/testdata/webp/ll_meta_32x32.webp +0 -0
  300. package/dist/lib/image/testdata/webp/ll_noise_17x9.webp +0 -0
  301. package/dist/lib/image/testdata/webp/ll_pal11_23x11.webp +0 -0
  302. package/dist/lib/image/testdata/webp/ll_pal2_19x7.webp +0 -0
  303. package/dist/lib/image/testdata/webp/ll_pal40_16x16.webp +0 -0
  304. package/dist/lib/image/testdata/webp/ll_pal4_13x5.webp +0 -0
  305. package/dist/lib/image/testdata/webp/ll_photo_40x30.webp +0 -0
  306. package/dist/lib/image/testdata/webp/ll_predictors_64x32.webp +0 -0
  307. package/dist/lib/image/testdata/webp/ly_1x1.webp +0 -0
  308. package/dist/lib/image/testdata/webp/ly_alph_raw_f0_21x13.webp +0 -0
  309. package/dist/lib/image/testdata/webp/ly_alph_raw_f1_21x13.webp +0 -0
  310. package/dist/lib/image/testdata/webp/ly_alph_raw_f2_21x13.webp +0 -0
  311. package/dist/lib/image/testdata/webp/ly_alph_raw_f3_21x13.webp +0 -0
  312. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f0_21x13.webp +0 -0
  313. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f1_21x13.webp +0 -0
  314. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f2_21x13.webp +0 -0
  315. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f3_21x13.webp +0 -0
  316. package/dist/lib/image/testdata/webp/ly_alpha_33x17.webp +0 -0
  317. package/dist/lib/image/testdata/webp/ly_exif_15x11.webp +0 -0
  318. package/dist/lib/image/testdata/webp/ly_i16_96x80.webp +0 -0
  319. package/dist/lib/image/testdata/webp/ly_nofilter_24x20.webp +0 -0
  320. package/dist/lib/image/testdata/webp/ly_odd_17x9.webp +0 -0
  321. package/dist/lib/image/testdata/webp/ly_photo_48x40.webp +0 -0
  322. package/dist/lib/image/testdata/webp/ly_q100_20x18.webp +0 -0
  323. package/dist/lib/image/testdata/webp/ly_sharp_40x36.webp +0 -0
  324. package/dist/lib/image/testdata/webp/ly_simple_filter_33x31.webp +0 -0
  325. package/dist/lib/image/testdata/webp/ly_vpx_parts8_37x45.webp +0 -0
  326. package/dist/lib/image/testdata/webp/ly_vpx_skip_96x80.webp +0 -0
  327. package/dist/lib/image/tests/WebPDecodeTool.rgr +81 -0
  328. package/dist/lib/image/tests/WebPDecoderTest.rgr +1162 -0
  329. package/dist/lib/image/tests/run_webp_tests.sh +39 -0
  330. package/dist/lib/rust/RsJson.rgr +468 -0
  331. package/dist/lib/rust/RsPrelude.rgr +1663 -0
  332. package/dist/lib/shell_test.rgr +7 -7
  333. package/dist/lib/stdlib.rgr +21 -5
  334. package/dist/lib/zip/ranger.json +6 -0
  335. package/dist/rgrc.js +87318 -67610
  336. package/package.json +1010 -1040
  337. package/dist/README.md +0 -117
  338. package/dist/package.json +0 -47
@@ -0,0 +1,1124 @@
1
+ # EVG: CSS-closer layout + font-correct rendering
2
+
3
+ **Status:** Phases 0-4 landed
4
+ **Date:** 2026-08-12, updated 2026-08-14
5
+ **Related:** `PLAN_EVG.md`, `SPEC.md`, `ISSUES.md`, `gallery/pdf_writer/TODO_PDF.md`
6
+
7
+ ## 1. Goal
8
+
9
+ Move EVG closer to a practical HTML/CSS subset so photo-book and print layouts can:
10
+
11
+ - use familiar **flexbox** (more complete than today)
12
+ - use simple **CSS Grid** for album pages
13
+ - switch look quickly via **classes / themes** (not only inline props)
14
+ - keep **layout and font rendering correct and consistent** across HTML preview, PDF, and raster
15
+
16
+ The hard requirement is not “full browser CSS”. It is:
17
+
18
+ > **The same text, with the same font file, must measure and paint to the same box on every target.**
19
+
20
+ Without that, flex/grid improvements will still look wrong in print.
21
+
22
+ ## 2. Non-goals (for this track)
23
+
24
+ - Full CSS cascade, specificity wars, or selector engine parity with browsers
25
+ - Media queries / responsive breakpoints as a first deliverable
26
+ - `:hover`, animations, or interactive pseudo-classes
27
+ - Replacing SwiftUI/AppKit for the macOS app chrome
28
+ - Perfect PDF vs browser visual identity for shadows/gradients (already known gaps)
29
+
30
+ ## 3. Current state (baseline)
31
+
32
+ ### Layout
33
+
34
+ | Area | Today | Gap |
35
+ | --- | --- | --- |
36
+ | Lengths | `px`, `%`, `em`, `rem`, `vw` / `vh`, and the absolute print units `pt` / `pc` / `in` / `mm` / `cm`, plus EVG's own `hp` and `fill` | `vmin` / `vmax`, `ch` / `ex`, `calc()` — all rejected rather than misread |
37
+ | Kerning | GPOS pair adjustments (formats 1 and 2, incl. Extension lookups) and the legacy `kern` table, in measurement and in paint | Ligatures and other GPOS features |
38
+ | Flex | Grow, per-item `flex-shrink`, `flex-basis` (as a size, growing or not), `flex` shorthand, min/max frozen and redistributed on BOTH sides of the distribution, `align-self` | Reversed directions (`row-reverse` / `column-reverse`) parse and warn |
39
+ | `gap` | Both axes, including between wrapped lines, with `row-gap` / `column-gap` overriding the shorthand per axis | — |
40
+ | `flex-wrap` | `wrap` (default) / `nowrap` / `wrap-reverse`, with the full `align-content` set including `stretch` | — |
41
+ | Alignment | `justifyContent`, `alignItems` (incl. `stretch` and `baseline`), plus legacy `align` / `verticalAlign` | Naming overlap between the CSS and legacy names |
42
+ | Text intrinsic size | Shrink-wraps to content measured from the real face | — |
43
+ | Grid | `display: grid` with fr/px/%/`auto`/`fit-content()`/repeat/`minmax()` tracks, gaps, spans, `grid-template-areas`, `grid-auto-flow: dense`, `subgrid` on both axes, named lines. 20 fixtures checked against Chromium | Intrinsic sizing of a container item (only definite widths and text leaves contribute) |
44
+ | Styles | Mostly inline JSX attributes | No class/theme stylesheet layer |
45
+
46
+ `min-width` / `max-width` / `min-height` / `max-height` parse, clamp, and are
47
+ ordered correctly: `max` first and `min` second, so the minimum wins a
48
+ contradiction, and inside a flex line a clamp freezes the item and hands the
49
+ space it refused to its siblings. Measured against Chromium in
50
+ [`bench/`](../evg/bench/); asserted in `EVGFlexRulesTest.rgr`.
51
+
52
+ ISSUES #1 (labels taking full parent width in a `row`) is resolved — see that
53
+ file for what the fix depended on.
54
+
55
+ ### Fonts
56
+
57
+ | Path | Role | Risk |
58
+ | --- | --- | --- |
59
+ | `EVGTextMeasurer` default | Heuristic widths (`fontSize * 0.55`) | Declares `isFontAccurate() == false`; reported, and fatal under `-strict-fonts` |
60
+ | `TTFTextMeasurer` + `FontManager` + `TrueTypeFont` | Real advance widths, ascender/descender/lineGap | Installed by every tool via `EVGFontSetup` |
61
+ | HTML renderer | Same TTFs via `@font-face`, measured with `TTFTextMeasurer` | Parity checked against Chromium in CI |
62
+ | Raster (`RasterText`) | Glyph outlines from same TTF | Layout now measures with the same faces |
63
+
64
+ All four targets resolve fonts through `EVGFontSetup` and measure through
65
+ `EVGTextEngine`, so layout and paint cannot disagree. See Phase 0 below for what
66
+ this replaced.
67
+
68
+ ## 4. Design principle: fonts drive layout
69
+
70
+ ```
71
+ ┌─────────────────────────────────────────────────────────────┐
72
+ │ Authoring │
73
+ │ TSX + className / theme (+ optional inline overrides) │
74
+ └─────────────────────────────┬───────────────────────────────┘
75
+ ▼
76
+ ┌─────────────────────────────────────────────────────────────┐
77
+ │ Style resolve │
78
+ │ stylesheet subset → computed style map on each node │
79
+ └─────────────────────────────┬───────────────────────────────┘
80
+ ▼
81
+ ┌─────────────────────────────────────────────────────────────┐
82
+ │ Font resolve │
83
+ │ family + weight + style → concrete TTF face (FontManager) │
84
+ │ FAIL or explicit fallback if face missing │
85
+ │ (never silent guess for print metrics) │
86
+ └─────────────────────────────┬───────────────────────────────┘
87
+ ▼
88
+ ┌─────────────────────────────────────────────────────────────┐
89
+ │ Text shaping / metrics (shared) │
90
+ │ width, ascent, descent, lineHeight, wrap breakpoints │
91
+ │ ONE implementation used by layout + all paint backends │
92
+ └─────────────────────────────┬───────────────────────────────┘
93
+ ▼
94
+ ┌─────────────────────────────────────────────────────────────┐
95
+ │ EVGLayout (flex v2 + grid v1) │
96
+ │ uses only resolved metrics + box model │
97
+ └─────────────────────────────┬───────────────────────────────┘
98
+ ▼
99
+ ┌──────────────┬──────────────┬──────────────┐
100
+ ▼ ▼ ▼
101
+ PDF paint Raster paint HTML preview
102
+ (embed TTF) (outline TTF) (@font-face same files)
103
+ ```
104
+
105
+ ### 4.1 Font correctness rules
106
+
107
+ 1. **Same face file for measure and paint**
108
+ Layout never uses the heuristic measurer when a document declares custom fonts.
109
+
110
+ 2. **Metrics come from TTF tables**
111
+ - advance widths from `hmtx` (via existing `TrueTypeFont.measureText`)
112
+ - `ascent` / `descent` / `lineGap` from `hhea` (already exposed)
113
+ - optional later: OS/2 `sTypo*` vs `hhea` policy documented and fixed
114
+
115
+ 3. **`line-height` semantics**
116
+ Support CSS-like:
117
+ - unitless multiplier (`1.2`) → relative to font size / content box policy (pick one, document it)
118
+ - absolute (`18px`, `14pt`)
119
+ Default should match current print-friendly behavior, not browser quirks mode.
120
+
121
+ 4. **Baseline alignment** ✅
122
+ `align-items: baseline` is implemented against real ascent metrics; text
123
+ nodes carry `calculatedBaseline` / `calculatedDescent`.
124
+
125
+ 5. **Wrapping**
126
+ Word wrap must use the same width measurement as final paint. PDF `wrapText` and layout height calculation must call the shared shaper, not duplicate heuristics.
127
+
128
+ 6. **Weight / style mapping**
129
+ `font-weight: 700` / `bold` resolves to a loaded face (e.g. `"Open Sans Bold"`), not a synthetic stroke in layout. If bold face is missing → warning + regular face (measurable), never “pretend bold” for width.
130
+
131
+ 7. **HTML parity** ✅
132
+ Preview loads the same TTF files through `@font-face`, and the widths a
133
+ browser measures for a set of golden strings are recorded into
134
+ `test/fixtures/browser_parity.snapshot` so the check runs **without a
135
+ browser**. `--update-snapshot` re-records; `--verify-snapshot` confirms the
136
+ stored numbers still match a live Chromium.
137
+
138
+ 8. **Encoding honesty** ✅
139
+ Source is decoded as UTF-8 so layout measures real glyphs. The encoder now
140
+ uses the actual WinAnsi repertoire (CP1252) rather than Latin-1, so the
141
+ 0x80–0x9F band — em and en dashes, curly quotes, ellipsis, bullet, euro —
142
+ reaches the page as the right byte instead of being refused; see Phase 4.2.
143
+ What WinAnsi genuinely cannot hold is reported with its codepoint (fatal
144
+ under `-strict-fonts`) rather than silently substituted. Embedding a subset
145
+ font, which would lift the repertoire limit rather than report it, is still
146
+ open. (A `/ToUnicode` cmap is already written, and now covers that band too,
147
+ so text extracts correctly — but it does not widen what can be encoded.)
148
+
149
+ 9. **Box model** ✅
150
+ Padding, margins, gaps, nesting and background colour are checked against
151
+ recorded browser geometry. Building it found two real divergences:
152
+ percentage padding/margin on the vertical axis resolved against the
153
+ containing block's *height* (CSS uses its **width** on all four sides — the
154
+ rule behind the padding-bottom aspect-ratio trick), and a box whose padding
155
+ exceeded its declared size kept that size and gave its children a negative
156
+ content box, where CSS grows the box instead. Both are fixed.
157
+
158
+ 10. **Kerning** ✅
159
+ EVG reads GPOS pair adjustments — and the legacy `kern` table on a face with
160
+ no GPOS — and applies them in measurement AND in paint. The parity snapshot
161
+ now targets the browser's **kerned** width: worst delta **0.015px** across
162
+ 24 fixtures, and **0.006px** against a live Chromium page. Before this, the
163
+ worst was 1.94px on "Helsinki, 2024" in Cinzel at 30px. See Phase 4.3.
164
+
165
+ ## 5. Style layer (quick theme changes)
166
+
167
+ ### 5.1 Authoring model
168
+
169
+ Keep JSX components; add:
170
+
171
+ ```tsx
172
+ <Document theme="classic">
173
+ <Page className="spread">
174
+ <Label className="caption">Helsinki, 2024</Label>
175
+ <View className="grid-2">
176
+ <Image className="photo" src={a} />
177
+ <Image className="photo" src={b} />
178
+ </View>
179
+ </Page>
180
+ </Document>
181
+ ```
182
+
183
+ Stylesheet (CSS subset or JSON equivalent — CSS surface preferred for familiarity):
184
+
185
+ ```css
186
+ .theme-classic .spread { padding: 24px; gap: 16px; }
187
+ .theme-classic .caption {
188
+ font-family: "Cinzel";
189
+ font-size: 14px;
190
+ line-height: 1.3;
191
+ color: #222;
192
+ }
193
+ .theme-classic .grid-2 {
194
+ display: grid;
195
+ grid-template-columns: 1fr 1fr;
196
+ gap: 12px;
197
+ }
198
+ .theme-classic .photo { object-fit: cover; width: 100%; height: 100%; }
199
+ ```
200
+
201
+ ### 5.2 Supported CSS subset (v1)
202
+
203
+ **Selectors:** `.class`, `.theme-x .class`, element type optional later
204
+ **Properties (style resolve → existing EVG fields):**
205
+
206
+ - box: `width`, `height`, `min/max-width/height`, `margin`, `padding`, `border-*`, `border-radius`
207
+ - flex: `display: flex|block`, `flex-direction`, `flex` / `flex-grow|shrink|basis`, `justify-content`, `align-items`, `align-content`, `flex-wrap`, `gap`
208
+ - grid (v1): `display: grid`, `grid-template-columns`, `grid-template-rows`, `gap`, `grid-column`, `grid-row` (simple span)
209
+ - text/font: `font-family`, `font-size`, `font-weight`, `font-style`, `line-height`, `text-align`, `color`
210
+ - visual: `background`, `background-color`, `opacity` (where backend allows), `object-fit`
211
+
212
+ **Cascade v1:** theme defaults < class < inline attributes. No IDs, no `!important`.
213
+
214
+ ### 5.3 Why not full CSS?
215
+
216
+ Print albums need predictable pages. A small stylesheet with themes gives fast visual iteration without implementing a browser. The resolve step outputs the same computed props PDF already understands.
217
+
218
+ ## 6. Layout: Flexbox v2
219
+
220
+ Bring `EVGLayout` closer to CSS Flexbox without rewriting callers.
221
+
222
+ ### 6.1 Must-have
223
+
224
+ | Feature | Notes |
225
+ | --- | --- |
226
+ | ~~`flex-grow` / `flex-basis`~~ ✅ | `flex` shorthand parses `1`, `1 1 auto`, `2 0 120px` |
227
+ | `flex-wrap` | Row/column wrap with correct line packing |
228
+ | `min-width` / `max-width` / `min-height` / `max-height` | Clamp before grow/shrink |
229
+ | Intrinsic text width | Fix ISSUES #1: content-sized labels in `row` |
230
+ | `gap` on both axes | Already partly present; define wrapping interaction |
231
+ | Shorthand `flex` | Parse `1`, `1 1 auto`, etc. |
232
+
233
+ ### 6.2 Should-have soon after
234
+
235
+ - `align-content` for wrapped lines
236
+ - `align-items: baseline` (depends on font ascent)
237
+ - Deprecate dual naming: prefer CSS names; map legacy `align` / `verticalAlign`
238
+
239
+ ### 6.3 Algorithm note
240
+
241
+ Keep one deterministic pass (or two-pass grow/shrink) in Ranger. Do not call into browser layout for PDF. HTML preview may either:
242
+
243
+ - **A (preferred for parity):** use precomputed EVG frames (absolute positions), or
244
+ - **B:** emit real CSS flex and **diff** against EVG frames in tests
245
+
246
+ For print trust, A or test-gated B is required; never “HTML looks fine so PDF must be fine”.
247
+
248
+ ## 7. Layout: Grid v1
249
+
250
+ Target photo-book pages, not full CSS Grid Level 2.
251
+
252
+ ### 7.1 v1 features
253
+
254
+ - `display: grid`
255
+ - `grid-template-columns` / `grid-template-rows` with:
256
+ - fixed (`120px`, `40%`)
257
+ - `fr`
258
+ - `repeat(n, 1fr)` (limited)
259
+ - `gap` / `row-gap` / `column-gap`
260
+ - child placement: auto flow row, plus `grid-column: span 2` / explicit line numbers (simple)
261
+ - stretch alignment default for images
262
+
263
+ ### 7.2 Example (replaces nested HOC grids)
264
+
265
+ ```tsx
266
+ <View className="page-grid">
267
+ <Image className="photo" src={a} />
268
+ <Image className="photo" src={b} />
269
+ <Image className="photo span-2" src={c} />
270
+ </View>
271
+ ```
272
+
273
+ ```css
274
+ .page-grid {
275
+ display: grid;
276
+ grid-template-columns: 1fr 1fr;
277
+ grid-template-rows: 1fr 1fr;
278
+ gap: 15px;
279
+ width: 100%;
280
+ height: 100%;
281
+ }
282
+ .span-2 { grid-column: span 2; }
283
+ ```
284
+
285
+ ### 7.3 Beyond v1 — landed
286
+
287
+ - **`minmax(min, max)`** sizes as its max and clamps into the range. A track
288
+ that hits a bound is pinned and the space it did not take is offered to the
289
+ rest, the same freeze-and-redistribute the flex axis uses — so
290
+ `minmax(120px, 1fr)` holds its floor without silently starving a neighbour.
291
+ Works inside `repeat()`, which is how a responsive album grid is written.
292
+ - **`grid-template-areas`** draws the page as a picture of names, and
293
+ `grid-area` claims a region. Each name must form a rectangle; a ragged or
294
+ bent one is reported rather than guessed at. With no explicit column template
295
+ the column count comes from the picture.
296
+ - **`grid-auto-flow: row dense`** restarts the scan from the top for each item,
297
+ so a later small item backfills a hole a wider one left behind. The default
298
+ only moves forward, which keeps source order but can leave gaps.
299
+ - **`subgrid`, both axes** — `grid-template-columns: subgrid` and
300
+ `grid-template-rows: subgrid` adopt the enclosing grid's tracks for the span
301
+ the element occupies, so nested cards line their columns *and* their internal
302
+ rows up with each other instead of each dividing its own box. Declared with
303
+ nothing to inherit from, either falls back — one full-width column, or
304
+ content-sized rows — and says so. See Phase 4.5.
305
+
306
+ - **Named grid lines** — `[full-start] 1fr [main] 2fr [main-end]` labels the
307
+ lines between tracks, and `grid-column: main-start / main-end` places against
308
+ them. One line may carry several names (`[a b]`); a name is resolved against
309
+ the container's template, so the placement is parsed from the item and
310
+ pointed at real lines by the layout, which is the only place that sees both.
311
+ A name the template does not define leaves the item auto-placed and is
312
+ reported.
313
+
314
+ - **Intrinsic tracks** — `auto` sizes to the content of the items placed in it,
315
+ and `fit-content(limit)` caps that without ever going below min-content. See
316
+ Phase 4.4.
317
+
318
+ Still out of scope: the intrinsic size of a **container** item, which needs a
319
+ recursive pass — see the end of Phase 4.4.
320
+
321
+ ## 8. Shared text engine API (contract)
322
+
323
+ Centralize in one module used by layout + PDF + raster (+ HTML measurement debug):
324
+
325
+ ```text
326
+ resolveFace(family, weight, style) -> FontFace
327
+ measureRun(face, text, fontSize) -> { width, ascent, descent, lineHeight }
328
+ breakLines(face, text, fontSize, maxWidth, lineHeight) -> [Line]
329
+ Line: { text, width, ascent, descent }
330
+ ```
331
+
332
+ Rules:
333
+
334
+ - Layout heights for text nodes come only from `breakLines`
335
+ - PDF draw uses the same lines (no second wrap pass with different widths)
336
+ - Raster draw uses the same face + fontSize scaling (`unitsPerEm`)
337
+ - Snapshot tests lock glyph advances for a fixture string/font
338
+
339
+ ## 9. Target parity matrix (definition of done)
340
+
341
+ | Concern | HTML preview | PDF | Raster |
342
+ | --- | --- | --- | --- |
343
+ | Face files | `@font-face` same TTF | embedded TTF | parsed TTF outlines |
344
+ | Advance widths | measured via shared engine (or DOM diff ≤ tolerance) | shared engine | shared engine |
345
+ | Line breaks | same breaks as PDF | shared engine | shared engine |
346
+ | Flex/grid frames | same boxes (or asserted) | EVGLayout | EVGLayout |
347
+ | Theme swap | change stylesheet only | same | same |
348
+
349
+ Tolerance: integer pixel/pt rounding policy documented (e.g. round half-up to 1/100 pt for PDF).
350
+
351
+ ## 10. Phased delivery
352
+
353
+ ### Phase 0 — Font/layout correctness foundation ✅
354
+
355
+ Landed last, which is why the earlier phases carried a caveat. The starting
356
+ state was worse than this plan assumed: **no target was measuring with real font
357
+ metrics at all.**
358
+
359
+ - The PDF tool's fonts directory was `./gallery/pdf_writer/Fonts` — a path that
360
+ does not exist (the files are under `assets/fonts`) and resolved against the
361
+ process working directory rather than the document. Every `loadFont` failed,
362
+ so `FontManager.measureText` fell through to `strlen * fontSize * 0.5`.
363
+ - The HTML tool never constructed a `FontManager` at all and measured with
364
+ `fontSize * 0.55`.
365
+ - The PNG tool loaded fonts, painted real glyph outlines, and laid out with the
366
+ heuristic measurer.
367
+
368
+ So preview and print disagreed with each other *and* with the faces being
369
+ painted. Measured against Chromium loading the same TTF, the title in
370
+ `test_theme.tsx` was 8.31px (4.3%) too wide.
371
+
372
+ What landed:
373
+
374
+ - **`EVGTextEngine.rgr`** — the §8 contract (`measureRun`, `breakLines`,
375
+ `lineCount`, `maxLineWidth`) in one place. There were four wrap
376
+ implementations; layout's and the PDF's broke lines in different places, so
377
+ the height layout reserved did not match the lines paint drew. The PDF's
378
+ algorithm won (it measures the whole candidate line, including the real space
379
+ advance, instead of adding a guessed `fontSize * 0.3`) and everything else
380
+ calls it.
381
+ - **The element's own font family is threaded through.** Layout passed a
382
+ hardcoded `"Helvetica"` to the measurer for every string, so a document set in
383
+ Cinzel was laid out with Helvetica widths even once a TTF measurer was
384
+ installed.
385
+ - **`EVGFontSetup.rgr`** — one font-resolution path for all four tools,
386
+ anchored on the document's directory. `-fonts DIR` overrides it and is
387
+ authoritative rather than merely first, since falling back after a typo is
388
+ exactly the silent substitution this is meant to prevent.
389
+ - **Honesty instead of fallback.** `FontManager.hasFont` distinguishes a real
390
+ hit from `getFont`'s substitution chain; measurers declare `isFontAccurate`;
391
+ the engine reports each unbacked family once and `-strict-fonts` refuses to
392
+ write output rather than guessing.
393
+ - **ISSUES #1 closed**, with the family and measurement caveats fixed.
394
+
395
+ Verification:
396
+
397
+ ```
398
+ bash gallery/pdf_writer/test/run_fonts.sh
399
+ ```
400
+
401
+ `font_metrics_test.rgr` locks advance widths, vertical metrics and wrap
402
+ positions against the real TTFs (29 assertions). `font_parity.js` renders a page
403
+ in Chromium with the same faces via `@font-face` and compares EVG's boxes to the
404
+ browser's — the check that catches EVG agreeing with itself while both sides are
405
+ wrong. Worst delta is now **0.375px against 8.31px before**.
406
+
407
+ ### Phase 1 — Flexbox v2
408
+
409
+ - ~~row/column grow, main-axis shrink, wrap gating, `gap`, `alignItems: stretch`~~
410
+ (landed with the engine unification — see §11.1)
411
+ - ~~`flex-basis` and the `flex` shorthand~~ — landed. `flex: 1` sets basis 0, so
412
+ an item shares the line even when it also carries a width; `flex: 2 1 120px`,
413
+ `flex: auto` and `flex: 120px` parse. This removed the wart both bundled
414
+ themes had to document.
415
+ - ~~Widen the JSX attribute surface~~ — see Phase 2.5.
416
+ - ~~Real per-item `flex-shrink` factors~~ — landed. Overflow is shared by
417
+ `flex-shrink x size`, so `flex-shrink: 0` holds an item's size while its
418
+ siblings absorb the whole overflow. With every factor at the CSS default of 1
419
+ this is the same uniform scale as before, which is why no existing page moved.
420
+ - ~~min/max clamped in the right order relative to grow/shrink~~ — landed.
421
+ Limits are resolved inside the distribution: an item that hits one is frozen
422
+ there and the space it did not use is offered back to the others, instead of
423
+ being clamped afterwards and leaving a hole in the row.
424
+ - ~~`align-content` for wrapped lines~~ — landed: `flex-start`, `flex-end`,
425
+ `center`, `space-between`, `space-around`, `space-evenly`. It applies only
426
+ when the content wrapped and the container height is definite, following the
427
+ same rule as the rest of the engine. `stretch` would have to grow each line
428
+ and re-lay its children out, so it currently behaves as `flex-start`.
429
+ - ~~`wrap-reverse`~~ — landed: the lines stack from the far edge, with the wrap
430
+ points unchanged.
431
+ - ~~`align-content: stretch`~~ — landed: each line grows by an equal share of
432
+ the spare space, and items that did not ask for a specific height grow with
433
+ their line and re-lay their own children inside the taller box.
434
+ - Update `PhotoLayouts` only where behavior changes
435
+
436
+ ### Phase 2 — Style layer ✅
437
+
438
+ Landed. Open decision #1 was settled in favour of a **real CSS subset parser**
439
+ rather than JSON style maps: the property names were already CSS-shaped, and
440
+ `EVGElement.setAttribute` already accepted both `font-size` and `fontSize`, so
441
+ the parser only had to tokenize and dispatch — no second authoring vocabulary.
442
+
443
+ - `EVGStyleSheet.rgr` parses `.class`, `.theme-<name> .class`, selector lists
444
+ and `/* comments */`, and applies rules over a tree
445
+ - Cascade: unscoped class < theme-scoped class < inline attributes, with source
446
+ order breaking ties inside each group
447
+ - Inline precedence is explicit, not positional: front-ends call
448
+ `EVGElement.markInline()` for every authored attribute and the applier skips
449
+ those properties. `toKebab` normalizes so `fontSize` and `font-size` are one key
450
+ - `-css FILE` (repeatable) and `-theme NAME` on the PDF and HTML tools, via the
451
+ shared `EVGStyleLoader` so both targets resolve identically
452
+ - Unsupported selectors are collected and printed, not silently dropped
453
+ - `examples/themes/classic.css` + `minimal.css` declare the same class names, so
454
+ `examples/test_theme.tsx` swaps look with no TSX edit:
455
+
456
+ ```
457
+ evg-html test_theme.tsx out.html -css themes/classic.css -theme classic
458
+ evg-html test_theme.tsx out.html -css themes/minimal.css -theme minimal
459
+ ```
460
+
461
+ Not in this subset: element/ID selectors, multi-level descendants, pseudo-classes,
462
+ `!important`, shorthand expansion beyond what `setAttribute` already does.
463
+
464
+ ### Phase 2.5 — Attribute surface ✅
465
+
466
+ Every phase turned up a property that parsed but never reached the engine,
467
+ because a property had to appear in **two** independent whitelists —
468
+ `parseAttributes` in `JSXToEVG.rgr` for JSX attributes, and
469
+ `EVGElement.setAttribute` for the stylesheet and `ComponentEngine` paths. Missing
470
+ from either meant silently dropped on that path only:
471
+
472
+ | Property | Was dropped from | Found during |
473
+ | --- | --- | --- |
474
+ | `gap` | JSX attributes | Phase 1 |
475
+ | `className` | JSX attributes (compared to `"className"`, arrives as `"class-name"`) | Phase 2 |
476
+ | `display` | `setAttribute` — so a stylesheet could set every grid property and still lay out as a block | Phase 3 |
477
+ | `justify-content`, `align-items`, `border-width`, `border-color` | JSX attributes | closing this gap |
478
+
479
+ `parseAttributes` now calls `setAttribute` first and the special cases refine
480
+ the result, so the two surfaces cannot drift: anything the element understands
481
+ is reachable from JSX. Unknown names are ignored, so component props like `key`
482
+ pass through harmlessly.
483
+
484
+ `gallery/pdf_writer/test/attrs_test.rgr` is the guard — it sets each property as
485
+ a JSX attribute and asserts it landed, so a refactor that reintroduces a
486
+ whitelist fails there rather than in someone's print run.
487
+
488
+ The examples show the cost of the old behaviour: `test_scandinavian`'s cover page
489
+ authored `justifyContent="center" alignItems="center"` and was never centred, and
490
+ `test_simple` authored a border that never drew.
491
+
492
+ ### Phase 3 — Grid v1 ✅
493
+
494
+ Landed in `EVGGrid.rgr` (track lists + placement parsing) and
495
+ `EVGLayout.layoutGrid` (auto-flow placement). `layoutChildren` branches to it on
496
+ `display: grid`, keeping the same "returns content height" contract, so a grid
497
+ nests inside flex and vice versa.
498
+
499
+ - `grid-template-columns` / `grid-template-rows`: fixed px, `%`, `fr`,
500
+ `repeat(n, …)`. Fixed and percentage tracks are taken first, `fr` splits the
501
+ remainder in proportion
502
+ - `gap`, plus `row-gap` / `column-gap` overriding it per axis
503
+ - `grid-column` / `grid-row` accepting `span N`, `N`, `N / M`, `N / span M`
504
+ - Auto-flow row placement over an occupancy map, so column *and* row spans
505
+ reserve their cells and later items flow around them
506
+ - Items stretch to their cell by default (§7.1); an explicit `height` still wins,
507
+ because `layoutElement` only consults the stretch height when `height.isSet`
508
+ is false
509
+ - Row sizing mirrors the Phase 1 auto-height rule: `grid-template-rows` is only
510
+ used when the container's height is definite. Otherwise rows are
511
+ content-sized from a measuring pass, so a grid in normal document flow grows
512
+ to its content instead of being squeezed into a stale inner height
513
+ - A grid with no column template is a single full-width column, rather than
514
+ collapsing to zero
515
+
516
+ `components/PhotoLayouts.tsx` `FourPhotoGrid` is now a real 2×2 grid. The old
517
+ version faked it with `48%` widths and `4%` margins, which made the horizontal
518
+ and vertical gutters different sizes; one `gap` value now drives both axes.
519
+
520
+ `examples/themes/album.css` + `examples/test_album_grid.tsx` are the print
521
+ fixtures — one tree, three compositions:
522
+
523
+ ```
524
+ evg-html test_album_grid.tsx a4.html -css themes/album.css -theme album -w 595 -h 842
525
+ evg-html test_album_grid.tsx land.html -css themes/album.css -theme album -w 842 -h 595
526
+ evg-html test_album_grid.tsx contact.html -css themes/album.css -theme contact -w 595 -h 842
527
+ ```
528
+
529
+ `grid-template-areas`, `grid-auto-flow: dense`, `minmax()` and column `subgrid`
530
+ landed afterwards — see §7.3. Still out of scope: row `subgrid`, `fit-content()`
531
+ and named lines. `auto` in a track list is accepted but behaves as `1fr` —
532
+ sizing it properly needs per-track content measurement.
533
+
534
+ ### Phase 4 — Baseline + polish ✅
535
+
536
+ **`align-items: baseline`.** Text nodes record `calculatedBaseline` (leading +
537
+ ascent from the real face) and `calculatedDescent`; containers inherit the first
538
+ in-flow child's baseline, so wrapping a label in a `View` does not break the
539
+ alignment. A box with no text baseline aligns on its bottom margin edge, which
540
+ is what CSS does and what makes an image sit on a caption's baseline. Row
541
+ alignment takes the max baseline offset and shifts each item down to meet it.
542
+ This is why the rule waited for Phase 0 — it is meaningless without real ascents.
543
+
544
+ **Encoding honesty (§4.1 rule 8).** The starting point was not the expected
545
+ "unsupported codepoint becomes `?`" — it was that `buffer_to_string` builds a
546
+ string with one character per *byte*, i.e. it reads UTF-8 source as Latin-1. So
547
+ `ä` arrived as two characters and `—` as three. Layout measured two or three
548
+ glyph advances for one character, and the PDF wrote each byte as its own WinAnsi
549
+ escape, printing `ä` where the source said `ä`. Nothing complained, because the
550
+ mangled bytes are all inside the byte range.
551
+
552
+ `Utf8.decode` now runs at every source-read boundary (TSX, components,
553
+ stylesheets). Text carries real codepoints, so measurement counts real glyphs,
554
+ Latin-1-range characters encode correctly, and characters genuinely outside
555
+ WinAnsi are reported with their codepoint and context instead of being hidden:
556
+
557
+ ```
558
+ Encoding warning: U+2014 in "Hyvää yötä — Kaivopuisto" is outside WinAnsi and was written as '?'
559
+ ```
560
+
561
+ `-strict-fonts` makes that fatal. Invalid byte sequences pass through unchanged,
562
+ so a file that really is Latin-1 keeps working.
563
+
564
+ **Bleed-aware page boxes.** `-bleed PT` grows the sheet by that much on every
565
+ side, translates the page content into the middle of it, and declares
566
+ `TrimBox`/`BleedBox` so a printer knows where the finished page is cut. Layout
567
+ keeps working in trim coordinates and knows nothing about bleed. With no bleed
568
+ the output is byte-identical to before — a single `MediaBox`, no translate.
569
+
570
+ ```
571
+ evg-pdf album.tsx out.pdf -bleed 8.5 # 3mm trade bleed
572
+ ```
573
+
574
+ Verification: `bash gallery/pdf_writer/test/run_print.sh` (23 assertions covering
575
+ UTF-8 decoding, WinAnsi detection and page boxes at both orientations), plus the
576
+ baseline cases in `evg_test`.
577
+
578
+ ### Phase 4.1 — CSS length units ✅
579
+
580
+ Checking the units against a browser turned up three things that were wrong in
581
+ ways nothing would have complained about.
582
+
583
+ **`font-size` never inherited.** Every element was constructed carrying a 14px
584
+ font-size, so `inheritProperties` copied the parent's size in and the default
585
+ immediately overwrote it — a `font-size` only ever applied to the element that
586
+ declared it, never to anything below. The root had the opposite problem: it has
587
+ no parent, so `inheritProperties` never ran on it at all and a size set on a
588
+ `Page` or `Print` was dropped outright. `fontSize` now starts *unset*, layout
589
+ fills it in from the inherited size (remembering that it did, so a re-layout
590
+ does not mistake its own fill-in for an authored value), and `EVGLayout.layout`
591
+ applies the root's own size before anything descends. This is what `em` needs,
592
+ and it fixes inherited text size at the same time.
593
+
594
+ **`2rem` silently meant `2em`.** The parser tested the two-character suffix
595
+ first, so `rem` was chopped to `"2r"` — and `to_double` stops at the first
596
+ character it cannot use, so that reads as 2. `rem` is now a unit of its own,
597
+ resolved against the root's font size, which is threaded down the tree
598
+ alongside the inherited one (a unit cannot reach the root on its own, so
599
+ whoever resolves it hands the value over).
600
+
601
+ **Any unknown unit became pixels.** The same `to_double` behaviour meant `10vw`
602
+ resolved to 10px and `calc(100% - 20px)` to 100px. An unrecognised suffix now
603
+ leaves the length unset — i.e. `auto`, which is what a browser does with a
604
+ declaration it cannot parse. `ch`/`ex` (they need font metrics the unit layer
605
+ cannot reach) and `calc()` stay out of scope, but they now stay out loudly.
606
+
607
+ A suffix test alone was not enough, and a later pass found the hole: the
608
+ NUMBER has to be a number too. `to_double` stops at the first character it
609
+ cannot use, so `10vmin` — which ends in `in`, one of the absolute units — came
610
+ out as ten inches, 960 pixels, for a unit this layer does not support. Every
611
+ branch now checks that what precedes the suffix is digits before it believes
612
+ the suffix.
613
+
614
+ **`vw` and `vh` are in.** The line above used to say they were out because a
615
+ print page has no viewport, and that was the wrong way round: a print page has
616
+ a *page area*, which is exactly what CSS says viewport-percentage lengths mean
617
+ in paged media, and it is already what the print renderers hand `EVGLayout` as
618
+ its page size (the section less two margins). So the same rule reads correctly
619
+ on paper and on a screen with no print-specific code: `vw`/`vh` resolve against
620
+ `EVGLayout.pageWidth` / `pageHeight`, published onto the root and inherited the
621
+ way the root font size is.
622
+
623
+ What they buy over `%` is the case `%` cannot express: `height: 100%` needs a
624
+ chain of ancestors that all have a definite height, so a page that wants to be
625
+ as tall as its window has to be handed that number by its host. The ui
626
+ gallery's dashboard was: its page, sidebar and hairline stated a constant
627
+ `1420px`, which looked full in every window shorter than that and stopped two
628
+ thirds of the way down an Android tablet in portrait. They say `100vh` now.
629
+
630
+ Verification: `npm run evg:viewport:test` — 22 checks over the parse, the
631
+ arithmetic, a nested `100vh` that is the page rather than the 300-pixel box it
632
+ sits in, box lengths in `vh`, and a page area of A4 less two 40pt margins.
633
+
634
+ The absolute print units — `pt`, `pc`, `in`, `mm`, `cm` — landed in the same
635
+ pass. They are pinned to CSS's reference pixel (`1in = 96px`) and folded to px
636
+ at parse time, so nothing downstream has to know about them. `12pt` used to
637
+ measure 12px.
638
+
639
+ Verification: 12 unit fixtures in the box-model gate (121 boxes, up from 60),
640
+ covering `rem` against a root size that differs from the local one, `em` on
641
+ `font-size` itself chaining down a tree, the box model in `em`, gaps in `rem`,
642
+ and all five absolute units resolving to the same 96px. All 22 examples that
643
+ render still produce byte-identical HTML.
644
+
645
+ ### Phase 4.2 — Saying so out loud ✅
646
+
647
+ Three things the engine could not do, each of which it had been doing quietly.
648
+
649
+ **WinAnsi is not Latin-1.** The encoder's repertoire check was `codepoint >
650
+ 255`, which is the Latin-1 boundary. WinAnsi is CP1252, and the band Latin-1
651
+ leaves as control codes is exactly where CP1252 keeps the punctuation a book
652
+ sets: `—` `–` `“” ‘’` `…` `•` `€` `†` `‰`. Every one of them was refused,
653
+ written as `?`, and — under `-strict-fonts` — fatal, on text the format can
654
+ carry perfectly well. `Utf8.toWinAnsi` / `fromWinAnsi` now hold the real
655
+ mapping; the encoder uses it, the font's `/Widths` array looks each glyph up by
656
+ its true codepoint instead of by the byte, and the `/ToUnicode` cmap gained the
657
+ 27 `bfchar` entries for the band so the text also extracts correctly.
658
+
659
+ Fixing that exposed a second bug behind it. The JSX parser hands text back one
660
+ token at a time, so `100% sure` arrives as three fragments and the joiner
661
+ decides what goes between them. It was guessing from a whitelist of characters
662
+ allowed to follow a word with no space — `, . ! ? : ; - ) ]` — which got
663
+ `Hello, world!` right and mangled everything else: `100 % sure`, `a / b`,
664
+ `a ( b) c`, `Kämp- hotellissa`, `Gallen- Kallelan`, `usePrintSettings ()`, and
665
+ every curly quote as `“ x ”`. The tokens carry source offsets, so adjacency is
666
+ a fact to read, not a guess: a space is written iff the next token starts past
667
+ where the previous one ended. Twelve of the 22 rendering examples changed, all
668
+ of them corrections.
669
+
670
+ **Grid rejections were invisible.** `fit-content()`, row `subgrid`, a bent
671
+ `grid-template-areas` and a missing `grid-area` name all set an error that was
672
+ handed to `this.log()` — a no-op unless `debug` is on. In a normal run the page
673
+ simply came out with the wrong number of columns. `EVGLayout` now collects
674
+ these, deduplicated, and every tool prints them:
675
+
676
+ ```
677
+ Layout warning: grid-template-columns: Unsupported track size: fit-content(100px)
678
+ Layout warning: grid-template-rows: Unsupported track size: subgrid
679
+ ```
680
+
681
+ **Named grid lines said nothing at all.** `grid-column: sidebar` went through
682
+ `to_double`, came back 0, and 0 is auto — indistinguishable from not having
683
+ written anything. `EVGGridPlacement` now rejects any token that is neither a
684
+ positive line number nor `span N` (which also catches negative line numbers,
685
+ CSS's count-from-the-end form, equally unsupported), keeps the auto placement,
686
+ and reports it.
687
+
688
+ The raster tool reported none of this before — not even font warnings — and now
689
+ reports both.
690
+
691
+ Verification: 12 new WinAnsi assertions plus a round-trip over all 27 assigned
692
+ codes, 12 text-spacing assertions, and 12 grid-warning assertions covering the
693
+ report, the dedup, and silence on a grid the engine fully understands.
694
+
695
+ > **Compiling is not a passing build.** `node dist/rgrc.js` prints
696
+ > `Compilation FAILED` and still exits 0, so `npm run <tool>:compile && echo OK`
697
+ > reports success on a broken tool. Grep the output for `Compilation FAILED`.
698
+ > This cost a round here: a tool that had not rebuilt looked like a feature
699
+ > that had not worked.
700
+
701
+ ### Phase 4.3 — Kerning ✅
702
+
703
+ The last open rule in §4.1, and the one that needed the most care to not make
704
+ things worse.
705
+
706
+ **Reading it.** `TrueTypeFont` walks GPOS: FeatureList for a `kern` feature,
707
+ its lookups, and the pair-adjustment subtables underneath — including
708
+ LookupType 9 (Extension), which large faces use to reach past the 64K offset
709
+ limit, so skipping it would have missed kerning in exactly the fonts that need
710
+ it most. Both PairPos formats are read: format 1 (per-glyph pair sets, binary
711
+ searched) and format 2 (class pairs). Coverage and ClassDef tables are binary
712
+ searched rather than expanded — a class subtable is a few hundred bytes that
713
+ expands to tens of thousands of pairs, almost none of which a given page uses.
714
+
715
+ Two things the browser snapshot caught that reading the spec alone did not:
716
+
717
+ - **Subtables within one lookup are first-match-wins**, not additive. Summing
718
+ every subtable double-counted any pair listed in more than one, which made
719
+ Noto Sans measure narrower than Chromium draws it. Separate lookups still
720
+ accumulate.
721
+ - **A face with GPOS is positioned by GPOS alone.** Open Sans has a GPOS table
722
+ with no `kern` feature *and* 18694 legacy `kern` pairs. Falling back to those
723
+ made EVG kern a run a browser leaves alone. OpenType says GPOS wins outright;
724
+ the fallback now only applies when there is no GPOS table at all.
725
+
726
+ **Painting it.** Measuring kerned while painting unkerned would have been worse
727
+ than not kerning: the box right and the ink wrong. A PDF viewer advances by the
728
+ `/Widths` entry and kerns nothing itself, so the renderer emits a `TJ` array
729
+ instead of `Tj` when the face kerns — `[(H) 15 (e) 10 (lsinki, 20) 25 (2) 15
730
+ (4)] TJ`, which is 65/1000 em at 30px, exactly the 1.95px the measurement
731
+ moved. A run that does not kern still emits a plain `Tj`. The raster pen kerns
732
+ between glyphs the same way; A/B-ing the PNG shows the right edge of that
733
+ Cinzel line moving 2px left with the left edge unchanged, which is kerning
734
+ between glyphs and not a shifted origin.
735
+
736
+ Results:
737
+
738
+ | | before | after |
739
+ | --- | --- | --- |
740
+ | worst vs browser (24 snapshot fixtures) | 1.94px | **0.015px** |
741
+ | worst vs live Chromium page | 0.375px | **0.006px** |
742
+
743
+ All 19 rendering examples that contain text moved, every diff a width or a
744
+ position — no text and no structure changed.
745
+
746
+ ### Phase 4.4 — Grid, against a browser ✅
747
+
748
+ The box-model parity gate learned `display: grid`, so the grid work stopped
749
+ being checked against hand-computed expectations and started being checked
750
+ against Chromium. Sixteen fixtures: fr tracks, mixed px/fr/%, gaps, `repeat()`,
751
+ spans, explicit lines, named lines, `minmax()` both clamped and not, percentage
752
+ tracks, and a padded container. The gate is now **894 assertions over 161
753
+ boxes**, and it needs no browser to run.
754
+
755
+ The existing grid passed all of it on the first recording. That is the useful
756
+ kind of result — it says the earlier phases were right, and it is what made the
757
+ two genuine gaps stand out.
758
+
759
+ **`auto` was a disguised `1fr`.** Predictable, documented, and not what CSS
760
+ does: an `auto` track sizes to the content of the items in it, and only then is
761
+ leftover space handed to the `fr` tracks. Column sizing is now deferred until
762
+ after placement — the tracks cannot be sized until it is known what lands in
763
+ them — and each intrinsic track takes the widest item placed in it.
764
+
765
+ **`fit-content(limit)`** is the same track with a ceiling on the max-content
766
+ side, floored at min-content. That floor is the whole subtlety: Chromium
767
+ measures `fit-content(100px)` around a 200px box as **200**, because a box with
768
+ a definite width cannot be squeezed and the clamp would push it out of its own
769
+ cell. The limit only bites on something that *can* be squeezed — text — which
770
+ is why `EVGTextEngine` gained `minLineWidth` (break at every opportunity, take
771
+ the widest line: CSS's min-content) to sit beside `maxLineWidth`.
772
+
773
+ What contributes to an intrinsic track is deliberately narrow: an item with a
774
+ definite width (which is both its min- and max-content size) and a text leaf.
775
+ A container item contributes nothing rather than a guess — sizing one to its
776
+ subtree needs a real recursive intrinsic pass, and a made-up number would
777
+ silently misplace every neighbour instead of merely leaving a track narrow.
778
+ Items spanning several tracks are left out for the same reason: there is no one
779
+ track to charge them to.
780
+
781
+ ### Phase 4.5 — Row subgrid ✅
782
+
783
+ Columns landed in §7.3; rows were left out because "row sizes are only known
784
+ after the items are measured". True, but the conclusion was wrong: the tracks
785
+ are handed to a subgrid child at *final placement*, and by then the parent's
786
+ rows are settled — whether they came from its own template or from its content
787
+ pass. The handoff is the same code as columns, one axis over.
788
+
789
+ What actually made rows harder is that a row subgrid **always spans** several
790
+ of its parent's rows, and spanning items are excluded from content-based row
791
+ sizing (there is no one row to charge them to). With only subgrid cards in a
792
+ grid, every row measured zero and the cards collapsed. The fix is the thing
793
+ that defines subgrid: it is the *grandchildren* that size those rows. Rather
794
+ than see through the card, the parent measures it once — a subgrid with nothing
795
+ inherited falls back to content-sized rows, which is exactly the measurement
796
+ wanted — and adopts the rows it came up with, one for one, via
797
+ `computedRowSizes`.
798
+
799
+ Fixing this also turned up a bug in what column subgrid had been reporting. A
800
+ subgrid child is laid out during its parent's content-measuring pass, before
801
+ any tracks can exist, and it was announcing "subgrid has no enclosing grid to
802
+ inherit tracks from" every time — which was simply untrue. The enclosing grid
803
+ now claims the child (`subgridPending`) as soon as it collects it, well before
804
+ it can size anything, so a child that is merely early is told apart from one
805
+ that really is orphaned.
806
+
807
+ Verification: four subgrid fixtures against Chromium — rows over an explicit
808
+ parent template, rows over content-sized parent rows, columns, and both axes at
809
+ once — plus 10 assertions on the case subgrid exists for: two cards, spanning
810
+ the same rows, whose captions line up without either card knowing about the
811
+ other. The box-model gate is now **1019 assertions over 203 boxes**.
812
+
813
+ One harness note recorded in the fixtures: a card that subgrids only its rows
814
+ has a template-less column axis, which CSS sizes as a single `auto` track. EVG
815
+ defaults that to `1fr`. Under this harness's `justify-content: start` an auto
816
+ track does not stretch, so the two disagree; with the CSS default of `normal`
817
+ they coincide. The fixtures pin the column axis rather than paper over it.
818
+
819
+ ### Phase 4.6 — The showcase, and the five bugs it found ✅
820
+
821
+ `lib/evg/showcase/` renders six example pages under two themes and to three
822
+ targets, and publishes them to `/evg/` on the project's Pages site
823
+ (`npm run showcase`). Nothing in `pages/*.tsx` carries a visual attribute:
824
+ the pages say what is on the page, one stylesheet says how it looks, and
825
+ swapping `-theme editorial` for `-theme studio` re-skins all six.
826
+
827
+ Rendering real pages found five bugs that every gate had missed, because each
828
+ one was silent:
829
+
830
+ - **Composite glyphs were never drawn.** `ä`, `ö`, `å`, `é` are a base letter
831
+ plus a diacritic, and the raster path skipped that entire glyph kind while
832
+ still reserving its advance — *päivää* rendered as *piv*. `RasterText` now
833
+ reads the component table, applies each component's 2×2 transform and offset,
834
+ and recurses for components that are themselves composite.
835
+ - **Bold was measured in the regular cut.** The renderers append `-Bold` at
836
+ paint time; layout never did, so a bold heading was measured narrow and drawn
837
+ wide, wrapping a line later than its box. Chromium lays "A Mysterious
838
+ Discovery" out at 18px as 196.02 regular and **209.25** bold; EVG reported
839
+ 196.00 for both. `EVGElement.effectiveFontFamily()` is now the one resolver
840
+ layout, the PDF renderer and the raster pen all go through — which also means
841
+ `font-weight` finally does something in a PDF, since the bold face is now
842
+ embedded rather than silently replaced by the regular one.
843
+ - **A grid item resolved percentages against the grid, not its cell.** Every
844
+ `width: 100%` item in a spread was laid out at the full grid width and they
845
+ overlapped. A grid item's containing block is its grid area.
846
+ - **The raster target had no image support at all**, so a photo book rendered
847
+ to PNG came out with the text and none of the pictures. It draws them now,
848
+ with `object-fit: cover`, a decode cache, and the progressive-JPEG decoder
849
+ for files the baseline one rejects — `Example.jpg` is progressive, and used
850
+ to fail with nothing but a line in the log.
851
+ - **An explicit `grid-template-rows` was dropped** when the container had no
852
+ declared height, so `170px auto` on an auto-height deck sized every row from
853
+ content. Tracks that need no container height to resolve are applied now;
854
+ `%` and `fr` genuinely do need one and stay content-sized.
855
+
856
+ Each is covered by the gates: three new browser-verified fixtures (percentage
857
+ items in a cell, with and without padding; an explicit row template on an
858
+ auto-height container) and six assertions pinning the bold face to the width a
859
+ browser actually draws.
860
+
861
+ Known limit the gallery shows rather than hides: the raster target's JPEG
862
+ decode has visible block artefacts, and the PDF path — which embeds the
863
+ original file untouched — does not. Both are on the page, side by side.
864
+
865
+ ### Phase 4.7 — Codepoints, and emoji that actually print ✅
866
+
867
+ **Text is stepped by codepoint.** `charAt` returns a UTF-16 code *unit*, so
868
+ everything outside the BMP — emoji, CJK extensions, most maths symbols — was
869
+ seen as two characters, and neither half is a real codepoint. On a page
870
+ containing `a😀b`:
871
+
872
+ | | before |
873
+ | --- | --- |
874
+ | measured width | 47.53px — the emoji was charged **two** `.notdef` advances |
875
+ | JSON display list | `ed a0 bd ed b8 80`, CESU-8 surrogate halves; a strict UTF-8 parser refuses the file |
876
+ | PDF | two encoding warnings, at U+D83D and U+DE00, which are not characters |
877
+
878
+ `EVGCodepoint` is now the one place that knows how to walk a string —
879
+ `codeAt`, `unitsAt`, `count`, `toArray`, `toStr`, `encodeUtf8` — and it is
880
+ threaded through every site that looks up a glyph or writes bytes.
881
+
882
+ **Emoji reach the page.** Three things had to be true at once, and each was
883
+ false:
884
+
885
+ - **A face that has the glyphs.** `Noto_Emoji/NotoEmoji-Regular.ttf` is loaded
886
+ last, so it is never picked as a substitute for a missing *text* face. It is
887
+ the monochrome `glyf` cut on purpose: the colour formats are bitmaps
888
+ (CBDT/sbix) or layered vectors (COLR/CPAL), and neither the outline
889
+ rasterizer nor the PDF font path can read those. Ordinary outlines mean the
890
+ same file works on all three targets with no new machinery.
891
+ - **A cmap that reaches past the BMP.** Format 4 is 16-bit and cannot address
892
+ U+1F600 at all. `TrueTypeFont` now prefers a format 12 subtable — `(3,10)`
893
+ or `(0,4)`/`(0,6)` — and binary-searches its groups.
894
+ - **A run that can cross faces.** A text face has no emoji and an emoji face
895
+ has no letters, so `Ready 🎉` cannot be measured or drawn from one file.
896
+ `FontManager.faceForCodepoint` resolves per codepoint, the primary family
897
+ winning whatever it can draw, so ordinary text takes exactly the same path it
898
+ did before. Kerning is applied only between two codepoints from the same
899
+ face — a pair spanning the boundary has no kern pair by definition.
900
+
901
+ Each target then does the one thing it has to:
902
+
903
+ - **PNG** — `RasterText` swaps face mid-run for a missing glyph and keeps the
904
+ primary face's baseline, so the line does not step where the face changes.
905
+ - **PDF** — WinAnsi is one byte wide and has no room for U+1F389 at any price,
906
+ so a fallback span is drawn through a **Type0 / Identity-H** resource
907
+ (`/E1..`, alongside the WinAnsi `/F1..`) whose strings are glyph ids, with a
908
+ `/W` array and a `/ToUnicode` cmap built from what was actually drawn. The
909
+ existing fonts are untouched: converting everything to Identity-H would
910
+ change the encoding, `/Widths` and cmap of text that is currently correct in
911
+ order to fix text that currently cannot be written at all.
912
+ - **HTML** — the fallback face is named in the `font-family` stack and
913
+ `@font-face`d, but *only when the document needs it*: without that the
914
+ browser substitutes its own emoji font, whose advances are not the ones EVG
915
+ measured with, and the line wraps where the PDF did not.
916
+
917
+ Emoji in a rendered PDF now extract as their real codepoints (`U+1F389`,
918
+ `U+1F600`, …) rather than as `?`.
919
+
920
+ One bug this surfaced, in code written the same afternoon: `unitsPerEm` is born
921
+ `1000`, so a *blank* `TrueTypeFont` — the "no face has this codepoint" answer —
922
+ passed a `unitsPerEm > 0` test and was used as if it were a font. `✓` (U+2713,
923
+ which nothing bundled actually has) came out as `.notdef` drawn from a face
924
+ that had never been opened, with no warning. `TrueTypeFont.isLoaded()` is the
925
+ test now, and the unencodable character is reported again.
926
+
927
+ ### Phase 4.8 — Clusters, ligatures, and a font that is only as big as the page ✅
928
+
929
+ Two of Phase 4.7's three known limits, closed.
930
+
931
+ **The embedded font carries the glyphs the page used.** `TTFSubset` keeps head,
932
+ hhea, maxp, hmtx, loca and glyf, and drops everything else — `cmap` included,
933
+ because a Type0/Identity-H font never consults it, which takes GSUB, post,
934
+ name and vmtx with it. It deliberately **keeps glyph ids**: a dense
935
+ renumbering would invalidate every string already written, since Identity-H
936
+ puts glyph ids in the content stream. Composite glyphs pull their components
937
+ in transitively.
938
+
939
+ | | before | after |
940
+ | --- | --- | --- |
941
+ | a page with three emoji | 1 298 067 | 429 332 |
942
+ | `test_for_loop_simple` | 1 717 604 | 840 059 |
943
+
944
+ The remainder in each is the WinAnsi text face, which is **not** subset: a
945
+ simple TrueType font is read through its cmap, so that path needs a different
946
+ set of tables kept.
947
+
948
+ **Text is stepped by grapheme cluster.** A codepoint is not what a reader calls
949
+ a character: 🇫🇮 is two, 👍🏽 is two, 1️⃣ is three, 👨‍👩‍👧 is five, and each is one
950
+ glyph, one advance, and one place a line may not break. Stepping by codepoint
951
+ cost three separate things — the face was chosen per codepoint, so a keycap put
952
+ its digit in the text face and its box in the emoji face and the ligature never
953
+ saw all three parts; the width was the sum of the parts, so a family measured
954
+ four advances wide and drew one; and the `/ToUnicode` entry named one codepoint
955
+ for a glyph made of five.
956
+
957
+ `EVGGrapheme` is the cluster rule — a deliberate subset of UAX #29: the
958
+ emoji-relevant rules and the combining marks, not the full property tables.
959
+ `TrueTypeFont.shape()` is the shaper: drop the variation selectors, then take
960
+ the longest GSUB **LookupType 4** ligature, with the Extension (type 7) wrapper
961
+ unwrapped. That is not a general OpenType shaper — no contextual lookups, no
962
+ reordering — but it is exactly what emoji sequences need, and it runs only on a
963
+ run already known to belong to one face, so it can never disturb ordinary text.
964
+ Measured on Noto Emoji, it resolves every case that matters:
965
+
966
+ ```
967
+ 👨‍👩‍👧 5 codepoints -> 1 glyph 🇫🇮 2 -> 1 1️⃣ 3 -> 1
968
+ 👍🏽 2 -> 1 🏳️‍🌈 4 -> 1
969
+ ```
970
+
971
+ A cluster the primary face draws is measured and painted exactly as before —
972
+ per codepoint, with kerning — because those two must not part company. Only a
973
+ cluster handed to a fallback face goes through the shaper. All 28 example PDFs
974
+ re-render **byte-identical** across this change.
975
+
976
+ One bug it turned up in its own first draft: a fallback segment was *measured*
977
+ through the ordinary per-codepoint path and *drawn* shaped, so a joined family
978
+ was charged five advances and drew one, pushing everything after it on the line
979
+ to the right. A segment's width now comes from the same walk that draws it.
980
+
981
+ HTML needed one more thing. Chromium treats U+FE0F as an instruction to use its
982
+ own colour emoji font whatever the `font-family` stack says, so keycaps and the
983
+ rainbow flag came out of the system font, in colour, at advances that were not
984
+ the ones EVG measured with. The HTML renderer now writes the text as the engine
985
+ shaped it — selectors dropped — because the engine has already chosen the
986
+ presentation by choosing the face. All three targets agree glyph for glyph.
987
+
988
+ ### Phase 4.9 — `emoji-color` ✅
989
+
990
+ A monochrome emoji face is outlines, so it takes whatever colour it is filled
991
+ with. Every target already tinted emoji with the element's `color` — the one
992
+ thing the engine could not do was give them a **different** colour from the
993
+ sentence they sit in, because EVG has no inline spans to hang a second colour
994
+ on.
995
+
996
+ ```css
997
+ .caption { color: #1f2937; emoji-color: #e11d48 }
998
+ ```
999
+
1000
+ Inherited like `color`, so a deck sets it once. Unset it is the text colour and
1001
+ every existing document is byte-identical — all 28 example PDFs confirm that.
1002
+
1003
+ - **PDF** — the fill colour is chosen per segment, and a fallback segment is
1004
+ already its own `BT`/`ET` block, so this is one `rg` operator.
1005
+ - **PNG** — the raster pen carries a second colour for fallback clusters.
1006
+ - **HTML** — fallback runs are wrapped in a `<span>` with their own colour,
1007
+ which is also the first thing in this engine that needs the renderer to know
1008
+ the faces rather than just their filenames.
1009
+
1010
+ ### Multi-colour emoji — sized, deliberately not built
1011
+
1012
+ Noto Color Emoji is **COLRv1 with no v0 layer records at all**, so there is no
1013
+ simple layered-glyph path to take. Measured on the v40 face:
1014
+
1015
+ | | |
1016
+ | --- | --- |
1017
+ | file | 25 MB, of which the `SVG ` table is 19 MB |
1018
+ | base colour glyphs | 3 993 |
1019
+ | using a gradient | **2 278 (57%)** |
1020
+ | deepest glyph | 230 layers |
1021
+ | paint formats | `PaintGlyph` 64 637, `PaintSolid` 51 493, transforms 29 107, gradients 8 630, `PaintComposite` 314 |
1022
+
1023
+ Two facts decide it. Gradients are not a rounding error — flattening them to a
1024
+ single stop would visibly degrade more than half the set — and PDF has no COLR
1025
+ support at all, so colour glyphs must become vector artwork with invisible text
1026
+ behind them for extraction. That is a paint-graph interpreter, a glyph
1027
+ outline → PDF path converter, axial and radial shading patterns on both the PDF
1028
+ and the raster side, and a subsetter that follows COLR layer references. It is
1029
+ tractable and it is scoped here; it is not a variation on what `emoji-color`
1030
+ does.
1031
+
1032
+ ## 11. File / module impact (expected)
1033
+
1034
+ | Area | Likely touch points |
1035
+ | --- | --- |
1036
+ | Layout | `lib/evg/EVGLayout.rgr`, `EVGElement.rgr`, `EVGText.rgr`, `EVGGrid.rgr` |
1037
+ | Fonts | `pdf_writer/src/fonts/FontManager.rgr`, `TrueTypeFont.rgr`, `EVGFontSetup.rgr`, `lib/evg/EVGTextEngine.rgr` |
1038
+ | JSX bridge | `pdf_writer/src/jsx/JSXToEVG.rgr`, component engine |
1039
+ | Style | `lib/evg/EVGStyleSheet.rgr` (parse/resolve), `pdf_writer/src/core/EVGStyleLoader.rgr` (CLI wiring) |
1040
+ | Renderers | `EVGPDFRenderer`, `EVGHTMLRenderer`, `EVGRasterRenderer` / `RasterText` |
1041
+ | Examples | `components/PhotoLayouts.tsx`, new theme CSS, album fixtures |
1042
+ | Docs | this plan → later SPEC sections for flex/grid/style |
1043
+
1044
+ ### 11.1 One engine, one location
1045
+
1046
+ `lib/evg/` is the single EVG engine. It was previously forked — the game
1047
+ engine carried its own copy under `gallery/game_engine/v2/evg/` that had drifted
1048
+ ~200 lines ahead (main-axis `gap`, column grow, shrink-to-fit, `alignItems:
1049
+ stretch`, `flexWrap`, `SVGPathParser.flatten`). That copy has been promoted into
1050
+ `lib/evg/` and deleted, so pdf_writer, the game engine (v1 and v2) and
1051
+ `watch_evg` all compile against the same files.
1052
+
1053
+ Only `EvElementToEVG.rgr` (which depends on the interpreter's `EvalValue`) and
1054
+ `evg_test.rgr` (which uses the v2 `RgTest` harness) remain under
1055
+ `game_engine/v2/evg/`. **Layout changes belong in `lib/evg/` only** — do not
1056
+ re-fork.
1057
+
1058
+ ### 11.2 Gates
1059
+
1060
+ ```
1061
+ npm run test:evg:all
1062
+ ```
1063
+
1064
+ runs all three, or individually:
1065
+
1066
+ | Script | Suite | Covers |
1067
+ | --- | --- | --- |
1068
+ | `test:evg` | `gallery/game_engine/v2/evg/run.sh` | layout engine: units, box model, flex, grid, stylesheet, text engine, baseline |
1069
+ | `test:evg:fonts` | `gallery/pdf_writer/test/run_fonts.sh` | advance-width goldens against the real TTFs, plus EVG-vs-browser parity from a recorded snapshot |
1070
+ | `test:evg:layout` | `gallery/pdf_writer/test/run_layout.sh` | box-model parity against the browser: padding, margins, gaps, nesting, background colour |
1071
+ | `test:evg:frontend` | `gallery/pdf_writer/test/run_print.sh` | UTF-8 decoding, the WinAnsi repertoire, page boxes, JSX text spacing, and the JSX attribute surface |
1072
+
1073
+ **None of these need a browser.** The browser-measured widths live in
1074
+ `gallery/pdf_writer/test/fixtures/browser_parity.snapshot`, so the parity gate
1075
+ runs anywhere. Chromium is only needed to change that file:
1076
+
1077
+ ```
1078
+ bash gallery/pdf_writer/test/run_fonts.sh --verify-snapshot # still matches a live browser?
1079
+ bash gallery/pdf_writer/test/run_fonts.sh --update-snapshot # re-record it
1080
+ bash gallery/pdf_writer/test/run_fonts.sh --page-parity # measure a whole rendered page
1081
+ bash gallery/pdf_writer/test/run_layout.sh --verify-snapshot # same, for the box model
1082
+ bash gallery/pdf_writer/test/run_layout.sh --update-snapshot
1083
+ ```
1084
+
1085
+ The box-model gate compares padding, per-side padding, margins, per-side
1086
+ margins, row and column gaps, nesting, background colour, and every supported
1087
+ length unit across 121 boxes. Both sides build their tree from the same
1088
+ `fixtures/box_model.fixtures`, and the browser side is configured to EVG's model
1089
+ rather than CSS defaults — `box-sizing: border-box` because EVG's width includes
1090
+ padding, and `display: flex` because EVG's flow is flex (block flow would
1091
+ collapse adjacent vertical margins and the two would disagree for a reason that
1092
+ has nothing to do with EVG).
1093
+
1094
+ Re-record only when the font files change. A snapshot diff with unchanged fonts
1095
+ means the browser disagreed with itself, which is worth reading before
1096
+ committing.
1097
+
1098
+ ## 12. Test plan (fonts first)
1099
+
1100
+ 1. **Advance width fixtures** — fixed strings per font; assert widths stable across PDF measure API and raster measure API
1101
+ 2. **Wrap fixtures** — paragraph + maxWidth → identical line arrays in layout and PDF
1102
+ 3. **Flex intrinsic text** — `row` + Label + Image does not force-wrap incorrectly
1103
+ 4. **Theme swap** — same TSX tree, two stylesheets → different fonts/spacing, still valid layout
1104
+ 5. **Grid album page** — 2×2 + span row; no overlap; gaps respected
1105
+ 6. **Cross-target visual** — HTML vs raster/PDF page render diff under tolerance for text blocks
1106
+
1107
+ ## 13. Open decisions
1108
+
1109
+ 1. ~~Stylesheet syntax: real CSS subset parser vs JSON style maps~~ — **decided:
1110
+ CSS subset parser** (`EVGStyleSheet.rgr`), see Phase 2
1111
+ 2. HTML preview strategy: precomputed frames (parity) vs native CSS (speed) + CI diffs
1112
+ 3. Default `line-height` policy when property omitted (font `lineGap` vs `1.2`)
1113
+ 4. ~~Whether Grid v1 lands before or after theme CSS~~ — **done in that order**: flex → themes → grid
1114
+
1115
+ ## 14. Summary
1116
+
1117
+ EVG should grow toward HTML/CSS **as a print-safe subset**, not as a browser. The order that protects quality is:
1118
+
1119
+ 1. **Shared TTF metrics + wrap** (layout and paint cannot disagree)
1120
+ 2. **Flexbox v2 + intrinsic text**
1121
+ 3. **Class/theme styles** for fast visual iteration
1122
+ 4. **Grid v1** for album pages
1123
+
1124
+ Fonts are not a side feature here: they are the constraint that makes flex/grid trustworthy on paper.