@markuplint/ml-spec 5.4.0 → 5.5.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/CHANGELOG.md +17 -0
- package/README.md +4 -0
- package/lib/algorithm/aria/accname/aria-steps.d.ts +12 -7
- package/lib/algorithm/aria/accname/aria-steps.js +16 -7
- package/lib/algorithm/aria/accname/compute.d.ts +3 -3
- package/lib/algorithm/aria/accname/compute.js +3 -3
- package/lib/algorithm/aria/accname/element-names.d.ts +2 -2
- package/lib/algorithm/aria/accname/element-names.js +29 -29
- package/lib/algorithm/aria/accname/helpers.d.ts +30 -9
- package/lib/algorithm/aria/accname/helpers.js +34 -12
- package/lib/algorithm/aria/accname/label-steps.d.ts +2 -2
- package/lib/algorithm/aria/accname/label-steps.js +2 -2
- package/lib/algorithm/aria/accname/svg-helpers.d.ts +2 -2
- package/lib/algorithm/aria/accname/svg-helpers.js +2 -2
- package/lib/algorithm/aria/accname/types.d.ts +12 -0
- package/lib/algorithm/aria/accname-computation.d.ts +1 -1
- package/lib/algorithm/aria/accname-computation.js +6 -6
- package/lib/algorithm/aria/get-aria.js +2 -2
- package/lib/algorithm/aria/get-computed-role.js +14 -9
- package/lib/algorithm/aria/get-explicit-role.d.ts +2 -2
- package/lib/algorithm/aria/get-explicit-role.js +11 -4
- package/lib/algorithm/aria/get-non-presentational-ancestor.d.ts +2 -0
- package/lib/algorithm/aria/get-non-presentational-ancestor.js +5 -2
- package/lib/algorithm/aria/is-exposed.d.ts +3 -1
- package/lib/algorithm/aria/is-exposed.js +15 -7
- package/lib/algorithm/aria/is-presentational.d.ts +2 -3
- package/lib/algorithm/aria/is-presentational.js +2 -3
- package/lib/algorithm/aria/matches-context-role.d.ts +3 -1
- package/lib/algorithm/aria/matches-context-role.js +10 -7
- package/lib/algorithm/dom/tree-boundary.d.ts +93 -0
- package/lib/algorithm/dom/tree-boundary.js +103 -0
- package/lib/algorithm/html/content-model-category-to-tag-names.d.ts +7 -3
- package/lib/algorithm/html/content-model-category-to-tag-names.js +7 -3
- package/lib/algorithm/html/select-display-size.d.ts +29 -0
- package/lib/algorithm/html/select-display-size.js +38 -0
- package/lib/const/accname.d.ts +1 -1
- package/lib/const/accname.js +1 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.js +3 -0
- package/lib/types/index.d.ts +63 -2
- package/lib/types/permitted-structures.d.ts +5 -1
- package/lib/utils/directive-resolver.d.ts +8 -0
- package/lib/utils/directive-resolver.js +1 -0
- package/lib/utils/schema-to-spec.d.ts +4 -0
- package/lib/utils/schema-to-spec.js +42 -0
- package/package.json +6 -5
|
@@ -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
|
|
9
|
-
*
|
|
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
|
+
}
|
package/lib/const/accname.d.ts
CHANGED
|
@@ -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://
|
|
4
|
+
* @see https://w3c.github.io/accname/#comp_embedded_control
|
|
5
5
|
*/
|
|
6
6
|
export declare const EMBEDDED_CONTROL_ROLES: ReadonlySet<string>;
|
|
7
7
|
/**
|
package/lib/const/accname.js
CHANGED
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';
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
*/
|
|
@@ -14,9 +14,13 @@ export interface ContentModel {
|
|
|
14
14
|
contents: PermittedContentPattern[] | boolean;
|
|
15
15
|
descendantOf?: string;
|
|
16
16
|
/**
|
|
17
|
-
* Elements that this element must NOT appear as a descendant of. Each entry is a CSS selector matching forbidden ancestor elements.
|
|
17
|
+
* 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
18
|
*/
|
|
19
19
|
forbiddenAncestors?: string[];
|
|
20
|
+
/**
|
|
21
|
+
* 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.
|
|
22
|
+
*/
|
|
23
|
+
permittedAncestors?: string[];
|
|
20
24
|
/**
|
|
21
25
|
* 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
26
|
*/
|
|
@@ -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
|
};
|
|
@@ -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.
|
|
3
|
+
"version": "5.5.0",
|
|
4
4
|
"description": "Types and schema that specs of the Markup languages for markuplint",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,14 +36,15 @@
|
|
|
36
36
|
"schema:aria": "npx json2ts ./schemas/aria.schema.json --cwd ./schemas > ./src/types/aria.ts"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@markuplint/ml-ast": "5.
|
|
40
|
-
"@markuplint/
|
|
39
|
+
"@markuplint/ml-ast": "5.5.0",
|
|
40
|
+
"@markuplint/shared": "5.5.0",
|
|
41
|
+
"@markuplint/types": "5.5.0",
|
|
41
42
|
"is-plain-object": "^5.1.0",
|
|
42
43
|
"type-fest": "^5.10.0"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
|
-
"@markuplint/test-tools": "5.
|
|
46
|
+
"@markuplint/test-tools": "5.5.0",
|
|
46
47
|
"json-schema-to-typescript": "16.0.0"
|
|
47
48
|
},
|
|
48
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "fedebcfd56eaec4642e68643b2f9e10750d810e1"
|
|
49
50
|
}
|