@markuplint/ml-spec 5.6.0 → 5.6.1

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 CHANGED
@@ -3,6 +3,13 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [5.6.1](https://github.com/markuplint/markuplint/compare/v5.6.0...v5.6.1) (2026-11-13)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **ml-spec:** let a directive pattern declare the key of its repeat ([df95b4c](https://github.com/markuplint/markuplint/commit/df95b4cd56753fa4c665991f6bc091e4c0620225))
11
+
12
+
6
13
  # [5.6.0](https://github.com/markuplint/markuplint/compare/v5.5.0...v5.6.0) (2026-11-12)
7
14
 
8
15
  ### Bug Fixes
@@ -155,11 +155,58 @@ export type DirectivePattern = {
155
155
  */
156
156
  readonly valueType?: 'string' | 'number' | 'boolean' | 'code';
157
157
  /**
158
- * If true, the attribute can appear multiple times on the same element.
159
- * Can also be a string array of potentialName values for which
160
- * duplication is allowed (e.g., ['class', 'style']).
158
+ * If true, the attribute is not a repeat of an attribute of the same name (`:class` beside `class`,
159
+ * which Vue merges). Can also be a string array of potentialName values for which this holds
160
+ * (e.g., ['class', 'style']).
161
+ *
162
+ * It is not whether the attribute may be written twice: that is told by {@link DirectivePattern.duplicateKey}
163
+ * (without it, the same spelling of a name that is not `true` is a repeat, and `true` is never one).
161
164
  */
162
165
  readonly isDuplicatable?: true | readonly string[];
166
+ /**
167
+ * The key under which the framework counts this directive as written twice, as a template that takes the
168
+ * captures of `pattern` (`$1`, `$2`, …) as they are spelled. Two attributes of an element with the same key
169
+ * are a repeat, whatever name the directive resolves to (`potentialName`): a framework tells a repeat by
170
+ * what it keeps of the name, and a directive is not an attribute of the DOM.
171
+ *
172
+ * Without it, a pattern is keyed by the name as it is written (how a framework that rejects only the
173
+ * same spelling of a directive tells a repeat), except a pattern with `isDuplicatable: true`, which is
174
+ * never a repeat (a framework that takes any number of the same directive).
175
+ *
176
+ * @example "$1" -- the key of `:view-box.camel` is its argument, `view-box` (with `duplicateKeyModifiers`, `viewBox`)
177
+ */
178
+ readonly duplicateKey?: string;
179
+ /**
180
+ * Modifiers of the directive that change {@link DirectivePattern.duplicateKey}: the modifiers are those
181
+ * written after the name, and a framework gives two spellings of a name that differ in them the same key
182
+ * (the order of them, a modifier that it does not know) or a different one (a modifier that changes the
183
+ * name that is bound). Only the modifiers listed here count, and a modifier is matched as it is spelled.
184
+ */
185
+ readonly duplicateKeyModifiers?: {
186
+ /**
187
+ * The capture group of `pattern` that holds the modifiers, joined with `.` and starting with it (`.prop.camel`).
188
+ */
189
+ readonly group: number;
190
+ /**
191
+ * The modifiers that turn the template into camelCase before the prefixes are put in front of it.
192
+ */
193
+ readonly camelize?: readonly string[];
194
+ /**
195
+ * The prefix that a modifier puts in front of the key. They are put in the order listed, so that
196
+ * the first of the listed modifiers that is written is the outermost.
197
+ */
198
+ readonly prefixes?: readonly {
199
+ readonly modifier: string;
200
+ readonly prefix: string;
201
+ }[];
202
+ /**
203
+ * When no prefix is put in front of the key, it is the name of an attribute (`:id.foo` binds `id`), so
204
+ * the element folds its case as it does for any attribute and it is a repeat of that attribute
205
+ * written as such (`:id`). A key with a prefix is a property or a namespaced name, which
206
+ * keeps its case (`.id` and `.ID` are two).
207
+ */
208
+ readonly foldedWithoutPrefix?: true;
209
+ };
163
210
  };
164
211
  /**
165
212
  * Reference URLs
@@ -22,6 +22,17 @@ export type DirectiveResolution = {
22
22
  readonly expressionLanguage?: ExpressionLanguage;
23
23
  readonly valueType?: 'string' | 'number' | 'boolean' | 'code';
24
24
  readonly isDuplicatable?: boolean;
25
+ /**
26
+ * Present only when the matched pattern decides it ({@link DirectivePattern.duplicateKey}): the key
27
+ * under which the directive counts as written twice, or `null` when it never does (a pattern that is
28
+ * `isDuplicatable: true` and declares no key). Absent, the key is the name as it is written.
29
+ */
30
+ readonly duplicateKey?: string | null;
31
+ /**
32
+ * Present only when the key is the name of an attribute that the element folds
33
+ * ({@link DirectivePattern.duplicateKeyModifiers}'s `foldedWithoutPrefix`) and no prefix is in front of it.
34
+ */
35
+ readonly duplicateKeyFolds?: true;
25
36
  };
