uxml-preview 0.4.0 → 0.5.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.
@@ -1,5 +1,5 @@
1
1
  import { ElementNode, NodeId, Warning } from '../model/types';
2
- import { ComputedStyle } from '../style/resolve';
2
+ import { ComputedStyle, DeclarationLocator } from '../style/resolve';
3
3
  /**
4
4
  * Purpose: load the Yoga WebAssembly module.
5
5
  * Deps/Effects: caches the module; awaiting it more than once is harmless.
@@ -41,6 +41,8 @@ export interface LayoutOptions {
41
41
  * accuracy if the measurement shifts underneath it.
42
42
  */
43
43
  measureText: MeasureText;
44
+ /** Points a value-level warning at the declaration it came from. */
45
+ locate?: DeclarationLocator;
44
46
  }
45
47
  /**
46
48
  * A laid-out element the file never mentioned, built by a control.
@@ -66,6 +66,8 @@ export interface Attribute {
66
66
  */
67
67
  value: string;
68
68
  span: Span;
69
+ /** Render-only provenance for a cloned/overridden attribute. */
70
+ sourceDocument?: string | null;
69
71
  }
70
72
  /**
71
73
  * Three spans rather than one, so that an edit stays local.
@@ -111,6 +113,21 @@ export interface ElementNode {
111
113
  * synthesized (copy a sibling's leading whitespace).
112
114
  */
113
115
  childrenDirty: boolean;
116
+ /**
117
+ * A render-only node synthesized while a template instance is expanded.
118
+ * Parsed nodes leave this absent; serialization never sees the derived tree.
119
+ */
120
+ derived?: {
121
+ kind: 'template-container';
122
+ /** The source `<ui:Instance>` that caused this container to exist. */
123
+ instance: NodeId;
124
+ };
125
+ /** URL of the UXML document that supplied a render-only clone. */
126
+ sourceDocument?: string | null;
127
+ /** URL that sourceDocument was resolved from, when it was relative. */
128
+ sourceDocumentFrom?: string | null;
129
+ /** Node id inside sourceDocument for a render-only clone. */
130
+ sourceNode?: NodeId;
114
131
  }
115
132
  export type Combinator = 'descendant' | 'child';
116
133
  /**
@@ -209,9 +226,90 @@ export interface StyleSheet {
209
226
  * passed directly to `parse()`. Used in warning messages only.
210
227
  */
211
228
  origin: string | null;
229
+ /** Containing URL used to resolve origin in a derived template document. */
230
+ sourceDocumentFrom?: string | null;
212
231
  items: SheetItem[];
213
232
  }
