@markuplint/ml-spec 5.4.0 → 5.6.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +4 -0
  3. package/lib/algorithm/aria/accname/aria-steps.d.ts +12 -7
  4. package/lib/algorithm/aria/accname/aria-steps.js +16 -7
  5. package/lib/algorithm/aria/accname/compute.d.ts +3 -3
  6. package/lib/algorithm/aria/accname/compute.js +3 -3
  7. package/lib/algorithm/aria/accname/element-names.d.ts +2 -2
  8. package/lib/algorithm/aria/accname/element-names.js +29 -29
  9. package/lib/algorithm/aria/accname/helpers.d.ts +30 -9
  10. package/lib/algorithm/aria/accname/helpers.js +34 -12
  11. package/lib/algorithm/aria/accname/label-steps.d.ts +2 -2
  12. package/lib/algorithm/aria/accname/label-steps.js +2 -2
  13. package/lib/algorithm/aria/accname/svg-helpers.d.ts +2 -2
  14. package/lib/algorithm/aria/accname/svg-helpers.js +2 -2
  15. package/lib/algorithm/aria/accname/types.d.ts +12 -0
  16. package/lib/algorithm/aria/accname-computation.d.ts +1 -1
  17. package/lib/algorithm/aria/accname-computation.js +6 -6
  18. package/lib/algorithm/aria/get-aria.js +2 -2
  19. package/lib/algorithm/aria/get-computed-role.js +14 -9
  20. package/lib/algorithm/aria/get-explicit-role.d.ts +2 -2
  21. package/lib/algorithm/aria/get-explicit-role.js +11 -4
  22. package/lib/algorithm/aria/get-non-presentational-ancestor.d.ts +2 -0
  23. package/lib/algorithm/aria/get-non-presentational-ancestor.js +5 -2
  24. package/lib/algorithm/aria/is-exposed.d.ts +3 -1
  25. package/lib/algorithm/aria/is-exposed.js +21 -8
  26. package/lib/algorithm/aria/is-presentational.d.ts +2 -3
  27. package/lib/algorithm/aria/is-presentational.js +2 -3
  28. package/lib/algorithm/aria/matches-context-role.d.ts +4 -1
  29. package/lib/algorithm/aria/matches-context-role.js +11 -7
  30. package/lib/algorithm/dom/tree-boundary.d.ts +108 -0
  31. package/lib/algorithm/dom/tree-boundary.js +172 -0
  32. package/lib/algorithm/html/content-model-category-to-tag-names.d.ts +7 -3
  33. package/lib/algorithm/html/content-model-category-to-tag-names.js +7 -3
  34. package/lib/algorithm/html/select-display-size.d.ts +29 -0
  35. package/lib/algorithm/html/select-display-size.js +38 -0
  36. package/lib/const/accname.d.ts +1 -1
  37. package/lib/const/accname.js +1 -1
  38. package/lib/index.d.ts +2 -0
  39. package/lib/index.js +3 -0
  40. package/lib/types/index.d.ts +63 -2
  41. package/lib/types/permitted-structures.d.ts +12 -1
  42. package/lib/utils/directive-resolver.d.ts +8 -0
  43. package/lib/utils/directive-resolver.js +1 -0
  44. package/lib/utils/schema-to-spec.d.ts +4 -0
  45. package/lib/utils/schema-to-spec.js +42 -0
  46. package/package.json +6 -8
