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.
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
  */
@@ -1 +1,38 @@
1
- export declare const createCSS: (html: string) => Promise<string>;
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
- export const createCSS = async (html) => {
4
- const uno = await createGenerator({ presets: [presetUno()] });
5
- return uno.generate(html).then((v) => v.css);
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, "&quot;");
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
  };