214
- export type WarningKind = 'unsupported-control' | 'unsupported-property' | 'unsupported-selector' | 'unsupported-unit' | 'version-dependent' | 'asset-unresolved' | 'import-unresolved' | 'malformed';
233
+ export type WarningKind = 'unsupported-control' | 'unsupported-property' | 'unsupported-selector' | 'unsupported-unit' | 'version-dependent' | 'asset-unresolved' | 'import-unresolved' | 'malformed' | 'template-src-unresolved' | 'template-not-declared' | 'template-cycle' | 'template-depth-exceeded' | 'override-target-missing' | 'duplicate-name-in-tree' | 'package-path-not-searched' | 'template-slot-unsupported' | 'override-style-ignored';
234
+ /** A mapped type consumers can use for exhaustive warning classification. */
235
+ export type WarningKindMap<T> = {
236
+ [K in WarningKind]: T;
237
+ };
238
+ /**
239
+ * Why a support warning was raised — what a consumer acts on instead of the
240
+ * message text.
241
+ *
242
+ * - `not-uss` the author wrote something USS does not have
243
+ * - `not-rendered` valid USS/UXML this preview does not draw or apply
244
+ * - `invalid-value` a valid name with a value or target that cannot work
245
+ * - `version-dependent` the result depends on the Unity version
246
+ * - `matches-unity` ignored, and Unity ignores it too; not a defect
247
+ *
248
+ * **An open set.** New reasons may be added in a minor release, so a consumer
249
+ * must give unknown values a default branch rather than an exhaustive switch.
250
+ * Absent when none applies, e.g. malformed text or a file the host could not
251
+ * resolve.
252
+ */
253
+ export type WarningReason = 'not-uss' | 'not-rendered' | 'invalid-value' | 'version-dependent' | 'matches-unity';
254
+ /**
255
+ * What a warning is about, as data, so a consumer never has to parse it back
256
+ * out of `message`.
257
+ *
258
+ * **An open set**, like `WarningReason`: new variants may be added in a minor
259
+ * release, so switch on `kind` with a default branch.
260
+ */
261
+ export type WarningSubject = {
262
+ kind: 'property';
263
+ property: string;
264
+ /** As written, before `var()` substitution where the warning is about the declaration. */
265
+ value?: string;
266
+ /** First Unity version whose USS reference lists the property, e.g. `"6000.3"`. */
267
+ since?: string;
268
+ /** USS properties to write instead, for a CSS name USS spells differently. */
269
+ replacements?: readonly string[];
270
+ /**
271
+ * On a `not-rendered` warning: whether the property can change a box's size
272
+ * or position in Unity. When true, this preview's coordinates may differ
273
+ * from Unity's — do not treat a coordinate-based finding (zero size,
274
+ * overflow) as settled.
275
+ */
276
+ affectsLayout?: boolean;
277
+ /**
278
+ * On a `not-rendered` warning: the part this preview did apply, when it
279
+ * applied some. `'paint'` means drawn, but not laid out as Unity would —
280
+ * `letter-spacing` is drawn and left out of text measurement.
281
+ */
282
+ applied?: 'paint';
283
+ } | {
284
+ kind: 'selector';
285
+ /** The selector text as written. */
286
+ selector: string;
287
+ /** The part of it the warning is about, when narrower than the whole. */
288
+ fragment?: string;
289
+ } | {
290
+ kind: 'states-key';
291
+ key: string;
292
+ } | {
293
+ kind: 'asset';
294
+ path: string;
295
+ form: 'url' | 'resource';
296
+ } | {
297
+ kind: 'import';
298
+ /** Exactly the string the resolver hook was (or would have been) called with. */
299
+ url: string;
300
+ from: string | null;
301
+ } | {
302
+ kind: 'control';
303
+ type: string;
304
+ } | {
305
+ kind: 'name';
306
+ name: string;
307
+ }
308
+ /** An at-rule block, without the `@`. */
309
+ | {
310
+ kind: 'at-rule';
311
+ name: string;
312
+ };
215
313
  /**
216
314
  * Never a thrown error. One unsupported property must not take down the render
217
315
  * (see CLAUDE.md rule 6).
@@ -221,11 +319,19 @@ export type WarningKind = 'unsupported-control' | 'unsupported-property' | 'unsu
221
319
  */
222
320
  export interface Warning {
223
321
  kind: WarningKind;
322
+ /**
323
+ * For people. Read `reason` and `subject` instead; the text may be
324
+ * reworded in any release.
325
+ */
224
326
  message: string;
327
+ /** Document URL for a warning originating outside the entry UXML. */
328
+ sourceDocument?: string | null;
225
329
  /** Text location, when the warning has one. */
226
330
  at?: SourceRef;
227
331
  /** Element the warning is about, when it has one. Lets the UI highlight it. */
228
332
  node?: NodeId;
333
+ reason?: WarningReason;
334
+ subject?: WarningSubject;
229
335
  }