@@ -0,0 +1,172 @@
1
+ import { XHTML_NAMESPACE } from '../../const/dom.js';
2
+ /**
3
+ * @module
4
+ *
5
+ * The parent of an element as the DOM sees it, for the algorithms that walk up from an element.
6
+ *
7
+ * The algorithms of this package are written against the DOM `Element`, where `parentElement`
8
+ * stops at the boundary of a tree: the parent element of a node whose parent is not an element
9
+ * (a document fragment, such as the content of a `template` element or a shadow root) is `null`.
10
+ * `@markuplint/ml-core` hands them its own elements, whose `parentElement` is the syntactical
11
+ * parent: the syntax tree has no other place for the content of a `template` element than its
12
+ * children, so the top of the content has the `template` element as its parent element. A walk
13
+ * that reads `parentElement` there leaves the content and enters the tree that holds the
14
+ * `template`. `getRootNode()` is where `@markuplint/ml-core` puts the boundary, and in a real DOM
15
+ * a parent is always in the tree of its child, so comparing the two roots gives the DOM's answer
16
+ * to both.
17
+ *
18
+ * This package cannot use `MLNode#treeParentElement` of `@markuplint/ml-core`, which depends on it,
19
+ * so the helpers derive the same answer from `getRootNode()`. The two are pinned to agree by
20
+ * `tree-scope.spec.ts` of `@markuplint/ml-core`: a change to the boundary in one belongs to both.
21
+ *
22
+ * Which of the two helpers a walk takes depends on what it follows:
23
+ *
24
+ * - {@link getTreeParentElement} follows a relation that the DOM defines in the tree, such as
25
+ * the ancestor `label` element of a labelable element (`findAncestorLabel`, the one walk of
26
+ * this package that does not follow the accessibility tree).
27
+ * - {@link getComposedParentElement} follows what the accessibility tree is built from. Its
28
+ * content is the flat tree: the content of a shadow root is rendered in its host, so the
29
+ * elements above it are the host and the ancestors of the host. The content of an ordinary
30
+ * `template` element is not rendered, and where it is inserted is not known from the markup,
31
+ * so a walk ends at its top.
32
+ *
33
+ * A `template` element that may have a declarative shadow root but whose attachment is not known
34
+ * from the markup (a host that is not in the file, a component, or a dynamic `shadowrootmode`),
35
+ * or that fails to attach one, is treated as an ordinary `template` element: a walk ends at the
36
+ * top of its content.
37
+ *
38
+ * The flat tree renders a light child of a shadow host where the `slot` element that takes it is,
39
+ * so the parent of the child is that `slot`: `getFlatTreeSlot()` of `@markuplint/ml-core` tells it
40
+ * (the named slot assignment of the shadow root). A child that no slot takes is not rendered, so a
41
+ * walk ends there as it does at the top of the content of an ordinary `template` element. Where the
42
+ * assignment is not known (`undefined`: manual assignment, a name written as code, a slot of a
43
+ * framework), the walk goes to the host as it does for any other child. The `slot` element itself
44
+ * is an element that is not mapped to the accessibility tree, which the algorithms of this package
45
+ * pass through (`isSlotElement`).
46
+ *
47
+ * Known limitation: a walk from the top of the shadow root goes to the host and does not model where
48
+ * the `slot` sits in the shadow root, so the elements of the shadow root that are above the `slot` are
49
+ * not between the host and a light child the `slot` takes. The fallback content of a slot that takes
50
+ * nodes is not rendered either, and a walk from it still goes through the `slot`.
51
+ *
52
+ * @see https://dom.spec.whatwg.org/#find-a-slot
53
+ * @see https://dom.spec.whatwg.org/#find-flattened-slotables
54
+ * @see https://dom.spec.whatwg.org/#parent-element
55
+ * @see https://dom.spec.whatwg.org/#concept-shadow-including-ancestor
56
+ * @see https://html.spec.whatwg.org/multipage/scripting.html#the-template-element
57
+ */
58
+ /** DOM `Node.DOCUMENT_FRAGMENT_NODE` */
59
+ const DOCUMENT_FRAGMENT_NODE = 11;
60
+ /**
61
+ * The parent element of an element in its own tree, which is what the DOM calls `parentElement`:
62
+ * `null` at the top of the content of a `template` element or of a shadow root, even where the
63
+ * `parentElement` of the element implementation is the `template` element.
64
+ *
65
+ * @template P - The type of the parent element the element reads
66
+ * @param el - The element to start from
67
+ * @returns The parent element, or `null` when the element is at the top of its tree
68
+ */
69
+ export function getTreeParentElement(el) {
70
+ const parent = el.parentElement;
71
+ if (!parent) {
72
+ return null;
73
+ }
74
+ // Without a tree on either side there is no boundary between them
75
+ if (!parent.getRootNode || !el.getRootNode) {
76
+ return parent;
77
+ }
78
+ return parent.getRootNode() === el.getRootNode() ? parent : null;
79
+ }
80
+ /**
81
+ * The parent element of an element as the accessibility tree is built: the parent element in its own
82
+ * tree, or, at the top of a shadow root, the host of the shadow root. `null` at the top of the content
83
+ * of an ordinary `template` element, and at the top of a document or of any other root that is not a
84
+ * shadow root. For a light child of a shadow host, the slot that takes it, or `null` when none does.
85
+ *
86
+ * @template P - The type of the parent element the element reads
87
+ * @param el - The element to start from
88
+ * @returns The parent element or the shadow host, or `null` when the walk ends
89
+ */
90
+ export function getComposedParentElement(el) {
91
+ const slot = el.getFlatTreeSlot?.();
92
+ if (slot !== undefined) {
93
+ return slot;
94
+ }
95
+ const parent = getTreeParentElement(el);
96
+ if (parent) {
97
+ return parent;
98
+ }
99
+ const root = el.getRootNode?.();
100
+ if (isShadowRoot(root)) {
101
+ return root.host;
102
+ }
103
+ return null;
104
+ }
105
+ /**
106
+ * Whether a root is a shadow root: a document fragment that has a host.
107
+ * The shadow root of `@markuplint/ml-core` is not a `ShadowRoot` of the DOM by class, and the
108
+ * content of an ordinary `template` element is a document fragment that has no host.
109
+ *
110
+ * @template P - The type of the host
111
+ * @param root - The root of a tree
112
+ * @returns `true` if the root has a host element
113
+ */
114
+ export function isShadowRoot(root) {
115
+ return (typeof root === 'object' &&
116
+ root !== null &&
117
+ 'nodeType' in root &&
118
+ root.nodeType === DOCUMENT_FRAGMENT_NODE &&
119
+ 'host' in root);
120
+ }
121
+ /**
122
+ * Whether an element is a `slot` element of the HTML namespace, which the accessibility tree does not
123
+ * map: it has no role of its own, and what it renders (the nodes assigned to it, or its children)
124
+ * belongs to the parent of the `slot`. A walk up the ancestors of the accessibility tree that reads
125
+ * roles passes through it. The walks that read an attribute of the ancestors (`hidden`, `aria-hidden`)
126
+ * do not skip it: what a `slot` that has them renders is hidden as well.
127
+ *
128
+ * It does not look at `MLParser#slotElement` on purpose: the `slot` of a framework (Vue, Svelte, Astro)
129
+ * is replaced with what it is given, so it is no element of the accessibility tree either, and the
130
+ * pass-through is right for it too. Which `slot` has an assignment is `MLElement#getAssignedNodes` of
131
+ * `@markuplint/ml-core`, which does look at it.
132
+ *
133
+ * > No corresponding role
134
+ *
135
+ * @param el - An element
136
+ * @returns `true` for the `slot` element of the HTML namespace
137
+ * @see https://w3c.github.io/html-aam/#el-slot
138
+ * @see https://dom.spec.whatwg.org/#find-flattened-slotables
139
+ */
140
+ export function isSlotElement(el) {
141
+ return el.localName === 'slot' && el.namespaceURI === XHTML_NAMESPACE;
142
+ }
143
+ /**
144
+ * Whether an element is not rendered: it is a light child of a shadow host, and no slot of the shadow
145
+ * root takes it (`getFlatTreeSlot()` is `null`). The flat tree has no place for it, so it is not in the
146
+ * accessibility tree either, whatever its host is.
147
+ *
148
+ * @param el - An element
149
+ * @returns `true` for an element that is known not to be rendered
150
+ * @see https://dom.spec.whatwg.org/#find-flattened-slotables
151
+ */
152
+ export function isNotRendered(el) {
153
+ // The DOM `Element` has no `getFlatTreeSlot`: only the element of `@markuplint/ml-core` knows it
154
+ return el.getFlatTreeSlot?.() === null;
155
+ }
156
+ /**
157
+ * The parent element of an element in the accessibility tree: the composed parent element
158
+ * ({@link getComposedParentElement}) with the `slot` elements passed through ({@link isSlotElement}). The walks
159
+ * that read the roles of the ancestors take it; the walks that read an attribute of the ancestors take
160
+ * {@link getComposedParentElement}, as what a `slot` that has `hidden` renders is hidden.
161
+ *
162
+ * @template E - The type of the element
163
+ * @param el - The element to start from
164
+ * @returns The parent element, or `null` when the walk ends
165
+ */
166
+ export function getAccessibilityParentElement(el) {
167
+ let parent = getComposedParentElement(el);
168
+ while (parent && isSlotElement(parent)) {
169
+ parent = getComposedParentElement(parent);
170
+ }
171
+ return parent;
172
+ }
@@ -6,9 +6,13 @@ import type { Category } from '../../types/permitted-structures.js';
6
6
  * Results are cached for repeated lookups.
