@typecaast/capture 0.0.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 +201 -0
- package/dist/chunk-2UORYZUZ.js +80 -0
- package/dist/chunk-2UORYZUZ.js.map +1 -0
- package/dist/chunk-MJRHEKPU.js +569 -0
- package/dist/chunk-MJRHEKPU.js.map +1 -0
- package/dist/distill-DviDU75P.d.ts +37 -0
- package/dist/distill-kGI5Nt9q.d.cts +37 -0
- package/dist/draft.cjs +85 -0
- package/dist/draft.cjs.map +1 -0
- package/dist/draft.d.cts +94 -0
- package/dist/draft.d.ts +94 -0
- package/dist/draft.js +3 -0
- package/dist/draft.js.map +1 -0
- package/dist/import-page.cjs +624 -0
- package/dist/import-page.cjs.map +1 -0
- package/dist/import-page.d.cts +26 -0
- package/dist/import-page.d.ts +26 -0
- package/dist/import-page.js +53 -0
- package/dist/import-page.js.map +1 -0
- package/dist/index.cjs +841 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +68 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +193 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var createDOMPurify = require('dompurify');
|
|
4
|
+
var zod = require('zod');
|
|
5
|
+
var react = require('react');
|
|
6
|
+
var reactDom = require('react-dom');
|
|
7
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
8
|
+
|
|
9
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
|
|
11
|
+
var createDOMPurify__default = /*#__PURE__*/_interopDefault(createDOMPurify);
|
|
12
|
+
|
|
13
|
+
// src/sanitize.ts
|
|
14
|
+
var ALLOWED_TAGS = [
|
|
15
|
+
"div",
|
|
16
|
+
"span",
|
|
17
|
+
"p",
|
|
18
|
+
"a",
|
|
19
|
+
"b",
|
|
20
|
+
"strong",
|
|
21
|
+
"i",
|
|
22
|
+
"em",
|
|
23
|
+
"u",
|
|
24
|
+
"s",
|
|
25
|
+
"small",
|
|
26
|
+
"sub",
|
|
27
|
+
"sup",
|
|
28
|
+
"br",
|
|
29
|
+
"hr",
|
|
30
|
+
"ul",
|
|
31
|
+
"ol",
|
|
32
|
+
"li",
|
|
33
|
+
"img",
|
|
34
|
+
"svg",
|
|
35
|
+
"path",
|
|
36
|
+
"g",
|
|
37
|
+
"circle",
|
|
38
|
+
"rect",
|
|
39
|
+
"line",
|
|
40
|
+
"polyline",
|
|
41
|
+
"polygon",
|
|
42
|
+
"ellipse",
|
|
43
|
+
"defs",
|
|
44
|
+
"use",
|
|
45
|
+
"title",
|
|
46
|
+
"h1",
|
|
47
|
+
"h2",
|
|
48
|
+
"h3",
|
|
49
|
+
"h4",
|
|
50
|
+
"h5",
|
|
51
|
+
"h6",
|
|
52
|
+
"blockquote",
|
|
53
|
+
"pre",
|
|
54
|
+
"code",
|
|
55
|
+
"time",
|
|
56
|
+
"header",
|
|
57
|
+
"footer",
|
|
58
|
+
"section",
|
|
59
|
+
"article",
|
|
60
|
+
"aside",
|
|
61
|
+
"nav",
|
|
62
|
+
"figure",
|
|
63
|
+
"figcaption",
|
|
64
|
+
"table",
|
|
65
|
+
"thead",
|
|
66
|
+
"tbody",
|
|
67
|
+
"tr",
|
|
68
|
+
"td",
|
|
69
|
+
"th"
|
|
70
|
+
];
|
|
71
|
+
var ALLOWED_ATTR = [
|
|
72
|
+
"class",
|
|
73
|
+
"style",
|
|
74
|
+
"role",
|
|
75
|
+
"alt",
|
|
76
|
+
"src",
|
|
77
|
+
"srcset",
|
|
78
|
+
"width",
|
|
79
|
+
"height",
|
|
80
|
+
"dir",
|
|
81
|
+
"title",
|
|
82
|
+
"viewbox",
|
|
83
|
+
"fill",
|
|
84
|
+
"stroke",
|
|
85
|
+
"stroke-width",
|
|
86
|
+
"d",
|
|
87
|
+
"points",
|
|
88
|
+
"cx",
|
|
89
|
+
"cy",
|
|
90
|
+
"r",
|
|
91
|
+
"x",
|
|
92
|
+
"y",
|
|
93
|
+
"x1",
|
|
94
|
+
"x2",
|
|
95
|
+
"y1",
|
|
96
|
+
"y2",
|
|
97
|
+
"rx",
|
|
98
|
+
"ry",
|
|
99
|
+
"transform",
|
|
100
|
+
"xmlns",
|
|
101
|
+
"href"
|
|
102
|
+
];
|
|
103
|
+
function scrubCss(css) {
|
|
104
|
+
let out = css;
|
|
105
|
+
out = out.replace(/expression\s*\(/gi, "/*blocked*/(");
|
|
106
|
+
out = out.replace(/-moz-binding\s*:/gi, "/*blocked*/:");
|
|
107
|
+
out = out.replace(/behavior\s*:/gi, "/*blocked*/:");
|
|
108
|
+
out = out.replace(/@import[^;]*;?/gi, "/*blocked-import*/");
|
|
109
|
+
out = out.replace(/url\(\s*(['"]?)([^)'"]*)\1\s*\)/gi, (whole, _q, uri) => {
|
|
110
|
+
const u = String(uri).trim().toLowerCase();
|
|
111
|
+
const blocked = u.startsWith("javascript:") || u.startsWith("vbscript:") || u.startsWith("data:") && !u.startsWith("data:image/");
|
|
112
|
+
return blocked ? "none" : whole;
|
|
113
|
+
});
|
|
114
|
+
out = out.replace(/javascript:/gi, "blocked:");
|
|
115
|
+
out = out.replace(/vbscript:/gi, "blocked:");
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
function getWindow(win) {
|
|
119
|
+
if (win) return win;
|
|
120
|
+
if (typeof window !== "undefined") return window;
|
|
121
|
+
throw new Error(
|
|
122
|
+
"sanitizeHtml requires a DOM window \u2014 pass { window } in non-browser environments."
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
var hooked = null;
|
|
126
|
+
function purifierFor(win) {
|
|
127
|
+
const purify = createDOMPurify__default.default(win);
|
|
128
|
+
if (hooked !== purify) {
|
|
129
|
+
purify.addHook("afterSanitizeAttributes", (node) => {
|
|
130
|
+
const el = node;
|
|
131
|
+
const style = el.getAttribute?.("style");
|
|
132
|
+
if (style) el.setAttribute("style", scrubCss(style));
|
|
133
|
+
const src = el.getAttribute?.("src");
|
|
134
|
+
if (src) {
|
|
135
|
+
const u = src.trim().toLowerCase();
|
|
136
|
+
if (u.startsWith("data:") && !u.startsWith("data:image/")) {
|
|
137
|
+
el.removeAttribute("src");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (el.tagName === "A") {
|
|
141
|
+
el.removeAttribute("href");
|
|
142
|
+
el.setAttribute("role", "link");
|
|
143
|
+
}
|
|
144
|
+
if (el.getAttribute?.("target")) el.removeAttribute("target");
|
|
145
|
+
});
|
|
146
|
+
hooked = purify;
|
|
147
|
+
}
|
|
148
|
+
return purify;
|
|
149
|
+
}
|
|
150
|
+
function sanitizeHtml(html, opts = {}) {
|
|
151
|
+
const win = getWindow(opts.window);
|
|
152
|
+
const purify = purifierFor(win);
|
|
153
|
+
return purify.sanitize(html, {
|
|
154
|
+
ALLOWED_TAGS,
|
|
155
|
+
ALLOWED_ATTR,
|
|
156
|
+
// Keep our slot marker even though data-* is otherwise dropped.
|
|
157
|
+
ADD_ATTR: ["data-tc-slot"],
|
|
158
|
+
ALLOW_DATA_ATTR: false,
|
|
159
|
+
ALLOW_ARIA_ATTR: true,
|
|
160
|
+
FORBID_TAGS: [
|
|
161
|
+
"script",
|
|
162
|
+
"style",
|
|
163
|
+
"iframe",
|
|
164
|
+
"object",
|
|
165
|
+
"embed",
|
|
166
|
+
"form",
|
|
167
|
+
"input",
|
|
168
|
+
"button",
|
|
169
|
+
"textarea",
|
|
170
|
+
"select",
|
|
171
|
+
"link",
|
|
172
|
+
"meta",
|
|
173
|
+
"base",
|
|
174
|
+
"video",
|
|
175
|
+
"audio",
|
|
176
|
+
"source"
|
|
177
|
+
],
|
|
178
|
+
FORBID_ATTR: ["srcdoc", "formaction", "xlink:href"]
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
var SLOT_TOKENS = {
|
|
182
|
+
messages: "{{messages}}",
|
|
183
|
+
author: "{{author}}",
|
|
184
|
+
avatar: "{{avatar}}",
|
|
185
|
+
body: "{{body}}",
|
|
186
|
+
time: "{{time}}",
|
|
187
|
+
composer: "{{composer}}"
|
|
188
|
+
};
|
|
189
|
+
var slotReportSchema = zod.z.object({
|
|
190
|
+
/** Whether this region was found at all. */
|
|
191
|
+
found: zod.z.boolean(),
|
|
192
|
+
/** Which inner slots were auto-detected (e.g. `["author","body"]`). */
|
|
193
|
+
detected: zod.z.array(zod.z.string()),
|
|
194
|
+
/** Heuristic confidence 0..1 for the auto-detection. */
|
|
195
|
+
confidence: zod.z.number().min(0).max(1)
|
|
196
|
+
});
|
|
197
|
+
var tokenSetSchema = zod.z.object({
|
|
198
|
+
colors: zod.z.record(zod.z.string(), zod.z.string()),
|
|
199
|
+
fonts: zod.z.record(zod.z.string(), zod.z.string()).optional(),
|
|
200
|
+
space: zod.z.record(zod.z.string(), zod.z.string()).optional(),
|
|
201
|
+
radius: zod.z.record(zod.z.string(), zod.z.string()).optional()
|
|
202
|
+
});
|
|
203
|
+
var skinDraftSchema = zod.z.object({
|
|
204
|
+
version: zod.z.literal(1),
|
|
205
|
+
meta: zod.z.object({
|
|
206
|
+
/** Human label, defaulted from the source title/host. */
|
|
207
|
+
name: zod.z.string(),
|
|
208
|
+
/** Source page URL, when known (informational only). */
|
|
209
|
+
sourceUrl: zod.z.string().optional(),
|
|
210
|
+
/** Theme the capture was taken under, when known. */
|
|
211
|
+
theme: zod.z.enum(["light", "dark"]).optional(),
|
|
212
|
+
/** Suggested canvas from the captured element's box. */
|
|
213
|
+
canvas: zod.z.object({ width: zod.z.number().int(), height: zod.z.number().int() }).optional()
|
|
214
|
+
}),
|
|
215
|
+
/**
|
|
216
|
+
* Slotted, sanitized HTML per region. Elements carry inline `style`
|
|
217
|
+
* attributes; `css` holds anything inline styles can't express.
|
|
218
|
+
*/
|
|
219
|
+
slots: zod.z.object({
|
|
220
|
+
frame: zod.z.string().optional(),
|
|
221
|
+
message: zod.z.string().optional(),
|
|
222
|
+
composer: zod.z.string().optional(),
|
|
223
|
+
typing: zod.z.string().optional()
|
|
224
|
+
}),
|
|
225
|
+
/** Best-effort extra CSS (pseudo-elements, keyframes) — may be empty. */
|
|
226
|
+
css: zod.z.string(),
|
|
227
|
+
/** Extracted design tokens (best effort) — the primary/captured theme. */
|
|
228
|
+
tokens: tokenSetSchema,
|
|
229
|
+
/**
|
|
230
|
+
* Dark-theme tokens from a paired capture (M5.7 double-capture flow). When
|
|
231
|
+
* present, the skin supports both themes and switches CSS vars by theme.
|
|
232
|
+
*/
|
|
233
|
+
darkTokens: tokenSetSchema.optional(),
|
|
234
|
+
/** Detection report per region. */
|
|
235
|
+
detection: zod.z.object({
|
|
236
|
+
frame: slotReportSchema,
|
|
237
|
+
message: slotReportSchema,
|
|
238
|
+
composer: slotReportSchema,
|
|
239
|
+
typing: slotReportSchema
|
|
240
|
+
}),
|
|
241
|
+
/** Human-readable warnings (hidden content dropped, slots missing, …). */
|
|
242
|
+
warnings: zod.z.array(zod.z.string())
|
|
243
|
+
});
|
|
244
|
+
function detectionScore(draft) {
|
|
245
|
+
const checks = [
|
|
246
|
+
draft.detection.message.detected.includes("body"),
|
|
247
|
+
draft.detection.message.detected.includes("author"),
|
|
248
|
+
draft.detection.message.detected.includes("avatar"),
|
|
249
|
+
draft.detection.message.detected.includes("time"),
|
|
250
|
+
draft.detection.composer.found
|
|
251
|
+
];
|
|
252
|
+
return checks.filter(Boolean).length / checks.length;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/tokens.ts
|
|
256
|
+
function parseStyle(style) {
|
|
257
|
+
const m = /* @__PURE__ */ new Map();
|
|
258
|
+
for (const decl of style.split(";")) {
|
|
259
|
+
const idx = decl.indexOf(":");
|
|
260
|
+
if (idx === -1) continue;
|
|
261
|
+
const prop = decl.slice(0, idx).trim().toLowerCase();
|
|
262
|
+
const val = decl.slice(idx + 1).trim();
|
|
263
|
+
if (prop && val) m.set(prop, val);
|
|
264
|
+
}
|
|
265
|
+
return m;
|
|
266
|
+
}
|
|
267
|
+
var COLOR_RE = /(#[0-9a-f]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\))/gi;
|
|
268
|
+
function extractTokens(root) {
|
|
269
|
+
const colorFreq = /* @__PURE__ */ new Map();
|
|
270
|
+
const fonts = /* @__PURE__ */ new Set();
|
|
271
|
+
const radii = /* @__PURE__ */ new Set();
|
|
272
|
+
const spaces = /* @__PURE__ */ new Set();
|
|
273
|
+
const bump = (raw) => {
|
|
274
|
+
const v = raw.trim();
|
|
275
|
+
if (!v || v === "transparent" || v === "inherit" || v === "currentcolor")
|
|
276
|
+
return;
|
|
277
|
+
colorFreq.set(v, (colorFreq.get(v) ?? 0) + 1);
|
|
278
|
+
};
|
|
279
|
+
for (const el of [root, ...root.querySelectorAll("*")]) {
|
|
280
|
+
const style = el.getAttribute("style");
|
|
281
|
+
if (!style) continue;
|
|
282
|
+
const decls = parseStyle(style);
|
|
283
|
+
for (const [prop, val] of decls) {
|
|
284
|
+
if (prop === "color" || prop.startsWith("background")) {
|
|
285
|
+
const matches = val.match(COLOR_RE);
|
|
286
|
+
if (matches) for (const c of matches) bump(c);
|
|
287
|
+
}
|
|
288
|
+
if (prop === "font-family") fonts.add(val);
|
|
289
|
+
if (prop === "border-radius") radii.add(val);
|
|
290
|
+
if (prop === "padding" || prop === "gap") spaces.add(val);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const colors = {};
|
|
294
|
+
[...colorFreq.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12).forEach(([c], i) => {
|
|
295
|
+
colors[`color-${i + 1}`] = c;
|
|
296
|
+
});
|
|
297
|
+
const out = { colors };
|
|
298
|
+
if (fonts.size) {
|
|
299
|
+
out.fonts = {};
|
|
300
|
+
[...fonts].slice(0, 4).forEach((f, i) => {
|
|
301
|
+
out.fonts[`font-${i + 1}`] = f;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
if (radii.size) {
|
|
305
|
+
out.radius = {};
|
|
306
|
+
[...radii].slice(0, 6).forEach((r, i) => {
|
|
307
|
+
out.radius[`radius-${i + 1}`] = r;
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
if (spaces.size) {
|
|
311
|
+
out.space = {};
|
|
312
|
+
[...spaces].slice(0, 8).forEach((s, i) => {
|
|
313
|
+
out.space[`space-${i + 1}`] = s;
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/distill.ts
|
|
320
|
+
var SLOT_ATTR = "data-tc-slot";
|
|
321
|
+
var INLINE_PROPS = [
|
|
322
|
+
"color",
|
|
323
|
+
"background-color",
|
|
324
|
+
"background",
|
|
325
|
+
"font-family",
|
|
326
|
+
"font-size",
|
|
327
|
+
"font-weight",
|
|
328
|
+
"font-style",
|
|
329
|
+
"line-height",
|
|
330
|
+
"letter-spacing",
|
|
331
|
+
"text-align",
|
|
332
|
+
"text-transform",
|
|
333
|
+
"border",
|
|
334
|
+
"border-radius",
|
|
335
|
+
"padding",
|
|
336
|
+
"margin",
|
|
337
|
+
"gap",
|
|
338
|
+
"display",
|
|
339
|
+
"flex-direction",
|
|
340
|
+
"align-items",
|
|
341
|
+
"justify-content",
|
|
342
|
+
"box-shadow",
|
|
343
|
+
"opacity"
|
|
344
|
+
];
|
|
345
|
+
var HIDDEN_CLASS_RE = /\b(sr-only|visually-hidden|hidden)\b/;
|
|
346
|
+
function getWindow2(win) {
|
|
347
|
+
if (win) return win;
|
|
348
|
+
if (typeof window !== "undefined") return window;
|
|
349
|
+
throw new Error("distill requires a DOM window \u2014 pass { window } in Node.");
|
|
350
|
+
}
|
|
351
|
+
function isHidden(el, win) {
|
|
352
|
+
if (el.getAttribute("aria-hidden") === "true") return true;
|
|
353
|
+
if (el.hasAttribute("hidden")) return true;
|
|
354
|
+
const cls = el.getAttribute("class") ?? "";
|
|
355
|
+
if (HIDDEN_CLASS_RE.test(cls)) return true;
|
|
356
|
+
const inline = (el.getAttribute("style") ?? "").toLowerCase();
|
|
357
|
+
if (/display\s*:\s*none/.test(inline)) return true;
|
|
358
|
+
if (/visibility\s*:\s*hidden/.test(inline)) return true;
|
|
359
|
+
const cs = win.getComputedStyle?.(el);
|
|
360
|
+
if (cs) {
|
|
361
|
+
if (cs.display === "none") return true;
|
|
362
|
+
if (cs.visibility === "hidden") return true;
|
|
363
|
+
}
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
function inlineStyles(orig, clone, win) {
|
|
367
|
+
const cs = win.getComputedStyle?.(orig);
|
|
368
|
+
if (!cs) return;
|
|
369
|
+
const decls = [];
|
|
370
|
+
for (const prop of INLINE_PROPS) {
|
|
371
|
+
const v = cs.getPropertyValue(prop);
|
|
372
|
+
if (v && v !== "none" && v !== "normal" && v.trim() !== "")
|
|
373
|
+
decls.push(`${prop}: ${v}`);
|
|
374
|
+
}
|
|
375
|
+
if (decls.length) clone.setAttribute("style", decls.join("; "));
|
|
376
|
+
}
|
|
377
|
+
function pruneAndInline(orig, clone, win, inline, dropped) {
|
|
378
|
+
if (inline) inlineStyles(orig, clone, win);
|
|
379
|
+
for (const attr of [...clone.attributes]) {
|
|
380
|
+
if (attr.name.startsWith("data-")) clone.removeAttribute(attr.name);
|
|
381
|
+
}
|
|
382
|
+
const origKids = [...orig.children];
|
|
383
|
+
const cloneKids = [...clone.children];
|
|
384
|
+
const remove = [];
|
|
385
|
+
for (let i = 0; i < origKids.length; i++) {
|
|
386
|
+
const o = origKids[i];
|
|
387
|
+
const c = cloneKids[i];
|
|
388
|
+
if (!o || !c) continue;
|
|
389
|
+
if (isHidden(o, win)) {
|
|
390
|
+
remove.push(c);
|
|
391
|
+
dropped.count++;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
pruneAndInline(o, c, win, inline, dropped);
|
|
395
|
+
}
|
|
396
|
+
for (const c of remove) c.remove();
|
|
397
|
+
}
|
|
398
|
+
function signature(el) {
|
|
399
|
+
const tag = el.tagName.toLowerCase();
|
|
400
|
+
const classes = (el.getAttribute("class") ?? "").split(/\s+/).filter(Boolean).sort().join(".");
|
|
401
|
+
const kidTags = [...el.children].map((c) => c.tagName.toLowerCase()).join(",");
|
|
402
|
+
return `${tag}|${classes}|${kidTags}`;
|
|
403
|
+
}
|
|
404
|
+
function findRepeatingRows(root) {
|
|
405
|
+
let best = null;
|
|
406
|
+
let bestScore = 0;
|
|
407
|
+
const visit = (el) => {
|
|
408
|
+
const groups = /* @__PURE__ */ new Map();
|
|
409
|
+
for (const child of el.children) {
|
|
410
|
+
const sig = signature(child);
|
|
411
|
+
const arr = groups.get(sig) ?? [];
|
|
412
|
+
arr.push(child);
|
|
413
|
+
groups.set(sig, arr);
|
|
414
|
+
}
|
|
415
|
+
for (const rows of groups.values()) {
|
|
416
|
+
if (rows.length < 2) continue;
|
|
417
|
+
const textLen = rows.reduce(
|
|
418
|
+
(n, r) => n + (r.textContent ?? "").trim().length,
|
|
419
|
+
0
|
|
420
|
+
);
|
|
421
|
+
const score = rows.length * 10 + Math.min(textLen, 400);
|
|
422
|
+
if (score > bestScore) {
|
|
423
|
+
bestScore = score;
|
|
424
|
+
best = { container: el, rows };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
for (const child of el.children) visit(child);
|
|
428
|
+
};
|
|
429
|
+
visit(root);
|
|
430
|
+
return best;
|
|
431
|
+
}
|
|
432
|
+
var AUTHOR_RE = /\b(name|author|sender|user|handle|username|display-?name)\b/i;
|
|
433
|
+
var AVATAR_RE = /\b(avatar|photo|userpic|profile-?pic|pic|gravatar)\b/i;
|
|
434
|
+
var TIME_RE = /\b(time|timestamp|date|ago|sent-?at)\b/i;
|
|
435
|
+
var BODY_RE = /\b(body|text|content|message|bubble|markdown|prose)\b/i;
|
|
436
|
+
var TIME_TEXT_RE = /^\s*(\d{1,2}:\d{2}(\s?[ap]\.?m\.?)?|\d+\s*(m|min|h|hr|d|days?|hours?|minutes?)( ago)?|yesterday|today)\s*$/i;
|
|
437
|
+
function classOf(el) {
|
|
438
|
+
return el.getAttribute("class") ?? "";
|
|
439
|
+
}
|
|
440
|
+
function markSlot(el, slot, token) {
|
|
441
|
+
el.setAttribute(SLOT_ATTR, slot);
|
|
442
|
+
el.textContent = token;
|
|
443
|
+
}
|
|
444
|
+
function slotifyRow(row) {
|
|
445
|
+
const detected = [];
|
|
446
|
+
const all = [...row.querySelectorAll("*")];
|
|
447
|
+
let avatar = row.querySelector("img") ?? all.find((e) => AVATAR_RE.test(classOf(e))) ?? null;
|
|
448
|
+
if (avatar) {
|
|
449
|
+
if (avatar.tagName === "IMG") {
|
|
450
|
+
const div = row.ownerDocument.createElement("div");
|
|
451
|
+
const cls = classOf(avatar);
|
|
452
|
+
if (cls) div.setAttribute("class", cls);
|
|
453
|
+
const st = avatar.getAttribute("style");
|
|
454
|
+
if (st) div.setAttribute("style", st);
|
|
455
|
+
avatar.replaceWith(div);
|
|
456
|
+
avatar = div;
|
|
457
|
+
}
|
|
458
|
+
markSlot(avatar, "avatar", "{{avatar}}");
|
|
459
|
+
detected.push("avatar");
|
|
460
|
+
}
|
|
461
|
+
let author = all.find(
|
|
462
|
+
(e) => e !== avatar && AUTHOR_RE.test(classOf(e)) && (e.textContent ?? "").trim()
|
|
463
|
+
);
|
|
464
|
+
if (!author) {
|
|
465
|
+
author = all.find((e) => {
|
|
466
|
+
if (e === avatar || e.getAttribute(SLOT_ATTR)) return false;
|
|
467
|
+
const t = (e.textContent ?? "").trim();
|
|
468
|
+
const fw = (e.getAttribute("style") ?? "").match(
|
|
469
|
+
/font-weight:\s*(\d+|bold)/i
|
|
470
|
+
);
|
|
471
|
+
const bold = e.tagName === "B" || e.tagName === "STRONG" || (fw ? fw[1] === "bold" || Number(fw[1]) >= 600 : false);
|
|
472
|
+
return bold && t.length > 0 && t.length < 40;
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (author && !author.getAttribute(SLOT_ATTR)) {
|
|
476
|
+
markSlot(author, "author", "{{author}}");
|
|
477
|
+
detected.push("author");
|
|
478
|
+
}
|
|
479
|
+
const time = row.querySelector("time") ?? all.find((e) => !e.getAttribute(SLOT_ATTR) && TIME_RE.test(classOf(e))) ?? all.find(
|
|
480
|
+
(e) => !e.getAttribute(SLOT_ATTR) && e.children.length === 0 && TIME_TEXT_RE.test((e.textContent ?? "").trim())
|
|
481
|
+
);
|
|
482
|
+
if (time && !time.getAttribute(SLOT_ATTR)) {
|
|
483
|
+
markSlot(time, "time", "{{time}}");
|
|
484
|
+
detected.push("time");
|
|
485
|
+
}
|
|
486
|
+
const named = all.find(
|
|
487
|
+
(e) => !e.getAttribute(SLOT_ATTR) && !e.querySelector(`[${SLOT_ATTR}]`) && BODY_RE.test(classOf(e))
|
|
488
|
+
);
|
|
489
|
+
let body = named ?? null;
|
|
490
|
+
if (!body) {
|
|
491
|
+
let bestLen = -1;
|
|
492
|
+
for (const e of all) {
|
|
493
|
+
if (e.getAttribute(SLOT_ATTR)) continue;
|
|
494
|
+
if (e.querySelector(`[${SLOT_ATTR}]`)) continue;
|
|
495
|
+
const t = (e.textContent ?? "").trim();
|
|
496
|
+
if (t.length > bestLen) {
|
|
497
|
+
bestLen = t.length;
|
|
498
|
+
body = e;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (body && !body.getAttribute(SLOT_ATTR)) {
|
|
503
|
+
markSlot(body, "body", "{{body}}");
|
|
504
|
+
detected.push("body");
|
|
505
|
+
} else if (!body) {
|
|
506
|
+
markSlot(row, "body", "{{body}}");
|
|
507
|
+
detected.push("body");
|
|
508
|
+
}
|
|
509
|
+
return { html: row.outerHTML, detected };
|
|
510
|
+
}
|
|
511
|
+
var COMPOSER_RE = /\b(composer|compose|reply|message-?box|message-?input|input-?box|textbox|editor|prompt)\b/i;
|
|
512
|
+
function findComposer(root, exclude) {
|
|
513
|
+
const candidates = [...root.querySelectorAll("*")].filter(
|
|
514
|
+
(e) => !exclude.contains(e) && !e.contains(exclude)
|
|
515
|
+
);
|
|
516
|
+
return candidates.find((e) => e.getAttribute("role") === "textbox") ?? candidates.find((e) => e.hasAttribute("contenteditable")) ?? candidates.find((e) => COMPOSER_RE.test(classOf(e))) ?? null;
|
|
517
|
+
}
|
|
518
|
+
function emptyReport() {
|
|
519
|
+
return { found: false, detected: [], confidence: 0 };
|
|
520
|
+
}
|
|
521
|
+
function distill(root, opts = {}) {
|
|
522
|
+
const win = getWindow2(opts.window);
|
|
523
|
+
const doc = win.document;
|
|
524
|
+
const warnings = [];
|
|
525
|
+
const clone = root.cloneNode(true);
|
|
526
|
+
const dropped = { count: 0 };
|
|
527
|
+
pruneAndInline(root, clone, win, opts.inlineComputedStyles ?? false, dropped);
|
|
528
|
+
if (dropped.count > 0)
|
|
529
|
+
warnings.push(
|
|
530
|
+
`Dropped ${dropped.count} hidden element(s) from the capture.`
|
|
531
|
+
);
|
|
532
|
+
const safe = sanitizeHtml(clone.outerHTML, { window: win });
|
|
533
|
+
const host = doc.createElement("div");
|
|
534
|
+
host.innerHTML = safe;
|
|
535
|
+
const frameRoot = host.firstElementChild ?? host;
|
|
536
|
+
const pick = findRepeatingRows(frameRoot);
|
|
537
|
+
const detection = {
|
|
538
|
+
frame: emptyReport(),
|
|
539
|
+
message: emptyReport(),
|
|
540
|
+
composer: emptyReport(),
|
|
541
|
+
typing: emptyReport()
|
|
542
|
+
};
|
|
543
|
+
let messageHtml;
|
|
544
|
+
let frameHtml;
|
|
545
|
+
let composerHtml;
|
|
546
|
+
if (pick) {
|
|
547
|
+
const sample = pick.rows[0].cloneNode(true);
|
|
548
|
+
const slotted = slotifyRow(sample);
|
|
549
|
+
messageHtml = slotted.html;
|
|
550
|
+
detection.message = {
|
|
551
|
+
found: true,
|
|
552
|
+
detected: slotted.detected,
|
|
553
|
+
confidence: Math.min(1, slotted.detected.length / 4)
|
|
554
|
+
};
|
|
555
|
+
const slot = doc.createElement("div");
|
|
556
|
+
slot.setAttribute(SLOT_ATTR, "messages");
|
|
557
|
+
slot.textContent = "{{messages}}";
|
|
558
|
+
const listCls = classOf(pick.container);
|
|
559
|
+
if (listCls) slot.setAttribute("class", `${listCls} tc-messages`);
|
|
560
|
+
const containerClone = pick.container.cloneNode(false);
|
|
561
|
+
if (containerClone.getAttribute("style"))
|
|
562
|
+
slot.setAttribute(
|
|
563
|
+
"style",
|
|
564
|
+
containerClone.getAttribute("style")
|
|
565
|
+
);
|
|
566
|
+
const frameClone = frameRoot.cloneNode(true);
|
|
567
|
+
const path = pathTo(frameRoot, pick.container);
|
|
568
|
+
const targetInClone = path ? nodeAtPath(frameClone, path) : null;
|
|
569
|
+
if (targetInClone && targetInClone.parentElement) {
|
|
570
|
+
targetInClone.replaceWith(slot);
|
|
571
|
+
frameHtml = frameClone.outerHTML;
|
|
572
|
+
detection.frame = { found: true, detected: ["messages"], confidence: 1 };
|
|
573
|
+
} else {
|
|
574
|
+
frameHtml = slot.outerHTML;
|
|
575
|
+
detection.frame = {
|
|
576
|
+
found: true,
|
|
577
|
+
detected: ["messages"],
|
|
578
|
+
confidence: 0.5
|
|
579
|
+
};
|
|
580
|
+
warnings.push(
|
|
581
|
+
"Message list is the captured root; no surrounding chrome was found."
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
const composer = findComposer(frameRoot, pick.container);
|
|
585
|
+
if (composer) {
|
|
586
|
+
const c = composer.cloneNode(true);
|
|
587
|
+
c.setAttribute(SLOT_ATTR, "composer");
|
|
588
|
+
c.removeAttribute("contenteditable");
|
|
589
|
+
c.textContent = "{{composer}}";
|
|
590
|
+
composerHtml = c.outerHTML;
|
|
591
|
+
detection.composer = {
|
|
592
|
+
found: true,
|
|
593
|
+
detected: ["composer"],
|
|
594
|
+
confidence: 1
|
|
595
|
+
};
|
|
596
|
+
} else {
|
|
597
|
+
warnings.push(
|
|
598
|
+
"No composer detected \u2014 add one by hand if the skin needs it."
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
} else {
|
|
602
|
+
warnings.push(
|
|
603
|
+
"No repeating message row found \u2014 capture a tighter subtree around the thread."
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
const tokens = extractTokens(frameRoot);
|
|
607
|
+
let canvas;
|
|
608
|
+
const rect = root.getBoundingClientRect?.();
|
|
609
|
+
if (rect && rect.width > 0 && rect.height > 0) {
|
|
610
|
+
canvas = { width: Math.round(rect.width), height: Math.round(rect.height) };
|
|
611
|
+
}
|
|
612
|
+
return {
|
|
613
|
+
version: 1,
|
|
614
|
+
meta: {
|
|
615
|
+
name: opts.name ?? "Captured skin",
|
|
616
|
+
...opts.sourceUrl ? { sourceUrl: opts.sourceUrl } : {},
|
|
617
|
+
...opts.theme ? { theme: opts.theme } : {},
|
|
618
|
+
...canvas ? { canvas } : {}
|
|
619
|
+
},
|
|
620
|
+
slots: {
|
|
621
|
+
...frameHtml ? { frame: frameHtml } : {},
|
|
622
|
+
...messageHtml ? { message: messageHtml } : {},
|
|
623
|
+
...composerHtml ? { composer: composerHtml } : {}
|
|
624
|
+
},
|
|
625
|
+
css: "",
|
|
626
|
+
tokens,
|
|
627
|
+
detection,
|
|
628
|
+
warnings
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function pathTo(root, target) {
|
|
632
|
+
if (root === target) return [];
|
|
633
|
+
for (let i = 0; i < root.children.length; i++) {
|
|
634
|
+
const child = root.children[i];
|
|
635
|
+
if (!child) continue;
|
|
636
|
+
const sub = pathTo(child, target);
|
|
637
|
+
if (sub) return [i, ...sub];
|
|
638
|
+
}
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
function nodeAtPath(root, path) {
|
|
642
|
+
let node = root;
|
|
643
|
+
for (const i of path) {
|
|
644
|
+
if (!node) return null;
|
|
645
|
+
node = node.children[i] ?? null;
|
|
646
|
+
}
|
|
647
|
+
return node;
|
|
648
|
+
}
|
|
649
|
+
function slug(s) {
|
|
650
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "captured";
|
|
651
|
+
}
|
|
652
|
+
function contentToText(content) {
|
|
653
|
+
const out = [];
|
|
654
|
+
for (const node of content) {
|
|
655
|
+
if (node.type === "text" && Array.isArray(node.spans)) {
|
|
656
|
+
for (const span of node.spans) {
|
|
657
|
+
out.push(span.value ?? span.label ?? "");
|
|
658
|
+
}
|
|
659
|
+
} else if (node.type === "image") {
|
|
660
|
+
out.push(node.alt ?? "\u{1F5BC}");
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return out.join("");
|
|
664
|
+
}
|
|
665
|
+
function initials(name) {
|
|
666
|
+
return name.split(/\s+/).filter(Boolean).slice(0, 2).map((w) => w[0]?.toUpperCase() ?? "").join("");
|
|
667
|
+
}
|
|
668
|
+
function fmtTime(atMs) {
|
|
669
|
+
const total = Math.floor(atMs / 1e3);
|
|
670
|
+
const m = Math.floor(total / 60);
|
|
671
|
+
const s = total % 60;
|
|
672
|
+
return `${m}:${String(s).padStart(2, "0")}`;
|
|
673
|
+
}
|
|
674
|
+
function styleText(tokens, css) {
|
|
675
|
+
const vars = Object.entries(tokens.colors ?? {}).map(([k, v]) => `--${k}: ${v};`).join(" ");
|
|
676
|
+
return `:host{all:initial; display:block; width:100%; height:100%; ${vars}}
|
|
677
|
+
*{box-sizing:border-box;}
|
|
678
|
+
${css}`;
|
|
679
|
+
}
|
|
680
|
+
function fillInto(host, templateHtml, values) {
|
|
681
|
+
host.innerHTML = templateHtml;
|
|
682
|
+
for (const node of host.querySelectorAll(`[${SLOT_ATTR}]`)) {
|
|
683
|
+
const slot = node.getAttribute(SLOT_ATTR);
|
|
684
|
+
if (slot && slot in values) node.textContent = values[slot] ?? "";
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
function revealStyle(progress) {
|
|
688
|
+
const p = Math.max(0, Math.min(1, progress));
|
|
689
|
+
return {
|
|
690
|
+
opacity: p,
|
|
691
|
+
transform: `translateY(${(1 - p) * 6}px)`,
|
|
692
|
+
willChange: "opacity, transform"
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
var DEFAULT_CAPS = {
|
|
696
|
+
events: {},
|
|
697
|
+
content: {},
|
|
698
|
+
reactions: false,
|
|
699
|
+
threads: false,
|
|
700
|
+
readReceipts: false
|
|
701
|
+
};
|
|
702
|
+
function templateSkinFromDraft(draft, opts = {}) {
|
|
703
|
+
const safe = {
|
|
704
|
+
frame: draft.slots.frame ? sanitizeHtml(draft.slots.frame) : void 0,
|
|
705
|
+
message: draft.slots.message ? sanitizeHtml(draft.slots.message) : void 0,
|
|
706
|
+
composer: draft.slots.composer ? sanitizeHtml(draft.slots.composer) : void 0
|
|
707
|
+
};
|
|
708
|
+
const lightTokens = { colors: draft.tokens.colors ?? {} };
|
|
709
|
+
const darkTokens = draft.darkTokens ? { colors: draft.darkTokens.colors ?? {} } : lightTokens;
|
|
710
|
+
const cssByTheme = {
|
|
711
|
+
light: styleText(lightTokens, draft.css ?? ""),
|
|
712
|
+
dark: styleText(darkTokens, draft.css ?? "")
|
|
713
|
+
};
|
|
714
|
+
const supportsThemes = draft.darkTokens ? ["light", "dark"] : [draft.meta.theme ?? "light"];
|
|
715
|
+
const Frame = ({
|
|
716
|
+
theme,
|
|
717
|
+
children
|
|
718
|
+
}) => {
|
|
719
|
+
const hostRef = react.useRef(null);
|
|
720
|
+
const [mount, setMount] = react.useState(null);
|
|
721
|
+
react.useLayoutEffect(() => {
|
|
722
|
+
const host = hostRef.current;
|
|
723
|
+
if (!host) return;
|
|
724
|
+
const shadow = host.shadowRoot ?? host.attachShadow({ mode: "open" });
|
|
725
|
+
shadow.innerHTML = "";
|
|
726
|
+
const style = host.ownerDocument.createElement("style");
|
|
727
|
+
style.textContent = theme === "dark" ? cssByTheme.dark : cssByTheme.light;
|
|
728
|
+
shadow.appendChild(style);
|
|
729
|
+
const wrapper = host.ownerDocument.createElement("div");
|
|
730
|
+
wrapper.style.width = "100%";
|
|
731
|
+
wrapper.style.height = "100%";
|
|
732
|
+
wrapper.innerHTML = safe.frame ?? `<div ${SLOT_ATTR}="messages"></div>`;
|
|
733
|
+
shadow.appendChild(wrapper);
|
|
734
|
+
const slot = wrapper.querySelector(`[${SLOT_ATTR}="messages"]`) ?? wrapper;
|
|
735
|
+
slot.textContent = "";
|
|
736
|
+
setMount(slot);
|
|
737
|
+
}, [theme]);
|
|
738
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: hostRef, style: { width: "100%", height: "100%" }, children: mount ? reactDom.createPortal(children, mount) : null });
|
|
739
|
+
};
|
|
740
|
+
const Message = ({ message, author }) => {
|
|
741
|
+
const ref = react.useRef(null);
|
|
742
|
+
react.useLayoutEffect(() => {
|
|
743
|
+
const el = ref.current;
|
|
744
|
+
if (!el || !safe.message) return;
|
|
745
|
+
fillInto(el, safe.message, {
|
|
746
|
+
author: author.name,
|
|
747
|
+
avatar: initials(author.name),
|
|
748
|
+
body: contentToText(message.content),
|
|
749
|
+
time: fmtTime(message.atMs)
|
|
750
|
+
});
|
|
751
|
+
}, [message, author]);
|
|
752
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, style: revealStyle(message.revealProgress) });
|
|
753
|
+
};
|
|
754
|
+
const SystemMessage = ({ message }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
755
|
+
"div",
|
|
756
|
+
{
|
|
757
|
+
style: { ...revealStyle(message.revealProgress), textAlign: "center" },
|
|
758
|
+
children: contentToText(message.content)
|
|
759
|
+
}
|
|
760
|
+
);
|
|
761
|
+
const TypingIndicator = ({ author }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { opacity: 0.7, fontStyle: "italic" }, children: [
|
|
762
|
+
author.name,
|
|
763
|
+
" is typing\u2026"
|
|
764
|
+
] });
|
|
765
|
+
const Composer = ({ composer }) => {
|
|
766
|
+
const ref = react.useRef(null);
|
|
767
|
+
react.useLayoutEffect(() => {
|
|
768
|
+
const el = ref.current;
|
|
769
|
+
if (!el) return;
|
|
770
|
+
if (safe.composer) {
|
|
771
|
+
fillInto(el, safe.composer, { composer: composer.text });
|
|
772
|
+
} else {
|
|
773
|
+
el.textContent = composer.text;
|
|
774
|
+
}
|
|
775
|
+
}, [composer]);
|
|
776
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { ref });
|
|
777
|
+
};
|
|
778
|
+
const Avatar = ({
|
|
779
|
+
participant,
|
|
780
|
+
size = 36
|
|
781
|
+
}) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
782
|
+
"div",
|
|
783
|
+
{
|
|
784
|
+
style: {
|
|
785
|
+
width: size,
|
|
786
|
+
height: size,
|
|
787
|
+
borderRadius: "50%",
|
|
788
|
+
display: "grid",
|
|
789
|
+
placeItems: "center",
|
|
790
|
+
background: "var(--color-1, #ccc)",
|
|
791
|
+
fontSize: size * 0.4
|
|
792
|
+
},
|
|
793
|
+
children: initials(participant.name)
|
|
794
|
+
}
|
|
795
|
+
);
|
|
796
|
+
const components = {
|
|
797
|
+
Frame,
|
|
798
|
+
Message,
|
|
799
|
+
SystemMessage,
|
|
800
|
+
TypingIndicator,
|
|
801
|
+
Reaction: () => null,
|
|
802
|
+
Composer,
|
|
803
|
+
Avatar
|
|
804
|
+
};
|
|
805
|
+
return {
|
|
806
|
+
id: opts.id ?? slug(draft.meta.name),
|
|
807
|
+
meta: {
|
|
808
|
+
name: draft.meta.name,
|
|
809
|
+
defaultCanvas: draft.meta.canvas ?? { width: 420, height: 720 },
|
|
810
|
+
supportsThemes,
|
|
811
|
+
capabilities: opts.capabilities ?? DEFAULT_CAPS
|
|
812
|
+
},
|
|
813
|
+
components,
|
|
814
|
+
tokens: { light: lightTokens, dark: darkTokens }
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/merge.ts
|
|
819
|
+
function mergeThemeDrafts(light, dark) {
|
|
820
|
+
return {
|
|
821
|
+
...light,
|
|
822
|
+
meta: { ...light.meta, theme: void 0 },
|
|
823
|
+
tokens: light.tokens,
|
|
824
|
+
darkTokens: dark.tokens,
|
|
825
|
+
warnings: [
|
|
826
|
+
...light.warnings,
|
|
827
|
+
...dark.detection.message.found ? [] : ["Dark capture found no message row; reusing light structure."]
|
|
828
|
+
]
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
exports.SLOT_TOKENS = SLOT_TOKENS;
|
|
833
|
+
exports.detectionScore = detectionScore;
|
|
834
|
+
exports.distill = distill;
|
|
835
|
+
exports.mergeThemeDrafts = mergeThemeDrafts;
|
|
836
|
+
exports.sanitizeHtml = sanitizeHtml;
|
|
837
|
+
exports.scrubCss = scrubCss;
|
|
838
|
+
exports.skinDraftSchema = skinDraftSchema;
|
|
839
|
+
exports.templateSkinFromDraft = templateSkinFromDraft;
|
|
840
|
+
//# sourceMappingURL=index.cjs.map
|
|
841
|
+
//# sourceMappingURL=index.cjs.map
|