@compact-design/html 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/LICENSE +373 -0
- package/README.md +44 -0
- package/dist/css.d.ts +31 -0
- package/dist/index.cjs +705 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +679 -0
- package/dist/render.d.ts +16 -0
- package/package.json +58 -0
- package/scripts/render.mjs +67 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
// src/render.ts
|
|
2
|
+
import {
|
|
3
|
+
indexDocument,
|
|
4
|
+
validate
|
|
5
|
+
} from "@compact-design/core";
|
|
6
|
+
|
|
7
|
+
// src/css.ts
|
|
8
|
+
var WEB_SAFE = /* @__PURE__ */ new Set([
|
|
9
|
+
"Arial",
|
|
10
|
+
"Helvetica",
|
|
11
|
+
"Times",
|
|
12
|
+
"Times New Roman",
|
|
13
|
+
"Courier",
|
|
14
|
+
"Courier New",
|
|
15
|
+
"Georgia",
|
|
16
|
+
"Verdana",
|
|
17
|
+
"Tahoma",
|
|
18
|
+
"Trebuchet MS",
|
|
19
|
+
"Impact",
|
|
20
|
+
"Comic Sans MS",
|
|
21
|
+
"system-ui",
|
|
22
|
+
"sans-serif",
|
|
23
|
+
"serif",
|
|
24
|
+
"monospace"
|
|
25
|
+
]);
|
|
26
|
+
function isRecord(value) {
|
|
27
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
function isDesignColor(value) {
|
|
30
|
+
return isRecord(value) && typeof value.r === "number" && typeof value.g === "number" && typeof value.b === "number";
|
|
31
|
+
}
|
|
32
|
+
function escapeHtml(value) {
|
|
33
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
34
|
+
}
|
|
35
|
+
function htmlId(id) {
|
|
36
|
+
const cleaned = id.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
37
|
+
return `n-${cleaned || "node"}`;
|
|
38
|
+
}
|
|
39
|
+
function cssEscape(value) {
|
|
40
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, (char) => `\\${char}`);
|
|
41
|
+
}
|
|
42
|
+
function finite(value, fallback) {
|
|
43
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
44
|
+
}
|
|
45
|
+
function cssColor(value, opacity = 1) {
|
|
46
|
+
if (!value) return `rgba(0, 0, 0, ${opacity})`;
|
|
47
|
+
const channel = (input) => {
|
|
48
|
+
const n = finite(input, 0);
|
|
49
|
+
return Math.round(n > 1 && n <= 255 ? n : n * 255);
|
|
50
|
+
};
|
|
51
|
+
const alpha = finite(value.a, 1) * opacity;
|
|
52
|
+
return `rgba(${channel(value.r)}, ${channel(value.g)}, ${channel(value.b)}, ${alpha})`;
|
|
53
|
+
}
|
|
54
|
+
function paintColor(paint) {
|
|
55
|
+
if (!paint || paint.type !== "SOLID" || !paint.color) return void 0;
|
|
56
|
+
return cssColor(paint.color, finite(paint.opacity, 1));
|
|
57
|
+
}
|
|
58
|
+
function gradientAngle(paint) {
|
|
59
|
+
const transform = paint.gradientTransform;
|
|
60
|
+
if (!transform) return 90;
|
|
61
|
+
const cosine = transform[0]?.[0] ?? 1;
|
|
62
|
+
const sine = transform[0]?.[1] ?? 0;
|
|
63
|
+
const compact = Math.atan2(sine, cosine) * 180 / Math.PI;
|
|
64
|
+
return compact + 90;
|
|
65
|
+
}
|
|
66
|
+
function gradientStops(paint) {
|
|
67
|
+
const stops = paint.gradientStops || [];
|
|
68
|
+
if (!stops.length) return `${cssColor(paint.color)}, ${cssColor(paint.color)}`;
|
|
69
|
+
return stops.map((stop) => `${cssColor(stop.color)} ${Math.round(finite(stop.position, 0) * 1e3) / 10}%`).join(", ");
|
|
70
|
+
}
|
|
71
|
+
function backgroundLayers(paints) {
|
|
72
|
+
const layers = [];
|
|
73
|
+
for (const paint of [...paints].reverse()) {
|
|
74
|
+
if (paint.type === "SOLID" && paint.color) layers.push(`linear-gradient(${cssColor(paint.color, finite(paint.opacity, 1))}, ${cssColor(paint.color, finite(paint.opacity, 1))})`);
|
|
75
|
+
else if (paint.type === "GRADIENT_LINEAR") layers.push(`linear-gradient(${gradientAngle(paint)}deg, ${gradientStops(paint)})`);
|
|
76
|
+
else if (paint.type === "GRADIENT_RADIAL" || paint.type === "GRADIENT_DIAMOND") layers.push(`radial-gradient(circle, ${gradientStops(paint)})`);
|
|
77
|
+
else if (paint.type === "GRADIENT_ANGULAR") layers.push(`conic-gradient(${gradientStops(paint)})`);
|
|
78
|
+
else if (paint.type === "IMAGE" && paint.src) {
|
|
79
|
+
const size = paint.scaleMode === "FIT" ? "contain" : paint.scaleMode === "TILE" ? "auto" : "cover";
|
|
80
|
+
const repeat = paint.scaleMode === "TILE" ? "repeat" : "no-repeat";
|
|
81
|
+
layers.push(`url(${JSON.stringify(paint.src)}) ${repeat} center / ${size}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return layers;
|
|
85
|
+
}
|
|
86
|
+
function fontWeight(style) {
|
|
87
|
+
const value = style.toLowerCase();
|
|
88
|
+
if (value.includes("thin")) return 100;
|
|
89
|
+
if (value.includes("extralight") || value.includes("ultralight")) return 200;
|
|
90
|
+
if (value.includes("light")) return 300;
|
|
91
|
+
if (value.includes("medium")) return 500;
|
|
92
|
+
if (value.includes("semibold") || value.includes("demibold")) return 600;
|
|
93
|
+
if (value.includes("extrabold") || value.includes("ultrabold")) return 800;
|
|
94
|
+
if (value.includes("black") || value.includes("heavy")) return 900;
|
|
95
|
+
if (value.includes("bold")) return 700;
|
|
96
|
+
return 400;
|
|
97
|
+
}
|
|
98
|
+
function isItalic(style) {
|
|
99
|
+
return /italic|oblique/i.test(style);
|
|
100
|
+
}
|
|
101
|
+
function quoteFont(family) {
|
|
102
|
+
return /[^a-zA-Z0-9-]/.test(family) ? `"${family.replace(/"/g, '\\"')}"` : family;
|
|
103
|
+
}
|
|
104
|
+
function isWebSafeFont(family) {
|
|
105
|
+
return WEB_SAFE.has(family);
|
|
106
|
+
}
|
|
107
|
+
function fontDeclarations(font) {
|
|
108
|
+
if (!font) return [];
|
|
109
|
+
const declarations = [
|
|
110
|
+
`font-family: ${quoteFont(font.family)}, sans-serif`,
|
|
111
|
+
`font-size: ${finite(font.size, 16)}px`,
|
|
112
|
+
`font-weight: ${fontWeight(font.style)}`
|
|
113
|
+
];
|
|
114
|
+
if (isItalic(font.style)) declarations.push("font-style: italic");
|
|
115
|
+
return declarations;
|
|
116
|
+
}
|
|
117
|
+
function lineHeightCss(value) {
|
|
118
|
+
if (!value || value.unit === "AUTO") return void 0;
|
|
119
|
+
if (value.unit === "PIXELS") return `${finite(value.value, 0)}px`;
|
|
120
|
+
return String(finite(value.value, 100) / 100);
|
|
121
|
+
}
|
|
122
|
+
function letterSpacingCss(value) {
|
|
123
|
+
if (!value) return void 0;
|
|
124
|
+
if (value.unit === "PERCENT") return `${finite(value.value, 0) / 100}em`;
|
|
125
|
+
return `${finite(value.value, 0)}px`;
|
|
126
|
+
}
|
|
127
|
+
function effectShadows(effects) {
|
|
128
|
+
const shadows = [];
|
|
129
|
+
for (const effect of effects) {
|
|
130
|
+
if (effect.visible === false) continue;
|
|
131
|
+
if (effect.type === "DROP_SHADOW" || effect.type === "INNER_SHADOW") {
|
|
132
|
+
const inset = effect.type === "INNER_SHADOW" ? "inset " : "";
|
|
133
|
+
shadows.push(`${inset}${finite(effect.offset?.x, 0)}px ${finite(effect.offset?.y, 4)}px ${finite(effect.radius, 8)}px ${finite(effect.spread, 0)}px ${cssColor(effect.color)}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return shadows;
|
|
137
|
+
}
|
|
138
|
+
function blurFilters(effects) {
|
|
139
|
+
const layer = effects.filter((effect) => effect.visible !== false && effect.type === "LAYER_BLUR");
|
|
140
|
+
const backdrop = effects.filter((effect) => effect.visible !== false && effect.type === "BACKGROUND_BLUR");
|
|
141
|
+
return {
|
|
142
|
+
filter: layer.length ? layer.map((effect) => `blur(${finite(effect.radius, 8)}px)`).join(" ") : void 0,
|
|
143
|
+
backdrop: backdrop.length ? backdrop.map((effect) => `blur(${finite(effect.radius, 8)}px)`).join(" ") : void 0
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
var ALIGN = {
|
|
147
|
+
MIN: "flex-start",
|
|
148
|
+
MAX: "flex-end",
|
|
149
|
+
CENTER: "center",
|
|
150
|
+
SPACE_BETWEEN: "space-between",
|
|
151
|
+
BASELINE: "baseline",
|
|
152
|
+
STRETCH: "stretch"
|
|
153
|
+
};
|
|
154
|
+
function layoutDeclarations(props) {
|
|
155
|
+
const layout = props.layout;
|
|
156
|
+
if (!layout?.direction) return [];
|
|
157
|
+
const declarations = [];
|
|
158
|
+
if (layout.direction === "GRID") {
|
|
159
|
+
declarations.push("display: grid");
|
|
160
|
+
if (typeof layout.itemSpacing === "number") declarations.push(`gap: ${layout.itemSpacing}px`);
|
|
161
|
+
} else {
|
|
162
|
+
declarations.push("display: flex");
|
|
163
|
+
declarations.push(`flex-direction: ${layout.direction === "HORIZONTAL" ? "row" : "column"}`);
|
|
164
|
+
if (layout.wrap) declarations.push("flex-wrap: wrap");
|
|
165
|
+
declarations.push(`justify-content: ${ALIGN[layout.primaryAxisAlignItems || "MIN"] || "flex-start"}`);
|
|
166
|
+
declarations.push(`align-items: ${ALIGN[layout.counterAxisAlignItems || "MIN"] || "flex-start"}`);
|
|
167
|
+
if (typeof layout.itemSpacing === "number") declarations.push(`gap: ${layout.itemSpacing}px`);
|
|
168
|
+
if (typeof layout.counterAxisSpacing === "number" && layout.wrap) declarations.push(`row-gap: ${layout.counterAxisSpacing}px`);
|
|
169
|
+
}
|
|
170
|
+
const padding = layout.padding;
|
|
171
|
+
if (padding) {
|
|
172
|
+
declarations.push(`padding: ${finite(padding.top, 0)}px ${finite(padding.right, 0)}px ${finite(padding.bottom, 0)}px ${finite(padding.left, 0)}px`);
|
|
173
|
+
}
|
|
174
|
+
return declarations;
|
|
175
|
+
}
|
|
176
|
+
function childSizingDeclarations(props, parentDirection) {
|
|
177
|
+
if (!parentDirection) return [];
|
|
178
|
+
const declarations = [];
|
|
179
|
+
const horizontal = props.layoutSizingHorizontal;
|
|
180
|
+
const vertical = props.layoutSizingVertical;
|
|
181
|
+
if (parentDirection === "HORIZONTAL") {
|
|
182
|
+
if (horizontal === "FILL") declarations.push("flex: 1 1 0", "min-width: 0");
|
|
183
|
+
else if (horizontal === "HUG") declarations.push("flex: 0 0 auto", "width: auto");
|
|
184
|
+
if (vertical === "FILL") declarations.push("align-self: stretch", "height: auto");
|
|
185
|
+
else if (vertical === "HUG") declarations.push("height: auto");
|
|
186
|
+
} else if (parentDirection === "VERTICAL") {
|
|
187
|
+
if (vertical === "FILL") declarations.push("flex: 1 1 0", "min-height: 0");
|
|
188
|
+
else if (vertical === "HUG") declarations.push("flex: 0 0 auto", "height: auto");
|
|
189
|
+
if (horizontal === "FILL") declarations.push("align-self: stretch", "width: auto");
|
|
190
|
+
else if (horizontal === "HUG") declarations.push("width: auto");
|
|
191
|
+
}
|
|
192
|
+
if (typeof props.layoutGrow === "number") declarations.push(`flex-grow: ${props.layoutGrow}`);
|
|
193
|
+
if (props.layoutAlign === "STRETCH") declarations.push("align-self: stretch");
|
|
194
|
+
if (typeof props.minWidth === "number") declarations.push(`min-width: ${props.minWidth}px`);
|
|
195
|
+
if (typeof props.maxWidth === "number") declarations.push(`max-width: ${props.maxWidth}px`);
|
|
196
|
+
if (typeof props.minHeight === "number") declarations.push(`min-height: ${props.minHeight}px`);
|
|
197
|
+
if (typeof props.maxHeight === "number") declarations.push(`max-height: ${props.maxHeight}px`);
|
|
198
|
+
return declarations;
|
|
199
|
+
}
|
|
200
|
+
function overflowDeclarations(props) {
|
|
201
|
+
if (props.clipsContent) return ["overflow: hidden"];
|
|
202
|
+
if (props.overflowDirection === "HORIZONTAL") return ["overflow-x: auto", "overflow-y: hidden"];
|
|
203
|
+
if (props.overflowDirection === "VERTICAL") return ["overflow-x: hidden", "overflow-y: auto"];
|
|
204
|
+
if (props.overflowDirection === "BOTH") return ["overflow: auto"];
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
function radiusDeclarations(props) {
|
|
208
|
+
if (Array.isArray(props.cornerRadii) && props.cornerRadii.length === 4) {
|
|
209
|
+
return [`border-radius: ${props.cornerRadii.map((value) => `${finite(value, 0)}px`).join(" ")}`];
|
|
210
|
+
}
|
|
211
|
+
if (typeof props.cornerRadius === "number") return [`border-radius: ${props.cornerRadius}px`];
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
function strokeDeclarations(props) {
|
|
215
|
+
const color = paintColor(props.styles.strokes[0]);
|
|
216
|
+
if (!color) return [];
|
|
217
|
+
const style = props.dashPattern?.length ? "dashed" : "solid";
|
|
218
|
+
const declarations = [];
|
|
219
|
+
const sides = [
|
|
220
|
+
["border-top-width", props.strokeTopWeight],
|
|
221
|
+
["border-right-width", props.strokeRightWeight],
|
|
222
|
+
["border-bottom-width", props.strokeBottomWeight],
|
|
223
|
+
["border-left-width", props.strokeLeftWeight]
|
|
224
|
+
];
|
|
225
|
+
const perSide = sides.some(([, value]) => typeof value === "number");
|
|
226
|
+
if (perSide) {
|
|
227
|
+
declarations.push(`border-style: ${style}`, `border-color: ${color}`, "border-width: 0");
|
|
228
|
+
for (const [property, value] of sides) if (typeof value === "number") declarations.push(`${property}: ${value}px`);
|
|
229
|
+
} else {
|
|
230
|
+
const weight = finite(props.strokeWeight, 1);
|
|
231
|
+
if (weight <= 0) return [];
|
|
232
|
+
declarations.push(`border: ${weight}px ${style} ${color}`);
|
|
233
|
+
}
|
|
234
|
+
return declarations;
|
|
235
|
+
}
|
|
236
|
+
function blendModeCss(value) {
|
|
237
|
+
if (!value || value === "PASS_THROUGH" || value === "NORMAL") return void 0;
|
|
238
|
+
return value.toLowerCase().replace(/_/g, "-");
|
|
239
|
+
}
|
|
240
|
+
function textCaseCss(value) {
|
|
241
|
+
if (value === "UPPER") return "uppercase";
|
|
242
|
+
if (value === "LOWER") return "lowercase";
|
|
243
|
+
if (value === "TITLE") return "capitalize";
|
|
244
|
+
return void 0;
|
|
245
|
+
}
|
|
246
|
+
function sanitizeSvg(markup) {
|
|
247
|
+
return markup.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/\son[a-z]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, "");
|
|
248
|
+
}
|
|
249
|
+
function regularPolygon(cx, cy, radius, points, innerRatio) {
|
|
250
|
+
const count = Math.max(3, Math.round(points));
|
|
251
|
+
const coords = [];
|
|
252
|
+
const steps = innerRatio === void 0 ? count : count * 2;
|
|
253
|
+
for (let index = 0; index < steps; index += 1) {
|
|
254
|
+
const ratio = innerRatio === void 0 || index % 2 === 0 ? 1 : innerRatio;
|
|
255
|
+
const angle = -Math.PI / 2 + index * Math.PI * 2 / steps;
|
|
256
|
+
coords.push(`${cx + radius * ratio * Math.cos(angle)},${cy + radius * ratio * Math.sin(angle)}`);
|
|
257
|
+
}
|
|
258
|
+
return coords.join(" ");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/render.ts
|
|
262
|
+
var RenderError = class extends Error {
|
|
263
|
+
issues;
|
|
264
|
+
constructor(message, issues = []) {
|
|
265
|
+
super(message);
|
|
266
|
+
this.name = "RenderError";
|
|
267
|
+
this.issues = issues;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
function render(input, options = {}) {
|
|
271
|
+
const result = validate(input);
|
|
272
|
+
if (result.patch) throw new RenderError("Cannot render a Compact Design patch document.");
|
|
273
|
+
if (!result.valid || !result.document) throw new RenderError("Compact Design document is not valid.", result.issues);
|
|
274
|
+
return renderDocument(result.document, options);
|
|
275
|
+
}
|
|
276
|
+
function renderDocument(document, options = {}) {
|
|
277
|
+
const ctx = createContext(document);
|
|
278
|
+
const css = [baseCss(options.background || "#d8d4cb")];
|
|
279
|
+
const body = document.nodes.map((node) => renderNode(node, ctx, { parent: void 0, parentHasLayout: false, modes: {} }, css)).join("\n");
|
|
280
|
+
const board = boardCss(document.nodes);
|
|
281
|
+
const fonts = fontLinks(ctx.fonts);
|
|
282
|
+
const script = runtimeScript(ctx.timeouts);
|
|
283
|
+
const stylesheet = [board, ...css].join("\n");
|
|
284
|
+
const title = escapeHtml(options.title || document.nodes[0]?.name || "Compact Design");
|
|
285
|
+
const page = `<!doctype html>
|
|
286
|
+
<html lang="en">
|
|
287
|
+
<head>
|
|
288
|
+
<meta charset="utf-8">
|
|
289
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
290
|
+
<title>${title}</title>
|
|
291
|
+
${fonts}
|
|
292
|
+
<style>
|
|
293
|
+
${stylesheet}
|
|
294
|
+
</style>
|
|
295
|
+
</head>
|
|
296
|
+
<body>
|
|
297
|
+
<main class="cd-board">
|
|
298
|
+
${body}
|
|
299
|
+
</main>
|
|
300
|
+
${script}
|
|
301
|
+
</body>
|
|
302
|
+
</html>
|
|
303
|
+
`;
|
|
304
|
+
return { html: page, body, css: stylesheet };
|
|
305
|
+
}
|
|
306
|
+
function createContext(document) {
|
|
307
|
+
const styles = /* @__PURE__ */ new Map();
|
|
308
|
+
for (const style of document.styles) {
|
|
309
|
+
if (style.id) styles.set(style.id, style);
|
|
310
|
+
styles.set(style.name, style);
|
|
311
|
+
}
|
|
312
|
+
const variables = /* @__PURE__ */ new Map();
|
|
313
|
+
for (const collection of document.variables) {
|
|
314
|
+
for (const item of collection.items) {
|
|
315
|
+
const ref = { collection: collection.name, item };
|
|
316
|
+
if (item.id) variables.set(item.id, ref);
|
|
317
|
+
variables.set(item.name, ref);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
document,
|
|
322
|
+
components: indexDocument(document).components,
|
|
323
|
+
styles,
|
|
324
|
+
variables,
|
|
325
|
+
fonts: /* @__PURE__ */ new Set(),
|
|
326
|
+
timeouts: []
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function boardCss(nodes) {
|
|
330
|
+
const minX = Math.min(0, ...nodes.map((node) => node.properties.position.x));
|
|
331
|
+
const minY = Math.min(0, ...nodes.map((node) => node.properties.position.y));
|
|
332
|
+
const width = Math.max(0, ...nodes.map((node) => node.properties.position.x + node.properties.size.width - minX));
|
|
333
|
+
const height = Math.max(0, ...nodes.map((node) => node.properties.position.y + node.properties.size.height - minY));
|
|
334
|
+
return `.cd-board { --cd-origin-x: ${minX}px; --cd-origin-y: ${minY}px; width: ${width}px; height: ${height}px; }`;
|
|
335
|
+
}
|
|
336
|
+
function baseCss(background) {
|
|
337
|
+
return `html, body { margin: 0; background: ${background}; }
|
|
338
|
+
.cd-board { position: relative; margin: 32px auto; }
|
|
339
|
+
.cd-node { box-sizing: border-box; }
|
|
340
|
+
.cd-node[data-type="TEXT"] { white-space: pre-wrap; overflow-wrap: break-word; }
|
|
341
|
+
.cd-node a.cd-hotspot, a.cd-node { color: inherit; text-decoration: none; }
|
|
342
|
+
.cd-shape { display: block; width: 100%; height: 100%; }`;
|
|
343
|
+
}
|
|
344
|
+
function renderNode(node, ctx, state, css) {
|
|
345
|
+
if (node.properties.visible === false) return "";
|
|
346
|
+
const modes = { ...state.modes, ...node.properties.variableModes || {} };
|
|
347
|
+
const instanceOf = node.type === "INSTANCE" && node.properties.componentId ? ctx.components.get(node.properties.componentId) : void 0;
|
|
348
|
+
const visual = instanceOf || node;
|
|
349
|
+
const props = mergeInstance(node, instanceOf);
|
|
350
|
+
noteFonts(props, ctx);
|
|
351
|
+
const id = htmlId(node.id);
|
|
352
|
+
const link = prototypeLink(props.prototype, ctx, id);
|
|
353
|
+
css.push(nodeRule(id, node, props, state, modes, ctx));
|
|
354
|
+
const childrenSource = instanceOf ? instanceOf.children : node.children;
|
|
355
|
+
const childState = {
|
|
356
|
+
parent: instanceOf || node,
|
|
357
|
+
parentHasLayout: Boolean((instanceOf || node).properties.layout?.direction),
|
|
358
|
+
modes
|
|
359
|
+
};
|
|
360
|
+
const inner = [
|
|
361
|
+
shapeMarkup(node.type === "INSTANCE" ? visual.type : node.type, props),
|
|
362
|
+
...childrenSource.map((child) => renderNode(applyInstanceText(child, node), ctx, childState, css))
|
|
363
|
+
].join("");
|
|
364
|
+
const attrs = [
|
|
365
|
+
`id="${escapeHtml(id)}"`,
|
|
366
|
+
`class="cd-node"`,
|
|
367
|
+
`data-type="${escapeHtml(node.type)}"`,
|
|
368
|
+
`data-name="${escapeHtml(node.name)}"`
|
|
369
|
+
];
|
|
370
|
+
if (link.timeout) attrs.push(`data-cd-timeout="${link.timeout.seconds}"`, `data-cd-navigate="${escapeHtml(htmlId(link.timeout.destination))}"`);
|
|
371
|
+
if (link.back) attrs.push(`data-cd-back="true"`);
|
|
372
|
+
const tag = link.href ? "a" : "div";
|
|
373
|
+
if (link.href) attrs.push(`href="${escapeHtml(link.href)}"`);
|
|
374
|
+
if (node.type === "TEXT") return `<${tag} ${attrs.join(" ")}>${textInner(props, ctx)}</${tag}>`;
|
|
375
|
+
if (node.type === "SVG" && props.svg) return `<${tag} ${attrs.join(" ")}>${sanitizeSvg(props.svg)}</${tag}>`;
|
|
376
|
+
return `<${tag} ${attrs.join(" ")}>${inner}</${tag}>`;
|
|
377
|
+
}
|
|
378
|
+
function mergeInstance(node, component) {
|
|
379
|
+
if (!component) return node.properties;
|
|
380
|
+
return {
|
|
381
|
+
...component.properties,
|
|
382
|
+
...node.properties,
|
|
383
|
+
styles: {
|
|
384
|
+
fills: node.properties.styles.fills.length ? node.properties.styles.fills : component.properties.styles.fills,
|
|
385
|
+
strokes: node.properties.styles.strokes.length ? node.properties.styles.strokes : component.properties.styles.strokes,
|
|
386
|
+
effects: node.properties.styles.effects.length ? node.properties.styles.effects : component.properties.styles.effects
|
|
387
|
+
},
|
|
388
|
+
size: node.properties.size,
|
|
389
|
+
position: node.properties.position,
|
|
390
|
+
layout: node.properties.layout || component.properties.layout,
|
|
391
|
+
prototype: node.properties.prototype || component.properties.prototype
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function applyInstanceText(child, instance) {
|
|
395
|
+
const overrides = instance.properties.instanceProperties;
|
|
396
|
+
if (!overrides || child.type !== "TEXT") return child;
|
|
397
|
+
const named = overrides[child.name];
|
|
398
|
+
if (typeof named !== "string") return child;
|
|
399
|
+
return { ...child, properties: { ...child.properties, text: named } };
|
|
400
|
+
}
|
|
401
|
+
function nodeRule(id, node, props, state, modes, ctx) {
|
|
402
|
+
const declarations = [
|
|
403
|
+
...positionDeclarations(props, state),
|
|
404
|
+
...sizeDeclarations(props, state),
|
|
405
|
+
...layoutDeclarations(props),
|
|
406
|
+
...childSizingDeclarations(props, state.parent?.properties.layout?.direction),
|
|
407
|
+
...overflowDeclarations(props),
|
|
408
|
+
...radiusDeclarations(props),
|
|
409
|
+
...strokeDeclarations(resolvedProps(props, ctx, modes)),
|
|
410
|
+
...paintDeclarations(node.type, resolvedProps(props, ctx, modes)),
|
|
411
|
+
...effectDeclarations(props),
|
|
412
|
+
...textDeclarations(node.type, resolvedProps(props, ctx, modes), ctx),
|
|
413
|
+
...variableCustomProperties(props, modes, ctx)
|
|
414
|
+
];
|
|
415
|
+
if (node.type === "ELLIPSE" || node.type === "ARC") declarations.push("border-radius: 50%");
|
|
416
|
+
if (typeof props.opacity === "number") declarations.push(`opacity: ${props.opacity}`);
|
|
417
|
+
if (typeof props.rotation === "number" && props.rotation !== 0) {
|
|
418
|
+
declarations.push("transform-origin: center center", `transform: rotate(${props.rotation}deg)`);
|
|
419
|
+
}
|
|
420
|
+
const blend = blendModeCss(props.blendMode);
|
|
421
|
+
if (blend) declarations.push(`mix-blend-mode: ${blend}`);
|
|
422
|
+
if (props.layoutPositioning === "ABSOLUTE") declarations.push("position: absolute");
|
|
423
|
+
return `#${cssEscape(id)} { ${declarations.filter(Boolean).join("; ")} }`;
|
|
424
|
+
}
|
|
425
|
+
function positionDeclarations(props, state) {
|
|
426
|
+
if (!state.parent) {
|
|
427
|
+
return [
|
|
428
|
+
"position: absolute",
|
|
429
|
+
`left: calc(${props.position.x}px - var(--cd-origin-x))`,
|
|
430
|
+
`top: calc(${props.position.y}px - var(--cd-origin-y))`
|
|
431
|
+
];
|
|
432
|
+
}
|
|
433
|
+
if (state.parentHasLayout && props.layoutPositioning !== "ABSOLUTE") return ["position: relative"];
|
|
434
|
+
const left = props.position.x - state.parent.properties.position.x;
|
|
435
|
+
const top = props.position.y - state.parent.properties.position.y;
|
|
436
|
+
return ["position: absolute", `left: ${left}px`, `top: ${top}px`];
|
|
437
|
+
}
|
|
438
|
+
function sizeDeclarations(props, state) {
|
|
439
|
+
const hugWidth = state.parentHasLayout && props.layoutSizingHorizontal === "HUG";
|
|
440
|
+
const hugHeight = state.parentHasLayout && props.layoutSizingVertical === "HUG";
|
|
441
|
+
const fillWidth = state.parentHasLayout && props.layoutSizingHorizontal === "FILL";
|
|
442
|
+
const fillHeight = state.parentHasLayout && props.layoutSizingVertical === "FILL";
|
|
443
|
+
const declarations = [];
|
|
444
|
+
if (!hugWidth && !fillWidth) declarations.push(`width: ${finite(props.size.width, 0)}px`);
|
|
445
|
+
if (!hugHeight && !fillHeight) declarations.push(`height: ${finite(props.size.height, 0)}px`);
|
|
446
|
+
return declarations;
|
|
447
|
+
}
|
|
448
|
+
function resolvedProps(props, ctx, modes) {
|
|
449
|
+
const fills = [...props.styles.fills];
|
|
450
|
+
const strokes = [...props.styles.strokes];
|
|
451
|
+
const refs = props.styleRefs || {};
|
|
452
|
+
if (refs.fill) {
|
|
453
|
+
const style = ctx.styles.get(refs.fill);
|
|
454
|
+
if (style?.paints?.length) fills.splice(0, fills.length, ...style.paints);
|
|
455
|
+
}
|
|
456
|
+
if (refs.stroke) {
|
|
457
|
+
const style = ctx.styles.get(refs.stroke);
|
|
458
|
+
if (style?.paints?.length) strokes.splice(0, strokes.length, ...style.paints);
|
|
459
|
+
}
|
|
460
|
+
const next = { ...props, styles: { ...props.styles, fills, strokes } };
|
|
461
|
+
const fillBinding = props.bindings?.fill;
|
|
462
|
+
if (fillBinding) {
|
|
463
|
+
const color = resolveColor(fillBinding, modes, ctx);
|
|
464
|
+
if (color) next.styles = { ...next.styles, fills: [{ type: "SOLID", color, opacity: 1 }, ...fills.slice(1)] };
|
|
465
|
+
}
|
|
466
|
+
const strokeBinding = props.bindings?.stroke;
|
|
467
|
+
if (strokeBinding) {
|
|
468
|
+
const color = resolveColor(strokeBinding, modes, ctx);
|
|
469
|
+
if (color) next.styles = { ...next.styles, strokes: [{ type: "SOLID", color, opacity: 1 }, ...strokes.slice(1)] };
|
|
470
|
+
}
|
|
471
|
+
return next;
|
|
472
|
+
}
|
|
473
|
+
function paintDeclarations(type, props) {
|
|
474
|
+
if (type === "TEXT") {
|
|
475
|
+
const color = paintColor(props.styles.fills[0]);
|
|
476
|
+
return color ? [`color: ${color}`] : [];
|
|
477
|
+
}
|
|
478
|
+
if (type === "STAR" || type === "POLYGON" || type === "VECTOR" || type === "SVG") return [];
|
|
479
|
+
const layers = backgroundLayers(props.styles.fills);
|
|
480
|
+
return layers.length ? [`background: ${layers.join(", ")}`] : [];
|
|
481
|
+
}
|
|
482
|
+
function effectDeclarations(props) {
|
|
483
|
+
const declarations = [];
|
|
484
|
+
const shadows = effectShadows(props.styles.effects);
|
|
485
|
+
if (shadows.length) declarations.push(`box-shadow: ${shadows.join(", ")}`);
|
|
486
|
+
const blur = blurFilters(props.styles.effects);
|
|
487
|
+
if (blur.filter) declarations.push(`filter: ${blur.filter}`);
|
|
488
|
+
if (blur.backdrop) declarations.push(`backdrop-filter: ${blur.backdrop}`);
|
|
489
|
+
return declarations;
|
|
490
|
+
}
|
|
491
|
+
function textDeclarations(type, props, ctx) {
|
|
492
|
+
if (type !== "TEXT") return [];
|
|
493
|
+
const font = textFont(props, ctx);
|
|
494
|
+
const declarations = fontDeclarations(font);
|
|
495
|
+
const lineHeight = lineHeightCss(props.lineHeight);
|
|
496
|
+
const letterSpacing = letterSpacingCss(props.letterSpacing);
|
|
497
|
+
if (lineHeight) declarations.push(`line-height: ${lineHeight}`);
|
|
498
|
+
if (letterSpacing) declarations.push(`letter-spacing: ${letterSpacing}`);
|
|
499
|
+
if (props.alignment) declarations.push(`text-align: ${props.alignment.toLowerCase()}`);
|
|
500
|
+
if (props.verticalAlignment === "CENTER") declarations.push("display: flex", "align-items: center");
|
|
501
|
+
if (props.verticalAlignment === "BOTTOM") declarations.push("display: flex", "align-items: flex-end");
|
|
502
|
+
if (props.textDecoration === "UNDERLINE") declarations.push("text-decoration: underline");
|
|
503
|
+
if (props.textDecoration === "STRIKETHROUGH") declarations.push("text-decoration: line-through");
|
|
504
|
+
const transform = textCaseCss(props.textCase);
|
|
505
|
+
if (transform) declarations.push(`text-transform: ${transform}`);
|
|
506
|
+
if (typeof props.paragraphIndent === "number") declarations.push(`text-indent: ${props.paragraphIndent}px`);
|
|
507
|
+
if (props.textTruncation === "ENDING" || props.textAutoResize === "TRUNCATE") {
|
|
508
|
+
declarations.push("overflow: hidden", "text-overflow: ellipsis", "white-space: nowrap");
|
|
509
|
+
}
|
|
510
|
+
if (typeof props.maxLines === "number") {
|
|
511
|
+
declarations.push("display: -webkit-box", "-webkit-box-orient: vertical", `-webkit-line-clamp: ${props.maxLines}`, "overflow: hidden");
|
|
512
|
+
}
|
|
513
|
+
return declarations;
|
|
514
|
+
}
|
|
515
|
+
function textFont(props, ctx) {
|
|
516
|
+
const ref = props.styleRefs?.text;
|
|
517
|
+
if (ref) {
|
|
518
|
+
const style = ctx.styles.get(ref);
|
|
519
|
+
if (style?.font) return style.font;
|
|
520
|
+
}
|
|
521
|
+
return props.font;
|
|
522
|
+
}
|
|
523
|
+
function textInner(props, ctx) {
|
|
524
|
+
if (Array.isArray(props.runs) && props.runs.length) {
|
|
525
|
+
return props.runs.map((run) => {
|
|
526
|
+
if (!isRecord(run)) return "";
|
|
527
|
+
const text = typeof run.text === "string" ? run.text : "";
|
|
528
|
+
const style = [];
|
|
529
|
+
if (isRecord(run.font)) {
|
|
530
|
+
const font = {
|
|
531
|
+
family: typeof run.font.family === "string" ? run.font.family : "Arial",
|
|
532
|
+
style: typeof run.font.style === "string" ? run.font.style : "Regular",
|
|
533
|
+
size: finite(run.font.size, 16)
|
|
534
|
+
};
|
|
535
|
+
ctx.fonts.add(font.family);
|
|
536
|
+
style.push(...fontDeclarations(font));
|
|
537
|
+
}
|
|
538
|
+
const fill = runFill(run.fill);
|
|
539
|
+
if (fill) style.push(`color: ${fill}`);
|
|
540
|
+
return `<span${style.length ? ` style="${escapeHtml(style.join("; "))}"` : ""}>${escapeHtml(text)}</span>`;
|
|
541
|
+
}).join("");
|
|
542
|
+
}
|
|
543
|
+
return escapeHtml(props.text || "");
|
|
544
|
+
}
|
|
545
|
+
function runFill(value) {
|
|
546
|
+
if (typeof value === "string" && /^#/.test(value)) return value;
|
|
547
|
+
if (isDesignColor(value)) return cssColor(value);
|
|
548
|
+
if (isRecord(value) && value.type === "SOLID") {
|
|
549
|
+
const paint = { type: "SOLID", color: isDesignColor(value.color) ? value.color : void 0, opacity: finite(value.opacity, 1) };
|
|
550
|
+
return paintColor(paint);
|
|
551
|
+
}
|
|
552
|
+
return void 0;
|
|
553
|
+
}
|
|
554
|
+
function variableCustomProperties(props, modes, ctx) {
|
|
555
|
+
const declarations = [];
|
|
556
|
+
for (const [collection, mode] of Object.entries(props.variableModes || {})) {
|
|
557
|
+
for (const [key, ref] of ctx.variables) {
|
|
558
|
+
if (ref.collection !== collection) continue;
|
|
559
|
+
if (ref.item.id !== key) continue;
|
|
560
|
+
const value = resolveVariableValue(ref, modes);
|
|
561
|
+
if (isDesignColor(value)) declarations.push(`--cd-${cssVar(ref.item.id || ref.item.name)}: ${cssColor(value)}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const fill = props.bindings?.fill;
|
|
565
|
+
if (fill) declarations.push(`--cd-bound-fill: var(--cd-${cssVar(fill)})`);
|
|
566
|
+
return declarations;
|
|
567
|
+
}
|
|
568
|
+
function cssVar(name) {
|
|
569
|
+
return name.replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
570
|
+
}
|
|
571
|
+
function resolveColor(key, modes, ctx) {
|
|
572
|
+
const ref = ctx.variables.get(key);
|
|
573
|
+
if (!ref) return void 0;
|
|
574
|
+
const value = resolveVariableValue(ref, modes);
|
|
575
|
+
return isDesignColor(value) ? value : void 0;
|
|
576
|
+
}
|
|
577
|
+
function resolveVariableValue(ref, modes) {
|
|
578
|
+
const mode = modes[ref.collection];
|
|
579
|
+
if (mode && ref.item.values && ref.item.values[mode] !== void 0) return ref.item.values[mode];
|
|
580
|
+
if (ref.item.value !== void 0) return ref.item.value;
|
|
581
|
+
if (ref.item.values) {
|
|
582
|
+
const first = Object.values(ref.item.values)[0];
|
|
583
|
+
if (first !== void 0) return first;
|
|
584
|
+
}
|
|
585
|
+
return void 0;
|
|
586
|
+
}
|
|
587
|
+
function noteFonts(props, ctx) {
|
|
588
|
+
if (props.font?.family) ctx.fonts.add(props.font.family);
|
|
589
|
+
const textStyle = props.styleRefs?.text ? ctx.styles.get(props.styleRefs.text) : void 0;
|
|
590
|
+
if (textStyle?.font?.family) ctx.fonts.add(textStyle.font.family);
|
|
591
|
+
for (const run of props.runs || []) {
|
|
592
|
+
if (isRecord(run) && isRecord(run.font) && typeof run.font.family === "string") ctx.fonts.add(run.font.family);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function prototypeLink(value, ctx, sourceId) {
|
|
596
|
+
if (!Array.isArray(value) || !value.length) return {};
|
|
597
|
+
const reaction = isRecord(value[0]) ? value[0] : void 0;
|
|
598
|
+
if (!reaction) return {};
|
|
599
|
+
const trigger = isRecord(reaction.trigger) ? reaction.trigger : {};
|
|
600
|
+
const actions = Array.isArray(reaction.actions) ? reaction.actions : [];
|
|
601
|
+
const action = actions.find(isRecord);
|
|
602
|
+
if (!action) return {};
|
|
603
|
+
const type = String(action.type || "").toUpperCase();
|
|
604
|
+
if (type === "URL" && typeof action.url === "string") return { href: action.url };
|
|
605
|
+
if (type === "BACK") return { href: "#", back: true };
|
|
606
|
+
if ((type === "NAVIGATE" || type === "SWAP" || type === "OVERLAY") && typeof action.destination === "string") {
|
|
607
|
+
if (String(trigger.type || "").toUpperCase() === "AFTER_TIMEOUT") {
|
|
608
|
+
const seconds = finite(trigger.timeout, 1);
|
|
609
|
+
ctx.timeouts.push({ id: sourceId, seconds, destination: action.destination });
|
|
610
|
+
return { timeout: { seconds, destination: action.destination } };
|
|
611
|
+
}
|
|
612
|
+
return { href: `#${htmlId(action.destination)}` };
|
|
613
|
+
}
|
|
614
|
+
return {};
|
|
615
|
+
}
|
|
616
|
+
function shapeMarkup(type, props) {
|
|
617
|
+
const fill = paintColor(props.styles.fills[0]) || "currentColor";
|
|
618
|
+
if (type === "ELLIPSE" || type === "ARC") {
|
|
619
|
+
return "";
|
|
620
|
+
}
|
|
621
|
+
if (type === "POLYGON" || type === "STAR") {
|
|
622
|
+
const width = Math.max(1, finite(props.size.width, 1));
|
|
623
|
+
const height = Math.max(1, finite(props.size.height, 1));
|
|
624
|
+
const points = regularPolygon(width / 2, height / 2, Math.min(width, height) / 2, finite(props.pointCount, type === "STAR" ? 5 : 6), type === "STAR" ? finite(props.innerRadius, 0.5) : void 0);
|
|
625
|
+
return `<svg class="cd-shape" viewBox="0 0 ${width} ${height}" aria-hidden="true"><polygon points="${points}" fill="${escapeHtml(fill)}"/></svg>`;
|
|
626
|
+
}
|
|
627
|
+
if (type === "VECTOR" && props.vectorPaths?.length) {
|
|
628
|
+
const width = Math.max(1, finite(props.size.width, 1));
|
|
629
|
+
const height = Math.max(1, finite(props.size.height, 1));
|
|
630
|
+
const paths = props.vectorPaths.map((path) => `<path d="${escapeHtml(path.data)}" fill-rule="${path.windingRule === "EVENODD" ? "evenodd" : "nonzero"}" fill="${escapeHtml(fill)}"/>`).join("");
|
|
631
|
+
return `<svg class="cd-shape" viewBox="0 0 ${width} ${height}" aria-hidden="true">${paths}</svg>`;
|
|
632
|
+
}
|
|
633
|
+
return "";
|
|
634
|
+
}
|
|
635
|
+
function fontLinks(families) {
|
|
636
|
+
const custom = [...families].filter((family) => !isWebSafeFont(family));
|
|
637
|
+
if (!custom.length) return "";
|
|
638
|
+
const query = custom.map((family) => `family=${encodeURIComponent(family)}:wght@400;500;600;700`).join("&");
|
|
639
|
+
return `<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
640
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
641
|
+
<link href="https://fonts.googleapis.com/css2?${query}&display=swap" rel="stylesheet">`;
|
|
642
|
+
}
|
|
643
|
+
function runtimeScript(timeouts) {
|
|
644
|
+
if (!timeouts.length) {
|
|
645
|
+
return `<script>
|
|
646
|
+
(() => {
|
|
647
|
+
const stack = [];
|
|
648
|
+
addEventListener("hashchange", () => stack.push(location.hash));
|
|
649
|
+
document.querySelectorAll("[data-cd-back]").forEach((node) => {
|
|
650
|
+
node.addEventListener("click", (event) => {
|
|
651
|
+
event.preventDefault();
|
|
652
|
+
history.back();
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
})();
|
|
656
|
+
</script>`;
|
|
657
|
+
}
|
|
658
|
+
return `<script>
|
|
659
|
+
(() => {
|
|
660
|
+
document.querySelectorAll("[data-cd-timeout]").forEach((node) => {
|
|
661
|
+
const seconds = Number(node.getAttribute("data-cd-timeout"));
|
|
662
|
+
const destination = node.getAttribute("data-cd-navigate");
|
|
663
|
+
if (!destination) return;
|
|
664
|
+
setTimeout(() => { location.hash = destination; }, Math.max(0, seconds) * 1000);
|
|
665
|
+
});
|
|
666
|
+
document.querySelectorAll("[data-cd-back]").forEach((node) => {
|
|
667
|
+
node.addEventListener("click", (event) => {
|
|
668
|
+
event.preventDefault();
|
|
669
|
+
history.back();
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
})();
|
|
673
|
+
</script>`;
|
|
674
|
+
}
|
|
675
|
+
export {
|
|
676
|
+
RenderError,
|
|
677
|
+
render,
|
|
678
|
+
renderDocument
|
|
679
|
+
};
|