7
7
  *
8
8
  * Note: entries are returned as stored in the spec's `#contentModels` map
9
- * without CSS selector parsing — they are expected to be bare tag names or
10
- * tag-with-attribute selectors (e.g. `a[href]`); complex selectors are not
11
- * decomposed into tag names.
9
+ * without CSS selector parsing. Besides bare tag names they include selectors
10
+ * (`a[href]`, `hgroup:has(h1,h2,h3,h4,h5,h6)`, `style:first-child`), which are
11
+ * not decomposed into tag names; `#custom` and `#text` stand for custom elements
12
+ * and text. An entry ending in `:first-child` holds as the first child as the HTML
13
+ * Standard counts it, text included, which markuplint's own paths match with
14
+ * `contentModelFirstChildBase` and `isContentModelFirstChild` of
15
+ * `@markuplint/selector`; matched as a plain selector, it counts elements only.
12
16
  *
13
17
  * @param contentModel - The content model category identifier
14
18
  * @param def - The specification definitions containing content model mappings
@@ -5,9 +5,13 @@ const cache = new Map();
5
5
  * Results are cached for repeated lookups.
6
6
  *
7
7
  * Note: entries are returned as stored in the spec's `#contentModels` map
8
- * without CSS selector parsing — they are expected to be bare tag names or
9
- * tag-with-attribute selectors (e.g. `a[href]`); complex selectors are not
10
- * decomposed into tag names.
8
+ * without CSS selector parsing. Besides bare tag names they include selectors
9
+ * (`a[href]`, `hgroup:has(h1,h2,h3,h4,h5,h6)`, `style:first-child`), which are
10
+ * not decomposed into tag names; `#custom` and `#text` stand for custom elements
11
+ * and text. An entry ending in `:first-child` holds as the first child as the HTML
12
+ * Standard counts it, text included, which markuplint's own paths match with
13
+ * `contentModelFirstChildBase` and `isContentModelFirstChild` of
14
+ * `@markuplint/selector`; matched as a plain selector, it counts elements only.
11
15
  *
