design-constraint-validator 2.1.0 → 2.2.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/README.md +17 -6
- package/cli/commands/build.d.ts.map +1 -1
- package/cli/commands/build.js +32 -24
- package/cli/commands/build.ts +26 -17
- package/cli/commands/graph.d.ts.map +1 -1
- package/cli/commands/graph.js +33 -16
- package/cli/commands/graph.ts +28 -15
- package/cli/commands/patch-apply.d.ts.map +1 -1
- package/cli/commands/patch-apply.js +4 -1
- package/cli/commands/patch-apply.ts +4 -1
- package/cli/commands/set.d.ts.map +1 -1
- package/cli/commands/set.js +18 -19
- package/cli/commands/set.ts +19 -19
- package/cli/commands/utils.d.ts +1 -0
- package/cli/commands/utils.d.ts.map +1 -1
- package/cli/commands/utils.js +20 -1
- package/cli/commands/utils.ts +23 -1
- package/cli/commands/validate.d.ts.map +1 -1
- package/cli/commands/validate.js +5 -17
- package/cli/commands/validate.ts +10 -18
- package/cli/commands/why.d.ts.map +1 -1
- package/cli/commands/why.js +22 -10
- package/cli/commands/why.ts +20 -9
- package/cli/config-schema.d.ts +144 -178
- package/cli/config-schema.d.ts.map +1 -1
- package/cli/config-schema.js +25 -5
- package/cli/config-schema.ts +27 -5
- package/cli/constraint-registry.d.ts.map +1 -1
- package/cli/constraint-registry.js +53 -15
- package/cli/constraint-registry.ts +53 -18
- package/cli/cross-axis-loader.d.ts +62 -0
- package/cli/cross-axis-loader.d.ts.map +1 -1
- package/cli/cross-axis-loader.js +186 -31
- package/cli/cross-axis-loader.ts +199 -24
- package/cli/dcv.js +23 -1
- package/cli/dcv.ts +23 -1
- package/cli/types.d.ts +19 -9
- package/cli/types.d.ts.map +1 -1
- package/cli/types.ts +23 -10
- package/cli/validate-api.d.ts.map +1 -1
- package/cli/validate-api.js +6 -1
- package/cli/validate-api.ts +6 -1
- package/core/constraints/cross-axis.d.ts.map +1 -1
- package/core/constraints/cross-axis.js +37 -9
- package/core/constraints/cross-axis.ts +37 -9
- package/core/constraints/monotonic.d.ts.map +1 -1
- package/core/constraints/monotonic.js +32 -8
- package/core/constraints/monotonic.ts +29 -8
- package/core/constraints/threshold.d.ts.map +1 -1
- package/core/constraints/threshold.js +24 -4
- package/core/constraints/threshold.ts +23 -4
- package/core/constraints/wcag.js +1 -1
- package/core/constraints/wcag.ts +1 -1
- package/core/flatten.d.ts.map +1 -1
- package/core/flatten.js +8 -0
- package/core/flatten.ts +9 -0
- package/core/poset.d.ts +6 -1
- package/core/poset.d.ts.map +1 -1
- package/core/poset.js +7 -2
- package/core/poset.ts +7 -2
- package/mcp/contracts.d.ts +1456 -13
- package/mcp/contracts.d.ts.map +1 -1
- package/mcp/contracts.js +45 -1
- package/mcp/contracts.ts +55 -1
- package/mcp/index.d.ts +6 -4
- package/mcp/index.d.ts.map +1 -1
- package/mcp/index.js +6 -3
- package/mcp/index.ts +28 -1
- package/mcp/insights.d.ts +94 -0
- package/mcp/insights.d.ts.map +1 -0
- package/mcp/insights.js +445 -0
- package/mcp/insights.ts +541 -0
- package/mcp/tools.d.ts +14 -3
- package/mcp/tools.d.ts.map +1 -1
- package/mcp/tools.js +133 -6
- package/mcp/tools.ts +188 -11
- package/package.json +2 -7
- package/server.json +3 -3
package/cli/validate-api.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { flattenTokens, type TokenNode, type FlatToken } from '../core/flatten.j
|
|
|
14
14
|
import { Engine, type ConstraintIssue } from '../core/engine.js';
|
|
15
15
|
import type { Breakpoint } from '../core/breakpoints.js';
|
|
16
16
|
import { loadConfig } from './config.js';
|
|
17
|
+
import { validateConfig } from './config-schema.js';
|
|
17
18
|
import { setupConstraints, collectReferencedIds } from './constraint-registry.js';
|
|
18
19
|
import { formatViolation, type ConstraintViolation } from './json-output.js';
|
|
19
20
|
import type { DcvConfig } from './types.js';
|
|
@@ -57,7 +58,11 @@ function readTokensFile(p: string): TokenNode {
|
|
|
57
58
|
|
|
58
59
|
function resolveConfig(input: ValidateInput): DcvConfig {
|
|
59
60
|
if (input.constraints !== undefined) {
|
|
60
|
-
|
|
61
|
+
const { value, errors } = validateConfig({ constraints: input.constraints });
|
|
62
|
+
if (errors) {
|
|
63
|
+
throw new Error(`Inline constraints validation failed:\n - ${errors.join('\n - ')}`);
|
|
64
|
+
}
|
|
65
|
+
return value!;
|
|
61
66
|
}
|
|
62
67
|
const res = loadConfig(input.configPath);
|
|
63
68
|
if (!res.ok) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cross-axis.d.ts","sourceRoot":"","sources":["cross-axis.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,MAAM,MAAM,aAAa,GACrB;IACE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,GAAC,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACnD,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAA;KAAE,CAAC;IACnD,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC;QAAC,GAAG,EAAE,CAAC,CAAC,EAAC,MAAM,EAAE,GAAG,EAAE,GAAG,KAAI,MAAM,CAAA;KAAE,CAAC;CACrG,GACD;IACE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,GAAC,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACnD,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,KAAI,MAAM,CAAC;QAAC,KAAK,EAAE,CAAC,IAAI,EAAC,MAAM,EAAC,EAAE,EAAC,MAAM,EAAC,GAAG,EAAE,GAAG,KAAI,MAAM,CAAA;KAAE,CAAC;CACvH,CAAC;AAEN,MAAM,MAAM,GAAG,GAAG;IAChB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACjC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IACzB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;
|
|
1
|
+
{"version":3,"file":"cross-axis.d.ts","sourceRoot":"","sources":["cross-axis.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,MAAM,MAAM,aAAa,GACrB;IACE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,GAAC,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACnD,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAA;KAAE,CAAC;IACnD,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC;QAAC,GAAG,EAAE,CAAC,CAAC,EAAC,MAAM,EAAE,GAAG,EAAE,GAAG,KAAI,MAAM,CAAA;KAAE,CAAC;CACrG,GACD;IACE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,GAAC,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACnD,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,KAAI,MAAM,CAAC;QAAC,KAAK,EAAE,CAAC,IAAI,EAAC,MAAM,EAAC,EAAE,EAAC,MAAM,EAAC,GAAG,EAAE,GAAG,KAAI,MAAM,CAAA;KAAE,CAAC;CACvH,CAAC;AAEN,MAAM,MAAM,GAAG,GAAG;IAChB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACjC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IACzB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAsCF,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAgErF;AAGD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,SAAI,GAAG,aAAa,EAAE,CAkBlG"}
|
|
@@ -1,23 +1,40 @@
|
|
|
1
|
+
// Shares the hardened finite-size policy used by monotonic/threshold (TASK-037):
|
|
2
|
+
// bare numbers / unitless as px, `rem`/`em` as 16px-relative, real numbers only
|
|
3
|
+
// (rejects ".", "5.", "1.2.3px"), and non-finite guarded. The one addition over
|
|
4
|
+
// the canonical parser is a clamp()/min()/max()/calc() heuristic — but it is
|
|
5
|
+
// gated on a `(` so a malformed bare value like "1.2.3px" is rejected rather
|
|
6
|
+
// than silently yielding a partial parse.
|
|
1
7
|
const px = (v) => {
|
|
2
8
|
if (typeof v === 'number')
|
|
3
|
-
return v;
|
|
9
|
+
return Number.isFinite(v) ? v : null;
|
|
4
10
|
if (typeof v !== 'string')
|
|
5
11
|
return null;
|
|
6
12
|
const trimmed = v.trim();
|
|
7
|
-
// Direct simple form
|
|
8
|
-
|
|
13
|
+
// Direct simple form: bare number / px / rem / em.
|
|
14
|
+
const m = trimmed.match(/^(\d*\.?\d+)\s*(px|rem|em)?$/i);
|
|
9
15
|
if (m) {
|
|
10
16
|
const n = parseFloat(m[1]);
|
|
11
|
-
|
|
17
|
+
if (!Number.isFinite(n))
|
|
18
|
+
return null;
|
|
19
|
+
const unit = (m[2] || 'px').toLowerCase();
|
|
20
|
+
return unit === 'rem' || unit === 'em' ? n * 16 : n;
|
|
12
21
|
}
|
|
13
|
-
// Heuristic:
|
|
14
|
-
|
|
15
|
-
if (
|
|
16
|
-
const
|
|
17
|
-
|
|
22
|
+
// Heuristic: first px/rem/em token inside a CSS function expression (clamp(),
|
|
23
|
+
// min(), max(), calc(), …). Gated on `(` so garbage simple values don't leak.
|
|
24
|
+
if (trimmed.includes('(')) {
|
|
25
|
+
const inner = trimmed.match(/(\d*\.?\d+)\s*(px|rem|em)/i);
|
|
26
|
+
if (inner) {
|
|
27
|
+
const n = parseFloat(inner[1]);
|
|
28
|
+
if (!Number.isFinite(n))
|
|
29
|
+
return null;
|
|
30
|
+
return inner[2].toLowerCase() === 'px' ? n : n * 16;
|
|
31
|
+
}
|
|
18
32
|
}
|
|
19
33
|
return null;
|
|
20
34
|
};
|
|
35
|
+
// Distinguish "operand absent" (skip silently) from "operand present but
|
|
36
|
+
// unparseable" (warn loudly) — a silent skip of a present value is a false green.
|
|
37
|
+
const presentButUnparseable = (raw) => raw != null && raw !== '' && px(raw) === null;
|
|
21
38
|
export function CrossAxisPlugin(rules, bp) {
|
|
22
39
|
return {
|
|
23
40
|
id: "cross-axis",
|
|
@@ -33,6 +50,17 @@ export function CrossAxisPlugin(rules, bp) {
|
|
|
33
50
|
// Evaluate if either referenced id is among candidates (looser gating so global validate works)
|
|
34
51
|
if (!candidates.has(r.when.id) && !candidates.has(r.require.id))
|
|
35
52
|
continue;
|
|
53
|
+
// Present-but-unparseable operand → warn, don't silently skip (TASK-037).
|
|
54
|
+
if (presentButUnparseable(ctx.get(r.when.id)) || presentButUnparseable(ctx.get(r.require.id))) {
|
|
55
|
+
issues.push({
|
|
56
|
+
id: r.require.id,
|
|
57
|
+
rule: "cross-axis",
|
|
58
|
+
level: "warn",
|
|
59
|
+
where: r.where,
|
|
60
|
+
message: `Cannot check cross-axis rule "${r.id}": unparseable size value(s)`
|
|
61
|
+
});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
36
64
|
const wv = ctx.getPx(r.when.id);
|
|
37
65
|
// compare-style loader rules set when.id = a, require.id = a; use same value if second missing
|
|
38
66
|
let rv = ctx.getPx(r.require.id);
|
|
@@ -18,25 +18,42 @@ export type Ctx = {
|
|
|
18
18
|
bp?: string;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
// Shares the hardened finite-size policy used by monotonic/threshold (TASK-037):
|
|
22
|
+
// bare numbers / unitless as px, `rem`/`em` as 16px-relative, real numbers only
|
|
23
|
+
// (rejects ".", "5.", "1.2.3px"), and non-finite guarded. The one addition over
|
|
24
|
+
// the canonical parser is a clamp()/min()/max()/calc() heuristic — but it is
|
|
25
|
+
// gated on a `(` so a malformed bare value like "1.2.3px" is rejected rather
|
|
26
|
+
// than silently yielding a partial parse.
|
|
21
27
|
const px = (v: unknown): number | null => {
|
|
22
|
-
if (typeof v === 'number') return v;
|
|
28
|
+
if (typeof v === 'number') return Number.isFinite(v) ? v : null;
|
|
23
29
|
if (typeof v !== 'string') return null;
|
|
24
30
|
const trimmed = v.trim();
|
|
25
|
-
// Direct simple form
|
|
26
|
-
|
|
31
|
+
// Direct simple form: bare number / px / rem / em.
|
|
32
|
+
const m = trimmed.match(/^(\d*\.?\d+)\s*(px|rem|em)?$/i);
|
|
27
33
|
if (m) {
|
|
28
34
|
const n = parseFloat(m[1]);
|
|
29
|
-
|
|
35
|
+
if (!Number.isFinite(n)) return null;
|
|
36
|
+
const unit = (m[2] || 'px').toLowerCase();
|
|
37
|
+
return unit === 'rem' || unit === 'em' ? n * 16 : n;
|
|
30
38
|
}
|
|
31
|
-
// Heuristic:
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
const
|
|
35
|
-
|
|
39
|
+
// Heuristic: first px/rem/em token inside a CSS function expression (clamp(),
|
|
40
|
+
// min(), max(), calc(), …). Gated on `(` so garbage simple values don't leak.
|
|
41
|
+
if (trimmed.includes('(')) {
|
|
42
|
+
const inner = trimmed.match(/(\d*\.?\d+)\s*(px|rem|em)/i);
|
|
43
|
+
if (inner) {
|
|
44
|
+
const n = parseFloat(inner[1]);
|
|
45
|
+
if (!Number.isFinite(n)) return null;
|
|
46
|
+
return inner[2].toLowerCase() === 'px' ? n : n * 16;
|
|
47
|
+
}
|
|
36
48
|
}
|
|
37
49
|
return null;
|
|
38
50
|
};
|
|
39
51
|
|
|
52
|
+
// Distinguish "operand absent" (skip silently) from "operand present but
|
|
53
|
+
// unparseable" (warn loudly) — a silent skip of a present value is a false green.
|
|
54
|
+
const presentButUnparseable = (raw: unknown): boolean =>
|
|
55
|
+
raw != null && raw !== '' && px(raw) === null;
|
|
56
|
+
|
|
40
57
|
export function CrossAxisPlugin(rules: CrossAxisRule[], bp?: string): ConstraintPlugin {
|
|
41
58
|
return {
|
|
42
59
|
id: "cross-axis",
|
|
@@ -57,6 +74,17 @@ export function CrossAxisPlugin(rules: CrossAxisRule[], bp?: string): Constraint
|
|
|
57
74
|
if ("when" in r) {
|
|
58
75
|
// Evaluate if either referenced id is among candidates (looser gating so global validate works)
|
|
59
76
|
if (!candidates.has(r.when.id) && !candidates.has(r.require.id)) continue;
|
|
77
|
+
// Present-but-unparseable operand → warn, don't silently skip (TASK-037).
|
|
78
|
+
if (presentButUnparseable(ctx.get(r.when.id)) || presentButUnparseable(ctx.get(r.require.id))) {
|
|
79
|
+
issues.push({
|
|
80
|
+
id: r.require.id,
|
|
81
|
+
rule: "cross-axis",
|
|
82
|
+
level: "warn",
|
|
83
|
+
where: r.where,
|
|
84
|
+
message: `Cannot check cross-axis rule "${r.id}": unparseable size value(s)`
|
|
85
|
+
});
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
60
88
|
const wv = ctx.getPx(r.when.id);
|
|
61
89
|
// compare-style loader rules set when.id = a, require.id = a; use same value if second missing
|
|
62
90
|
let rv = ctx.getPx(r.require.id);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"monotonic.d.ts","sourceRoot":"","sources":["monotonic.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,wBAAgB,eAAe,CAC7B,MAAM,EAAE,KAAK,EAAE,EACf,KAAK,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,EACpC,MAAM,SAAc,GACnB,gBAAgB,
|
|
1
|
+
{"version":3,"file":"monotonic.d.ts","sourceRoot":"","sources":["monotonic.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,wBAAgB,eAAe,CAC7B,MAAM,EAAE,KAAK,EAAE,EACf,KAAK,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,EACpC,MAAM,SAAc,GACnB,gBAAgB,CAoClB;AAMD,eAAO,MAAM,SAAS,GAAI,GAAG,OAAO,KAAG,MAAM,GAAG,IAW/C,CAAC;AAGF,eAAO,MAAM,WAAW,GAAI,GAAG,OAAO,KAAG,MAAM,GAAG,IAOjD,CAAC;AAGF,eAAO,MAAM,cAAc,GAAI,GAAG,OAAO,KAAG,MAAM,GAAG,IAoBpD,CAAC"}
|
|
@@ -4,12 +4,27 @@ export function MonotonicPlugin(orders, parse, ruleId = "monotonic") {
|
|
|
4
4
|
evaluate(engine, candidates) {
|
|
5
5
|
const issues = [];
|
|
6
6
|
for (const [a, op, b] of orders) {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
if (!candidates.has(a) && !candidates.has(b))
|
|
8
|
+
continue; // incremental
|
|
9
|
+
const rawA = engine.get(a);
|
|
10
|
+
const rawB = engine.get(b);
|
|
11
|
+
const va = parse(rawA);
|
|
12
|
+
const vb = parse(rawB);
|
|
13
|
+
// A present-but-unparseable size operand (e.g. "50%", "10vw") can't be
|
|
14
|
+
// compared — warn instead of silently skipping (TASK-031).
|
|
15
|
+
if ((rawA != null && rawA !== "" && va == null) || (rawB != null && rawB !== "" && vb == null)) {
|
|
16
|
+
issues.push({
|
|
17
|
+
id: `${a}|${b}`,
|
|
18
|
+
rule: "monotonic",
|
|
19
|
+
level: "warn",
|
|
20
|
+
message: `Cannot check ${a} ${op} ${b}: unparseable size value(s)`
|
|
21
|
+
});
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
9
24
|
if (va == null || vb == null)
|
|
10
|
-
continue; //
|
|
25
|
+
continue; // operand absent — nothing to check
|
|
11
26
|
const ok = op === ">=" ? va >= vb : va <= vb;
|
|
12
|
-
if (!ok
|
|
27
|
+
if (!ok) {
|
|
13
28
|
issues.push({
|
|
14
29
|
id: `${a}|${b}`,
|
|
15
30
|
rule: "monotonic",
|
|
@@ -22,16 +37,25 @@ export function MonotonicPlugin(orders, parse, ruleId = "monotonic") {
|
|
|
22
37
|
}
|
|
23
38
|
};
|
|
24
39
|
}
|
|
25
|
-
// a
|
|
40
|
+
// Parse a size to px for comparison. Coerces bare numbers and unitless strings to
|
|
41
|
+
// px (TASK-031: numeric `$value` and aliases-to-numbers previously returned null,
|
|
42
|
+
// silently skipping the rule). `rem`/`em` are 16px-relative. Returns null only for
|
|
43
|
+
// genuinely unparseable operands (e.g. "50%", "10vw") — callers warn, not skip.
|
|
26
44
|
export const parseSize = (v) => {
|
|
45
|
+
if (typeof v === "number")
|
|
46
|
+
return Number.isFinite(v) ? v : null; // bare number == px
|
|
27
47
|
if (typeof v !== "string")
|
|
28
48
|
return null;
|
|
29
|
-
|
|
49
|
+
// Real number only: rejects ".", "5.", "1.2.3px" (which previously slipped
|
|
50
|
+
// through `[0-9.]+` + parseFloat as NaN/garbage and became spurious errors).
|
|
51
|
+
const m = v.trim().match(/^(\d*\.?\d+)\s*(px|rem|em)?$/i);
|
|
30
52
|
if (!m)
|
|
31
53
|
return null;
|
|
32
54
|
const num = parseFloat(m[1]);
|
|
33
|
-
|
|
34
|
-
|
|
55
|
+
if (!Number.isFinite(num))
|
|
56
|
+
return null;
|
|
57
|
+
const unit = (m[2] || "px").toLowerCase();
|
|
58
|
+
return unit === "rem" || unit === "em" ? num * 16 : num; // px and unitless as-is
|
|
35
59
|
};
|
|
36
60
|
// Parser for unitless numbers (like scale factors)
|
|
37
61
|
export const parseNumber = (v) => {
|
|
@@ -12,11 +12,25 @@ export function MonotonicPlugin(
|
|
|
12
12
|
evaluate(engine, candidates) {
|
|
13
13
|
const issues = [];
|
|
14
14
|
for (const [a, op, b] of orders) {
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
15
|
+
if (!candidates.has(a) && !candidates.has(b)) continue; // incremental
|
|
16
|
+
const rawA = engine.get(a);
|
|
17
|
+
const rawB = engine.get(b);
|
|
18
|
+
const va = parse(rawA);
|
|
19
|
+
const vb = parse(rawB);
|
|
20
|
+
// A present-but-unparseable size operand (e.g. "50%", "10vw") can't be
|
|
21
|
+
// compared — warn instead of silently skipping (TASK-031).
|
|
22
|
+
if ((rawA != null && rawA !== "" && va == null) || (rawB != null && rawB !== "" && vb == null)) {
|
|
23
|
+
issues.push({
|
|
24
|
+
id: `${a}|${b}`,
|
|
25
|
+
rule: "monotonic",
|
|
26
|
+
level: "warn" as const,
|
|
27
|
+
message: `Cannot check ${a} ${op} ${b}: unparseable size value(s)`
|
|
28
|
+
});
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (va == null || vb == null) continue; // operand absent — nothing to check
|
|
18
32
|
const ok = op === ">=" ? va >= vb : va <= vb;
|
|
19
|
-
if (!ok
|
|
33
|
+
if (!ok) {
|
|
20
34
|
issues.push({
|
|
21
35
|
id: `${a}|${b}`,
|
|
22
36
|
rule: "monotonic",
|
|
@@ -30,14 +44,21 @@ export function MonotonicPlugin(
|
|
|
30
44
|
};
|
|
31
45
|
}
|
|
32
46
|
|
|
33
|
-
// a
|
|
47
|
+
// Parse a size to px for comparison. Coerces bare numbers and unitless strings to
|
|
48
|
+
// px (TASK-031: numeric `$value` and aliases-to-numbers previously returned null,
|
|
49
|
+
// silently skipping the rule). `rem`/`em` are 16px-relative. Returns null only for
|
|
50
|
+
// genuinely unparseable operands (e.g. "50%", "10vw") — callers warn, not skip.
|
|
34
51
|
export const parseSize = (v: unknown): number | null => {
|
|
52
|
+
if (typeof v === "number") return Number.isFinite(v) ? v : null; // bare number == px
|
|
35
53
|
if (typeof v !== "string") return null;
|
|
36
|
-
|
|
54
|
+
// Real number only: rejects ".", "5.", "1.2.3px" (which previously slipped
|
|
55
|
+
// through `[0-9.]+` + parseFloat as NaN/garbage and became spurious errors).
|
|
56
|
+
const m = v.trim().match(/^(\d*\.?\d+)\s*(px|rem|em)?$/i);
|
|
37
57
|
if (!m) return null;
|
|
38
58
|
const num = parseFloat(m[1]);
|
|
39
|
-
|
|
40
|
-
|
|
59
|
+
if (!Number.isFinite(num)) return null;
|
|
60
|
+
const unit = (m[2] || "px").toLowerCase();
|
|
61
|
+
return unit === "rem" || unit === "em" ? num * 16 : num; // px and unitless as-is
|
|
41
62
|
};
|
|
42
63
|
|
|
43
64
|
// Parser for unitless numbers (like scale factors)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"threshold.d.ts","sourceRoot":"","sources":["threshold.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC1B,CAAC;
|
|
1
|
+
{"version":3,"file":"threshold.d.ts","sourceRoot":"","sources":["threshold.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC1B,CAAC;AAiBF,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,MAAM,SAAc,GAAG,gBAAgB,CAoC9F"}
|
|
@@ -1,12 +1,20 @@
|
|
|
1
|
+
// Coerces bare numbers and unitless strings to px (TASK-031: numeric `$value`
|
|
2
|
+
// previously returned null, silently skipping the threshold). `rem`/`em` are
|
|
3
|
+
// 16px-relative. Returns null only for genuinely unparseable operands.
|
|
1
4
|
const parseSizePx = (v) => {
|
|
5
|
+
if (typeof v === "number")
|
|
6
|
+
return Number.isFinite(v) ? v : null;
|
|
2
7
|
if (typeof v !== "string")
|
|
3
8
|
return null;
|
|
4
|
-
|
|
9
|
+
// Real number only — rejects ".", "5.", "1.2.3px" (TASK-034).
|
|
10
|
+
const m = v.trim().match(/^(\d*\.?\d+)\s*(px|rem|em)?$/i);
|
|
5
11
|
if (!m)
|
|
6
12
|
return null;
|
|
7
13
|
const n = parseFloat(m[1]);
|
|
14
|
+
if (!Number.isFinite(n))
|
|
15
|
+
return null;
|
|
8
16
|
const unit = (m[2] || "px").toLowerCase();
|
|
9
|
-
return unit === "rem" ? n * 16 : n;
|
|
17
|
+
return unit === "rem" || unit === "em" ? n * 16 : n;
|
|
10
18
|
};
|
|
11
19
|
export function ThresholdPlugin(rules, ruleId = "threshold") {
|
|
12
20
|
return {
|
|
@@ -16,9 +24,21 @@ export function ThresholdPlugin(rules, ruleId = "threshold") {
|
|
|
16
24
|
for (const r of rules) {
|
|
17
25
|
if (!candidates.has(r.id))
|
|
18
26
|
continue; // incremental
|
|
19
|
-
const
|
|
20
|
-
|
|
27
|
+
const raw = engine.get(r.id);
|
|
28
|
+
const px = parseSizePx(raw);
|
|
29
|
+
if (px == null) {
|
|
30
|
+
// Present-but-unparseable size — warn instead of silently skipping (TASK-031).
|
|
31
|
+
if (raw != null && raw !== "") {
|
|
32
|
+
issues.push({
|
|
33
|
+
id: r.id,
|
|
34
|
+
rule: ruleId,
|
|
35
|
+
level: "warn",
|
|
36
|
+
where: r.where,
|
|
37
|
+
message: `Cannot check ${r.id} ${r.op} ${r.valuePx}px: unparseable size value`
|
|
38
|
+
});
|
|
39
|
+
}
|
|
21
40
|
continue;
|
|
41
|
+
}
|
|
22
42
|
const ok = r.op === ">=" ? px >= r.valuePx : px <= r.valuePx;
|
|
23
43
|
if (!ok) {
|
|
24
44
|
issues.push({
|
|
@@ -8,13 +8,19 @@ export type ThresholdRule = {
|
|
|
8
8
|
level?: "error" | "warn"; // default error
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// Coerces bare numbers and unitless strings to px (TASK-031: numeric `$value`
|
|
12
|
+
// previously returned null, silently skipping the threshold). `rem`/`em` are
|
|
13
|
+
// 16px-relative. Returns null only for genuinely unparseable operands.
|
|
11
14
|
const parseSizePx = (v: unknown): number | null => {
|
|
15
|
+
if (typeof v === "number") return Number.isFinite(v) ? v : null;
|
|
12
16
|
if (typeof v !== "string") return null;
|
|
13
|
-
|
|
17
|
+
// Real number only — rejects ".", "5.", "1.2.3px" (TASK-034).
|
|
18
|
+
const m = v.trim().match(/^(\d*\.?\d+)\s*(px|rem|em)?$/i);
|
|
14
19
|
if (!m) return null;
|
|
15
20
|
const n = parseFloat(m[1]);
|
|
21
|
+
if (!Number.isFinite(n)) return null;
|
|
16
22
|
const unit = (m[2] || "px").toLowerCase();
|
|
17
|
-
return unit === "rem" ? n * 16 : n;
|
|
23
|
+
return unit === "rem" || unit === "em" ? n * 16 : n;
|
|
18
24
|
};
|
|
19
25
|
|
|
20
26
|
export function ThresholdPlugin(rules: ThresholdRule[], ruleId = "threshold"): ConstraintPlugin {
|
|
@@ -24,8 +30,21 @@ export function ThresholdPlugin(rules: ThresholdRule[], ruleId = "threshold"): C
|
|
|
24
30
|
const issues = [];
|
|
25
31
|
for (const r of rules) {
|
|
26
32
|
if (!candidates.has(r.id)) continue; // incremental
|
|
27
|
-
const
|
|
28
|
-
|
|
33
|
+
const raw = engine.get(r.id);
|
|
34
|
+
const px = parseSizePx(raw);
|
|
35
|
+
if (px == null) {
|
|
36
|
+
// Present-but-unparseable size — warn instead of silently skipping (TASK-031).
|
|
37
|
+
if (raw != null && raw !== "") {
|
|
38
|
+
issues.push({
|
|
39
|
+
id: r.id,
|
|
40
|
+
rule: ruleId,
|
|
41
|
+
level: "warn" as const,
|
|
42
|
+
where: r.where,
|
|
43
|
+
message: `Cannot check ${r.id} ${r.op} ${r.valuePx}px: unparseable size value`
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
29
48
|
const ok = r.op === ">=" ? px >= r.valuePx : px <= r.valuePx;
|
|
30
49
|
if (!ok) {
|
|
31
50
|
issues.push({
|
package/core/constraints/wcag.js
CHANGED
|
@@ -3,7 +3,7 @@ function resolveColor(engineGet, x) {
|
|
|
3
3
|
if (typeof x !== "string")
|
|
4
4
|
return undefined;
|
|
5
5
|
// If it's a CSS color literal, return as is; else treat as token id.
|
|
6
|
-
const isLiteral =
|
|
6
|
+
const isLiteral = /^(#|rgba?\(|hsla?\(|oklch\(|oklab\(|transparent$)/i.test(x);
|
|
7
7
|
return isLiteral ? x : String(engineGet(x) ?? "");
|
|
8
8
|
}
|
|
9
9
|
export function WcagContrastPlugin(pairs) {
|
package/core/constraints/wcag.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type ContrastPair = {
|
|
|
14
14
|
function resolveColor(engineGet: (id: TokenId)=>unknown, x: TokenId | string): string | undefined {
|
|
15
15
|
if (typeof x !== "string") return undefined;
|
|
16
16
|
// If it's a CSS color literal, return as is; else treat as token id.
|
|
17
|
-
const isLiteral =
|
|
17
|
+
const isLiteral = /^(#|rgba?\(|hsla?\(|oklch\(|oklab\(|transparent$)/i.test(x);
|
|
18
18
|
return isLiteral ? x : String(engineGet(x) ?? "");
|
|
19
19
|
}
|
|
20
20
|
|
package/core/flatten.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flatten.d.ts","sourceRoot":"","sources":["flatten.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAC7B,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAGzC,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAC1D,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,UAAU,GAAG,mBAAmB,CAAC;IAC1C,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAAC;CAC5E,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,UAAU,CAAC;IAClB,GAAG,EAAE,UAAU,GAAG,mBAAmB,CAAC;IACtC,IAAI,EAAE,OAAO,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACjC,KAAK,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;CAC5C,CAAC;AAKF,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,aAAa,
|
|
1
|
+
{"version":3,"file":"flatten.d.ts","sourceRoot":"","sources":["flatten.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAC7B,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAGzC,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAC1D,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,UAAU,GAAG,mBAAmB,CAAC;IAC1C,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAAC;CAC5E,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,UAAU,CAAC;IAClB,GAAG,EAAE,UAAU,GAAG,mBAAmB,CAAC;IACtC,IAAI,EAAE,OAAO,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACjC,KAAK,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;CAC5C,CAAC;AAKF,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,aAAa,CAqH5D"}
|
package/core/flatten.js
CHANGED
|
@@ -2,6 +2,14 @@ import { normalizeDtcgValue } from "./dtcg.js";
|
|
|
2
2
|
const REF_RE = /\{([a-z0-9_.-]+)\}/gi;
|
|
3
3
|
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
4
4
|
export function flattenTokens(root) {
|
|
5
|
+
// Fail closed on a malformed token root (TASK-017). A non-object root — null,
|
|
6
|
+
// an array, or a scalar — previously walked to a silently-empty set that
|
|
7
|
+
// "validated" with ok:true; that hid garbage input. The token root must be a
|
|
8
|
+
// JSON object. (An empty object `{}` is still valid — it just has no tokens.)
|
|
9
|
+
if (root === null || typeof root !== 'object' || Array.isArray(root)) {
|
|
10
|
+
const got = root === null ? 'null' : Array.isArray(root) ? 'array' : typeof root;
|
|
11
|
+
throw new Error(`Token root must be a JSON object (got ${got}).`);
|
|
12
|
+
}
|
|
5
13
|
const flat = {};
|
|
6
14
|
const edges = [];
|
|
7
15
|
// First pass: collect all tokens
|
package/core/flatten.ts
CHANGED
|
@@ -29,6 +29,15 @@ const REF_RE = /\{([a-z0-9_.-]+)\}/gi;
|
|
|
29
29
|
const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
30
30
|
|
|
31
31
|
export function flattenTokens(root: TokenNode): FlattenResult {
|
|
32
|
+
// Fail closed on a malformed token root (TASK-017). A non-object root — null,
|
|
33
|
+
// an array, or a scalar — previously walked to a silently-empty set that
|
|
34
|
+
// "validated" with ok:true; that hid garbage input. The token root must be a
|
|
35
|
+
// JSON object. (An empty object `{}` is still valid — it just has no tokens.)
|
|
36
|
+
if (root === null || typeof root !== 'object' || Array.isArray(root)) {
|
|
37
|
+
const got = root === null ? 'null' : Array.isArray(root) ? 'array' : typeof root;
|
|
38
|
+
throw new Error(`Token root must be a JSON object (got ${got}).`);
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
const flat: Record<TokenId, FlatToken> = {};
|
|
33
42
|
const edges: Array<[TokenId, TokenId]> = [];
|
|
34
43
|
|
package/core/poset.d.ts
CHANGED
|
@@ -3,7 +3,12 @@ export type Comp = "<=" | ">=";
|
|
|
3
3
|
export type Order = [Id, Comp, Id];
|
|
4
4
|
export type Digraph = Map<Id, Set<Id>>;
|
|
5
5
|
export type EdgeLabels = Map<string, string>;
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Safe, INJECTIVE node identifier for Mermaid/DOT. Each non-alphanumeric byte
|
|
8
|
+
* (including `_`) is encoded as `_<charCode>_`, so distinct token ids can never
|
|
9
|
+
* collapse to the same node id (e.g. `a.b` -> `a_46_b`, `a_b` -> `a_95_b`).
|
|
10
|
+
* A naive `[^a-zA-Z0-9_] -> _` replacement merged those, corrupting graphs.
|
|
11
|
+
*/
|
|
7
12
|
export declare function sanitizeId(id: string): string;
|
|
8
13
|
export type Highlight = {
|
|
9
14
|
nodes?: Set<string>;
|
package/core/poset.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"poset.d.ts","sourceRoot":"","sources":["poset.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,EAAE,GAAG,MAAM,CAAC;AACxB,MAAM,MAAM,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAC/B,MAAM,MAAM,KAAK,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;AAEnC,MAAM,MAAM,OAAO,GAAG,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;AAGvC,MAAM,MAAM,UAAU,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAM7C
|
|
1
|
+
{"version":3,"file":"poset.d.ts","sourceRoot":"","sources":["poset.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,EAAE,GAAG,MAAM,CAAC;AACxB,MAAM,MAAM,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAC/B,MAAM,MAAM,KAAK,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;AAEnC,MAAM,MAAM,OAAO,GAAG,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;AAGvC,MAAM,MAAM,UAAU,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAM7C;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACpB,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAYnD;AAED,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAwBvD;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,EAAC,KAAe,EAAC;;CAAK,GAAG,MAAM,CAQ9E;AAGD,wBAAgB,oBAAoB,CAClC,CAAC,EAAE,OAAO,EACV,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAAC,MAAM,CAAC,EAAE,UAAU,CAAA;CAAO,GACxE,MAAM,CAuCR;AAED,wBAAgB,UAAU,CACxB,MAAM,EAAE,OAAO,EACf,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,UAAU,CAAA;CAAO,GACjD,MAAM,CAoBR;AAGD,wBAAgB,gBAAgB,CAC9B,CAAC,EAAE,OAAO,EACV,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAAC,MAAM,CAAC,EAAE,UAAU,CAAA;CAAO,GACxE,MAAM,CAyCR;AAGD,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,CAAA;CAAE,CA2C7E;AAED,wBAAgB,aAAa,CAC3B,CAAC,EAAE,OAAO,EACV,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,GACjC,OAAO,CAYT;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAKtE;AAED,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAK3E;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,SAAI,GAAG,OAAO,CA8B3E;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAOxF"}
|
package/core/poset.js
CHANGED
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
// Small escapes so Mermaid/DOT don't choke on special characters
|
|
4
4
|
const escMermaid = (s) => s.replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
5
5
|
const escDot = (s) => s.replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Safe, INJECTIVE node identifier for Mermaid/DOT. Each non-alphanumeric byte
|
|
8
|
+
* (including `_`) is encoded as `_<charCode>_`, so distinct token ids can never
|
|
9
|
+
* collapse to the same node id (e.g. `a.b` -> `a_46_b`, `a_b` -> `a_95_b`).
|
|
10
|
+
* A naive `[^a-zA-Z0-9_] -> _` replacement merged those, corrupting graphs.
|
|
11
|
+
*/
|
|
7
12
|
export function sanitizeId(id) {
|
|
8
|
-
return id.replace(/[^a-zA-Z0-
|
|
13
|
+
return id.replace(/[^a-zA-Z0-9]/g, (c) => `_${c.charCodeAt(0)}_`);
|
|
9
14
|
}
|
|
10
15
|
export function buildPoset(orders) {
|
|
11
16
|
const g = new Map();
|
package/core/poset.ts
CHANGED
|
@@ -14,9 +14,14 @@ export type EdgeLabels = Map<string, string>; // key = "a|b" (raw ids)
|
|
|
14
14
|
const escMermaid = (s: string) => s.replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
15
15
|
const escDot = (s: string) => s.replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
16
16
|
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Safe, INJECTIVE node identifier for Mermaid/DOT. Each non-alphanumeric byte
|
|
19
|
+
* (including `_`) is encoded as `_<charCode>_`, so distinct token ids can never
|
|
20
|
+
* collapse to the same node id (e.g. `a.b` -> `a_46_b`, `a_b` -> `a_95_b`).
|
|
21
|
+
* A naive `[^a-zA-Z0-9_] -> _` replacement merged those, corrupting graphs.
|
|
22
|
+
*/
|
|
18
23
|
export function sanitizeId(id: string): string {
|
|
19
|
-
return id.replace(/[^a-zA-Z0-
|
|
24
|
+
return id.replace(/[^a-zA-Z0-9]/g, (c) => `_${c.charCodeAt(0)}_`);
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
export type Highlight = {
|