uxml-preview 0.5.1 → 0.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.
@@ -1,5 +1,6 @@
1
1
  import { ElementNode, NodeId, Warning } from '../model/types';
2
2
  import { ComputedStyle, DeclarationLocator } from '../style/resolve';
3
+ import { FaceInfo } from '../render/measure';
3
4
  /**
4
5
  * Purpose: load the Yoga WebAssembly module.
5
6
  * Deps/Effects: caches the module; awaiting it more than once is harmless.
@@ -26,9 +27,19 @@ export interface TextContext {
26
27
  /** `normal` | `bold` | `italic` | `bold-and-italic`. */
27
28
  fontStyle: string;
28
29
  whiteSpace: string;
30
+ fontFamily?: string;
31
+ faceInfo?: FaceInfo;
32
+ letterSpacing?: number;
33
+ wordSpacing?: number;
34
+ paragraphSpacing?: number;
35
+ textGenerator?: 'standard' | 'advanced';
29
36
  }
30
37
  export type MeasureText = (text: string, context: TextContext, availableWidth: number) => TextMetrics;
31
38
  export interface LayoutOptions {
39
+ fonts?: ReadonlyMap<ComputedStyle, {
40
+ family: string;
41
+ faceInfo?: FaceInfo;
42
+ }>;
32
43
  size: {
33
44
  width: number;
34
45
  height: number;
@@ -1,3 +1,4 @@
1
+ import { UssVersion } from '../style/registry';
1
2
  /**
2
3
  * uxml-preview — in-memory document model.
3
4
  *
@@ -32,6 +33,8 @@ export type SourceRef = {
32
33
  in: 'uss';
33
34
  sheet: number;
34
35
  span: Span;
36
+ url?: string | null;
37
+ from?: string | null;
35
38
  };
36
39
  /**
37
40
  * Opaque handle to a node, stable for the lifetime of the document.
@@ -241,6 +244,7 @@ export type WarningKindMap<T> = {
241
244
  *
242
245
  * - `not-uss` the author wrote something USS does not have
243
246
  * - `not-rendered` valid USS/UXML this preview does not draw or apply
247
+ * - `font-fallback` drawn, but Unity draws some glyphs with a fallback font, so text size can differ
244
248
  * - `invalid-value` a valid name with a value or target that cannot work
245
249
  * - `version-dependent` the result depends on the Unity version
246
250
  * - `matches-unity` ignored, and Unity ignores it too; not a defect
@@ -250,7 +254,7 @@ export type WarningKindMap<T> = {
250
254
  * Absent when none applies, e.g. malformed text or a file the host could not
251
255
  * resolve.
252
256
  */
253
- export type WarningReason = 'not-uss' | 'not-rendered' | 'invalid-value' | 'version-dependent' | 'matches-unity';
257
+ export type WarningReason = 'not-uss' | 'not-rendered' | 'font-fallback' | 'invalid-value' | 'version-dependent' | 'matches-unity';
254
258
  /**
255
259
  * What a warning is about, as data, so a consumer never has to parse it back
256
260
  * out of `message`.
@@ -268,7 +272,7 @@ export type WarningSubject = {
268
272
  /** USS properties to write instead, for a CSS name USS spells differently. */
269
273
  replacements?: readonly string[];
270
274
  /**
271
- * On a `not-rendered` warning: whether the property can change a box's size
275
+ * On a `not-rendered` or `font-fallback` warning: whether the property can change a box's size
272
276
  * or position in Unity. When true, this preview's coordinates may differ
273
277
  * from Unity's — do not treat a coordinate-based finding (zero size,
274
278
  * overflow) as settled.
@@ -276,10 +280,16 @@ export type WarningSubject = {
276
280
  affectsLayout?: boolean;
277
281
  /**
278
282
  * 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.
283
+ * applied some. Reserved for a property drawn but not laid out as Unity
284
+ * would; no registry row currently uses this field.
281
285
  */
282
286
  applied?: 'paint';
287
+ /** On a `font-fallback` warning: the script the editor default font (Inter) has no glyphs for. */
288
+ missingGlyphs?: 'hangul' | 'han' | 'kana';
289
+ unityEffect?: {
290
+ kind: 'unstyled-from-here' | 'file-inline-dropped';
291
+ versions: readonly UssVersion[];
292
+ };
283
293
  } | {
284
294
  kind: 'selector';
285
295
  /** The selector text as written. */
@@ -289,10 +299,13 @@ export type WarningSubject = {
289
299
  } | {
290
300
  kind: 'states-key';
291
301
  key: string;
302
+ problem?: 'matches-nothing' | 'state-in-key' | 'unknown-state';
303
+ state?: string;
292
304
  } | {
293
305
  kind: 'asset';
294
306
  path: string;
295
307
  form: 'url' | 'resource';
308
+ role?: 'font';
296
309
  } | {
297
310
  kind: 'import';
298
311
  /** Exactly the string the resolver hook was (or would have been) called with. */
@@ -330,6 +343,7 @@ export interface Warning {
330
343
  at?: SourceRef;
331
344
  /** Element the warning is about, when it has one. Lets the UI highlight it. */
332
345
  node?: NodeId;
346
+ nodes?: NodeId[];
333
347
  reason?: WarningReason;
334
348
  subject?: WarningSubject;
335
349
  }
@@ -18,6 +18,21 @@ export interface CssMapResult {
18
18
  declarations: Record<string, string>;
19
19
  warnings: Warning[];
20
20
  }
21
+ /**
22
+ * `url("project://...")` or `resource("...")` to its form and bare path.
23
+ *
24
+ * Deps/Effects: decodes XML entities before parsing the wrapper. A value from
25
+ * a `<Style src>`-loaded .uss file never has any (that file was never XML),
26
+ * but a value from an inline `style="…"` attribute is parsed straight from
27
+ * the raw XML source (`inlineDeclarations` in style/resolve.ts) and can still
28
+ * carry `&apos;`/`&amp;`/`&quot;`. Decoding after the regex would be too late:
29
+ * `&apos;` doesn't match `(["']?)`, so the quote wrapper would be read as part
30
+ * of the path instead of stripped from it.
31
+ */
32
+ export declare function assetPath(value: string): {
33
+ form: 'url' | 'resource';
34
+ path: string;
35
+ } | null;
21
36
  /**
22
37
  * Purpose: one element's computed style -> CSS declarations.
23
38
  * Deps/Effects: calls `resolveAsset` for background images; warnings are
@@ -0,0 +1,21 @@
1
+ import { NodeId, Warning } from '../model/types';
2
+ import { ComputedStyle, DeclarationLocator } from '../style/resolve';
3
+ import { FaceInfo } from './measure';
4
+ export type ResolvedFont = {
5
+ family: string;
6
+ faceInfo?: FaceInfo;
7
+ };
8
+ export type ResolveFont = (path: string, form: 'url' | 'resource') => string | ResolvedFont | null;
9
+ /**
10
+ * Purpose: resolve font assets for measurement and painting, applying Unity's
11
+ * documented but unmeasured -unity-font-definition precedence.
12
+ * Deps/Effects: calls the host hook once per distinct (path, form) per invocation.
13
+ * Ensures: filters only not-rendered property warnings for processed font
14
+ * declarations, keyed by property, source document and offset
15
+ * (or node when no offset exists). Unresolved assets replace those
16
+ * warnings with one asset-unresolved warning per declaration.
17
+ */
18
+ export declare function resolveFonts(styles: ReadonlyMap<NodeId, ComputedStyle>, warnings: readonly Warning[], locate: DeclarationLocator, resolve?: ResolveFont): {
19
+ fonts: Map<ComputedStyle, ResolvedFont>;
20
+ warnings: Warning[];
21
+ };
@@ -1,4 +1,13 @@
1
1
  import { MeasureText } from '../layout/yoga';
2
+ export interface FaceInfo {
3
+ pointSize: number;
4
+ ascentLine: number;
5
+ descentLine: number;
6
+ lineHeight?: number;
7
+ }
8
+ export declare const EDITOR_DEFAULT_FACE_INFO: FaceInfo;
9
+ export declare function textHeight(lines: number, fontSize: number, face: FaceInfo, paragraphBreaks?: number, paragraphSpacing?: number): number;
10
+ export declare function fontList(family?: string): string;
2
11
  /**
3
12
  * Purpose: measure text with a canvas when one is available.
4
13
  * Deps/Effects: creates one offscreen canvas and reuses it.
@@ -1,3 +1,4 @@
1
+ import { FaceInfo } from './measure';
1
2
  import { ElementNode, NodeId, Warning } from '../model/types';
2
3
  import { ComputedStyle } from '../style/resolve';
3
4
  import { LayoutBox, LayoutPart } from '../layout/yoga';
@@ -22,6 +23,10 @@ export interface PaintResult {
22
23
  }
23
24
  export interface PaintOptions extends CssMapOptions {
24
25
  document: Document;
26
+ fonts?: ReadonlyMap<ComputedStyle, {
27
+ family: string;
28
+ faceInfo?: FaceInfo;
29
+ }>;
25
30
  }
26
31
  /**
27
32
  * Purpose: build the DOM for one laid-out document under `container`.
@@ -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
+ /** Structured marker for a recognised but unsupported USS unit. */
40
+ code?: 'unsupported-unit';
39
41
  /** Set with `problem`: USS lacks the syntax, or the value is simply unreadable. */
40
42
  reason?: 'not-uss' | 'invalid-value';
41
43
  }
@@ -0,0 +1,20 @@
1
+ import { RenderResult } from '../index';
2
+ import { NodeId, StyleOrigin } from '../model/types';
3
+ export type Visibility = {
4
+ kind: 'shown';
5
+ } | {
6
+ kind: 'hidden';
7
+ by: NodeId;
8
+ origin: StyleOrigin;
9
+ } | {
10
+ kind: 'emptied';
11
+ } | {
12
+ kind: 'collapsed';
13
+ explicitZero: boolean;
14
+ };
15
+ /**
16
+ * Purpose: classify layout absence using the uxml-author 0.2.1 rules.
17
+ * Deps/Effects: pure; reads the expanded tree and computed styles from render.
18
+ * Ensures: every box has a verdict; opacity and visibility do not affect it.
19
+ */
20
+ export declare function classifyVisibility(result: RenderResult): ReadonlyMap<NodeId, Visibility>;
@@ -75,7 +75,7 @@ export declare function ussProperty(name: string): UssProperty | undefined;
75
75
  export declare const CSS_REPLACEMENTS: Readonly<Record<string, readonly string[]>>;
76
76
  export interface PropertyWarning {
77
77
  message: string;
78
- reason: 'not-uss' | 'not-rendered';
78
+ reason: 'not-uss' | 'not-rendered' | 'invalid-value';
79
79
  subject: {
80
80
  kind: 'property';
81
81
  property: string;
@@ -84,6 +84,10 @@ export interface PropertyWarning {
84
84
  replacements?: readonly string[];
85
85
  affectsLayout?: boolean;
86
86
  applied?: 'paint';
87
+ unityEffect?: {
88
+ kind: 'unstyled-from-here';
89
+ versions: readonly UssVersion[];
90
+ };
87
91
  };
88
92
  }
89
93
  /**
@@ -30,6 +30,8 @@ export interface ResolveOptions {
30
30
  states?: Readonly<Record<string, readonly string[]>>;
31
31
  }
32
32
  export interface ResolveResult {
33
+ /** @internal Element matches collected during this cascade, keyed by sheet and item. */
34
+ matchedNodes: ReadonlyMap<string, readonly NodeId[]>;
33
35
  styles: ReadonlyMap<NodeId, ComputedStyle>;
34
36
  /**
35
37
  * Style per control part, keyed by the owning element and then Unity's name
@@ -72,8 +74,13 @@ export interface Candidate {
72
74
  order: number;
73
75
  winner: boolean;
74
76
  }
77
+ /** Derives the resolver request without adding importer state to the model. */
78
+ export declare function sheetLocation(doc: UxmlDocument, index: number): {
79
+ url: string | null;
80
+ from: string | null;
81
+ };
75
82
  /** Where a computed value was written, as warning fields. */
76
- export type DeclarationLocator = (origin: StyleOrigin) => Pick<Warning, 'at' | 'sourceDocument'>;
83
+ export type DeclarationLocator = (origin: StyleOrigin) => Pick<Warning, 'at' | 'sourceDocument' | 'nodes'>;
77
84
  /**
78
85
  * Purpose: turn a computed value's origin back into the text it came from,
79
86
  * so a value-level warning points at one declaration.
@@ -81,7 +88,7 @@ export type DeclarationLocator = (origin: StyleOrigin) => Pick<Warning, 'at' | '
81
88
  * reported where it was declared, not on every descendant — and
82
89
  * for a template's inline style, which names its file instead.
83
90
  */
84
- export declare function declarationLocator(doc: UxmlDocument): DeclarationLocator;
91
+ export declare function declarationLocator(doc: UxmlDocument, nodes: ReadonlyMap<string, readonly NodeId[]>): DeclarationLocator;
85
92
  /**
86
93
  * Purpose: compute the style of every element in the document.
87
94
  * Deps/Effects: reads `doc` only; nothing on the model is mutated.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "uxml-preview",
3
- "version": "0.5.1",
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.",
3
+ "version": "0.6.0",
4
+ "description": "Render Unity UI Toolkit UXML/USS in a browser without the Unity Editor. Laid out by Yoga, the engine UI Toolkit uses; reference Unity 6.3 LTS, measured against real Unity. Byte-exact round trips.",
5
5
  "keywords": [
6
6
  "unity",
7
7
  "unity3d",
@@ -67,6 +67,7 @@
67
67
  "test": "vitest run",
68
68
  "test:watch": "vitest",
69
69
  "test:roundtrip": "vitest run tests/roundtrip",
70
+ "test:text": "node tests/text-cohort/run.mjs && node tests/text-cohort/paint.mjs",
70
71
  "test:golden": "vitest run tests/golden --passWithNoTests",
71
72
  "golden:emit": "node --experimental-strip-types --no-warnings scripts/emit-golden-cases.mjs",
72
73
  "template:emit": "node --experimental-strip-types --no-warnings scripts/emit-template-cases.mjs",
@@ -82,6 +83,7 @@
82
83
  "@types/node": "^22.0.0",
83
84
  "jsdom": "^30.0.1",
84
85
  "pixelmatch": "^6.0.0",
86
+ "playwright-core": "1.48.2",
85
87
  "pngjs": "^7.0.0",
86
88
  "typescript": "^5.6.0",
87
89
  "vite": "^6.0.0",