wasm-html-to-image 1.0.3 → 1.0.7
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/README.md +121 -147
- package/dist/cli-options.js +2 -2
- package/dist/cli.js +4 -2
- package/dist/core-orchestrate.js +6 -2
- package/dist/core-resources.d.ts +1 -0
- package/dist/core-resources.js +5 -2
- package/dist/core.d.ts +3 -3
- package/dist/core.js +2 -2
- package/dist/encode-args.js +2 -0
- package/dist/html-to-image-wasm.js +3 -3
- package/dist/html-to-image.wasm +0 -0
- package/dist/input.d.ts +1 -1
- package/dist/input.js +11 -1
- package/dist/tailwind.d.ts +38 -1
- package/dist/tailwind.js +187 -3
- package/dist/web-workers.js +10 -6
- package/dist/workers-parent.js +2 -2
- package/package.json +7 -7
package/dist/html-to-image.wasm
CHANGED
|
Binary file
|
package/dist/input.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export declare function toBytes(data: Uint8Array | ArrayBuffer): Uint8Array;
|
|
|
13
13
|
* Whether `value` is image input.
|
|
14
14
|
*
|
|
15
15
|
* - Binary (`Uint8Array` / `ArrayBuffer`): magic-byte sniffing for
|
|
16
|
-
* PNG/JPEG/WebP/GIF/AVIF/BMP. Unknown magic throws.
|
|
16
|
+
* PNG/JPEG/WebP/GIF/AVIF/JXL/BMP. Unknown magic throws.
|
|
17
17
|
* - `string`: `data:image/` prefix means image input; anything else is HTML.
|
|
18
18
|
* - `string[]` (HTML vector) / `undefined`: not image input.
|
|
19
19
|
*/
|
package/dist/input.js
CHANGED
|
@@ -41,13 +41,23 @@ function sniffImageFormat(bytes) {
|
|
|
41
41
|
return "avif";
|
|
42
42
|
if (bytes.length >= 2 && bytes[0] === 0x42 && bytes[1] === 0x4d)
|
|
43
43
|
return "bmp";
|
|
44
|
+
// JPEG XL: bare codestream (FF 0A) or ISO BMFF container signature box.
|
|
45
|
+
if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0x0a)
|
|
46
|
+
return "jxl";
|
|
47
|
+
if (bytes.length >= 12 &&
|
|
48
|
+
bytes[0] === 0 &&
|
|
49
|
+
bytes[1] === 0 &&
|
|
50
|
+
bytes[2] === 0 &&
|
|
51
|
+
bytes[3] === 0x0c &&
|
|
52
|
+
ascii(bytes, 4, 8) === "JXL ")
|
|
53
|
+
return "jxl";
|
|
44
54
|
return null;
|
|
45
55
|
}
|
|
46
56
|
/**
|
|
47
57
|
* Whether `value` is image input.
|
|
48
58
|
*
|
|
49
59
|
* - Binary (`Uint8Array` / `ArrayBuffer`): magic-byte sniffing for
|
|
50
|
-
* PNG/JPEG/WebP/GIF/AVIF/BMP. Unknown magic throws.
|
|
60
|
+
* PNG/JPEG/WebP/GIF/AVIF/JXL/BMP. Unknown magic throws.
|
|
51
61
|
* - `string`: `data:image/` prefix means image input; anything else is HTML.
|
|
52
62
|
* - `string[]` (HTML vector) / `undefined`: not image input.
|
|
53
63
|
*/
|
package/dist/tailwind.d.ts
CHANGED
|
@@ -1 +1,38 @@
|
|
|
1
|
-
|
|
1
|
+
/** Options shared by {@link createCSS}, {@link tailwind} and {@link convertTw}. */
|
|
2
|
+
export interface TailwindOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Extra stylesheet that may use Tailwind v4 `@theme { ... }` and `@apply`.
|
|
5
|
+
* It is converted like the `<style>` blocks of the HTML and appended to the result.
|
|
6
|
+
*/
|
|
7
|
+
css?: string;
|
|
8
|
+
/** Called for problems that are skipped (unknown / unsupported `@apply` utilities). Defaults to `console.warn`. */
|
|
9
|
+
warn?: (message: string) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Rewrite Tailwind v4 style `tw="..."` attributes into `class` (merged with an existing `class`).
|
|
13
|
+
* Comments, `<script>` and `<style>` contents are left untouched.
|
|
14
|
+
*/
|
|
15
|
+
export declare const convertTw: (html: string) => string;
|
|
16
|
+
/**
|
|
17
|
+
* Generate the CSS for `html`: UnoCSS / Tailwind utilities for every class (and `tw`) token,
|
|
18
|
+
* plus Tailwind v4 `@theme` variables (`:root`) and `@apply` expansion for `<style>` blocks of
|
|
19
|
+
* `html` and for `options.css`.
|
|
20
|
+
*
|
|
21
|
+
* `tw="..."` attributes are not rewritten here: pass the HTML through {@link convertTw} (or use
|
|
22
|
+
* {@link tailwind}) so the elements actually carry the classes.
|
|
23
|
+
*/
|
|
24
|
+
export declare const createCSS: (html: string, options?: TailwindOptions) => Promise<string>;
|
|
25
|
+
/**
|
|
26
|
+
* One call for Tailwind v4 style markup: `tw="..."` becomes `class`, and the returned `css` holds the
|
|
27
|
+
* utilities, `@theme` variables and `@apply` expansion. Feed both to `render()`.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const { html, css } = await tailwind('<div tw="flex gap-2 bg-brand">…</div>', {
|
|
31
|
+
* css: "@theme { --color-brand: #f00 }",
|
|
32
|
+
* });
|
|
33
|
+
* await render({ value: html, css, width: 600, format: "png" });
|
|
34
|
+
*/
|
|
35
|
+
export declare const tailwind: (html: string, options?: TailwindOptions) => Promise<{
|
|
36
|
+
html: string;
|
|
37
|
+
css: string;
|
|
38
|
+
}>;
|
package/dist/tailwind.js
CHANGED
|
@@ -1,6 +1,190 @@
|
|
|
1
1
|
import { createGenerator } from "@unocss/core";
|
|
2
2
|
import presetUno from "@unocss/preset-wind4";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
const warnDefault = (message) => console.warn(message);
|
|
4
|
+
// --- tw prop -> class ------------------------------------------------------
|
|
5
|
+
const PROTECTED_BLOCKS = /<!--[\s\S]*?-->|<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
|
|
6
|
+
const OPEN_TAG = /<([a-zA-Z][\w:-]*)((?:\s+[^\s"'<>/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'<>=`]+))?)*)\s*(\/?)>/g;
|
|
7
|
+
const ATTR = /([^\s"'<>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'<>=`]+)))?/g;
|
|
8
|
+
const escapeAttr = (value) => value.replace(/"/g, """);
|
|
9
|
+
/**
|
|
10
|
+
* Rewrite Tailwind v4 style `tw="..."` attributes into `class` (merged with an existing `class`).
|
|
11
|
+
* Comments, `<script>` and `<style>` contents are left untouched.
|
|
12
|
+
*/
|
|
13
|
+
export const convertTw = (html) => {
|
|
14
|
+
const protectedParts = [];
|
|
15
|
+
const masked = html.replace(PROTECTED_BLOCKS, (block) => {
|
|
16
|
+
protectedParts.push(block);
|
|
17
|
+
return `\u0000${protectedParts.length - 1}\u0000`;
|
|
18
|
+
});
|
|
19
|
+
const converted = masked.replace(OPEN_TAG, (whole, name, attrs, selfClose) => {
|
|
20
|
+
if (!/\stw\b/i.test(attrs))
|
|
21
|
+
return whole;
|
|
22
|
+
let twValue = "";
|
|
23
|
+
let classValue;
|
|
24
|
+
const kept = [];
|
|
25
|
+
for (const m of attrs.matchAll(ATTR)) {
|
|
26
|
+
const attrName = m[1];
|
|
27
|
+
const value = m[2] ?? m[3] ?? m[4] ?? "";
|
|
28
|
+
const lower = attrName.toLowerCase();
|
|
29
|
+
if (lower === "tw") {
|
|
30
|
+
twValue = `${twValue} ${value}`.trim();
|
|
31
|
+
}
|
|
32
|
+
else if (lower === "class") {
|
|
33
|
+
classValue = `${classValue ?? ""} ${value}`.trim();
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
kept.push(m[0]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!twValue)
|
|
40
|
+
return whole;
|
|
41
|
+
const merged = `${classValue ?? ""} ${twValue}`.trim();
|
|
42
|
+
const rest = kept.length ? ` ${kept.join(" ")}` : "";
|
|
43
|
+
return `<${name}${rest} class="${escapeAttr(merged)}"${selfClose ? " /" : ""}>`;
|
|
44
|
+
});
|
|
45
|
+
return converted.replace(/\u0000(\d+)\u0000/g, (_, i) => protectedParts[Number(i)]);
|
|
46
|
+
};
|
|
47
|
+
// --- @theme / @apply -------------------------------------------------------
|
|
48
|
+
/** Index of the `}` matching the `{` at `open`, or -1. */
|
|
49
|
+
const matchBrace = (css, open) => {
|
|
50
|
+
let depth = 0;
|
|
51
|
+
for (let i = open; i < css.length; i++) {
|
|
52
|
+
const ch = css[i];
|
|
53
|
+
if (ch === "{")
|
|
54
|
+
depth++;
|
|
55
|
+
else if (ch === "}" && --depth === 0)
|
|
56
|
+
return i;
|
|
57
|
+
}
|
|
58
|
+
return -1;
|
|
59
|
+
};
|
|
60
|
+
/** Replace every `@theme { --x: v }` block by a `:root` rule carrying the same variables. */
|
|
61
|
+
const extractTheme = (css) => {
|
|
62
|
+
const variables = {};
|
|
63
|
+
let out = "";
|
|
64
|
+
let pos = 0;
|
|
65
|
+
const re = /@theme\b[^{;]*\{/g;
|
|
66
|
+
for (let m = re.exec(css); m; m = re.exec(css)) {
|
|
67
|
+
const open = m.index + m[0].length - 1;
|
|
68
|
+
const close = matchBrace(css, open);
|
|
69
|
+
if (close < 0)
|
|
70
|
+
break;
|
|
71
|
+
const body = css.slice(open + 1, close);
|
|
72
|
+
const decls = [];
|
|
73
|
+
for (const d of body.matchAll(/(--[\w-]+)\s*:\s*([^;]+);?/g)) {
|
|
74
|
+
variables[d[1]] = d[2].trim();
|
|
75
|
+
decls.push(`${d[1]}:${d[2].trim()}`);
|
|
76
|
+
}
|
|
77
|
+
out += css.slice(pos, m.index);
|
|
78
|
+
out += decls.length ? `:root{${decls.join(";")}}` : "";
|
|
79
|
+
pos = close + 1;
|
|
80
|
+
re.lastIndex = pos;
|
|
81
|
+
}
|
|
82
|
+
return { css: out + css.slice(pos), variables };
|
|
83
|
+
};
|
|
84
|
+
/** `--color-brand-500` style theme variables as a UnoCSS `theme.colors` tree. */
|
|
85
|
+
const themeColors = (variables) => {
|
|
86
|
+
const colors = {};
|
|
87
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
88
|
+
const m = /^--color-([\w-]+)$/.exec(key);
|
|
89
|
+
if (!m)
|
|
90
|
+
continue;
|
|
91
|
+
const parts = m[1].split("-");
|
|
92
|
+
let node = colors;
|
|
93
|
+
for (const [i, part] of parts.entries()) {
|
|
94
|
+
const existing = node[part];
|
|
95
|
+
if (i === parts.length - 1) {
|
|
96
|
+
// `--color-brand` and `--color-brand-500` coexist as brand.DEFAULT + brand[500].
|
|
97
|
+
if (existing && typeof existing === "object")
|
|
98
|
+
existing.DEFAULT = value;
|
|
99
|
+
else
|
|
100
|
+
node[part] = value;
|
|
101
|
+
}
|
|
102
|
+
else if (existing && typeof existing === "object") {
|
|
103
|
+
node = existing;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
const next = typeof existing === "string" ? { DEFAULT: existing } : {};
|
|
107
|
+
node[part] = next;
|
|
108
|
+
node = next;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return colors;
|
|
113
|
+
};
|
|
114
|
+
const APPLY = /@apply\s+([^;}]+?)\s*(;|(?=}))/g;
|
|
115
|
+
const applyTokens = (css) => {
|
|
116
|
+
const tokens = [];
|
|
117
|
+
for (const m of css.matchAll(APPLY))
|
|
118
|
+
tokens.push(...m[1].split(/\s+/));
|
|
119
|
+
return tokens.filter(Boolean);
|
|
120
|
+
};
|
|
121
|
+
const expandApply = async (css, uno, warn) => {
|
|
122
|
+
const cache = new Map();
|
|
123
|
+
for (const token of new Set(applyTokens(css))) {
|
|
124
|
+
const parsed = await uno.parseToken(token);
|
|
125
|
+
// Only plain utilities (`.token { ... }`) can be inlined into another rule.
|
|
126
|
+
const plain = parsed?.filter((u) => !u[3] && u[1] && u[1].replace(/\\/g, "") === `.${token}`);
|
|
127
|
+
if (!plain || plain.length === 0) {
|
|
128
|
+
warn(`@apply: cannot resolve utility "${token}"; ignored.`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
cache.set(token, plain.map((u) => u[2].trim().replace(/;+$/, "")).join(";"));
|
|
132
|
+
}
|
|
133
|
+
return css.replace(APPLY, (_, list, end) => {
|
|
134
|
+
const body = list
|
|
135
|
+
.split(/\s+/)
|
|
136
|
+
.filter(Boolean)
|
|
137
|
+
.map((token) => cache.get(token))
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
.join(";");
|
|
140
|
+
return body ? `${body}${end === ";" ? ";" : ""}` : "";
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
const STYLE_BLOCK = /<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi;
|
|
144
|
+
const needsTailwindCss = (css) => /@theme\b|@apply\b/.test(css);
|
|
145
|
+
/**
|
|
146
|
+
* Generate the CSS for `html`: UnoCSS / Tailwind utilities for every class (and `tw`) token,
|
|
147
|
+
* plus Tailwind v4 `@theme` variables (`:root`) and `@apply` expansion for `<style>` blocks of
|
|
148
|
+
* `html` and for `options.css`.
|
|
149
|
+
*
|
|
150
|
+
* `tw="..."` attributes are not rewritten here: pass the HTML through {@link convertTw} (or use
|
|
151
|
+
* {@link tailwind}) so the elements actually carry the classes.
|
|
152
|
+
*/
|
|
153
|
+
export const createCSS = async (html, options = {}) => {
|
|
154
|
+
const warn = options.warn ?? warnDefault;
|
|
155
|
+
const userBlocks = [];
|
|
156
|
+
for (const m of html.matchAll(STYLE_BLOCK)) {
|
|
157
|
+
if (needsTailwindCss(m[1]))
|
|
158
|
+
userBlocks.push(m[1]);
|
|
159
|
+
}
|
|
160
|
+
if (options.css)
|
|
161
|
+
userBlocks.push(options.css);
|
|
162
|
+
// Theme tokens feed UnoCSS' theme (colors) so utilities such as `bg-brand` resolve.
|
|
163
|
+
const themed = userBlocks.map((css) => extractTheme(css));
|
|
164
|
+
const variables = Object.assign({}, ...themed.map((t) => t.variables));
|
|
165
|
+
const uno = await createGenerator({
|
|
166
|
+
presets: [presetUno()],
|
|
167
|
+
theme: { colors: themeColors(variables) },
|
|
168
|
+
});
|
|
169
|
+
const extraTokens = userBlocks.flatMap(applyTokens).join(" ");
|
|
170
|
+
const utilities = (await uno.generate(`${html}\n${extraTokens}`)).css;
|
|
171
|
+
const converted = [];
|
|
172
|
+
for (const t of themed) {
|
|
173
|
+
converted.push(await expandApply(t.css, uno, warn));
|
|
174
|
+
}
|
|
175
|
+
return [utilities, ...converted.filter((css) => css.trim())].join("\n");
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* One call for Tailwind v4 style markup: `tw="..."` becomes `class`, and the returned `css` holds the
|
|
179
|
+
* utilities, `@theme` variables and `@apply` expansion. Feed both to `render()`.
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* const { html, css } = await tailwind('<div tw="flex gap-2 bg-brand">…</div>', {
|
|
183
|
+
* css: "@theme { --color-brand: #f00 }",
|
|
184
|
+
* });
|
|
185
|
+
* await render({ value: html, css, width: 600, format: "png" });
|
|
186
|
+
*/
|
|
187
|
+
export const tailwind = async (html, options = {}) => {
|
|
188
|
+
const converted = convertTw(html);
|
|
189
|
+
return { html: converted, css: await createCSS(converted, options) };
|
|
6
190
|
};
|