ranger-compiler 3.5.1 → 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 +1064 -20
  2. package/LICENSE +3 -1
  3. package/README.md +123 -28
  4. package/dist/Lang.rgr +1188 -290
  5. package/dist/api.d.ts +2343 -879
  6. package/dist/api.js +79923 -56921
  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,2148 @@
1
+ ; SPDX-License-Identifier: MIT
2
+
3
+ ; EVGStyleSheet.rgr - A small, print-safe CSS subset for EVG
4
+ ;
5
+ ; This is deliberately NOT a browser cascade. It supports exactly what a
6
+ ; photo-book / print document needs to switch its look without editing the
7
+ ; component tree:
8
+ ;
9
+ ; .caption { ... } ; class rule, applies in every theme
10
+ ; .theme-classic .caption { ... } ; class rule, only under theme "classic"
11
+ ;
12
+ ; MORE THAN ONE THEME CAN BE ON. The theme handed to `applyTree` is a
13
+ ; space-separated list — "session ocean" — and a rule scoped to any name in
14
+ ; it applies. One slot is not enough for an app that scopes some rules to a
15
+ ; place and its colours to a palette; with two the palette stops fighting the
16
+ ; screen for the slot. A single name is a list of one.
17
+ ; .a, .b { ... } ; selector lists
18
+ ; /* comments */
19
+ ;
20
+ ; @media (max-width: 640px) { .caption { ... } }
21
+ ; @media (min-width: 900px) and (orientation: landscape) { ... }
22
+ ; @media (pointer: coarse) { ... } ; a finger, not a mouse
23
+ ;
24
+ ; @vars { --ink: #09090b; } ; the palette, in every theme
25
+ ; @vars classic { --ink: #1b1a17; } ; ...and what "classic" makes of it
26
+ ; .caption { color: var(--ink); } ; used anywhere in any value
27
+ ; .rule { border: 1px solid var(--line, #ddd); } ; with a fallback
28
+ ;
29
+ ; CUSTOM PROPERTIES ARE SHEET-LEVEL HERE, and `@vars` says so out loud. In a
30
+ ; browser `--x` is an inherited property of an ELEMENT and `:root { --x }` is
31
+ ; only the commonest place to put it; this engine has no root element and no
32
+ ; inheritance, so a palette written on a class would look element-scoped and
33
+ ; not be. Declaring `--x` in a class rule is therefore an error rather than a
34
+ ; thing that half works.
35
+ ;
36
+ ; `var()` is resolved when a plan is BUILT — once per (class, theme, state) —
37
+ ; and never per element, so a sheet that is all variables costs what one that
38
+ ; is all literals costs. It is textual substitution before the value reaches
39
+ ; `setAttribute`, which is what lets it work inside a shorthand
40
+ ; (`border: 1px solid var(--line)`) with no property having to know.
41
+ ;
42
+ ; A media block is a CONDITION on the rules inside it, not a new kind of
43
+ ; selector: the rules keep the same specificity they would have had outside,
44
+ ; and a block that does not match contributes nothing. Conditions are
45
+ ; evaluated against a viewport the caller states — width, height and whether
46
+ ; the pointer is coarse — because a Ranger program has no window to ask.
47
+ ;
48
+ ; With no viewport stated, a conditional rule does NOT apply: a media query
49
+ ; that cannot be evaluated has no truth value, and guessing "yes" would style
50
+ ; a print page for a phone.
51
+ ;
52
+ ; No IDs, no element selectors, no `!important`, no specificity arithmetic
53
+ ; beyond "theme-scoped beats unscoped". Resolution order is:
54
+ ;
55
+ ; unscoped class rules < theme-scoped class rules < inline attributes
56
+ ;
57
+ ; ...with source order breaking ties inside each group. Inline always wins:
58
+ ; the applier skips any property the authoring layer already set, which
59
+ ; EVGElement records in `inlineProps`.
60
+ ;
61
+ ; Properties are not interpreted here. Every declaration is handed to
62
+ ; `EVGElement.setAttribute`, which already accepts both `font-size` and
63
+ ; `fontSize` spellings for the whole EVG property surface, so the stylesheet
64
+ ; and inline attributes cannot drift apart.
65
+
66
+ Import "EVGElement.rgr"
67
+
68
+ class EVGStyleDecl {
69
+ def name:string ""
70
+ def value:string ""
71
+
72
+ Constructor () {
73
+ name = ""
74
+ value = ""
75
+ }
76
+ }
77
+
78
+ ; What a `@media` block asks about the surface being drawn on.
79
+ ;
80
+ ; Each bound is in CSS pixels, and -1 means "not asked about". `coarse` is a
81
+ ; tri-state for the same reason: a sheet that says nothing about the pointer
82
+ ; must match whether there is a finger on the screen or not.
83
+ class EVGMediaQuery {
84
+ def minWidth:double (0.0 - 1.0)
85
+ def maxWidth:double (0.0 - 1.0)
86
+ def minHeight:double (0.0 - 1.0)
87
+ def maxHeight:double (0.0 - 1.0)
88
+ ; "", "portrait" or "landscape".
89
+ def orientation:string ""
90
+ ; 0 = not asked, 1 = must be coarse, 2 = must be fine.
91
+ def pointer:int 0
92
+ ; A condition nobody could parse. Kept rather than dropped so the rules
93
+ ; inside never apply — a misspelt query that styles everything is worse
94
+ ; than one that styles nothing, because it is invisible until it is not.
95
+ def broken:boolean false
96
+
97
+ Constructor () {
98
+ }
99
+
100
+ fn isEmpty:boolean () {
101
+ if broken {
102
+ return false
103
+ }
104
+ if (minWidth >= 0.0) { return false }
105
+ if (maxWidth >= 0.0) { return false }
106
+ if (minHeight >= 0.0) { return false }
107
+ if (maxHeight >= 0.0) { return false }
108
+ if ((strlen orientation) > 0) { return false }
109
+ if (pointer != 0) { return false }
110
+ return true
111
+ }
112
+
113
+ ; `w` and `h` in CSS pixels; `coarse` says the pointer is a finger. A
114
+ ; viewport of zero width means the caller never said, so nothing
115
+ ; conditional matches.
116
+ fn matches:boolean (w:double h:double coarse:boolean) {
117
+ if broken {
118
+ return false
119
+ }
120
+ if (this.isEmpty()) {
121
+ return true
122
+ }
123
+ if (w <= 0.0) {
124
+ return false
125
+ }
126
+ if (minWidth >= 0.0) {
127
+ if (w < minWidth) { return false }
128
+ }
129
+ if (maxWidth >= 0.0) {
130
+ if (w > maxWidth) { return false }
131
+ }
132
+ if (minHeight >= 0.0) {
133
+ if (h < minHeight) { return false }
134
+ }
135
+ if (maxHeight >= 0.0) {
136
+ if (h > maxHeight) { return false }
137
+ }
138
+ if ((strlen orientation) > 0) {
139
+ def want:string "landscape"
140
+ if (h > w) {
141
+ want = "portrait"
142
+ }
143
+ if (orientation != want) { return false }
144
+ }
145
+ if (pointer == 1) {
146
+ if (coarse == false) { return false }
147
+ }
148
+ if (pointer == 2) {
149
+ if coarse { return false }
150
+ }
151
+ return true
152
+ }
153
+ }
154
+
155
+ ; The interaction states a rule can ask for. Not an enum of every CSS
156
+ ; pseudo-class: these four are the ones an element can answer from what it
157
+ ; knows, and anything else is reported rather than silently never matching.
158
+ class EVGPseudo {
159
+ sfn none:int () { return 0 }
160
+ sfn hover:int () { return 1 }
161
+ sfn focus:int () { return 2 }
162
+ sfn active:int () { return 3 }
163
+ sfn disabled:int () { return 4 }
164
+
165
+ sfn parse:int (name:string) {
166
+ if (name == "hover") { return 1 }
167
+ if (name == "focus") { return 2 }
168
+ if (name == "active") { return 3 }
169
+ if (name == "disabled") { return 4 }
170
+ return -1
171
+ }
172
+
173
+ sfn holds:boolean (code:int el:EVGElement) {
174
+ if (code == 0) { return true }
175
+ if (code == 1) { return el.isHovered }
176
+ if (code == 2) { return el.isFocused }
177
+ if (code == 3) { return el.isPressed }
178
+ if (code == 4) { return el.a11yDisabled }
179
+ return false
180
+ }
181
+ }
182
+
183
+ class EVGStyleRule {
184
+ ; Theme this rule is scoped to, or "" when it applies in every theme.
185
+ def theme:string ""
186
+ ; Class this rule selects. Always set — the subset has no other selectors.
187
+ def className:string ""
188
+ ; EVGPseudo: 0 for a plain class rule, otherwise the state it asks for.
189
+ def pseudo:int 0
190
+ def decls:[EVGStyleDecl]
191
+ ; Source order, used to break ties between rules of equal specificity.
192
+ def order:int 0
193
+ ; The `@media` block this rule was written inside, if any. It gates the
194
+ ; rule; it does not change how strong it is.
195
+ def media:EVGMediaQuery (new EVGMediaQuery())
196
+
197
+ Constructor () {
198
+ theme = ""
199
+ className = ""
200
+ order = 0
201
+ }
202
+
203
+ fn isThemeScoped:boolean () {
204
+ return ((strlen theme) > 0)
205
+ }
206
+ }
207
+
208
+ class EVGStyleSheet {
209
+ def rules:[EVGStyleRule]
210
+ ; --- the palette ----------------------------------------------------------
211
+ ;
212
+ ; Every `@vars` declaration in source order, as four parallel arrays: the
213
+ ; theme it was written under ("" for every theme), the media condition of
214
+ ; the block it sat in, the name and the value. A list and not a map,
215
+ ; because which declaration wins depends on the theme and the viewport
216
+ ; being asked about — the same question `rules` answers the same way.
217
+ def varThemes:[string]
218
+ def varMedia:[EVGMediaQuery]
219
+ def varNames:[string]
220
+ def varValues:[string]
221
+ ; Timing functions already checked, so a `transition` built out of
222
+ ; variables is reported once rather than once per plan that resolves it.
223
+ def varChecked:[string:int]
224
+ ; And the same for what resolution complains about. A parse error happens
225
+ ; once because parsing happens once; a resolution error happens wherever a
226
+ ; plan is built, so one typo in one rule would be reported once per class
227
+ ; that rule matches — forty copies of one sentence in the panel that is
228
+ ; supposed to tell you which sentence it is.
229
+ def varReported:[string:int]
230
+ ; Rules that failed to parse, kept so tools can report them instead of
231
+ ; silently rendering an unstyled page.
232
+ def errors:[string]
233
+ def ruleCounter:int 0
234
+ ; The `@media` condition the rules being parsed right now sit under.
235
+ def pendingMedia:EVGMediaQuery (new EVGMediaQuery())
236
+ ; The surface the sheet is being applied to. Zero width means the caller
237
+ ; never said, and then nothing conditional matches — see the header.
238
+ def viewportW:double 0.0
239
+ def viewportH:double 0.0
240
+ def coarsePointer:boolean false
241
+
242
+ ; --- the resolved-style cache ---------------------------------------------
243
+ ;
244
+ ; `applyTo` used to do five full scans of the rule list PER ELEMENT, PER
245
+ ; FRAME: one to clear the state properties and four to apply the groups.
246
+ ; Measured on a 1600-row table (`npm run ui:bench`), that was 178ms of a
247
+ ; 252ms frame — 71% of it, and more than layout and display-list
248
+ ; construction put together.
249
+ ;
250
+ ; The waste is that the answer barely varies. What a rule scan produces is
251
+ ; an ordered list of (property, value) writes, and that list depends on
252
+ ; exactly four things: the element's class string, the theme, the four
253
+ ; interaction bits `EVGPseudo.holds` reads, and the viewport. That same
254
+ ; 1600-row table has EIGHTEEN distinct class strings in 22,403 elements.
255
+ ;
256
+ ; So the scan happens once per distinct key and the result is replayed. The
257
+ ; plans are stored as flat parallel arrays rather than a list of lists: one
258
+ ; allocation that grows, and no nested array type to thread through the
259
+ ; backends.
260
+ def planNames:[string]
261
+ def planValues:[string]
262
+ def planStart:[int]
263
+ def planCount:[int]
264
+ def planIndex:[string:int]
265
+ ; How often the cache answered, and how often it had to scan. Reported
266
+ ; rather than inferred: a cache that never hits looks exactly like one that
267
+ ; works, only slower.
268
+ def planHits:int 0
269
+ def planMisses:int 0
270
+ ; Bumped whenever every plan becomes a guess. An element records it
271
+ ; alongside what it was styled with, so one increment invalidates the whole
272
+ ; tree without walking it.
273
+ def generation:int 1
274
+ ; Per plan: the LAYOUT-relevant declarations, as one string. Two plans with
275
+ ; the same signature cannot move anything, however much else differs
276
+ ; between them — which is what lets a hover skip layout entirely.
277
+ def planLayoutSig:[string]
278
+ ; Which RULE asked for each planned declaration — an index into `rules`,
279
+ ; or -1 for a state clear's initial value. Parallel to `planNames` and
280
+ ; `planValues`, and the only reason the inspector can say what set a value.
281
+ ;
282
+ ; Recorded HERE, in the plan, and not at apply time. By the time a value
283
+ ; reaches an element the rule is long gone: `buildPlan` flattens every
284
+ ; matching rule into two arrays and `applyGroup` replays them. Reconstructing
285
+ ; the answer afterwards by re-matching selectors would be a second
286
+ ; implementation of the cascade, and a second implementation is a thing
287
+ ; that can disagree with the first.
288
+ ;
289
+ ; It costs one int per planned declaration, PER PLAN. A 1600-row table has
290
+ ; a handful of plans and a hundred thousand applications of them, so the
291
+ ; same record made at apply time would be paid a hundred thousand times.
292
+ def planRules:[int]
293
+
294
+ ; --- what the last `applyTree` actually did -------------------------------
295
+ ; Reset at the start of every pass. `layoutDirty` is the one that matters:
296
+ ; while it is zero, nothing that was written can have moved a box, and the
297
+ ; layout pass is doing arithmetic it already did.
298
+ def passSkipped:int 0
299
+ def passStyled:int 0
300
+ def passLayoutDirty:int 0
301
+ def passPaintDirty:int 0
302
+
303
+ Constructor () {
304
+ ruleCounter = 0
305
+ this.dropPlans()
306
+ }
307
+
308
+ ; Everything cached is a function of the rules and the viewport, so both of
309
+ ; those changing throws the lot away. There is no finer invalidation here on
310
+ ; purpose: a sheet is parsed once and a viewport changes when the window
311
+ ; does, and being clever about either would be optimising the rare case.
312
+ fn dropPlans:void () {
313
+ generation = (generation + 1)
314
+ def sig:[string]
315
+ planLayoutSig = sig
316
+ def a:[string]
317
+ planNames = a
318
+ def b:[string]
319
+ planValues = b
320
+ def c:[int]
321
+ planStart = c
322
+ def d:[int]
323
+ planCount = d
324
+ def e:[string:int]
325
+ planIndex = e
326
+ def r:[int]
327
+ planRules = r
328
+ }
329
+
330
+ ; State the surface once; `applyTree` then honours the media blocks.
331
+ ; Width and height are CSS pixels, and `coarse` says the pointer is a
332
+ ; finger rather than a mouse.
333
+ fn setViewport:void (w:double h:double coarse:boolean) {
334
+ if (((w != viewportW) || (h != viewportH)) || (coarse != coarsePointer)) {
335
+ ; A media query can turn a rule on or off, so every plan built under
336
+ ; the old surface is now a guess.
337
+ this.dropPlans()
338
+ }
339
+ viewportW = w
340
+ viewportH = h
341
+ coarsePointer = coarse
342
+ }
343
+
344
+ fn getRuleCount:int () {
345
+ return (array_length rules)
346
+ }
347
+
348
+ fn getErrorCount:int () {
349
+ return (array_length errors)
350
+ }
351
+
352
+ fn getError:string (i:int) {
353
+ return (itemAt errors i)
354
+ }
355
+
356
+ ; -----------------------------------------------------------------------
357
+ ; Parsing
358
+ ; -----------------------------------------------------------------------
359
+
360
+ fn parse:void (css:string) {
361
+ this.dropPlans()
362
+ def src:string (this.stripComments(css))
363
+ this.parseBlock(src (new EVGMediaQuery()))
364
+ }
365
+
366
+ ; Replace the sheet's contents with a new stylesheet, in place.
367
+ ;
368
+ ; TWO REASONS THIS IS NOT `new EVGStyleSheet()` AT THE CALL SITE, and both
369
+ ; of them are silent when got wrong.
370
+ ;
371
+ ; `parse` APPENDS. It drops the plans and adds rules; it does not remove
372
+ ; the ones already there. Parsing twice into one sheet leaves every rule
373
+ ; in it twice, which mostly looks like it worked because the duplicates
374
+ ; agree with each other — until one of them is edited.
375
+ ;
376
+ ; And `generation` starts at 1 on a fresh sheet, so a fresh one is at 2
377
+ ; after its first parse — exactly where the sheet it replaced was.
378
+ ; `applyTo` skips an element whose `styleGen` already equals the
379
+ ; generation, so every RETAINED element would be skipped and the new CSS
380
+ ; would land on nothing at all. Worse, it fails asymmetrically: a page that
381
+ ; rebuilds its tree hands the cascade fresh elements with `styleGen` 0 and
382
+ ; works, so this shows up only in the apps that keep their trees.
383
+ ;
384
+ ; Reloading in place fixes both: the rules go, and the counter only ever
385
+ ; goes up, so every element in the tree is stale by construction.
386
+ fn reload:void (css:string) {
387
+ def r:[EVGStyleRule]
388
+ rules = r
389
+ def e:[string]
390
+ errors = e
391
+ ; AND THE PALETTE, for the same reason the rules go: `parse` appends,
392
+ ; so a sheet reloaded without this keeps every variable the old text
393
+ ; declared — and an old declaration outlives the new one, because the
394
+ ; later of two equals wins.
395
+ def vt:[string]
396
+ varThemes = vt
397
+ def vm:[EVGMediaQuery]
398
+ varMedia = vm
399
+ def vn:[string]
400
+ varNames = vn
401
+ def vv:[string]
402
+ varValues = vv
403
+ def vc:[string:int]
404
+ varChecked = vc
405
+ def vr:[string:int]
406
+ varReported = vr
407
+ ruleCounter = 0
408
+ this.parse(css)
409
+ }
410
+
411
+ ; --- what set this value -------------------------------------------------
412
+ ;
413
+ ; The plan for `slot` as (property, value, rule) triples, in cascade order.
414
+ ; The order is not sorted here and nothing is scored: `buildPlan` calls
415
+ ; `planGroup` in the four passes that ARE the precedence — plain, themed,
416
+ ; stateful, themed-and-stateful — and the last write wins. So "which
417
+ ; declaration won" is read off the end of the list rather than recomputed,
418
+ ; which is what stops a panel from disagreeing with the engine about it.
419
+
420
+ fn planLength:int (slot:int) {
421
+ if (slot < 0) {
422
+ return 0
423
+ }
424
+ if (slot >= (array_length planCount)) {
425
+ return 0
426
+ }
427
+ return (itemAt planCount slot)
428
+ }
429
+
430
+ fn planNameAt:string (slot:int i:int) {
431
+ def from:int (itemAt planStart slot)
432
+ return (itemAt planNames (from + i))
433
+ }
434
+
435
+ fn planValueAt:string (slot:int i:int) {
436
+ def from:int (itemAt planStart slot)
437
+ return (itemAt planValues (from + i))
438
+ }
439
+
440
+ ; -1 where no rule asked for it — a state clear's initial value.
441
+ fn planRuleAt:int (slot:int i:int) {
442
+ def from:int (itemAt planStart slot)
443
+ if ((from + i) >= (array_length planRules)) {
444
+ return (0 - 1)
445
+ }
446
+ return (itemAt planRules (from + i))
447
+ }
448
+
449
+ ; The selector as it was written, reassembled rather than stored. A rule
450
+ ; knows its class, its state, its theme and its media block, and the
451
+ ; subset of CSS this cascade implements has nothing else in a selector —
452
+ ; so this is the whole of it and not a summary of it.
453
+ fn selectorOf:string (ruleIdx:int) {
454
+ if (ruleIdx < 0) {
455
+ return "(initial)"
456
+ }
457
+ if (ruleIdx >= (array_length rules)) {
458
+ return "(gone)"
459
+ }
460
+ def rule:EVGStyleRule (itemAt rules ruleIdx)
461
+ def out:string ("." + rule.className)
462
+ if (rule.pseudo == 1) { out = (out + ":hover") }
463
+ if (rule.pseudo == 2) { out = (out + ":focus") }
464
+ if (rule.pseudo == 3) { out = (out + ":active") }
465
+ if (rule.pseudo == 4) { out = (out + ":disabled") }
466
+ if (rule.isThemeScoped()) {
467
+ out = (((out + " [theme ") + rule.theme) + "]")
468
+ }
469
+ return out
470
+ }
471
+
472
+ fn ruleClassOf:string (ruleIdx:int) {
473
+ if (ruleIdx < 0) {
474
+ return ""
475
+ }
476
+ if (ruleIdx >= (array_length rules)) {
477
+ return ""
478
+ }
479
+ def rule:EVGStyleRule (itemAt rules ruleIdx)
480
+ return rule.className
481
+ }
482
+
483
+ ; The `@media` condition the rule was written inside, printed, or "" when
484
+ ; it was not inside one. It gates the rule; it does not change how strong
485
+ ; the rule is, and the panel says so by keeping it in its own column.
486
+ fn ruleMediaOf:string (ruleIdx:int) {
487
+ if (ruleIdx < 0) {
488
+ return ""
489
+ }
490
+ if (ruleIdx >= (array_length rules)) {
491
+ return ""
492
+ }
493
+ def rule:EVGStyleRule (itemAt rules ruleIdx)
494
+ def q:EVGMediaQuery rule.media
495
+ if (q.isEmpty()) {
496
+ return ""
497
+ }
498
+ if q.broken {
499
+ return "(unparseable — the rules inside it never apply)"
500
+ }
501
+ def out:string ""
502
+ if (q.minWidth >= 0.0) {
503
+ def v:int (to_int q.minWidth)
504
+ out = (((out + "min-width: ") + (to_string v)) + "px ")
505
+ }
506
+ if (q.maxWidth >= 0.0) {
507
+ def v2:int (to_int q.maxWidth)
508
+ out = (((out + "max-width: ") + (to_string v2)) + "px ")
509
+ }
510
+ if (q.minHeight >= 0.0) {
511
+ def v3:int (to_int q.minHeight)
512
+ out = (((out + "min-height: ") + (to_string v3)) + "px ")
513
+ }
514
+ if (q.maxHeight >= 0.0) {
515
+ def v4:int (to_int q.maxHeight)
516
+ out = (((out + "max-height: ") + (to_string v4)) + "px ")
517
+ }
518
+ if ((strlen q.orientation) > 0) {
519
+ out = ((out + "orientation: ") + q.orientation + " ")
520
+ }
521
+ if (q.pointer == 1) { out = (out + "pointer: coarse ") }
522
+ if (q.pointer == 2) { out = (out + "pointer: fine ") }
523
+ return out
524
+ }
525
+
526
+ ; One level of rules, all under the same media condition. A `@media` block
527
+ ; recurses with its own condition; the nesting is one deep in CSS and one
528
+ ; deep here, because a query inside a query means the same as `and`.
529
+ fn parseBlock:void (src:string cond:EVGMediaQuery) {
530
+ def len:int (strlen src)
531
+ def i:int 0
532
+ while (i < len) {
533
+ ; Selector text runs up to the next '{'
534
+ def braceAt:int (this.findChar(src i 123))
535
+ if (braceAt < 0) {
536
+ ; Trailing text with no block — ignore unless it has content
537
+ def tail:string (trim (substring src i len))
538
+ if ((strlen tail) > 0) {
539
+ push errors ("Ignored trailing text with no rule body: " + tail)
540
+ }
541
+ return
542
+ }
543
+ def selectorText:string (trim (substring src i braceAt))
544
+ ; `@media (...) { ... }` — the body is itself a list of rules, and
545
+ ; it holds nested braces, so the end of it is the brace that
546
+ ; balances the opening one rather than the first `}` after it.
547
+ if (this.startsWith(selectorText "@media")) {
548
+ def endAt:int (this.matchingBrace(src braceAt))
549
+ if (endAt < 0) {
550
+ push errors ("Unclosed @media block: " + selectorText)
551
+ return
552
+ }
553
+ def inner:string (substring src (braceAt + 1) endAt)
554
+ def q:EVGMediaQuery (this.parseMedia((trim (substring selectorText 6 (strlen selectorText)))))
555
+ ; Nested queries combine: both have to hold.
556
+ this.parseBlock(inner (this.andQuery(cond q)))
557
+ i = (endAt + 1)
558
+ } {
559
+ if ((strlen selectorText) > 0) {
560
+ if ((charAt selectorText 0) == 64) {
561
+ def skipTo:int (this.matchingBrace(src braceAt))
562
+ if (skipTo < 0) {
563
+ push errors ("Unclosed at-rule: " + selectorText)
564
+ return
565
+ }
566
+ ; `@vars` and `@vars <theme>` — the palette. It carries
567
+ ; the media condition of the block it sits in, like a
568
+ ; rule does, so a variable can answer a narrow window.
569
+ if (this.startsWith(selectorText "@vars")) {
570
+ this.addVars(selectorText (substring src (braceAt + 1) skipTo) cond)
571
+ i = (skipTo + 1)
572
+ continue
573
+ }
574
+ ; Some other at-rule. Skipped as a whole, and said so,
575
+ ; rather than its body being read as bare selectors.
576
+ push errors ("Unsupported at-rule ignored: " + selectorText)
577
+ i = (skipTo + 1)
578
+ continue
579
+ }
580
+ }
581
+ def closeAt:int (this.findChar(src (braceAt + 1) 125))
582
+ if (closeAt < 0) {
583
+ push errors ("Unclosed rule body for selector: " + selectorText)
584
+ return
585
+ }
586
+ def body:string (substring src (braceAt + 1) closeAt)
587
+ this.addRulesIn(selectorText body cond)
588
+ i = (closeAt + 1)
589
+ }
590
+ }
591
+ }
592
+
593
+ ; The index of the `}` that closes the `{` at `open`, or -1.
594
+ fn matchingBrace:int (s:string open:int) {
595
+ def len:int (strlen s)
596
+ def depth:int 0
597
+ def i:int open
598
+ while (i < len) {
599
+ def c:int (charAt s i)
600
+ if (c == 123) {
601
+ depth = (depth + 1)
602
+ }
603
+ if (c == 125) {
604
+ depth = (depth - 1)
605
+ if (depth == 0) {
606
+ return i
607
+ }
608
+ }
609
+ i = (i + 1)
610
+ }
611
+ return (0 - 1)
612
+ }
613
+
614
+ ; "(max-width: 640px) and (orientation: portrait)".
615
+ ;
616
+ ; `and` is the only combinator: a comma-separated list is an OR in CSS and
617
+ ; would need a rule per branch, which the subset does not do — so it is
618
+ ; reported rather than half-implemented.
619
+ fn parseMedia:EVGMediaQuery (text:string) {
620
+ def q (new EVGMediaQuery())
621
+ def body:string (trim text)
622
+ if ((strlen body) == 0) {
623
+ push errors "Empty @media condition"
624
+ q.broken = true
625
+ return q
626
+ }
627
+ ; A comma is an OR, which needs one rule per branch. Refused rather
628
+ ; than silently read as an AND, which would be the opposite meaning.
629
+ if ((this.findChar(body 0 44)) >= 0) {
630
+ push errors ("Comma-separated media queries are not supported: " + body)
631
+ q.broken = true
632
+ return q
633
+ }
634
+ def parts:[string] (this.splitFeatures(body))
635
+ def i:int 0
636
+ while (i < (array_length parts)) {
637
+ def feat:string (trim (itemAt parts i))
638
+ if ((strlen feat) > 0) {
639
+ this.applyFeature(q feat body)
640
+ }
641
+ i = (i + 1)
642
+ }
643
+ return q
644
+ }
645
+
646
+ ; "(a: 1) and (b: 2)" -> ["a: 1", "b: 2"]. Splitting on the parentheses
647
+ ; rather than on the word `and` keeps a value containing "and" intact and
648
+ ; makes a missing bracket visible as an empty feature.
649
+ fn splitFeatures:[string] (body:string) {
650
+ def out:[string]
651
+ def len:int (strlen body)
652
+ def i:int 0
653
+ while (i < len) {
654
+ def open:int (this.findChar(body i 40))
655
+ if (open < 0) {
656
+ def tail:string (trim (substring body i len))
657
+ if ((strlen tail) > 0) {
658
+ if (tail != "and") {
659
+ push out tail
660
+ }
661
+ }
662
+ return out
663
+ }
664
+ def close:int (this.findChar(body (open + 1) 41))
665
+ if (close < 0) {
666
+ push out (substring body (open + 1) len)
667
+ return out
668
+ }
669
+ push out (substring body (open + 1) close)
670
+ i = (close + 1)
671
+ }
672
+ return out
673
+ }
674
+
675
+ fn applyFeature:void (q:EVGMediaQuery feat:string whole:string) {
676
+ def colon:int (this.findChar(feat 0 58))
677
+ if (colon < 0) {
678
+ push errors ("Media feature without a value: " + feat)
679
+ q.broken = true
680
+ return
681
+ }
682
+ def name:string (trim (substring feat 0 colon))
683
+ def value:string (trim (substring feat (colon + 1) (strlen feat)))
684
+ if (name == "orientation") {
685
+ if ((value == "portrait") || (value == "landscape")) {
686
+ q.orientation = value
687
+ return
688
+ }
689
+ push errors ("Unknown orientation: " + value)
690
+ q.broken = true
691
+ return
692
+ }
693
+ if (name == "pointer") {
694
+ if (value == "coarse") {
695
+ q.pointer = 1
696
+ return
697
+ }
698
+ if (value == "fine") {
699
+ q.pointer = 2
700
+ return
701
+ }
702
+ push errors ("Unknown pointer value: " + value)
703
+ q.broken = true
704
+ return
705
+ }
706
+ def px@(optional):double (this.parsePx(value))
707
+ if (null? px) {
708
+ push errors ("Media feature value is not a length: " + feat)
709
+ q.broken = true
710
+ return
711
+ }
712
+ def v:double (unwrap px)
713
+ if (name == "min-width") {
714
+ q.minWidth = v
715
+ return
716
+ }
717
+ if (name == "max-width") {
718
+ q.maxWidth = v
719
+ return
720
+ }
721
+ if (name == "min-height") {
722
+ q.minHeight = v
723
+ return
724
+ }
725
+ if (name == "max-height") {
726
+ q.maxHeight = v
727
+ return
728
+ }
729
+ push errors ("Unsupported media feature: " + name)
730
+ q.broken = true
731
+ }
732
+
733
+ ; "640px" or "640". Anything with a unit this subset cannot resolve
734
+ ; without a font or a viewport — em, rem, vw — is refused, because the
735
+ ; number it would silently become is not the one the author meant.
736
+ fn parsePx@(optional):double (value:string) {
737
+ ; `return null` is not a thing in this language; an unset optional is.
738
+ def __none@(optional):double
739
+ def v:string (trim value)
740
+ def len:int (strlen v)
741
+ if (len == 0) {
742
+ return __none
743
+ }
744
+ def digits:string v
745
+ if (len > 2) {
746
+ if ((substring v (len - 2) len) == "px") {
747
+ digits = (trim (substring v 0 (len - 2)))
748
+ }
749
+ }
750
+ if ((strlen digits) == 0) {
751
+ return __none
752
+ }
753
+ def i:int 0
754
+ def dots:int 0
755
+ while (i < (strlen digits)) {
756
+ def c:int (charAt digits i)
757
+ if (c == 46) {
758
+ dots = (dots + 1)
759
+ } {
760
+ if ((c < 48) || (c > 57)) {
761
+ return __none
762
+ }
763
+ }
764
+ i = (i + 1)
765
+ }
766
+ if (dots > 1) {
767
+ return __none
768
+ }
769
+ return (to_double digits)
770
+ }
771
+
772
+ ; Nested `@media` blocks mean both conditions hold. The tighter bound
773
+ ; wins on each axis, which is what `and` does.
774
+ fn andQuery:EVGMediaQuery (a:EVGMediaQuery b:EVGMediaQuery) {
775
+ if (a.isEmpty()) {
776
+ return b
777
+ }
778
+ if (b.isEmpty()) {
779
+ return a
780
+ }
781
+ def q (new EVGMediaQuery())
782
+ q.broken = (a.broken || b.broken)
783
+ q.minWidth = (EVGStyleSheet.larger(a.minWidth b.minWidth))
784
+ q.maxWidth = (EVGStyleSheet.smaller(a.maxWidth b.maxWidth))
785
+ q.minHeight = (EVGStyleSheet.larger(a.minHeight b.minHeight))
786
+ q.maxHeight = (EVGStyleSheet.smaller(a.maxHeight b.maxHeight))
787
+ q.orientation = a.orientation
788
+ if ((strlen b.orientation) > 0) {
789
+ if ((strlen a.orientation) > 0) {
790
+ if (a.orientation != b.orientation) {
791
+ ; Cannot be both at once.
792
+ q.broken = true
793
+ }
794
+ }
795
+ q.orientation = b.orientation
796
+ }
797
+ q.pointer = a.pointer
798
+ if (b.pointer != 0) {
799
+ if (a.pointer != 0) {
800
+ if (a.pointer != b.pointer) {
801
+ q.broken = true
802
+ }
803
+ }
804
+ q.pointer = b.pointer
805
+ }
806
+ return q
807
+ }
808
+
809
+ sfn larger:double (a:double b:double) {
810
+ if (a < 0.0) { return b }
811
+ if (b < 0.0) { return a }
812
+ if (a > b) { return a }
813
+ return b
814
+ }
815
+
816
+ sfn smaller:double (a:double b:double) {
817
+ if (a < 0.0) { return b }
818
+ if (b < 0.0) { return a }
819
+ if (a < b) { return a }
820
+ return b
821
+ }
822
+
823
+ ; Replace /* ... */ spans with a single space so tokens either side stay apart.
824
+ ; Comments out, everything else through unchanged.
825
+ ;
826
+ ; This appends whole SLICES between comments rather than one character at a
827
+ ; time. The difference is not style: on a target where a string is immutable
828
+ ; — Kotlin, Swift, C#, Java — `out = out + oneChar` copies the whole
829
+ ; accumulator on every character, so stripping an n-byte stylesheet cost
830
+ ; O(n^2). JavaScript hid it, because V8 concatenates with a rope; the
831
+ ; Kotlin build of a 6 KB sheet spent 11 ms here against 0.3 ms in Node, and
832
+ ; that ratio is the whole of the bug. Slices make it linear everywhere.
833
+ fn stripComments:string (css:string) {
834
+ def out:string ""
835
+ def len:int (strlen css)
836
+ def i:int 0
837
+ def segStart:int 0
838
+ while (i < len) {
839
+ def c:int (charAt css i)
840
+ def isStart:boolean false
841
+ if (c == 47) {
842
+ if ((i + 1) < len) {
843
+ if ((charAt css (i + 1)) == 42) {
844
+ isStart = true
845
+ }
846
+ }
847
+ }
848
+ if isStart {
849
+ def j:int (i + 2)
850
+ def closed:boolean false
851
+ while ((j < len) && (closed == false)) {
852
+ if ((charAt css j) == 42) {
853
+ if ((j + 1) < len) {
854
+ if ((charAt css (j + 1)) == 47) {
855
+ closed = true
856
+ }
857
+ }
858
+ }
859
+ if (closed == false) {
860
+ j = (j + 1)
861
+ }
862
+ }
863
+ out = out + (substring css segStart i) + " "
864
+ ; An unterminated comment puts `i` past the end, which ends the
865
+ ; walk and drops the rest — the same thing the per-character
866
+ ; version did, and the reason `segStart` is clamped below.
867
+ i = (j + 2)
868
+ segStart = i
869
+ } {
870
+ i = (i + 1)
871
+ }
872
+ }
873
+ if (segStart < len) {
874
+ out = out + (substring css segStart len)
875
+ }
876
+ return out
877
+ }
878
+
879
+ fn findChar:int (s:string from:int ch:int) {
880
+ def len:int (strlen s)
881
+ def i:int from
882
+ while (i < len) {
883
+ if ((charAt s i) == ch) {
884
+ return i
885
+ }
886
+ i = (i + 1)
887
+ }
888
+ return 0 - 1
889
+ }
890
+
891
+ ; A selector list ("`.a, .theme-x .b`") plus one body becomes one rule per
892
+ ; selector, all sharing the same declarations.
893
+ fn addRules:void (selectorText:string body:string) {
894
+ this.addRulesIn(selectorText body (new EVGMediaQuery()))
895
+ }
896
+
897
+ fn addRulesIn:void (selectorText:string body:string cond:EVGMediaQuery) {
898
+ def decls:[EVGStyleDecl] (this.parseDeclarations(body))
899
+ def selectors:[string] (this.splitOn(selectorText 44))
900
+ def i:int 0
901
+ while (i < (array_length selectors)) {
902
+ def sel:string (trim (itemAt selectors i))
903
+ if ((strlen sel) > 0) {
904
+ pendingMedia = cond
905
+ this.addRuleForSelector(sel decls)
906
+ }
907
+ i = (i + 1)
908
+ }
909
+ pendingMedia = (new EVGMediaQuery())
910
+ }
911
+
912
+ ; `@vars { --a: 1 }` / `@vars classic { --a: 2 }`.
913
+ ;
914
+ ; The header is the at-rule word and, at most, a theme name. A second word
915
+ ; is an error rather than a guess: `@vars theme classic` reads as if it
916
+ ; ought to work, and quietly filing it under the theme "theme" would be the
917
+ ; kind of failure that looks like a wrong colour.
918
+ fn addVars:void (header:string body:string cond:EVGMediaQuery) {
919
+ def parts:[string] (this.splitWhitespace(header))
920
+ def theme:string ""
921
+ if ((array_length parts) > 2) {
922
+ push errors ("@vars takes at most a theme name: " + header)
923
+ return
924
+ }
925
+ if ((array_length parts) == 2) {
926
+ theme = (itemAt parts 1)
927
+ }
928
+ def decls:[EVGStyleDecl] (this.parseDeclarations(body))
929
+ def i:int 0
930
+ while (i < (array_length decls)) {
931
+ def d:EVGStyleDecl (itemAt decls i)
932
+ if ((this.startsWith(d.name "--")) == false) {
933
+ ; A property in here would go nowhere: `@vars` declares the
934
+ ; palette and nothing wears it.
935
+ push errors ("Only custom properties belong in @vars: " + d.name)
936
+ } {
937
+ push varThemes theme
938
+ push varMedia cond
939
+ push varNames d.name
940
+ push varValues d.value
941
+ }
942
+ i = (i + 1)
943
+ }
944
+ }
945
+
946
+ fn getVarCount:int () {
947
+ return (array_length varNames)
948
+ }
949
+
950
+ ; What `--name` is worth under `theme` and the stated viewport, or "" when
951
+ ; nothing defines it. A value is never empty — `parseDeclarations` drops an
952
+ ; empty one — so "" is an unambiguous "no".
953
+ ;
954
+ ; Precedence is the sheet's own, applied to the palette: a theme-scoped
955
+ ; declaration beats an unscoped one, and among equals the last one written
956
+ ; wins. A declaration inside a `@media` block that does not hold is not a
957
+ ; candidate at all.
958
+ ; THE ACTIVE THEME IS A LIST, not a name. `applyTree(root "session ocean")`
959
+ ; turns on both, and a rule written `.theme-ocean .card` applies because
960
+ ; `ocean` is one of them. An app needs this the moment it scopes some
961
+ ; rules to a place — a screen, a print size — and its colours to a
962
+ ; palette: with one slot the two fight over it and the palette loses.
963
+ ;
964
+ ; A single name is a list of one, so every caller that passed one reads
965
+ ; exactly as it did. Precedence does not change: theme-scoped still beats
966
+ ; unscoped, and among the scoped ones source order still decides — being
967
+ ; named earlier in the list buys a rule nothing.
968
+ sfn themeOn:boolean (ruleTheme:string active:string) {
969
+ if (ruleTheme == active) {
970
+ return true
971
+ }
972
+ if ((strlen ruleTheme) == 0) {
973
+ return false
974
+ }
975
+ def n:int (strlen active)
976
+ def start@(mutable):int 0
977
+ def i@(mutable):int 0
978
+ while (i <= n) {
979
+ def sep@(mutable):boolean (i == n)
980
+ if (sep == false) {
981
+ sep = ((charAt active i) == 32)
982
+ }
983
+ if sep {
984
+ if (i > start) {
985
+ if ((substring active start i) == ruleTheme) {
986
+ return true
987
+ }
988
+ }
989
+ start = (i + 1)
990
+ }
991
+ i = (i + 1)
992
+ }
993
+ return false
994
+ }
995
+
996
+ fn lookupVar:string (name:string theme:string) {
997
+ def best:string ""
998
+ def bestScoped:boolean false
999
+ def i:int 0
1000
+ def n:int (array_length varNames)
1001
+ while (i < n) {
1002
+ if ((itemAt varNames i) == name) {
1003
+ def th:string (itemAt varThemes i)
1004
+ def scoped:boolean ((strlen th) > 0)
1005
+ def applies:boolean true
1006
+ if scoped {
1007
+ applies = (EVGStyleSheet.themeOn(th theme))
1008
+ }
1009
+ if applies {
1010
+ def mq:EVGMediaQuery (itemAt varMedia i)
1011
+ applies = (mq.matches(viewportW viewportH coarsePointer))
1012
+ }
1013
+ if applies {
1014
+ ; An unscoped declaration cannot displace a themed one.
1015
+ if (scoped || (bestScoped == false)) {
1016
+ best = (itemAt varValues i)
1017
+ bestScoped = scoped
1018
+ }
1019
+ }
1020
+ }
1021
+ i = (i + 1)
1022
+ }
1023
+ return best
1024
+ }
1025
+
1026
+ ; `var(--name)` and `var(--name, fallback)`, resolved against the palette.
1027
+ ;
1028
+ ; Called from the two places a rule's value becomes an element's — the plan
1029
+ ; builder and the direct applier — so it runs once per (class, theme,
1030
+ ; state) and not once per element. `prop` is carried only to say which
1031
+ ; declaration was at fault.
1032
+ ;
1033
+ ; Returns "" when the value cannot be resolved, and the caller then writes
1034
+ ; NOTHING: half a shorthand is worse than the value that was there before,
1035
+ ; and the error list says what happened.
1036
+ fn resolveVars:string (value:string theme:string prop:string) {
1037
+ def at:int (this.findVar(value 0))
1038
+ if (at < 0) {
1039
+ return value
1040
+ }
1041
+ def out:string value
1042
+ def guard:int 0
1043
+ while (at >= 0) {
1044
+ guard = (guard + 1)
1045
+ if (guard > 32) {
1046
+ ; `--a: var(--b)` and `--b: var(--a)`. A depth limit rather
1047
+ ; than a visited-set: the limit is also what catches a chain
1048
+ ; nobody meant to write.
1049
+ this.reportOnce(("Custom property nested too deeply (a cycle?): " + prop))
1050
+ return ""
1051
+ }
1052
+ def close:int (this.matchParen(out (at + 3)))
1053
+ if (close < 0) {
1054
+ this.reportOnce(("Unclosed var(): " + value))
1055
+ return ""
1056
+ }
1057
+ def inner:string (trim (substring out (at + 4) close))
1058
+ def comma:int (this.findChar(inner 0 44))
1059
+ def name:string inner
1060
+ def fallback:string ""
1061
+ def hasFallback:boolean false
1062
+ if (comma >= 0) {
1063
+ name = (trim (substring inner 0 comma))
1064
+ fallback = (trim (substring inner (comma + 1) (strlen inner)))
1065
+ hasFallback = true
1066
+ }
1067
+ def got:string (this.lookupVar(name theme))
1068
+ def repl:string got
1069
+ if ((strlen got) == 0) {
1070
+ if (hasFallback == false) {
1071
+ this.reportOnce(((("Undefined custom property " + name) + " in: ") + prop))
1072
+ return ""
1073
+ }
1074
+ repl = fallback
1075
+ }
1076
+ out = (((substring out 0 at) + repl) + (substring out (close + 1) (strlen out)))
1077
+ at = (this.findVar(out 0))
1078
+ }
1079
+ ; A `transition` assembled out of variables cannot be checked when the
1080
+ ; sheet is parsed, because until a theme is named it has no value. It
1081
+ ; is checked here instead, once per distinct resolved value.
1082
+ if (prop == "transition") {
1083
+ def seen@(optional):int (get varChecked out)
1084
+ if (null? seen) {
1085
+ set varChecked out 1
1086
+ this.checkTransition(out)
1087
+ }
1088
+ }
1089
+ return out
1090
+ }
1091
+
1092
+ ; Say it once. See `varReported`.
1093
+ fn reportOnce:void (msg:string) {
1094
+ def seen@(optional):int (get varReported msg)
1095
+ if (null? seen) {
1096
+ set varReported msg 1
1097
+ push errors msg
1098
+ }
1099
+ }
1100
+
1101
+ ; The next "var(" at or after `from`, or -1. Four character comparisons in
1102
+ ; the common case, and the common case is a value with no variable in it —
1103
+ ; which is why this is a scan and not a split.
1104
+ fn findVar:int (s:string from:int) {
1105
+ def n:int (strlen s)
1106
+ def i:int from
1107
+ while ((i + 4) <= n) {
1108
+ if ((charAt s i) == 118) {
1109
+ if ((charAt s (i + 1)) == 97) {
1110
+ if ((charAt s (i + 2)) == 114) {
1111
+ if ((charAt s (i + 3)) == 40) {
1112
+ return i
1113
+ }
1114
+ }
1115
+ }
1116
+ }
1117
+ i = (i + 1)
1118
+ }
1119
+ return (0 - 1)
1120
+ }
1121
+
1122
+ ; The ')' that closes the '(' at `open`. Nested parentheses are counted,
1123
+ ; because a fallback may be a function: `var(--c, rgb(0, 0, 0))`.
1124
+ fn matchParen:int (s:string open:int) {
1125
+ def n:int (strlen s)
1126
+ def depth:int 0
1127
+ def i:int open
1128
+ while (i < n) {
1129
+ def c:int (charAt s i)
1130
+ if (c == 40) {
1131
+ depth = (depth + 1)
1132
+ }
1133
+ if (c == 41) {
1134
+ depth = (depth - 1)
1135
+ if (depth == 0) {
1136
+ return i
1137
+ }
1138
+ }
1139
+ i = (i + 1)
1140
+ }
1141
+ return (0 - 1)
1142
+ }
1143
+
1144
+ ; ".caption" -> class rule; ".theme-classic .caption" -> theme-scoped rule.
1145
+ ; Anything else is outside the subset and is reported rather than guessed at.
1146
+ fn addRuleForSelector:void (sel:string decls:[EVGStyleDecl]) {
1147
+ def parts:[string] (this.splitWhitespace(sel))
1148
+ def n:int (array_length parts)
1149
+
1150
+ if (n == 1) {
1151
+ def whole:string (itemAt parts 0)
1152
+ def bits:[string] (this.splitPseudo(whole))
1153
+ def only:string (itemAt bits 0)
1154
+ def pseudoName:string (itemAt bits 1)
1155
+ if ((this.isClassToken(only)) == false) {
1156
+ push errors ("Unsupported selector (only .class and .theme-x .class are supported): " + sel)
1157
+ return
1158
+ }
1159
+ def code:int 0
1160
+ if ((strlen pseudoName) > 0) {
1161
+ code = (EVGPseudo.parse(pseudoName))
1162
+ if (code < 0) {
1163
+ ; Reported rather than dropped: a rule that never fires and
1164
+ ; says nothing is a developer looking at the wrong file.
1165
+ push errors ("Unsupported pseudo-class (hover, focus, active, disabled): " + sel)
1166
+ return
1167
+ }
1168
+ }
1169
+ def rule (new EVGStyleRule())
1170
+ rule.className = (substring only 1 (strlen only))
1171
+ rule.pseudo = code
1172
+ this.pushRule(rule decls)
1173
+ return
1174
+ }
1175
+
1176
+ if (n == 2) {
1177
+ def scope:string (itemAt parts 0)
1178
+ def target:string (itemAt parts 1)
1179
+ def targetBits:[string] (this.splitPseudo(target))
1180
+ def targetOnly:string (itemAt targetBits 0)
1181
+ if (((this.isClassToken(scope)) == false) || ((this.isClassToken(targetOnly)) == false)) {
1182
+ push errors ("Unsupported selector (only .class and .theme-x .class are supported): " + sel)
1183
+ return
1184
+ }
1185
+ def scopeName:string (substring scope 1 (strlen scope))
1186
+ if ((this.startsWith(scopeName "theme-")) == false) {
1187
+ push errors ("Descendant selectors are only supported as `.theme-<name> .class`: " + sel)
1188
+ return
1189
+ }
1190
+ def bits2:[string] (this.splitPseudo(target))
1191
+ def targetClass:string (itemAt bits2 0)
1192
+ def pseudo2:string (itemAt bits2 1)
1193
+ def code2:int 0
1194
+ if ((strlen pseudo2) > 0) {
1195
+ code2 = (EVGPseudo.parse(pseudo2))
1196
+ if (code2 < 0) {
1197
+ push errors ("Unsupported pseudo-class (hover, focus, active, disabled): " + sel)
1198
+ return
1199
+ }
1200
+ }
1201
+ def rule2 (new EVGStyleRule())
1202
+ rule2.theme = (substring scopeName 6 (strlen scopeName))
1203
+ rule2.className = (substring targetClass 1 (strlen targetClass))
1204
+ rule2.pseudo = code2
1205
+ this.pushRule(rule2 decls)
1206
+ return
1207
+ }
1208
+
1209
+ push errors ("Unsupported selector (too many parts): " + sel)
1210
+ }
1211
+
1212
+ fn pushRule:void (rule:EVGStyleRule decls:[EVGStyleDecl]) {
1213
+ ; `--x` on a class. There is no element-scoped palette here, so this
1214
+ ; would be planned for every element wearing the class and dropped in
1215
+ ; silence by `setAttribute` — a declaration that reads like CSS and
1216
+ ; does nothing. It is reported and removed instead.
1217
+ def keep:[EVGStyleDecl]
1218
+ def k:int 0
1219
+ while (k < (array_length decls)) {
1220
+ def d:EVGStyleDecl (itemAt decls k)
1221
+ if (this.startsWith(d.name "--")) {
1222
+ push errors ((("Custom properties are declared in @vars, not on ." + rule.className) + ": ") + d.name)
1223
+ } {
1224
+ push keep d
1225
+ }
1226
+ k = (k + 1)
1227
+ }
1228
+ rule.decls = keep
1229
+ rule.media = pendingMedia
1230
+ rule.order = ruleCounter
1231
+ ruleCounter = (ruleCounter + 1)
1232
+ push rules rule
1233
+ }
1234
+
1235
+ ; ".btn:hover" -> ".btn" and "hover". The colon is the only place a
1236
+ ; pseudo-class can appear in the selectors this engine supports, so finding
1237
+ ; one is the whole parse.
1238
+ fn splitPseudo:[string] (tok:string) {
1239
+ def out:[string]
1240
+ def at:int (this.findChar(tok 0 58))
1241
+ if (at < 0) {
1242
+ push out tok
1243
+ push out ""
1244
+ return out
1245
+ }
1246
+ push out (substring tok 0 at)
1247
+ push out (substring tok (at + 1) (strlen tok))
1248
+ return out
1249
+ }
1250
+
1251
+ fn isClassToken:boolean (tok:string) {
1252
+ if ((strlen tok) < 2) {
1253
+ return false
1254
+ }
1255
+ return ((charAt tok 0) == 46)
1256
+ }
1257
+
1258
+ fn startsWith:boolean (s:string prefix:string) {
1259
+ def pl:int (strlen prefix)
1260
+ if ((strlen s) < pl) {
1261
+ return false
1262
+ }
1263
+ return ((substring s 0 pl) == prefix)
1264
+ }
1265
+
1266
+ ; "a: 1; b: 2" -> declarations. Values may contain ':' and ',' (urls,
1267
+ ; gradients, rgba) so only the FIRST ':' separates name from value.
1268
+ fn parseDeclarations:[EVGStyleDecl] (body:string) {
1269
+ def out:[EVGStyleDecl]
1270
+ def parts:[string] (this.splitOn(body 59))
1271
+ def i:int 0
1272
+ while (i < (array_length parts)) {
1273
+ def part:string (trim (itemAt parts i))
1274
+ if ((strlen part) > 0) {
1275
+ def colon:int (this.findChar(part 0 58))
1276
+ if (colon < 0) {
1277
+ push errors ("Declaration without ':' ignored: " + part)
1278
+ } {
1279
+ def d (new EVGStyleDecl())
1280
+ d.name = (trim (substring part 0 colon))
1281
+ d.value = (this.unquote((trim (substring part (colon + 1) (strlen part)))))
1282
+ if (((strlen d.name) > 0) && ((strlen d.value) > 0)) {
1283
+ ; A transition assembled out of variables has no
1284
+ ; value yet — until a theme is named, `var(--dur)`
1285
+ ; is neither a duration nor a timing function, and
1286
+ ; checking it here reports the variable itself as an
1287
+ ; unsupported function. `resolveVars` checks the
1288
+ ; resolved value instead, once per distinct result.
1289
+ if (d.name == "transition") {
1290
+ if ((this.findVar(d.value 0)) < 0) {
1291
+ this.checkTransition(d.value)
1292
+ }
1293
+ }
1294
+ push out d
1295
+ } {
1296
+ push errors ("Incomplete declaration ignored: " + part)
1297
+ }
1298
+ }
1299
+ }
1300
+ i = (i + 1)
1301
+ }
1302
+ return out
1303
+ }
1304
+
1305
+ ; A `transition` whose timing function does not parse is reported here, at
1306
+ ; the sheet, rather than discovered as motion that looks subtly wrong.
1307
+ ; `cubic-bezier(0.4, 0, 0.2, 1)` and `spring(1, 2)` are the same shape and
1308
+ ; only one of them is CSS.
1309
+ fn checkTransition:void (value:string) {
1310
+ def parts:[string] (EVGEasing.splitTop(value 44))
1311
+ def i:int 0
1312
+ while (i < (array_length parts)) {
1313
+ def words:[string] (EVGEasing.splitWordsTop((trim (itemAt parts i))))
1314
+ def w:int 0
1315
+ while (w < (array_length words)) {
1316
+ def word:string (itemAt words w)
1317
+ if (EVGEasing.looksLikeFunction(word)) {
1318
+ def e:EVGEasing (EVGEasing.parse(word))
1319
+ if ((e.ok) == false) {
1320
+ push errors ("Unsupported timing function (linear, ease, ease-in, ease-out, ease-in-out, step-start, step-end, cubic-bezier(), steps()): " + word)
1321
+ }
1322
+ }
1323
+ w = (w + 1)
1324
+ }
1325
+ i = (i + 1)
1326
+ }
1327
+ }
1328
+
1329
+ ; font-family: "Cinzel" — the quotes are CSS syntax, not part of the name.
1330
+ ;
1331
+ ; Only a quote wrapping the WHOLE value is a delimiter. grid-template-areas
1332
+ ; is a list of quoted rows, so its value legitimately starts and ends with a
1333
+ ; quote while carrying more inside; stripping there would silently delete
1334
+ ; the first and last row of the layout.
1335
+ fn unquote:string (s:string) {
1336
+ def len:int (strlen s)
1337
+ if (len < 2) {
1338
+ return s
1339
+ }
1340
+ def first:int (charAt s 0)
1341
+ def last:int (charAt s (len - 1))
1342
+ if (((first == 34) && (last == 34)) || ((first == 39) && (last == 39))) {
1343
+ def inner:int 1
1344
+ while (inner < (len - 1)) {
1345
+ if ((charAt s inner) == first) {
1346
+ return s
1347
+ }
1348
+ inner = (inner + 1)
1349
+ }
1350
+ return (substring s 1 (len - 1))
1351
+ }
1352
+ return s
1353
+ }
1354
+
1355
+ fn splitOn:[string] (s:string sep:int) {
1356
+ def out:[string]
1357
+ def len:int (strlen s)
1358
+ def start:int 0
1359
+ def i:int 0
1360
+ while (i < len) {
1361
+ if ((charAt s i) == sep) {
1362
+ push out (substring s start i)
1363
+ start = (i + 1)
1364
+ }
1365
+ i = (i + 1)
1366
+ }
1367
+ push out (substring s start len)
1368
+ return out
1369
+ }
1370
+
1371
+ fn splitWhitespace:[string] (s:string) {
1372
+ def out:[string]
1373
+ def len:int (strlen s)
1374
+ def start:int 0
1375
+ def inTok:boolean false
1376
+ def i:int 0
1377
+ while (i < len) {
1378
+ def c:int (charAt s i)
1379
+ def isSpace:boolean (((c == 32) || (c == 9)) || ((c == 10) || (c == 13)))
1380
+ if isSpace {
1381
+ if inTok {
1382
+ push out (substring s start i)
1383
+ inTok = false
1384
+ }
1385
+ } {
1386
+ if (inTok == false) {
1387
+ start = i
1388
+ inTok = true
1389
+ }
1390
+ }
1391
+ i = (i + 1)
1392
+ }
1393
+ if inTok {
1394
+ push out (substring s start len)
1395
+ }
1396
+ return out
1397
+ }
1398
+
1399
+ ; -----------------------------------------------------------------------
1400
+ ; Resolution
1401
+ ; -----------------------------------------------------------------------
1402
+
1403
+ ; Apply this stylesheet to `root` and everything below it.
1404
+ ; Apply with the surface stated in the same call, for a caller that has a
1405
+ ; window and no reason to keep the sheet around between frames.
1406
+ fn applyTreeIn:void (root:EVGElement theme:string w:double h:double coarse:boolean) {
1407
+ this.setViewport(w h coarse)
1408
+ this.applyTree(root theme)
1409
+ }
1410
+
1411
+ fn applyTree:void (root:EVGElement theme:string) {
1412
+ passSkipped = 0
1413
+ passStyled = 0
1414
+ passLayoutDirty = 0
1415
+ passPaintDirty = 0
1416
+ this.applyIn(root theme)
1417
+ }
1418
+
1419
+ ; True when nothing this pass wrote can have moved a box. The caller may
1420
+ ; then skip layout entirely — which on a hover is 40% of the frame spent
1421
+ ; recomputing positions that provably did not change.
1422
+ fn layoutClean:boolean () {
1423
+ return (passLayoutDirty == 0)
1424
+ }
1425
+
1426
+ ; True when the pass wrote nothing at all, so even the display list is
1427
+ ; still correct.
1428
+ fn nothingChanged:boolean () {
1429
+ return ((passLayoutDirty + passPaintDirty) == 0)
1430
+ }
1431
+
1432
+ ; …and, on the way back up, whether the pass moved a box anywhere in
1433
+ ; the subtree: `EVGElement.layoutClean`, which the layout reads for a
1434
+ ; subtree an app asked it to keep. An element never laid out is not
1435
+ ; clean, so a subtree with a new element in it is laid out.
1436
+ fn applyIn:void (root:EVGElement theme:string) {
1437
+ def before:int passLayoutDirty
1438
+ def beforePaint:int passPaintDirty
1439
+ this.applyTo(root theme)
1440
+ def ownClean:boolean (passLayoutDirty == before)
1441
+ def ownPaintClean:boolean ((passPaintDirty == beforePaint) && ownClean)
1442
+ def kidsClean@(mutable):boolean true
1443
+ def kidsPaintClean@(mutable):boolean true
1444
+ def i:int 0
1445
+ def n:int (root.getChildCount())
1446
+ while (i < n) {
1447
+ def kid:EVGElement (root.getChild(i))
1448
+ this.applyIn(kid theme)
1449
+ if (kid.layoutClean == false) {
1450
+ kidsClean = false
1451
+ }
1452
+ if (kid.paintClean == false) {
1453
+ kidsPaintClean = false
1454
+ }
1455
+ i = (i + 1)
1456
+ }
1457
+ root.layoutClean = ((ownClean && kidsClean) && root.hasLayout)
1458
+ root.paintClean = (ownPaintClean && kidsPaintClean)
1459
+ if (root.paintClean == false) {
1460
+ root.paintStamp = (root.paintStamp + 1)
1461
+ }
1462
+ }
1463
+
1464
+ ; The four interaction bits `EVGPseudo.holds` reads, as a string. Nothing
1465
+ ; else about an element can change which rules match it, which is the whole
1466
+ ; reason a plan can be shared.
1467
+ sfn stateKey:string (el:EVGElement) {
1468
+ def out:string "...."
1469
+ if el.isHovered { out = ("h" + (substring out 1 4)) }
1470
+ if el.isFocused { out = ((substring out 0 1) + "f" + (substring out 2 4)) }
1471
+ if el.isPressed { out = ((substring out 0 2) + "a" + (substring out 3 4)) }
1472
+ if el.a11yDisabled { out = ((substring out 0 3) + "d") }
1473
+ return out
1474
+ }
1475
+
1476
+ ; Apply matching rules to a single element, from a plan built once per
1477
+ ; distinct (class string, theme, interaction state) and replayed after that.
1478
+ ; See the cache fields for why.
1479
+ ; The four interaction bits as an int, so the common case compares numbers
1480
+ ; and allocates nothing. The string form is only built when a plan has to
1481
+ ; be looked up.
1482
+ sfn stateBits:int (el:EVGElement) {
1483
+ def b:int 0
1484
+ if el.isHovered { b = (b + 1) }
1485
+ if el.isFocused { b = (b + 2) }
1486
+ if el.isPressed { b = (b + 4) }
1487
+ if el.a11yDisabled { b = (b + 8) }
1488
+ return b
1489
+ }
1490
+
1491
+ fn applyTo:void (el:EVGElement theme:string) {
1492
+ if ((strlen el.className) == 0) {
1493
+ return
1494
+ }
1495
+ def bits:int (EVGStyleSheet.stateBits(el))
1496
+
1497
+ ; Already holding exactly this. Four scalar comparisons and no
1498
+ ; allocation, and on a hover it is the answer for every element but
1499
+ ; one — which is the whole point: the pass still WALKS the tree, but it
1500
+ ; stops writing to it.
1501
+ ;
1502
+ ; `styleGen` is what makes this safe against everything that is not a
1503
+ ; class or a state change: re-parsing and resizing bump the sheet's
1504
+ ; generation, and `EVGTransition.writeBack` sets the element's to -1
1505
+ ; because after a flight has written a showing value the element is no
1506
+ ; longer holding what the sheet declared.
1507
+ if (((((el.styleGen == generation) && (el.styleBits == bits))
1508
+ && (el.styleKids == (array_length el.children)))
1509
+ && (el.styleTheme == theme)) && (el.styleClass == el.className)) {
1510
+ passSkipped = (passSkipped + 1)
1511
+ return
1512
+ }
1513
+
1514
+ def key:string ((((el.className + "|") + theme) + "|") + (to_string bits))
1515
+ def at@(optional):int (get planIndex key)
1516
+ def slot:int 0
1517
+ if (null? at) {
1518
+ slot = (this.buildPlan(el theme key))
1519
+ planMisses = (planMisses + 1)
1520
+ } {
1521
+ slot = (unwrap at)
1522
+ planHits = (planHits + 1)
1523
+ }
1524
+
1525
+ ; What KIND of change this is. Two plans whose layout-relevant
1526
+ ; declarations are identical cannot move a box, however much else
1527
+ ; differs — a `:hover` rule that only sets a colour is the case this
1528
+ ; exists for. An element with inline attributes is counted as
1529
+ ; layout-dirty regardless, because the plan does not know what they say.
1530
+ ;
1531
+ ; Only a slot from THIS generation's plans is compared: `dropPlans`
1532
+ ; empties the tables, and an element that outlives that — a kept
1533
+ ; card, styled before the viewport changed — still names a slot in
1534
+ ; the old ones. Reading the new table at it is past its end, which
1535
+ ; JavaScript answers with undefined and Swift with a trap.
1536
+ passStyled = (passStyled + 1)
1537
+ def layoutMoved:boolean true
1538
+ if (((el.styleSlot >= 0) && (el.styleGen == generation)) && (el.styleKids == (array_length el.children))) {
1539
+ if ((array_length el.inlineProps) == 0) {
1540
+ if ((itemAt planLayoutSig el.styleSlot) == (itemAt planLayoutSig slot)) {
1541
+ layoutMoved = false
1542
+ }
1543
+ }
1544
+ }
1545
+ if layoutMoved {
1546
+ passLayoutDirty = (passLayoutDirty + 1)
1547
+ } {
1548
+ passPaintDirty = (passPaintDirty + 1)
1549
+ }
1550
+ el.styleClass = el.className
1551
+ el.styleTheme = theme
1552
+ el.styleBits = bits
1553
+ el.styleGen = generation
1554
+ el.styleSlot = slot
1555
+ el.styleKids = (array_length el.children)
1556
+
1557
+ def from:int (itemAt planStart slot)
1558
+ def n:int (itemAt planCount slot)
1559
+ def i:int 0
1560
+ while (i < n) {
1561
+ def name:string (itemAt planNames (from + i))
1562
+ ; The one thing that is NOT in the key. Inline attributes outrank
1563
+ ; every rule and they belong to the element, not to its class — the
1564
+ ; sortable's placeholder carries an inline `transform` that its
1565
+ ; siblings do not. So the plan records every write the rules asked
1566
+ ; for and this filter runs per element, which is cheap because
1567
+ ; `hasInline` answers immediately when nothing is inline.
1568
+ if ((el.hasInline(name)) == false) {
1569
+ el.setAttribute(name (itemAt planValues (from + i)))
1570
+ }
1571
+ i = (i + 1)
1572
+ }
1573
+ }
1574
+
1575
+ ; The original, direct implementation. Kept — not as dead code but as the
1576
+ ; ORACLE: `EVGStyleCacheTest` applies the same tree both ways and compares
1577
+ ; the display lists, so the cache is checked against the thing it replaced
1578
+ ; rather than against a description of it.
1579
+ fn applyToDirect:void (el:EVGElement theme:string) {
1580
+ if ((strlen el.className) == 0) {
1581
+ return
1582
+ }
1583
+ def classes:[string] (this.splitWhitespace(el.className))
1584
+ this.clearStateProps(el classes theme)
1585
+ ; Weakest first, so stronger overwrites: plain classes, then
1586
+ ; theme-scoped ones, then the state rules in the same two groups.
1587
+ ;
1588
+ ; A state rule wins WHEREVER IT IS WRITTEN, which is why this is four
1589
+ ; passes rather than one loop in file order: `:hover` is more specific
1590
+ ; than the bare class, and the version that simply applied rules in
1591
+ ; order let a base rule declared later undo it.
1592
+ this.applyGroup(el classes theme false false)
1593
+ this.applyGroup(el classes theme true false)
1594
+ this.applyGroup(el classes theme false true)
1595
+ this.applyGroup(el classes theme true true)
1596
+ }
1597
+
1598
+ fn applyTreeDirect:void (root:EVGElement theme:string) {
1599
+ this.applyToDirect(root theme)
1600
+ def i:int 0
1601
+ def n:int (root.getChildCount())
1602
+ while (i < n) {
1603
+ this.applyTreeDirect((root.getChild(i)) theme)
1604
+ i = (i + 1)
1605
+ }
1606
+ }
1607
+
1608
+ ; Record what `applyToDirect` WOULD write, in the same order, and remember
1609
+ ; it under `key`. The element is read for its state bits and its classes and
1610
+ ; is not written to.
1611
+ fn buildPlan:int (el:EVGElement theme:string key:string) {
1612
+ def classes:[string] (this.splitWhitespace(el.className))
1613
+ def from:int (array_length planNames)
1614
+ this.planStateClears(classes theme)
1615
+ this.planGroup(el classes theme false false)
1616
+ this.planGroup(el classes theme true false)
1617
+ this.planGroup(el classes theme false true)
1618
+ this.planGroup(el classes theme true true)
1619
+ def slot:int (array_length planStart)
1620
+ push planStart from
1621
+ def count:int ((array_length planNames) - from)
1622
+ push planCount count
1623
+ ; The layout-relevant half of the plan, as one string. Built once here
1624
+ ; so the comparison at apply time is a single string equality.
1625
+ def sig:string ""
1626
+ def k:int 0
1627
+ while (k < count) {
1628
+ def nm:string (itemAt planNames (from + k))
1629
+ if (EVGStyleSheet.isLayoutProperty(nm)) {
1630
+ sig = ((((sig + nm) + ":") + (itemAt planValues (from + k))) + ";")
1631
+ }
1632
+ k = (k + 1)
1633
+ }
1634
+ push planLayoutSig sig
1635
+ set planIndex key slot
1636
+ return slot
1637
+ }
1638
+
1639
+ ; `clearStateProps`, recorded. Note it does NOT consult `EVGPseudo.holds`:
1640
+ ; every state rule that could match this class resets its properties first,
1641
+ ; whether or not the state is currently on — which is what makes leaving a
1642
+ ; state put the base value back.
1643
+ fn planStateClears:void (classes:[string] theme:string) {
1644
+ def i:int 0
1645
+ def n:int (array_length rules)
1646
+ while (i < n) {
1647
+ def rule:EVGStyleRule (itemAt rules i)
1648
+ if (rule.pseudo != 0) {
1649
+ def applies:boolean true
1650
+ if (rule.isThemeScoped()) {
1651
+ applies = (EVGStyleSheet.themeOn(rule.theme theme))
1652
+ }
1653
+ if applies {
1654
+ applies = (rule.media.matches(viewportW viewportH coarsePointer))
1655
+ }
1656
+ if applies {
1657
+ applies = (this.matchesClass(classes rule.className))
1658
+ }
1659
+ if applies {
1660
+ def d:int 0
1661
+ while (d < (array_length rule.decls)) {
1662
+ def decl:EVGStyleDecl (itemAt rule.decls d)
1663
+ def init:string (EVGStyleSheet.initialValue(decl.name))
1664
+ if ((strlen init) > 0) {
1665
+ push planNames decl.name
1666
+ push planValues init
1667
+ ; No rule asked for this: it is the initial value,
1668
+ ; put back before the state rules are consulted.
1669
+ push planRules (0 - 1)
1670
+ }
1671
+ d = (d + 1)
1672
+ }
1673
+ }
1674
+ }
1675
+ i = (i + 1)
1676
+ }
1677
+ }
1678
+
1679
+ ; `applyGroup`, recorded.
1680
+ fn planGroup:void (el:EVGElement classes:[string] theme:string themeScoped:boolean stateful:boolean) {
1681
+ def i:int 0
1682
+ def n:int (array_length rules)
1683
+ while (i < n) {
1684
+ def rule:EVGStyleRule (itemAt rules i)
1685
+ def isStateful:boolean (rule.pseudo != 0)
1686
+ if ((isStateful == stateful) && ((rule.isThemeScoped()) == themeScoped)) {
1687
+ def applies:boolean true
1688
+ if themeScoped {
1689
+ applies = (EVGStyleSheet.themeOn(rule.theme theme))
1690
+ }
1691
+ if applies {
1692
+ applies = (rule.media.matches(viewportW viewportH coarsePointer))
1693
+ }
1694
+ if applies {
1695
+ applies = (EVGPseudo.holds(rule.pseudo el))
1696
+ }
1697
+ if applies {
1698
+ if (this.matchesClass(classes rule.className)) {
1699
+ def d:int 0
1700
+ while (d < (array_length rule.decls)) {
1701
+ def decl:EVGStyleDecl (itemAt rule.decls d)
1702
+ ; ONCE PER PLAN, which is once per (class, theme,
1703
+ ; state) — the reason a sheet written entirely in
1704
+ ; variables costs what a literal one costs.
1705
+ def v:string (this.resolveVars(decl.value theme decl.name))
1706
+ if ((strlen v) > 0) {
1707
+ push planNames decl.name
1708
+ push planValues v
1709
+ push planRules i
1710
+ }
1711
+ d = (d + 1)
1712
+ }
1713
+ }
1714
+ }
1715
+ }
1716
+ i = (i + 1)
1717
+ }
1718
+ }
1719
+
1720
+ ; Put back the initial value of everything a STATE rule for this element
1721
+ ; can set, before deciding which states hold.
1722
+ ;
1723
+ ; The cascade in a browser recomputes a style from nothing every time. This
1724
+ ; one mutates the element in place, which is fast and which quietly breaks
1725
+ ; the moment a property is declared ONLY under `:hover`: the pointer leaves,
1726
+ ; no rule writes the property, and the hover value stays on the element
1727
+ ; forever. A row that lifts and never comes back down.
1728
+ ;
1729
+ ; The properties at risk are exactly those a state rule declares — a base
1730
+ ; rule's are rewritten by the base pass a moment later — so that is the set
1731
+ ; cleared here, whether or not the state currently holds.
1732
+ ;
1733
+ ; Only properties with an unambiguous initial value are cleared, and
1734
+ ; `initialValue` says which those are. A property outside that table keeps
1735
+ ; the old behaviour, so the fix cannot make anything worse than it was; the
1736
+ ; workaround there is the one that has always worked, which is to declare
1737
+ ; the property on the base rule too.
1738
+ fn clearStateProps:void (el:EVGElement classes:[string] theme:string) {
1739
+ def i:int 0
1740
+ def n:int (array_length rules)
1741
+ while (i < n) {
1742
+ def rule:EVGStyleRule (itemAt rules i)
1743
+ if (rule.pseudo != 0) {
1744
+ def applies:boolean true
1745
+ if (rule.isThemeScoped()) {
1746
+ applies = (EVGStyleSheet.themeOn(rule.theme theme))
1747
+ }
1748
+ if applies {
1749
+ applies = (rule.media.matches(viewportW viewportH coarsePointer))
1750
+ }
1751
+ if applies {
1752
+ applies = (this.matchesClass(classes rule.className))
1753
+ }
1754
+ if applies {
1755
+ def d:int 0
1756
+ while (d < (array_length rule.decls)) {
1757
+ def decl:EVGStyleDecl (itemAt rule.decls d)
1758
+ def init:string (EVGStyleSheet.initialValue(decl.name))
1759
+ if ((strlen init) > 0) {
1760
+ ; Inline still outranks the sheet, so an inline
1761
+ ; value is not something a state rule may clear.
1762
+ if ((el.hasInline(decl.name)) == false) {
1763
+ el.setAttribute(decl.name init)
1764
+ }
1765
+ }
1766
+ d = (d + 1)
1767
+ }
1768
+ }
1769
+ }
1770
+ i = (i + 1)
1771
+ }
1772
+ }
1773
+
1774
+ ; The CSS initial value, for the properties where it is unambiguous and
1775
+ ; cheap to write back. "" means "no safe initial" and the property is left
1776
+ ; alone.
1777
+ ;
1778
+ ; `color` is deliberately absent: its initial value is `inherit`, and
1779
+ ; writing a literal here would break inheritance rather than restore it.
1780
+ sfn initialValue:string (name:string) {
1781
+ if (name == "transform") { return "none" }
1782
+ if (name == "transform-origin") { return "50% 50%" }
1783
+ if (name == "transformOrigin") { return "50% 50%" }
1784
+ if (name == "opacity") { return "1" }
1785
+ if (name == "background-color") { return "transparent" }
1786
+ if (name == "backgroundColor") { return "transparent" }
1787
+ if (name == "rotate") { return "0" }
1788
+ if (name == "scale") { return "1" }
1789
+ return ""
1790
+ }
1791
+
1792
+ ; Does this property take part in deciding where boxes go?
1793
+ ;
1794
+ ; Conservative by construction: anything not listed here counts as LAYOUT,
1795
+ ; so a property nobody thought about costs a layout pass rather than
1796
+ ; silently leaving a stale one. The listed set is the paint-only one —
1797
+ ; colours, opacity, shadows, radii, and `transform`, which in EVG is applied
1798
+ ; to the display list after layout and therefore moves nothing.
1799
+ sfn isLayoutProperty:boolean (name:string) {
1800
+ if (name == "color") { return false }
1801
+ if (name == "background-color") { return false }
1802
+ if (name == "backgroundColor") { return false }
1803
+ if (name == "border-color") { return false }
1804
+ if (name == "borderColor") { return false }
1805
+ if (name == "fill") { return false }
1806
+ if (name == "stroke") { return false }
1807
+ if (name == "opacity") { return false }
1808
+ if (name == "border-radius") { return false }
1809
+ if (name == "borderRadius") { return false }
1810
+ if (name == "transform") { return false }
1811
+ if (name == "transform-origin") { return false }
1812
+ if (name == "transformOrigin") { return false }
1813
+ if (name == "rotate") { return false }
1814
+ if (name == "scale") { return false }
1815
+ if (name == "box-shadow") { return false }
1816
+ if (name == "shadow-color") { return false }
1817
+ if (name == "background-gradient") { return false }
1818
+ if (name == "backdrop-filter") { return false }
1819
+ if (name == "cursor") { return false }
1820
+ if (name == "transition") { return false }
1821
+ if (name == "scrollbar-width") { return false }
1822
+ if (name == "scrollbar-color") { return false }
1823
+ if (name == "evg-scrollbar-label") { return false }
1824
+ return true
1825
+ }
1826
+
1827
+ fn applyGroup:void (el:EVGElement classes:[string] theme:string themeScoped:boolean stateful:boolean) {
1828
+ def i:int 0
1829
+ def n:int (array_length rules)
1830
+ while (i < n) {
1831
+ def rule:EVGStyleRule (itemAt rules i)
1832
+ def isStateful:boolean (rule.pseudo != 0)
1833
+ if ((isStateful == stateful) && ((rule.isThemeScoped()) == themeScoped)) {
1834
+ def applies:boolean true
1835
+ if themeScoped {
1836
+ applies = (EVGStyleSheet.themeOn(rule.theme theme))
1837
+ }
1838
+ if applies {
1839
+ applies = (rule.media.matches(viewportW viewportH coarsePointer))
1840
+ }
1841
+ if applies {
1842
+ applies = (EVGPseudo.holds(rule.pseudo el))
1843
+ }
1844
+ if applies {
1845
+ if (this.matchesClass(classes rule.className)) {
1846
+ this.applyDecls(el rule theme)
1847
+ }
1848
+ }
1849
+ }
1850
+ i = (i + 1)
1851
+ }
1852
+ }
1853
+
1854
+ fn matchesClass:boolean (classes:[string] want:string) {
1855
+ def i:int 0
1856
+ while (i < (array_length classes)) {
1857
+ if ((itemAt classes i) == want) {
1858
+ return true
1859
+ }
1860
+ i = (i + 1)
1861
+ }
1862
+ return false
1863
+ }
1864
+
1865
+ ; The direct path's write. It resolves `var()` like the plan builder does,
1866
+ ; and it has to: `EVGStyleCacheTest` styles the same tree both ways and
1867
+ ; compares the display lists, so a variable the cache understood and this
1868
+ ; did not would show up as the cache being wrong.
1869
+ fn applyDecls:void (el:EVGElement rule:EVGStyleRule theme:string) {
1870
+ def i:int 0
1871
+ while (i < (array_length rule.decls)) {
1872
+ def d:EVGStyleDecl (itemAt rule.decls i)
1873
+ ; Inline attributes outrank every stylesheet rule.
1874
+ if ((el.hasInline(d.name)) == false) {
1875
+ def v:string (this.resolveVars(d.value theme d.name))
1876
+ if ((strlen v) > 0) {
1877
+ el.setAttribute(d.name v)
1878
+ ; So a serializer can tell a cascaded value from an
1879
+ ; authored one. Without it, writing the tree after layout
1880
+ ; copies the whole sheet into every node it matched.
1881
+ el.markCss(d.name)
1882
+ }
1883
+ }
1884
+ i = (i + 1)
1885
+ }
1886
+ }
1887
+
1888
+ ; -----------------------------------------------------------------------
1889
+ ; The sheet as an artifact
1890
+ ; -----------------------------------------------------------------------
1891
+ ;
1892
+ ; PLAN_WEB_LOADING.md S5, C2. Parsing this stylesheet is deterministic
1893
+ ; work: the same 104 KB of text, on every load, on every device, produces
1894
+ ; the same 786 rules and 3436 declarations. It costs about 12 ms cold in
1895
+ ; a browser, which is 40-50 on a phone, and it costs it before the first
1896
+ ; pixel — for an answer that was already known when the page was built.
1897
+ ;
1898
+ ; So the answer can be written down. `toText` writes the parsed sheet in a
1899
+ ; line-and-tab format; `loadText` reads it back. What that buys, measured
1900
+ ; on RealTrainer's sheet: 12 ms of parsing becomes 2 ms of reading, and
1901
+ ; 18.0 KB gzipped of CSS becomes 11.9 KB — SMALLER on the wire as well,
1902
+ ; because what is written down is what the parser kept, and every comment,
1903
+ ; every space and every selector it did not need is gone.
1904
+ ;
1905
+ ; The format is deliberately dull: one line per record, tab-separated,
1906
+ ; a version on the first line. `R` is a rule, the `D` lines under it are
1907
+ ; its declarations, `V` is a palette entry. A reader that meets a version
1908
+ ; it does not know says so by returning false rather than by guessing.
1909
+
1910
+ sfn sheetHeader:string () {
1911
+ return "evg-sheet 1"
1912
+ }
1913
+
1914
+ ; A backslash, a tab, a newline and a carriage return are the four
1915
+ ; characters that would break a line-and-tab format, and a CSS value may
1916
+ ; contain any of them.
1917
+ sfn escChar:string (c:int piece:string) {
1918
+ if (c == 92) { return "\\\\" }
1919
+ if (c == 9) { return "\\t" }
1920
+ if (c == 10) { return "\\n" }
1921
+ if (c == 13) { return "\\r" }
1922
+ return piece
1923
+ }
1924
+
1925
+ ; A SCAN BEFORE A BUILD, and it is most of what this format costs. Nearly
1926
+ ; no CSS value contains a backslash, a tab or a newline, so nearly every
1927
+ ; string here is already its own escaping — and building it again
1928
+ ; character by character, eleven thousand times, was the difference
1929
+ ; between reading the sheet in 5 ms and reading it in 1.
1930
+ sfn needsEscape:boolean (s:string) {
1931
+ def len:int (strlen s)
1932
+ def i@(mutable):int 0
1933
+ while (i < len) {
1934
+ def c:int (charAt s i)
1935
+ if (((c == 92) || (c == 9)) || ((c == 10) || (c == 13))) {
1936
+ return true
1937
+ }
1938
+ i = (i + 1)
1939
+ }
1940
+ return false
1941
+ }
1942
+
1943
+ sfn escText:string (s:string) {
1944
+ if ((EVGStyleSheet.needsEscape(s)) == false) {
1945
+ return s
1946
+ }
1947
+ def out@(mutable):string ""
1948
+ def len:int (strlen s)
1949
+ def i:int 0
1950
+ while (i < len) {
1951
+ out = (out + (EVGStyleSheet.escChar((charAt s i) (substring s i (i + 1)))))
1952
+ i = (i + 1)
1953
+ }
1954
+ return out
1955
+ }
1956
+
1957
+ ; The same scan the other way: a string with no backslash in it is
1958
+ ; already what it unescapes to.
1959
+ sfn hasBackslash:boolean (s:string) {
1960
+ def len:int (strlen s)
1961
+ def i@(mutable):int 0
1962
+ while (i < len) {
1963
+ if ((charAt s i) == 92) {
1964
+ return true
1965
+ }
1966
+ i = (i + 1)
1967
+ }
1968
+ return false
1969
+ }
1970
+
1971
+ sfn unescText:string (s:string) {
1972
+ if ((EVGStyleSheet.hasBackslash(s)) == false) {
1973
+ return s
1974
+ }
1975
+ def out@(mutable):string ""
1976
+ def len:int (strlen s)
1977
+ def i@(mutable):int 0
1978
+ while (i < len) {
1979
+ def c:int (charAt s i)
1980
+ if (c == 92) {
1981
+ if ((i + 1) < len) {
1982
+ def n:int (charAt s (i + 1))
1983
+ if (n == 92) { out = (out + "\\") }
1984
+ if (n == 116) { out = (out + "\t") }
1985
+ if (n == 110) { out = (out + "\n") }
1986
+ if (n == 114) { out = (out + "\r") }
1987
+ i = (i + 2)
1988
+ } {
1989
+ i = (i + 1)
1990
+ }
1991
+ } {
1992
+ out = (out + (substring s i (i + 1)))
1993
+ i = (i + 1)
1994
+ }
1995
+ }
1996
+ return out
1997
+ }
1998
+
1999
+ ; `to_double` on a string the writer produced. The writer produced it with
2000
+ ; `to_string`, so the only shape to allow for beyond a plain number is the
2001
+ ; minus sign an unset bound carries.
2002
+ sfn readNum:double (s:string) {
2003
+ def t:string (trim s)
2004
+ if ((strlen t) == 0) {
2005
+ return 0.0
2006
+ }
2007
+ def neg:boolean ((charAt t 0) == 45)
2008
+ def digits@(mutable):string t
2009
+ if neg {
2010
+ digits = (substring t 1 (strlen t))
2011
+ }
2012
+ ; `to_double` answers with an optional, because a string is not
2013
+ ; necessarily a number. This one came out of `to_string` a moment ago,
2014
+ ; so the miss is a corrupt artifact and zero is the safe reading.
2015
+ def v@(optional):double (to_double digits)
2016
+ if v {
2017
+ if neg {
2018
+ return (0.0 - (unwrap v))
2019
+ }
2020
+ return (unwrap v)
2021
+ }
2022
+ return 0.0
2023
+ }
2024
+
2025
+ sfn readInt:int (s:string) {
2026
+ return (to_int (EVGStyleSheet.readNum(s)))
2027
+ }
2028
+
2029
+ ; A media query as seven fields, in the order `readMedia` expects them.
2030
+ sfn writeMedia:string (m:EVGMediaQuery) {
2031
+ def out@(mutable):string ((to_string m.minWidth) + "\t")
2032
+ out = (out + ((to_string m.maxWidth) + "\t"))
2033
+ out = (out + ((to_string m.minHeight) + "\t"))
2034
+ out = (out + ((to_string m.maxHeight) + "\t"))
2035
+ out = (out + ((EVGStyleSheet.escText(m.orientation)) + "\t"))
2036
+ out = (out + ((to_string m.pointer) + "\t"))
2037
+ if m.broken {
2038
+ return (out + "1")
2039
+ }
2040
+ return (out + "0")
2041
+ }
2042
+
2043
+ sfn readMedia:EVGMediaQuery (parts:[string] at:int) {
2044
+ def m (new EVGMediaQuery())
2045
+ m.minWidth = (EVGStyleSheet.readNum((itemAt parts at)))
2046
+ m.maxWidth = (EVGStyleSheet.readNum((itemAt parts (at + 1))))
2047
+ m.minHeight = (EVGStyleSheet.readNum((itemAt parts (at + 2))))
2048
+ m.maxHeight = (EVGStyleSheet.readNum((itemAt parts (at + 3))))
2049
+ m.orientation = (EVGStyleSheet.unescText((itemAt parts (at + 4))))
2050
+ m.pointer = (EVGStyleSheet.readInt((itemAt parts (at + 5))))
2051
+ m.broken = ((itemAt parts (at + 6)) == "1")
2052
+ return m
2053
+ }
2054
+
2055
+ ; The parsed sheet, written down. Rules in source order with their
2056
+ ; declarations under them, then the palette — which is the order
2057
+ ; `loadText` wants and the order the parser produced.
2058
+ fn toText:string () {
2059
+ def out@(mutable):string ((EVGStyleSheet.sheetHeader()) + "\n")
2060
+ for rules r:EVGStyleRule i {
2061
+ def line@(mutable):string ("R\t" + ((EVGStyleSheet.escText(r.theme)) + "\t"))
2062
+ line = (line + ((EVGStyleSheet.escText(r.className)) + "\t"))
2063
+ line = (line + ((to_string r.pseudo) + "\t"))
2064
+ line = (line + ((to_string r.order) + "\t"))
2065
+ line = (line + (EVGStyleSheet.writeMedia(r.media)))
2066
+ out = (out + (line + "\n"))
2067
+ for r.decls d:EVGStyleDecl j {
2068
+ def dl:string (("D\t" + (EVGStyleSheet.escText(d.name))) + ("\t" + (EVGStyleSheet.escText(d.value))))
2069
+ out = (out + (dl + "\n"))
2070
+ }
2071
+ }
2072
+ def vn:int (array_length varNames)
2073
+ def k@(mutable):int 0
2074
+ while (k < vn) {
2075
+ def line@(mutable):string ("V\t" + ((EVGStyleSheet.escText((itemAt varThemes k))) + "\t"))
2076
+ line = (line + ((EVGStyleSheet.escText((itemAt varNames k))) + "\t"))
2077
+ line = (line + ((EVGStyleSheet.escText((itemAt varValues k))) + "\t"))
2078
+ line = (line + (EVGStyleSheet.writeMedia((itemAt varMedia k))))
2079
+ out = (out + (line + "\n"))
2080
+ k = (k + 1)
2081
+ }
2082
+ return out
2083
+ }
2084
+
2085
+ ; Is this text a written-down sheet rather than CSS? Asked by callers that
2086
+ ; take either — `RealTrainerDemo.init` does — so the page can hand over
2087
+ ; the artifact and every check can go on handing over the source.
2088
+ sfn isSheetText:boolean (text:string) {
2089
+ def h:string (EVGStyleSheet.sheetHeader())
2090
+ def n:int (strlen h)
2091
+ if ((strlen text) < n) {
2092
+ return false
2093
+ }
2094
+ return ((substring text 0 n) == h)
2095
+ }
2096
+
2097
+ ; The other half of `toText`. False means the text was not a sheet this
2098
+ ; reader knows, and the caller should parse the CSS it has instead of
2099
+ ; drawing an unstyled page.
2100
+ fn loadText:boolean (text:string) {
2101
+ if ((EVGStyleSheet.isSheetText(text)) == false) {
2102
+ return false
2103
+ }
2104
+ this.dropPlans()
2105
+ def lines:[string] (this.splitOn(text 10))
2106
+ def current@(optional):EVGStyleRule
2107
+ for lines line:string i {
2108
+ if ((strlen line) == 0) { continue }
2109
+ def parts:[string] (this.splitOn(line 9))
2110
+ def kind:string (itemAt parts 0)
2111
+ if (kind == "R") {
2112
+ if ((array_length parts) < 12) { continue }
2113
+ def r (new EVGStyleRule())
2114
+ r.theme = (EVGStyleSheet.unescText((itemAt parts 1)))
2115
+ r.className = (EVGStyleSheet.unescText((itemAt parts 2)))
2116
+ r.pseudo = (EVGStyleSheet.readInt((itemAt parts 3)))
2117
+ r.order = (EVGStyleSheet.readInt((itemAt parts 4)))
2118
+ r.media = (EVGStyleSheet.readMedia(parts 5))
2119
+ push rules r
2120
+ current = r
2121
+ if (r.order >= ruleCounter) {
2122
+ ruleCounter = (r.order + 1)
2123
+ }
2124
+ continue
2125
+ }
2126
+ if (kind == "D") {
2127
+ if ((array_length parts) < 3) { continue }
2128
+ if current {
2129
+ def d (new EVGStyleDecl())
2130
+ d.name = (EVGStyleSheet.unescText((itemAt parts 1)))
2131
+ d.value = (EVGStyleSheet.unescText((itemAt parts 2)))
2132
+ def owner:EVGStyleRule (unwrap current)
2133
+ push owner.decls d
2134
+ }
2135
+ continue
2136
+ }
2137
+ if (kind == "V") {
2138
+ if ((array_length parts) < 11) { continue }
2139
+ push varThemes (EVGStyleSheet.unescText((itemAt parts 1)))
2140
+ push varNames (EVGStyleSheet.unescText((itemAt parts 2)))
2141
+ push varValues (EVGStyleSheet.unescText((itemAt parts 3)))
2142
+ push varMedia (EVGStyleSheet.readMedia(parts 4))
2143
+ continue
2144
+ }
2145
+ }
2146
+ return true
2147
+ }
2148
+ }