uxml-preview 0.1.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 +50 -0
- package/LICENSE +201 -0
- package/README.md +400 -0
- package/THIRD-PARTY-NOTICES.md +55 -0
- package/dist/controls/registry.d.ts +10 -0
- package/dist/index.d.ts +110 -0
- package/dist/index.js +1292 -0
- package/dist/layout/yoga.d.ts +63 -0
- package/dist/model/types.d.ts +288 -0
- package/dist/parser/uss.d.ts +33 -0
- package/dist/parser/uxml.d.ts +12 -0
- package/dist/render/css-map.d.ts +16 -0
- package/dist/render/measure.d.ts +9 -0
- package/dist/render/paint.d.ts +19 -0
- package/dist/render/values.d.ts +50 -0
- package/dist/serializer/uss.d.ts +6 -0
- package/dist/serializer/uxml.d.ts +8 -0
- package/dist/style/properties.d.ts +20 -0
- package/dist/style/resolve.d.ts +42 -0
- package/dist/style/selector.d.ts +21 -0
- package/dist/style/specificity.d.ts +6 -0
- package/package.json +59 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ElementNode, NodeId, Warning } from '../model/types';
|
|
2
|
+
import { ComputedStyle } from '../style/resolve';
|
|
3
|
+
/**
|
|
4
|
+
* Purpose: load the Yoga WebAssembly module.
|
|
5
|
+
* Deps/Effects: caches the module; awaiting it more than once is harmless.
|
|
6
|
+
*
|
|
7
|
+
* Imported from `yoga-layout/load` rather than `yoga-layout`, whose default
|
|
8
|
+
* entry uses top-level await. Going through `load` keeps this library's module
|
|
9
|
+
* graph synchronous, which is what lets `render` stay a synchronous call.
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadLayoutEngine(): Promise<void>;
|
|
12
|
+
export declare function isLayoutEngineReady(): boolean;
|
|
13
|
+
export declare function liveNodeCount(): number;
|
|
14
|
+
export interface LayoutBox {
|
|
15
|
+
left: number;
|
|
16
|
+
top: number;
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
}
|
|
20
|
+
export interface TextMetrics {
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
}
|
|
24
|
+
export interface TextContext {
|
|
25
|
+
fontSize: number;
|
|
26
|
+
/** `normal` | `bold` | `italic` | `bold-and-italic`. */
|
|
27
|
+
fontStyle: string;
|
|
28
|
+
whiteSpace: string;
|
|
29
|
+
}
|
|
30
|
+
export type MeasureText = (text: string, context: TextContext, availableWidth: number) => TextMetrics;
|
|
31
|
+
export interface LayoutOptions {
|
|
32
|
+
size: {
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Measures a control's text.
|
|
38
|
+
*
|
|
39
|
+
* Injectable because the default depends on a canvas, so results move with
|
|
40
|
+
* the platform and the installed fonts — and Phase 5 cannot put a number on
|
|
41
|
+
* accuracy if the measurement shifts underneath it.
|
|
42
|
+
*/
|
|
43
|
+
measureText: MeasureText;
|
|
44
|
+
}
|
|
45
|
+
export interface LayoutTree {
|
|
46
|
+
/** Computed box per element, in panel coordinates. */
|
|
47
|
+
boxes: ReadonlyMap<NodeId, LayoutBox>;
|
|
48
|
+
/** Elements that were laid out, parents before children. */
|
|
49
|
+
painted: readonly ElementNode[];
|
|
50
|
+
warnings: readonly Warning[];
|
|
51
|
+
/**
|
|
52
|
+
* Deps/Effects: frees every Yoga node this tree owns.
|
|
53
|
+
* Requires: `boxes` must not be read afterwards. Calling twice is harmless.
|
|
54
|
+
*/
|
|
55
|
+
dispose(): void;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Purpose: build a Yoga tree, compute layout, hand back boxes in panel
|
|
59
|
+
* coordinates.
|
|
60
|
+
* Deps/Effects: creates Yoga nodes; the returned tree owns them until disposed.
|
|
61
|
+
* Requires: `loadLayoutEngine()` must have resolved.
|
|
62
|
+
*/
|
|
63
|
+
export declare function layoutDocument(root: ElementNode, styles: ReadonlyMap<NodeId, ComputedStyle>, options: LayoutOptions): LayoutTree;
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* uxml-preview — in-memory document model.
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 deliverable: types only. The parser that fills these lands in Phase 2.
|
|
5
|
+
* See docs/ROADMAP.md and the decision table in docs/progress.md.
|
|
6
|
+
*
|
|
7
|
+
* One rule governs this file:
|
|
8
|
+
*
|
|
9
|
+
* The model holds what the file said, and nothing that can be computed from it.
|
|
10
|
+
*
|
|
11
|
+
* Computed styles, "can we render this control", line numbers, expanded
|
|
12
|
+
* shorthands — all derived, all kept out. Stored derived data goes stale when
|
|
13
|
+
* the document is edited, and stale data nobody notices is worse than no data.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* A half-open range into a source string: `[start, end)`.
|
|
17
|
+
*
|
|
18
|
+
* Which string it indexes is implied by where the span is reached from — spans
|
|
19
|
+
* under `UxmlDocument.root` index `UxmlDocument.source`, spans under a
|
|
20
|
+
* `StyleSheet` index that sheet's own `source`. `SourceRef` makes it explicit
|
|
21
|
+
* where that context is not available.
|
|
22
|
+
*/
|
|
23
|
+
export interface Span {
|
|
24
|
+
start: number;
|
|
25
|
+
end: number;
|
|
26
|
+
}
|
|
27
|
+
/** An explicit text location, for warnings and editor navigation. */
|
|
28
|
+
export type SourceRef = {
|
|
29
|
+
in: 'uxml';
|
|
30
|
+
span: Span;
|
|
31
|
+
} | {
|
|
32
|
+
in: 'uss';
|
|
33
|
+
sheet: number;
|
|
34
|
+
span: Span;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Opaque handle to a node, stable for the lifetime of the document.
|
|
38
|
+
*
|
|
39
|
+
* Assigned by the parser and never reused. Style provenance and warnings both
|
|
40
|
+
* point at nodes, and a positional path would shift those pointers whenever an
|
|
41
|
+
* unrelated sibling is inserted.
|
|
42
|
+
*/
|
|
43
|
+
export type NodeId = number & {
|
|
44
|
+
readonly __brand: 'NodeId';
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Element name exactly as written.
|
|
48
|
+
*
|
|
49
|
+
* `prefix` is kept because serialization must restore `<ui:Label>` verbatim.
|
|
50
|
+
* `local` is kept separate because USS type selectors match the C# class name
|
|
51
|
+
* (`Label`), not the qualified tag.
|
|
52
|
+
*/
|
|
53
|
+
export interface ElementName {
|
|
54
|
+
/** Namespace prefix as written, or null for an unprefixed tag. */
|
|
55
|
+
prefix: string | null;
|
|
56
|
+
local: string;
|
|
57
|
+
}
|
|
58
|
+
export interface Attribute {
|
|
59
|
+
name: string;
|
|
60
|
+
/**
|
|
61
|
+
* Value exactly as written, XML entities NOT decoded.
|
|
62
|
+
*
|
|
63
|
+
* Decoding is lossy in the direction we care about: `&` and `&` both
|
|
64
|
+
* decode to `&`, so a decoded value cannot be written back as the author
|
|
65
|
+
* typed it. Consumers decode when they need the text.
|
|
66
|
+
*/
|
|
67
|
+
value: string;
|
|
68
|
+
span: Span;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Three spans rather than one, so that an edit stays local.
|
|
72
|
+
*
|
|
73
|
+
* With a single outer span, editing any descendant would force the whole
|
|
74
|
+
* subtree to be regenerated — losing the author's indentation and attribute
|
|
75
|
+
* formatting all the way up to the root. Splitting the tag lets the serializer
|
|
76
|
+
* reuse whichever parts are untouched:
|
|
77
|
+
*
|
|
78
|
+
* <ui:VisualElement class="row"> <- openTag
|
|
79
|
+
* <ui:Button text="Use" /> <- inner: children AND the gaps between them
|
|
80
|
+
* </ui:VisualElement> <- closeTag
|
|
81
|
+
*
|
|
82
|
+
* For a self-closing element, `inner` is empty and `closeTag` is null.
|
|
83
|
+
*/
|
|
84
|
+
export interface ElementSpans {
|
|
85
|
+
openTag: Span;
|
|
86
|
+
inner: Span;
|
|
87
|
+
closeTag: Span | null;
|
|
88
|
+
}
|
|
89
|
+
export interface ElementNode {
|
|
90
|
+
id: NodeId;
|
|
91
|
+
name: ElementName;
|
|
92
|
+
/** Source order. Round-trip must not reorder attributes. */
|
|
93
|
+
attributes: Attribute[];
|
|
94
|
+
children: ElementNode[];
|
|
95
|
+
spans: ElementSpans;
|
|
96
|
+
/**
|
|
97
|
+
* The open tag no longer matches `spans.openTag` (name or attributes changed),
|
|
98
|
+
* so it must be regenerated.
|
|
99
|
+
*
|
|
100
|
+
* Deliberately does not propagate upward: a dirty child leaves its parent's
|
|
101
|
+
* tag reusable, which is the entire reason `ElementSpans` is split.
|
|
102
|
+
*/
|
|
103
|
+
tagDirty: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* The child list changed — insert, remove, or reorder.
|
|
106
|
+
*
|
|
107
|
+
* Separate from `tagDirty` because it invalidates something different: the
|
|
108
|
+
* whitespace between children. While this is false the serializer can slice
|
|
109
|
+
* those gaps out of `spans.inner` even when individual children are dirty,
|
|
110
|
+
* so the author's indentation survives. Once true, separators must be
|
|
111
|
+
* synthesized (copy a sibling's leading whitespace).
|
|
112
|
+
*/
|
|
113
|
+
childrenDirty: boolean;
|
|
114
|
+
}
|
|
115
|
+
export type Combinator = 'descendant' | 'child';
|
|
116
|
+
/**
|
|
117
|
+
* One simple selector.
|
|
118
|
+
*
|
|
119
|
+
* `unknown` is the escape hatch for syntax USS does not support (`:nth-child`,
|
|
120
|
+
* `[attr]`, `::before`) and for anything the parser does not recognize. The
|
|
121
|
+
* parser stores it and moves on; the resolver drops the whole rule and warns.
|
|
122
|
+
* Keeping the text means the rule still round-trips intact.
|
|
123
|
+
*/
|
|
124
|
+
export type SimpleSelector = {
|
|
125
|
+
kind: 'universal';
|
|
126
|
+
}
|
|
127
|
+
/** C# class name — `Label`, `Button`, `VisualElement`. Case-sensitive. */
|
|
128
|
+
| {
|
|
129
|
+
kind: 'type';
|
|
130
|
+
name: string;
|
|
131
|
+
} | {
|
|
132
|
+
kind: 'class';
|
|
133
|
+
name: string;
|
|
134
|
+
}
|
|
135
|
+
/** `#foo` — matches the UXML `name` attribute, not an HTML id. */
|
|
136
|
+
| {
|
|
137
|
+
kind: 'name';
|
|
138
|
+
name: string;
|
|
139
|
+
}
|
|
140
|
+
/** `hover`, `active`, `focus`, `disabled`, `checked`, `selected`, `root`, `inactive`. */
|
|
141
|
+
| {
|
|
142
|
+
kind: 'pseudo';
|
|
143
|
+
name: string;
|
|
144
|
+
} | {
|
|
145
|
+
kind: 'unknown';
|
|
146
|
+
text: string;
|
|
147
|
+
};
|
|
148
|
+
/** A compound selector plus how it attaches to the part before it. */
|
|
149
|
+
export interface SelectorPart {
|
|
150
|
+
/** Ignored on the first part of a selector. */
|
|
151
|
+
combinator: Combinator;
|
|
152
|
+
/** All must match the same element (`.a.b`). */
|
|
153
|
+
simple: SimpleSelector[];
|
|
154
|
+
}
|
|
155
|
+
/** One selector out of a comma-separated group. */
|
|
156
|
+
export interface Selector {
|
|
157
|
+
parts: SelectorPart[];
|
|
158
|
+
span: Span;
|
|
159
|
+
}
|
|
160
|
+
export interface Declaration {
|
|
161
|
+
/** Custom properties keep their `--` prefix; they are not special-cased. */
|
|
162
|
+
property: string;
|
|
163
|
+
/**
|
|
164
|
+
* Value exactly as written — `6px 14px`, not four expanded longhands.
|
|
165
|
+
*
|
|
166
|
+
* Expanding here would force serialization to guess a form the author never
|
|
167
|
+
* typed (`6px 14px 6px 14px`), which breaks round-trip. The resolver expands;
|
|
168
|
+
* the model does not. Unrecognized value syntax survives for the same reason:
|
|
169
|
+
* it is just text until someone asks what it means.
|
|
170
|
+
*/
|
|
171
|
+
value: string;
|
|
172
|
+
span: Span;
|
|
173
|
+
dirty: boolean;
|
|
174
|
+
}
|
|
175
|
+
export interface Rule {
|
|
176
|
+
/** Comma-separated group. */
|
|
177
|
+
selectors: Selector[];
|
|
178
|
+
declarations: Declaration[];
|
|
179
|
+
/** The whole rule, selector through closing brace. */
|
|
180
|
+
span: Span;
|
|
181
|
+
selectorSpan: Span;
|
|
182
|
+
selectorDirty: boolean;
|
|
183
|
+
/** See `ElementNode.childrenDirty` — same reason, applied to declarations. */
|
|
184
|
+
declarationsDirty: boolean;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* A top-level item, in source order.
|
|
188
|
+
*
|
|
189
|
+
* `unknown` covers `@media`, malformed input, and anything else the parser does
|
|
190
|
+
* not model. It is preserved by span and never interpreted.
|
|
191
|
+
*/
|
|
192
|
+
export type SheetItem = {
|
|
193
|
+
kind: 'rule';
|
|
194
|
+
rule: Rule;
|
|
195
|
+
} | {
|
|
196
|
+
kind: 'import';
|
|
197
|
+
url: string;
|
|
198
|
+
span: Span;
|
|
199
|
+
} | {
|
|
200
|
+
kind: 'unknown';
|
|
201
|
+
span: Span;
|
|
202
|
+
};
|
|
203
|
+
export interface StyleSheet {
|
|
204
|
+
/** The text this sheet's spans index into. */
|
|
205
|
+
source: string;
|
|
206
|
+
/**
|
|
207
|
+
* Where the text came from — a path for `@import`ed sheets, null for the one
|
|
208
|
+
* passed directly to `parse()`. Used in warning messages only.
|
|
209
|
+
*/
|
|
210
|
+
origin: string | null;
|
|
211
|
+
items: SheetItem[];
|
|
212
|
+
}
|
|
213
|
+
export type WarningKind = 'unsupported-control' | 'unsupported-property' | 'unsupported-selector' | 'unsupported-unit' | 'version-dependent' | 'asset-unresolved' | 'import-unresolved' | 'malformed';
|
|
214
|
+
/**
|
|
215
|
+
* Never a thrown error. One unsupported property must not take down the render
|
|
216
|
+
* (see CLAUDE.md rule 6).
|
|
217
|
+
*
|
|
218
|
+
* There is no line number: it is derivable from `at.span` by counting newlines,
|
|
219
|
+
* and a stored one silently points at the wrong place after an edit.
|
|
220
|
+
*/
|
|
221
|
+
export interface Warning {
|
|
222
|
+
kind: WarningKind;
|
|
223
|
+
message: string;
|
|
224
|
+
/** Text location, when the warning has one. */
|
|
225
|
+
at?: SourceRef;
|
|
226
|
+
/** Element the warning is about, when it has one. Lets the UI highlight it. */
|
|
227
|
+
node?: NodeId;
|
|
228
|
+
}
|
|
229
|
+
export interface UxmlDocument {
|
|
230
|
+
/** The exact UXML text every tree span indexes into. */
|
|
231
|
+
source: string;
|
|
232
|
+
/**
|
|
233
|
+
* The `<ui:UXML>` element itself. It is a container, not a rendered control,
|
|
234
|
+
* but it is kept in the tree because it carries the namespace declarations
|
|
235
|
+
* that serialization must restore.
|
|
236
|
+
*
|
|
237
|
+
* Anything outside it — the XML declaration, a leading comment, a trailing
|
|
238
|
+
* newline — has no field of its own. It is `source` either side of the root's
|
|
239
|
+
* span, so serialization slices it back. Storing it would be storing a copy.
|
|
240
|
+
*/
|
|
241
|
+
root: ElementNode;
|
|
242
|
+
/**
|
|
243
|
+
* At most one in v0.1. `@import` (Phase 3) appends sheets, each carrying its
|
|
244
|
+
* own `source`, which is why the source text lives on the sheet.
|
|
245
|
+
*/
|
|
246
|
+
sheets: StyleSheet[];
|
|
247
|
+
/** Parse-time only — malformed input. Support judgments happen downstream. */
|
|
248
|
+
warnings: Warning[];
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Where a computed value came from.
|
|
252
|
+
*
|
|
253
|
+
* Phase 8 needs this to answer "write to the inline style, or edit the rule?".
|
|
254
|
+
* Every variant points into the model, so the editor can jump straight to the
|
|
255
|
+
* declaration's span and rewrite exactly that text.
|
|
256
|
+
*
|
|
257
|
+
* Note the inline variant indexes declarations parsed on demand from the
|
|
258
|
+
* element's `style` attribute — those declarations are not stored on the node.
|
|
259
|
+
* The attribute string is the single source; a parsed copy alongside it could
|
|
260
|
+
* disagree with it.
|
|
261
|
+
*/
|
|
262
|
+
export type StyleOrigin = {
|
|
263
|
+
kind: 'inline';
|
|
264
|
+
node: NodeId;
|
|
265
|
+
declIndex: number;
|
|
266
|
+
} | {
|
|
267
|
+
kind: 'rule';
|
|
268
|
+
sheet: number;
|
|
269
|
+
item: number;
|
|
270
|
+
declIndex: number;
|
|
271
|
+
} | {
|
|
272
|
+
kind: 'inherited';
|
|
273
|
+
from: NodeId;
|
|
274
|
+
origin: StyleOrigin;
|
|
275
|
+
}
|
|
276
|
+
/** USS default, which is not always the CSS default — `flex-direction` is `column`. */
|
|
277
|
+
| {
|
|
278
|
+
kind: 'default';
|
|
279
|
+
};
|
|
280
|
+
/**
|
|
281
|
+
* Deliberately absent from this file:
|
|
282
|
+
*
|
|
283
|
+
* - computed style values — derived, recomputed on demand (Phase 3)
|
|
284
|
+
* - "is this control supported" — a renderer concern that changes in Phase 7,
|
|
285
|
+
* while the file it was parsed from does not
|
|
286
|
+
* - line/column numbers — derived from spans
|
|
287
|
+
* - expanded shorthands — derived from declaration values
|
|
288
|
+
*/
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Declaration, Span, StyleSheet, Warning } from '../model/types';
|
|
2
|
+
export interface UssParseResult {
|
|
3
|
+
sheet: StyleSheet;
|
|
4
|
+
warnings: Warning[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Purpose: read `property: value` pairs out of [start, end).
|
|
8
|
+
* Deps/Effects: calls `onProblem` for text it cannot make a declaration of;
|
|
9
|
+
* it never throws and never stops early.
|
|
10
|
+
*
|
|
11
|
+
* Shared by rule blocks and by the inline `style` attribute, which is the same
|
|
12
|
+
* grammar without the braces. Spans are absolute, so declarations parsed out of
|
|
13
|
+
* an inline style index the UXML source rather than a stylesheet.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseDeclarationList(source: string, start: number, end: number, onProblem?: (message: string, from: number, to: number) => void): Declaration[];
|
|
16
|
+
/**
|
|
17
|
+
* Byte offset of an attribute value inside the UXML source.
|
|
18
|
+
*
|
|
19
|
+
* `Attribute.span` runs from the name to just past the closing quote, and the
|
|
20
|
+
* value is stored raw, so the value's start is fixed by arithmetic rather than
|
|
21
|
+
* by another field on the model. Returns null for the malformed shapes the
|
|
22
|
+
* scanner records without a quoted value.
|
|
23
|
+
*/
|
|
24
|
+
export declare function attributeValueStart(attr: {
|
|
25
|
+
value: string;
|
|
26
|
+
span: Span;
|
|
27
|
+
}): number | null;
|
|
28
|
+
/**
|
|
29
|
+
* Purpose: USS text -> stylesheet with the spans serialization needs.
|
|
30
|
+
* Ensures: never throws; unterminated blocks and unknown at-rules become
|
|
31
|
+
* warnings plus an `unknown` item covering the remaining text.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseUss(source: string, origin?: string | null, sheetIndex?: number): UssParseResult;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ElementName, ElementNode, Warning } from '../model/types';
|
|
2
|
+
export interface UxmlParseResult {
|
|
3
|
+
root: ElementNode;
|
|
4
|
+
warnings: Warning[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Purpose: UXML text -> element tree with the spans serialization needs.
|
|
8
|
+
* Ensures: never throws; malformed input yields warnings and whatever tree
|
|
9
|
+
* was recovered. Spans always stay within `source`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseUxml(source: string): UxmlParseResult;
|
|
12
|
+
export declare function qualify(name: ElementName): string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NodeId, Warning } from '../model/types';
|
|
2
|
+
import { ComputedStyle } from '../style/resolve';
|
|
3
|
+
export interface CssMapOptions {
|
|
4
|
+
resolveAsset?: ((path: string) => string | null) | undefined;
|
|
5
|
+
}
|
|
6
|
+
export interface CssMapResult {
|
|
7
|
+
declarations: Record<string, string>;
|
|
8
|
+
warnings: Warning[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Purpose: one element's computed style -> CSS declarations.
|
|
12
|
+
* Deps/Effects: calls `resolveAsset` for background images; warnings are
|
|
13
|
+
* returned rather than thrown so one bad property cannot take
|
|
14
|
+
* down the render.
|
|
15
|
+
*/
|
|
16
|
+
export declare function toCss(style: ComputedStyle, node: NodeId, options?: CssMapOptions): CssMapResult;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { MeasureText } from '../layout/yoga';
|
|
2
|
+
/**
|
|
3
|
+
* Purpose: measure text with a canvas when one is available.
|
|
4
|
+
* Deps/Effects: creates one offscreen canvas and reuses it.
|
|
5
|
+
* Ensures: falls back to a fixed per-character estimate when no canvas
|
|
6
|
+
* exists, so headless callers still lay out rather than
|
|
7
|
+
* collapsing every label to zero.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createDefaultMeasureText(doc?: Document): MeasureText;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ElementNode, NodeId, Warning } from '../model/types';
|
|
2
|
+
import { ComputedStyle } from '../style/resolve';
|
|
3
|
+
import { LayoutBox } from '../layout/yoga';
|
|
4
|
+
import { CssMapOptions } from './css-map';
|
|
5
|
+
export declare const NODE_ATTRIBUTE = "data-uxml-node";
|
|
6
|
+
export interface PaintResult {
|
|
7
|
+
warnings: Warning[];
|
|
8
|
+
/** Painted element per model node, for hit-testing and for tests. */
|
|
9
|
+
elements: Map<NodeId, HTMLElement>;
|
|
10
|
+
}
|
|
11
|
+
export interface PaintOptions extends CssMapOptions {
|
|
12
|
+
document: Document;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Purpose: build the DOM for one laid-out document under `container`.
|
|
16
|
+
* Deps/Effects: replaces the container's children. The caller owns disposal.
|
|
17
|
+
* Requires: `boxes` must come from a layout of this same tree.
|
|
18
|
+
*/
|
|
19
|
+
export declare function paint(root: ElementNode, boxes: ReadonlyMap<NodeId, LayoutBox>, styles: ReadonlyMap<NodeId, ComputedStyle>, container: HTMLElement, options: PaintOptions): PaintResult;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* USS values: initial values, and turning declaration text into numbers.
|
|
3
|
+
*
|
|
4
|
+
* This is the first place in the pipeline that interprets a value. The model
|
|
5
|
+
* and the resolver both keep declarations as the author wrote them, so `6px`
|
|
6
|
+
* stays `6px` until something needs a number — which is here.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* USS initial values for the properties layout depends on.
|
|
10
|
+
*
|
|
11
|
+
* The resolver deliberately does not fill defaults in, so this table is the
|
|
12
|
+
* only statement of them. Two entries are worth staring at:
|
|
13
|
+
*
|
|
14
|
+
* - `flex-direction` is `column`. CSS says `row`. Getting this wrong rotates
|
|
15
|
+
* every layout by ninety degrees, and it is the single most common way to
|
|
16
|
+
* mis-port a stylesheet.
|
|
17
|
+
* - `flex-shrink` is `1` here, while Yoga's own engine default is `0`. That
|
|
18
|
+
* disagreement is exactly why every supported property is written to the
|
|
19
|
+
* Yoga node explicitly rather than left at whatever the engine assumes.
|
|
20
|
+
* Unverified against a running editor; Phase 5 settles it.
|
|
21
|
+
*/
|
|
22
|
+
export declare const INITIAL: Readonly<Record<string, string>>;
|
|
23
|
+
export declare function initialValue(property: string): string | undefined;
|
|
24
|
+
export type Length = {
|
|
25
|
+
kind: 'px';
|
|
26
|
+
value: number;
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'percent';
|
|
29
|
+
value: number;
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'auto';
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'none';
|
|
34
|
+
};
|
|
35
|
+
export interface LengthResult {
|
|
36
|
+
length: Length | null;
|
|
37
|
+
/** Set when the text names a unit USS does not support, or uses calc(). */
|
|
38
|
+
problem?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Purpose: `"6px"` / `"50%"` / `"auto"` / `"0"` -> a number with a unit.
|
|
42
|
+
* Ensures: never throws. Returns `length: null` with a `problem` for text
|
|
43
|
+
* it cannot make sense of, so the caller can warn and fall back
|
|
44
|
+
* rather than render something wrong.
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseLength(text: string): LengthResult;
|
|
47
|
+
/** Bare numbers such as `flex-grow: 1`. Returns null when it is not one. */
|
|
48
|
+
export declare function parseNumber(text: string): number | null;
|
|
49
|
+
/** Seconds or milliseconds to milliseconds. */
|
|
50
|
+
export declare function parseDuration(text: string): number | null;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ElementNode } from '../model/types';
|
|
2
|
+
/**
|
|
3
|
+
* Purpose: element tree -> UXML text.
|
|
4
|
+
* Deps/Effects: reads `source`, which must be the exact text the spans were
|
|
5
|
+
* recorded against. Passing a different string yields garbage.
|
|
6
|
+
* Ensures: with no node marked dirty, the result equals `source`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function serializeUxml(source: string, root: ElementNode): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which properties inherit, and how shorthands split.
|
|
3
|
+
*
|
|
4
|
+
* Shorthand expansion belongs to the cascade rather than to rendering: once a
|
|
5
|
+
* sheet contains both `padding` and `padding-top`, deciding which one wins is a
|
|
6
|
+
* cascade question, and it cannot be answered without splitting the shorthand
|
|
7
|
+
* first.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isInherited(property: string): boolean;
|
|
10
|
+
/** Split a value on top-level whitespace, keeping `rgb(1, 2, 3)` in one piece. */
|
|
11
|
+
export declare function splitValue(value: string): string[];
|
|
12
|
+
/**
|
|
13
|
+
* Purpose: one declaration -> the longhand declarations it stands for.
|
|
14
|
+
* Ensures: returns `[[property, value]]` unchanged for anything it does
|
|
15
|
+
* not split, so callers never need to special-case shorthands.
|
|
16
|
+
*
|
|
17
|
+
* `transition` is left whole on purpose: it is a comma-separated list whose
|
|
18
|
+
* longhands are lists too, and nothing before Phase 4 reads it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function expandShorthand(property: string, value: string): Array<[string, string]>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ElementNode, NodeId, StyleOrigin, UxmlDocument, Warning } from '../model/types';
|
|
2
|
+
import { Specificity } from './specificity';
|
|
3
|
+
export interface ComputedValue {
|
|
4
|
+
/** `var()` already substituted. Otherwise exactly as the author wrote it. */
|
|
5
|
+
value: string;
|
|
6
|
+
origin: StyleOrigin;
|
|
7
|
+
}
|
|
8
|
+
export type ComputedStyle = ReadonlyMap<string, ComputedValue>;
|
|
9
|
+
export interface ResolveOptions {
|
|
10
|
+
/** Pseudo-classes to treat as active, e.g. `new Set(['hover'])`. */
|
|
11
|
+
activeStates?: ReadonlySet<string>;
|
|
12
|
+
}
|
|
13
|
+
export interface ResolveResult {
|
|
14
|
+
styles: ReadonlyMap<NodeId, ComputedStyle>;
|
|
15
|
+
warnings: readonly Warning[];
|
|
16
|
+
}
|
|
17
|
+
/** One declaration that competed, winner or not. */
|
|
18
|
+
export interface Candidate {
|
|
19
|
+
property: string;
|
|
20
|
+
value: string;
|
|
21
|
+
origin: StyleOrigin;
|
|
22
|
+
specificity: Specificity;
|
|
23
|
+
/** Position in cascade order; higher wins a specificity tie. */
|
|
24
|
+
order: number;
|
|
25
|
+
winner: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Purpose: compute the style of every element in the document.
|
|
29
|
+
* Deps/Effects: reads `doc` only; nothing on the model is mutated.
|
|
30
|
+
* Ensures: a property absent from a node's map is at its USS default.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveStyles(doc: UxmlDocument, options?: ResolveOptions): ResolveResult;
|
|
33
|
+
/**
|
|
34
|
+
* Purpose: every declaration that competed for one property on one element.
|
|
35
|
+
* Ensures: the entry marked `winner` is the one `resolveStyles` chose;
|
|
36
|
+
* both call `collectCandidates` and `pickWinners`.
|
|
37
|
+
*
|
|
38
|
+
* This is what an editor asks before offering "change the inline style, or edit
|
|
39
|
+
* the rule?". Nothing precomputes it: a stored candidate list would go stale the
|
|
40
|
+
* moment a rule changed, and a stale answer here rewrites the wrong file.
|
|
41
|
+
*/
|
|
42
|
+
export declare function explainProperty(doc: UxmlDocument, node: ElementNode, property: string, options?: ResolveOptions): Candidate[];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ElementNode, Selector } from '../model/types';
|
|
2
|
+
export interface MatchContext {
|
|
3
|
+
/** Parent of every node, built once per document. */
|
|
4
|
+
parentOf: (node: ElementNode) => ElementNode | null;
|
|
5
|
+
/**
|
|
6
|
+
* The element `:root` refers to.
|
|
7
|
+
*
|
|
8
|
+
* USS `:root` is not the CSS one: it names whichever element the stylesheet
|
|
9
|
+
* was applied to, and does not match that element's children. A preview has
|
|
10
|
+
* no UIDocument to attach to, so the `<ui:UXML>` element stands in — which
|
|
11
|
+
* keeps the usual custom-property token pattern working, since values
|
|
12
|
+
* declared there inherit down the whole tree.
|
|
13
|
+
*/
|
|
14
|
+
root: ElementNode;
|
|
15
|
+
/** Pseudo-classes currently active, e.g. `hover`. Empty in a static render. */
|
|
16
|
+
activeStates: ReadonlySet<string>;
|
|
17
|
+
}
|
|
18
|
+
export declare function matchesSelector(selector: Selector, node: ElementNode, ctx: MatchContext): boolean;
|
|
19
|
+
/** The first unsupported fragment in a selector group, if any. */
|
|
20
|
+
export declare function unsupportedFragment(selectors: readonly Selector[]): string | null;
|
|
21
|
+
export declare function buildParentMap(root: ElementNode): Map<ElementNode, ElementNode | null>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Selector } from '../model/types';
|
|
2
|
+
/** `[#name, .class + :pseudo, Type]`, compared left to right. */
|
|
3
|
+
export type Specificity = readonly [number, number, number];
|
|
4
|
+
export declare function specificityOf(selector: Selector): Specificity;
|
|
5
|
+
/** Negative when `a` loses to `b`, positive when it wins, 0 on a tie. */
|
|
6
|
+
export declare function compareSpecificity(a: Specificity, b: Specificity): number;
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "uxml-preview",
|
|
3
|
+
"version": "0.1.0",
|
|
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: 242 of 244 element coordinates identical to Unity 6. Parses and writes files back byte-for-byte.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"unity", "unity3d", "ui-toolkit", "uitk", "uielements", "uxml", "uss",
|
|
7
|
+
"yoga", "yoga-layout", "flexbox", "webassembly", "renderer", "preview",
|
|
8
|
+
"game-ui", "typescript", "editor-tooling", "unity-editor", "css-to-uss"
|
|
9
|
+
],
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": ["dist", "README.md", "LICENSE", "THIRD-PARTY-NOTICES.md", "CHANGELOG.md"],
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"engines": { "node": ">=18" },
|
|
24
|
+
"publishConfig": { "access": "public" },
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/ReuHomi/uxml-preview.git"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://reuhomi.github.io/uxml-preview/",
|
|
30
|
+
"bugs": { "url": "https://github.com/ReuHomi/uxml-preview/issues" },
|
|
31
|
+
"scripts": {
|
|
32
|
+
"dev": "vite",
|
|
33
|
+
"build": "tsc --noEmit && vite build",
|
|
34
|
+
"build:site": "vite build --config vite.playground.config.ts",
|
|
35
|
+
"preview:site": "vite preview --config vite.playground.config.ts",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:watch": "vitest",
|
|
38
|
+
"test:roundtrip": "vitest run tests/roundtrip",
|
|
39
|
+
"test:golden": "vitest run tests/golden --passWithNoTests",
|
|
40
|
+
"golden:emit": "node --experimental-strip-types --no-warnings scripts/emit-golden-cases.mjs",
|
|
41
|
+
"golden:update": "node scripts/update-golden.mjs",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"prepublishOnly": "npm run typecheck && npm run test && npm run build"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"yoga-layout": "^3.2.1"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^22.0.0",
|
|
50
|
+
"jsdom": "^30.0.1",
|
|
51
|
+
"pixelmatch": "^6.0.0",
|
|
52
|
+
"pngjs": "^7.0.0",
|
|
53
|
+
"typescript": "^5.6.0",
|
|
54
|
+
"vite": "^6.0.0",
|
|
55
|
+
"vite-plugin-dts": "^4.3.0",
|
|
56
|
+
"vitest": "^2.1.0"
|
|
57
|
+
},
|
|
58
|
+
"packageManager": "pnpm@9.12.0"
|
|
59
|
+
}
|