@typecad/ui 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +1443 -0
- package/dist/engine-index.d.ts +7 -0
- package/dist/engine-index.js +41 -0
- package/dist/index.d.ts +165 -0
- package/dist/index.js +42 -0
- package/dist/preview/build-program.d.ts +7 -0
- package/dist/preview/build-program.js +665 -0
- package/dist/preview/host-gfx.d.ts +90 -0
- package/dist/preview/host-gfx.js +552 -0
- package/dist/preview/host-ui-runtime.d.ts +241 -0
- package/dist/preview/host-ui-runtime.js +3236 -0
- package/dist/preview/types.d.ts +83 -0
- package/dist/preview/types.js +1 -0
- package/dist/types.d.ts +108 -0
- package/dist/types.js +10 -0
- package/dist/ui-engine/block-layout.d.ts +8 -0
- package/dist/ui-engine/block-layout.js +95 -0
- package/dist/ui-engine/color.d.ts +49 -0
- package/dist/ui-engine/color.js +273 -0
- package/dist/ui-engine/css-parser.d.ts +155 -0
- package/dist/ui-engine/css-parser.js +1058 -0
- package/dist/ui-engine/default-keyboards.d.ts +3 -0
- package/dist/ui-engine/default-keyboards.js +53 -0
- package/dist/ui-engine/easing.d.ts +12 -0
- package/dist/ui-engine/easing.js +76 -0
- package/dist/ui-engine/font-assets.d.ts +50 -0
- package/dist/ui-engine/font-assets.js +431 -0
- package/dist/ui-engine/html-parser.d.ts +116 -0
- package/dist/ui-engine/html-parser.js +284 -0
- package/dist/ui-engine/image-assets.d.ts +21 -0
- package/dist/ui-engine/image-assets.js +125 -0
- package/dist/ui-engine/inline-parser.d.ts +30 -0
- package/dist/ui-engine/inline-parser.js +66 -0
- package/dist/ui-engine/keyframes.d.ts +3 -0
- package/dist/ui-engine/keyframes.js +39 -0
- package/dist/ui-engine/layout-engine.d.ts +35 -0
- package/dist/ui-engine/layout-engine.js +203 -0
- package/dist/ui-engine/model.d.ts +239 -0
- package/dist/ui-engine/model.js +987 -0
- package/dist/ui-engine/rich-layout.d.ts +51 -0
- package/dist/ui-engine/rich-layout.js +220 -0
- package/dist/ui-engine/run-types.d.ts +13 -0
- package/dist/ui-engine/run-types.js +1 -0
- package/dist/ui-engine/runtime-header/antialiasing.d.ts +1 -0
- package/dist/ui-engine/runtime-header/antialiasing.js +141 -0
- package/dist/ui-engine/runtime-header/blend-bodies.d.ts +1 -0
- package/dist/ui-engine/runtime-header/blend-bodies.js +39 -0
- package/dist/ui-engine/runtime-header/canvas-helpers.d.ts +1 -0
- package/dist/ui-engine/runtime-header/canvas-helpers.js +155 -0
- package/dist/ui-engine/runtime-header/canvas-scrollbar.d.ts +1 -0
- package/dist/ui-engine/runtime-header/canvas-scrollbar.js +105 -0
- package/dist/ui-engine/runtime-header/color-mono-refresh.d.ts +1 -0
- package/dist/ui-engine/runtime-header/color-mono-refresh.js +154 -0
- package/dist/ui-engine/runtime-header/dirty-scroll-mutators.d.ts +1 -0
- package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +169 -0
- package/dist/ui-engine/runtime-header/display-shim.d.ts +1 -0
- package/dist/ui-engine/runtime-header/display-shim.js +104 -0
- package/dist/ui-engine/runtime-header/forward-decls.d.ts +1 -0
- package/dist/ui-engine/runtime-header/forward-decls.js +137 -0
- package/dist/ui-engine/runtime-header/guard-close.d.ts +1 -0
- package/dist/ui-engine/runtime-header/guard-close.js +8 -0
- package/dist/ui-engine/runtime-header/image-drawing.d.ts +1 -0
- package/dist/ui-engine/runtime-header/image-drawing.js +300 -0
- package/dist/ui-engine/runtime-header/init-press-input.d.ts +1 -0
- package/dist/ui-engine/runtime-header/init-press-input.js +126 -0
- package/dist/ui-engine/runtime-header/keyboard.d.ts +1 -0
- package/dist/ui-engine/runtime-header/keyboard.js +342 -0
- package/dist/ui-engine/runtime-header/node-decoration.d.ts +1 -0
- package/dist/ui-engine/runtime-header/node-decoration.js +212 -0
- package/dist/ui-engine/runtime-header/paint-order-coords.d.ts +1 -0
- package/dist/ui-engine/runtime-header/paint-order-coords.js +190 -0
- package/dist/ui-engine/runtime-header/paint-rects-repair.d.ts +1 -0
- package/dist/ui-engine/runtime-header/paint-rects-repair.js +553 -0
- package/dist/ui-engine/runtime-header/scroll-physics.d.ts +1 -0
- package/dist/ui-engine/runtime-header/scroll-physics.js +161 -0
- package/dist/ui-engine/runtime-header/state-bindings-nav.d.ts +1 -0
- package/dist/ui-engine/runtime-header/state-bindings-nav.js +176 -0
- package/dist/ui-engine/runtime-header/structs.d.ts +1 -0
- package/dist/ui-engine/runtime-header/structs.js +220 -0
- package/dist/ui-engine/runtime-header/text-rendering.d.ts +1 -0
- package/dist/ui-engine/runtime-header/text-rendering.js +715 -0
- package/dist/ui-engine/runtime-header/tick/bindings-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/bindings-phase.js +103 -0
- package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +1051 -0
- package/dist/ui-engine/runtime-header/tick/flush-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/flush-phase.js +17 -0
- package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +15 -0
- package/dist/ui-engine/runtime-header/tick/transitions-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/transitions-phase.js +213 -0
- package/dist/ui-engine/runtime-header/tick-motion-helpers.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick-motion-helpers.js +26 -0
- package/dist/ui-engine/runtime-header/tick.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick.js +17 -0
- package/dist/ui-engine/runtime-header/touch-keyboard-fwd.d.ts +1 -0
- package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +406 -0
- package/dist/ui-engine/runtime-header/types-defines.d.ts +1 -0
- package/dist/ui-engine/runtime-header/types-defines.js +79 -0
- package/dist/ui-engine/runtime-header.d.ts +1 -0
- package/dist/ui-engine/runtime-header.js +69 -0
- package/dist/ui-engine/scroll-memory-diagnostics.d.ts +16 -0
- package/dist/ui-engine/scroll-memory-diagnostics.js +58 -0
- package/dist/ui-engine/select-engine.d.ts +4 -0
- package/dist/ui-engine/select-engine.js +19 -0
- package/dist/ui-engine/style-resolver.d.ts +77 -0
- package/dist/ui-engine/style-resolver.js +341 -0
- package/dist/ui-engine/text-layout.d.ts +19 -0
- package/dist/ui-engine/text-layout.js +92 -0
- package/dist/ui-engine/transpile-ui.d.ts +11 -0
- package/dist/ui-engine/transpile-ui.js +33 -0
- package/dist/ui-engine/ua-stylesheet.d.ts +3 -0
- package/dist/ui-engine/ua-stylesheet.js +56 -0
- package/dist/ui-engine/ui-file-splitter.d.ts +14 -0
- package/dist/ui-engine/ui-file-splitter.js +47 -0
- package/dist/ui-engine/ui-lowering.d.ts +33 -0
- package/dist/ui-engine/ui-lowering.js +390 -0
- package/dist/ui-engine/ui-registry.d.ts +81 -0
- package/dist/ui-engine/ui-registry.js +429 -0
- package/dist/ui-engine/yoga-layout.d.ts +12 -0
- package/dist/ui-engine/yoga-layout.js +314 -0
- package/package.json +86 -0
- package/src/engine-index.ts +51 -0
- package/src/index.ts +232 -0
- package/src/preview/build-program.ts +734 -0
- package/src/preview/host-gfx.ts +580 -0
- package/src/preview/host-ui-runtime.ts +3245 -0
- package/src/preview/types.ts +96 -0
- package/src/types.ts +127 -0
- package/src/ui-engine/block-layout.ts +104 -0
- package/src/ui-engine/color.ts +263 -0
- package/src/ui-engine/css-parser.ts +1003 -0
- package/src/ui-engine/default-keyboards.ts +58 -0
- package/src/ui-engine/easing.ts +53 -0
- package/src/ui-engine/font-assets.ts +512 -0
- package/src/ui-engine/html-parser.ts +397 -0
- package/src/ui-engine/image-assets.ts +170 -0
- package/src/ui-engine/inline-parser.ts +78 -0
- package/src/ui-engine/keyframes.ts +47 -0
- package/src/ui-engine/layout-engine.ts +228 -0
- package/src/ui-engine/model.ts +1225 -0
- package/src/ui-engine/rich-layout.ts +257 -0
- package/src/ui-engine/run-types.ts +14 -0
- package/src/ui-engine/runtime-header/antialiasing.ts +141 -0
- package/src/ui-engine/runtime-header/blend-bodies.ts +39 -0
- package/src/ui-engine/runtime-header/canvas-helpers.ts +155 -0
- package/src/ui-engine/runtime-header/canvas-scrollbar.ts +105 -0
- package/src/ui-engine/runtime-header/color-mono-refresh.ts +154 -0
- package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +169 -0
- package/src/ui-engine/runtime-header/display-shim.ts +104 -0
- package/src/ui-engine/runtime-header/forward-decls.ts +137 -0
- package/src/ui-engine/runtime-header/guard-close.ts +8 -0
- package/src/ui-engine/runtime-header/image-drawing.ts +300 -0
- package/src/ui-engine/runtime-header/init-press-input.ts +126 -0
- package/src/ui-engine/runtime-header/keyboard.ts +342 -0
- package/src/ui-engine/runtime-header/node-decoration.ts +212 -0
- package/src/ui-engine/runtime-header/paint-order-coords.ts +190 -0
- package/src/ui-engine/runtime-header/paint-rects-repair.ts +553 -0
- package/src/ui-engine/runtime-header/scroll-physics.ts +161 -0
- package/src/ui-engine/runtime-header/state-bindings-nav.ts +176 -0
- package/src/ui-engine/runtime-header/structs.ts +220 -0
- package/src/ui-engine/runtime-header/text-rendering.ts +715 -0
- package/src/ui-engine/runtime-header/tick/bindings-phase.ts +103 -0
- package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +1051 -0
- package/src/ui-engine/runtime-header/tick/flush-phase.ts +17 -0
- package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +15 -0
- package/src/ui-engine/runtime-header/tick/transitions-phase.ts +213 -0
- package/src/ui-engine/runtime-header/tick-motion-helpers.ts +26 -0
- package/src/ui-engine/runtime-header/tick.ts +18 -0
- package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +406 -0
- package/src/ui-engine/runtime-header/types-defines.ts +79 -0
- package/src/ui-engine/runtime-header.ts +71 -0
- package/src/ui-engine/scroll-memory-diagnostics.ts +79 -0
- package/src/ui-engine/select-engine.ts +21 -0
- package/src/ui-engine/style-resolver.ts +416 -0
- package/src/ui-engine/text-layout.ts +114 -0
- package/src/ui-engine/transpile-ui.ts +45 -0
- package/src/ui-engine/ua-stylesheet.ts +60 -0
- package/src/ui-engine/ui-file-splitter.ts +59 -0
- package/src/ui-engine/ui-lowering.ts +459 -0
- package/src/ui-engine/ui-registry.ts +530 -0
- package/src/ui-engine/yoga-layout.ts +309 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Style resolver — match CSS rules to element nodes and produce a computed
|
|
3
|
+
// style per node (base state + :pressed state).
|
|
4
|
+
//
|
|
5
|
+
// Walks the tree; for each node, applies every matching rule in source order
|
|
6
|
+
// (cascade: later rules win). :pressed rules populate the separate
|
|
7
|
+
// `style.pressed` object so the runtime can lerp to it on press.
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
import { UIElementNode } from "./html-parser.js";
|
|
11
|
+
import type { Diagnostic } from "@typecad/cuttlefish/api/shared";
|
|
12
|
+
import { CSSRule, CSSProperty, CSSSelector, SimpleSelector, parseInlineStyle } from "./css-parser.js";
|
|
13
|
+
import { getUARules } from "./ua-stylesheet.js";
|
|
14
|
+
import { TextRun } from "./run-types.js";
|
|
15
|
+
import { InlineItem } from "./inline-parser.js";
|
|
16
|
+
|
|
17
|
+
// The CSS-standard set of properties that inherit from parent to child. A
|
|
18
|
+
// child inherits the parent's resolved value for each of these unless the
|
|
19
|
+
// child sets its own (rule or inline); the child's own value always wins.
|
|
20
|
+
// Box-model and decorative properties (background, border, padding, margin,
|
|
21
|
+
// width/height, opacity, ...) do NOT inherit — matching browser behavior.
|
|
22
|
+
// `opacity` is excluded because the lowering already compounds
|
|
23
|
+
// effectiveOpacity down the tree; `text-decoration` is excluded because the
|
|
24
|
+
// only producer of default underlines is the UA `a` rule applied directly.
|
|
25
|
+
export const INHERITED_KEYS = [
|
|
26
|
+
"color", "fontFamily", "fontSize", "fontWeight", "fontStyle", "fontSmoothing",
|
|
27
|
+
"lineHeight", "letterSpacing", "textAlign", "textTransform", "whiteSpace",
|
|
28
|
+
"textShadow", "textOverflow", "visibility",
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
export interface StyledNode {
|
|
32
|
+
tag: string;
|
|
33
|
+
origTag?: string;
|
|
34
|
+
id?: string;
|
|
35
|
+
classes: string[];
|
|
36
|
+
text?: string;
|
|
37
|
+
/** Rich-text runs. Present only for text nodes with mixed inline content;
|
|
38
|
+
* when present, `text` is empty and the runs are the node's content. */
|
|
39
|
+
runs?: TextRun[];
|
|
40
|
+
value?: string;
|
|
41
|
+
style: CSSProperty;
|
|
42
|
+
children: StyledNode[];
|
|
43
|
+
/** For <select>: parsed option list. */
|
|
44
|
+
options?: Array<{ value: string; text: string }>;
|
|
45
|
+
/** For <radio>: group name. */
|
|
46
|
+
name?: string;
|
|
47
|
+
/** For <radio>: initially selected. */
|
|
48
|
+
checked?: boolean;
|
|
49
|
+
/** For <range>: minimum value. */
|
|
50
|
+
min?: string;
|
|
51
|
+
/** For <range>: maximum value. */
|
|
52
|
+
max?: string;
|
|
53
|
+
/** For <input>: text or number keyboard. */
|
|
54
|
+
type?: "text" | "number";
|
|
55
|
+
/** For <input>: placeholder text. */
|
|
56
|
+
placeholder?: string;
|
|
57
|
+
/** For <input>: max character length. */
|
|
58
|
+
maxlen?: number;
|
|
59
|
+
/** For <input>: keyboard template id ref. */
|
|
60
|
+
keyboard?: string;
|
|
61
|
+
/** HTML hidden attribute: removes the element subtree from layout/rendering. */
|
|
62
|
+
hidden?: boolean;
|
|
63
|
+
/** Navigation target for <a href="#screenId"> links. */
|
|
64
|
+
href?: string;
|
|
65
|
+
/** Image source path for <img src="...">. */
|
|
66
|
+
src?: string;
|
|
67
|
+
/** Image width in pixels (for <img>). */
|
|
68
|
+
imgWidth?: number;
|
|
69
|
+
/** Image height in pixels (for <img>). */
|
|
70
|
+
imgHeight?: number;
|
|
71
|
+
/** Item height in pixels (for <list>). */
|
|
72
|
+
itemHeight?: number;
|
|
73
|
+
/** Canvas buffer width in pixels (for <canvas>). */
|
|
74
|
+
canvasW?: number;
|
|
75
|
+
/** Canvas buffer height in pixels (for <canvas>). */
|
|
76
|
+
canvasH?: number;
|
|
77
|
+
disabled?: boolean;
|
|
78
|
+
/** True when text contains a `{expr}` interpolation; auto-wire synthesizes an
|
|
79
|
+
* implicit text binding. Propagated from UIElementNode for the auto-wire walk. */
|
|
80
|
+
hasInterpolation?: boolean;
|
|
81
|
+
/** Declarative on:* event handlers (named-function references). Propagated
|
|
82
|
+
* from UIElementNode for the auto-wire walk. */
|
|
83
|
+
events?: { click?: string; hold?: string; release?: string; change?: string };
|
|
84
|
+
/** Declarative bind:* two-way bindings (signal names). Propagated from
|
|
85
|
+
* UIElementNode for the auto-wire walk. */
|
|
86
|
+
bind?: { text?: string; value?: string };
|
|
87
|
+
/** TS handle name (screen.<ref>), separate from CSS #id. Falls back to id.
|
|
88
|
+
* Propagated for type-decl generation + node-index resolution. */
|
|
89
|
+
ref?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Look up an HTML attribute value on a node by name, for attribute selectors.
|
|
93
|
+
* Returns the value as a string, or undefined when absent. Boolean attributes
|
|
94
|
+
* (checked/disabled/hidden) are "present" only when truthy. UIElementNode has
|
|
95
|
+
* no generic attribute map, so each name maps to a typed field. */
|
|
96
|
+
function attributeValue(node: UIElementNode, name: string): string | undefined {
|
|
97
|
+
switch (name) {
|
|
98
|
+
case "type": return node.type;
|
|
99
|
+
case "name": return node.name;
|
|
100
|
+
case "value": return node.value;
|
|
101
|
+
case "href": return node.href;
|
|
102
|
+
case "src": return node.src;
|
|
103
|
+
case "placeholder": return node.placeholder;
|
|
104
|
+
case "maxlength": return node.maxlength != null ? String(node.maxlength) : undefined;
|
|
105
|
+
// Boolean attributes: report presence by returning the attribute name.
|
|
106
|
+
case "checked": return node.checked ? "checked" : undefined;
|
|
107
|
+
case "disabled": return node.disabled ? "disabled" : undefined;
|
|
108
|
+
case "hidden": return node.hidden ? "hidden" : undefined;
|
|
109
|
+
default: return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Does a single element match a compound selector (all simples must match)? */
|
|
114
|
+
function matchesCompound(node: UIElementNode, compound: SimpleSelector[]): boolean {
|
|
115
|
+
for (const s of compound) {
|
|
116
|
+
switch (s.kind) {
|
|
117
|
+
case "element": if (node.tag !== s.name && node.origTag !== s.name) return false; break;
|
|
118
|
+
case "id": if (node.id !== s.name) return false; break;
|
|
119
|
+
case "class": if (!node.classes.includes(s.name)) return false; break;
|
|
120
|
+
case "attribute": {
|
|
121
|
+
// Presence ([disabled]) when no value; exact equality ([type="number"])
|
|
122
|
+
// when a value is present. NOTE: the parser captures [~=|^|$*]= but
|
|
123
|
+
// drops the operator, so only presence/equality are supported here.
|
|
124
|
+
const v = attributeValue(node, s.name);
|
|
125
|
+
if (v === undefined) return false;
|
|
126
|
+
if (s.value !== undefined && v !== s.value) return false;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Match the ancestor-side compounds (indices 0..c) against the `ancestors`
|
|
135
|
+
* chain, honoring combinators. `pos` is the exclusive upper bound: compound c
|
|
136
|
+
* may match any ancestor at index < pos. The node itself sits conceptually at
|
|
137
|
+
* index ancestors.length, so the first ancestor-side compound starts with the
|
|
138
|
+
* whole chain available. Returns true if all remaining compounds match. */
|
|
139
|
+
function matchAncestors(
|
|
140
|
+
compounds: SimpleSelector[][],
|
|
141
|
+
combinators: (">" | " " | "+" | "~")[],
|
|
142
|
+
c: number,
|
|
143
|
+
pos: number,
|
|
144
|
+
ancestors: UIElementNode[],
|
|
145
|
+
precedingSiblings: UIElementNode[],
|
|
146
|
+
): boolean {
|
|
147
|
+
if (c < 0) return true;
|
|
148
|
+
const comb = c < combinators.length ? combinators[c] : " ";
|
|
149
|
+
if (comb === ">") {
|
|
150
|
+
// Child combinator: compound c must match the immediate parent (index
|
|
151
|
+
// pos-1) of wherever compound c+1 matched. No scanning.
|
|
152
|
+
const i = pos - 1;
|
|
153
|
+
if (i < 0) return false;
|
|
154
|
+
if (!matchesCompound(ancestors[i], compounds[c])) return false;
|
|
155
|
+
// Children of the matched ancestor are its siblings-in-context; for a
|
|
156
|
+
// child match there are no further preceding siblings to track here.
|
|
157
|
+
return matchAncestors(compounds, combinators, c - 1, i, ancestors, []);
|
|
158
|
+
}
|
|
159
|
+
if (comb === "+") {
|
|
160
|
+
// Adjacent sibling: compound c must match the IMMEDIATE preceding sibling
|
|
161
|
+
// of compound c+1's match. precedingSiblings[0] is the nearest.
|
|
162
|
+
if (precedingSiblings.length === 0) return false;
|
|
163
|
+
if (!matchesCompound(precedingSiblings[0], compounds[c])) return false;
|
|
164
|
+
// The matched sibling's own preceding siblings (for chains like a + b + c).
|
|
165
|
+
const sibAncestors = ancestors; // siblings share the same ancestors
|
|
166
|
+
return matchAncestors(compounds, combinators, c - 1, pos, sibAncestors, precedingSiblings.slice(1));
|
|
167
|
+
}
|
|
168
|
+
if (comb === "~") {
|
|
169
|
+
// General sibling: compound c matches ANY preceding sibling. Scan from
|
|
170
|
+
// nearest outward; backtrack if a later compound fails.
|
|
171
|
+
for (let si = 0; si < precedingSiblings.length; si++) {
|
|
172
|
+
if (matchesCompound(precedingSiblings[si], compounds[c])) {
|
|
173
|
+
if (matchAncestors(compounds, combinators, c - 1, pos, ancestors, precedingSiblings.slice(si + 1))) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
// Descendant combinator: scan ancestors upward (decreasing index) for a
|
|
181
|
+
// match. Backtracking — if a later compound fails, try an earlier ancestor.
|
|
182
|
+
for (let i = pos - 1; i >= 0; i--) {
|
|
183
|
+
if (matchesCompound(ancestors[i], compounds[c])) {
|
|
184
|
+
if (matchAncestors(compounds, combinators, c - 1, i, ancestors, [])) return true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Does an element match a full selector (compound + combinators)?
|
|
191
|
+
* The last compound must match the node; preceding compounds must match
|
|
192
|
+
* ancestors per the combinators (` ` = descendant, `>` = child). */
|
|
193
|
+
function matches(node: UIElementNode, sel: CSSSelector, ancestors: UIElementNode[], precedingSiblings: UIElementNode[]): boolean {
|
|
194
|
+
const compounds = sel.compounds;
|
|
195
|
+
// Target compound (last) must match the node.
|
|
196
|
+
if (!matchesCompound(node, compounds[compounds.length - 1])) return false;
|
|
197
|
+
// :not(...) negation: if ANY inner compound matches the node, reject.
|
|
198
|
+
if (sel.not) {
|
|
199
|
+
for (const inner of sel.not) {
|
|
200
|
+
if (matchesCompound(node, inner)) return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Match preceding compounds against the ancestor chain, honoring combinators.
|
|
204
|
+
return matchAncestors(compounds, sel.combinators ?? [], compounds.length - 2, ancestors.length, ancestors, precedingSiblings);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Styling-tag default style, applied as a low-priority UA-equivalent rule on
|
|
208
|
+
// inline runs derived from <b>/<strong>/<i>/<em>/<u>.
|
|
209
|
+
const INLINE_TAG_DEFAULTS: Record<string, Partial<CSSProperty>> = {
|
|
210
|
+
b: { fontWeight: "bold" }, strong: { fontWeight: "bold" },
|
|
211
|
+
i: { fontStyle: "italic" }, em: { fontStyle: "italic" },
|
|
212
|
+
u: { textDecoration: "underline" },
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/** A minimal node-like shape for matching inline element items against the
|
|
216
|
+
* existing matchesCompound() helper (which reads tag/origTag/id/classes). */
|
|
217
|
+
interface InlineMatchable {
|
|
218
|
+
tag: string;
|
|
219
|
+
origTag?: string;
|
|
220
|
+
id?: string;
|
|
221
|
+
classes: string[];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Does an inline element item match a compound selector? Reuses the same
|
|
225
|
+
* element/class/id/attribute semantics as the main resolver. Attribute and id
|
|
226
|
+
* selectors always fail for inline items (they carry neither), which matches
|
|
227
|
+
* browser behavior where inline elements are usually targeted by tag/class. */
|
|
228
|
+
function inlineMatchesCompound(item: Extract<InlineItem, { kind: "element" }>, compound: SimpleSelector[]): boolean {
|
|
229
|
+
for (const s of compound) {
|
|
230
|
+
switch (s.kind) {
|
|
231
|
+
case "element":
|
|
232
|
+
if (item.origTag !== s.name) return false;
|
|
233
|
+
break;
|
|
234
|
+
case "class":
|
|
235
|
+
if (!item.classes.includes(s.name)) return false;
|
|
236
|
+
break;
|
|
237
|
+
case "id":
|
|
238
|
+
case "attribute":
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Resolve an inline element item's style: match its class/origTag selectors
|
|
246
|
+
* against the rule list (source order, last-wins), then layer its inlineStyle
|
|
247
|
+
* on top. Returns only the keys this item sets. */
|
|
248
|
+
function matchInlineRules(item: Extract<InlineItem, { kind: "element" }>, rules: CSSRule[]): Partial<CSSProperty> {
|
|
249
|
+
const out: Partial<CSSProperty> = {};
|
|
250
|
+
for (const rule of rules) {
|
|
251
|
+
const compounds = rule.selector.compounds;
|
|
252
|
+
const target = compounds[compounds.length - 1];
|
|
253
|
+
// v1: match by the target compound only (no ancestor combinators for inline
|
|
254
|
+
// items — they're absorbed and no longer have a stable ancestor chain).
|
|
255
|
+
if (inlineMatchesCompound(item, target)) {
|
|
256
|
+
Object.assign(out, rule.properties);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (item.inlineStyle) {
|
|
260
|
+
Object.assign(out, parseInlineStyle(item.inlineStyle));
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Flatten the inline sequence into a runs[] list in document order. Each run
|
|
266
|
+
* carries the most-specific style at that point: inherited (parent's resolved
|
|
267
|
+
* style) → styling-tag default → matched rules → inlineStyle, last-wins.
|
|
268
|
+
* An <a href> element threads its href onto every text run inside it. */
|
|
269
|
+
function flattenInline(
|
|
270
|
+
items: InlineItem[],
|
|
271
|
+
parentStyle: CSSProperty,
|
|
272
|
+
rules: CSSRule[],
|
|
273
|
+
ancestors: UIElementNode[],
|
|
274
|
+
diagnostics: Diagnostic[] | undefined,
|
|
275
|
+
): TextRun[] {
|
|
276
|
+
const runs: TextRun[] = [];
|
|
277
|
+
const walk = (items: InlineItem[], inheritedStyle: Partial<CSSProperty>, inheritedHref: string | undefined) => {
|
|
278
|
+
for (const item of items) {
|
|
279
|
+
if (item.kind === "break") {
|
|
280
|
+
runs.push({ text: "\n", style: { ...inheritedStyle }, hardBreak: true, href: inheritedHref });
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (item.kind === "text") {
|
|
284
|
+
if (item.text) runs.push({ text: item.text, style: { ...inheritedStyle }, href: inheritedHref });
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
// element: compute its style and recurse / emit.
|
|
288
|
+
const tagDefault = item.origTag ? INLINE_TAG_DEFAULTS[item.origTag] ?? {} : {};
|
|
289
|
+
const matched = matchInlineRules(item, rules);
|
|
290
|
+
const runStyle: Partial<CSSProperty> = { ...inheritedStyle, ...tagDefault, ...matched };
|
|
291
|
+
const runHref = item.href ?? inheritedHref;
|
|
292
|
+
if (item.inline && item.inline.length > 0) {
|
|
293
|
+
walk(item.inline, runStyle, runHref);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
walk(items, parentStyle, undefined);
|
|
298
|
+
return runs;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function resolveStyles(root: UIElementNode, rules: CSSRule[], diagnostics?: Diagnostic[]): StyledNode {
|
|
302
|
+
const allRules = [...getUARules(), ...rules];
|
|
303
|
+
return resolveNode(root, allRules, [], diagnostics, undefined);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function resolveNode(node: UIElementNode, rules: CSSRule[], ancestors: UIElementNode[], diagnostics?: Diagnostic[], parentInherited?: CSSProperty): StyledNode {
|
|
307
|
+
const base: CSSProperty = {};
|
|
308
|
+
const pressed: CSSProperty = {};
|
|
309
|
+
|
|
310
|
+
// Preceding siblings of this node (most-recent-first), for + and ~ combinators.
|
|
311
|
+
// The parent is the last ancestor; its children before this node are siblings.
|
|
312
|
+
const parent = ancestors.length > 0 ? ancestors[ancestors.length - 1] : undefined;
|
|
313
|
+
let precedingSiblings: UIElementNode[] = [];
|
|
314
|
+
if (parent) {
|
|
315
|
+
const idx = parent.children.indexOf(node);
|
|
316
|
+
if (idx > 0) precedingSiblings = parent.children.slice(0, idx).reverse();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (const rule of rules) {
|
|
320
|
+
if (!matches(node, rule.selector, ancestors, precedingSiblings)) continue;
|
|
321
|
+
// Gate pseudo-class rules on the node's actual state. :pressed is stored
|
|
322
|
+
// separately (the transition driver reads it); :checked/:disabled/:focus
|
|
323
|
+
// apply to base only when the node is in that state, otherwise skip.
|
|
324
|
+
const pseudo = rule.selector.pseudo;
|
|
325
|
+
if (pseudo === "pressed") {
|
|
326
|
+
Object.assign(pressed, rule.properties);
|
|
327
|
+
} else if (pseudo === "checked") {
|
|
328
|
+
if (node.checked) Object.assign(base, rule.properties);
|
|
329
|
+
} else if (pseudo === "disabled") {
|
|
330
|
+
if (node.disabled) Object.assign(base, rule.properties);
|
|
331
|
+
} else if (pseudo === "focus") {
|
|
332
|
+
// Focus is a runtime-only state (set on tap), not known at resolve time,
|
|
333
|
+
// so :focus rules can't match statically — skip them here.
|
|
334
|
+
continue;
|
|
335
|
+
} else {
|
|
336
|
+
Object.assign(base, rule.properties);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const style: CSSProperty = base;
|
|
341
|
+
// Inline style attribute has the highest priority — merge last.
|
|
342
|
+
if (node.inlineStyle) {
|
|
343
|
+
Object.assign(style, parseInlineStyle(node.inlineStyle, diagnostics));
|
|
344
|
+
}
|
|
345
|
+
if (node.hidden) {
|
|
346
|
+
style.display = "none";
|
|
347
|
+
}
|
|
348
|
+
if (Object.keys(pressed).length > 0) {
|
|
349
|
+
// Attach pressed overrides; the transition driver reads these on press.
|
|
350
|
+
(style as CSSProperty & { pressed?: CSSProperty }).pressed = pressed;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Apply CSS inheritance: for each inherited key the node didn't set itself
|
|
354
|
+
// (no rule, no inline, no hidden override), take the parent's resolved value.
|
|
355
|
+
// The node's own value always wins; only the gaps are filled.
|
|
356
|
+
if (parentInherited) {
|
|
357
|
+
for (const key of INHERITED_KEYS) {
|
|
358
|
+
if (style[key] === undefined && parentInherited[key] !== undefined) {
|
|
359
|
+
style[key] = parentInherited[key];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// The resolved inherited subset to pass to this node's children: the node's
|
|
365
|
+
// own value where it set one, else whatever it inherited. Since `style` now
|
|
366
|
+
// holds the effective value after the merge above, read straight off it.
|
|
367
|
+
const inherited: CSSProperty = {};
|
|
368
|
+
for (const key of INHERITED_KEYS) {
|
|
369
|
+
if (style[key] !== undefined) inherited[key] = style[key];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Absorb an inline sequence into runs. Each run's style = the parent text
|
|
373
|
+
// node's resolved style + the inline element's matched rules + inheritance,
|
|
374
|
+
// with styling-tag defaults (b/strong→bold, i/em→italic, u→underline) applied
|
|
375
|
+
// as low-priority UA-equivalent rules. Inline children do not become separate
|
|
376
|
+
// StyledNodes — they're flattened into this node's run list.
|
|
377
|
+
let runs: TextRun[] | undefined;
|
|
378
|
+
if (node.inline && node.inline.length > 0) {
|
|
379
|
+
runs = flattenInline(node.inline, style, rules, ancestors, diagnostics);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const childAncestors = [...ancestors, node];
|
|
383
|
+
return {
|
|
384
|
+
tag: node.tag,
|
|
385
|
+
origTag: node.origTag,
|
|
386
|
+
id: node.id,
|
|
387
|
+
classes: node.classes,
|
|
388
|
+
text: node.text,
|
|
389
|
+
runs,
|
|
390
|
+
value: node.value,
|
|
391
|
+
style,
|
|
392
|
+
children: node.children.map(c => resolveNode(c, rules, childAncestors, diagnostics, inherited)),
|
|
393
|
+
options: node.options,
|
|
394
|
+
name: node.name,
|
|
395
|
+
checked: node.checked,
|
|
396
|
+
min: node.min,
|
|
397
|
+
max: node.max,
|
|
398
|
+
type: node.type,
|
|
399
|
+
placeholder: node.placeholder,
|
|
400
|
+
maxlen: node.maxlength,
|
|
401
|
+
keyboard: node.keyboard,
|
|
402
|
+
hidden: node.hidden,
|
|
403
|
+
href: node.href,
|
|
404
|
+
src: node.src,
|
|
405
|
+
imgWidth: node.imgWidth,
|
|
406
|
+
imgHeight: node.imgHeight,
|
|
407
|
+
itemHeight: node.itemHeight,
|
|
408
|
+
canvasW: node.canvasW,
|
|
409
|
+
canvasH: node.canvasH,
|
|
410
|
+
disabled: (node as any).disabled,
|
|
411
|
+
hasInterpolation: node.hasInterpolation,
|
|
412
|
+
events: node.events,
|
|
413
|
+
bind: node.bind,
|
|
414
|
+
ref: node.ref,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export type WhiteSpaceMode = "normal" | "nowrap" | "pre" | "pre-line";
|
|
2
|
+
|
|
3
|
+
export interface TextLayoutLine {
|
|
4
|
+
text: string;
|
|
5
|
+
width: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface TextLayoutResult {
|
|
9
|
+
lines: TextLayoutLine[];
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
lineHeight: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TextLayoutOptions {
|
|
16
|
+
maxWidth?: number;
|
|
17
|
+
whiteSpace?: string;
|
|
18
|
+
lineHeight: number;
|
|
19
|
+
measureText: (text: string) => number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function whiteSpaceMode(value: string | undefined): WhiteSpaceMode {
|
|
23
|
+
switch ((value ?? "normal").trim().toLowerCase()) {
|
|
24
|
+
case "nowrap": return "nowrap";
|
|
25
|
+
case "pre": return "pre";
|
|
26
|
+
case "pre-line": return "pre-line";
|
|
27
|
+
default: return "normal";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function finiteMaxWidth(value: number | undefined): number | undefined {
|
|
32
|
+
if (value === undefined || !Number.isFinite(value)) return undefined;
|
|
33
|
+
const px = Math.trunc(value);
|
|
34
|
+
return px > 0 ? px : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeText(text: string, mode: WhiteSpaceMode): string {
|
|
38
|
+
const normalizedNewlines = text.replace(/\r\n?/g, "\n");
|
|
39
|
+
if (mode === "pre") return normalizedNewlines;
|
|
40
|
+
if (mode === "pre-line") return normalizedNewlines.replace(/[ \t\f\v]+/g, " ");
|
|
41
|
+
return normalizedNewlines.replace(/\s+/g, " ");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function splitLongWord(word: string, maxWidth: number, measureText: (text: string) => number): TextLayoutLine[] {
|
|
45
|
+
const out: TextLayoutLine[] = [];
|
|
46
|
+
let current = "";
|
|
47
|
+
for (const ch of word) {
|
|
48
|
+
const next = current + ch;
|
|
49
|
+
if (current && measureText(next) > maxWidth) {
|
|
50
|
+
out.push({ text: current, width: measureText(current) });
|
|
51
|
+
current = ch;
|
|
52
|
+
} else {
|
|
53
|
+
current = next;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (current) out.push({ text: current, width: measureText(current) });
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function wrapParagraph(paragraph: string, maxWidth: number, measureText: (text: string) => number): TextLayoutLine[] {
|
|
61
|
+
const words = paragraph.trim().split(/ +/).filter(Boolean);
|
|
62
|
+
if (words.length === 0) return [{ text: "", width: 0 }];
|
|
63
|
+
|
|
64
|
+
const out: TextLayoutLine[] = [];
|
|
65
|
+
let line = "";
|
|
66
|
+
for (const word of words) {
|
|
67
|
+
const candidate = line ? `${line} ${word}` : word;
|
|
68
|
+
if (measureText(candidate) <= maxWidth) {
|
|
69
|
+
line = candidate;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (line) {
|
|
74
|
+
out.push({ text: line, width: measureText(line) });
|
|
75
|
+
line = "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (measureText(word) <= maxWidth) {
|
|
79
|
+
line = word;
|
|
80
|
+
} else {
|
|
81
|
+
const split = splitLongWord(word, maxWidth, measureText);
|
|
82
|
+
out.push(...split.slice(0, -1));
|
|
83
|
+
line = split.at(-1)?.text ?? "";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (line || out.length === 0) out.push({ text: line, width: measureText(line) });
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function layoutText(text: string | undefined, options: TextLayoutOptions): TextLayoutResult {
|
|
92
|
+
const mode = whiteSpaceMode(options.whiteSpace);
|
|
93
|
+
const lineHeight = Math.max(1, Math.trunc(options.lineHeight));
|
|
94
|
+
const maxWidth = finiteMaxWidth(options.maxWidth);
|
|
95
|
+
const normalized = normalizeText(text ?? "", mode);
|
|
96
|
+
const hardLines = (mode === "pre" || mode === "pre-line") ? normalized.split("\n") : [normalized];
|
|
97
|
+
const canWrap = mode !== "nowrap" && mode !== "pre" && maxWidth !== undefined;
|
|
98
|
+
|
|
99
|
+
const lines: TextLayoutLine[] = [];
|
|
100
|
+
for (const hardLine of hardLines) {
|
|
101
|
+
if (canWrap) lines.push(...wrapParagraph(hardLine, maxWidth, options.measureText));
|
|
102
|
+
else lines.push({ text: hardLine, width: options.measureText(hardLine) });
|
|
103
|
+
}
|
|
104
|
+
if (lines.length === 0) lines.push({ text: "", width: 0 });
|
|
105
|
+
|
|
106
|
+
const width = lines.reduce((max, line) => Math.max(max, line.width), 0);
|
|
107
|
+
return {
|
|
108
|
+
lines,
|
|
109
|
+
width,
|
|
110
|
+
height: lines.length * lineHeight,
|
|
111
|
+
lineHeight,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// transpileUI — end-to-end facade: HTML + CSS → lowered C++.
|
|
3
|
+
//
|
|
4
|
+
// Orchestrates: parse HTML → parse CSS → resolve styles → layout → lower.
|
|
5
|
+
// This is the integration contract used by the AST visitor that resolves
|
|
6
|
+
// .ui.html imports during a normal `cuttlefish build`.
|
|
7
|
+
//
|
|
8
|
+
// Selecting the layout engine on the `display` property happens here in v2;
|
|
9
|
+
// v1 always uses BlockLayoutEngine.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { parseHtml } from "./html-parser.js";
|
|
14
|
+
import { parseCss, parseFontFaces, parseKeyframes } from "./css-parser.js";
|
|
15
|
+
import { resolveStyles } from "./style-resolver.js";
|
|
16
|
+
import { selectEngine } from "./select-engine.js";
|
|
17
|
+
import { measure, Box } from "./layout-engine.js";
|
|
18
|
+
import { lowerUIToCpp, LoweredUI } from "./ui-lowering.js";
|
|
19
|
+
import { buildUIFontAssets } from "./font-assets.js";
|
|
20
|
+
import { buildKeyframeSets } from "./keyframes.js";
|
|
21
|
+
|
|
22
|
+
export interface TranspileUIOptions {
|
|
23
|
+
colorFormat: "rgb565" | "mono";
|
|
24
|
+
storage: "progmem" | "flash";
|
|
25
|
+
viewport: { width: number; height: number };
|
|
26
|
+
assetBaseDir?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function transpileUI(html: string, css: string, opts: TranspileUIOptions): LoweredUI {
|
|
30
|
+
const tree = parseHtml(html);
|
|
31
|
+
const rules = parseCss(css);
|
|
32
|
+
const fontFaces = parseFontFaces(css);
|
|
33
|
+
const rawKeyframes = parseKeyframes(css);
|
|
34
|
+
const styled = resolveStyles(tree, rules);
|
|
35
|
+
const fontAssets = buildUIFontAssets(styled, fontFaces, path.resolve(opts.assetBaseDir ?? process.cwd()));
|
|
36
|
+
|
|
37
|
+
// Select layout engine: Yoga for flexbox, BlockLayout as fallback.
|
|
38
|
+
const engine = selectEngine(styled);
|
|
39
|
+
const viewport: Box = { x: 0, y: 0, w: opts.viewport.width, h: opts.viewport.height };
|
|
40
|
+
const boxes = engine.arrange(styled, viewport, measure);
|
|
41
|
+
|
|
42
|
+
const keyframeSets = buildKeyframeSets(rawKeyframes, opts.colorFormat);
|
|
43
|
+
|
|
44
|
+
return lowerUIToCpp(styled, boxes, opts.colorFormat, opts.storage, [], rules, undefined, fontAssets, [], new Map(), keyframeSets);
|
|
45
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// UA (User-Agent) default stylesheet.
|
|
3
|
+
// Applied before user CSS so elements have sensible built-in behavior without
|
|
4
|
+
// requiring explicit CSS. User CSS overrides (cascade: last-wins).
|
|
5
|
+
// Minimal and embedded-oriented, not a browser clone.
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
import { parseCss } from "./css-parser.js";
|
|
9
|
+
import type { CSSRule } from "./css-parser.js";
|
|
10
|
+
|
|
11
|
+
const UA_CSS = `
|
|
12
|
+
screen {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
}
|
|
16
|
+
body {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
flex: 1;
|
|
20
|
+
min-height: 0;
|
|
21
|
+
}
|
|
22
|
+
view {
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
}
|
|
26
|
+
button {
|
|
27
|
+
padding: 4px 10px;
|
|
28
|
+
border: 1px solid;
|
|
29
|
+
text-align: center;
|
|
30
|
+
}
|
|
31
|
+
input {
|
|
32
|
+
padding: 4px 8px;
|
|
33
|
+
border: 1px solid;
|
|
34
|
+
}
|
|
35
|
+
list {
|
|
36
|
+
overflow: scroll;
|
|
37
|
+
flex-grow: 1;
|
|
38
|
+
}
|
|
39
|
+
a {
|
|
40
|
+
text-decoration: underline;
|
|
41
|
+
}
|
|
42
|
+
label {
|
|
43
|
+
text-align: left;
|
|
44
|
+
}
|
|
45
|
+
h1 { font-size: 24px; font-weight: bold; }
|
|
46
|
+
h2 { font-size: 20px; font-weight: bold; }
|
|
47
|
+
h3 { font-size: 18px; font-weight: bold; }
|
|
48
|
+
h4 { font-size: 16px; font-weight: bold; }
|
|
49
|
+
h5 { font-size: 14px; }
|
|
50
|
+
h6 { font-size: 12px; }
|
|
51
|
+
p { font-size: 16px; }
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
let UA_RULES: CSSRule[] | null = null;
|
|
55
|
+
|
|
56
|
+
/** Get the parsed UA default rules. Lazily computed, cached. */
|
|
57
|
+
export function getUARules(): CSSRule[] {
|
|
58
|
+
if (!UA_RULES) UA_RULES = parseCss(UA_CSS);
|
|
59
|
+
return UA_RULES;
|
|
60
|
+
}
|