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,1736 @@
|
|
|
1
|
+
; SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
Import "SVGPathParser.rgr"
|
|
4
|
+
Import "VectorShapes.rgr"
|
|
5
|
+
Import "VectorViewBox.rgr"
|
|
6
|
+
Import "EVGColor.rgr"
|
|
7
|
+
|
|
8
|
+
; ==============================================================================
|
|
9
|
+
; SvgParser.rgr — an SVG document, read into the vector layer
|
|
10
|
+
; ==============================================================================
|
|
11
|
+
; PLAN_VECTOR_IR.md Stage 4 and Stage 5.
|
|
12
|
+
;
|
|
13
|
+
; The plan's central decision is that EVG does not become an SVG engine: it has
|
|
14
|
+
; a renderer-neutral vector layer, and SVG is one importer into it. Stages 0-3
|
|
15
|
+
; built the layer — the shared viewBox transform, the full path grammar, the
|
|
16
|
+
; shared rasterizer, the basic shapes as paths. This file is the importer, and
|
|
17
|
+
; it is deliberately the only place in the tree that knows any XML.
|
|
18
|
+
;
|
|
19
|
+
; What comes out is not a node tree. It is a flat list of `SvgVectorItem` —
|
|
20
|
+
; resolved geometry plus resolved paint — because that is what every renderer
|
|
21
|
+
; can already draw:
|
|
22
|
+
;
|
|
23
|
+
; markup ──► SvgParser ──► [SvgVectorItem] ──┬──► PDF (path operators)
|
|
24
|
+
; ├──► HTML (inline <svg>)
|
|
25
|
+
; └──► raster (VectorRasterizer)
|
|
26
|
+
;
|
|
27
|
+
; Three consequences worth stating, because they are the point rather than
|
|
28
|
+
; side effects.
|
|
29
|
+
;
|
|
30
|
+
; **Transforms are baked.** Each item's commands are absolute, in the document's
|
|
31
|
+
; own user space, with every enclosing `transform` already applied. A renderer
|
|
32
|
+
; therefore needs no transform stack, no group traversal and no state machine —
|
|
33
|
+
; it draws a list. Affine maps take cubics to cubics exactly, so nothing is
|
|
34
|
+
; approximated by doing this.
|
|
35
|
+
;
|
|
36
|
+
; **Paint is resolved.** Inheritance down groups, `fill="none"`, the opacity
|
|
37
|
+
; properties: all settled here, once, so the three renderers cannot disagree
|
|
38
|
+
; about what colour a shape is the way they disagree about gradients today
|
|
39
|
+
; (§6).
|
|
40
|
+
;
|
|
41
|
+
; **HTML goes through the IR too.** It would be less code to hand the original
|
|
42
|
+
; markup to the browser and let it render — and it would immediately reintroduce
|
|
43
|
+
; the whole class of defect this plan exists to remove, because the browser
|
|
44
|
+
; would draw filters, gradients, text and clip paths that PDF and PNG cannot.
|
|
45
|
+
; Out of profile has to mean out of profile everywhere, or the profile is not
|
|
46
|
+
; real.
|
|
47
|
+
;
|
|
48
|
+
; The profile is §8: no script, no animation, no foreignObject, no filters, no
|
|
49
|
+
; masks, no external CSS, no external references. Everything outside it is
|
|
50
|
+
; *reported* rather than silently dropped (§7.4) — that is what makes a small
|
|
51
|
+
; profile safe to ship, because "not supported" is a state you can see instead
|
|
52
|
+
; of a shape that quietly went missing.
|
|
53
|
+
;
|
|
54
|
+
; Imported SVG is untrusted input, so the parser also refuses by construction
|
|
55
|
+
; rather than by mitigation: no entity machinery exists at all (which is what
|
|
56
|
+
; rules out billion-laughs and XXE), no href leaves the document, and node,
|
|
57
|
+
; depth and command counts are capped and reported.
|
|
58
|
+
; ==============================================================================
|
|
59
|
+
|
|
60
|
+
; ------------------------------------------------------------------------------
|
|
61
|
+
; SvgVectorItem — one drawable, ready to render.
|
|
62
|
+
; ------------------------------------------------------------------------------
|
|
63
|
+
; `commands` are absolute PathCommands in the document's user space. An unset
|
|
64
|
+
; fill or stroke colour means that operation does not happen at all — the same
|
|
65
|
+
; convention EVGElement already uses, so a renderer's existing "is there a fill?"
|
|
66
|
+
; check works unchanged.
|
|
67
|
+
class SvgVectorItem {
|
|
68
|
+
def commands:[PathCommand]
|
|
69
|
+
def fillColor:EVGColor
|
|
70
|
+
def strokeColor:EVGColor
|
|
71
|
+
def strokeWidth:double 1.0
|
|
72
|
+
def fillRule:string "nonzero"
|
|
73
|
+
def dashArray:string ""
|
|
74
|
+
def dashOffset:double 0.0
|
|
75
|
+
|
|
76
|
+
Constructor () {
|
|
77
|
+
def c:[PathCommand]
|
|
78
|
+
commands = c
|
|
79
|
+
fillColor = (EVGColor.noColor())
|
|
80
|
+
strokeColor = (EVGColor.noColor())
|
|
81
|
+
strokeWidth = 1.0
|
|
82
|
+
fillRule = "nonzero"
|
|
83
|
+
dashArray = ""
|
|
84
|
+
dashOffset = 0.0
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn hasFill:boolean () {
|
|
88
|
+
return fillColor.isSet
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fn hasStroke:boolean () {
|
|
92
|
+
if (strokeColor.isSet == false) {
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
return (strokeWidth > 0.0)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
; The `d` string for this item, for targets that speak path data. Round-trips
|
|
99
|
+
; through SVGPathParser, which is what vector_shapes_test already asserts of
|
|
100
|
+
; VectorShapes.asPathData.
|
|
101
|
+
fn pathData:string () {
|
|
102
|
+
return (VectorShapes.asPathData(commands))
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
; ------------------------------------------------------------------------------
|
|
107
|
+
; SvgStyleState — what a shape inherits from the groups above it.
|
|
108
|
+
; ------------------------------------------------------------------------------
|
|
109
|
+
; SVG's initial values, not EVG's: fill is black, stroke is none, stroke-width
|
|
110
|
+
; is 1. An imported file that sets no fill anywhere is black in a browser, so it
|
|
111
|
+
; is black here.
|
|
112
|
+
;
|
|
113
|
+
; The three opacity properties are kept apart while parsing and multiplied into
|
|
114
|
+
; the paint alpha at emit time. Group opacity is genuinely NOT the same thing —
|
|
115
|
+
; the spec composites a group off-screen and then fades it as a unit, so
|
|
116
|
+
; overlapping children inside a faded group show through each other under this
|
|
117
|
+
; treatment and would not in a browser. Doing it properly needs an off-screen
|
|
118
|
+
; buffer per group, which the raster target has no notion of and the PDF target
|
|
119
|
+
; would need a transparency group for; it is recorded here rather than left to
|
|
120
|
+
; be discovered, and PLAN_VECTOR_IR.md Stage 4 lists it among what did not
|
|
121
|
+
; land.
|
|
122
|
+
class SvgStyleState {
|
|
123
|
+
def ctm:Matrix2D
|
|
124
|
+
def fill:EVGColor
|
|
125
|
+
def stroke:EVGColor
|
|
126
|
+
def strokeWidth:double 1.0
|
|
127
|
+
def fillRule:string "nonzero"
|
|
128
|
+
def dashArray:string ""
|
|
129
|
+
def dashOffset:double 0.0
|
|
130
|
+
def fillOpacity:double 1.0
|
|
131
|
+
def strokeOpacity:double 1.0
|
|
132
|
+
def groupOpacity:double 1.0
|
|
133
|
+
|
|
134
|
+
Constructor () {
|
|
135
|
+
ctm = (Matrix2D.identity())
|
|
136
|
+
fill = (EVGColor.black())
|
|
137
|
+
stroke = (EVGColor.noColor())
|
|
138
|
+
strokeWidth = 1.0
|
|
139
|
+
fillRule = "nonzero"
|
|
140
|
+
dashArray = ""
|
|
141
|
+
dashOffset = 0.0
|
|
142
|
+
fillOpacity = 1.0
|
|
143
|
+
strokeOpacity = 1.0
|
|
144
|
+
groupOpacity = 1.0
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fn copy:SvgStyleState () {
|
|
148
|
+
def s (new SvgStyleState())
|
|
149
|
+
s.ctm = ctm
|
|
150
|
+
s.fill = fill
|
|
151
|
+
s.stroke = stroke
|
|
152
|
+
s.strokeWidth = strokeWidth
|
|
153
|
+
s.fillRule = fillRule
|
|
154
|
+
s.dashArray = dashArray
|
|
155
|
+
s.dashOffset = dashOffset
|
|
156
|
+
s.fillOpacity = fillOpacity
|
|
157
|
+
s.strokeOpacity = strokeOpacity
|
|
158
|
+
s.groupOpacity = groupOpacity
|
|
159
|
+
return s
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
; ------------------------------------------------------------------------------
|
|
164
|
+
; SvgDocument — the parse result.
|
|
165
|
+
; ------------------------------------------------------------------------------
|
|
166
|
+
; `errors` are conditions that stopped or truncated the parse. `warnings` are
|
|
167
|
+
; things the document asked for that this profile does not do — the document is
|
|
168
|
+
; still drawable, just not completely. Keeping the two apart matters for a
|
|
169
|
+
; caller deciding whether to print or to fail.
|
|
170
|
+
class SvgDocument {
|
|
171
|
+
def items:[SvgVectorItem]
|
|
172
|
+
def viewBox:ViewBoxRect
|
|
173
|
+
; The `width`/`height` attributes on the root, in user units. Zero when
|
|
174
|
+
; absent or given in a unit this profile does not resolve (%, em, ...).
|
|
175
|
+
def width:double 0.0
|
|
176
|
+
def height:double 0.0
|
|
177
|
+
def warnings:[string]
|
|
178
|
+
def errors:[string]
|
|
179
|
+
def truncated:boolean false
|
|
180
|
+
|
|
181
|
+
Constructor () {
|
|
182
|
+
def it:[SvgVectorItem]
|
|
183
|
+
items = it
|
|
184
|
+
def w:[string]
|
|
185
|
+
warnings = w
|
|
186
|
+
def e:[string]
|
|
187
|
+
errors = e
|
|
188
|
+
viewBox = (new ViewBoxRect())
|
|
189
|
+
width = 0.0
|
|
190
|
+
height = 0.0
|
|
191
|
+
truncated = false
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fn itemCount:int () {
|
|
195
|
+
return (array_length items)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
fn hasErrors:boolean () {
|
|
199
|
+
return ((array_length errors) > 0)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fn hasWarnings:boolean () {
|
|
203
|
+
return ((array_length warnings) > 0)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
fn commandCount:int () {
|
|
207
|
+
def total:int 0
|
|
208
|
+
def k:int 0
|
|
209
|
+
while (k < (array_length items)) {
|
|
210
|
+
def it:SvgVectorItem (itemAt items k)
|
|
211
|
+
total = (total + (array_length it.commands))
|
|
212
|
+
k = (k + 1)
|
|
213
|
+
}
|
|
214
|
+
return total
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
fn joinLines:string (lines:[string]) {
|
|
218
|
+
def n:int (array_length lines)
|
|
219
|
+
if (n == 0) {
|
|
220
|
+
return ""
|
|
221
|
+
}
|
|
222
|
+
def out:string (itemAt lines 0)
|
|
223
|
+
def k:int 1
|
|
224
|
+
while (k < n) {
|
|
225
|
+
out = (out + "; " + (itemAt lines k))
|
|
226
|
+
k = (k + 1)
|
|
227
|
+
}
|
|
228
|
+
return out
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fn errorSummary:string () {
|
|
232
|
+
return (this.joinLines(errors))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fn warningSummary:string () {
|
|
236
|
+
return (this.joinLines(warnings))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
; The ink bounds of every item, control points included — the same
|
|
240
|
+
; over-estimate SVGPathParser.calculateBounds makes, deliberately, so a
|
|
241
|
+
; document with no viewBox is fitted the same way a `<Path>` with no viewBox
|
|
242
|
+
; is.
|
|
243
|
+
fn bounds:PathBounds () {
|
|
244
|
+
def b (new PathBounds())
|
|
245
|
+
def minX:double 999999.0
|
|
246
|
+
def minY:double 999999.0
|
|
247
|
+
def maxX:double -999999.0
|
|
248
|
+
def maxY:double -999999.0
|
|
249
|
+
def any:boolean false
|
|
250
|
+
|
|
251
|
+
def k:int 0
|
|
252
|
+
while (k < (array_length items)) {
|
|
253
|
+
def it:SvgVectorItem (itemAt items k)
|
|
254
|
+
def j:int 0
|
|
255
|
+
while (j < (array_length it.commands)) {
|
|
256
|
+
def c:PathCommand (itemAt it.commands j)
|
|
257
|
+
if ((c.type == "Z") == false) {
|
|
258
|
+
any = true
|
|
259
|
+
if (c.x < minX) { minX = c.x }
|
|
260
|
+
if (c.x > maxX) { maxX = c.x }
|
|
261
|
+
if (c.y < minY) { minY = c.y }
|
|
262
|
+
if (c.y > maxY) { maxY = c.y }
|
|
263
|
+
}
|
|
264
|
+
if ((c.type == "C") || (c.type == "Q")) {
|
|
265
|
+
if (c.x1 < minX) { minX = c.x1 }
|
|
266
|
+
if (c.x1 > maxX) { maxX = c.x1 }
|
|
267
|
+
if (c.y1 < minY) { minY = c.y1 }
|
|
268
|
+
if (c.y1 > maxY) { maxY = c.y1 }
|
|
269
|
+
}
|
|
270
|
+
if (c.type == "C") {
|
|
271
|
+
if (c.x2 < minX) { minX = c.x2 }
|
|
272
|
+
if (c.x2 > maxX) { maxX = c.x2 }
|
|
273
|
+
if (c.y2 < minY) { minY = c.y2 }
|
|
274
|
+
if (c.y2 > maxY) { maxY = c.y2 }
|
|
275
|
+
}
|
|
276
|
+
j = (j + 1)
|
|
277
|
+
}
|
|
278
|
+
k = (k + 1)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (any == false) {
|
|
282
|
+
return b
|
|
283
|
+
}
|
|
284
|
+
b.minX = minX
|
|
285
|
+
b.minY = minY
|
|
286
|
+
b.maxX = maxX
|
|
287
|
+
b.maxY = maxY
|
|
288
|
+
b.width = (maxX - minX)
|
|
289
|
+
b.height = (maxY - minY)
|
|
290
|
+
return b
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
; Which viewBox governs this document: the declared one, else one
|
|
294
|
+
; synthesised from the ink. Same rule, same fallback and same shared
|
|
295
|
+
; implementation a `<Path>` goes through (VectorViewBox §2.1), so an
|
|
296
|
+
; imported file and a hand-written path are placed by the same code.
|
|
297
|
+
fn effectiveViewBox:ViewBoxRect () {
|
|
298
|
+
if viewBox.isSet {
|
|
299
|
+
return (unwrap viewBox)
|
|
300
|
+
}
|
|
301
|
+
if ((width > 0.0) && (height > 0.0)) {
|
|
302
|
+
return (ViewBoxRect.create(0.0 0.0 width height))
|
|
303
|
+
}
|
|
304
|
+
def b:PathBounds (this.bounds())
|
|
305
|
+
return (VectorViewBox.effectiveViewBox("" b.minX b.minY b.width b.height))
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
; ------------------------------------------------------------------------------
|
|
310
|
+
; XML scanning
|
|
311
|
+
; ------------------------------------------------------------------------------
|
|
312
|
+
class SvgAttr {
|
|
313
|
+
def name:string ""
|
|
314
|
+
def value:string ""
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
class SvgTag {
|
|
318
|
+
def name:string ""
|
|
319
|
+
def isEnd:boolean false
|
|
320
|
+
def selfClose:boolean false
|
|
321
|
+
def attrs:[SvgAttr]
|
|
322
|
+
; False at end of input, or when scanning gave up.
|
|
323
|
+
def valid:boolean false
|
|
324
|
+
|
|
325
|
+
Constructor () {
|
|
326
|
+
def a:[SvgAttr]
|
|
327
|
+
attrs = a
|
|
328
|
+
name = ""
|
|
329
|
+
isEnd = false
|
|
330
|
+
selfClose = false
|
|
331
|
+
valid = false
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
fn has:boolean (n:string) {
|
|
335
|
+
def k:int 0
|
|
336
|
+
while (k < (array_length attrs)) {
|
|
337
|
+
def a:SvgAttr (itemAt attrs k)
|
|
338
|
+
if (a.name == n) {
|
|
339
|
+
return true
|
|
340
|
+
}
|
|
341
|
+
k = (k + 1)
|
|
342
|
+
}
|
|
343
|
+
return false
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
fn attr:string (n:string) {
|
|
347
|
+
def k:int 0
|
|
348
|
+
while (k < (array_length attrs)) {
|
|
349
|
+
def a:SvgAttr (itemAt attrs k)
|
|
350
|
+
if (a.name == n) {
|
|
351
|
+
return a.value
|
|
352
|
+
}
|
|
353
|
+
k = (k + 1)
|
|
354
|
+
}
|
|
355
|
+
return ""
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
; ------------------------------------------------------------------------------
|
|
360
|
+
; SvgParser
|
|
361
|
+
; ------------------------------------------------------------------------------
|
|
362
|
+
class SvgParser {
|
|
363
|
+
def src:string ""
|
|
364
|
+
def pos:int 0
|
|
365
|
+
def len:int 0
|
|
366
|
+
def doc:SvgDocument
|
|
367
|
+
|
|
368
|
+
; Caps. These exist so a hostile or merely enormous file fails with a
|
|
369
|
+
; sentence rather than with an out-of-memory (§8).
|
|
370
|
+
def maxNodes:int 50000
|
|
371
|
+
def maxDepth:int 64
|
|
372
|
+
def maxCommands:int 500000
|
|
373
|
+
def maxUseDepth:int 8
|
|
374
|
+
|
|
375
|
+
; The paint a shape gets when neither it nor anything above it names one.
|
|
376
|
+
; SVG's initial value is black, and that is the default here — but the
|
|
377
|
+
; hosting element can set it, which is what lets a themed icon set work:
|
|
378
|
+
; the file declares geometry, the element (and therefore the stylesheet)
|
|
379
|
+
; decides the colour, exactly as a hand-written `<Path>` already does. A
|
|
380
|
+
; document that sets its own fills is unaffected, because its own value
|
|
381
|
+
; wins. It is also the only sensible answer for `currentColor`, which is
|
|
382
|
+
; otherwise a reference to a CSS property an imported file cannot see.
|
|
383
|
+
def initialFill:EVGColor
|
|
384
|
+
|
|
385
|
+
def nodeCount:int 0
|
|
386
|
+
def emittedCommands:int 0
|
|
387
|
+
def useDepth:int 0
|
|
388
|
+
def aborted:boolean false
|
|
389
|
+
|
|
390
|
+
; id -> that element's own markup, for same-document <use>. Recorded in a
|
|
391
|
+
; first pass so a <use> may appear before its target, which is legal and
|
|
392
|
+
; common (Illustrator puts <defs> at the end as often as at the start).
|
|
393
|
+
def spans:[string:string]
|
|
394
|
+
|
|
395
|
+
; Warnings are deduplicated by key. A file with four hundred gradient fills
|
|
396
|
+
; should say "gradient paint is not supported" once; four hundred copies of
|
|
397
|
+
; it is the same as saying nothing.
|
|
398
|
+
def warnedKeys:[string:boolean]
|
|
399
|
+
|
|
400
|
+
Constructor () {
|
|
401
|
+
doc = (new SvgDocument())
|
|
402
|
+
initialFill = (EVGColor.black())
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
fn setInitialFill:void (c:EVGColor) {
|
|
406
|
+
initialFill = c
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
; --------------------------------------------------------------------------
|
|
410
|
+
; Diagnostics
|
|
411
|
+
; --------------------------------------------------------------------------
|
|
412
|
+
|
|
413
|
+
fn warn:void (key:string msg:string) {
|
|
414
|
+
def seen@(optional):boolean (get warnedKeys key)
|
|
415
|
+
; Optional booleans are plain `bool` on C++; a bare `if seen` becomes
|
|
416
|
+
; `seen != NULL` and warns. `!null?` emits the bool itself.
|
|
417
|
+
if (!null? seen) {
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
set warnedKeys key true
|
|
421
|
+
push doc.warnings msg
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
fn fail:void (msg:string) {
|
|
425
|
+
push doc.errors msg
|
|
426
|
+
doc.truncated = true
|
|
427
|
+
aborted = true
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
; --------------------------------------------------------------------------
|
|
431
|
+
; Entry point
|
|
432
|
+
; --------------------------------------------------------------------------
|
|
433
|
+
fn parse:SvgDocument (source:string) {
|
|
434
|
+
doc = (new SvgDocument())
|
|
435
|
+
def emptySpans:[string:string]
|
|
436
|
+
spans = emptySpans
|
|
437
|
+
def emptyWarned:[string:boolean]
|
|
438
|
+
warnedKeys = emptyWarned
|
|
439
|
+
nodeCount = 0
|
|
440
|
+
emittedCommands = 0
|
|
441
|
+
useDepth = 0
|
|
442
|
+
aborted = false
|
|
443
|
+
|
|
444
|
+
src = source
|
|
445
|
+
len = (strlen source)
|
|
446
|
+
pos = 0
|
|
447
|
+
|
|
448
|
+
this.indexIds()
|
|
449
|
+
if aborted {
|
|
450
|
+
return (unwrap doc)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
src = source
|
|
454
|
+
len = (strlen source)
|
|
455
|
+
pos = 0
|
|
456
|
+
|
|
457
|
+
def root (new SvgStyleState())
|
|
458
|
+
root.fill = (unwrap initialFill)
|
|
459
|
+
this.parseChildren(root 0 "")
|
|
460
|
+
|
|
461
|
+
return (unwrap doc)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
; --------------------------------------------------------------------------
|
|
465
|
+
; Pass one: where every id'd element starts and ends
|
|
466
|
+
; --------------------------------------------------------------------------
|
|
467
|
+
; Only `id` matters here, and only so that <use href="#id"> can re-parse the
|
|
468
|
+
; target's markup as a fragment. Storing the SOURCE rather than a parsed node
|
|
469
|
+
; is what keeps <use> honest: the referenced element goes through exactly the
|
|
470
|
+
; same code path as if it had been written inline, including its own
|
|
471
|
+
; transform and its own inheritance.
|
|
472
|
+
fn indexIds:void () {
|
|
473
|
+
def openNames:[string]
|
|
474
|
+
def openIds:[string]
|
|
475
|
+
def openStarts:[int]
|
|
476
|
+
|
|
477
|
+
while (aborted == false) {
|
|
478
|
+
def tagStart:int (this.findTagStart())
|
|
479
|
+
if (tagStart < 0) {
|
|
480
|
+
return
|
|
481
|
+
}
|
|
482
|
+
def tag:SvgTag (this.readTag())
|
|
483
|
+
if (tag.valid == false) {
|
|
484
|
+
return
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if tag.isEnd {
|
|
488
|
+
def depth:int (array_length openNames)
|
|
489
|
+
if (depth > 0) {
|
|
490
|
+
def openName:string (itemAt openNames (depth - 1))
|
|
491
|
+
if (openName == tag.name) {
|
|
492
|
+
def id:string (itemAt openIds (depth - 1))
|
|
493
|
+
if ((strlen id) > 0) {
|
|
494
|
+
def from:int (itemAt openStarts (depth - 1))
|
|
495
|
+
set spans id (substring src from pos)
|
|
496
|
+
}
|
|
497
|
+
removeLast openNames
|
|
498
|
+
removeLast openIds
|
|
499
|
+
removeLast openStarts
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
} {
|
|
503
|
+
def id2:string (tag.attr("id"))
|
|
504
|
+
if tag.selfClose {
|
|
505
|
+
if ((strlen id2) > 0) {
|
|
506
|
+
set spans id2 (substring src tagStart pos)
|
|
507
|
+
}
|
|
508
|
+
} {
|
|
509
|
+
push openNames tag.name
|
|
510
|
+
push openIds id2
|
|
511
|
+
push openStarts tagStart
|
|
512
|
+
if ((array_length openNames) > maxDepth) {
|
|
513
|
+
this.fail("nesting deeper than " + (to_string maxDepth) + " levels")
|
|
514
|
+
return
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
; --------------------------------------------------------------------------
|
|
522
|
+
; Pass two: the walk that produces items
|
|
523
|
+
; --------------------------------------------------------------------------
|
|
524
|
+
; `endName` is the element whose closing tag ends this level, or "" at the
|
|
525
|
+
; top of a document or of a <use> fragment, where end of input ends it.
|
|
526
|
+
fn parseChildren:void (inherited:SvgStyleState depth:int endName:string) {
|
|
527
|
+
if (depth > maxDepth) {
|
|
528
|
+
this.fail("nesting deeper than " + (to_string maxDepth) + " levels")
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
while (aborted == false) {
|
|
533
|
+
def tagStart:int (this.findTagStart())
|
|
534
|
+
if (tagStart < 0) {
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
def tag:SvgTag (this.readTag())
|
|
538
|
+
if (tag.valid == false) {
|
|
539
|
+
return
|
|
540
|
+
}
|
|
541
|
+
if tag.isEnd {
|
|
542
|
+
; The matching end tag, or a stray one. Either way this level is
|
|
543
|
+
; over: XML is well-formed or it is not, and guessing which tag
|
|
544
|
+
; a stray `</g>` was meant to close invents structure.
|
|
545
|
+
return
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
nodeCount = (nodeCount + 1)
|
|
549
|
+
if (nodeCount > maxNodes) {
|
|
550
|
+
this.fail("more than " + (to_string maxNodes) + " elements")
|
|
551
|
+
return
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
this.handleElement(tag inherited depth)
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
fn handleElement:void (tag:SvgTag inherited:SvgStyleState depth:int) {
|
|
559
|
+
def name:string tag.name
|
|
560
|
+
|
|
561
|
+
; ---- out of profile, and said so ------------------------------------
|
|
562
|
+
if (this.isRefused(name)) {
|
|
563
|
+
this.refuse(name)
|
|
564
|
+
if (tag.selfClose == false) {
|
|
565
|
+
this.skipSubtree(name)
|
|
566
|
+
}
|
|
567
|
+
return
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
; ---- carries no geometry --------------------------------------------
|
|
571
|
+
; title/desc/metadata are documentation, not drawing. Skipping them is
|
|
572
|
+
; not a limitation and needs no warning.
|
|
573
|
+
if (((name == "title") || (name == "desc")) || (name == "metadata")) {
|
|
574
|
+
if (tag.selfClose == false) {
|
|
575
|
+
this.skipSubtree(name)
|
|
576
|
+
}
|
|
577
|
+
return
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
; ---- <defs> ----------------------------------------------------------
|
|
581
|
+
; Definitions are drawn only where they are referenced. Pass one already
|
|
582
|
+
; recorded their markup by id, so nothing is lost by not walking in.
|
|
583
|
+
if (name == "defs") {
|
|
584
|
+
if (tag.selfClose == false) {
|
|
585
|
+
this.skipSubtree(name)
|
|
586
|
+
}
|
|
587
|
+
return
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
; ---- the root, and any nested viewport -------------------------------
|
|
591
|
+
if (name == "svg") {
|
|
592
|
+
if (depth == 0) {
|
|
593
|
+
this.readRootAttributes(tag)
|
|
594
|
+
def rootState:SvgStyleState (this.applyPresentation(inherited tag depth))
|
|
595
|
+
if (tag.selfClose == false) {
|
|
596
|
+
this.parseChildren(rootState (depth + 1) name)
|
|
597
|
+
}
|
|
598
|
+
return
|
|
599
|
+
}
|
|
600
|
+
; A nested <svg> establishes a new viewport with its own viewBox and
|
|
601
|
+
; its own clip. Drawing its children in the OUTER coordinate system
|
|
602
|
+
; would put them in the wrong place at the wrong size, which is worse
|
|
603
|
+
; than not drawing them, so it is refused rather than approximated.
|
|
604
|
+
this.warn("nested-svg" "a nested <svg> element establishes its own viewport and is not supported; its contents are not drawn")
|
|
605
|
+
if (tag.selfClose == false) {
|
|
606
|
+
this.skipSubtree(name)
|
|
607
|
+
}
|
|
608
|
+
return
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
; ---- groups ----------------------------------------------------------
|
|
612
|
+
; <a> is a group that happens to link somewhere. The link is meaningless
|
|
613
|
+
; in a printed page and harmless in the others, so its children are
|
|
614
|
+
; drawn and the href is ignored.
|
|
615
|
+
if ((name == "g") || (name == "a")) {
|
|
616
|
+
def groupState:SvgStyleState (this.applyPresentation(inherited tag depth))
|
|
617
|
+
if (tag.selfClose == false) {
|
|
618
|
+
this.parseChildren(groupState (depth + 1) name)
|
|
619
|
+
}
|
|
620
|
+
return
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
; ---- <switch> --------------------------------------------------------
|
|
624
|
+
; Real conditional processing needs requiredFeatures/requiredExtensions/
|
|
625
|
+
; systemLanguage evaluation. This profile has no feature strings to test
|
|
626
|
+
; against, so it would render every branch — which is why the branches
|
|
627
|
+
; are refused instead.
|
|
628
|
+
if (name == "switch") {
|
|
629
|
+
this.warn("switch" "<switch> conditional processing is not supported; its contents are not drawn")
|
|
630
|
+
if (tag.selfClose == false) {
|
|
631
|
+
this.skipSubtree(name)
|
|
632
|
+
}
|
|
633
|
+
return
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
; ---- <use> -----------------------------------------------------------
|
|
637
|
+
if (name == "use") {
|
|
638
|
+
this.handleUse(tag inherited depth)
|
|
639
|
+
if (tag.selfClose == false) {
|
|
640
|
+
this.skipSubtree(name)
|
|
641
|
+
}
|
|
642
|
+
return
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
; ---- shapes ----------------------------------------------------------
|
|
646
|
+
if (this.isShape(name)) {
|
|
647
|
+
def shapeState:SvgStyleState (this.applyPresentation(inherited tag depth))
|
|
648
|
+
this.emitShape(tag shapeState)
|
|
649
|
+
; A shape may carry <title>/<desc>/<animate> children. Nothing in
|
|
650
|
+
; there is geometry.
|
|
651
|
+
if (tag.selfClose == false) {
|
|
652
|
+
this.skipSubtree(name)
|
|
653
|
+
}
|
|
654
|
+
return
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
; ---- anything else ---------------------------------------------------
|
|
658
|
+
this.warn(("unknown-" + name) ("<" + name + "> is not part of the supported SVG profile and was skipped"))
|
|
659
|
+
if (tag.selfClose == false) {
|
|
660
|
+
this.skipSubtree(name)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
; Elements the profile refuses by name, each for a stated reason (§8, §6).
|
|
665
|
+
fn isRefused:boolean (name:string) {
|
|
666
|
+
if (name == "script") { return true }
|
|
667
|
+
if (name == "foreignObject") { return true }
|
|
668
|
+
if (name == "filter") { return true }
|
|
669
|
+
if (name == "mask") { return true }
|
|
670
|
+
if (name == "clipPath") { return true }
|
|
671
|
+
if (name == "pattern") { return true }
|
|
672
|
+
if (name == "marker") { return true }
|
|
673
|
+
if (name == "symbol") { return true }
|
|
674
|
+
if (name == "style") { return true }
|
|
675
|
+
if (name == "text") { return true }
|
|
676
|
+
if (name == "image") { return true }
|
|
677
|
+
if (name == "linearGradient") { return true }
|
|
678
|
+
if (name == "radialGradient") { return true }
|
|
679
|
+
if (name == "animate") { return true }
|
|
680
|
+
if (name == "animateTransform") { return true }
|
|
681
|
+
if (name == "animateMotion") { return true }
|
|
682
|
+
if (name == "set") { return true }
|
|
683
|
+
return false
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
fn refuse:void (name:string) {
|
|
687
|
+
; <text> gets its own sentence because it is the single most likely
|
|
688
|
+
; cause of a "your SVG support is broken" report: Illustrator and Figma
|
|
689
|
+
; exports carry live text constantly, and a logo that quietly loses its
|
|
690
|
+
; wordmark looks like a rendering bug rather than an unsupported feature
|
|
691
|
+
; (§8). Converting it needs the font machinery and text layout, which is
|
|
692
|
+
; a different piece of work entirely.
|
|
693
|
+
if (name == "text") {
|
|
694
|
+
this.warn("text" "<text> is not supported; convert text to outlines before export, or the wordmark will be missing")
|
|
695
|
+
return
|
|
696
|
+
}
|
|
697
|
+
if (name == "image") {
|
|
698
|
+
this.warn("image" "<image> references an external resource, which the importer does not fetch; it was skipped")
|
|
699
|
+
return
|
|
700
|
+
}
|
|
701
|
+
if (name == "style") {
|
|
702
|
+
this.warn("style" "a <style> element is not applied; CSS in the document is outside the profile, so use presentation attributes instead")
|
|
703
|
+
return
|
|
704
|
+
}
|
|
705
|
+
if ((name == "linearGradient") || (name == "radialGradient")) {
|
|
706
|
+
this.warn("gradient" "gradient paint is deferred (PLAN_VECTOR_IR.md §6): the three renderers do not agree on gradients yet, so a gradient fill is dropped rather than rendered differently in each output")
|
|
707
|
+
return
|
|
708
|
+
}
|
|
709
|
+
if ((name == "clipPath") || (name == "mask")) {
|
|
710
|
+
this.warn(("clip-" + name) ("<" + name + "> is not supported; the shapes it would have cut are drawn whole"))
|
|
711
|
+
return
|
|
712
|
+
}
|
|
713
|
+
this.warn(("refused-" + name) ("<" + name + "> is outside the supported SVG profile and was skipped"))
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
fn isShape:boolean (name:string) {
|
|
717
|
+
if (name == "path") { return true }
|
|
718
|
+
if (name == "rect") { return true }
|
|
719
|
+
if (name == "circle") { return true }
|
|
720
|
+
if (name == "ellipse") { return true }
|
|
721
|
+
if (name == "line") { return true }
|
|
722
|
+
if (name == "polyline") { return true }
|
|
723
|
+
if (name == "polygon") { return true }
|
|
724
|
+
return false
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
; --------------------------------------------------------------------------
|
|
728
|
+
; <use>
|
|
729
|
+
; --------------------------------------------------------------------------
|
|
730
|
+
; Same-document fragments only. `href` to anything else — another file, a
|
|
731
|
+
; URL — is dropped, which is the profile's "no network or filesystem access
|
|
732
|
+
; from inside the parser" rule enforced rather than documented (§8).
|
|
733
|
+
fn handleUse:void (tag:SvgTag inherited:SvgStyleState depth:int) {
|
|
734
|
+
def href:string (tag.attr("href"))
|
|
735
|
+
if ((strlen href) == 0) {
|
|
736
|
+
href = (tag.attr("xlink:href"))
|
|
737
|
+
}
|
|
738
|
+
if ((strlen href) == 0) {
|
|
739
|
+
this.warn("use-nohref" "<use> without an href draws nothing")
|
|
740
|
+
return
|
|
741
|
+
}
|
|
742
|
+
if ((charAt href 0) != 35) {
|
|
743
|
+
this.warn("use-external" "<use> may only reference a fragment in the same document; an external reference was dropped")
|
|
744
|
+
return
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
def id:string (substring href 1 (strlen href))
|
|
748
|
+
def target@(optional):string (get spans id)
|
|
749
|
+
def fragment:string ""
|
|
750
|
+
; Prefer `!null?`: optional strings are plain std::string on C++, so a
|
|
751
|
+
; bare `if target` becomes `target != NULL` and does not compile.
|
|
752
|
+
if (!null? target) {
|
|
753
|
+
fragment = (unwrap target)
|
|
754
|
+
} {
|
|
755
|
+
this.warn(("use-missing-" + id) ("<use href=\"#" + id + "\"> refers to an id that is not in this document"))
|
|
756
|
+
return
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (useDepth >= maxUseDepth) {
|
|
760
|
+
; A <use> whose target contains a <use> back to it is a cycle, and
|
|
761
|
+
; the spec calls it an error. Bounding the depth ends it without
|
|
762
|
+
; needing to track which ids are currently open.
|
|
763
|
+
this.warn("use-depth" ("<use> references nested more than " + (to_string maxUseDepth) + " deep, which is either a cycle or deeper than this profile expands"))
|
|
764
|
+
return
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
; x/y on a <use> is a translation of the referenced content. width and
|
|
768
|
+
; height only apply when the target is a <symbol> or an <svg>, both of
|
|
769
|
+
; which this profile refuses anyway.
|
|
770
|
+
def state:SvgStyleState (this.applyPresentation(inherited tag depth))
|
|
771
|
+
def ux:double (this.numAttr(tag "x" 0.0))
|
|
772
|
+
def uy:double (this.numAttr(tag "y" 0.0))
|
|
773
|
+
if (((ux == 0.0) && (uy == 0.0)) == false) {
|
|
774
|
+
state.ctm = (state.ctm.multiply((Matrix2D.translate(ux uy))))
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
; The fragment is re-parsed with its own scanner position, then the
|
|
778
|
+
; outer one is put back exactly where it was.
|
|
779
|
+
def savedSrc:string src
|
|
780
|
+
def savedPos:int pos
|
|
781
|
+
def savedLen:int len
|
|
782
|
+
|
|
783
|
+
src = fragment
|
|
784
|
+
len = (strlen src)
|
|
785
|
+
pos = 0
|
|
786
|
+
useDepth = (useDepth + 1)
|
|
787
|
+
this.parseChildren(state (depth + 1) "")
|
|
788
|
+
useDepth = (useDepth - 1)
|
|
789
|
+
|
|
790
|
+
src = savedSrc
|
|
791
|
+
pos = savedPos
|
|
792
|
+
len = savedLen
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
; --------------------------------------------------------------------------
|
|
796
|
+
; Presentation attributes
|
|
797
|
+
; --------------------------------------------------------------------------
|
|
798
|
+
; Order matters and is the CSS one: presentation attributes first, then the
|
|
799
|
+
; `style` attribute, which outranks them. An exporter that writes both —
|
|
800
|
+
; and they do — otherwise gets the losing value.
|
|
801
|
+
fn applyPresentation:SvgStyleState (inherited:SvgStyleState tag:SvgTag depth:int) {
|
|
802
|
+
def s:SvgStyleState (inherited.copy())
|
|
803
|
+
|
|
804
|
+
def k:int 0
|
|
805
|
+
while (k < (array_length tag.attrs)) {
|
|
806
|
+
def a:SvgAttr (itemAt tag.attrs k)
|
|
807
|
+
this.applyProperty(s a.name a.value)
|
|
808
|
+
k = (k + 1)
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
def styleAttr:string (tag.attr("style"))
|
|
812
|
+
if ((strlen styleAttr) > 0) {
|
|
813
|
+
this.applyStyleAttribute(s styleAttr)
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
; The transform composes onto whatever the parent established, and is
|
|
817
|
+
; applied after the parent's — the CTM is parent ∘ own.
|
|
818
|
+
def tf:string (tag.attr("transform"))
|
|
819
|
+
if ((strlen tf) > 0) {
|
|
820
|
+
def m:Matrix2D (this.parseTransform(tf))
|
|
821
|
+
s.ctm = (s.ctm.multiply(m))
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return s
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
; "fill:#f00; stroke : none" — the same property names as the attributes.
|
|
828
|
+
fn applyStyleAttribute:void (s:SvgStyleState style:string) {
|
|
829
|
+
def decls:[string] (this.splitOn(style 59))
|
|
830
|
+
def k:int 0
|
|
831
|
+
while (k < (array_length decls)) {
|
|
832
|
+
def decl:string (itemAt decls k)
|
|
833
|
+
def colon:int (indexOf decl ":")
|
|
834
|
+
if (colon > 0) {
|
|
835
|
+
def n:string (trim (substring decl 0 colon))
|
|
836
|
+
def v:string (trim (substring decl (colon + 1) (strlen decl)))
|
|
837
|
+
this.applyProperty(s n v)
|
|
838
|
+
}
|
|
839
|
+
k = (k + 1)
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
fn applyProperty:void (s:SvgStyleState name:string value:string) {
|
|
844
|
+
def v:string (trim value)
|
|
845
|
+
|
|
846
|
+
if (name == "fill") {
|
|
847
|
+
s.fill = (this.parsePaint(v (unwrap s.fill)))
|
|
848
|
+
return
|
|
849
|
+
}
|
|
850
|
+
if (name == "stroke") {
|
|
851
|
+
s.stroke = (this.parsePaint(v (unwrap s.stroke)))
|
|
852
|
+
return
|
|
853
|
+
}
|
|
854
|
+
if (name == "stroke-width") {
|
|
855
|
+
def w@(optional):double (str2double v)
|
|
856
|
+
if w {
|
|
857
|
+
s.strokeWidth = (unwrap w)
|
|
858
|
+
}
|
|
859
|
+
return
|
|
860
|
+
}
|
|
861
|
+
if (name == "fill-rule") {
|
|
862
|
+
if (v == "evenodd") {
|
|
863
|
+
s.fillRule = "evenodd"
|
|
864
|
+
}
|
|
865
|
+
if (v == "nonzero") {
|
|
866
|
+
s.fillRule = "nonzero"
|
|
867
|
+
}
|
|
868
|
+
return
|
|
869
|
+
}
|
|
870
|
+
if (name == "stroke-dasharray") {
|
|
871
|
+
if (v == "none") {
|
|
872
|
+
s.dashArray = ""
|
|
873
|
+
} {
|
|
874
|
+
s.dashArray = v
|
|
875
|
+
}
|
|
876
|
+
return
|
|
877
|
+
}
|
|
878
|
+
if (name == "stroke-dashoffset") {
|
|
879
|
+
def o@(optional):double (str2double v)
|
|
880
|
+
if o {
|
|
881
|
+
s.dashOffset = (unwrap o)
|
|
882
|
+
}
|
|
883
|
+
return
|
|
884
|
+
}
|
|
885
|
+
if (name == "fill-opacity") {
|
|
886
|
+
s.fillOpacity = (this.parseOpacity(v s.fillOpacity))
|
|
887
|
+
return
|
|
888
|
+
}
|
|
889
|
+
if (name == "stroke-opacity") {
|
|
890
|
+
s.strokeOpacity = (this.parseOpacity(v s.strokeOpacity))
|
|
891
|
+
return
|
|
892
|
+
}
|
|
893
|
+
if (name == "opacity") {
|
|
894
|
+
; Group opacity accumulates down the tree. See the note on
|
|
895
|
+
; SvgStyleState for what this does not reproduce.
|
|
896
|
+
def o2:double (this.parseOpacity(v 1.0))
|
|
897
|
+
s.groupOpacity = (s.groupOpacity * o2)
|
|
898
|
+
return
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
; Accepted and ignored, once, out loud. The raster target strokes with
|
|
902
|
+
; round joins and butt caps (Stage 2), so a document asking for anything
|
|
903
|
+
; else gets something that is close but not what it asked for — which is
|
|
904
|
+
; worth one line of output and not worth failing over.
|
|
905
|
+
if (((name == "stroke-linecap") || (name == "stroke-linejoin")) || (name == "stroke-miterlimit")) {
|
|
906
|
+
this.warn("stroke-joins" "stroke-linecap/linejoin/miterlimit are not represented; strokes are drawn with butt caps and round joins")
|
|
907
|
+
return
|
|
908
|
+
}
|
|
909
|
+
if (name == "class") {
|
|
910
|
+
this.warn("class" "class attributes have no effect because the profile applies no CSS")
|
|
911
|
+
return
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
; A paint value. `none` means the operation does not happen; anything
|
|
916
|
+
; unreadable keeps what was inherited rather than inventing a colour, so a
|
|
917
|
+
; misspelling shows up as a warning and the wrong-but-visible shape rather
|
|
918
|
+
; than as a hole in the drawing.
|
|
919
|
+
fn parsePaint:EVGColor (v:string inheritedPaint:EVGColor) {
|
|
920
|
+
if (v == "none") {
|
|
921
|
+
return (EVGColor.noColor())
|
|
922
|
+
}
|
|
923
|
+
if (v == "inherit") {
|
|
924
|
+
return inheritedPaint
|
|
925
|
+
}
|
|
926
|
+
if (v == "currentColor") {
|
|
927
|
+
; `currentColor` names the CSS `color` property, which an imported
|
|
928
|
+
; file cannot see. The element's own paint is what it is asking for.
|
|
929
|
+
return (unwrap initialFill)
|
|
930
|
+
}
|
|
931
|
+
if ((indexOf v "url(") == 0) {
|
|
932
|
+
this.warn("gradient" "gradient paint is deferred (PLAN_VECTOR_IR.md §6): the three renderers do not agree on gradients yet, so a gradient fill is dropped rather than rendered differently in each output")
|
|
933
|
+
return (EVGColor.noColor())
|
|
934
|
+
}
|
|
935
|
+
def c:EVGColor (EVGColor.parse(v))
|
|
936
|
+
if (c.isSet == false) {
|
|
937
|
+
this.warn(("color-" + v) ("could not read the colour \"" + v + "\"; the inherited paint was used instead"))
|
|
938
|
+
return inheritedPaint
|
|
939
|
+
}
|
|
940
|
+
return c
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
fn parseOpacity:double (v:string fallback:double) {
|
|
944
|
+
def pct:boolean false
|
|
945
|
+
def s:string v
|
|
946
|
+
if ((strlen s) > 0) {
|
|
947
|
+
if ((charAt s ((strlen s) - 1)) == 37) {
|
|
948
|
+
pct = true
|
|
949
|
+
s = (substring s 0 ((strlen s) - 1))
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
def d@(optional):double (str2double s)
|
|
953
|
+
def out:double fallback
|
|
954
|
+
if d {
|
|
955
|
+
out = (unwrap d)
|
|
956
|
+
} {
|
|
957
|
+
return fallback
|
|
958
|
+
}
|
|
959
|
+
if pct {
|
|
960
|
+
out = (out / 100.0)
|
|
961
|
+
}
|
|
962
|
+
if (out < 0.0) {
|
|
963
|
+
out = 0.0
|
|
964
|
+
}
|
|
965
|
+
if (out > 1.0) {
|
|
966
|
+
out = 1.0
|
|
967
|
+
}
|
|
968
|
+
return out
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
; --------------------------------------------------------------------------
|
|
972
|
+
; transform
|
|
973
|
+
; --------------------------------------------------------------------------
|
|
974
|
+
; matrix / translate / scale / rotate / skewX / skewY, composed left to
|
|
975
|
+
; right, which is the order SVG applies them in.
|
|
976
|
+
fn parseTransform:Matrix2D (s:string) {
|
|
977
|
+
def m:Matrix2D (Matrix2D.identity())
|
|
978
|
+
def i:int 0
|
|
979
|
+
def n:int (strlen s)
|
|
980
|
+
|
|
981
|
+
while (i < n) {
|
|
982
|
+
; function name up to '('
|
|
983
|
+
def open:int (this.findFrom(s i 40))
|
|
984
|
+
if (open < 0) {
|
|
985
|
+
return m
|
|
986
|
+
}
|
|
987
|
+
def fname:string (trim (substring s i open))
|
|
988
|
+
def close:int (this.findFrom(s (open + 1) 41))
|
|
989
|
+
if (close < 0) {
|
|
990
|
+
this.warn("transform-unclosed" ("a transform is missing its closing parenthesis: " + s))
|
|
991
|
+
return m
|
|
992
|
+
}
|
|
993
|
+
def args:[double] (this.parseNumberList((substring s (open + 1) close)))
|
|
994
|
+
def na:int (array_length args)
|
|
995
|
+
i = (close + 1)
|
|
996
|
+
|
|
997
|
+
; A transform list may be separated by commas as well as spaces.
|
|
998
|
+
def part:Matrix2D (Matrix2D.identity())
|
|
999
|
+
def known:boolean true
|
|
1000
|
+
|
|
1001
|
+
if (fname == "matrix") {
|
|
1002
|
+
if (na == 6) {
|
|
1003
|
+
part = (Matrix2D.create((itemAt args 0) (itemAt args 1) (itemAt args 2) (itemAt args 3) (itemAt args 4) (itemAt args 5)))
|
|
1004
|
+
} {
|
|
1005
|
+
known = false
|
|
1006
|
+
}
|
|
1007
|
+
} {
|
|
1008
|
+
if (fname == "translate") {
|
|
1009
|
+
if (na == 1) {
|
|
1010
|
+
part = (Matrix2D.translate((itemAt args 0) 0.0))
|
|
1011
|
+
} {
|
|
1012
|
+
if (na == 2) {
|
|
1013
|
+
part = (Matrix2D.translate((itemAt args 0) (itemAt args 1)))
|
|
1014
|
+
} {
|
|
1015
|
+
known = false
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
} {
|
|
1019
|
+
if (fname == "scale") {
|
|
1020
|
+
if (na == 1) {
|
|
1021
|
+
part = (Matrix2D.scale((itemAt args 0) (itemAt args 0)))
|
|
1022
|
+
} {
|
|
1023
|
+
if (na == 2) {
|
|
1024
|
+
part = (Matrix2D.scale((itemAt args 0) (itemAt args 1)))
|
|
1025
|
+
} {
|
|
1026
|
+
known = false
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
} {
|
|
1030
|
+
if (fname == "rotate") {
|
|
1031
|
+
if (na == 1) {
|
|
1032
|
+
part = (this.rotation((itemAt args 0)))
|
|
1033
|
+
} {
|
|
1034
|
+
if (na == 3) {
|
|
1035
|
+
; rotate(a cx cy) is translate(cx cy) rotate(a) translate(-cx -cy)
|
|
1036
|
+
def cx:double (itemAt args 1)
|
|
1037
|
+
def cy:double (itemAt args 2)
|
|
1038
|
+
def r:Matrix2D (this.rotation((itemAt args 0)))
|
|
1039
|
+
part = (Matrix2D.translate(cx cy))
|
|
1040
|
+
part = (part.multiply(r))
|
|
1041
|
+
part = (part.multiply((Matrix2D.translate((0.0 - cx) (0.0 - cy)))))
|
|
1042
|
+
} {
|
|
1043
|
+
known = false
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
} {
|
|
1047
|
+
if (fname == "skewX") {
|
|
1048
|
+
if (na == 1) {
|
|
1049
|
+
part = (Matrix2D.create(1.0 0.0 (this.tanDeg((itemAt args 0))) 1.0 0.0 0.0))
|
|
1050
|
+
} {
|
|
1051
|
+
known = false
|
|
1052
|
+
}
|
|
1053
|
+
} {
|
|
1054
|
+
if (fname == "skewY") {
|
|
1055
|
+
if (na == 1) {
|
|
1056
|
+
part = (Matrix2D.create(1.0 (this.tanDeg((itemAt args 0))) 0.0 1.0 0.0 0.0))
|
|
1057
|
+
} {
|
|
1058
|
+
known = false
|
|
1059
|
+
}
|
|
1060
|
+
} {
|
|
1061
|
+
known = false
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (known == false) {
|
|
1070
|
+
this.warn(("transform-" + fname) ("the transform \"" + fname + "\" was not applied: unknown, or given the wrong number of arguments"))
|
|
1071
|
+
} {
|
|
1072
|
+
m = (m.multiply(part))
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
return m
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
fn rotation:Matrix2D (deg:double) {
|
|
1079
|
+
def rad:double ((deg * 3.141592653589793) / 180.0)
|
|
1080
|
+
def cs:double (cos rad)
|
|
1081
|
+
def sn:double (sin rad)
|
|
1082
|
+
return (Matrix2D.create(cs sn (0.0 - sn) cs 0.0 0.0))
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
fn tanDeg:double (deg:double) {
|
|
1086
|
+
def rad:double ((deg * 3.141592653589793) / 180.0)
|
|
1087
|
+
return ((sin rad) / (cos rad))
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
; --------------------------------------------------------------------------
|
|
1091
|
+
; Shapes
|
|
1092
|
+
; --------------------------------------------------------------------------
|
|
1093
|
+
fn emitShape:void (tag:SvgTag s:SvgStyleState) {
|
|
1094
|
+
def name:string tag.name
|
|
1095
|
+
def cmds:[PathCommand]
|
|
1096
|
+
|
|
1097
|
+
if (name == "path") {
|
|
1098
|
+
def d:string (tag.attr("d"))
|
|
1099
|
+
if ((strlen d) == 0) {
|
|
1100
|
+
return
|
|
1101
|
+
}
|
|
1102
|
+
def p (new SVGPathParser())
|
|
1103
|
+
p.parse(d)
|
|
1104
|
+
; The path grammar reports what it refused (Stage 1). Passing that
|
|
1105
|
+
; through is the difference between "this icon looks wrong" and
|
|
1106
|
+
; "this icon used an arc flag the parser could not read".
|
|
1107
|
+
if (p.hasErrors()) {
|
|
1108
|
+
this.warn(("pathdata-" + (p.errorSummary())) ("path data was not fully read: " + (p.errorSummary())))
|
|
1109
|
+
}
|
|
1110
|
+
cmds = (p.getCommands())
|
|
1111
|
+
} {
|
|
1112
|
+
if (name == "rect") {
|
|
1113
|
+
def rx:double (this.numAttr(tag "rx" -1.0))
|
|
1114
|
+
def ry:double (this.numAttr(tag "ry" -1.0))
|
|
1115
|
+
cmds = (VectorShapes.rect((this.numAttr(tag "x" 0.0)) (this.numAttr(tag "y" 0.0)) (this.numAttr(tag "width" 0.0)) (this.numAttr(tag "height" 0.0)) rx ry))
|
|
1116
|
+
} {
|
|
1117
|
+
if (name == "circle") {
|
|
1118
|
+
cmds = (VectorShapes.circle((this.numAttr(tag "cx" 0.0)) (this.numAttr(tag "cy" 0.0)) (this.numAttr(tag "r" 0.0))))
|
|
1119
|
+
} {
|
|
1120
|
+
if (name == "ellipse") {
|
|
1121
|
+
cmds = (VectorShapes.ellipse((this.numAttr(tag "cx" 0.0)) (this.numAttr(tag "cy" 0.0)) (this.numAttr(tag "rx" 0.0)) (this.numAttr(tag "ry" 0.0))))
|
|
1122
|
+
} {
|
|
1123
|
+
if (name == "line") {
|
|
1124
|
+
cmds = (VectorShapes.line((this.numAttr(tag "x1" 0.0)) (this.numAttr(tag "y1" 0.0)) (this.numAttr(tag "x2" 0.0)) (this.numAttr(tag "y2" 0.0))))
|
|
1125
|
+
} {
|
|
1126
|
+
if (name == "polyline") {
|
|
1127
|
+
cmds = (VectorShapes.polyline((this.parseNumberList((tag.attr("points"))))))
|
|
1128
|
+
} {
|
|
1129
|
+
if (name == "polygon") {
|
|
1130
|
+
cmds = (VectorShapes.polygon((this.parseNumberList((tag.attr("points"))))))
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
def count:int (array_length cmds)
|
|
1140
|
+
if (count == 0) {
|
|
1141
|
+
return
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
emittedCommands = (emittedCommands + count)
|
|
1145
|
+
if (emittedCommands > maxCommands) {
|
|
1146
|
+
this.fail("more than " + (to_string maxCommands) + " path commands")
|
|
1147
|
+
return
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
def item (new SvgVectorItem())
|
|
1151
|
+
item.commands = (this.transformCommands(cmds (unwrap s.ctm)))
|
|
1152
|
+
item.fillRule = s.fillRule
|
|
1153
|
+
item.dashArray = s.dashArray
|
|
1154
|
+
item.dashOffset = s.dashOffset
|
|
1155
|
+
|
|
1156
|
+
if s.fill.isSet {
|
|
1157
|
+
item.fillColor = (this.withAlpha((unwrap s.fill) (s.fillOpacity * s.groupOpacity)))
|
|
1158
|
+
}
|
|
1159
|
+
if s.stroke.isSet {
|
|
1160
|
+
item.strokeColor = (this.withAlpha((unwrap s.stroke) (s.strokeOpacity * s.groupOpacity)))
|
|
1161
|
+
; A stroke is drawn in the shape's own space, so a scaled group
|
|
1162
|
+
; scales the pen with it. Under a non-uniform scale there is no
|
|
1163
|
+
; single width that is right — SVG strokes an outline in user space
|
|
1164
|
+
; and transforms THAT — so the geometric mean is used, which is
|
|
1165
|
+
; exact whenever the scale is uniform and is the usual approximation
|
|
1166
|
+
; when it is not.
|
|
1167
|
+
item.strokeWidth = (s.strokeWidth * (this.scaleOf((unwrap s.ctm))))
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
; A shape that is neither filled nor stroked draws nothing. Keeping it
|
|
1171
|
+
; would make item counts misleading for no gain.
|
|
1172
|
+
if ((item.hasFill() == false) && (item.hasStroke() == false)) {
|
|
1173
|
+
return
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
push doc.items item
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
fn withAlpha:EVGColor (c:EVGColor mul:double) {
|
|
1180
|
+
if (mul >= 1.0) {
|
|
1181
|
+
return c
|
|
1182
|
+
}
|
|
1183
|
+
return (EVGColor.create(c.r c.g c.b (c.a * mul)))
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
; sqrt of |det|: the factor by which the transform changes area, which is
|
|
1187
|
+
; the scale a uniform transform applies and the sensible middle otherwise.
|
|
1188
|
+
fn scaleOf:double (m:Matrix2D) {
|
|
1189
|
+
def det:double ((m.a * m.d) - (m.b * m.c))
|
|
1190
|
+
if (det < 0.0) {
|
|
1191
|
+
det = (0.0 - det)
|
|
1192
|
+
}
|
|
1193
|
+
if (det == 0.0) {
|
|
1194
|
+
return 0.0
|
|
1195
|
+
}
|
|
1196
|
+
return (sqrt det)
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
; An affine map takes a cubic to a cubic and a quadratic to a quadratic, so
|
|
1200
|
+
; transforming the control points transforms the curve exactly. This is the
|
|
1201
|
+
; whole reason the IR can be flat: there is nothing a transform stack could
|
|
1202
|
+
; do later that cannot be done here, once.
|
|
1203
|
+
fn transformCommands:[PathCommand] (cmds:[PathCommand] m:Matrix2D) {
|
|
1204
|
+
def out:[PathCommand]
|
|
1205
|
+
if (m.isIdentity()) {
|
|
1206
|
+
return cmds
|
|
1207
|
+
}
|
|
1208
|
+
def k:int 0
|
|
1209
|
+
while (k < (array_length cmds)) {
|
|
1210
|
+
def c:PathCommand (itemAt cmds k)
|
|
1211
|
+
def n (new PathCommand())
|
|
1212
|
+
n.type = c.type
|
|
1213
|
+
n.x = (m.applyX(c.x c.y))
|
|
1214
|
+
n.y = (m.applyY(c.x c.y))
|
|
1215
|
+
n.x1 = (m.applyX(c.x1 c.y1))
|
|
1216
|
+
n.y1 = (m.applyY(c.x1 c.y1))
|
|
1217
|
+
n.x2 = (m.applyX(c.x2 c.y2))
|
|
1218
|
+
n.y2 = (m.applyY(c.x2 c.y2))
|
|
1219
|
+
push out n
|
|
1220
|
+
k = (k + 1)
|
|
1221
|
+
}
|
|
1222
|
+
return out
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
; --------------------------------------------------------------------------
|
|
1226
|
+
; Root attributes
|
|
1227
|
+
; --------------------------------------------------------------------------
|
|
1228
|
+
fn readRootAttributes:void (tag:SvgTag) {
|
|
1229
|
+
def vb:string (tag.attr("viewBox"))
|
|
1230
|
+
if ((strlen vb) > 0) {
|
|
1231
|
+
def parsed:ViewBoxRect (VectorViewBox.parseViewBox(vb))
|
|
1232
|
+
if parsed.isSet {
|
|
1233
|
+
doc.viewBox = parsed
|
|
1234
|
+
} {
|
|
1235
|
+
this.warn("viewbox" ("the root viewBox \"" + vb + "\" is not four numbers with a positive width and height, and was ignored"))
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
doc.width = (this.lengthAttr(tag "width"))
|
|
1239
|
+
doc.height = (this.lengthAttr(tag "height"))
|
|
1240
|
+
|
|
1241
|
+
def par:string (tag.attr("preserveAspectRatio"))
|
|
1242
|
+
if ((strlen par) > 0) {
|
|
1243
|
+
; The element box decides how an imported document is fitted, the
|
|
1244
|
+
; same way it does for a <Path>. Honouring the file's own
|
|
1245
|
+
; preserveAspectRatio would mean two different fitting rules for the
|
|
1246
|
+
; same element depending on where the geometry came from.
|
|
1247
|
+
this.warn("par" "preserveAspectRatio on the imported root is ignored; the element that hosts the drawing decides how it is fitted")
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
; A root width/height in user units. `100%` is the SVG default and means
|
|
1252
|
+
; "fill the viewport", which for an imported drawing is exactly what the
|
|
1253
|
+
; hosting element already decides — so it, and any other unit this profile
|
|
1254
|
+
; does not resolve, reads as "unspecified" rather than as a number.
|
|
1255
|
+
fn lengthAttr:double (tag:SvgTag name:string) {
|
|
1256
|
+
def raw:string (trim (tag.attr(name)))
|
|
1257
|
+
if ((strlen raw) == 0) {
|
|
1258
|
+
return 0.0
|
|
1259
|
+
}
|
|
1260
|
+
def s:string raw
|
|
1261
|
+
if ((indexOf s "px") > 0) {
|
|
1262
|
+
s = (substring s 0 (indexOf s "px"))
|
|
1263
|
+
}
|
|
1264
|
+
if ((this.isPlainNumber((trim s))) == false) {
|
|
1265
|
+
return 0.0
|
|
1266
|
+
}
|
|
1267
|
+
def d@(optional):double (str2double (trim s))
|
|
1268
|
+
def out:double 0.0
|
|
1269
|
+
if d {
|
|
1270
|
+
out = (unwrap d)
|
|
1271
|
+
} {
|
|
1272
|
+
return 0.0
|
|
1273
|
+
}
|
|
1274
|
+
if (out < 0.0) {
|
|
1275
|
+
return 0.0
|
|
1276
|
+
}
|
|
1277
|
+
return out
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
; str2double is happy to read "100%" as 100, which is exactly the wrong
|
|
1281
|
+
; answer for a length: a percentage is relative to a viewport this profile
|
|
1282
|
+
; does not resolve, and reading it as user units would size a drawing by a
|
|
1283
|
+
; number that means something else. So a value is a number only if every
|
|
1284
|
+
; character in it belongs to one.
|
|
1285
|
+
fn isPlainNumber:boolean (s:string) {
|
|
1286
|
+
def n:int (strlen s)
|
|
1287
|
+
if (n == 0) {
|
|
1288
|
+
return false
|
|
1289
|
+
}
|
|
1290
|
+
def k:int 0
|
|
1291
|
+
while (k < n) {
|
|
1292
|
+
def c:int (charAt s k)
|
|
1293
|
+
def ok:boolean false
|
|
1294
|
+
if ((c >= 48) && (c <= 57)) { ok = true }
|
|
1295
|
+
if (c == 46) { ok = true }
|
|
1296
|
+
if (c == 45) { ok = true }
|
|
1297
|
+
if (c == 43) { ok = true }
|
|
1298
|
+
if (c == 101) { ok = true }
|
|
1299
|
+
if (c == 69) { ok = true }
|
|
1300
|
+
if (ok == false) {
|
|
1301
|
+
return false
|
|
1302
|
+
}
|
|
1303
|
+
k = (k + 1)
|
|
1304
|
+
}
|
|
1305
|
+
return true
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
fn numAttr:double (tag:SvgTag name:string fallback:double) {
|
|
1309
|
+
def raw:string (trim (tag.attr(name)))
|
|
1310
|
+
if ((strlen raw) == 0) {
|
|
1311
|
+
return fallback
|
|
1312
|
+
}
|
|
1313
|
+
def s:string raw
|
|
1314
|
+
if ((indexOf s "px") > 0) {
|
|
1315
|
+
s = (substring s 0 (indexOf s "px"))
|
|
1316
|
+
}
|
|
1317
|
+
if (this.isPlainNumber((trim s))) {
|
|
1318
|
+
def d@(optional):double (str2double (trim s))
|
|
1319
|
+
if d {
|
|
1320
|
+
return (unwrap d)
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
this.warn(("num-" + name) ("the value \"" + raw + "\" on " + name + " is not a plain number, and this profile resolves no units; it was treated as unspecified"))
|
|
1324
|
+
return fallback
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
; --------------------------------------------------------------------------
|
|
1328
|
+
; Scanning primitives
|
|
1329
|
+
; --------------------------------------------------------------------------
|
|
1330
|
+
|
|
1331
|
+
; Move `pos` to the next '<' that starts a tag, stepping over comments,
|
|
1332
|
+
; CDATA, processing instructions and declarations on the way. Returns the
|
|
1333
|
+
; offset of that '<', or -1 at end of input.
|
|
1334
|
+
fn findTagStart:int () {
|
|
1335
|
+
while (pos < len) {
|
|
1336
|
+
def c:int (charAt src pos)
|
|
1337
|
+
if (c != 60) {
|
|
1338
|
+
pos = (pos + 1)
|
|
1339
|
+
} {
|
|
1340
|
+
; <!-- comment -->
|
|
1341
|
+
if (this.matchesAt((pos + 1) "!--")) {
|
|
1342
|
+
def end:int (this.findString((pos + 4) "-->"))
|
|
1343
|
+
if (end < 0) {
|
|
1344
|
+
pos = len
|
|
1345
|
+
return -1
|
|
1346
|
+
}
|
|
1347
|
+
pos = (end + 3)
|
|
1348
|
+
} {
|
|
1349
|
+
; <![CDATA[ ... ]]>
|
|
1350
|
+
if (this.matchesAt((pos + 1) "![CDATA[")) {
|
|
1351
|
+
def cend:int (this.findString((pos + 9) "]]>"))
|
|
1352
|
+
if (cend < 0) {
|
|
1353
|
+
pos = len
|
|
1354
|
+
return -1
|
|
1355
|
+
}
|
|
1356
|
+
pos = (cend + 3)
|
|
1357
|
+
} {
|
|
1358
|
+
; <?xml ... ?>
|
|
1359
|
+
if (this.matchesAt((pos + 1) "?")) {
|
|
1360
|
+
def pend:int (this.findString((pos + 2) "?>"))
|
|
1361
|
+
if (pend < 0) {
|
|
1362
|
+
pos = len
|
|
1363
|
+
return -1
|
|
1364
|
+
}
|
|
1365
|
+
pos = (pend + 2)
|
|
1366
|
+
} {
|
|
1367
|
+
; <!DOCTYPE ...>
|
|
1368
|
+
if (this.matchesAt((pos + 1) "!")) {
|
|
1369
|
+
this.skipDeclaration()
|
|
1370
|
+
if aborted {
|
|
1371
|
+
return -1
|
|
1372
|
+
}
|
|
1373
|
+
} {
|
|
1374
|
+
return pos
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return -1
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
; A `<!...>` declaration. The internal subset — the `[ ... ]` part of a
|
|
1385
|
+
; DOCTYPE — is where entity declarations live, and this parser implements no
|
|
1386
|
+
; entities at all, so a document carrying one is refused rather than parsed
|
|
1387
|
+
; with the entities left as literal text. That is the §8 rule "no XML entity
|
|
1388
|
+
; expansion" enforced at the only place it could enter: billion-laughs and
|
|
1389
|
+
; XXE are ruled out by there being nothing to expand.
|
|
1390
|
+
fn skipDeclaration:void () {
|
|
1391
|
+
def i:int (pos + 2)
|
|
1392
|
+
while (i < len) {
|
|
1393
|
+
def c:int (charAt src i)
|
|
1394
|
+
if (c == 91) {
|
|
1395
|
+
this.fail("this document has a DOCTYPE internal subset, which is where entity declarations live; the importer implements no entities and will not guess at one")
|
|
1396
|
+
pos = len
|
|
1397
|
+
return
|
|
1398
|
+
}
|
|
1399
|
+
if (c == 62) {
|
|
1400
|
+
pos = (i + 1)
|
|
1401
|
+
return
|
|
1402
|
+
}
|
|
1403
|
+
i = (i + 1)
|
|
1404
|
+
}
|
|
1405
|
+
pos = len
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
; Read one tag, assuming `pos` is on its '<'. Leaves `pos` just past '>'.
|
|
1409
|
+
fn readTag:SvgTag () {
|
|
1410
|
+
def tag (new SvgTag())
|
|
1411
|
+
if (pos >= len) {
|
|
1412
|
+
return tag
|
|
1413
|
+
}
|
|
1414
|
+
def i:int (pos + 1)
|
|
1415
|
+
|
|
1416
|
+
if (i < len) {
|
|
1417
|
+
if ((charAt src i) == 47) {
|
|
1418
|
+
tag.isEnd = true
|
|
1419
|
+
i = (i + 1)
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
def nameStart:int i
|
|
1424
|
+
while (i < len) {
|
|
1425
|
+
def c:int (charAt src i)
|
|
1426
|
+
if (this.isNameChar(c)) {
|
|
1427
|
+
i = (i + 1)
|
|
1428
|
+
} {
|
|
1429
|
+
break
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
tag.name = (this.localName((substring src nameStart i)))
|
|
1433
|
+
if ((strlen tag.name) == 0) {
|
|
1434
|
+
; A '<' that starts nothing. Step over it so the scan makes
|
|
1435
|
+
; progress; the caller sees an invalid tag and stops this level.
|
|
1436
|
+
pos = (pos + 1)
|
|
1437
|
+
return tag
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
while (i < len) {
|
|
1441
|
+
i = (this.skipSpaceFrom(i))
|
|
1442
|
+
if (i >= len) {
|
|
1443
|
+
break
|
|
1444
|
+
}
|
|
1445
|
+
def c2:int (charAt src i)
|
|
1446
|
+
|
|
1447
|
+
if (c2 == 62) {
|
|
1448
|
+
i = (i + 1)
|
|
1449
|
+
tag.valid = true
|
|
1450
|
+
pos = i
|
|
1451
|
+
return tag
|
|
1452
|
+
}
|
|
1453
|
+
if (c2 == 47) {
|
|
1454
|
+
tag.selfClose = true
|
|
1455
|
+
i = (i + 1)
|
|
1456
|
+
; The '>' after '/'. Anything else is malformed; the tag still
|
|
1457
|
+
; ends here rather than running into the document.
|
|
1458
|
+
if (i < len) {
|
|
1459
|
+
if ((charAt src i) == 62) {
|
|
1460
|
+
i = (i + 1)
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
tag.valid = true
|
|
1464
|
+
pos = i
|
|
1465
|
+
return tag
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
def attrStart:int i
|
|
1469
|
+
while (i < len) {
|
|
1470
|
+
def c3:int (charAt src i)
|
|
1471
|
+
if (this.isNameChar(c3)) {
|
|
1472
|
+
i = (i + 1)
|
|
1473
|
+
} {
|
|
1474
|
+
break
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
if (i == attrStart) {
|
|
1478
|
+
; Not a name character and not a tag terminator: skip it rather
|
|
1479
|
+
; than spin.
|
|
1480
|
+
i = (i + 1)
|
|
1481
|
+
} {
|
|
1482
|
+
def attrName:string (substring src attrStart i)
|
|
1483
|
+
i = (this.skipSpaceFrom(i))
|
|
1484
|
+
def value:string ""
|
|
1485
|
+
if (i < len) {
|
|
1486
|
+
if ((charAt src i) == 61) {
|
|
1487
|
+
i = (i + 1)
|
|
1488
|
+
i = (this.skipSpaceFrom(i))
|
|
1489
|
+
if (i < len) {
|
|
1490
|
+
def q:int (charAt src i)
|
|
1491
|
+
if ((q == 34) || (q == 39)) {
|
|
1492
|
+
def vstart:int (i + 1)
|
|
1493
|
+
def vend:int (this.findFrom(src vstart q))
|
|
1494
|
+
if (vend < 0) {
|
|
1495
|
+
this.warn("unquoted" ("an attribute value on <" + tag.name + "> is missing its closing quote"))
|
|
1496
|
+
pos = len
|
|
1497
|
+
return tag
|
|
1498
|
+
}
|
|
1499
|
+
value = (this.decodeEntities((substring src vstart vend)))
|
|
1500
|
+
i = (vend + 1)
|
|
1501
|
+
} {
|
|
1502
|
+
; XML requires quoted values. HTML tolerance
|
|
1503
|
+
; here would mean guessing where the value ends.
|
|
1504
|
+
this.warn("unquoted" ("an attribute value on <" + tag.name + "> is not quoted; XML requires quotes, so it was skipped"))
|
|
1505
|
+
while (i < len) {
|
|
1506
|
+
def c4:int (charAt src i)
|
|
1507
|
+
if ((c4 == 62) || (this.isSpace(c4))) {
|
|
1508
|
+
break
|
|
1509
|
+
}
|
|
1510
|
+
i = (i + 1)
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
def a (new SvgAttr())
|
|
1517
|
+
a.name = (this.attrName(attrName))
|
|
1518
|
+
a.value = value
|
|
1519
|
+
push tag.attrs a
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
pos = len
|
|
1524
|
+
return tag
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
; Everything from here to the matching `</name>`, counting nested opens of
|
|
1528
|
+
; the same name so that a <g> inside a skipped <g> does not end it early.
|
|
1529
|
+
fn skipSubtree:void (name:string) {
|
|
1530
|
+
def depth:int 1
|
|
1531
|
+
while ((depth > 0) && (aborted == false)) {
|
|
1532
|
+
def start:int (this.findTagStart())
|
|
1533
|
+
if (start < 0) {
|
|
1534
|
+
return
|
|
1535
|
+
}
|
|
1536
|
+
def tag:SvgTag (this.readTag())
|
|
1537
|
+
if (tag.valid == false) {
|
|
1538
|
+
return
|
|
1539
|
+
}
|
|
1540
|
+
if (tag.selfClose == false) {
|
|
1541
|
+
if (tag.name == name) {
|
|
1542
|
+
if tag.isEnd {
|
|
1543
|
+
depth = (depth - 1)
|
|
1544
|
+
} {
|
|
1545
|
+
depth = (depth + 1)
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
; --------------------------------------------------------------------------
|
|
1553
|
+
; Small string helpers
|
|
1554
|
+
; --------------------------------------------------------------------------
|
|
1555
|
+
|
|
1556
|
+
; The five entities XML predefines, and nothing else. They cannot nest or
|
|
1557
|
+
; recurse, so expanding them costs nothing in safety; every other `&name;`
|
|
1558
|
+
; is left exactly as written, with one warning, because a document that
|
|
1559
|
+
; needs a custom entity needs an entity machinery this parser deliberately
|
|
1560
|
+
; does not have.
|
|
1561
|
+
fn decodeEntities:string (s:string) {
|
|
1562
|
+
if ((indexOf s "&") < 0) {
|
|
1563
|
+
return s
|
|
1564
|
+
}
|
|
1565
|
+
def out:string ""
|
|
1566
|
+
def i:int 0
|
|
1567
|
+
def n:int (strlen s)
|
|
1568
|
+
while (i < n) {
|
|
1569
|
+
def c:int (charAt s i)
|
|
1570
|
+
if (c != 38) {
|
|
1571
|
+
out = (out + (strfromcode c))
|
|
1572
|
+
i = (i + 1)
|
|
1573
|
+
} {
|
|
1574
|
+
def semi:int (this.findFrom(s i 59))
|
|
1575
|
+
def handled:boolean false
|
|
1576
|
+
if (semi > i) {
|
|
1577
|
+
def ent:string (substring s i (semi + 1))
|
|
1578
|
+
if (ent == "&") {
|
|
1579
|
+
out = (out + "&")
|
|
1580
|
+
handled = true
|
|
1581
|
+
}
|
|
1582
|
+
if (ent == "<") {
|
|
1583
|
+
out = (out + "<")
|
|
1584
|
+
handled = true
|
|
1585
|
+
}
|
|
1586
|
+
if (ent == ">") {
|
|
1587
|
+
out = (out + ">")
|
|
1588
|
+
handled = true
|
|
1589
|
+
}
|
|
1590
|
+
if (ent == """) {
|
|
1591
|
+
out = (out + "\"")
|
|
1592
|
+
handled = true
|
|
1593
|
+
}
|
|
1594
|
+
if (ent == "'") {
|
|
1595
|
+
out = (out + "'")
|
|
1596
|
+
handled = true
|
|
1597
|
+
}
|
|
1598
|
+
if handled {
|
|
1599
|
+
i = (semi + 1)
|
|
1600
|
+
} {
|
|
1601
|
+
this.warn("entity" "only the five predefined XML entities are expanded; any other reference is left as written")
|
|
1602
|
+
out = (out + "&")
|
|
1603
|
+
i = (i + 1)
|
|
1604
|
+
}
|
|
1605
|
+
} {
|
|
1606
|
+
out = (out + "&")
|
|
1607
|
+
i = (i + 1)
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
return out
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
; "svg:path" -> "path". Namespace prefixes are common in exported files and
|
|
1615
|
+
; carry no meaning this profile acts on.
|
|
1616
|
+
fn localName:string (raw:string) {
|
|
1617
|
+
def colon:int (indexOf raw ":")
|
|
1618
|
+
if (colon < 0) {
|
|
1619
|
+
return raw
|
|
1620
|
+
}
|
|
1621
|
+
return (substring raw (colon + 1) (strlen raw))
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
; Attribute names keep their prefix only where the prefix is the name:
|
|
1625
|
+
; `xlink:href` is how most exporters still write a reference.
|
|
1626
|
+
fn attrName:string (raw:string) {
|
|
1627
|
+
if ((indexOf raw "xlink:") == 0) {
|
|
1628
|
+
return raw
|
|
1629
|
+
}
|
|
1630
|
+
def colon:int (indexOf raw ":")
|
|
1631
|
+
if (colon < 0) {
|
|
1632
|
+
return raw
|
|
1633
|
+
}
|
|
1634
|
+
return (substring raw (colon + 1) (strlen raw))
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
fn isSpace:boolean (c:int) {
|
|
1638
|
+
if (c == 32) { return true }
|
|
1639
|
+
if (c == 9) { return true }
|
|
1640
|
+
if (c == 10) { return true }
|
|
1641
|
+
if (c == 13) { return true }
|
|
1642
|
+
return false
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
fn isNameChar:boolean (c:int) {
|
|
1646
|
+
if ((c >= 65) && (c <= 90)) { return true }
|
|
1647
|
+
if ((c >= 97) && (c <= 122)) { return true }
|
|
1648
|
+
if ((c >= 48) && (c <= 57)) { return true }
|
|
1649
|
+
if (c == 58) { return true }
|
|
1650
|
+
if (c == 45) { return true }
|
|
1651
|
+
if (c == 95) { return true }
|
|
1652
|
+
if (c == 46) { return true }
|
|
1653
|
+
return false
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
fn skipSpaceFrom:int (from:int) {
|
|
1657
|
+
def i:int from
|
|
1658
|
+
while (i < len) {
|
|
1659
|
+
if (this.isSpace((charAt src i))) {
|
|
1660
|
+
i = (i + 1)
|
|
1661
|
+
} {
|
|
1662
|
+
break
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
return i
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
fn findFrom:int (s:string from:int ch:int) {
|
|
1669
|
+
def i:int from
|
|
1670
|
+
def n:int (strlen s)
|
|
1671
|
+
while (i < n) {
|
|
1672
|
+
if ((charAt s i) == ch) {
|
|
1673
|
+
return i
|
|
1674
|
+
}
|
|
1675
|
+
i = (i + 1)
|
|
1676
|
+
}
|
|
1677
|
+
return -1
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
fn findString:int (from:int needle:string) {
|
|
1681
|
+
def nl:int (strlen needle)
|
|
1682
|
+
def i:int from
|
|
1683
|
+
while ((i + nl) <= len) {
|
|
1684
|
+
if (this.matchesAt(i needle)) {
|
|
1685
|
+
return i
|
|
1686
|
+
}
|
|
1687
|
+
i = (i + 1)
|
|
1688
|
+
}
|
|
1689
|
+
return -1
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
fn matchesAt:boolean (at:int needle:string) {
|
|
1693
|
+
def nl:int (strlen needle)
|
|
1694
|
+
if ((at + nl) > len) {
|
|
1695
|
+
return false
|
|
1696
|
+
}
|
|
1697
|
+
return ((substring src at (at + nl)) == needle)
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
fn splitOn:[string] (s:string sep:int) {
|
|
1701
|
+
def out:[string]
|
|
1702
|
+
def n:int (strlen s)
|
|
1703
|
+
def start:int 0
|
|
1704
|
+
def i:int 0
|
|
1705
|
+
while (i < n) {
|
|
1706
|
+
if ((charAt s i) == sep) {
|
|
1707
|
+
push out (substring s start i)
|
|
1708
|
+
start = (i + 1)
|
|
1709
|
+
}
|
|
1710
|
+
i = (i + 1)
|
|
1711
|
+
}
|
|
1712
|
+
push out (substring s start n)
|
|
1713
|
+
return out
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
; Numbers separated by whitespace and/or commas, which is how points lists
|
|
1717
|
+
; and transform arguments are both written. A token that is not a number
|
|
1718
|
+
; ends the list rather than being read as zero — inventing a coordinate is
|
|
1719
|
+
; how malformed data turns into geometry that looks deliberate.
|
|
1720
|
+
fn parseNumberList:[double] (s:string) {
|
|
1721
|
+
def out:[double]
|
|
1722
|
+
def toks:[string] (VectorViewBox.splitTokens(s))
|
|
1723
|
+
def k:int 0
|
|
1724
|
+
while (k < (array_length toks)) {
|
|
1725
|
+
def d@(optional):double (str2double (itemAt toks k))
|
|
1726
|
+
if d {
|
|
1727
|
+
push out (unwrap d)
|
|
1728
|
+
} {
|
|
1729
|
+
this.warn(("numlist-" + (itemAt toks k)) ("\"" + (itemAt toks k) + "\" is not a number; the rest of that list was not read"))
|
|
1730
|
+
return out
|
|
1731
|
+
}
|
|
1732
|
+
k = (k + 1)
|
|
1733
|
+
}
|
|
1734
|
+
return out
|
|
1735
|
+
}
|
|
1736
|
+
}
|