@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
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var jsdom = require('jsdom');
|
|
4
|
+
var createDOMPurify = require('dompurify');
|
|
5
|
+
|
|
6
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var createDOMPurify__default = /*#__PURE__*/_interopDefault(createDOMPurify);
|
|
9
|
+
|
|
10
|
+
// src/import-page.ts
|
|
11
|
+
var ALLOWED_TAGS = [
|
|
12
|
+
"div",
|
|
13
|
+
"span",
|
|
14
|
+
"p",
|
|
15
|
+
"a",
|
|
16
|
+
"b",
|
|
17
|
+
"strong",
|
|
18
|
+
"i",
|
|
19
|
+
"em",
|
|
20
|
+
"u",
|
|
21
|
+
"s",
|
|
22
|
+
"small",
|
|
23
|
+
"sub",
|
|
24
|
+
"sup",
|
|
25
|
+
"br",
|
|
26
|
+
"hr",
|
|
27
|
+
"ul",
|
|
28
|
+
"ol",
|
|
29
|
+
"li",
|
|
30
|
+
"img",
|
|
31
|
+
"svg",
|
|
32
|
+
"path",
|
|
33
|
+
"g",
|
|
34
|
+
"circle",
|
|
35
|
+
"rect",
|
|
36
|
+
"line",
|
|
37
|
+
"polyline",
|
|
38
|
+
"polygon",
|
|
39
|
+
"ellipse",
|
|
40
|
+
"defs",
|
|
41
|
+
"use",
|
|
42
|
+
"title",
|
|
43
|
+
"h1",
|
|
44
|
+
"h2",
|
|
45
|
+
"h3",
|
|
46
|
+
"h4",
|
|
47
|
+
"h5",
|
|
48
|
+
"h6",
|
|
49
|
+
"blockquote",
|
|
50
|
+
"pre",
|
|
51
|
+
"code",
|
|
52
|
+
"time",
|
|
53
|
+
"header",
|
|
54
|
+
"footer",
|
|
55
|
+
"section",
|
|
56
|
+
"article",
|
|
57
|
+
"aside",
|
|
58
|
+
"nav",
|
|
59
|
+
"figure",
|
|
60
|
+
"figcaption",
|
|
61
|
+
"table",
|
|
62
|
+
"thead",
|
|
63
|
+
"tbody",
|
|
64
|
+
"tr",
|
|
65
|
+
"td",
|
|
66
|
+
"th"
|
|
67
|
+
];
|
|
68
|
+
var ALLOWED_ATTR = [
|
|
69
|
+
"class",
|
|
70
|
+
"style",
|
|
71
|
+
"role",
|
|
72
|
+
"alt",
|
|
73
|
+
"src",
|
|
74
|
+
"srcset",
|
|
75
|
+
"width",
|
|
76
|
+
"height",
|
|
77
|
+
"dir",
|
|
78
|
+
"title",
|
|
79
|
+
"viewbox",
|
|
80
|
+
"fill",
|
|
81
|
+
"stroke",
|
|
82
|
+
"stroke-width",
|
|
83
|
+
"d",
|
|
84
|
+
"points",
|
|
85
|
+
"cx",
|
|
86
|
+
"cy",
|
|
87
|
+
"r",
|
|
88
|
+
"x",
|
|
89
|
+
"y",
|
|
90
|
+
"x1",
|
|
91
|
+
"x2",
|
|
92
|
+
"y1",
|
|
93
|
+
"y2",
|
|
94
|
+
"rx",
|
|
95
|
+
"ry",
|
|
96
|
+
"transform",
|
|
97
|
+
"xmlns",
|
|
98
|
+
"href"
|
|
99
|
+
];
|
|
100
|
+
function scrubCss(css) {
|
|
101
|
+
let out = css;
|
|
102
|
+
out = out.replace(/expression\s*\(/gi, "/*blocked*/(");
|
|
103
|
+
out = out.replace(/-moz-binding\s*:/gi, "/*blocked*/:");
|
|
104
|
+
out = out.replace(/behavior\s*:/gi, "/*blocked*/:");
|
|
105
|
+
out = out.replace(/@import[^;]*;?/gi, "/*blocked-import*/");
|
|
106
|
+
out = out.replace(/url\(\s*(['"]?)([^)'"]*)\1\s*\)/gi, (whole, _q, uri) => {
|
|
107
|
+
const u = String(uri).trim().toLowerCase();
|
|
108
|
+
const blocked = u.startsWith("javascript:") || u.startsWith("vbscript:") || u.startsWith("data:") && !u.startsWith("data:image/");
|
|
109
|
+
return blocked ? "none" : whole;
|
|
110
|
+
});
|
|
111
|
+
out = out.replace(/javascript:/gi, "blocked:");
|
|
112
|
+
out = out.replace(/vbscript:/gi, "blocked:");
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
function getWindow(win) {
|
|
116
|
+
if (win) return win;
|
|
117
|
+
if (typeof window !== "undefined") return window;
|
|
118
|
+
throw new Error(
|
|
119
|
+
"sanitizeHtml requires a DOM window \u2014 pass { window } in non-browser environments."
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
var hooked = null;
|
|
123
|
+
function purifierFor(win) {
|
|
124
|
+
const purify = createDOMPurify__default.default(win);
|
|
125
|
+
if (hooked !== purify) {
|
|
126
|
+
purify.addHook("afterSanitizeAttributes", (node) => {
|
|
127
|
+
const el = node;
|
|
128
|
+
const style = el.getAttribute?.("style");
|
|
129
|
+
if (style) el.setAttribute("style", scrubCss(style));
|
|
130
|
+
const src = el.getAttribute?.("src");
|
|
131
|
+
if (src) {
|
|
132
|
+
const u = src.trim().toLowerCase();
|
|
133
|
+
if (u.startsWith("data:") && !u.startsWith("data:image/")) {
|
|
134
|
+
el.removeAttribute("src");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (el.tagName === "A") {
|
|
138
|
+
el.removeAttribute("href");
|
|
139
|
+
el.setAttribute("role", "link");
|
|
140
|
+
}
|
|
141
|
+
if (el.getAttribute?.("target")) el.removeAttribute("target");
|
|
142
|
+
});
|
|
143
|
+
hooked = purify;
|
|
144
|
+
}
|
|
145
|
+
return purify;
|
|
146
|
+
}
|
|
147
|
+
function sanitizeHtml(html, opts = {}) {
|
|
148
|
+
const win = getWindow(opts.window);
|
|
149
|
+
const purify = purifierFor(win);
|
|
150
|
+
return purify.sanitize(html, {
|
|
151
|
+
ALLOWED_TAGS,
|
|
152
|
+
ALLOWED_ATTR,
|
|
153
|
+
// Keep our slot marker even though data-* is otherwise dropped.
|
|
154
|
+
ADD_ATTR: ["data-tc-slot"],
|
|
155
|
+
ALLOW_DATA_ATTR: false,
|
|
156
|
+
ALLOW_ARIA_ATTR: true,
|
|
157
|
+
FORBID_TAGS: [
|
|
158
|
+
"script",
|
|
159
|
+
"style",
|
|
160
|
+
"iframe",
|
|
161
|
+
"object",
|
|
162
|
+
"embed",
|
|
163
|
+
"form",
|
|
164
|
+
"input",
|
|
165
|
+
"button",
|
|
166
|
+
"textarea",
|
|
167
|
+
"select",
|
|
168
|
+
"link",
|
|
169
|
+
"meta",
|
|
170
|
+
"base",
|
|
171
|
+
"video",
|
|
172
|
+
"audio",
|
|
173
|
+
"source"
|
|
174
|
+
],
|
|
175
|
+
FORBID_ATTR: ["srcdoc", "formaction", "xlink:href"]
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/tokens.ts
|
|
180
|
+
function parseStyle(style) {
|
|
181
|
+
const m = /* @__PURE__ */ new Map();
|
|
182
|
+
for (const decl of style.split(";")) {
|
|
183
|
+
const idx = decl.indexOf(":");
|
|
184
|
+
if (idx === -1) continue;
|
|
185
|
+
const prop = decl.slice(0, idx).trim().toLowerCase();
|
|
186
|
+
const val = decl.slice(idx + 1).trim();
|
|
187
|
+
if (prop && val) m.set(prop, val);
|
|
188
|
+
}
|
|
189
|
+
return m;
|
|
190
|
+
}
|
|
191
|
+
var COLOR_RE = /(#[0-9a-f]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\))/gi;
|
|
192
|
+
function extractTokens(root) {
|
|
193
|
+
const colorFreq = /* @__PURE__ */ new Map();
|
|
194
|
+
const fonts = /* @__PURE__ */ new Set();
|
|
195
|
+
const radii = /* @__PURE__ */ new Set();
|
|
196
|
+
const spaces = /* @__PURE__ */ new Set();
|
|
197
|
+
const bump = (raw) => {
|
|
198
|
+
const v = raw.trim();
|
|
199
|
+
if (!v || v === "transparent" || v === "inherit" || v === "currentcolor")
|
|
200
|
+
return;
|
|
201
|
+
colorFreq.set(v, (colorFreq.get(v) ?? 0) + 1);
|
|
202
|
+
};
|
|
203
|
+
for (const el of [root, ...root.querySelectorAll("*")]) {
|
|
204
|
+
const style = el.getAttribute("style");
|
|
205
|
+
if (!style) continue;
|
|
206
|
+
const decls = parseStyle(style);
|
|
207
|
+
for (const [prop, val] of decls) {
|
|
208
|
+
if (prop === "color" || prop.startsWith("background")) {
|
|
209
|
+
const matches = val.match(COLOR_RE);
|
|
210
|
+
if (matches) for (const c of matches) bump(c);
|
|
211
|
+
}
|
|
212
|
+
if (prop === "font-family") fonts.add(val);
|
|
213
|
+
if (prop === "border-radius") radii.add(val);
|
|
214
|
+
if (prop === "padding" || prop === "gap") spaces.add(val);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const colors = {};
|
|
218
|
+
[...colorFreq.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12).forEach(([c], i) => {
|
|
219
|
+
colors[`color-${i + 1}`] = c;
|
|
220
|
+
});
|
|
221
|
+
const out = { colors };
|
|
222
|
+
if (fonts.size) {
|
|
223
|
+
out.fonts = {};
|
|
224
|
+
[...fonts].slice(0, 4).forEach((f, i) => {
|
|
225
|
+
out.fonts[`font-${i + 1}`] = f;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (radii.size) {
|
|
229
|
+
out.radius = {};
|
|
230
|
+
[...radii].slice(0, 6).forEach((r, i) => {
|
|
231
|
+
out.radius[`radius-${i + 1}`] = r;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (spaces.size) {
|
|
235
|
+
out.space = {};
|
|
236
|
+
[...spaces].slice(0, 8).forEach((s, i) => {
|
|
237
|
+
out.space[`space-${i + 1}`] = s;
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/distill.ts
|
|
244
|
+
var SLOT_ATTR = "data-tc-slot";
|
|
245
|
+
var INLINE_PROPS = [
|
|
246
|
+
"color",
|
|
247
|
+
"background-color",
|
|
248
|
+
"background",
|
|
249
|
+
"font-family",
|
|
250
|
+
"font-size",
|
|
251
|
+
"font-weight",
|
|
252
|
+
"font-style",
|
|
253
|
+
"line-height",
|
|
254
|
+
"letter-spacing",
|
|
255
|
+
"text-align",
|
|
256
|
+
"text-transform",
|
|
257
|
+
"border",
|
|
258
|
+
"border-radius",
|
|
259
|
+
"padding",
|
|
260
|
+
"margin",
|
|
261
|
+
"gap",
|
|
262
|
+
"display",
|
|
263
|
+
"flex-direction",
|
|
264
|
+
"align-items",
|
|
265
|
+
"justify-content",
|
|
266
|
+
"box-shadow",
|
|
267
|
+
"opacity"
|
|
268
|
+
];
|
|
269
|
+
var HIDDEN_CLASS_RE = /\b(sr-only|visually-hidden|hidden)\b/;
|
|
270
|
+
function getWindow2(win) {
|
|
271
|
+
if (win) return win;
|
|
272
|
+
if (typeof window !== "undefined") return window;
|
|
273
|
+
throw new Error("distill requires a DOM window \u2014 pass { window } in Node.");
|
|
274
|
+
}
|
|
275
|
+
function isHidden(el, win) {
|
|
276
|
+
if (el.getAttribute("aria-hidden") === "true") return true;
|
|
277
|
+
if (el.hasAttribute("hidden")) return true;
|
|
278
|
+
const cls = el.getAttribute("class") ?? "";
|
|
279
|
+
if (HIDDEN_CLASS_RE.test(cls)) return true;
|
|
280
|
+
const inline = (el.getAttribute("style") ?? "").toLowerCase();
|
|
281
|
+
if (/display\s*:\s*none/.test(inline)) return true;
|
|
282
|
+
if (/visibility\s*:\s*hidden/.test(inline)) return true;
|
|
283
|
+
const cs = win.getComputedStyle?.(el);
|
|
284
|
+
if (cs) {
|
|
285
|
+
if (cs.display === "none") return true;
|
|
286
|
+
if (cs.visibility === "hidden") return true;
|
|
287
|
+
}
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
function inlineStyles(orig, clone, win) {
|
|
291
|
+
const cs = win.getComputedStyle?.(orig);
|
|
292
|
+
if (!cs) return;
|
|
293
|
+
const decls = [];
|
|
294
|
+
for (const prop of INLINE_PROPS) {
|
|
295
|
+
const v = cs.getPropertyValue(prop);
|
|
296
|
+
if (v && v !== "none" && v !== "normal" && v.trim() !== "")
|
|
297
|
+
decls.push(`${prop}: ${v}`);
|
|
298
|
+
}
|
|
299
|
+
if (decls.length) clone.setAttribute("style", decls.join("; "));
|
|
300
|
+
}
|
|
301
|
+
function pruneAndInline(orig, clone, win, inline, dropped) {
|
|
302
|
+
if (inline) inlineStyles(orig, clone, win);
|
|
303
|
+
for (const attr of [...clone.attributes]) {
|
|
304
|
+
if (attr.name.startsWith("data-")) clone.removeAttribute(attr.name);
|
|
305
|
+
}
|
|
306
|
+
const origKids = [...orig.children];
|
|
307
|
+
const cloneKids = [...clone.children];
|
|
308
|
+
const remove = [];
|
|
309
|
+
for (let i = 0; i < origKids.length; i++) {
|
|
310
|
+
const o = origKids[i];
|
|
311
|
+
const c = cloneKids[i];
|
|
312
|
+
if (!o || !c) continue;
|
|
313
|
+
if (isHidden(o, win)) {
|
|
314
|
+
remove.push(c);
|
|
315
|
+
dropped.count++;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
pruneAndInline(o, c, win, inline, dropped);
|
|
319
|
+
}
|
|
320
|
+
for (const c of remove) c.remove();
|
|
321
|
+
}
|
|
322
|
+
function signature(el) {
|
|
323
|
+
const tag = el.tagName.toLowerCase();
|
|
324
|
+
const classes = (el.getAttribute("class") ?? "").split(/\s+/).filter(Boolean).sort().join(".");
|
|
325
|
+
const kidTags = [...el.children].map((c) => c.tagName.toLowerCase()).join(",");
|
|
326
|
+
return `${tag}|${classes}|${kidTags}`;
|
|
327
|
+
}
|
|
328
|
+
function findRepeatingRows(root) {
|
|
329
|
+
let best = null;
|
|
330
|
+
let bestScore = 0;
|
|
331
|
+
const visit = (el) => {
|
|
332
|
+
const groups = /* @__PURE__ */ new Map();
|
|
333
|
+
for (const child of el.children) {
|
|
334
|
+
const sig = signature(child);
|
|
335
|
+
const arr = groups.get(sig) ?? [];
|
|
336
|
+
arr.push(child);
|
|
337
|
+
groups.set(sig, arr);
|
|
338
|
+
}
|
|
339
|
+
for (const rows of groups.values()) {
|
|
340
|
+
if (rows.length < 2) continue;
|
|
341
|
+
const textLen = rows.reduce(
|
|
342
|
+
(n, r) => n + (r.textContent ?? "").trim().length,
|
|
343
|
+
0
|
|
344
|
+
);
|
|
345
|
+
const score = rows.length * 10 + Math.min(textLen, 400);
|
|
346
|
+
if (score > bestScore) {
|
|
347
|
+
bestScore = score;
|
|
348
|
+
best = { container: el, rows };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
for (const child of el.children) visit(child);
|
|
352
|
+
};
|
|
353
|
+
visit(root);
|
|
354
|
+
return best;
|
|
355
|
+
}
|
|
356
|
+
var AUTHOR_RE = /\b(name|author|sender|user|handle|username|display-?name)\b/i;
|
|
357
|
+
var AVATAR_RE = /\b(avatar|photo|userpic|profile-?pic|pic|gravatar)\b/i;
|
|
358
|
+
var TIME_RE = /\b(time|timestamp|date|ago|sent-?at)\b/i;
|
|
359
|
+
var BODY_RE = /\b(body|text|content|message|bubble|markdown|prose)\b/i;
|
|
360
|
+
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;
|
|
361
|
+
function classOf(el) {
|
|
362
|
+
return el.getAttribute("class") ?? "";
|
|
363
|
+
}
|
|
364
|
+
function markSlot(el, slot, token) {
|
|
365
|
+
el.setAttribute(SLOT_ATTR, slot);
|
|
366
|
+
el.textContent = token;
|
|
367
|
+
}
|
|
368
|
+
function slotifyRow(row) {
|
|
369
|
+
const detected = [];
|
|
370
|
+
const all = [...row.querySelectorAll("*")];
|
|
371
|
+
let avatar = row.querySelector("img") ?? all.find((e) => AVATAR_RE.test(classOf(e))) ?? null;
|
|
372
|
+
if (avatar) {
|
|
373
|
+
if (avatar.tagName === "IMG") {
|
|
374
|
+
const div = row.ownerDocument.createElement("div");
|
|
375
|
+
const cls = classOf(avatar);
|
|
376
|
+
if (cls) div.setAttribute("class", cls);
|
|
377
|
+
const st = avatar.getAttribute("style");
|
|
378
|
+
if (st) div.setAttribute("style", st);
|
|
379
|
+
avatar.replaceWith(div);
|
|
380
|
+
avatar = div;
|
|
381
|
+
}
|
|
382
|
+
markSlot(avatar, "avatar", "{{avatar}}");
|
|
383
|
+
detected.push("avatar");
|
|
384
|
+
}
|
|
385
|
+
let author = all.find(
|
|
386
|
+
(e) => e !== avatar && AUTHOR_RE.test(classOf(e)) && (e.textContent ?? "").trim()
|
|
387
|
+
);
|
|
388
|
+
if (!author) {
|
|
389
|
+
author = all.find((e) => {
|
|
390
|
+
if (e === avatar || e.getAttribute(SLOT_ATTR)) return false;
|
|
391
|
+
const t = (e.textContent ?? "").trim();
|
|
392
|
+
const fw = (e.getAttribute("style") ?? "").match(
|
|
393
|
+
/font-weight:\s*(\d+|bold)/i
|
|
394
|
+
);
|
|
395
|
+
const bold = e.tagName === "B" || e.tagName === "STRONG" || (fw ? fw[1] === "bold" || Number(fw[1]) >= 600 : false);
|
|
396
|
+
return bold && t.length > 0 && t.length < 40;
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
if (author && !author.getAttribute(SLOT_ATTR)) {
|
|
400
|
+
markSlot(author, "author", "{{author}}");
|
|
401
|
+
detected.push("author");
|
|
402
|
+
}
|
|
403
|
+
const time = row.querySelector("time") ?? all.find((e) => !e.getAttribute(SLOT_ATTR) && TIME_RE.test(classOf(e))) ?? all.find(
|
|
404
|
+
(e) => !e.getAttribute(SLOT_ATTR) && e.children.length === 0 && TIME_TEXT_RE.test((e.textContent ?? "").trim())
|
|
405
|
+
);
|
|
406
|
+
if (time && !time.getAttribute(SLOT_ATTR)) {
|
|
407
|
+
markSlot(time, "time", "{{time}}");
|
|
408
|
+
detected.push("time");
|
|
409
|
+
}
|
|
410
|
+
const named = all.find(
|
|
411
|
+
(e) => !e.getAttribute(SLOT_ATTR) && !e.querySelector(`[${SLOT_ATTR}]`) && BODY_RE.test(classOf(e))
|
|
412
|
+
);
|
|
413
|
+
let body = named ?? null;
|
|
414
|
+
if (!body) {
|
|
415
|
+
let bestLen = -1;
|
|
416
|
+
for (const e of all) {
|
|
417
|
+
if (e.getAttribute(SLOT_ATTR)) continue;
|
|
418
|
+
if (e.querySelector(`[${SLOT_ATTR}]`)) continue;
|
|
419
|
+
const t = (e.textContent ?? "").trim();
|
|
420
|
+
if (t.length > bestLen) {
|
|
421
|
+
bestLen = t.length;
|
|
422
|
+
body = e;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (body && !body.getAttribute(SLOT_ATTR)) {
|
|
427
|
+
markSlot(body, "body", "{{body}}");
|
|
428
|
+
detected.push("body");
|
|
429
|
+
} else if (!body) {
|
|
430
|
+
markSlot(row, "body", "{{body}}");
|
|
431
|
+
detected.push("body");
|
|
432
|
+
}
|
|
433
|
+
return { html: row.outerHTML, detected };
|
|
434
|
+
}
|
|
435
|
+
var COMPOSER_RE = /\b(composer|compose|reply|message-?box|message-?input|input-?box|textbox|editor|prompt)\b/i;
|
|
436
|
+
function findComposer(root, exclude) {
|
|
437
|
+
const candidates = [...root.querySelectorAll("*")].filter(
|
|
438
|
+
(e) => !exclude.contains(e) && !e.contains(exclude)
|
|
439
|
+
);
|
|
440
|
+
return candidates.find((e) => e.getAttribute("role") === "textbox") ?? candidates.find((e) => e.hasAttribute("contenteditable")) ?? candidates.find((e) => COMPOSER_RE.test(classOf(e))) ?? null;
|
|
441
|
+
}
|
|
442
|
+
function emptyReport() {
|
|
443
|
+
return { found: false, detected: [], confidence: 0 };
|
|
444
|
+
}
|
|
445
|
+
function distill(root, opts = {}) {
|
|
446
|
+
const win = getWindow2(opts.window);
|
|
447
|
+
const doc = win.document;
|
|
448
|
+
const warnings = [];
|
|
449
|
+
const clone = root.cloneNode(true);
|
|
450
|
+
const dropped = { count: 0 };
|
|
451
|
+
pruneAndInline(root, clone, win, opts.inlineComputedStyles ?? false, dropped);
|
|
452
|
+
if (dropped.count > 0)
|
|
453
|
+
warnings.push(
|
|
454
|
+
`Dropped ${dropped.count} hidden element(s) from the capture.`
|
|
455
|
+
);
|
|
456
|
+
const safe = sanitizeHtml(clone.outerHTML, { window: win });
|
|
457
|
+
const host = doc.createElement("div");
|
|
458
|
+
host.innerHTML = safe;
|
|
459
|
+
const frameRoot = host.firstElementChild ?? host;
|
|
460
|
+
const pick = findRepeatingRows(frameRoot);
|
|
461
|
+
const detection = {
|
|
462
|
+
frame: emptyReport(),
|
|
463
|
+
message: emptyReport(),
|
|
464
|
+
composer: emptyReport(),
|
|
465
|
+
typing: emptyReport()
|
|
466
|
+
};
|
|
467
|
+
let messageHtml;
|
|
468
|
+
let frameHtml;
|
|
469
|
+
let composerHtml;
|
|
470
|
+
if (pick) {
|
|
471
|
+
const sample = pick.rows[0].cloneNode(true);
|
|
472
|
+
const slotted = slotifyRow(sample);
|
|
473
|
+
messageHtml = slotted.html;
|
|
474
|
+
detection.message = {
|
|
475
|
+
found: true,
|
|
476
|
+
detected: slotted.detected,
|
|
477
|
+
confidence: Math.min(1, slotted.detected.length / 4)
|
|
478
|
+
};
|
|
479
|
+
const slot = doc.createElement("div");
|
|
480
|
+
slot.setAttribute(SLOT_ATTR, "messages");
|
|
481
|
+
slot.textContent = "{{messages}}";
|
|
482
|
+
const listCls = classOf(pick.container);
|
|
483
|
+
if (listCls) slot.setAttribute("class", `${listCls} tc-messages`);
|
|
484
|
+
const containerClone = pick.container.cloneNode(false);
|
|
485
|
+
if (containerClone.getAttribute("style"))
|
|
486
|
+
slot.setAttribute(
|
|
487
|
+
"style",
|
|
488
|
+
containerClone.getAttribute("style")
|
|
489
|
+
);
|
|
490
|
+
const frameClone = frameRoot.cloneNode(true);
|
|
491
|
+
const path = pathTo(frameRoot, pick.container);
|
|
492
|
+
const targetInClone = path ? nodeAtPath(frameClone, path) : null;
|
|
493
|
+
if (targetInClone && targetInClone.parentElement) {
|
|
494
|
+
targetInClone.replaceWith(slot);
|
|
495
|
+
frameHtml = frameClone.outerHTML;
|
|
496
|
+
detection.frame = { found: true, detected: ["messages"], confidence: 1 };
|
|
497
|
+
} else {
|
|
498
|
+
frameHtml = slot.outerHTML;
|
|
499
|
+
detection.frame = {
|
|
500
|
+
found: true,
|
|
501
|
+
detected: ["messages"],
|
|
502
|
+
confidence: 0.5
|
|
503
|
+
};
|
|
504
|
+
warnings.push(
|
|
505
|
+
"Message list is the captured root; no surrounding chrome was found."
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
const composer = findComposer(frameRoot, pick.container);
|
|
509
|
+
if (composer) {
|
|
510
|
+
const c = composer.cloneNode(true);
|
|
511
|
+
c.setAttribute(SLOT_ATTR, "composer");
|
|
512
|
+
c.removeAttribute("contenteditable");
|
|
513
|
+
c.textContent = "{{composer}}";
|
|
514
|
+
composerHtml = c.outerHTML;
|
|
515
|
+
detection.composer = {
|
|
516
|
+
found: true,
|
|
517
|
+
detected: ["composer"],
|
|
518
|
+
confidence: 1
|
|
519
|
+
};
|
|
520
|
+
} else {
|
|
521
|
+
warnings.push(
|
|
522
|
+
"No composer detected \u2014 add one by hand if the skin needs it."
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
} else {
|
|
526
|
+
warnings.push(
|
|
527
|
+
"No repeating message row found \u2014 capture a tighter subtree around the thread."
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
const tokens = extractTokens(frameRoot);
|
|
531
|
+
let canvas;
|
|
532
|
+
const rect = root.getBoundingClientRect?.();
|
|
533
|
+
if (rect && rect.width > 0 && rect.height > 0) {
|
|
534
|
+
canvas = { width: Math.round(rect.width), height: Math.round(rect.height) };
|
|
535
|
+
}
|
|
536
|
+
return {
|
|
537
|
+
version: 1,
|
|
538
|
+
meta: {
|
|
539
|
+
name: opts.name ?? "Captured skin",
|
|
540
|
+
...opts.sourceUrl ? { sourceUrl: opts.sourceUrl } : {},
|
|
541
|
+
...opts.theme ? { theme: opts.theme } : {},
|
|
542
|
+
...canvas ? { canvas } : {}
|
|
543
|
+
},
|
|
544
|
+
slots: {
|
|
545
|
+
...frameHtml ? { frame: frameHtml } : {},
|
|
546
|
+
...messageHtml ? { message: messageHtml } : {},
|
|
547
|
+
...composerHtml ? { composer: composerHtml } : {}
|
|
548
|
+
},
|
|
549
|
+
css: "",
|
|
550
|
+
tokens,
|
|
551
|
+
detection,
|
|
552
|
+
warnings
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function pathTo(root, target) {
|
|
556
|
+
if (root === target) return [];
|
|
557
|
+
for (let i = 0; i < root.children.length; i++) {
|
|
558
|
+
const child = root.children[i];
|
|
559
|
+
if (!child) continue;
|
|
560
|
+
const sub = pathTo(child, target);
|
|
561
|
+
if (sub) return [i, ...sub];
|
|
562
|
+
}
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
function nodeAtPath(root, path) {
|
|
566
|
+
let node = root;
|
|
567
|
+
for (const i of path) {
|
|
568
|
+
if (!node) return null;
|
|
569
|
+
node = node.children[i] ?? null;
|
|
570
|
+
}
|
|
571
|
+
return node;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/import-page.ts
|
|
575
|
+
function mhtmlToHtml(raw) {
|
|
576
|
+
const idx = raw.search(/Content-Type:\s*text\/html/i);
|
|
577
|
+
if (idx === -1) return raw;
|
|
578
|
+
let body = raw.slice(idx);
|
|
579
|
+
const start = body.search(/\r?\n\r?\n/);
|
|
580
|
+
if (start !== -1) body = body.slice(start + 2);
|
|
581
|
+
const boundary = body.search(/\r?\n--/);
|
|
582
|
+
if (boundary !== -1) body = body.slice(0, boundary);
|
|
583
|
+
body = body.replace(/=\r?\n/g, "");
|
|
584
|
+
body = body.replace(
|
|
585
|
+
/=([0-9A-Fa-f]{2})/g,
|
|
586
|
+
(_m, hex) => String.fromCharCode(parseInt(hex, 16))
|
|
587
|
+
);
|
|
588
|
+
return body;
|
|
589
|
+
}
|
|
590
|
+
var THREAD_GUESS = [
|
|
591
|
+
'[role="log"]',
|
|
592
|
+
'[role="list"]',
|
|
593
|
+
'[aria-label*="message" i]',
|
|
594
|
+
'[class*="message-list" i]',
|
|
595
|
+
'[class*="messages" i]',
|
|
596
|
+
'[class*="thread" i]',
|
|
597
|
+
'[class*="conversation" i]',
|
|
598
|
+
"main"
|
|
599
|
+
];
|
|
600
|
+
function guessThread(doc) {
|
|
601
|
+
for (const sel of THREAD_GUESS) {
|
|
602
|
+
const el = doc.querySelector(sel);
|
|
603
|
+
if (el) return el;
|
|
604
|
+
}
|
|
605
|
+
return doc.body;
|
|
606
|
+
}
|
|
607
|
+
function importHtml(content, opts = {}) {
|
|
608
|
+
const html = opts.mhtml ? mhtmlToHtml(content) : content;
|
|
609
|
+
const dom = new jsdom.JSDOM(html);
|
|
610
|
+
const doc = dom.window.document;
|
|
611
|
+
const root = opts.selector ? doc.querySelector(opts.selector) ?? guessThread(doc) : guessThread(doc);
|
|
612
|
+
const { selector: _selector, mhtml: _mhtml, ...rest } = opts;
|
|
613
|
+
return distill(root, {
|
|
614
|
+
...rest,
|
|
615
|
+
window: dom.window,
|
|
616
|
+
name: rest.name ?? doc.title ?? "Imported skin",
|
|
617
|
+
// Saved pages carry their styles inline/in <style>; don't compute.
|
|
618
|
+
inlineComputedStyles: false
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
exports.importHtml = importHtml;
|
|
623
|
+
//# sourceMappingURL=import-page.cjs.map
|
|
624
|
+
//# sourceMappingURL=import-page.cjs.map
|