12
16
  * @param contentModel - The content model category identifier
13
17
  * @param def - The specification definitions containing content model mappings
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The display size of a `<select>`.
3
+ *
4
+ * > The display size of a select element is the result of applying the rules for parsing non-negative integers to the
5
+ * > value of the element's size attribute, if it has one and parsing it is successful. If applying those rules to the
6
+ * > attribute's value is not successful, or if the size attribute is absent, then the element's display size is 4 if
7
+ * > the element's multiple content attribute is present, and 1 otherwise.
8
+ *
9
+ * The value is whatever text the `size` attribute holds. Deciding that a template expression
10
+ * (`:size="n"`) is not known yet is up to the caller, which has the attribute node; this function
11
+ * reads the text as it is.
12
+ *
13
+ * @param size - The value of the `size` attribute, or `null` when it is absent
14
+ * @param hasMultiple - Whether the `multiple` attribute is present
15
+ * @returns The display size
16
+ * @see https://html.spec.whatwg.org/multipage/form-elements.html#concept-select-size
17
+ */
18
+ export declare function getSelectDisplaySize(size: string | null, hasMultiple: boolean): number;
19
+ /**
20
+ * The rules for parsing non-negative integers: leading ASCII whitespace is
21
+ * skipped, an optional `+` or `-` is read, then the leading run of ASCII
22
+ * digits; whatever follows the digits is ignored (`"3abc"` is 3, `"01"` is 1).
23
+ * No digits, or a negative number, is an error. `-0` is not less than zero.
24
+ *
25
+ * @param text - The attribute value, or `null` when it is absent
26
+ * @returns The number, or `null` for an error
27
+ * @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-non-negative-integers
28
+ */
29
+ export declare function parseNonNegativeInteger(text: string | null): number | null;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The display size of a `<select>`.
3
+ *
4
+ * > The display size of a select element is the result of applying the rules for parsing non-negative integers to the
5
+ * > value of the element's size attribute, if it has one and parsing it is successful. If applying those rules to the
6
+ * > attribute's value is not successful, or if the size attribute is absent, then the element's display size is 4 if
7
+ * > the element's multiple content attribute is present, and 1 otherwise.
8
+ *
9
+ * The value is whatever text the `size` attribute holds. Deciding that a template expression
10
+ * (`:size="n"`) is not known yet is up to the caller, which has the attribute node; this function
11
+ * reads the text as it is.
12
+ *
13
+ * @param size - The value of the `size` attribute, or `null` when it is absent
14
+ * @param hasMultiple - Whether the `multiple` attribute is present
15
+ * @returns The display size
16
+ * @see https://html.spec.whatwg.org/multipage/form-elements.html#concept-select-size
17
+ */
18
+ export function getSelectDisplaySize(size, hasMultiple) {
19
+ return parseNonNegativeInteger(size) ?? (hasMultiple ? 4 : 1);
20
+ }
21
+ /**
22
+ * The rules for parsing non-negative integers: leading ASCII whitespace is
23
+ * skipped, an optional `+` or `-` is read, then the leading run of ASCII
24
+ * digits; whatever follows the digits is ignored (`"3abc"` is 3, `"01"` is 1).
25
+ * No digits, or a negative number, is an error. `-0` is not less than zero.
26
+ *
27
+ * @param text - The attribute value, or `null` when it is absent
28
+ * @returns The number, or `null` for an error
29
+ * @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-non-negative-integers
30
+ */
31
+ export function parseNonNegativeInteger(text) {
32
+ const match = /^[\t\n\f\r ]*([+-]?)(\d+)/.exec(text ?? '');
33
+ if (!match) {
34
+ return null;
35
+ }
36
+ const value = Number(match[2]);
37
+ return match[1] === '-' && value !== 0 ? null : value;
38
+ }
@@ -1,7 +1,7 @@
1
1
  import type { AccnameElement } from '../algorithm/aria/accname/types.js';