230
336
  export interface UxmlDocument {
231
337
  /** The exact UXML text every tree span indexes into. */
@@ -276,11 +382,17 @@ export type StyleOrigin = {
276
382
  kind: 'inline';
277
383
  node: NodeId;
278
384
  declIndex: number;
385
+ /** URL of the UXML/USS document that supplied this declaration. */
386
+ sourceDocument?: string | null;
387
+ sourceDocumentFrom?: string | null;
279
388
  } | {
280
389
  kind: 'rule';
281
390
  sheet: number;
282
391
  item: number;
283
392
  declIndex: number;
393
+ /** URL of the stylesheet containing the declaration, when external. */
394
+ sourceDocument?: string | null;
395
+ sourceDocumentFrom?: string | null;
284
396
  /**
285
397
  * Pseudo-class states the matched selector required, e.g. `['hover']`.
286
398
  *
@@ -1,5 +1,5 @@
1
1
  import { NodeId, Warning } from '../model/types';
2
- import { ComputedStyle } from '../style/resolve';
2
+ import { ComputedStyle, DeclarationLocator } from '../style/resolve';
3
3
  export interface CssMapOptions {
4
4
  /**
5
5
  * `form` tells `url("...")` apart from `resource("...")` — Unity resolves
@@ -11,6 +11,8 @@ export interface CssMapOptions {
11
11
  * file happens to sit alongside it — or resolves nothing at all.
12
12
  */
13
13
  resolveAsset?: ((path: string, form: 'url' | 'resource') => string | null) | undefined;
14
+ /** Points a value-level warning at the declaration it came from. */
15
+ locate?: DeclarationLocator | undefined;
14
16
  }
15
17
  export interface CssMapResult {
16
18
  declarations: Record<string, string>;
@@ -36,6 +36,8 @@ export interface LengthResult {
36
36
  length: Length | null;
37
37
  /** Set when the text names a unit USS does not support, or uses calc(). */
38
38
  problem?: string;
39
+ /** Set with `problem`: USS lacks the syntax, or the value is simply unreadable. */
40
+ reason?: 'not-uss' | 'invalid-value';
39
41
  }
40
42
  /**
41
43
  * Purpose: `"6px"` / `"50%"` / `"auto"` / `"0"` -> a number with a unit.
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Every USS property Unity documents, and what this renderer does with it.
3
+ *
4
+ * Transcribed from Unity's USS property reference, not reasoned from CSS. The
5
+ * renderer's first failure mode is assuming a CSS name or default carries over,
6
+ * and a hand-kept list per concern (inherited, unsupported, version-dependent)
7
+ * had already drifted three ways: `text-shadow` missing from inheritance,
8
+ * `filter` reported as non-USS after 6.3 added it, `gap` reported as
9
+ * version-dependent although no reference table up to 6.6 has it.
10
+ *
11
+ * Source (checked 2026-10-13, see `USS_PROPERTY_SOURCE`):
12
+ * https://docs.unity.com/en-us/engine/6000.0/manual/uitoolkits/uielements/uie-uss/properties/reference (95 rows)
13
+ * https://docs.unity.com/en-us/engine/6000.3/manual/uitoolkits/uielements/uie-uss/properties/reference (99 rows)
14
+ * https://docs.unity.com/en-us/engine/6000.6/manual/uitoolkits/uielements/uie-uss/properties/reference (102 rows)
15
+ *
16
+ * `since` and `inherited` are Unity's facts and come from those tables alone.
17
+ * `handling` is this renderer's fact, observed rather than declared: `layout`
18
+ * means it can move a box (src/layout/yoga.ts), `paint` means it changes what
19
+ * is drawn and never where (src/render/css-map.ts), `none` means nothing
20
+ * reads it. A row's handling may only change together with the code —
21
+ * tests/style/registry.test.ts renders every row and fails when the two
22
+ * disagree.
23
+ */
24
+ /** The first Unity release whose reference table lists the property. */
25
+ export type UssVersion = '6000.0' | '6000.3' | '6000.6';
26
+ export type Handling = 'layout' | 'paint' | 'none';
27
+ /** What a property can change in Unity: a box's size or position, only pixels, or nothing in a settled frame. */
28
+ export type UnityEffect = 'layout' | 'paint' | 'none';
29
+ export interface UssProperty {
30
+ readonly name: string;
31
+ readonly since: UssVersion;
32
+ /** Unity's "Inherited" column. */
33
+ readonly inherited: boolean;
34
+ /** What THIS renderer does with it. Observed; see the header. */
35
+ readonly handling: Handling;
36
+ /**
37
+ * What it can do IN UNITY — a different question from `handling`, and the
38
+ * two must not be read for each other. Where they disagree (`unity: 'layout'`
39
+ * with `handling` other than `'layout'`), this preview's coordinates can
40
+ * differ from Unity's whenever the property is written.
41
+ */
42
+ readonly unity: UnityEffect;
43
+ /** Why `unity` is what it is, in one line. */
44
+ readonly basis: string;
45
+ /** True when `basis` is reasoning rather than a Unity statement or a measurement. */
46
+ readonly inferred: boolean;
47
+ }
48
+ export declare const USS_PROPERTY_SOURCE: {
49
+ readonly checked: "2026-10-13";
50
+ readonly tables: {
51
+ readonly '6000.0': "https://docs.unity.com/en-us/engine/6000.0/manual/uitoolkits/uielements/uie-uss/properties/reference";
52
+ readonly '6000.3': "https://docs.unity.com/en-us/engine/6000.3/manual/uitoolkits/uielements/uie-uss/properties/reference";
53
+ readonly '6000.6': "https://docs.unity.com/en-us/engine/6000.6/manual/uitoolkits/uielements/uie-uss/properties/reference";
54
+ };
55
+ /** Row count of each table on the date above. A test holds the data to these. */
56
+ readonly counts: {
57
+ readonly '6000.0': 95;
58
+ readonly '6000.3': 99;
59
+ readonly '6000.6': 102;
60
+ };
61
+ };
62
+ export declare const USS_PROPERTIES: readonly UssProperty[];
63
+ /** Undefined for anything Unity does not document, custom properties included. */
64
+ export declare function ussProperty(name: string): UssProperty | undefined;
65
+ /**
66
+ * CSS names an author porting a stylesheet reaches for, and the USS
67
+ * properties that do the same job.
68
+ *
69
+ * Chosen, not exhaustive. The first six are the renamed rows of CLAUDE.md's
70
+ * "이름이 다른 속성" table and docs/uss-reference.md; the rest are CSS
71
+ * shorthands or per-axis splits whose every part USS has under another name.
72
+ * A CSS property with no USS counterpart up to Unity 6.6 (`z-index`, `box-shadow`, `gap`) is
73
+ * deliberately absent — offering a replacement there would invent one.
74
+ */
75
+ export declare const CSS_REPLACEMENTS: Readonly<Record<string, readonly string[]>>;
76
+ export interface PropertyWarning {
77
+ message: string;
78
+ reason: 'not-uss' | 'not-rendered';
79
+ subject: {
80
+ kind: 'property';
81
+ property: string;
82
+ value: string;
83
+ since?: string;
84
+ replacements?: readonly string[];
85
+ affectsLayout?: boolean;
86
+ applied?: 'paint';
87
+ };
88
+ }
89
+ /**
90
+ * Purpose: what a declaration warrants by its property name alone.
91
+ * Ensures: null for custom properties and for anything this renderer
92
+ * reads; a value-level problem is the reader's to report.
93
+ *
94
+ * Judged per declaration rather than per element, so a rule matching a
95
+ * hundred elements — or matching none — says it once, and an inherited value
96
+ * never says it again on every descendant.
97
+ */
98
+ export declare function propertyWarning(property: string, value: string): PropertyWarning | null;
99
+ /**
100
+ * The pseudo-class states a Unity element can be in — what `states` and
101
+ * `activeStates` accept and what a USS state selector can match.
102
+ *
103
+ * From Unity's "USS selectors: Pseudo-classes" page (checked 2026-10-13):
104
+ * https://docs.unity3d.com/Manual/UIE-USS-Selectors-Pseudo-Classes.html
105
+ * `:root` is excluded because it names a place in the tree, not a state.
106
+ * `:selected` is excluded because that page says USS does not support it
107
+ * ("Use :checked instead"). Exported so consumers stop keeping their own copy.
108
+ */
109
+ export declare const USS_STATES: readonly ["hover", "active", "inactive", "focus", "disabled", "enabled", "checked"];
110
+ export type UssState = (typeof USS_STATES)[number];
@@ -72,6 +72,16 @@ export interface Candidate {
72
72
  order: number;
73
73
  winner: boolean;
74
74
  }
75
+ /** Where a computed value was written, as warning fields. */
76
+ export type DeclarationLocator = (origin: StyleOrigin) => Pick<Warning, 'at' | 'sourceDocument'>;
77
+ /**
78
+ * Purpose: turn a computed value's origin back into the text it came from,
79
+ * so a value-level warning points at one declaration.
80
+ * Ensures: empty for inherited and built-in values — an inherited value is
81
+ * reported where it was declared, not on every descendant — and
82
+ * for a template's inline style, which names its file instead.
83
+ */
84
+ export declare function declarationLocator(doc: UxmlDocument): DeclarationLocator;
75
85
  /**
76
86
  * Purpose: compute the style of every element in the document.
77
87
  * Deps/Effects: reads `doc` only; nothing on the model is mutated.
@@ -0,0 +1,36 @@
1
+ import { UxmlDocument, Warning } from '../model/types';
2
+ export type TemplateResolver = (url: string, from: string | null) => string | null;
3
+ type TemplateDocumentParser = (source: string, resolver: TemplateResolver | undefined) => UxmlDocument;
4
+ /** Internal registration avoids making the template loader depend on index.ts. */
5
+ export declare function registerTemplateParser(parser: TemplateDocumentParser): void;
6
+ /** Called by parse() so expansion can reuse the exact resolver the host passed. */
7
+ export declare function rememberTemplateResolver(document: UxmlDocument, resolver: TemplateResolver | undefined): void;
8
+ /**
9
+ * Purpose: collect direct template dependencies without a regex pass.
10
+ * Deps/Effects: parses `source`; no I/O and no model mutation.
11
+ * Ensures: returns decoded `<Template src>` values in source order.
12
+ */
13
+ export declare function collectDependencies(source: string): string[];
14
+ export interface TemplateExpansion {
15
+ /** Render-only document. The caller must keep the original for serialization. */
16
+ document: UxmlDocument;
17
+ warnings: readonly Warning[];
18
+ }
19
+ /**
20
+ * How deeply `<ui:Instance>` may nest before expansion stops with
21
+ * `template-depth-exceeded`.
22
+ *
23
+ * Exported so a host that prefetches template files can walk exactly as deep
24
+ * as the core will expand, instead of copying the number and drifting (the
25
+ * VS Code extension's prefetch used 16 while the core went to 32). Not a
26
+ * Unity limit — a bound on this renderer's work.
27
+ */
28
+ export declare const TEMPLATE_DEPTH_LIMIT = 32;
29
+ /**
30
+ * Expand all instances in a document into a derived tree.
31
+ *
32
+ * The entry UXML is represented as a tiny loaded record so the same recursive
33
+ * path handles top-level and nested declarations. Its URL is null by contract.
34
+ */
35
+ export declare function expandTemplates(document: UxmlDocument): TemplateExpansion;
36
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uxml-preview",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Preview Unity UI Toolkit UXML/USS in a browser without the Unity Editor. Uses Yoga (WebAssembly), the same layout engine UI Toolkit uses: 660 of 676 element coordinates identical to Unity 6. Parses and writes files back byte-for-byte.",
5
5
  "keywords": [
6
6
  "unity",
@@ -46,7 +46,7 @@
46
46
  ],
47
47
  "sideEffects": false,
48
48
  "engines": {
49
- "node": ">=18"
49
+ "node": ">=22"
50
50
  },
51
51
  "publishConfig": {
52
52
  "access": "public"
@@ -69,9 +69,11 @@
69
69
  "test:roundtrip": "vitest run tests/roundtrip",
70
70
  "test:golden": "vitest run tests/golden --passWithNoTests",
71
71
  "golden:emit": "node --experimental-strip-types --no-warnings scripts/emit-golden-cases.mjs",
72
+ "template:emit": "node --experimental-strip-types --no-warnings scripts/emit-template-cases.mjs",
72
73
  "golden:update": "node scripts/update-golden.mjs",
74
+ "check:self-reference": "node scripts/check-self-reference.mjs",
73
75
  "typecheck": "tsc --noEmit",
74
- "prepublishOnly": "npm run typecheck && npm run test && npm run build"
76
+ "prepublishOnly": "npm run check:self-reference && npm run typecheck && npm run test && npm run build"
75
77
  },
76
78
  "dependencies": {
77
79
  "yoga-layout": "^3.2.1"