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,566 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* evg-a11y.js — the accessibility tree, as DOM, over the canvas.
|
|
3
|
+
*
|
|
4
|
+
* A screen reader does not look at pixels. It asks the browser for the tree it
|
|
5
|
+
* built out of the DOM, and a <canvas> contributes one empty graphic to that
|
|
6
|
+
* tree no matter what was drawn into it. So the app publishes what the frame
|
|
7
|
+
* MEANS — `EVGA11yTree`, built in Ranger beside the display list — and this
|
|
8
|
+
* file turns it into real DOM elements positioned over the canvas, with the
|
|
9
|
+
* roles, names and states the tree carries.
|
|
10
|
+
*
|
|
11
|
+
* GridApp ─┬─ sceneJson() ─► evg-webgl.js ─► pixels
|
|
12
|
+
* └─ a11yJson() ─► THIS FILE ─► DOM ─► NVDA / VoiceOver / Orca
|
|
13
|
+
*
|
|
14
|
+
* Google Sheets does the same thing for the same reason. There is no other
|
|
15
|
+
* approach that works: `role="application"` plus an announcement region gives a
|
|
16
|
+
* reader nothing to explore, and nothing to put a braille cursor on.
|
|
17
|
+
*
|
|
18
|
+
* What the elements look like
|
|
19
|
+
* ---------------------------
|
|
20
|
+
* They are transparent rather than hidden. `display:none` and
|
|
21
|
+
* `visibility:hidden` remove a node from the accessibility tree, which is the
|
|
22
|
+
* one thing being built here, and `opacity:0` is treated as hidden by some
|
|
23
|
+
* combinations. Transparent ink over a canvas that already drew the same text
|
|
24
|
+
* is visible to the accessibility API and invisible to the eye.
|
|
25
|
+
*
|
|
26
|
+
* They are positioned at their real rectangles, because a rectangle is not
|
|
27
|
+
* decoration: touch exploration on a tablet, a screen magnifier following the
|
|
28
|
+
* cursor, and "route the mouse to the focus" all use it.
|
|
29
|
+
*
|
|
30
|
+
* The container takes no pointer events, so the mouse keeps going to the canvas
|
|
31
|
+
* and the app's own hit testing stays the only hit testing. A reader ACTIVATING
|
|
32
|
+
* an element still arrives as a click on that element — and this file answers it
|
|
33
|
+
* by pressing the app where the element is. That is deliberately not a mapping
|
|
34
|
+
* table from node ids to commands: there is nothing to keep in step with the
|
|
35
|
+
* app, and a button that moved is still pressed in the right place.
|
|
36
|
+
*
|
|
37
|
+
* Focus
|
|
38
|
+
* -----
|
|
39
|
+
* The app owns it. Whatever `tree.focus` names is the only element with
|
|
40
|
+
* `tabindex="0"` — the roving tabindex an ARIA grid is built on — and this file
|
|
41
|
+
* calls `.focus()` on it when it changes. It never tells the app that focus
|
|
42
|
+
* moved, because a mirror that reports focus back gets into a loop with the app
|
|
43
|
+
* that is trying to set it.
|
|
44
|
+
*
|
|
45
|
+
* Updates
|
|
46
|
+
* -------
|
|
47
|
+
* Elements are kept in a map by node id and reused. That is what makes the ids
|
|
48
|
+
* matter: rebuilding the subtree under a reader's cursor loses the cursor, and
|
|
49
|
+
* the ids the tree carries are derived from identity — `sheet/cell:B7` — so a
|
|
50
|
+
* repaint, a scroll and a recalculation all leave the same element in place.
|
|
51
|
+
* A tree whose `gen` has not changed is skipped entirely.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
const HIDDEN_ROLES = new Set(["none", "presentation"]);
|
|
55
|
+
|
|
56
|
+
// Roles whose own text IS the content, so the element carries it as text and
|
|
57
|
+
// takes no aria-label — a label would override the content and read instead of
|
|
58
|
+
// it. The rest get an aria-label and are containers for their children.
|
|
59
|
+
const CONTENT_ROLES = new Set([
|
|
60
|
+
"text", "heading", "button", "gridcell", "columnheader", "rowheader",
|
|
61
|
+
"checkbox", "radio", "tab", "menuitem", "menuitemcheckbox", "menuitemradio",
|
|
62
|
+
"listitem", "link",
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
// A native <button> is better understood by every screen reader than a div with
|
|
66
|
+
// role="button", and it is activatable without any extra key handling. A
|
|
67
|
+
// textbox is a native <input> for the same reason, and for one more: it is
|
|
68
|
+
// the element the text-input bridge edits in. A div with role="textbox" and
|
|
69
|
+
// the value as its text is a textbox a reader can read and not type into —
|
|
70
|
+
// measured with the RealTrainer port: the reader found the field, Tab could
|
|
71
|
+
// not leave it for the send button, and the mouse could not select in it.
|
|
72
|
+
function elementFor(role) {
|
|
73
|
+
if (role === "button") return document.createElement("button");
|
|
74
|
+
if (role === "textbox") {
|
|
75
|
+
const input = document.createElement("input");
|
|
76
|
+
input.type = "text";
|
|
77
|
+
input.autocapitalize = "off";
|
|
78
|
+
input.autocomplete = "off";
|
|
79
|
+
input.spellcheck = false;
|
|
80
|
+
return input;
|
|
81
|
+
}
|
|
82
|
+
return document.createElement("div");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* NOTHING HERE MAY PAINT. The canvas underneath already drew all of it, so a
|
|
87
|
+
* mirror node that puts one pixel on the screen is a second copy of the page
|
|
88
|
+
* — and, because the mirror is rebuilt on its own schedule rather than every
|
|
89
|
+
* frame, a second copy AT AN OLDER SCROLL OFFSET. Two pages superimposed.
|
|
90
|
+
*
|
|
91
|
+
* `color: transparent` is not enough to promise that, and the gap is exactly
|
|
92
|
+
* where it hurts: the mirror makes NATIVE controls, because a real <button>
|
|
93
|
+
* and a real <input> are what readers understand and what the text bridge
|
|
94
|
+
* edits in — and a native control on iOS is painted through
|
|
95
|
+
* `-webkit-text-fill-color` and `-webkit-appearance`, neither of which
|
|
96
|
+
* `color` reaches. Everything a UA can decide to draw is turned off here by
|
|
97
|
+
* name, and `evg:a11y:paint` holds the list.
|
|
98
|
+
*/
|
|
99
|
+
export function styleBase(el) {
|
|
100
|
+
const s = el.style;
|
|
101
|
+
s.position = "absolute";
|
|
102
|
+
s.margin = "0";
|
|
103
|
+
s.padding = "0";
|
|
104
|
+
s.border = "0";
|
|
105
|
+
s.background = "transparent";
|
|
106
|
+
s.color = "transparent";
|
|
107
|
+
s.font = "inherit";
|
|
108
|
+
// SIXTEEN PIXELS, AND IT IS NOT A DESIGN CHOICE. Nothing here paints, so
|
|
109
|
+
// the size is invisible — but iOS Safari zooms the whole page in when a
|
|
110
|
+
// text field it is focusing computes to less than 16px, and the mirror's
|
|
111
|
+
// `<input>` is what a tap on a drawn field focuses. The page then sat at
|
|
112
|
+
// some magnification the person had to pinch their way out of, over a
|
|
113
|
+
// canvas that used to refuse the pinch as well.
|
|
114
|
+
//
|
|
115
|
+
// It costs nothing: the node is clipped by `overflow: hidden` below and its
|
|
116
|
+
// text is transparent either way. Anyone tidying this back to 10px would
|
|
117
|
+
// bring the zoom back with it, which is why `evg:a11y:paint` holds it.
|
|
118
|
+
s.fontSize = "16px";
|
|
119
|
+
s.lineHeight = "1";
|
|
120
|
+
s.overflow = "hidden";
|
|
121
|
+
s.whiteSpace = "pre";
|
|
122
|
+
s.outline = "none";
|
|
123
|
+
// The three a native control paints itself with, which `color` does not
|
|
124
|
+
// reach: the fill iOS uses for button and input text, the platform chrome
|
|
125
|
+
// it would draw around them, and the caret that blinks in a text field
|
|
126
|
+
// even when its text is invisible.
|
|
127
|
+
s.webkitTextFillColor = "transparent";
|
|
128
|
+
s.webkitAppearance = "none";
|
|
129
|
+
s.appearance = "none";
|
|
130
|
+
s.caretColor = "transparent";
|
|
131
|
+
// And the two that paint outside the box, so they are not clipped away by
|
|
132
|
+
// `overflow: hidden` above.
|
|
133
|
+
s.textShadow = "none";
|
|
134
|
+
s.boxShadow = "none";
|
|
135
|
+
// The canvas underneath already drew all of this. Nothing here should paint,
|
|
136
|
+
// and nothing here should take a click away from it.
|
|
137
|
+
s.pointerEvents = "none";
|
|
138
|
+
s.userSelect = "none";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function setAttr(el, name, value) {
|
|
142
|
+
if (value === null || value === undefined || value === "") {
|
|
143
|
+
if (el.hasAttribute(name)) el.removeAttribute(name);
|
|
144
|
+
} else if (el.getAttribute(name) !== String(value)) {
|
|
145
|
+
el.setAttribute(name, String(value));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const TRI = { 1: "false", 2: "true", 3: "mixed" };
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Create a mirror over a canvas.
|
|
153
|
+
*
|
|
154
|
+
* host element that positions the canvas; the mirror is appended to it
|
|
155
|
+
* and must sit exactly over the canvas, so the host needs
|
|
156
|
+
* `position: relative` and no padding of its own.
|
|
157
|
+
* canvas hidden from the accessibility tree, since the mirror replaces it
|
|
158
|
+
* onActivate(node) a reader pressed something. Default: press the app at the
|
|
159
|
+
* node's centre.
|
|
160
|
+
* scale app pixels → CSS pixels, when the canvas is not drawn 1:1
|
|
161
|
+
*/
|
|
162
|
+
/**
|
|
163
|
+
* @param {object} opts
|
|
164
|
+
* @param {"roving"|"all"} [opts.tabbable] "roving" (the default, and what an
|
|
165
|
+
* ARIA grid wants): exactly one element is tabbable, the one the app
|
|
166
|
+
* says has focus. "all": every focusable node is a tab stop in tree
|
|
167
|
+
* order — a page of buttons and fields, where Tab has to walk from the
|
|
168
|
+
* field to the send button beside it.
|
|
169
|
+
* @param {(node: object) => void} [opts.onFocus] Focus moved inside the
|
|
170
|
+
* mirror by the reader or the keyboard — not by the app. Reported so
|
|
171
|
+
* the app can follow; the mirror never reports the focus it set itself.
|
|
172
|
+
*/
|
|
173
|
+
export function createA11yMirror(host, { canvas, onActivate, onFocus, canMoveFocus, scale = 1, label = "Application", tabbable = "roving" } = {}) {
|
|
174
|
+
const root = document.createElement("div");
|
|
175
|
+
root.className = "evg-a11y";
|
|
176
|
+
root.style.position = "absolute";
|
|
177
|
+
root.style.left = "0";
|
|
178
|
+
root.style.top = "0";
|
|
179
|
+
root.style.right = "0";
|
|
180
|
+
root.style.bottom = "0";
|
|
181
|
+
root.style.overflow = "hidden";
|
|
182
|
+
root.style.pointerEvents = "none";
|
|
183
|
+
host.appendChild(root);
|
|
184
|
+
|
|
185
|
+
if (canvas) {
|
|
186
|
+
// The canvas is scenery now. Leaving it in the accessibility tree gives the
|
|
187
|
+
// reader an empty graphic beside a full tree of real nodes.
|
|
188
|
+
canvas.setAttribute("aria-hidden", "true");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const els = new Map(); // node id → element
|
|
192
|
+
let lastGen = -1;
|
|
193
|
+
let lastFocus = null;
|
|
194
|
+
let settingFocus = false;
|
|
195
|
+
// The tab stop held open while the app names no focus — see `update`.
|
|
196
|
+
let entryStop = null;
|
|
197
|
+
|
|
198
|
+
function activate(node) {
|
|
199
|
+
if (onActivate) onActivate(node);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function ensure(node) {
|
|
203
|
+
let entry = els.get(node.id);
|
|
204
|
+
// A node that changed role cannot keep its element: a <button> does not
|
|
205
|
+
// become a gridcell, and a reader would keep the old one's semantics.
|
|
206
|
+
if (entry && entry.role !== node.role) {
|
|
207
|
+
entry.el.remove();
|
|
208
|
+
entry = null;
|
|
209
|
+
}
|
|
210
|
+
if (!entry) {
|
|
211
|
+
const el = elementFor(node.role);
|
|
212
|
+
styleBase(el);
|
|
213
|
+
if (node.role === "button") {
|
|
214
|
+
el.type = "button";
|
|
215
|
+
el.tabIndex = -1;
|
|
216
|
+
}
|
|
217
|
+
if (node.role === "textbox") {
|
|
218
|
+
el.tabIndex = -1;
|
|
219
|
+
// The bridge takes the pointer while it edits here; until then the
|
|
220
|
+
// canvas keeps every hit test, as for every other element.
|
|
221
|
+
el.style.pointerEvents = "none";
|
|
222
|
+
}
|
|
223
|
+
el.dataset.a11yId = node.id;
|
|
224
|
+
el.addEventListener("click", (ev) => {
|
|
225
|
+
ev.preventDefault();
|
|
226
|
+
// The elements are NESTED — a row is inside its menu is inside the
|
|
227
|
+
// bar — so a click that keeps bubbling reaches every ancestor's
|
|
228
|
+
// listener too, and each one presses the app at ITS OWN centre. One
|
|
229
|
+
// activation on a menu row then became three: the row, the menu
|
|
230
|
+
// behind it, and the page behind that.
|
|
231
|
+
ev.stopPropagation();
|
|
232
|
+
const n = els.get(node.id);
|
|
233
|
+
if (n) activate(n.node);
|
|
234
|
+
});
|
|
235
|
+
entry = { el, role: node.role, node };
|
|
236
|
+
els.set(node.id, entry);
|
|
237
|
+
}
|
|
238
|
+
entry.node = node;
|
|
239
|
+
return entry;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function applyAria(el, node, byId) {
|
|
243
|
+
const role = node.role;
|
|
244
|
+
// A native <button> already has the role; anything else is a div and needs
|
|
245
|
+
// to be told what it is.
|
|
246
|
+
if (el.tagName !== "BUTTON") setAttr(el, "role", role);
|
|
247
|
+
|
|
248
|
+
const content = CONTENT_ROLES.has(role);
|
|
249
|
+
if (role === "textbox") {
|
|
250
|
+
// A field's label and its contents are two different strings, and a
|
|
251
|
+
// reader announces both: "Formula, equals SUM open paren…". The value
|
|
252
|
+
// is the input's own; while the input has focus the bridge is writing
|
|
253
|
+
// it and the app is mirroring it, so it is left alone.
|
|
254
|
+
setAttr(el, "aria-label", node.name);
|
|
255
|
+
const text = node.value || "";
|
|
256
|
+
if (document.activeElement !== el && el.value !== text) el.value = text;
|
|
257
|
+
} else if (role === "status") {
|
|
258
|
+
const text = node.value || node.name || "";
|
|
259
|
+
if (el.textContent !== text) el.textContent = text;
|
|
260
|
+
setAttr(el, "aria-label", null);
|
|
261
|
+
} else if (content) {
|
|
262
|
+
const text = node.name || "";
|
|
263
|
+
if (el.textContent !== text) el.textContent = text;
|
|
264
|
+
setAttr(el, "aria-label", null);
|
|
265
|
+
} else {
|
|
266
|
+
setAttr(el, "aria-label", node.name);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
setAttr(el, "aria-description", node.desc);
|
|
270
|
+
// Some controls are a known role doing an unfamiliar job. A sortable row
|
|
271
|
+
// is a `button`, and a reader told only that learns nothing about being
|
|
272
|
+
// able to move it.
|
|
273
|
+
setAttr(el, "aria-roledescription", node.roledesc);
|
|
274
|
+
// Open or closed. A trigger that never says which is a trigger a reader
|
|
275
|
+
// cannot tell has already been pressed.
|
|
276
|
+
setAttr(el, "aria-expanded", node.expanded ? TRI[node.expanded] || null : null);
|
|
277
|
+
// What it opens. The tree has carried `haspopup` ("menu", "listbox",
|
|
278
|
+
// "dialog") and serialised it all along; nothing here read it, so a select
|
|
279
|
+
// trigger or a date picker's button reached a reader as a plain button
|
|
280
|
+
// that happened to expand.
|
|
281
|
+
setAttr(el, "aria-haspopup", node.haspopup || null);
|
|
282
|
+
setAttr(el, "aria-disabled", node.disabled ? "true" : null);
|
|
283
|
+
// A native button that is disabled is not a tab stop and not activatable,
|
|
284
|
+
// which is what the drawn one is.
|
|
285
|
+
if (el.tagName === "BUTTON") el.disabled = !!node.disabled;
|
|
286
|
+
if (tabbable === "all" && !(el.tagName === "BUTTON" && node.disabled)) {
|
|
287
|
+
const stop = node.focusable ? 0 : -1;
|
|
288
|
+
if (el.tabIndex !== stop) el.tabIndex = stop;
|
|
289
|
+
}
|
|
290
|
+
// Three states, passed STRAIGHT THROUGH: "true", "false", or absent.
|
|
291
|
+
// `node.readonly ? "true" : null` was right while the field was a boolean
|
|
292
|
+
// and became a bug the moment it became a string — "false" is truthy in
|
|
293
|
+
// JavaScript, so a field that had been asked and answered no would have
|
|
294
|
+
// been mirrored as yes. Absent and "false" are different claims, which is
|
|
295
|
+
// the entire reason these are strings.
|
|
296
|
+
setAttr(el, "aria-readonly", node.readonly || null);
|
|
297
|
+
setAttr(el, "aria-required", node.required || null);
|
|
298
|
+
setAttr(el, "aria-invalid", node.invalid || null);
|
|
299
|
+
setAttr(el, "aria-modal", node.modal ? "true" : null);
|
|
300
|
+
// A tab says "false" out loud when it is not the selected one, as Radix's
|
|
301
|
+
// (and the APG's) do; elsewhere an unselected node carries nothing.
|
|
302
|
+
setAttr(el, "aria-selected", node.selected ? "true" : node.role === "tab" ? "false" : null);
|
|
303
|
+
// A toggle button that says so in its own field. `node.pressed` has been
|
|
304
|
+
// in the tree and in the serialisation all along and NOTHING read it here:
|
|
305
|
+
// the only route to `aria-pressed` was through `checked`, so a control
|
|
306
|
+
// that set `pressed` honestly got nothing. Checked first, because a
|
|
307
|
+
// checkbox mirrored as a toggle would be worse than either.
|
|
308
|
+
if (!node.checked && node.pressed) {
|
|
309
|
+
setAttr(el, "aria-pressed", role === "button" ? TRI[node.pressed] || null : null);
|
|
310
|
+
setAttr(el, "aria-checked", null);
|
|
311
|
+
} else if (node.checked) {
|
|
312
|
+
const state = TRI[node.checked] || null;
|
|
313
|
+
if (role === "button") {
|
|
314
|
+
setAttr(el, "aria-pressed", state);
|
|
315
|
+
setAttr(el, "aria-checked", null);
|
|
316
|
+
} else {
|
|
317
|
+
setAttr(el, "aria-checked", state);
|
|
318
|
+
setAttr(el, "aria-pressed", null);
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
setAttr(el, "aria-pressed", null);
|
|
322
|
+
setAttr(el, "aria-checked", null);
|
|
323
|
+
}
|
|
324
|
+
// A number the reader announces. `now` is deliberately absent on an
|
|
325
|
+
// indeterminate progress bar: a range with no position in it is how every
|
|
326
|
+
// platform spells "busy, length unknown", and writing 0 there instead
|
|
327
|
+
// would have the reader say the work has not started.
|
|
328
|
+
setAttr(el, "aria-valuenow", node.now == null ? null : node.now);
|
|
329
|
+
setAttr(el, "aria-valuemin", node.min == null ? null : node.min);
|
|
330
|
+
setAttr(el, "aria-valuemax", node.max == null ? null : node.max);
|
|
331
|
+
// Virtualization: the counts are the whole sheet, the indices say where the
|
|
332
|
+
// emitted rows sit in it.
|
|
333
|
+
setAttr(el, "aria-rowcount", node.rows || null);
|
|
334
|
+
setAttr(el, "aria-colcount", node.cols || null);
|
|
335
|
+
setAttr(el, "aria-rowindex", node.row || null);
|
|
336
|
+
setAttr(el, "aria-colindex", node.col || null);
|
|
337
|
+
setAttr(el, "aria-posinset", node.pos || null);
|
|
338
|
+
setAttr(el, "aria-setsize", node.size || null);
|
|
339
|
+
// A flat tree carries its nesting HERE and nowhere else: without it a
|
|
340
|
+
// reader announces a tree as a list of siblings all at the same depth.
|
|
341
|
+
setAttr(el, "aria-level", node.level || null);
|
|
342
|
+
// What a key MEANS on a splitter, and the only thing that tells a
|
|
343
|
+
// horizontal one from a vertical one — every other observation about the
|
|
344
|
+
// two is identical.
|
|
345
|
+
setAttr(el, "aria-orientation", node.orientation || null);
|
|
346
|
+
setAttr(el, "aria-current", node.current || null);
|
|
347
|
+
setAttr(el, "aria-live", node.live === 2 ? "assertive" : node.live === 1 ? "polite" : null);
|
|
348
|
+
|
|
349
|
+
const b = node.b || [0, 0, 0, 0];
|
|
350
|
+
const parent = hostNode(node, byId);
|
|
351
|
+
const pb = parent ? parent.b || [0, 0, 0, 0] : [0, 0, 0, 0];
|
|
352
|
+
// Relative to the element this one is placed IN — see `hostNode` — because
|
|
353
|
+
// an absolutely positioned ancestor is the containing block for everything
|
|
354
|
+
// under it.
|
|
355
|
+
const left = (b[0] - pb[0]) * scale;
|
|
356
|
+
const top = (b[1] - pb[1]) * scale;
|
|
357
|
+
const w = Math.max(0, b[2] * scale);
|
|
358
|
+
const h = Math.max(0, b[3] * scale);
|
|
359
|
+
const s = el.style;
|
|
360
|
+
if (s.left !== left + "px") s.left = left + "px";
|
|
361
|
+
if (s.top !== top + "px") s.top = top + "px";
|
|
362
|
+
if (s.width !== w + "px") s.width = w + "px";
|
|
363
|
+
if (s.height !== h + "px") s.height = h + "px";
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Hide everything a modal dialog is covering. A reader that can still walk
|
|
367
|
+
* the sheet behind an open dialog will walk it, and none of those keys are
|
|
368
|
+
* going anywhere. */
|
|
369
|
+
function applyModal(tree, byId) {
|
|
370
|
+
const modal = tree.nodes.find((n) => n.modal);
|
|
371
|
+
const keep = new Set();
|
|
372
|
+
if (modal) {
|
|
373
|
+
let at = modal;
|
|
374
|
+
while (at) {
|
|
375
|
+
keep.add(at.id);
|
|
376
|
+
at = at.p ? byId.get(at.p) : null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Only the top-level regions are hidden or shown: hiding deeper would have
|
|
380
|
+
// to be undone node by node, and hiding a subtree the dialog is inside of
|
|
381
|
+
// would hide the dialog.
|
|
382
|
+
for (const n of tree.nodes) {
|
|
383
|
+
if (n.p !== tree.root) continue;
|
|
384
|
+
const entry = els.get(n.id);
|
|
385
|
+
if (!entry) continue;
|
|
386
|
+
const hide = modal && !keep.has(n.id);
|
|
387
|
+
setAttr(entry.el, "aria-hidden", hide ? "true" : null);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// The node whose element holds this one's. Usually the parent; but a
|
|
392
|
+
// textbox is a native <input>, which can hold no children, so a node under
|
|
393
|
+
// one — a password field's show/hide button, drawn inside the field's box —
|
|
394
|
+
// goes under the nearest ancestor that can hold it, and is placed relative
|
|
395
|
+
// to that one's box. Appended into the <input> it had no box at all: the
|
|
396
|
+
// reader was told of a button that was nowhere, and a pointer aimed at its
|
|
397
|
+
// rectangle landed at the page's corner.
|
|
398
|
+
function hostNode(node, byId) {
|
|
399
|
+
let at = node.p ? byId.get(node.p) : null;
|
|
400
|
+
while (at && at.role === "textbox") at = at.p ? byId.get(at.p) : null;
|
|
401
|
+
return at;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function update(tree) {
|
|
405
|
+
if (!tree || !tree.nodes) return { skipped: true };
|
|
406
|
+
if (tree.gen === lastGen && tree.focus === lastFocus) return { skipped: true };
|
|
407
|
+
lastGen = tree.gen;
|
|
408
|
+
|
|
409
|
+
const byId = new Map();
|
|
410
|
+
for (const n of tree.nodes) byId.set(n.id, n);
|
|
411
|
+
|
|
412
|
+
const seen = new Set();
|
|
413
|
+
for (const node of tree.nodes) {
|
|
414
|
+
if (HIDDEN_ROLES.has(node.role)) continue;
|
|
415
|
+
seen.add(node.id);
|
|
416
|
+
const entry = ensure(node);
|
|
417
|
+
applyAria(entry.el, node, byId);
|
|
418
|
+
// Re-parent only when it actually changed: moving an element the reader
|
|
419
|
+
// is sitting on drops its cursor.
|
|
420
|
+
const host = hostNode(node, byId);
|
|
421
|
+
const parentEl = host ? (els.get(host.id) || {}).el : root;
|
|
422
|
+
const want = parentEl || root;
|
|
423
|
+
if (entry.el.parentNode !== want) want.appendChild(entry.el);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
for (const [id, entry] of els) {
|
|
427
|
+
if (!seen.has(id)) {
|
|
428
|
+
entry.el.remove();
|
|
429
|
+
els.delete(id);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// `aria-describedby`, once every element exists: the message a field
|
|
434
|
+
// points at is usually built AFTER the field. The target gets a DOM id
|
|
435
|
+
// derived from its node id; the resolved `aria-description` is dropped
|
|
436
|
+
// when the reference resolves, or a reader would say the sentence twice.
|
|
437
|
+
// A reference to a node that is not in the mirror falls back to the text.
|
|
438
|
+
for (const node of tree.nodes) {
|
|
439
|
+
const entry = els.get(node.id);
|
|
440
|
+
if (!entry) continue;
|
|
441
|
+
const target = node.describedby ? els.get(node.describedby) : null;
|
|
442
|
+
if (target) {
|
|
443
|
+
const domId = "evg-a11y-" + String(node.describedby).replace(/[^A-Za-z0-9_-]/g, "_");
|
|
444
|
+
if (target.el.id !== domId) target.el.id = domId;
|
|
445
|
+
setAttr(entry.el, "aria-describedby", domId);
|
|
446
|
+
setAttr(entry.el, "aria-description", null);
|
|
447
|
+
} else {
|
|
448
|
+
setAttr(entry.el, "aria-describedby", null);
|
|
449
|
+
}
|
|
450
|
+
// `aria-controls` and `aria-labelledby` resolve the same way: a tab
|
|
451
|
+
// names its panel, the panel is named by its tab. A reference to a node
|
|
452
|
+
// that is not in the mirror (an inactive panel) is left off rather than
|
|
453
|
+
// pointing at nothing, which axe reports as a broken reference.
|
|
454
|
+
for (const [key, attr] of [["controls", "aria-controls"], ["labelledby", "aria-labelledby"]]) {
|
|
455
|
+
const ref = node[key];
|
|
456
|
+
const to = ref ? els.get(ref) : null;
|
|
457
|
+
if (to) {
|
|
458
|
+
const domId = "evg-a11y-" + String(ref).replace(/[^A-Za-z0-9_-]/g, "_");
|
|
459
|
+
if (to.el.id !== domId) to.el.id = domId;
|
|
460
|
+
setAttr(entry.el, attr, domId);
|
|
461
|
+
} else {
|
|
462
|
+
setAttr(entry.el, attr, null);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
applyModal(tree, byId);
|
|
468
|
+
|
|
469
|
+
// Roving tabindex: exactly one element in the whole mirror is tabbable, and
|
|
470
|
+
// it is the one the app says has focus. With every focusable a tab stop
|
|
471
|
+
// the app's focus is still followed, only nothing is taken away.
|
|
472
|
+
if (tabbable !== "all" && tree.focus !== lastFocus) {
|
|
473
|
+
const prev = lastFocus ? els.get(lastFocus) : null;
|
|
474
|
+
if (prev) prev.el.tabIndex = -1;
|
|
475
|
+
}
|
|
476
|
+
const focused = tree.focus ? els.get(tree.focus) : null;
|
|
477
|
+
if (focused) {
|
|
478
|
+
if (tabbable !== "all") focused.el.tabIndex = 0;
|
|
479
|
+
// `canMoveFocus` is how a host that parks the keyboard somewhere else —
|
|
480
|
+
// a text-input bridge editing in its own transparent <input> — says so.
|
|
481
|
+
// The tab stop still moves; only the `.focus()` is withheld, because two
|
|
482
|
+
// elements calling `.focus()` for one field take it off each other.
|
|
483
|
+
if (document.activeElement !== focused.el && (!canMoveFocus || canMoveFocus(focused.node))) {
|
|
484
|
+
settingFocus = true;
|
|
485
|
+
try {
|
|
486
|
+
focused.el.focus({ preventScroll: true });
|
|
487
|
+
} finally {
|
|
488
|
+
settingFocus = false;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
lastFocus = tree.focus;
|
|
493
|
+
|
|
494
|
+
// THE WAY IN. An app that has not been touched yet has nothing focused,
|
|
495
|
+
// and a roving tabindex read literally then makes NO element tabbable —
|
|
496
|
+
// so Tab walks straight past the whole widget and there is no way to reach
|
|
497
|
+
// it from the keyboard at all. That is not the roving pattern; a roving
|
|
498
|
+
// pattern always has one tab stop, and before anything is chosen it is the
|
|
499
|
+
// first focusable node. Once the app names a focus this does nothing.
|
|
500
|
+
if (tabbable !== "all") {
|
|
501
|
+
if (!focused) {
|
|
502
|
+
if (entryStop && (!entryStop.isConnected || entryStop.tabIndex !== 0)) entryStop = null;
|
|
503
|
+
if (!entryStop) {
|
|
504
|
+
for (const node of tree.nodes) {
|
|
505
|
+
if (!node.focusable || node.disabled) continue;
|
|
506
|
+
const entry = els.get(node.id);
|
|
507
|
+
if (!entry) continue;
|
|
508
|
+
entry.el.tabIndex = 0;
|
|
509
|
+
entryStop = entry.el;
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
} else if (entryStop) {
|
|
514
|
+
if (entryStop !== focused.el && entryStop.isConnected) entryStop.tabIndex = -1;
|
|
515
|
+
entryStop = null;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return { nodes: tree.nodes.length, elements: els.size, gen: tree.gen };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function destroy() {
|
|
523
|
+
root.remove();
|
|
524
|
+
els.clear();
|
|
525
|
+
if (canvas) canvas.removeAttribute("aria-hidden");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Focus that moved on its own — Tab, or a reader's cursor — is reported.
|
|
529
|
+
// The app's own `.focus()` above is not: `settingFocus` is what tells them
|
|
530
|
+
// apart, and a mirror that reported its own focus back would loop with the
|
|
531
|
+
// app that is setting it.
|
|
532
|
+
root.addEventListener("focusin", (ev) => {
|
|
533
|
+
if (settingFocus || !onFocus) return;
|
|
534
|
+
const el = ev.target && ev.target.closest ? ev.target.closest("[data-a11y-id]") : null;
|
|
535
|
+
if (!el) return;
|
|
536
|
+
const entry = els.get(el.dataset.a11yId);
|
|
537
|
+
if (entry) onFocus(entry.node);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
root.setAttribute("aria-label", label);
|
|
541
|
+
return {
|
|
542
|
+
root,
|
|
543
|
+
update,
|
|
544
|
+
destroy,
|
|
545
|
+
/** The element a node is mirrored as, or null — the bridge edits in it. */
|
|
546
|
+
elementOf(id) {
|
|
547
|
+
const entry = els.get(id);
|
|
548
|
+
return entry ? entry.el : null;
|
|
549
|
+
},
|
|
550
|
+
get size() {
|
|
551
|
+
return els.size;
|
|
552
|
+
},
|
|
553
|
+
isSettingFocus() {
|
|
554
|
+
return settingFocus;
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** The default answer to "a reader pressed this": press the app where it is.
|
|
560
|
+
* No mapping table, and nothing to keep in step with the app's own commands. */
|
|
561
|
+
export function pressAtCentre(node, press) {
|
|
562
|
+
const b = node.b || [0, 0, 0, 0];
|
|
563
|
+
const cx = Math.round(b[0] + b[2] / 2);
|
|
564
|
+
const cy = Math.round(b[1] + b[3] / 2);
|
|
565
|
+
press(cx, cy);
|
|
566
|
+
}
|