ranger-compiler 3.5.0 → 3.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1160 -19
- package/LICENSE +3 -1
- package/README.md +124 -29
- package/dist/Lang.rgr +1188 -290
- package/dist/api.d.ts +2343 -879
- package/dist/api.js +79924 -56922
- package/dist/lib/JSON.rgr +102 -91
- package/dist/lib/Shell.rgr +4 -4
- package/dist/lib/apple/AppleToolchain.rgr +4 -4
- package/dist/lib/apple/README.md +5 -4
- package/dist/lib/apple/apple_test.rgr +1 -1
- package/dist/lib/core/README.md +1 -1
- package/dist/lib/evg/EVG.rgr +12 -0
- package/dist/lib/evg/EVGA11yFromTree.rgr +302 -0
- package/dist/lib/evg/EVGA11yTree.rgr +894 -0
- package/dist/lib/evg/EVGBox.rgr +267 -0
- package/dist/lib/evg/EVGBoxShorthandTest.rgr +220 -0
- package/dist/lib/evg/EVGCodepoint.rgr +316 -0
- package/dist/lib/evg/EVGColor.rgr +700 -0
- package/dist/lib/evg/EVGCommands.rgr +177 -0
- package/dist/lib/evg/EVGComponent.rgr +331 -0
- package/dist/lib/evg/EVGComponentTest.rgr +387 -0
- package/dist/lib/evg/EVGConnector.rgr +541 -0
- package/dist/lib/evg/EVGConnectorTest.rgr +306 -0
- package/dist/lib/evg/EVGDisplayList.rgr +4186 -0
- package/dist/lib/evg/EVGEasing.rgr +370 -0
- package/dist/lib/evg/EVGEffectTest.rgr +253 -0
- package/dist/lib/evg/EVGElement.rgr +4914 -0
- package/dist/lib/evg/EVGFixedTest.rgr +289 -0
- package/dist/lib/evg/EVGFlexRulesTest.rgr +317 -0
- package/dist/lib/evg/EVGFlexWrapTest.rgr +246 -0
- package/dist/lib/evg/EVGFling.rgr +244 -0
- package/dist/lib/evg/EVGFocus.rgr +400 -0
- package/dist/lib/evg/EVGFocusTest.rgr +399 -0
- package/dist/lib/evg/EVGGradient.rgr +322 -0
- package/dist/lib/evg/EVGGrapheme.rgr +190 -0
- package/dist/lib/evg/EVGGrid.rgr +975 -0
- package/dist/lib/evg/EVGHitTest.rgr +208 -0
- package/dist/lib/evg/EVGHoles.rgr +294 -0
- package/dist/lib/evg/EVGHostMeasurerTest.rgr +281 -0
- package/dist/lib/evg/EVGHostTextMeasurer.rgr +331 -0
- package/dist/lib/evg/EVGHostTree.rgr +834 -0
- package/dist/lib/evg/EVGHostTreeTest.rgr +447 -0
- package/dist/lib/evg/EVGImageDecode.rgr +117 -0
- package/dist/lib/evg/EVGImageMeasurer.rgr +86 -0
- package/dist/lib/evg/EVGInspect.rgr +883 -0
- package/dist/lib/evg/EVGInvalidateTest.rgr +432 -0
- package/dist/lib/evg/EVGJsonTest.rgr +535 -0
- package/dist/lib/evg/EVGLayout.rgr +4220 -0
- package/dist/lib/evg/EVGMeasure.rgr +1267 -0
- package/dist/lib/evg/EVGOverlayTest.rgr +626 -0
- package/dist/lib/evg/EVGPatch.rgr +1721 -0
- package/dist/lib/evg/EVGPatchTest.rgr +692 -0
- package/dist/lib/evg/EVGPopoverTest.rgr +395 -0
- package/dist/lib/evg/EVGReconcile.rgr +226 -0
- package/dist/lib/evg/EVGReconcileTest.rgr +421 -0
- package/dist/lib/evg/EVGReject.rgr +126 -0
- package/dist/lib/evg/EVGRelayoutTest.rgr +375 -0
- package/dist/lib/evg/EVGRtlLayoutTest.rgr +319 -0
- package/dist/lib/evg/EVGRuler.rgr +232 -0
- package/dist/lib/evg/EVGRulerTest.rgr +172 -0
- package/dist/lib/evg/EVGSelectChrome.rgr +178 -0
- package/dist/lib/evg/EVGStyleCacheTest.rgr +419 -0
- package/dist/lib/evg/EVGStyleSheet.rgr +2148 -0
- package/dist/lib/evg/EVGStyleStateTest.rgr +407 -0
- package/dist/lib/evg/EVGStyleVarTest.rgr +471 -0
- package/dist/lib/evg/EVGText.rgr +105 -0
- package/dist/lib/evg/EVGTextEngine.rgr +536 -0
- package/dist/lib/evg/EVGTextMeasurer.rgr +720 -0
- package/dist/lib/evg/EVGTimingTest.rgr +1417 -0
- package/dist/lib/evg/EVGToolbar.rgr +1369 -0
- package/dist/lib/evg/EVGTransition.rgr +728 -0
- package/dist/lib/evg/EVGTreeJson.rgr +721 -0
- package/dist/lib/evg/EVGUnit.rgr +554 -0
- package/dist/lib/evg/EVGViewportUnitTest.rgr +236 -0
- package/dist/lib/evg/EvgApp.rgr +189 -0
- package/dist/lib/evg/EvgBitmapTracer.rgr +4277 -0
- package/dist/lib/evg/EvgBitmapTracerTest.rgr +2119 -0
- package/dist/lib/evg/EvgHost.rgr +451 -0
- package/dist/lib/evg/EvgTest.rgr +91 -0
- package/dist/lib/evg/EvgTraceColor.rgr +236 -0
- package/dist/lib/evg/EvgTraceCurve.rgr +648 -0
- package/dist/lib/evg/EvgTraceFit.rgr +767 -0
- package/dist/lib/evg/EvgTracePath.rgr +435 -0
- package/dist/lib/evg/EvgTraceTypes.rgr +559 -0
- package/dist/lib/evg/EvgViewport.rgr +313 -0
- package/dist/lib/evg/FxDemoDoc.rgr +185 -0
- package/dist/lib/evg/HOSTS.md +227 -0
- package/dist/lib/evg/ISSUES.md +835 -0
- package/dist/lib/evg/PLAN_ACCESSIBILITY.md +650 -0
- package/dist/lib/evg/PLAN_CSS_LAYOUT_AND_FONTS.md +1124 -0
- package/dist/lib/evg/PLAN_EFFECTS.md +431 -0
- package/dist/lib/evg/PLAN_EVG.md +518 -0
- package/dist/lib/evg/PLAN_EVG_RENDERER.md +1674 -0
- package/dist/lib/evg/PLAN_INSPECTOR.md +788 -0
- package/dist/lib/evg/PLAN_LINKS_AND_FORMS.md +157 -0
- package/dist/lib/evg/PLAN_NATIVE_HOSTS.md +671 -0
- package/dist/lib/evg/PLAN_VECTOR_IR.md +664 -0
- package/dist/lib/evg/PLAN_VIEW_TRANSFORM.md +322 -0
- package/dist/lib/evg/PathBuilder.rgr +333 -0
- package/dist/lib/evg/README.md +1236 -0
- package/dist/lib/evg/SPEC.md +1252 -0
- package/dist/lib/evg/SVGPathParser.rgr +1199 -0
- package/dist/lib/evg/SvgParser.rgr +1736 -0
- package/dist/lib/evg/TODO_EVG_RENDERER.md +595 -0
- package/dist/lib/evg/VectorShapes.rgr +331 -0
- package/dist/lib/evg/VectorStroke.rgr +107 -0
- package/dist/lib/evg/VectorViewBox.rgr +380 -0
- package/dist/lib/evg/agent/README.md +290 -0
- package/dist/lib/evg/agent/evg_agent.rgr +899 -0
- package/dist/lib/evg/agent/fixtures/broken.evg.json +7 -0
- package/dist/lib/evg/agent/fixtures/card.evg.json +9 -0
- package/dist/lib/evg/agent/fixtures/connector.css +63 -0
- package/dist/lib/evg/agent/fixtures/connector.evg.json +14 -0
- package/dist/lib/evg/agent/fixtures/drawn.evg.json +129 -0
- package/dist/lib/evg/agent/fixtures/gradient.evg.json +4 -0
- package/dist/lib/evg/agent/fixtures/popover.css +99 -0
- package/dist/lib/evg/agent/fixtures/popover.evg.json +30 -0
- package/dist/lib/evg/agent/roundtrip.sh +74 -0
- package/dist/lib/evg/agent/smoke.sh +230 -0
- package/dist/lib/evg/android/README.md +97 -0
- package/dist/lib/evg/android/androidstubs/AndroidStubs.kt +175 -0
- package/dist/lib/evg/android/androidstubs/Annotation.kt +10 -0
- package/dist/lib/evg/android/androidstubs/App.kt +30 -0
- package/dist/lib/evg/android/androidstubs/Content.kt +41 -0
- package/dist/lib/evg/android/androidstubs/ContentRes.kt +12 -0
- package/dist/lib/evg/android/androidstubs/InputMethod.kt +46 -0
- package/dist/lib/evg/android/androidstubs/Net.kt +6 -0
- package/dist/lib/evg/android/androidstubs/Os.kt +30 -0
- package/dist/lib/evg/android/androidstubs/Util.kt +15 -0
- package/dist/lib/evg/android/androidstubs/View.kt +113 -0
- package/dist/lib/evg/android/androidstubs/Widget.kt +16 -0
- package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidEvgSurface.kt +323 -0
- package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidTextMeasurer.kt +56 -0
- package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/RippleEffect.kt +262 -0
- package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtEvgSurface.kt +238 -0
- package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtTextMeasurer.kt +85 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgEngineThread.kt +115 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgPainter.kt +231 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgSurface.kt +117 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/RecordingSurface.kt +94 -0
- package/dist/lib/evg/apple/README.md +128 -0
- package/dist/lib/evg/apple/Sources/CoreGraphicsEvgSurface.swift +329 -0
- package/dist/lib/evg/apple/Sources/CoreTextMeasurer.swift +70 -0
- package/dist/lib/evg/apple/Sources/EvgEngineQueue.swift +105 -0
- package/dist/lib/evg/apple/Sources/EvgPainter.swift +225 -0
- package/dist/lib/evg/apple/Sources/EvgSurface.swift +163 -0
- package/dist/lib/evg/apple/Sources/RecordingSurface.swift +110 -0
- package/dist/lib/evg/bench/EvgLayoutBench.rgr +34 -0
- package/dist/lib/evg/bench/README.md +64 -0
- package/dist/lib/evg/bench/layout-bench.mjs +316 -0
- package/dist/lib/evg/bench/layout-cases.mjs +510 -0
- package/dist/lib/evg/bench/layout-conformance.mjs +254 -0
- package/dist/lib/evg/bin/.gitignore +17 -0
- package/dist/lib/evg/evg_test.rgr +313 -0
- package/dist/lib/evg/gl/README.md +198 -0
- package/dist/lib/evg/gl/a11y-paint-check.mjs +73 -0
- package/dist/lib/evg/gl/blur-check.mjs +399 -0
- package/dist/lib/evg/gl/boxmodel.json +1 -0
- package/dist/lib/evg/gl/demo.html +46 -0
- package/dist/lib/evg/gl/effect-presets.css +285 -0
- package/dist/lib/evg/gl/effect-presets.js +63 -0
- package/dist/lib/evg/gl/effect-shots.mjs +135 -0
- package/dist/lib/evg/gl/evg-a11y.js +566 -0
- package/dist/lib/evg/gl/evg-binary.js +160 -0
- package/dist/lib/evg/gl/evg-engine.js +296 -0
- package/dist/lib/evg/gl/evg-fx.js +237 -0
- package/dist/lib/evg/gl/evg-gestures.js +209 -0
- package/dist/lib/evg/gl/evg-list.js +167 -0
- package/dist/lib/evg/gl/evg-measure.js +186 -0
- package/dist/lib/evg/gl/evg-textinput.js +303 -0
- package/dist/lib/evg/gl/evg-view.js +144 -0
- package/dist/lib/evg/gl/evg-webgl.js +3942 -0
- package/dist/lib/evg/gl/fx-check.mjs +998 -0
- package/dist/lib/evg/gl/fx-demo.html +166 -0
- package/dist/lib/evg/gl/fx-demo.js +2 -0
- package/dist/lib/evg/gl/fx-serve.mjs +47 -0
- package/dist/lib/evg/gl/gestures-check.mjs +189 -0
- package/dist/lib/evg/gl/list-binary-check.mjs +152 -0
- package/dist/lib/evg/gl/measure-check.mjs +135 -0
- package/dist/lib/evg/gl/rotation-check.mjs +207 -0
- package/dist/lib/evg/gl/shift-check.mjs +113 -0
- package/dist/lib/evg/gl/stroke-check.mjs +168 -0
- package/dist/lib/evg/gl/text-snap-check.mjs +139 -0
- package/dist/lib/evg/gl/view-check.mjs +343 -0
- package/dist/lib/evg/gl/view-policy-check.mjs +184 -0
- package/dist/lib/evg/html/evg-dom.js +318 -0
- package/dist/lib/evg/html/evg-html.js +600 -0
- package/dist/lib/evg/inspect/README.md +415 -0
- package/dist/lib/evg/inspect/browser-smoke.mjs +115 -0
- package/dist/lib/evg/inspect/evg-inspect.js +947 -0
- package/dist/lib/evg/inspect/shots/css.png +0 -0
- package/dist/lib/evg/inspect/shots/dashboard.png +0 -0
- package/dist/lib/evg/inspect/shots/pptx-slide.png +0 -0
- package/dist/lib/evg/inspect/shots/state.png +0 -0
- package/dist/lib/evg/inspect/shots.mjs +226 -0
- package/dist/lib/evg/oracle/css-blur.json +576 -0
- package/dist/lib/evg/oracle/css-box.json +157 -0
- package/dist/lib/evg/oracle/css-timing.json +709 -0
- package/dist/lib/evg/oracle/css_blur_oracle.mjs +529 -0
- package/dist/lib/evg/oracle/css_box_oracle.mjs +106 -0
- package/dist/lib/evg/oracle/css_timing_oracle.mjs +389 -0
- package/dist/lib/evg/original/EVGColor.clj +310 -0
- package/dist/lib/evg/original/EVGColorContext.rgr +178 -0
- package/dist/lib/evg/original/SVGPath.rgr +627 -0
- package/dist/lib/evg/original/Vec2.crgr +103 -0
- package/dist/lib/evg/ranger.json +9 -0
- package/dist/lib/evg/showcase/README.md +246 -0
- package/dist/lib/evg/showcase/assets/emblem.svg +30 -0
- package/dist/lib/evg/showcase/assets/rosette.svg +22 -0
- package/dist/lib/evg/showcase/build.mjs +651 -0
- package/dist/lib/evg/showcase/pages/album.tsx +30 -0
- package/dist/lib/evg/showcase/pages/boxmodel.tsx +37 -0
- package/dist/lib/evg/showcase/pages/cards.tsx +41 -0
- package/dist/lib/evg/showcase/pages/chart_api.tsx +170 -0
- package/dist/lib/evg/showcase/pages/charts.tsx +219 -0
- package/dist/lib/evg/showcase/pages/drawing.tsx +160 -0
- package/dist/lib/evg/showcase/pages/emoji.tsx +91 -0
- package/dist/lib/evg/showcase/pages/flex.tsx +46 -0
- package/dist/lib/evg/showcase/pages/more.tsx +332 -0
- package/dist/lib/evg/showcase/pages/plots.tsx +262 -0
- package/dist/lib/evg/showcase/pages/svg.tsx +63 -0
- package/dist/lib/evg/showcase/pages/tables.tsx +209 -0
- package/dist/lib/evg/showcase/pages/typography.tsx +43 -0
- package/dist/lib/evg/showcase/pages/units.tsx +35 -0
- package/dist/lib/evg/showcase/pages/variants.tsx +233 -0
- package/dist/lib/evg/showcase/pages/vector.tsx +75 -0
- package/dist/lib/evg/showcase/pages/views.tsx +223 -0
- package/dist/lib/evg/showcase/tests/chart_api_smoke.mjs +339 -0
- package/dist/lib/evg/showcase/tests/gl_smoke.mjs +125 -0
- package/dist/lib/evg/showcase/themes/chart_api-default.css +15 -0
- package/dist/lib/evg/showcase/themes/charts-default.css +29 -0
- package/dist/lib/evg/showcase/themes/drawing-default.css +35 -0
- package/dist/lib/evg/showcase/themes/more-default.css +128 -0
- package/dist/lib/evg/showcase/themes/plots-default.css +18 -0
- package/dist/lib/evg/showcase/themes/showcase.css +508 -0
- package/dist/lib/evg/showcase/themes/tables-default.css +123 -0
- package/dist/lib/evg/showcase/themes/variants-default.css +15 -0
- package/dist/lib/evg/showcase/themes/views-default.css +15 -0
- package/dist/lib/evg/tools/bench_vs_potrace.mjs +290 -0
- package/dist/lib/evg/tools/evg_image_tool.rgr +488 -0
- package/dist/lib/evg/tools/evg_trace_bench.rgr +114 -0
- package/dist/lib/evg/tools/evg_trace_cli.rgr +662 -0
- package/dist/lib/evg/tools/evg_trace_cpp_bench.rgr +83 -0
- package/dist/lib/evg/tools/run_trace_bench.sh +74 -0
- package/dist/lib/evg/tools/run_trace_cli_smoke.sh +128 -0
- package/dist/lib/evg/web/responsive/EvgResponsiveCheck.rgr +271 -0
- package/dist/lib/evg/web/responsive/EvgResponsiveDemo.rgr +531 -0
- package/dist/lib/evg/web/responsive/README.md +97 -0
- package/dist/lib/evg/web/responsive/build.mjs +90 -0
- package/dist/lib/evg/web/responsive/dom-check.mjs +169 -0
- package/dist/lib/evg/web/responsive/index.html +177 -0
- package/dist/lib/evg/web/responsive/smoke.mjs +221 -0
- package/dist/lib/evg/web/tools/assets-client.mjs +95 -0
- package/dist/lib/evg/web/tools/boot-bench.mjs +189 -0
- package/dist/lib/evg/web/tools/inline-assets.mjs +119 -0
- package/dist/lib/evg/web/tools/minify.mjs +60 -0
- package/dist/lib/evg/web/tracer/build.mjs +87 -0
- package/dist/lib/evg/web/tracer/index.html +2805 -0
- package/dist/lib/evg/web/tracer/sample.png +0 -0
- package/dist/lib/evg/web/tracer/smoke.mjs +1162 -0
- package/dist/lib/evgr/Cargo.lock +21 -0
- package/dist/lib/evgr/Cargo.toml +19 -0
- package/dist/lib/evgr/README.md +140 -0
- package/dist/lib/evgr/bench/NativeBench.rgr +246 -0
- package/dist/lib/evgr/bench/compare.mjs +252 -0
- package/dist/lib/evgr/bench/speed.mjs +277 -0
- package/dist/lib/evgr/bin/.gitignore +3 -0
- package/dist/lib/evgr/src/bin/bench.rs +106 -0
- package/dist/lib/evgr/src/bin/smoke.rs +13 -0
- package/dist/lib/evgr/src/grid.rs +730 -0
- package/dist/lib/evgr/src/lib.rs +1003 -0
- package/dist/lib/evgr/src/style.rs +468 -0
- package/dist/lib/evgr/src/text.rs +83 -0
- package/dist/lib/image/BitReader.rgr +171 -0
- package/dist/lib/image/Buffer.rgr +173 -0
- package/dist/lib/image/DCT.rgr +283 -0
- package/dist/lib/image/Deflate.rgr +339 -0
- package/dist/lib/image/HuffmanDecoder.rgr +181 -0
- package/dist/lib/image/ImageBuffer.rgr +706 -0
- package/dist/lib/image/JPEGDecoder.rgr +808 -0
- package/dist/lib/image/PNGDecoder.rgr +544 -0
- package/dist/lib/image/PNGEncoder.rgr +388 -0
- package/dist/lib/image/PPMImage.rgr +224 -0
- package/dist/lib/image/ProgressiveJPEGDecoder.rgr +1154 -0
- package/dist/lib/image/README.md +33 -0
- package/dist/lib/image/RasterBuffer.rgr +294 -0
- package/dist/lib/image/VP8BoolDecoder.rgr +163 -0
- package/dist/lib/image/WebPDecoder.rgr +433 -0
- package/dist/lib/image/WebPLossless.rgr +1035 -0
- package/dist/lib/image/WebPLossy.rgr +1929 -0
- package/dist/lib/image/ranger.json +9 -0
- package/dist/lib/image/testdata/webp/anim_2f_24x16.webp +0 -0
- package/dist/lib/image/testdata/webp/fixtures.txt +33 -0
- package/dist/lib/image/testdata/webp/gen_fixtures.py +261 -0
- package/dist/lib/image/testdata/webp/ll_1x1.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_alpha_29x21.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_gradient_64x64.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_meta_32x32.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_noise_17x9.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal11_23x11.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal2_19x7.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal40_16x16.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal4_13x5.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_photo_40x30.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_predictors_64x32.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_1x1.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f0_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f1_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f2_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f3_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f0_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f1_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f2_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f3_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alpha_33x17.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_exif_15x11.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_i16_96x80.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_nofilter_24x20.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_odd_17x9.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_photo_48x40.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_q100_20x18.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_sharp_40x36.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_simple_filter_33x31.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_vpx_parts8_37x45.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_vpx_skip_96x80.webp +0 -0
- package/dist/lib/image/tests/WebPDecodeTool.rgr +81 -0
- package/dist/lib/image/tests/WebPDecoderTest.rgr +1162 -0
- package/dist/lib/image/tests/run_webp_tests.sh +39 -0
- package/dist/lib/rust/RsJson.rgr +468 -0
- package/dist/lib/rust/RsPrelude.rgr +1663 -0
- package/dist/lib/shell_test.rgr +7 -7
- package/dist/lib/stdlib.rgr +21 -5
- package/dist/lib/zip/ranger.json +6 -0
- package/dist/rgrc.js +87318 -67610
- package/package.json +1010 -1040
- package/dist/README.md +0 -117
- 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
|
+
}
|