2
2
  /**
3
3
  * ARIA roles that are embedded controls per AccName Step 2C.
4
- * @see https://www.w3.org/TR/accname-1.2/#comp_embedded_control
4
+ * @see https://w3c.github.io/accname/#comp_embedded_control
5
5
  */
6
6
  export declare const EMBEDDED_CONTROL_ROLES: ReadonlySet<string>;
7
7
  /**
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * ARIA roles that are embedded controls per AccName Step 2C.
3
- * @see https://www.w3.org/TR/accname-1.2/#comp_embedded_control
3
+ * @see https://w3c.github.io/accname/#comp_embedded_control
4
4
  */
5
5
  export const EMBEDDED_CONTROL_ROLES = new Set([
6
6
  'textbox',
package/lib/index.d.ts CHANGED
@@ -41,6 +41,7 @@ export * from './algorithm/aria/aria-specs.js';
41
41
  export * from './algorithm/aria/get-aria.js';
42
42
  export * from './algorithm/aria/get-role-spec.js';
43
43
  export * from './algorithm/aria/is-presentational.js';
44
+ export * from './algorithm/dom/tree-boundary.js';
44
45
  export * from './algorithm/html/get-content-model.js';
45
46
  export * from './algorithm/html/may-be-focusable.js';
46
47
  export * from './algorithm/html/content-model-category-to-tag-names.js';
@@ -48,6 +49,7 @@ export * from './algorithm/html/get-selectors-by-content-model-category.js';
48
49
  export * from './algorithm/html/is-nothing-content-model.js';
49
50
  export * from './algorithm/html/is-palpable-elements.js';
50
51
  export * from './algorithm/html/is-void-element.js';
52
+ export { getSelectDisplaySize } from './algorithm/html/select-display-size.js';
51
53
  export * from './utils/get-attr-specs.js';
52
54
  export * from './utils/get-spec.js';
53
55
  export * from './utils/get-spec-by-tag-name.js';
package/lib/index.js CHANGED
@@ -43,6 +43,8 @@ export * from './algorithm/aria/aria-specs.js';
43
43
  export * from './algorithm/aria/get-aria.js';
44
44
  export * from './algorithm/aria/get-role-spec.js';
45
45
  export * from './algorithm/aria/is-presentational.js';
46
+ // DOM algorithms
47
+ export * from './algorithm/dom/tree-boundary.js';
46
48
  // HTML algorithms
47
49
  export * from './algorithm/html/get-content-model.js';
48
50
  export * from './algorithm/html/may-be-focusable.js';
@@ -51,6 +53,7 @@ export * from './algorithm/html/get-selectors-by-content-model-category.js';
51
53
  export * from './algorithm/html/is-nothing-content-model.js';
52
54
  export * from './algorithm/html/is-palpable-elements.js';
53
55
  export * from './algorithm/html/is-void-element.js';
56
+ export { getSelectDisplaySize } from './algorithm/html/select-display-size.js';
54
57
  // Shared utilities
55
58
  export * from './utils/get-attr-specs.js';
56
59
  export * from './utils/get-spec.js';
@@ -2,7 +2,7 @@ import type { ARIA } from './aria.js';
2
2
  import type { AttributeJSON, AttributeType, ConditionalAttributeType, GlobalAttributes } from './attributes.js';
3
3
  import type { ContentModel, Category } from './permitted-structures.js';
4
4
  import type { ariaVersions } from '../utils/aria-version.js';
5
- import type { NamespaceURI } from '@markuplint/ml-ast';
5
+ import type { ExpressionLanguage, LiteralValueDeclaration, NamespaceURI } from '@markuplint/ml-ast';
6
6
  import type { ReadonlyDeep } from 'type-fest';
7
7
  /**
8
8
  * markuplint Markup-language spec
@@ -12,6 +12,20 @@ export interface MLMLSpec {
12
12
  readonly def: SpecDefs;
13
13
  readonly specs: readonly ElementSpec[];
14
14
  readonly directivePatterns?: readonly DirectivePattern[];
15
+ /**
16
+ * Selectors of the elements that the framework moves to the `<head>` wherever they are written, so
17
+ * the content model of the element that contains them does not apply to them. An element in the
18
+ * `<head>` itself is not moved, and is judged as usual.
19
+ *
20
+ * React does so for `<title>`, `<meta>`, `<link>` and `<style>` under conditions the selectors
21
+ * express (`@markuplint/react-spec`):
22
+ *
23
+ * > React will always place the DOM element corresponding to the `<title>` component within the
24
+ * > document's `<head>`, regardless of where in the React tree it is rendered.
25
+ *
26
+ * @see https://react.dev/reference/react-dom/components/title#special-rendering-behavior
27
+ */
28
+ readonly movedToHead?: readonly string[];
15
29
  /**
16
30
  * Controls how `@markuplint/ml-core`'s `MLAttr` resolves attribute names.
17
31
  *
@@ -23,6 +37,30 @@ export interface MLMLSpec {
23
37
  * names and IDL property names without suggesting either. Used by Svelte.
24
38
  */
25
39
  readonly acceptedAttrNames?: 'idl' | 'both';
40
+ /**
41
+ * Where the browser holds the children of a `template` element that the author wrote without
42
+ * a directive of the framework.
43
+ *
44
+ * - `'separate-tree'` (the default, as the HTML parser does it): in the `content` of the element, a
45
+ * `DocumentFragment` that is a tree of its own. An id in it is unique in that tree only, and an
46
+ * id reference in it is resolved in that tree only. A declarative shadow root (`shadowrootmode`)
47
+ * may be attached to the parent element.
48
+ * - `'in-place'`: in the element itself, in the tree of the document. The framework creates the
49
+ * element and appends the children to it through the DOM API, which never fills `content` and
50
+ * never attaches a declarative shadow root: the `template` element is an ordinary element.
51
+ *
52
+ * Known limitations of `'in-place'`: React does not render the same tree when it hydrates server
53
+ * output (the server output has the children in `content`), and Vue puts them in `content` when
54
+ * it renders static markup as a string and replaces the element with its content when it hydrates.
55
+ */
56
+ readonly templateContent?: 'separate-tree' | 'in-place';
57
+ /**
58
+ * How the framework renders a literal in an attribute value that is written as code, where it
59
+ * differs from the default of `@markuplint/ml-core`: by kind of attribute, and by attribute name
60
+ * in `attrs`, in the form that a parser declares as well (see {@link LiteralValueDeclaration}).
61
+ * What differs on a custom element is in `customElements`.
62
+ */
63
+ readonly literalValues?: LiteralValueDeclaration;
26
64
  }
27
65
  /**
28
66
  * An element specification for extending or overriding parts of the base spec.
@@ -42,11 +80,25 @@ export type ExtendedSpec = {
42
80
  readonly def?: Partial<SpecDefs>;
43
81
  readonly specs?: readonly ExtendedElementSpec[];
44
82
  readonly directivePatterns?: readonly DirectivePattern[];
83
+ /**
84
+ * See {@link MLMLSpec.movedToHead}. Concatenated across merged specs, in the order of the specs.
85
+ */
86
+ readonly movedToHead?: readonly string[];
45
87
  /**
46
88
  * See {@link MLMLSpec.acceptedAttrNames}. Last-write-wins across merged
47
89
  * specs; omitting the property does not reset a previously set value.
48
90
  */
49
91
  readonly acceptedAttrNames?: 'idl' | 'both';
92
+ /**
93
+ * See {@link MLMLSpec.templateContent}. Last-write-wins across merged specs; omitting the
94
+ * property does not reset a previously set value.
95
+ */
96
+ readonly templateContent?: 'separate-tree' | 'in-place';
97
+ /**
98
+ * See {@link MLMLSpec.literalValues}. Merged by kind of attribute, by attribute name and by key,
99
+ * and the later spec wins on a key that two specs have.
100
+ */
101
+ readonly literalValues?: LiteralValueDeclaration;
50
102
  };
