@velarscript/web 0.30.1 → 0.32.0
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/README.md +1 -1
- package/dist/analysis/calls/intrinsics/config.d.ts +10 -0
- package/dist/analysis/calls/intrinsics/config.d.ts.map +1 -0
- package/dist/analysis/calls/intrinsics/config.js +19 -0
- package/dist/analysis/calls/intrinsics/config.js.map +1 -0
- package/dist/analysis/calls/intrinsics/forms.d.ts +10 -0
- package/dist/analysis/calls/intrinsics/forms.d.ts.map +1 -0
- package/dist/analysis/calls/intrinsics/forms.js +38 -0
- package/dist/analysis/calls/intrinsics/forms.js.map +1 -0
- package/dist/analysis/calls/intrinsics/http.d.ts +10 -0
- package/dist/analysis/calls/intrinsics/http.d.ts.map +1 -0
- package/dist/analysis/calls/intrinsics/http.js +37 -0
- package/dist/analysis/calls/intrinsics/http.js.map +1 -0
- package/dist/analysis/calls/intrinsics/reactive.d.ts +10 -0
- package/dist/analysis/calls/intrinsics/reactive.d.ts.map +1 -0
- package/dist/analysis/calls/intrinsics/reactive.js +30 -0
- package/dist/analysis/calls/intrinsics/reactive.js.map +1 -0
- package/dist/analysis/calls/intrinsics/storage.d.ts +12 -0
- package/dist/analysis/calls/intrinsics/storage.d.ts.map +1 -0
- package/dist/analysis/calls/intrinsics/storage.js +86 -0
- package/dist/analysis/calls/intrinsics/storage.js.map +1 -0
- package/dist/analysis/calls/intrinsics/web.d.ts +10 -0
- package/dist/analysis/calls/intrinsics/web.d.ts.map +1 -0
- package/dist/analysis/calls/intrinsics/web.js +100 -0
- package/dist/analysis/calls/intrinsics/web.js.map +1 -0
- package/dist/analysis/calls/intrinsics.d.ts +13 -0
- package/dist/analysis/calls/intrinsics.d.ts.map +1 -0
- package/dist/analysis/calls/intrinsics.js +31 -0
- package/dist/analysis/calls/intrinsics.js.map +1 -0
- package/dist/analysis/components/contracts.d.ts +19 -0
- package/dist/analysis/components/contracts.d.ts.map +1 -0
- package/dist/analysis/components/contracts.js +83 -0
- package/dist/analysis/components/contracts.js.map +1 -0
- package/dist/analysis/components/declaration.d.ts +4 -0
- package/dist/analysis/components/declaration.d.ts.map +1 -0
- package/dist/analysis/components/declaration.js +163 -0
- package/dist/analysis/components/declaration.js.map +1 -0
- package/dist/analysis/components/host.d.ts +61 -0
- package/dist/analysis/components/host.d.ts.map +1 -0
- package/dist/analysis/components/host.js +22 -0
- package/dist/analysis/components/host.js.map +1 -0
- package/dist/analysis/components/lifecycle.d.ts +22 -0
- package/dist/analysis/components/lifecycle.d.ts.map +1 -0
- package/dist/analysis/components/lifecycle.js +33 -0
- package/dist/analysis/components/lifecycle.js.map +1 -0
- package/dist/analysis/components/ownership.d.ts +74 -0
- package/dist/analysis/components/ownership.d.ts.map +1 -0
- package/dist/analysis/components/ownership.js +106 -0
- package/dist/analysis/components/ownership.js.map +1 -0
- package/dist/analysis/declarations.d.ts +99 -0
- package/dist/analysis/declarations.d.ts.map +1 -0
- package/dist/analysis/declarations.js +176 -0
- package/dist/analysis/declarations.js.map +1 -0
- package/dist/analysis/inference.d.ts +67 -0
- package/dist/analysis/inference.d.ts.map +1 -0
- package/dist/analysis/inference.js +236 -0
- package/dist/analysis/inference.js.map +1 -0
- package/dist/analysis/jsx/attributes.d.ts +103 -0
- package/dist/analysis/jsx/attributes.d.ts.map +1 -0
- package/dist/analysis/jsx/attributes.js +601 -0
- package/dist/analysis/jsx/attributes.js.map +1 -0
- package/dist/analysis/jsx/elements.d.ts +17 -0
- package/dist/analysis/jsx/elements.d.ts.map +1 -0
- package/dist/analysis/jsx/elements.js +213 -0
- package/dist/analysis/jsx/elements.js.map +1 -0
- package/dist/analysis/jsx/host.d.ts +61 -0
- package/dist/analysis/jsx/host.d.ts.map +1 -0
- package/dist/analysis/jsx/host.js +15 -0
- package/dist/analysis/jsx/host.js.map +1 -0
- package/dist/analysis/jsx/keys.d.ts +15 -0
- package/dist/analysis/jsx/keys.d.ts.map +1 -0
- package/dist/analysis/jsx/keys.js +67 -0
- package/dist/analysis/jsx/keys.js.map +1 -0
- package/dist/analysis/jsx/security.d.ts +29 -0
- package/dist/analysis/jsx/security.d.ts.map +1 -0
- package/dist/analysis/jsx/security.js +61 -0
- package/dist/analysis/jsx/security.js.map +1 -0
- package/dist/analysis/jsx-detection.d.ts +49 -0
- package/dist/analysis/jsx-detection.d.ts.map +1 -0
- package/dist/analysis/jsx-detection.js +196 -0
- package/dist/analysis/jsx-detection.js.map +1 -0
- package/dist/analysis/keyed-rebuild.d.ts +148 -0
- package/dist/analysis/keyed-rebuild.d.ts.map +1 -0
- package/dist/analysis/keyed-rebuild.js +299 -0
- package/dist/analysis/keyed-rebuild.js.map +1 -0
- package/dist/analysis/keyframes-analysis.d.ts +6 -0
- package/dist/analysis/keyframes-analysis.d.ts.map +1 -0
- package/dist/analysis/keyframes-analysis.js +79 -0
- package/dist/analysis/keyframes-analysis.js.map +1 -0
- package/dist/analysis/look/builders.d.ts +29 -0
- package/dist/analysis/look/builders.d.ts.map +1 -0
- package/dist/analysis/look/builders.js +168 -0
- package/dist/analysis/look/builders.js.map +1 -0
- package/dist/analysis/look/conditions.d.ts +13 -0
- package/dist/analysis/look/conditions.d.ts.map +1 -0
- package/dist/analysis/look/conditions.js +84 -0
- package/dist/analysis/look/conditions.js.map +1 -0
- package/dist/analysis/look/edits.d.ts +25 -0
- package/dist/analysis/look/edits.d.ts.map +1 -0
- package/dist/analysis/look/edits.js +57 -0
- package/dist/analysis/look/edits.js.map +1 -0
- package/dist/analysis/look/entries.d.ts +56 -0
- package/dist/analysis/look/entries.d.ts.map +1 -0
- package/dist/analysis/look/entries.js +223 -0
- package/dist/analysis/look/entries.js.map +1 -0
- package/dist/analysis/look/host.d.ts +47 -0
- package/dist/analysis/look/host.d.ts.map +1 -0
- package/dist/analysis/look/host.js +16 -0
- package/dist/analysis/look/host.js.map +1 -0
- package/dist/analysis/look/tokens.d.ts +75 -0
- package/dist/analysis/look/tokens.d.ts.map +1 -0
- package/dist/analysis/look/tokens.js +151 -0
- package/dist/analysis/look/tokens.js.map +1 -0
- package/dist/analysis/look/values.d.ts +45 -0
- package/dist/analysis/look/values.d.ts.map +1 -0
- package/dist/analysis/look/values.js +148 -0
- package/dist/analysis/look/values.js.map +1 -0
- package/dist/analysis/look-conditions.d.ts +47 -0
- package/dist/analysis/look-conditions.d.ts.map +1 -0
- package/dist/analysis/look-conditions.js +157 -0
- package/dist/analysis/look-conditions.js.map +1 -0
- package/dist/analysis/look-sites.d.ts +54 -0
- package/dist/analysis/look-sites.d.ts.map +1 -0
- package/dist/analysis/look-sites.js +104 -0
- package/dist/analysis/look-sites.js.map +1 -0
- package/dist/analysis/look-vocabulary-guidance.d.ts +76 -0
- package/dist/analysis/look-vocabulary-guidance.d.ts.map +1 -0
- package/dist/analysis/look-vocabulary-guidance.js +338 -0
- package/dist/analysis/look-vocabulary-guidance.js.map +1 -0
- package/dist/analysis/media-conditions.d.ts +33 -0
- package/dist/analysis/media-conditions.d.ts.map +1 -0
- package/dist/analysis/media-conditions.js +92 -0
- package/dist/analysis/media-conditions.js.map +1 -0
- package/dist/analysis/program-passes.d.ts +106 -0
- package/dist/analysis/program-passes.d.ts.map +1 -0
- package/dist/analysis/program-passes.js +149 -0
- package/dist/analysis/program-passes.js.map +1 -0
- package/dist/analysis/reactivity/bindings.d.ts +37 -0
- package/dist/analysis/reactivity/bindings.d.ts.map +1 -0
- package/dist/analysis/reactivity/bindings.js +88 -0
- package/dist/analysis/reactivity/bindings.js.map +1 -0
- package/dist/analysis/reactivity/derived.d.ts +39 -0
- package/dist/analysis/reactivity/derived.d.ts.map +1 -0
- package/dist/analysis/reactivity/derived.js +80 -0
- package/dist/analysis/reactivity/derived.js.map +1 -0
- package/dist/analysis/reactivity/host.d.ts +38 -0
- package/dist/analysis/reactivity/host.d.ts.map +1 -0
- package/dist/analysis/reactivity/host.js +14 -0
- package/dist/analysis/reactivity/host.js.map +1 -0
- package/dist/analysis/reactivity/retired-accessors.d.ts +82 -0
- package/dist/analysis/reactivity/retired-accessors.d.ts.map +1 -0
- package/dist/analysis/reactivity/retired-accessors.js +118 -0
- package/dist/analysis/reactivity/retired-accessors.js.map +1 -0
- package/dist/analysis/renderable.d.ts +47 -0
- package/dist/analysis/renderable.d.ts.map +1 -0
- package/dist/analysis/renderable.js +105 -0
- package/dist/analysis/renderable.js.map +1 -0
- package/dist/analysis/routes.d.ts +64 -0
- package/dist/analysis/routes.d.ts.map +1 -0
- package/dist/analysis/routes.js +182 -0
- package/dist/analysis/routes.js.map +1 -0
- package/dist/analysis/url-attributes.d.ts +12 -0
- package/dist/analysis/url-attributes.d.ts.map +1 -0
- package/dist/analysis/url-attributes.js +48 -0
- package/dist/analysis/url-attributes.js.map +1 -0
- package/dist/analysis/watch-cycles.d.ts +102 -0
- package/dist/analysis/watch-cycles.d.ts.map +1 -1
- package/dist/analysis/watch-cycles.js +240 -1
- package/dist/analysis/watch-cycles.js.map +1 -1
- package/dist/analysis/watch-subject.d.ts +45 -0
- package/dist/analysis/watch-subject.d.ts.map +1 -0
- package/dist/analysis/watch-subject.js +272 -0
- package/dist/analysis/watch-subject.js.map +1 -0
- package/dist/analysis/web-types.d.ts +27 -0
- package/dist/analysis/web-types.d.ts.map +1 -0
- package/dist/analysis/web-types.js +170 -0
- package/dist/analysis/web-types.js.map +1 -0
- package/dist/analyzer.d.ts +18 -617
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +208 -4853
- package/dist/analyzer.js.map +1 -1
- package/dist/compiler.d.ts +20 -0
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +29 -864
- package/dist/compiler.js.map +1 -1
- package/dist/documentation.d.ts +28 -0
- package/dist/documentation.d.ts.map +1 -0
- package/dist/documentation.js +67 -0
- package/dist/documentation.js.map +1 -0
- package/dist/editor/completions.d.ts +12 -0
- package/dist/editor/completions.d.ts.map +1 -0
- package/dist/editor/completions.js +6 -0
- package/dist/editor/completions.js.map +1 -0
- package/dist/editor/jsx-completion.d.ts +12 -0
- package/dist/editor/jsx-completion.d.ts.map +1 -0
- package/dist/editor/jsx-completion.js +137 -0
- package/dist/editor/jsx-completion.js.map +1 -0
- package/dist/editor/look-completion.d.ts +29 -0
- package/dist/editor/look-completion.d.ts.map +1 -0
- package/dist/editor/look-completion.js +159 -0
- package/dist/editor/look-completion.js.map +1 -0
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +12 -288
- package/dist/editor.js.map +1 -1
- package/dist/emit/components.d.ts +33 -0
- package/dist/emit/components.d.ts.map +1 -0
- package/dist/emit/components.js +135 -0
- package/dist/emit/components.js.map +1 -0
- package/dist/emit/jsx.d.ts +57 -0
- package/dist/emit/jsx.d.ts.map +1 -0
- package/dist/emit/jsx.js +384 -0
- package/dist/emit/jsx.js.map +1 -0
- package/dist/emit/look-css.d.ts +44 -0
- package/dist/emit/look-css.d.ts.map +1 -0
- package/dist/emit/look-css.js +177 -0
- package/dist/emit/look-css.js.map +1 -0
- package/dist/emit/look.d.ts +39 -0
- package/dist/emit/look.d.ts.map +1 -0
- package/dist/emit/look.js +218 -0
- package/dist/emit/look.js.map +1 -0
- package/dist/emit/runtime-imports.d.ts +40 -0
- package/dist/emit/runtime-imports.d.ts.map +1 -0
- package/dist/emit/runtime-imports.js +184 -0
- package/dist/emit/runtime-imports.js.map +1 -0
- package/dist/emitter.d.ts +15 -68
- package/dist/emitter.d.ts.map +1 -1
- package/dist/emitter.js +79 -1008
- package/dist/emitter.js.map +1 -1
- package/dist/look/animation.d.ts +7 -0
- package/dist/look/animation.d.ts.map +1 -0
- package/dist/look/animation.js +13 -0
- package/dist/look/animation.js.map +1 -0
- package/dist/look/builders.d.ts +35 -0
- package/dist/look/builders.d.ts.map +1 -0
- package/dist/look/builders.js +74 -0
- package/dist/look/builders.js.map +1 -0
- package/dist/look/css-functions.d.ts +28 -0
- package/dist/look/css-functions.d.ts.map +1 -0
- package/dist/look/css-functions.js +54 -0
- package/dist/look/css-functions.js.map +1 -0
- package/dist/look/keyword-sets.d.ts +41 -0
- package/dist/look/keyword-sets.d.ts.map +1 -0
- package/dist/look/keyword-sets.js +117 -0
- package/dist/look/keyword-sets.js.map +1 -0
- package/dist/look/keywords.d.ts +55 -0
- package/dist/look/keywords.d.ts.map +1 -0
- package/dist/look/keywords.js +387 -0
- package/dist/look/keywords.js.map +1 -0
- package/dist/look/media.d.ts +19 -0
- package/dist/look/media.d.ts.map +1 -0
- package/dist/look/media.js +27 -0
- package/dist/look/media.js.map +1 -0
- package/dist/look/naming.d.ts +9 -0
- package/dist/look/naming.d.ts.map +1 -0
- package/dist/look/naming.js +45 -0
- package/dist/look/naming.js.map +1 -0
- package/dist/look/properties.d.ts +16 -0
- package/dist/look/properties.d.ts.map +1 -0
- package/dist/look/properties.js +52 -0
- package/dist/look/properties.js.map +1 -0
- package/dist/look/shorthands.d.ts +25 -0
- package/dist/look/shorthands.d.ts.map +1 -0
- package/dist/look/shorthands.js +130 -0
- package/dist/look/shorthands.js.map +1 -0
- package/dist/look/tokens.d.ts +38 -0
- package/dist/look/tokens.d.ts.map +1 -0
- package/dist/look/tokens.js +59 -0
- package/dist/look/tokens.js.map +1 -0
- package/dist/look/units.d.ts +24 -0
- package/dist/look/units.d.ts.map +1 -0
- package/dist/look/units.js +33 -0
- package/dist/look/units.js.map +1 -0
- package/dist/look.d.ts +32 -227
- package/dist/look.d.ts.map +1 -1
- package/dist/look.js +28 -933
- package/dist/look.js.map +1 -1
- package/dist/module-roster.d.ts +16 -0
- package/dist/module-roster.d.ts.map +1 -0
- package/dist/module-roster.js +28 -0
- package/dist/module-roster.js.map +1 -0
- package/dist/modules/app.d.ts +9 -0
- package/dist/modules/app.d.ts.map +1 -0
- package/dist/modules/app.js +20 -0
- package/dist/modules/app.js.map +1 -0
- package/dist/modules/browser-test.d.ts +9 -0
- package/dist/modules/browser-test.d.ts.map +1 -0
- package/dist/modules/browser-test.js +67 -0
- package/dist/modules/browser-test.js.map +1 -0
- package/dist/modules/browser.d.ts +10 -0
- package/dist/modules/browser.d.ts.map +1 -0
- package/dist/modules/browser.js +89 -0
- package/dist/modules/browser.js.map +1 -0
- package/dist/modules/config.d.ts +9 -0
- package/dist/modules/config.d.ts.map +1 -0
- package/dist/modules/config.js +14 -0
- package/dist/modules/config.js.map +1 -0
- package/dist/modules/files.d.ts +9 -0
- package/dist/modules/files.d.ts.map +1 -0
- package/dist/modules/files.js +16 -0
- package/dist/modules/files.js.map +1 -0
- package/dist/modules/forms.d.ts +9 -0
- package/dist/modules/forms.d.ts.map +1 -0
- package/dist/modules/forms.js +26 -0
- package/dist/modules/forms.js.map +1 -0
- package/dist/modules/http.d.ts +9 -0
- package/dist/modules/http.d.ts.map +1 -0
- package/dist/modules/http.js +127 -0
- package/dist/modules/http.js.map +1 -0
- package/dist/modules/look.d.ts +9 -0
- package/dist/modules/look.d.ts.map +1 -0
- package/dist/modules/look.js +58 -0
- package/dist/modules/look.js.map +1 -0
- package/dist/modules/realtime.d.ts +9 -0
- package/dist/modules/realtime.d.ts.map +1 -0
- package/dist/modules/realtime.js +163 -0
- package/dist/modules/realtime.js.map +1 -0
- package/dist/modules/storage.d.ts +9 -0
- package/dist/modules/storage.d.ts.map +1 -0
- package/dist/modules/storage.js +59 -0
- package/dist/modules/storage.js.map +1 -0
- package/dist/modules/types.d.ts +78 -0
- package/dist/modules/types.d.ts.map +1 -0
- package/dist/modules/types.js +154 -0
- package/dist/modules/types.js.map +1 -0
- package/dist/modules/web.d.ts +9 -0
- package/dist/modules/web.d.ts.map +1 -0
- package/dist/modules/web.js +63 -0
- package/dist/modules/web.js.map +1 -0
- package/dist/modules/websocket.d.ts +11 -0
- package/dist/modules/websocket.d.ts.map +1 -0
- package/dist/modules/websocket.js +75 -0
- package/dist/modules/websocket.js.map +1 -0
- package/dist/parser/expressions/jsx.d.ts +17 -0
- package/dist/parser/expressions/jsx.d.ts.map +1 -0
- package/dist/parser/expressions/jsx.js +43 -0
- package/dist/parser/expressions/jsx.js.map +1 -0
- package/dist/parser/expressions/keyframes.d.ts +15 -0
- package/dist/parser/expressions/keyframes.d.ts.map +1 -0
- package/dist/parser/expressions/keyframes.js +33 -0
- package/dist/parser/expressions/keyframes.js.map +1 -0
- package/dist/parser/expressions/look.d.ts +16 -0
- package/dist/parser/expressions/look.d.ts.map +1 -0
- package/dist/parser/expressions/look.js +37 -0
- package/dist/parser/expressions/look.js.map +1 -0
- package/dist/parser/expressions/visual-block.d.ts +17 -0
- package/dist/parser/expressions/visual-block.d.ts.map +1 -0
- package/dist/parser/expressions/visual-block.js +21 -0
- package/dist/parser/expressions/visual-block.js.map +1 -0
- package/dist/parser/keyframes-source.d.ts +24 -0
- package/dist/parser/keyframes-source.d.ts.map +1 -0
- package/dist/parser/keyframes-source.js +133 -0
- package/dist/parser/keyframes-source.js.map +1 -0
- package/dist/parser/look-source.d.ts +25 -0
- package/dist/parser/look-source.d.ts.map +1 -0
- package/dist/parser/look-source.js +189 -0
- package/dist/parser/look-source.js.map +1 -0
- package/dist/parser/spans.d.ts +24 -0
- package/dist/parser/spans.d.ts.map +1 -0
- package/dist/parser/spans.js +113 -0
- package/dist/parser/spans.js.map +1 -0
- package/dist/parser/statements/components.d.ts +37 -0
- package/dist/parser/statements/components.d.ts.map +1 -0
- package/dist/parser/statements/components.js +185 -0
- package/dist/parser/statements/components.js.map +1 -0
- package/dist/parser/statements/heads.d.ts +38 -0
- package/dist/parser/statements/heads.d.ts.map +1 -0
- package/dist/parser/statements/heads.js +55 -0
- package/dist/parser/statements/heads.js.map +1 -0
- package/dist/parser/statements/imports.d.ts +9 -0
- package/dist/parser/statements/imports.d.ts.map +1 -0
- package/dist/parser/statements/imports.js +31 -0
- package/dist/parser/statements/imports.js.map +1 -0
- package/dist/parser/statements/reactive.d.ts +31 -0
- package/dist/parser/statements/reactive.d.ts.map +1 -0
- package/dist/parser/statements/reactive.js +68 -0
- package/dist/parser/statements/reactive.js.map +1 -0
- package/dist/parser/statements/unsafe-css.d.ts +21 -0
- package/dist/parser/statements/unsafe-css.d.ts.map +1 -0
- package/dist/parser/statements/unsafe-css.js +42 -0
- package/dist/parser/statements/unsafe-css.js.map +1 -0
- package/dist/parser.d.ts +37 -50
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +98 -882
- package/dist/parser.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every attribute a JSX element can carry: the visual directives (`look`,
|
|
3
|
+
* `look:`, `style:`, `class`), the `bind:` family, the `on:` events and the
|
|
4
|
+
* `on`-prefixed names a native element reserves, `ref`, `key`, and the native
|
|
5
|
+
* attribute names and values the DOM and ARIA rosters answer for.
|
|
6
|
+
*
|
|
7
|
+
* D115 P4 R3b. `analyzeNativeJsxAttribute` stays the one guarded chain the
|
|
8
|
+
* order of these rules lives in — the name check sits at its tail so every
|
|
9
|
+
* directive above owns its own spelling — and the arms whose bodies are long
|
|
10
|
+
* enough to read on their own are named functions below it.
|
|
11
|
+
*/
|
|
12
|
+
import { mechanicalFix } from "@velarscript/compiler";
|
|
13
|
+
import { anyType, boolType, describeType, isInvalidType, nonOptional, nullType, numberType, optionalOf, stringType, unknownType } from "@velarscript/compiler/extension";
|
|
14
|
+
import {} from "../../ast.js";
|
|
15
|
+
import { WEB_ARIA_ATTRIBUTES, WEB_ARIA_ENUMERATED_VALUES, WEB_ARIA_ROLE_SYNONYMS, WEB_ARIA_ROLES, WEB_BOOL_PRESENCE_HTML_ATTRIBUTES, WEB_HTML_ELEMENTS, WEB_MISSPELLED_ATTRIBUTES, WEB_NATIVE_ELEMENTS } from "../../elements.js";
|
|
16
|
+
import { nearestLookName } from "../../look.js";
|
|
17
|
+
import { lookContributions } from "../look-conditions.js";
|
|
18
|
+
import { lookLiteralZero, mentionsLookUnitType } from "../look-sites.js";
|
|
19
|
+
import { analyzeLookValue } from "../look/entries.js";
|
|
20
|
+
import { reportLookNumberWithoutUnit } from "../look/values.js";
|
|
21
|
+
import { bindTargetGuidance, diagnostic, htmlEventHandlerAttributes, LOOK_PROPERTY_TYPES, nativeDomEventNames, webEventType } from "../web-types.js";
|
|
22
|
+
import {} from "./host.js";
|
|
23
|
+
import { reportUrlAttributeScheme } from "./security.js";
|
|
24
|
+
export function analyzeNativeJsxAttribute(host, expression, attribute) {
|
|
25
|
+
const value = attribute.value;
|
|
26
|
+
reportUrlAttributeScheme(host, attribute);
|
|
27
|
+
const eventName = attribute.name.startsWith("on:") ? attribute.name.slice(3).split(".")[0] ?? "" : "";
|
|
28
|
+
const expectedEvent = eventName ? webEventType(eventName) : null;
|
|
29
|
+
// GRM-A4: the declared handler type returns null. `() => {}` after a fat
|
|
30
|
+
// arrow is an empty-record factory rather than an empty block, so a handler
|
|
31
|
+
// position that accepted any result silently accepted that record.
|
|
32
|
+
const eventHandlerType = expectedEvent ? { kind: "function", parameters: [expectedEvent], requiredParameters: 1, result: nullType } : null;
|
|
33
|
+
// An event arrow that assigns a state binding from an event field is the
|
|
34
|
+
// hand-rolled spelling of a two-way binding: it receives bind:value
|
|
35
|
+
// guidance and skips ordinary handler inference so the guidance is not
|
|
36
|
+
// buried under a cascade from the recovered assignment.
|
|
37
|
+
const boundState = (eventName || /^on[A-Z]/u.test(attribute.name)) ? eventAssignedStateBinding(host, value) : null;
|
|
38
|
+
if (boundState) {
|
|
39
|
+
host.diagnostics.push(diagnostic("VEL5019", `Use 'bind:value={${boundState}}'; VelarScript binds input state with the bind: directive instead of assigning state from event fields`, attribute.span));
|
|
40
|
+
}
|
|
41
|
+
// A `look=` value is inferred inside its own check so an inline Look block
|
|
42
|
+
// is reported once rather than analyzed as a snapshot as well.
|
|
43
|
+
const inferred = typeof value === "string" ? stringType
|
|
44
|
+
: boundState || attribute.name === "look" ? anyType
|
|
45
|
+
: value ? host.inferExpression(value, eventHandlerType ?? unknownType) : boolType;
|
|
46
|
+
if (attribute.name === "style") {
|
|
47
|
+
host.diagnostics.push(diagnostic("VEL5041", "Raw JSX style is not supported; use style:property for a checked high-priority inline override, or prefer Look for ordinary visuals", attribute.span));
|
|
48
|
+
}
|
|
49
|
+
else if (attribute.name.startsWith("style:")) {
|
|
50
|
+
analyzeInlineVisualAttribute(host, attribute, inferred, "style");
|
|
51
|
+
}
|
|
52
|
+
else if (attribute.name === "look") {
|
|
53
|
+
analyzeJsxLookAttribute(host, attribute);
|
|
54
|
+
}
|
|
55
|
+
else if (attribute.name.startsWith("look:")) {
|
|
56
|
+
analyzeInlineVisualAttribute(host, attribute, inferred, "look");
|
|
57
|
+
}
|
|
58
|
+
else if (attribute.name === "class") {
|
|
59
|
+
if (!host.isClassInput(inferred))
|
|
60
|
+
host.diagnostics.push(diagnostic("VEL5040", `JSX class requires string, string?, or a list of strings; received ${describeType(inferred)}`, attribute.span));
|
|
61
|
+
}
|
|
62
|
+
else if (attribute.name === "unsafe:html") {
|
|
63
|
+
if (!isInvalidType(inferred) && inferred.kind !== "any" && !host.isOptionalString(inferred)) {
|
|
64
|
+
host.diagnostics.push(diagnostic("VEL5047", `unsafe:html requires string or string?, received ${describeType(inferred)}`, attribute.span));
|
|
65
|
+
}
|
|
66
|
+
// D90 coherence-3, one step sideways: `unsafe:` is a closed prefix with
|
|
67
|
+
// exactly one member, so `unsafe:script=` is wrong by construction rather
|
|
68
|
+
// than merely unrecognised — and it used to be emitted verbatim as a dead
|
|
69
|
+
// attribute, which is the worst possible answer for a name that reads like
|
|
70
|
+
// an escape hatch the author believes they opened.
|
|
71
|
+
}
|
|
72
|
+
else if (attribute.name.startsWith("unsafe:")) {
|
|
73
|
+
host.diagnostics.push(diagnostic("VEL5015", `Unknown escape hatch '${attribute.name}'; 'unsafe:html' is the only one an element has, and it takes the HTML text as a string`, attribute.span));
|
|
74
|
+
}
|
|
75
|
+
else if (attribute.name === "bind:value") {
|
|
76
|
+
analyzeBindValue(host, expression, attribute, value, inferred);
|
|
77
|
+
}
|
|
78
|
+
else if (attribute.name === "bind:checked") {
|
|
79
|
+
analyzeBindChecked(host, expression, attribute, value, inferred);
|
|
80
|
+
}
|
|
81
|
+
else if (attribute.name === "bind:group") {
|
|
82
|
+
analyzeBindGroup(host, expression, attribute, value, inferred);
|
|
83
|
+
}
|
|
84
|
+
else if (attribute.name === "ref") {
|
|
85
|
+
analyzeRefAttribute(host, expression, attribute, value);
|
|
86
|
+
// The three branches above are the whole `bind:` family, so a fourth suffix
|
|
87
|
+
// names no binding — and it used to reach the attribute emitter and render
|
|
88
|
+
// a dead `bind:foo` attribute, silently binding nothing. Same shape as the
|
|
89
|
+
// React-spelling rule: a closed VelarScript vocabulary, so an outside name
|
|
90
|
+
// is wrong rather than unknown.
|
|
91
|
+
}
|
|
92
|
+
else if (attribute.name === "bind" || attribute.name.startsWith("bind:")) {
|
|
93
|
+
reportUnknownBinding(host, attribute);
|
|
94
|
+
// WEB-S1: the guard used to be anchored on an uppercase letter, which
|
|
95
|
+
// closed the React reflex `onClick=` and left the lowercase HTML spelling
|
|
96
|
+
// open. Both are the same attribute — an HTML attribute name is matched
|
|
97
|
+
// ASCII-case-insensitively, and `setAttribute` lowercases on an HTML
|
|
98
|
+
// element, so `onClick` and `ONCLICK` reach `onclick` too. That attribute
|
|
99
|
+
// is the one that matters: the browser compiles its value as script, so any
|
|
100
|
+
// string routed there is executable code in the application's origin, which
|
|
101
|
+
// the charter reserves for `unsafe:html`. So a native element reserves the
|
|
102
|
+
// whole `on` prefix by name rather than by a roster of handler names: a
|
|
103
|
+
// roster leaves the next handler spelling open, and every event is written
|
|
104
|
+
// with the `on:` directive anyway. The lookahead keeps that directive out.
|
|
105
|
+
}
|
|
106
|
+
else if (/^on(?!:)/iu.test(attribute.name)) {
|
|
107
|
+
reportReservedOnAttribute(host, attribute);
|
|
108
|
+
}
|
|
109
|
+
else if (attribute.name.startsWith("on:")) {
|
|
110
|
+
analyzeEventDirective(host, attribute, value, inferred, expectedEvent);
|
|
111
|
+
}
|
|
112
|
+
else if (attribute.name.startsWith("class:")) {
|
|
113
|
+
host.requireAssignable(inferred, boolType, attribute.span);
|
|
114
|
+
}
|
|
115
|
+
else if (attribute.name === "key" && !isInvalidType(inferred) && inferred.kind !== "string" && inferred.kind !== "number" && inferred.kind !== "enum" && inferred.kind !== "enumMember" && inferred.kind !== "any") {
|
|
116
|
+
host.diagnostics.push(diagnostic("VEL5022", "A JSX key must be a string, string-backed enum, or number", attribute.span));
|
|
117
|
+
// The name check sits at the tail of the chain so every directive above it
|
|
118
|
+
// — look:, class:, on:, bind:, ref, key — keeps owning its own spelling and
|
|
119
|
+
// is never read as an HTML attribute name. It reports at most once per
|
|
120
|
+
// attribute and short-circuits the value-shape message, so a React spelling
|
|
121
|
+
// is answered with its successor rather than with two half-answers.
|
|
122
|
+
}
|
|
123
|
+
else if (!reportNativeAttributeSpelling(host, expression, attribute)
|
|
124
|
+
&& !isInvalidType(inferred) && !host.isJsxAttributeValue(inferred)) {
|
|
125
|
+
host.diagnostics.push(diagnostic("VEL5047", `Native JSX attributes require text, finite numbers, bool, enums, or null; received ${describeType(inferred)}`, attribute.span));
|
|
126
|
+
}
|
|
127
|
+
if (!isInvalidType(inferred))
|
|
128
|
+
adviseNativeBooleanTextConversion(host, expression.tag, attribute);
|
|
129
|
+
if (attribute.name.startsWith("on:click") && !["button", "a", "input", "select", "textarea", "summary"].includes(expression.tag)
|
|
130
|
+
&& !expression.attributes.some((item) => item.name === "role"))
|
|
131
|
+
host.diagnostics.push(diagnostic("VEL5023", `Clickable <${expression.tag}> requires an explicit role`, expression.span));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The `look=` attribute on either host kind. A Look block written inline is
|
|
135
|
+
* reported without inferring its entries, so the one directive-level message
|
|
136
|
+
* stands alone; an empty list names the accepted family rather than rendering
|
|
137
|
+
* `List<unknown>` (LOK-I3, LOK-I6).
|
|
138
|
+
*/
|
|
139
|
+
export function analyzeJsxLookAttribute(host, attribute) {
|
|
140
|
+
const value = attribute.value;
|
|
141
|
+
if (!value) {
|
|
142
|
+
host.diagnostics.push(diagnostic("VEL5040", "JSX look requires an expression value", attribute.span));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (typeof value === "string") {
|
|
146
|
+
host.diagnostics.push(diagnostic("VEL5040", "JSX look requires an expression value such as look={cardLook}", attribute.span));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (value.kind === "ExtensionExpression:web:look") {
|
|
150
|
+
host.diagnostics.push(diagnostic("VEL5053", "An inline Look block is not supported; use look:property directives for simple overrides or extract a const Look for conditions and targets", attribute.span));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (value.kind === "ListExpression" && value.elements.length === 0) {
|
|
154
|
+
host.diagnostics.push(diagnostic("VEL5040", "JSX look accepts a Look, a Look?, or a list of Look values; an empty list composes nothing — remove the attribute", attribute.span));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const actual = host.inferExpression(value);
|
|
158
|
+
if (!host.isLookInput(actual))
|
|
159
|
+
host.diagnostics.push(diagnostic("VEL5040", `JSX look requires Look, Look?, or a list of Look values; received ${describeType(actual)}`, attribute.span));
|
|
160
|
+
if (value.kind === "ListExpression")
|
|
161
|
+
reportIndependentLookCollisions(host, value, attribute);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Composition is how a Look overrides another one: `...baseLook` puts the two
|
|
165
|
+
* in an order the reader can see, and everything after it wins. Two looks
|
|
166
|
+
* placed side by side on one element state no order at all, so a property both
|
|
167
|
+
* of them set has no answer the source gives — the winner used to fall out of
|
|
168
|
+
* whichever rule the stylesheet happened to carry last. The shape is refused
|
|
169
|
+
* rather than ordered by fiat, and the refusal names the spelling that states
|
|
170
|
+
* the order the author meant.
|
|
171
|
+
*/
|
|
172
|
+
export function reportIndependentLookCollisions(host, value, attribute) {
|
|
173
|
+
const entries = [];
|
|
174
|
+
for (const element of value.elements) {
|
|
175
|
+
if (element.kind !== "IdentifierExpression")
|
|
176
|
+
continue;
|
|
177
|
+
const look = host.lookDeclarations.get(element.name);
|
|
178
|
+
if (look)
|
|
179
|
+
entries.push({ name: element.name, look });
|
|
180
|
+
}
|
|
181
|
+
for (const [index, first] of entries.entries()) {
|
|
182
|
+
for (const second of entries.slice(index + 1)) {
|
|
183
|
+
if (first.name === second.name)
|
|
184
|
+
continue;
|
|
185
|
+
const left = lookContributions(first.look, host.lookDeclarations);
|
|
186
|
+
const right = lookContributions(second.look, host.lookDeclarations);
|
|
187
|
+
// A look that composes the other is already ordered against it: the
|
|
188
|
+
// spread says which one is the base, so nothing here is ambiguous.
|
|
189
|
+
if (left.composed.has(second.name) || right.composed.has(first.name))
|
|
190
|
+
continue;
|
|
191
|
+
const collision = [...left.properties].find((property) => right.properties.has(property));
|
|
192
|
+
if (collision === undefined)
|
|
193
|
+
continue;
|
|
194
|
+
const property = collision.slice(collision.indexOf(":") + 1);
|
|
195
|
+
const target = collision.slice(0, collision.indexOf(":"));
|
|
196
|
+
host.diagnostics.push(diagnostic("VEL5068", `Look '${first.name}' and Look '${second.name}' both set '${property}'${target ? ` on '@${target}'` : ""}, and placing them side by side states no order between them; write one Look that starts with '...${first.name}' and overrides '${property}' from there, then pass that one`, attribute.span));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export function analyzeInlineVisualAttribute(host, attribute, actual, directive) {
|
|
202
|
+
const property = attribute.name.slice(`${directive}:`.length);
|
|
203
|
+
if (!property) {
|
|
204
|
+
host.diagnostics.push(diagnostic("VEL5038", `A ${directive}: directive requires a camelCase Look property name`, attribute.span));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (attribute.value === null) {
|
|
208
|
+
host.diagnostics.push(diagnostic("VEL5040", `JSX ${directive}:${property} requires a string or expression value`, attribute.span));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const expression = typeof attribute.value === "string"
|
|
212
|
+
? { kind: "LiteralExpression", value: attribute.value, raw: JSON.stringify(attribute.value), span: attribute.span }
|
|
213
|
+
: attribute.value;
|
|
214
|
+
if (!analyzeLookValue(host, property, expression, attribute.span, directive))
|
|
215
|
+
return;
|
|
216
|
+
const expected = LOOK_PROPERTY_TYPES.get(property) ?? stringType;
|
|
217
|
+
if (mentionsLookUnitType(expected) && lookLiteralZero(expression))
|
|
218
|
+
return;
|
|
219
|
+
if (reportLookNumberWithoutUnit(host, property, actual, expected, expression.span))
|
|
220
|
+
return;
|
|
221
|
+
if (expected.kind !== "unknown")
|
|
222
|
+
host.requireAssignable(actual, optionalOf(expected), expression.span);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* A14: `condition ? "true" : "false"` is the expanded spelling of the Core
|
|
226
|
+
* text conversion `str(condition)`. Keep that conversion explicit at a DOM
|
|
227
|
+
* text boundary instead of teaching every attribute its own coercion rule.
|
|
228
|
+
*
|
|
229
|
+
* The rule does not reach component props or HTML bool-presence attributes.
|
|
230
|
+
* A string "false" keeps such an attribute present; accepting that spelling
|
|
231
|
+
* as canonical would hide a likely presence bug whose direct expression is
|
|
232
|
+
* the bool itself.
|
|
233
|
+
*/
|
|
234
|
+
export function adviseNativeBooleanTextConversion(host, tag, attribute) {
|
|
235
|
+
if (!WEB_NATIVE_ELEMENTS.has(tag))
|
|
236
|
+
return;
|
|
237
|
+
const value = attribute.value;
|
|
238
|
+
if (!value || typeof value === "string" || !plainNativeTextAttribute(tag, attribute.name))
|
|
239
|
+
return;
|
|
240
|
+
const written = host.webSourceText.slice(value.span.start, value.span.end);
|
|
241
|
+
if (written.length === 0)
|
|
242
|
+
return;
|
|
243
|
+
if (value.kind === "ConditionalExpression"
|
|
244
|
+
&& value.thenValue.kind === "LiteralExpression" && value.thenValue.value === "true"
|
|
245
|
+
&& value.elseValue.kind === "LiteralExpression" && value.elseValue.value === "false") {
|
|
246
|
+
if (host.expandAliases(host.inferredExpressionType(value.condition)).kind !== "bool")
|
|
247
|
+
return;
|
|
248
|
+
const replacement = host.webSourceText.slice(value.condition.span.start, value.condition.span.end);
|
|
249
|
+
if (!replacement)
|
|
250
|
+
return;
|
|
251
|
+
// The condition is copied verbatim into str(...), so comments inside it
|
|
252
|
+
// survive. Only comments in discarded punctuation/branches make the
|
|
253
|
+
// mechanical rewrite unsafe.
|
|
254
|
+
const omitted = `${host.webSourceText.slice(value.span.start, value.condition.span.start)}${host.webSourceText.slice(value.condition.span.end, value.span.end)}`;
|
|
255
|
+
host.advise("A14", `Native attribute '${attribute.name}' spells this bool as text; write the equivalent str(condition) conversion directly`, value.span, omitted.includes("//") || omitted.includes("/*")
|
|
256
|
+
? undefined
|
|
257
|
+
: mechanicalFix(value.span, `str(${replacement})`, `Convert the bool with str() for '${attribute.name}'`));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** Directives and compiler-owned control attributes do not use DOM text conversion. */
|
|
261
|
+
function plainNativeTextAttribute(tag, name) {
|
|
262
|
+
if (WEB_MISSPELLED_ATTRIBUTES.has(name) || /^aria[A-Z]/u.test(name))
|
|
263
|
+
return false;
|
|
264
|
+
if (name.startsWith("aria-") && !WEB_ARIA_ATTRIBUTES.has(name))
|
|
265
|
+
return false;
|
|
266
|
+
if (WEB_HTML_ELEMENTS.has(tag) && WEB_BOOL_PRESENCE_HTML_ATTRIBUTES.has(name.toLowerCase()))
|
|
267
|
+
return false;
|
|
268
|
+
return name !== "class" && name !== "host" && name !== "key" && name !== "look" && name !== "ref"
|
|
269
|
+
&& name !== "style" && !/^(?:bind|class|look|on|style|unsafe):/u.test(name) && !/^on/iu.test(name);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* D90 coherence-3: DOM attribute names and ARIA are documented as checked
|
|
273
|
+
* surfaces, and nothing checked them — `className="panel"` compiled clean and
|
|
274
|
+
* emitted a dead attribute, while the sibling React reflex `onClick=` on the
|
|
275
|
+
* same element was already refused. So a model got a clean bill of health on
|
|
276
|
+
* exactly the half of its React habit that silently breaks the page.
|
|
277
|
+
*
|
|
278
|
+
* The rule diagnoses names that are KNOWN wrong, never names that are merely
|
|
279
|
+
* unknown. HTML lets a document carry attributes no roster can enumerate, so
|
|
280
|
+
* `foo="bar"`, `data-*`, and a framework's own prefixes stay legal; a false
|
|
281
|
+
* positive there would block a correct program, which is worse than the
|
|
282
|
+
* silence it replaces. Three closed rosters answer three closed questions:
|
|
283
|
+
* the React / JavaScript-property spellings, the ARIA attribute names (ARIA,
|
|
284
|
+
* unlike HTML, admits no custom names), and the ARIA and role vocabularies.
|
|
285
|
+
*
|
|
286
|
+
* D61 bounds the value half. Every native attribute uses the same presence
|
|
287
|
+
* rule: false/null remove it, true writes an empty value, and strings carry
|
|
288
|
+
* literal text. A token vocabulary is therefore read only against a string
|
|
289
|
+
* literal; expression values are runtime questions. A14 separately shortens
|
|
290
|
+
* an explicit bool-to-text conditional to `str(bool)` when that rewrite is
|
|
291
|
+
* provably equivalent.
|
|
292
|
+
*
|
|
293
|
+
* Returns true when it reported, so the caller does not stack a value-shape
|
|
294
|
+
* message on an attribute whose name is already answered.
|
|
295
|
+
*/
|
|
296
|
+
export function reportNativeAttributeSpelling(host, expression, attribute) {
|
|
297
|
+
// A custom element owns its own attribute vocabulary, and an unknown tag was
|
|
298
|
+
// already reported as a tag; neither is a surface these rosters describe.
|
|
299
|
+
if (!WEB_NATIVE_ELEMENTS.has(expression.tag))
|
|
300
|
+
return false;
|
|
301
|
+
const name = attribute.name;
|
|
302
|
+
// The attribute span opens on the name, so the name occupies its first
|
|
303
|
+
// `name.length` characters — the range a rename rewrites, leaving the value
|
|
304
|
+
// exactly as written.
|
|
305
|
+
const nameSpan = { start: attribute.span.start, end: attribute.span.start + name.length };
|
|
306
|
+
const rename = (write) => ({ fix: { title: `Use '${write}'`, edits: [{ span: nameSpan, text: write }] } });
|
|
307
|
+
const spelling = WEB_MISSPELLED_ATTRIBUTES.get(name);
|
|
308
|
+
if (spelling) {
|
|
309
|
+
host.diagnostics.push({
|
|
310
|
+
code: "VEL5070",
|
|
311
|
+
message: `'${name}' is not a native attribute name; write '${spelling.write}'${spelling.note ? ` ${spelling.note}` : ""}`,
|
|
312
|
+
span: attribute.span,
|
|
313
|
+
...(spelling.note === undefined ? rename(spelling.write) : {}),
|
|
314
|
+
});
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
if (/^aria[A-Z]/u.test(name)) {
|
|
318
|
+
// Every ARIA attribute is `aria-` followed by one lowercase word, so
|
|
319
|
+
// lowercasing the remainder recovers the spelling whenever one exists.
|
|
320
|
+
const hyphenated = `aria-${name.slice(4).toLowerCase()}`;
|
|
321
|
+
const known = WEB_ARIA_ATTRIBUTES.has(hyphenated);
|
|
322
|
+
host.diagnostics.push({
|
|
323
|
+
code: "VEL5070",
|
|
324
|
+
message: known
|
|
325
|
+
? `'${name}' is not a native attribute name; ARIA attribute names are hyphenated — write '${hyphenated}'`
|
|
326
|
+
: `'${name}' is not a native attribute name; ARIA attribute names are hyphenated and lowercase, such as 'aria-label'`,
|
|
327
|
+
span: attribute.span,
|
|
328
|
+
...(known ? rename(hyphenated) : {}),
|
|
329
|
+
});
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
if (name.startsWith("aria-") && !WEB_ARIA_ATTRIBUTES.has(name)) {
|
|
333
|
+
const nearest = nearestLookName(name, WEB_ARIA_ATTRIBUTES);
|
|
334
|
+
host.diagnostics.push({
|
|
335
|
+
code: "VEL5070",
|
|
336
|
+
message: nearest
|
|
337
|
+
? `Unknown ARIA attribute '${name}'; did you mean '${nearest}'?`
|
|
338
|
+
: `Unknown ARIA attribute '${name}'; ARIA defines a closed set of aria-* names, so a state or property it does not define belongs on a data-* attribute`,
|
|
339
|
+
span: attribute.span,
|
|
340
|
+
...(nearest ? rename(nearest) : {}),
|
|
341
|
+
});
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
// Only a literal value is read. A fix is not registered for a value: the
|
|
345
|
+
// attribute span opens on the name, so the value's own range is not known
|
|
346
|
+
// here, and the message names the token instead.
|
|
347
|
+
const literal = attribute.value;
|
|
348
|
+
// The empty string is not an out-of-vocabulary token: ARIA reads it as the
|
|
349
|
+
// attribute's own default, so `aria-hidden=""` is an uncommon but correct
|
|
350
|
+
// spelling of "not hidden" and refusing it would block a working document.
|
|
351
|
+
// `role=""` is already silent for the same reason — its loop skips empty
|
|
352
|
+
// tokens — so this keeps the two halves of the value check agreeing.
|
|
353
|
+
if (typeof literal !== "string" || literal === "")
|
|
354
|
+
return false;
|
|
355
|
+
const vocabulary = WEB_ARIA_ENUMERATED_VALUES.get(name);
|
|
356
|
+
if (vocabulary && !vocabulary.has(literal)) {
|
|
357
|
+
const nearest = nearestLookName(literal, vocabulary);
|
|
358
|
+
host.diagnostics.push(diagnostic("VEL5070", `'${name}' takes one of ${[...vocabulary].join(", ")}; '${literal}' is not one of them${nearest ? ` — did you mean '${nearest}'?` : ""}`, attribute.span));
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
if (name === "role") {
|
|
362
|
+
// `role` takes a space-separated fallback list, so each token is its own
|
|
363
|
+
// question and only the unknown ones are named.
|
|
364
|
+
let reported = false;
|
|
365
|
+
for (const token of literal.split(/\s+/u)) {
|
|
366
|
+
if (!token || WEB_ARIA_ROLES.has(token))
|
|
367
|
+
continue;
|
|
368
|
+
// A published second spelling is answered as a synonym rather than as
|
|
369
|
+
// an unknown name. `image` is ARIA's own later spelling of `img`, and
|
|
370
|
+
// "Unknown ARIA role 'image'" would assert something false about it.
|
|
371
|
+
const synonym = WEB_ARIA_ROLE_SYNONYMS.get(token);
|
|
372
|
+
const nearest = synonym ? null : nearestLookName(token, WEB_ARIA_ROLES);
|
|
373
|
+
host.diagnostics.push(diagnostic("VEL5070", synonym
|
|
374
|
+
? `ARIA publishes '${token}' and '${synonym}' as one role; VelarScript writes '${synonym}'`
|
|
375
|
+
: nearest
|
|
376
|
+
? `Unknown ARIA role '${token}'; did you mean '${nearest}'?`
|
|
377
|
+
: `Unknown ARIA role '${token}'; role takes an ARIA role name such as 'button', 'dialog', or 'status'`, attribute.span));
|
|
378
|
+
reported = true;
|
|
379
|
+
}
|
|
380
|
+
return reported;
|
|
381
|
+
}
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* GRM-A4: an event handler runs for effect and returns null. The hole this
|
|
386
|
+
* closes is `on:click={() => {}}`: after a fat arrow, braces build a record,
|
|
387
|
+
* never a block, so the empty-record factory used to be accepted as a handler
|
|
388
|
+
* and silently did nothing on every click.
|
|
389
|
+
*/
|
|
390
|
+
export function checkEventHandlerResult(host, event, value, result, attributeSpan) {
|
|
391
|
+
const resolved = host.expandAliases(result);
|
|
392
|
+
// An asynchronous handler stays legal whatever it resolves to: attaching an
|
|
393
|
+
// action directly or through a wrapper is a decided spelling, and an action
|
|
394
|
+
// already owns its pending state, its errors, and its result.
|
|
395
|
+
if (resolved.kind === "promise")
|
|
396
|
+
return;
|
|
397
|
+
if (resolved.kind === "null" || resolved.kind === "any" || isInvalidType(resolved) || resolved.kind === "unknown")
|
|
398
|
+
return;
|
|
399
|
+
const emptyRecordBody = value && typeof value !== "string" && value.kind === "ArrowFunctionExpression"
|
|
400
|
+
&& value.body.kind === "ObjectExpression" && value.body.properties.length === 0;
|
|
401
|
+
host.diagnostics.push(diagnostic("VEL5021", emptyRecordBody
|
|
402
|
+
? `Event '${event}' handlers return null, and '{}' after '=>' builds an empty record rather than an empty block; write '() => null' for a handler that does nothing, or name a 'def' that performs the work`
|
|
403
|
+
: `Event '${event}' handlers return null; this handler returns ${describeType(result)} — the result is discarded, so call it inside a 'def' that returns null`, attributeSpan));
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* D47 rule 84(A): a bind target is a writable reactive location — a state
|
|
407
|
+
* name, or a record-field / List-index / Map-key path rooted in one. A
|
|
408
|
+
* computed accessor, a const, and a function result stay rejected: nothing
|
|
409
|
+
* would receive the write.
|
|
410
|
+
*/
|
|
411
|
+
export function isWritableBindTarget(host, value) {
|
|
412
|
+
if (!value || typeof value === "string")
|
|
413
|
+
return false;
|
|
414
|
+
if (value.kind === "IdentifierExpression")
|
|
415
|
+
return host.writableStateName(value.name);
|
|
416
|
+
return bindPathRoot(host, value) !== null;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Walks a member/index path inward to its root state binding, checking every
|
|
420
|
+
* segment is a writable location: a declared record field, a List element, or
|
|
421
|
+
* a Map value. Returns the root state name, or null when the path is not a
|
|
422
|
+
* writable reactive location.
|
|
423
|
+
*/
|
|
424
|
+
export function bindPathRoot(host, value) {
|
|
425
|
+
const segments = [];
|
|
426
|
+
let node = value;
|
|
427
|
+
while (node.kind === "MemberExpression" || node.kind === "IndexExpression") {
|
|
428
|
+
if (node.kind === "MemberExpression" && node.optional)
|
|
429
|
+
return null;
|
|
430
|
+
segments.push(node);
|
|
431
|
+
node = node.object;
|
|
432
|
+
}
|
|
433
|
+
if (node.kind !== "IdentifierExpression" || !host.writableStateName(node.name))
|
|
434
|
+
return null;
|
|
435
|
+
let current = host.expandAliases(host.lookup(node.name).type);
|
|
436
|
+
for (const segment of segments.reverse()) {
|
|
437
|
+
if (segment.kind === "MemberExpression") {
|
|
438
|
+
const fields = current.kind === "object" ? current.fields
|
|
439
|
+
: current.kind === "named" ? host.fieldsOf(current.identity ?? current.name) : null;
|
|
440
|
+
const field = fields?.get(segment.property);
|
|
441
|
+
if (!field)
|
|
442
|
+
return null;
|
|
443
|
+
current = host.expandAliases(field);
|
|
444
|
+
}
|
|
445
|
+
else if (current.kind === "list")
|
|
446
|
+
current = host.expandAliases(current.element);
|
|
447
|
+
else if (current.kind === "map" || current.kind === "record")
|
|
448
|
+
current = host.expandAliases(current.value);
|
|
449
|
+
else
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
return node.name;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* D47 rule 84: `bind:group` binds a set of inputs that share one decision.
|
|
456
|
+
* A radio group holds the selected input's `value`; a checkbox group holds the
|
|
457
|
+
* checked values as a List<string>, so checking and unchecking are membership
|
|
458
|
+
* changes.
|
|
459
|
+
*/
|
|
460
|
+
export function analyzeBindGroup(host, expression, attribute, value, inferred) {
|
|
461
|
+
const inputType = expression.attributes.find((item) => item.name === "type")?.value;
|
|
462
|
+
const kind = expression.tag === "input" && typeof inputType === "string" && (inputType === "radio" || inputType === "checkbox") ? inputType : null;
|
|
463
|
+
if (!kind) {
|
|
464
|
+
host.diagnostics.push(diagnostic("VEL5019", `bind:group binds a group of choices and requires <input type="radio"> or <input type="checkbox">; use bind:value for a single field and bind:checked for a single flag`, attribute.span));
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (!expression.attributes.some((item) => item.name === "value")) {
|
|
468
|
+
host.diagnostics.push(diagnostic("VEL5019", `bind:group identifies each choice by its value attribute; add value="..." to this <input type="${kind}">`, attribute.span));
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (!isWritableBindTarget(host, value)) {
|
|
472
|
+
host.diagnostics.push(diagnostic("VEL5019", bindTargetGuidance("bind:group"), attribute.span));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const expected = kind === "radio" ? stringType : { kind: "list", element: stringType };
|
|
476
|
+
if (kind === "radio" && inferred.kind === "enum") {
|
|
477
|
+
host.enumValueBindings.set(attribute.span.start, inferred.name);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
host.requireAssignable(inferred, expected, attribute.span);
|
|
481
|
+
}
|
|
482
|
+
// Matches 'event => stateName = event.field' (any member depth) where the
|
|
483
|
+
// assignment target is a writable state binding, and returns that binding's
|
|
484
|
+
// name; anything else returns null.
|
|
485
|
+
export function eventAssignedStateBinding(host, value) {
|
|
486
|
+
if (!value || typeof value === "string")
|
|
487
|
+
return null;
|
|
488
|
+
if (value.kind !== "ArrowFunctionExpression" || value.asynchronous || value.parameters.length !== 1)
|
|
489
|
+
return null;
|
|
490
|
+
const body = value.body;
|
|
491
|
+
if (body.kind !== "AssignmentExpression" || body.target.kind !== "IdentifierExpression")
|
|
492
|
+
return null;
|
|
493
|
+
const state = body.target.name;
|
|
494
|
+
if (!host.writableStateName(state))
|
|
495
|
+
return null;
|
|
496
|
+
let source = body.value;
|
|
497
|
+
while (source.kind === "MemberExpression")
|
|
498
|
+
source = source.object;
|
|
499
|
+
return source.kind === "IdentifierExpression" && source.name === value.parameters[0].name && body.value.kind === "MemberExpression"
|
|
500
|
+
? state
|
|
501
|
+
: null;
|
|
502
|
+
}
|
|
503
|
+
/** D47 rule 84: `bind:value` holds one field, on the three elements that have one. */
|
|
504
|
+
function analyzeBindValue(host, expression, attribute, value, inferred) {
|
|
505
|
+
if (!isWritableBindTarget(host, value)) {
|
|
506
|
+
host.diagnostics.push(diagnostic("VEL5019", bindTargetGuidance("bind:value"), attribute.span));
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
if (!["input", "textarea", "select"].includes(expression.tag))
|
|
510
|
+
host.diagnostics.push(diagnostic("VEL5019", `bind:value is not valid on <${expression.tag}>`, attribute.span));
|
|
511
|
+
const numeric = expression.tag === "input" && expression.attributes.some((item) => item.name === "type" && item.value === "number");
|
|
512
|
+
host.requireAssignable(inferred, numeric ? numberType : stringType, attribute.span);
|
|
513
|
+
if (!numeric && inferred.kind === "enum")
|
|
514
|
+
host.enumValueBindings.set(attribute.span.start, inferred.name);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
/** D47 rule 84: `bind:checked` holds one flag, and only a checkbox or radio has one. */
|
|
518
|
+
function analyzeBindChecked(host, expression, attribute, value, inferred) {
|
|
519
|
+
if (!isWritableBindTarget(host, value)) {
|
|
520
|
+
host.diagnostics.push(diagnostic("VEL5019", bindTargetGuidance("bind:checked"), attribute.span));
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
if (expression.tag !== "input")
|
|
524
|
+
host.diagnostics.push(diagnostic("VEL5019", `bind:checked is not valid on <${expression.tag}>`, attribute.span));
|
|
525
|
+
host.requireAssignable(inferred, boolType, attribute.span);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/** `ref` on a native element: the binding it writes has to be optional, so cleanup can restore null, and wide enough for the element the tag names. */
|
|
529
|
+
function analyzeRefAttribute(host, expression, attribute, value) {
|
|
530
|
+
if (!value || typeof value === "string" || value.kind !== "IdentifierExpression" || !host.lookup(value.name)?.mutable) {
|
|
531
|
+
host.diagnostics.push(diagnostic("VEL5020", "ref requires a mutable let binding", attribute.span));
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
const bindingType = host.lookup(value.name).type;
|
|
535
|
+
const target = nonOptional(bindingType);
|
|
536
|
+
const expected = expression.tag === "canvas" ? "CanvasElement" : expression.tag === "dialog" ? "DialogElement" : expression.tag === "textarea" ? "TextAreaElement" : ["input", "select"].includes(expression.tag) ? "InputElement" : "Element";
|
|
537
|
+
const accepted = expected === "TextAreaElement" ? new Set(["TextAreaElement", "InputElement", "Element"]) : new Set([expected, "Element"]);
|
|
538
|
+
if (bindingType.kind !== "any" && bindingType.kind !== "optional")
|
|
539
|
+
host.diagnostics.push(diagnostic("VEL5024", `A <${expression.tag}> ref requires ${expected}? or a parent element type so cleanup can restore null`, attribute.span));
|
|
540
|
+
else if (target.kind !== "any" && (target.kind !== "named" || !accepted.has(target.name)))
|
|
541
|
+
host.diagnostics.push(diagnostic("VEL5024", `A <${expression.tag}> ref requires ${expected}? or a parent element type`, attribute.span));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/** A fourth `bind:` suffix names no binding, so it is answered with the three that exist rather than emitted as a dead attribute. */
|
|
545
|
+
function reportUnknownBinding(host, attribute) {
|
|
546
|
+
const property = attribute.name.slice("bind:".length);
|
|
547
|
+
const nearest = property ? nearestLookName(property, ["value", "checked", "group"]) : null;
|
|
548
|
+
host.diagnostics.push(diagnostic("VEL5019", property
|
|
549
|
+
? `Unknown binding 'bind:${property}'${nearest ? `; did you mean 'bind:${nearest}'?` : "; an element binds bind:value for a field, bind:checked for a flag, and bind:group for a set of choices"}`
|
|
550
|
+
: "Use 'bind:value={name}'; the bind directive names the bound property, such as bind:value or bind:checked", attribute.span));
|
|
551
|
+
}
|
|
552
|
+
/** WEB-S1: the `on` prefix a native element reserves — the message names the `on:` spelling, and says so about the browser only for a name the browser really compiles as script. */
|
|
553
|
+
function reportReservedOnAttribute(host, attribute) {
|
|
554
|
+
const camel = attribute.name.slice(2);
|
|
555
|
+
const event = camel === "DoubleClick" || camel === "DblClick" ? "dblclick" : camel.toLowerCase();
|
|
556
|
+
// Only a name the browser really compiles as script earns the executable
|
|
557
|
+
// clause; a name that is no event at all is refused by the prefix rule,
|
|
558
|
+
// not by the browser, and a message that claimed otherwise would state a
|
|
559
|
+
// rule the author did not hit. The roster answering that question is
|
|
560
|
+
// HTML's handler attributes, read case-insensitively as the browser reads
|
|
561
|
+
// them — the `on:` directive vocabulary is a different question and
|
|
562
|
+
// answered it wrongly in both directions.
|
|
563
|
+
const executable = htmlEventHandlerAttributes.has(attribute.name.toLowerCase());
|
|
564
|
+
// A concrete `on:` replacement is worth naming only when the remainder is
|
|
565
|
+
// a real event; `onward` would otherwise be answered with `on:ward`.
|
|
566
|
+
const named = executable || nativeDomEventNames.has(event);
|
|
567
|
+
const script = executable
|
|
568
|
+
? ` — an '${attribute.name}' attribute is compiled as script by the browser, so any value written there runs`
|
|
569
|
+
: "";
|
|
570
|
+
if (attribute.name === "onEnter") {
|
|
571
|
+
host.diagnostics.push(diagnostic("VEL5025", "Use 'on:keydown' with a handler that checks 'event.key == \"Enter\"'; VelarScript has no dedicated enter-key event", attribute.span));
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
host.diagnostics.push(diagnostic("VEL5025", named
|
|
575
|
+
? `Use 'on:${event}'; VelarScript event attributes use the on: directive, written 'on:${event}={handler}'${script}`
|
|
576
|
+
: "Use an 'on:event' directive with a native DOM event name, such as 'on:click={handler}' or 'on:keydown={handler}'; an element reserves every attribute name beginning with 'on' other than the 'on:' directive itself, because the handler spellings among them — 'onclick', 'onerror', 'onload' — are executable script in the browser", attribute.span));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
/** The `on:event.modifier` directive: the modifier vocabulary, and the handler shape the event provides for. */
|
|
580
|
+
function analyzeEventDirective(host, attribute, value, inferred, expectedEvent) {
|
|
581
|
+
const [event, ...modifiers] = attribute.name.slice(3).split(".");
|
|
582
|
+
const supported = new Set(["prevent", "stop", "once", "capture", "self"]);
|
|
583
|
+
if (!event)
|
|
584
|
+
host.diagnostics.push(diagnostic("VEL5025", "An event directive requires an event name", attribute.span));
|
|
585
|
+
for (const modifier of modifiers)
|
|
586
|
+
if (!supported.has(modifier))
|
|
587
|
+
host.diagnostics.push(diagnostic("VEL5025", `Unknown event modifier '${modifier}'`, attribute.span));
|
|
588
|
+
if (new Set(modifiers).size !== modifiers.length)
|
|
589
|
+
host.diagnostics.push(diagnostic("VEL5025", "Event modifiers cannot be repeated", attribute.span));
|
|
590
|
+
if (!isInvalidType(inferred) && inferred.kind !== "function" && inferred.kind !== "action" && inferred.kind !== "intrinsic" && inferred.kind !== "any") {
|
|
591
|
+
host.diagnostics.push(diagnostic("VEL5021", `Event '${event}' requires a function`, attribute.span));
|
|
592
|
+
}
|
|
593
|
+
else if (inferred.kind === "function" || inferred.kind === "action" || inferred.kind === "intrinsic") {
|
|
594
|
+
if (inferred.rest || inferred.parameters.length > 1)
|
|
595
|
+
host.diagnostics.push(diagnostic("VEL5021", `Event '${event}' handlers accept zero parameters or one ${describeType(expectedEvent ?? { kind: "named", name: "Event" })} parameter`, attribute.span));
|
|
596
|
+
else if (inferred.parameters.length === 1 && expectedEvent && !host.isAssignableHere(expectedEvent, inferred.parameters[0]))
|
|
597
|
+
host.diagnostics.push(diagnostic("VEL5021", `Event '${event}' provides ${describeType(expectedEvent)}, not ${describeType(inferred.parameters[0])}`, attribute.span));
|
|
598
|
+
checkEventHandlerResult(host, event ?? "", value, inferred.result, attribute.span);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
//# sourceMappingURL=attributes.js.map
|