26
37
  /**
27
38
  * Resolves an attribute name against a list of compiled directive patterns.
@@ -1,3 +1,4 @@
1
+ import { camelize } from '@markuplint/shared';
1
2
  const cache = new WeakMap();
2
3
  /**
3
4
  * Compiles directive patterns into RegExp objects, caching them
@@ -14,6 +15,33 @@ export function compileDirectivePatterns(patterns) {
14
15
  }
15
16
  return compiled;
16
17
  }
18
+ /**
19
+ * The key of an attribute that a pattern gives, as {@link DirectivePattern.duplicateKey} says.
20
+ */
21
+ function resolveDuplicateKey(
22
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
23
+ pattern, match) {
24
+ if (pattern.duplicateKey == null) {
25
+ return { key: pattern.isDuplicatable === true ? null : undefined, folds: false };
26
+ }
27
+ const modifiers = pattern.duplicateKeyModifiers;
28
+ const written = new Set((modifiers ? (match[modifiers.group] ?? '') : '').split('.').filter(Boolean));
29
+ // Only what the directive writes is camelized (`transformBind` of Vue does so to the argument), not the
30
+ // text that the template puts around it
31
+ const camelized = modifiers?.camelize?.some(modifier => written.has(modifier)) === true;
32
+ const key = pattern.duplicateKey.replaceAll(/\$(\d+)/g, (_, idx) => {
33
+ const capture = match[Number(idx)] ?? '';
34
+ return camelized ? camelize(capture) : capture;
35
+ });
36
+ if (!modifiers) {
37
+ return { key, folds: false };
38
+ }
39
+ const prefix = (modifiers.prefixes ?? [])
40
+ .filter(({ modifier }) => written.has(modifier))
41
+ .map(({ prefix }) => prefix)
42
+ .join('');
43
+ return { key: prefix + key, folds: modifiers.foldedWithoutPrefix === true && prefix === '' };
44
+ }
17
45
  /**
18
46
  * Resolves an attribute name against a list of compiled directive patterns.
19
47
  * Returns the first match's resolution, or null if no pattern matches.
@@ -38,6 +66,7 @@ compiledPatterns) {
38
66
  else if (Array.isArray(pattern.isDuplicatable) && potentialName) {
39
67
  isDuplicatable = pattern.isDuplicatable.includes(potentialName);
40
68
  }
69
+ const duplicateKey = resolveDuplicateKey(pattern, match);
41
70
  return {
42
71
  potentialName,
43
72
  isDirective: pattern.isDirective,
@@ -45,6 +74,8 @@ compiledPatterns) {
45
74
  valueType: pattern.valueType,
46
75
  isDuplicatable,
47
76
  ...(pattern.expressionLanguage ? { expressionLanguage: pattern.expressionLanguage } : {}),
77
+ ...(duplicateKey.key === undefined ? {} : { duplicateKey: duplicateKey.key }),
78
+ ...(duplicateKey.folds ? { duplicateKeyFolds: true } : {}),
48
79
  };
49
80
  }
50
81
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/ml-spec",
3
- "version": "5.6.0",
3
+ "version": "5.6.1",
4
4
  "description": "Types and schema that specs of the Markup languages for markuplint",
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,8 +40,8 @@
40
40
  "type-fest": "^5.10.0"
41
41
  },
42
42
  "devDependencies": {
43
- "@markuplint/test-tools": "5.6.0",
43
+ "@markuplint/test-tools": "5.6.1",
44
44
  "json-schema-to-typescript": "16.0.0"
45
45
  },
46
- "gitHead": "eade3d7202682e567f3c1b6985478f1d4b5c5481"
46
+ "gitHead": "a9245162df3699c4880469f493aaaf6103343d83"
47
47
  }