51
103
  /**
52
104
  * A declarative pattern for resolving framework-specific directive
@@ -86,9 +138,18 @@ export type DirectivePattern = {
86
138
  */
87
139
  readonly isDirective?: true;
88
140
  /**
89
- * Whether this attribute has a dynamic value.
141
+ * Whether the value of this attribute is written as code (a binding). It is the source of
142
+ * `MLAttr#isExpressionValue`; whether the value is known is up to `MLAttr#isDynamicValue`, which
143
+ * reads the code.
90
144
  */
91
145
  readonly isDynamicValue?: true;
146
+ /**
147
+ * The language of the code that the value is written in. A value that is nothing but a literal is
148
+ * rendered as the framework renders it (see {@link MLMLSpec.literalValues}), which needs the
149
+ * attribute to be the one the pattern names (`potentialName`) and the value to be what the attribute
150
+ * becomes, not a condition or a binding target. A pattern without it is not read as a literal.
151
+ */
152
+ readonly expressionLanguage?: ExpressionLanguage;
92
153
  /**
93
154
  * The semantic value type for matched attributes.
94
155
  */
@@ -12,11 +12,22 @@ export interface ContentModelsSchema {
12
12
  }
13
13
  export interface ContentModel {
14
14
  contents: PermittedContentPattern[] | boolean;
15
+ /**
16
+ * A CSS selector of the ancestor that this element must have. The ancestor is looked for in the tree of the element: the walk up the tree stops at the content of a `template` element.
17
+ */
15
18
  descendantOf?: string;
16
19
  /**
17
- * Elements that this element must NOT appear as a descendant of. Each entry is a CSS selector matching forbidden ancestor elements.
20
+ * Whether `descendantOf` is required only of an element that has a parent node, as the HTML Standard says of the `area` element ("An area element with a parent node must have a map element ancestor"). The top of a partial or of a component has none in the file, so the element there is not reported.
21
+ */
22
+ descendantOfWithParentNode?: boolean;
23
+ /**
24
+ * Elements that this element must NOT appear as a descendant of. Each entry is a CSS selector matching forbidden ancestor elements. The walk up the tree stops at the content of a `template` element, which has no ancestors.
18
25
  */
19
26
  forbiddenAncestors?: string[];
27
+ /**
28
+ * Elements that this element's ancestors are limited to: every ancestor must match at least one entry. Each entry is a CSS selector, or `#custom` for a custom element (a component or an autonomous custom element). A selector can use `:aria(has no name)`, which is evaluated with the recommended ARIA version. The walk up the tree stops at the content of a `template` element, which has no ancestors.
29
+ */
30
+ permittedAncestors?: string[];
20
31
  /**
21
32
  * Attributes that must be unique among sibling elements of the same type. If this element has one of these attributes, no other sibling element of the same type may also have it. Example: `["default"]` for `<track>` — only one `<track>` per parent may have `default`.
22
33
  */
@@ -1,4 +1,5 @@
1
1
  import type { DirectivePattern } from '../types/index.js';
2
+ import type { ExpressionLanguage } from '@markuplint/ml-ast';
2
3
  type CompiledDirectivePattern = {
3
4
  readonly regex: RegExp;
4
5
  readonly pattern: DirectivePattern;
@@ -11,7 +12,14 @@ export declare function compileDirectivePatterns(patterns: readonly DirectivePat
11
12
  export type DirectiveResolution = {
12
13
  readonly potentialName?: string;
13
14
  readonly isDirective?: true;
15
+ /**
16
+ * Whether the value is written as code (a binding). Whether its value is known is up to `MLAttr`.
17
+ */
14
18
  readonly isDynamicValue?: true;
19
+ /**
20
+ * Present only when the matched pattern declares it.
21
+ */
22
+ readonly expressionLanguage?: ExpressionLanguage;
15
23
  readonly valueType?: 'string' | 'number' | 'boolean' | 'code';
16
24
  readonly isDuplicatable?: boolean;
17
25
  };
@@ -44,6 +44,7 @@ compiledPatterns) {
44
44
  isDynamicValue: pattern.isDynamicValue,
45
45
  valueType: pattern.valueType,
46
46
  isDuplicatable,
47
+ ...(pattern.expressionLanguage ? { expressionLanguage: pattern.expressionLanguage } : {}),
47
48
  };
48
49
  }
49
50
  return null;
@@ -1,4 +1,5 @@
1
1
  import type { ElementSpec, ExtendedSpec, MLMLSpec } from '../types/index.js';
2
+ import type { LiteralValueDeclaration } from '@markuplint/ml-ast';
2
3
  /**
3
4
  * Merges an HTML-spec schema with zero or more extended spec schemas into a single
4
5
  * unified specification. Extended specs can add or override global attributes,
@@ -24,5 +25,8 @@ export declare function schemaToSpec(schemas: readonly [MLMLSpec, ...ExtendedSpe
24
25
  def: import("../types/index.js").SpecDefs;
25
26
  specs: readonly ElementSpec[];
26
27
  directivePatterns?: readonly import("../types/index.js").DirectivePattern[];
28
+ movedToHead?: readonly string[];
27
29
  acceptedAttrNames?: "idl" | "both";
30
+ templateContent?: "separate-tree" | "in-place";
31
+ literalValues?: LiteralValueDeclaration;
28
32
  };
@@ -78,9 +78,18 @@ export function schemaToSpec(schemas) {
78
78
  if (extendedSpec.directivePatterns) {
79
79
  result.directivePatterns = [...(result.directivePatterns ?? []), ...extendedSpec.directivePatterns];
80
80
  }
81
+ if (extendedSpec.movedToHead) {
82
+ result.movedToHead = [...(result.movedToHead ?? []), ...extendedSpec.movedToHead];
83
+ }
81
84
  if (extendedSpec.acceptedAttrNames != null) {
82
85
  result.acceptedAttrNames = extendedSpec.acceptedAttrNames;
83
86
  }
87
+ if (extendedSpec.templateContent != null) {
88
+ result.templateContent = extendedSpec.templateContent;
89
+ }
90
+ if (extendedSpec.literalValues) {
91
+ result.literalValues = mergeLiteralValues(result.literalValues, extendedSpec.literalValues);
92
+ }
84
93
  if (extendedSpec.specs) {
85
94
  const exSpecs = [...extendedSpec.specs];
86
95
  const specs = [];
@@ -121,3 +130,36 @@ function mergeAttrSpec(std, ex = {}) {
121
130
  }
122
131
  return result;
123
132
  }
133
+ /**
134
+ * Merges the `literalValues` of two specs by kind of attribute, by attribute name and by key; the
135
+ * later spec wins on a key that both have. `customElements` is merged the same way.
136
+ */
137
+ function mergeLiteralValues(base, ex) {
138
+ const { customElements: baseCustomElements, ...baseTable } = base ?? {};
139
+ const { customElements: exCustomElements, ...exTable } = ex;
140
+ return {
141
+ ...mergeLiteralValueTable(baseTable, exTable),
142
+ ...(baseCustomElements || exCustomElements
143
+ ? { customElements: mergeLiteralValueTable(baseCustomElements, exCustomElements) }
144
+ : {}),
145
+ };
146
+ }
147
+ function mergeLiteralValueTable(base = {}, ex = {}) {
148
+ const { attrs: baseAttrs, ...baseKinds } = base;
149
+ const { attrs: exAttrs, ...exKinds } = ex;
150
+ const attrs = { ...baseAttrs };
151
+ for (const [name, values] of Object.entries(exAttrs ?? {})) {
152
+ attrs[name] = { ...baseAttrs?.[name], ...values };
153
+ }
154
+ return {
155
+ ...mergeLiteralValueKinds(baseKinds, exKinds),
156
+ ...(baseAttrs || exAttrs ? { attrs } : {}),
157
+ };
158
+ }
159
+ function mergeLiteralValueKinds(base, ex) {
160
+ const result = { ...base };
161
+ for (const [kind, values] of Object.entries(ex)) {
162
+ result[kind] = { ...result[kind], ...values };
163
+ }
164
+ return result;
165
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/ml-spec",
3
- "version": "5.4.0",
3
+ "version": "5.6.0",
4
4
  "description": "Types and schema that specs of the Markup languages for markuplint",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,9 +22,6 @@
22
22
  "publishConfig": {
23
23
  "access": "public"
24
24
  },
25
- "typedoc": {
26
- "entryPoint": "./src/index.ts"
27
- },
28
25
  "scripts": {
29
26
  "build": "tsc --project tsconfig.build.json",
30
27
  "dev": "tsc --watch --project tsconfig.build.json",
@@ -36,14 +33,15 @@
36
33
  "schema:aria": "npx json2ts ./schemas/aria.schema.json --cwd ./schemas > ./src/types/aria.ts"
37
34
  },
38
35
  "dependencies": {
39
- "@markuplint/ml-ast": "5.3.0",
40
- "@markuplint/types": "5.4.0",
36
+ "@markuplint/ml-ast": "5.6.0",
37
+ "@markuplint/shared": "5.6.0",
38
+ "@markuplint/types": "5.6.0",
41
39
  "is-plain-object": "^5.1.0",
42
40
  "type-fest": "^5.10.0"
43
41
  },
44
42
  "devDependencies": {
45
- "@markuplint/test-tools": "5.4.0",
43
+ "@markuplint/test-tools": "5.6.0",
46
44
  "json-schema-to-typescript": "16.0.0"
47
45
  },
48
- "gitHead": "ab56aa8e12b67fbf793ddf1267aaba9fda90269d"
46
+ "gitHead": "eade3d7202682e567f3c1b6985478f1d4b5c5481"
49
47
  }