figdiff 0.1.0-alpha.0 → 0.1.0-alpha.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.
Files changed (46) hide show
  1. package/README.md +67 -19
  2. package/dist/action/changed-files.d.ts +14 -0
  3. package/dist/action/changed-files.js +35 -0
  4. package/dist/action/changed-files.js.map +1 -0
  5. package/dist/action/comment.d.ts +28 -0
  6. package/dist/action/comment.js +70 -0
  7. package/dist/action/comment.js.map +1 -0
  8. package/dist/action/github.d.ts +15 -0
  9. package/dist/action/github.js +62 -0
  10. package/dist/action/github.js.map +1 -0
  11. package/dist/action/glob.d.ts +10 -0
  12. package/dist/action/glob.js +45 -0
  13. package/dist/action/glob.js.map +1 -0
  14. package/dist/action/main.d.ts +20 -0
  15. package/dist/action/main.js +111 -0
  16. package/dist/action/main.js.map +1 -0
  17. package/dist/action.d.ts +2 -0
  18. package/dist/action.js +7 -0
  19. package/dist/action.js.map +1 -0
  20. package/dist/cli.js +16 -2
  21. package/dist/cli.js.map +1 -1
  22. package/dist/git.d.ts +12 -0
  23. package/dist/git.js +32 -0
  24. package/dist/git.js.map +1 -0
  25. package/dist/html.d.ts +25 -0
  26. package/dist/html.js +205 -0
  27. package/dist/html.js.map +1 -0
  28. package/dist/index.d.ts +12 -0
  29. package/dist/index.js +6 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/render/compare.d.ts +19 -0
  32. package/dist/render/compare.js +61 -0
  33. package/dist/render/compare.js.map +1 -0
  34. package/dist/render/figcraft-cli.d.ts +21 -0
  35. package/dist/render/figcraft-cli.js +63 -0
  36. package/dist/render/figcraft-cli.js.map +1 -0
  37. package/dist/render/frames.d.ts +19 -0
  38. package/dist/render/frames.js +53 -0
  39. package/dist/render/frames.js.map +1 -0
  40. package/dist/render/png.d.ts +14 -0
  41. package/dist/render/png.js +185 -0
  42. package/dist/render/png.js.map +1 -0
  43. package/dist/run.d.ts +18 -3
  44. package/dist/run.js +117 -17
  45. package/dist/run.js.map +1 -1
  46. package/package.json +3 -2
package/dist/cli.js CHANGED
@@ -1,7 +1,21 @@
1
1
  #!/usr/bin/env node
2
+ import { mkdirSync, writeFileSync } from "node:fs";
3
+ import { dirname } from "node:path";
2
4
  import { run } from "./run.js";
3
5
  const result = run(process.argv.slice(2));
6
+ let code = result.code;
7
+ let stderr = result.stderr;
8
+ for (const file of result.writes) {
9
+ try {
10
+ mkdirSync(dirname(file.path), { recursive: true });
11
+ writeFileSync(file.path, file.content);
12
+ }
13
+ catch (err) {
14
+ stderr = `figdiff: cannot write ${file.path}: ${err instanceof Error ? err.message : String(err)}\n`;
15
+ code = 2;
16
+ }
17
+ }
4
18
  process.stdout.write(result.stdout);
5
- process.stderr.write(result.stderr);
6
- process.exitCode = result.code;
19
+ process.stderr.write(stderr);
20
+ process.exitCode = code;
7
21
  //# sourceMappingURL=cli.js.map
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACpC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACpC,OAAO,CAAC,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1C,IAAI,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;AACvB,IAAI,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;AAE3B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;IACjC,IAAI,CAAC;QACH,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACnD,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACzC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,GAAG,yBAAyB,IAAI,CAAC,IAAI,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;QACrG,IAAI,GAAG,CAAC,CAAC;IACX,CAAC;AACH,CAAC;AAED,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACpC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;AAC7B,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC"}
package/dist/git.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ /** A copy of an old file version on disk, to be removed with `cleanup()`. */
2
+ export interface GitCopy {
3
+ path: string;
4
+ cleanup(): void;
5
+ }
6
+ /** Runs git and returns what it printed to stdout; must throw on failure. */
7
+ export type GitRun = (args: string[], cwd: string) => Uint8Array;
8
+ /**
9
+ * Save the version of `file` found at git revision `ref` (a branch, tag or commit) to a
10
+ * temporary file, so it can be compared with the working copy.
11
+ */
12
+ export declare function extractFromGit(ref: string, file: string, git?: GitRun): GitCopy;
package/dist/git.js ADDED
@@ -0,0 +1,32 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, dirname, join, resolve } from "node:path";
5
+ const defaultGit = (args, cwd) => execFileSync("git", args, { cwd, maxBuffer: 512 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
6
+ /**
7
+ * Save the version of `file` found at git revision `ref` (a branch, tag or commit) to a
8
+ * temporary file, so it can be compared with the working copy.
9
+ */
10
+ export function extractFromGit(ref, file, git = defaultGit) {
11
+ if (ref.startsWith("-"))
12
+ throw new Error(`Invalid git revision "${ref}"`);
13
+ const absolute = resolve(file);
14
+ const name = basename(absolute);
15
+ let content;
16
+ try {
17
+ // "<ref>:./<name>" is resolved relative to the directory git runs in.
18
+ content = git(["show", `${ref}:./${name}`], dirname(absolute));
19
+ }
20
+ catch (err) {
21
+ const e = err;
22
+ if (e.code === "ENOENT")
23
+ throw new Error("git was not found. Install git to use --against.");
24
+ const detail = String(e.stderr ?? "").trim().split("\n")[0];
25
+ throw new Error(`Cannot read ${name} at "${ref}"${detail ? `: ${detail}` : ""}`);
26
+ }
27
+ const dir = mkdtempSync(join(tmpdir(), "figdiff-git-"));
28
+ const path = join(dir, name);
29
+ writeFileSync(path, content);
30
+ return { path, cleanup: () => rmSync(dir, { recursive: true, force: true }) };
31
+ }
32
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAW7D,MAAM,UAAU,GAAW,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CACvC,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;AAEtG;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW,EAAE,IAAY,EAAE,MAAc,UAAU;IAChF,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,GAAG,GAAG,CAAC,CAAC;IAC1E,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAEhC,IAAI,OAAmB,CAAC;IACxB,IAAI,CAAC;QACH,sEAAsE;QACtE,OAAO,GAAG,GAAG,CAAC,CAAC,MAAM,EAAE,GAAG,GAAG,MAAM,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IACjE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,GAAG,GAAsD,CAAC;QACjE,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;QAC7F,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5D,MAAM,IAAI,KAAK,CAAC,eAAe,IAAI,QAAQ,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACnF,CAAC;IAED,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC;IACxD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC7B,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC7B,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AAChF,CAAC"}
package/dist/html.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { Change } from "./diff/types.js";
2
+ /**
3
+ * Images for one frame, as `data:image/...` URIs or http(s) URLs.
4
+ * A frame that exists in only one version (added or removed) has only one side.
5
+ */
6
+ export interface FrameImages {
7
+ before?: string;
8
+ after?: string;
9
+ /** Image highlighting the pixels that differ. */
10
+ diff?: string;
11
+ /** Share of pixels that differ, from 0 to 1. */
12
+ changedRatio?: number;
13
+ }
14
+ export interface HtmlOptions {
15
+ title?: string;
16
+ beforeName?: string;
17
+ afterName?: string;
18
+ /** Optional rendered images per frame; see `frameKey`. */
19
+ images?: Record<string, FrameImages>;
20
+ }
21
+ /** Key used in `HtmlOptions.images` for a top-level frame. */
22
+ export declare const frameKey: (page: string, frame: string) => string;
23
+ export declare function escapeHtml(text: string): string;
24
+ /** A single self-contained HTML page describing the changes. No network access needed to view it. */
25
+ export declare function formatHtml(changes: Change[], options?: HtmlOptions): string;
package/dist/html.js ADDED
@@ -0,0 +1,205 @@
1
+ import { summarize } from "./diff/summary.js";
2
+ import { formatSummary, formatValue } from "./report.js";
3
+ /** Key used in `HtmlOptions.images` for a top-level frame. */
4
+ export const frameKey = (page, frame) => `${page}\u0000${frame}`;
5
+ const SEP = " › ";
6
+ const KIND_SYMBOL = { added: "+", removed: "−", moved: ">", modified: "~", reordered: "↕" };
7
+ const KIND_LABEL = { added: "Added", removed: "Removed", moved: "Moved", modified: "Changed", reordered: "Reordered" };
8
+ export function escapeHtml(text) {
9
+ return text
10
+ .replace(/&/g, "&amp;")
11
+ .replace(/</g, "&lt;")
12
+ .replace(/>/g, "&gt;")
13
+ .replace(/"/g, "&quot;")
14
+ .replace(/'/g, "&#39;");
15
+ }
16
+ /** Only embedded images and web URLs are accepted, never script-capable schemes. */
17
+ function safeImageSrc(src) {
18
+ return /^(data:image\/(png|jpeg|webp|gif|svg\+xml);|https?:\/\/)/i.test(src) ? src : undefined;
19
+ }
20
+ /** Escape a value for display and add a small color swatch next to every hex color. */
21
+ function valueHtml(value) {
22
+ let text = formatValue(value);
23
+ if (text.length > 240)
24
+ text = text.slice(0, 240) + "…";
25
+ return escapeHtml(text).replace(/#[0-9A-F]{6}(?:[0-9A-F]{2})?\b/g, (hex) => `<span class="sw" style="background:${hex}"></span><code>${hex}</code>`);
26
+ }
27
+ function rowBody(c) {
28
+ switch (c.kind) {
29
+ case "added":
30
+ case "removed":
31
+ return `<span class="type">${escapeHtml(c.type)}</span>`;
32
+ case "moved":
33
+ return `<span class="note">moved from ${escapeHtml(c.fromTrail.join(SEP) || "(root)")}</span>`;
34
+ case "reordered":
35
+ return `<span class="note">layer order changed</span>`;
36
+ case "modified":
37
+ return (`<ul class="props">` +
38
+ c.props
39
+ .map((p) => `<li><span class="prop">${escapeHtml(p.prop)}</span>` +
40
+ `<span class="from">${valueHtml(p.from)}</span><span class="arrow">→</span>` +
41
+ `<span class="to">${valueHtml(p.to)}</span></li>`)
42
+ .join("") +
43
+ `</ul>`);
44
+ }
45
+ }
46
+ function row(c, label) {
47
+ return (`<li class="chg ${c.kind}" data-kind="${c.kind}">` +
48
+ `<span class="badge" title="${KIND_LABEL[c.kind]}">${KIND_SYMBOL[c.kind]}</span>` +
49
+ `<div class="what"><span class="label">${escapeHtml(label)}</span>${rowBody(c)}</div></li>`);
50
+ }
51
+ function percent(ratio) {
52
+ const p = ratio * 100;
53
+ if (p === 0)
54
+ return "0%";
55
+ return p < 0.1 ? "<0.1%" : `${p < 10 ? p.toFixed(1) : Math.round(p)}%`;
56
+ }
57
+ function compare(images) {
58
+ if (!images)
59
+ return "";
60
+ const before = images.before && safeImageSrc(images.before);
61
+ const after = images.after && safeImageSrc(images.after);
62
+ const diff = images.diff && safeImageSrc(images.diff);
63
+ if (before && after) {
64
+ const tabs = `<div class="tabs"><button type="button" data-view="swipe" aria-pressed="true">Swipe</button>` +
65
+ `<button type="button" data-view="side" aria-pressed="false">Side by side</button>` +
66
+ (diff ? `<button type="button" data-view="diff" aria-pressed="false">Difference</button>` : "") +
67
+ (diff && images.changedRatio !== undefined
68
+ ? `<span class="ratio">${escapeHtml(percent(images.changedRatio))} of pixels differ</span>`
69
+ : "") +
70
+ `</div>`;
71
+ return (`<div class="cmp" data-view="swipe" style="--pos:50%">${tabs}` +
72
+ `<div class="stack"><img class="b" alt="Before" src="${escapeHtml(before)}">` +
73
+ `<img class="a" alt="After" src="${escapeHtml(after)}"></div>` +
74
+ (diff ? `<img class="diffimg" alt="Differences highlighted" src="${escapeHtml(diff)}" hidden>` : "") +
75
+ `<input type="range" min="0" max="100" value="50" aria-label="Slide between before and after">` +
76
+ `<div class="cap"><span>Before</span><span>After</span></div></div>`);
77
+ }
78
+ const only = after ?? before;
79
+ if (!only)
80
+ return "";
81
+ return (`<figure class="single"><img alt="${after ? "After" : "Before"}" src="${escapeHtml(only)}">` +
82
+ `<figcaption>${after ? "Added: only in the new version" : "Removed: only in the old version"}</figcaption></figure>`);
83
+ }
84
+ const CSS = `
85
+ :root{--bg:#fff;--fg:#1c1c1e;--muted:#6b6b70;--line:#e3e3e8;--card:#f7f7f9;--add:#1a7f37;--rem:#c62828;--mod:#8a5a00;--mov:#5b3fc0;--reo:#00796b}
86
+ @media (prefers-color-scheme:dark){:root{--bg:#161618;--fg:#ececf0;--muted:#9a9aa3;--line:#2e2e33;--card:#1f1f23;--add:#56d364;--rem:#ff7b72;--mod:#e3b341;--mov:#a99bff;--reo:#4fd1c5}}
87
+ *{box-sizing:border-box}
88
+ body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
89
+ main{max-width:920px;margin:0 auto;padding:32px 16px 64px}
90
+ h1{font-size:22px;margin:0 0 4px}
91
+ .files{color:var(--muted);margin:0 0 16px;overflow-wrap:anywhere}
92
+ .files code{color:var(--fg)}
93
+ .summary{font-size:16px;margin:0 0 16px}
94
+ .filters{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 24px}
95
+ .filters button{font:inherit;font-size:13px;border:1px solid var(--line);background:var(--card);color:var(--fg);border-radius:999px;padding:4px 12px;cursor:pointer}
96
+ .filters button[aria-pressed=false]{opacity:.45;text-decoration:line-through}
97
+ h2{font-size:17px;margin:32px 0 8px;padding-bottom:6px;border-bottom:1px solid var(--line)}
98
+ details{border:1px solid var(--line);border-radius:10px;margin:12px 0;background:var(--card)}
99
+ summary{cursor:pointer;padding:10px 14px;font-weight:600;display:flex;justify-content:space-between;gap:12px}
100
+ summary .count{color:var(--muted);font-weight:400;white-space:nowrap}
101
+ .body{padding:0 14px 12px}
102
+ ul.list{list-style:none;margin:0;padding:0}
103
+ .chg{display:flex;gap:10px;padding:8px 0;border-top:1px solid var(--line)}
104
+ .chg[hidden]{display:none}
105
+ .badge{flex:none;width:22px;height:22px;border-radius:6px;display:grid;place-items:center;font:600 13px/1 ui-monospace,monospace;color:var(--bg)}
106
+ .added .badge{background:var(--add)}.removed .badge{background:var(--rem)}.modified .badge{background:var(--mod)}.moved .badge{background:var(--mov)}.reordered .badge{background:var(--reo)}
107
+ .what{min-width:0}
108
+ .label{font-weight:600;overflow-wrap:anywhere}
109
+ .type,.note{color:var(--muted);margin-left:8px;font-size:13px}
110
+ ul.props{list-style:none;margin:4px 0 0;padding:0}
111
+ ul.props li{display:flex;flex-wrap:wrap;align-items:center;gap:6px;font-size:13px;padding:1px 0}
112
+ .prop{color:var(--muted);min-width:84px}
113
+ .arrow{color:var(--muted)}
114
+ code{font:12.5px ui-monospace,SFMono-Regular,Menlo,monospace}
115
+ .sw{display:inline-block;width:12px;height:12px;border-radius:3px;border:1px solid var(--line);vertical-align:-1px;margin-right:4px}
116
+ .cmp{margin:4px 0 12px}
117
+ .stack{position:relative;border:1px solid var(--line);border-radius:8px;overflow:hidden;background:var(--bg)}
118
+ .stack img{display:block;width:100%;height:auto}
119
+ .stack img.a{position:absolute;inset:0;clip-path:inset(0 0 0 var(--pos))}
120
+ .cmp input{width:100%;margin:6px 0 0}
121
+ .cap{display:flex;justify-content:space-between;color:var(--muted);font-size:12px}
122
+ .tabs{display:flex;align-items:center;gap:6px;margin:0 0 6px}
123
+ .tabs button{font:inherit;font-size:12px;border:1px solid var(--line);background:var(--bg);color:var(--fg);border-radius:6px;padding:2px 10px;cursor:pointer}
124
+ .tabs button[aria-pressed=true]{background:var(--fg);color:var(--bg);border-color:var(--fg)}
125
+ .tabs .ratio{margin-left:auto;color:var(--muted);font-size:12px}
126
+ .diffimg{display:block;width:100%;height:auto;border:1px solid var(--line);border-radius:8px}
127
+ .diffimg[hidden]{display:none}
128
+ .cmp[data-view=diff] input,.cmp[data-view=diff] .cap,.cmp[data-view=diff] .stack,.cmp[data-view=side] input{display:none}
129
+ .cmp[data-view=side] .stack{display:grid;grid-template-columns:1fr 1fr;gap:8px;border:0;border-radius:0;overflow:visible;background:none}
130
+ .cmp[data-view=side] .stack img{border:1px solid var(--line);border-radius:8px;background:var(--bg)}
131
+ .cmp[data-view=side] .stack img.a{position:static;clip-path:none}
132
+ figure.single{margin:4px 0 12px}
133
+ figure.single img{display:block;width:100%;height:auto;border:1px solid var(--line);border-radius:8px;background:var(--bg)}
134
+ figure.single figcaption{color:var(--muted);font-size:12px;margin-top:4px}
135
+ footer{margin-top:40px;color:var(--muted);font-size:12px}
136
+ `;
137
+ const SCRIPT = `
138
+ document.querySelectorAll('.filters button').forEach(function(btn){
139
+ btn.addEventListener('click',function(){
140
+ var on=btn.getAttribute('aria-pressed')!=='true';
141
+ btn.setAttribute('aria-pressed',String(on));
142
+ var kind=btn.getAttribute('data-kind');
143
+ document.querySelectorAll('.chg[data-kind="'+kind+'"]').forEach(function(el){el.hidden=!on;});
144
+ });
145
+ });
146
+ document.querySelectorAll('.cmp').forEach(function(c){
147
+ var r=c.querySelector('input');
148
+ r.addEventListener('input',function(){c.style.setProperty('--pos',r.value+'%');});
149
+
150
+ c.querySelectorAll('.tabs button').forEach(function(t){
151
+ t.addEventListener('click',function(){
152
+ var view=t.getAttribute('data-view');
153
+ c.setAttribute('data-view',view);
154
+ var d=c.querySelector('.diffimg');
155
+ if(d)d.hidden=view!=='diff';
156
+ c.querySelectorAll('.tabs button').forEach(function(o){o.setAttribute('aria-pressed',String(o===t));});
157
+ });
158
+ });
159
+ });
160
+ `;
161
+ /** A single self-contained HTML page describing the changes. No network access needed to view it. */
162
+ export function formatHtml(changes, options = {}) {
163
+ const title = options.title ?? "figdiff report";
164
+ const summary = summarize(changes);
165
+ const pages = new Map();
166
+ for (const c of changes) {
167
+ const page = c.trail[0] ?? "(document)";
168
+ const frame = c.trail[1] ?? "";
169
+ const frames = pages.get(page) ?? new Map();
170
+ frames.set(frame, [...(frames.get(frame) ?? []), c]);
171
+ pages.set(page, frames);
172
+ }
173
+ const kinds = ["added", "removed", "modified", "moved", "reordered"].filter((k) => changes.some((c) => c.kind === k));
174
+ const filters = kinds
175
+ .map((k) => `<button type="button" data-kind="${k}" aria-pressed="true">${KIND_SYMBOL[k]} ${KIND_LABEL[k]}</button>`)
176
+ .join("");
177
+ let sections = "";
178
+ for (const [page, frames] of pages) {
179
+ sections += `<h2>Page “${escapeHtml(page)}”</h2>`;
180
+ for (const [frame, group] of frames) {
181
+ const heading = frame === "" ? "Page itself" : `Frame “${escapeHtml(frame)}”`;
182
+ const images = frame === "" ? undefined : options.images?.[frameKey(page, frame)];
183
+ const nodeCount = new Set(group.map((c) => c.id)).size;
184
+ const items = group
185
+ .map((c) => row(c, c.trail.length > 2 ? c.trail.slice(2).join(SEP) : frame === "" ? "(page)" : "(frame itself)"))
186
+ .join("");
187
+ sections +=
188
+ `<details open><summary><span>${heading}</span><span class="count">${nodeCount} ${nodeCount === 1 ? "node" : "nodes"}</span></summary>` +
189
+ `<div class="body">${compare(images)}<ul class="list">${items}</ul></div></details>`;
190
+ }
191
+ }
192
+ const files = options.beforeName || options.afterName
193
+ ? `<p class="files"><code>${escapeHtml(options.beforeName ?? "before")}</code> → <code>${escapeHtml(options.afterName ?? "after")}</code></p>`
194
+ : "";
195
+ const body = changes.length === 0
196
+ ? `<p class="summary">No changes.</p>`
197
+ : `<p class="summary">${escapeHtml(formatSummary(summary))}</p><div class="filters">${filters}</div>${sections}`;
198
+ return (`<!doctype html><html lang="en"><head><meta charset="utf-8">` +
199
+ `<meta name="viewport" content="width=device-width,initial-scale=1">` +
200
+ `<title>${escapeHtml(title)}</title><style>${CSS}</style></head><body><main>` +
201
+ `<h1>${escapeHtml(title)}</h1>${files}${body}` +
202
+ `<footer>Generated by figdiff. Changes are matched by node id.</footer>` +
203
+ `</main><script>${SCRIPT}</script></body></html>\n`);
204
+ }
205
+ //# sourceMappingURL=html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.js","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAuBzD,8DAA8D;AAC9D,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAE,KAAa,EAAU,EAAE,CAAC,GAAG,IAAI,SAAS,KAAK,EAAE,CAAC;AAEzF,MAAM,GAAG,GAAG,KAAK,CAAC;AAClB,MAAM,WAAW,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAW,CAAC;AACrG,MAAM,UAAU,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAW,CAAC;AAEhI,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,IAAI;SACR,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC5B,CAAC;AAED,oFAAoF;AACpF,SAAS,YAAY,CAAC,GAAW;IAC/B,OAAO,2DAA2D,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;AACjG,CAAC;AAED,uFAAuF;AACvF,SAAS,SAAS,CAAC,KAAc;IAC/B,IAAI,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAC9B,IAAI,IAAI,CAAC,MAAM,GAAG,GAAG;QAAE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;IACvD,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,OAAO,CAC7B,iCAAiC,EACjC,CAAC,GAAG,EAAE,EAAE,CAAC,sCAAsC,GAAG,kBAAkB,GAAG,SAAS,CACjF,CAAC;AACJ,CAAC;AAED,SAAS,OAAO,CAAC,CAAS;IACxB,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;QACf,KAAK,OAAO,CAAC;QACb,KAAK,SAAS;YACZ,OAAO,sBAAsB,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC;QAC3D,KAAK,OAAO;YACV,OAAO,iCAAiC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,SAAS,CAAC;QACjG,KAAK,WAAW;YACd,OAAO,+CAA+C,CAAC;QACzD,KAAK,UAAU;YACb,OAAO,CACL,oBAAoB;gBACpB,CAAC,CAAC,KAAK;qBACJ,GAAG,CACF,CAAC,CAAC,EAAE,EAAE,CACJ,0BAA0B,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS;oBACrD,sBAAsB,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,qCAAqC;oBAC5E,oBAAoB,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,cAAc,CACpD;qBACA,IAAI,CAAC,EAAE,CAAC;gBACX,OAAO,CACR,CAAC;IACN,CAAC;AACH,CAAC;AAED,SAAS,GAAG,CAAC,CAAS,EAAE,KAAa;IACnC,OAAO,CACL,kBAAkB,CAAC,CAAC,IAAI,gBAAgB,CAAC,CAAC,IAAI,IAAI;QAClD,8BAA8B,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS;QACjF,yCAAyC,UAAU,CAAC,KAAK,CAAC,UAAU,OAAO,CAAC,CAAC,CAAC,aAAa,CAC5F,CAAC;AACJ,CAAC;AAED,SAAS,OAAO,CAAC,KAAa;IAC5B,MAAM,CAAC,GAAG,KAAK,GAAG,GAAG,CAAC;IACtB,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACzB,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC;AACzE,CAAC;AAED,SAAS,OAAO,CAAC,MAA+B;IAC9C,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IACvB,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACzD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAEtD,IAAI,MAAM,IAAI,KAAK,EAAE,CAAC;QACpB,MAAM,IAAI,GACR,8FAA8F;YAC9F,mFAAmF;YACnF,CAAC,IAAI,CAAC,CAAC,CAAC,iFAAiF,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/F,CAAC,IAAI,IAAI,MAAM,CAAC,YAAY,KAAK,SAAS;gBACxC,CAAC,CAAC,uBAAuB,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,0BAA0B;gBAC3F,CAAC,CAAC,EAAE,CAAC;YACP,QAAQ,CAAC;QACX,OAAO,CACL,wDAAwD,IAAI,EAAE;YAC9D,uDAAuD,UAAU,CAAC,MAAM,CAAC,IAAI;YAC7E,mCAAmC,UAAU,CAAC,KAAK,CAAC,UAAU;YAC9D,CAAC,IAAI,CAAC,CAAC,CAAC,2DAA2D,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;YACpG,+FAA+F;YAC/F,oEAAoE,CACrE,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,IAAI,MAAM,CAAC;IAC7B,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,OAAO,CACL,oCAAoC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,UAAU,UAAU,CAAC,IAAI,CAAC,IAAI;QAC5F,eAAe,KAAK,CAAC,CAAC,CAAC,gCAAgC,CAAC,CAAC,CAAC,kCAAkC,wBAAwB,CACrH,CAAC;AACJ,CAAC;AAED,MAAM,GAAG,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoDX,CAAC;AAEF,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;CAuBd,CAAC;AAEF,qGAAqG;AACrG,MAAM,UAAU,UAAU,CAAC,OAAiB,EAAE,UAAuB,EAAE;IACrE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,gBAAgB,CAAC;IAChD,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IAEnC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAiC,CAAC;IACvD,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,YAAY,CAAC;QACxC,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,EAAoB,CAAC;QAC9D,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACrD,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC1B,CAAC;IAED,MAAM,KAAK,GAAI,CAAC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,CAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAC3F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAClC,CAAC;IACF,MAAM,OAAO,GAAG,KAAK;SAClB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,oCAAoC,CAAC,yBAAyB,WAAW,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC;SACpH,IAAI,CAAC,EAAE,CAAC,CAAC;IAEZ,IAAI,QAAQ,GAAG,EAAE,CAAC;IAClB,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACnC,QAAQ,IAAI,aAAa,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC;QAClD,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;YACpC,MAAM,OAAO,GAAG,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,UAAU,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC;YAC9E,MAAM,MAAM,GAAG,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;YAClF,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;YACvD,MAAM,KAAK,GAAG,KAAK;iBAChB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC;iBAChH,IAAI,CAAC,EAAE,CAAC,CAAC;YACZ,QAAQ;gBACN,gCAAgC,OAAO,8BAA8B,SAAS,IAAI,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,mBAAmB;oBACvI,qBAAqB,OAAO,CAAC,MAAM,CAAC,oBAAoB,KAAK,uBAAuB,CAAC;QACzF,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GACT,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,SAAS;QACrC,CAAC,CAAC,0BAA0B,UAAU,CAAC,OAAO,CAAC,UAAU,IAAI,QAAQ,CAAC,mBAAmB,UAAU,CAAC,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,aAAa;QAC9I,CAAC,CAAC,EAAE,CAAC;IAET,MAAM,IAAI,GACR,OAAO,CAAC,MAAM,KAAK,CAAC;QAClB,CAAC,CAAC,oCAAoC;QACtC,CAAC,CAAC,sBAAsB,UAAU,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,4BAA4B,OAAO,SAAS,QAAQ,EAAE,CAAC;IAErH,OAAO,CACL,6DAA6D;QAC7D,qEAAqE;QACrE,UAAU,UAAU,CAAC,KAAK,CAAC,kBAAkB,GAAG,6BAA6B;QAC7E,OAAO,UAAU,CAAC,KAAK,CAAC,QAAQ,KAAK,GAAG,IAAI,EAAE;QAC9C,wEAAwE;QACxE,kBAAkB,MAAM,2BAA2B,CACpD,CAAC;AACJ,CAAC"}
package/dist/index.d.ts CHANGED
@@ -4,3 +4,15 @@ export { parseFigcraft, readDocumentJson, documentToTree } from "./parser/index.
4
4
  export { formatText, formatSummary, formatValue } from "./report.js";
5
5
  export { run } from "./run.js";
6
6
  export type { Change, DesignNode, PropChange, Summary } from "./diff/types.js";
7
+ export { formatHtml, frameKey, escapeHtml } from "./html.js";
8
+ export type { FrameImages, HtmlOptions } from "./html.js";
9
+ export { decodePng, encodePng } from "./render/png.js";
10
+ export type { Image } from "./render/png.js";
11
+ export { compareImages } from "./render/compare.js";
12
+ export type { Comparison } from "./render/compare.js";
13
+ export { createCliRenderer, findFigcraftCli, RendererUnavailableError } from "./render/figcraft-cli.js";
14
+ export type { Renderer, Exec } from "./render/figcraft-cli.js";
15
+ export { renderChangedFrames } from "./render/frames.js";
16
+ export type { RenderOptions, RenderedFrames } from "./render/frames.js";
17
+ export { extractFromGit } from "./git.js";
18
+ export type { GitCopy, GitRun } from "./git.js";
package/dist/index.js CHANGED
@@ -3,4 +3,10 @@ export { summarize } from "./diff/summary.js";
3
3
  export { parseFigcraft, readDocumentJson, documentToTree } from "./parser/index.js";
4
4
  export { formatText, formatSummary, formatValue } from "./report.js";
5
5
  export { run } from "./run.js";
6
+ export { formatHtml, frameKey, escapeHtml } from "./html.js";
7
+ export { decodePng, encodePng } from "./render/png.js";
8
+ export { compareImages } from "./render/compare.js";
9
+ export { createCliRenderer, findFigcraftCli, RendererUnavailableError } from "./render/figcraft-cli.js";
10
+ export { renderChangedFrames } from "./render/frames.js";
11
+ export { extractFromGit } from "./git.js";
6
12
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACpF,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACpF,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE7D,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEvD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AAExG,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { Image } from "./png.js";
2
+ export interface Comparison {
3
+ width: number;
4
+ height: number;
5
+ changedPixels: number;
6
+ totalPixels: number;
7
+ /** changedPixels / totalPixels, from 0 to 1. */
8
+ ratio: number;
9
+ /** Faded copy of the new image with changed pixels in solid red. */
10
+ diff: Image;
11
+ }
12
+ /**
13
+ * Compare two images pixel by pixel. If sizes differ, the comparison canvas is the larger of
14
+ * the two, and any area covered by only one image counts as changed.
15
+ *
16
+ * `tolerance` is the largest per-channel difference (0 to 255) still treated as unchanged,
17
+ * which absorbs anti-aliasing noise.
18
+ */
19
+ export declare function compareImages(before: Image, after: Image, tolerance?: number): Comparison;
@@ -0,0 +1,61 @@
1
+ const HIGHLIGHT = [255, 45, 85, 255];
2
+ /**
3
+ * Compare two images pixel by pixel. If sizes differ, the comparison canvas is the larger of
4
+ * the two, and any area covered by only one image counts as changed.
5
+ *
6
+ * `tolerance` is the largest per-channel difference (0 to 255) still treated as unchanged,
7
+ * which absorbs anti-aliasing noise.
8
+ */
9
+ export function compareImages(before, after, tolerance = 8) {
10
+ const width = Math.max(before.width, after.width);
11
+ const height = Math.max(before.height, after.height);
12
+ const out = new Uint8Array(width * height * 4);
13
+ let changed = 0;
14
+ const read = (img, x, y) => x < img.width && y < img.height ? (y * img.width + x) * 4 : undefined;
15
+ for (let y = 0; y < height; y++) {
16
+ for (let x = 0; x < width; x++) {
17
+ const a = read(before, x, y);
18
+ const b = read(after, x, y);
19
+ const o = (y * width + x) * 4;
20
+ let isChanged;
21
+ if (a === undefined || b === undefined) {
22
+ isChanged = a !== b;
23
+ }
24
+ else {
25
+ isChanged = false;
26
+ for (let c = 0; c < 4; c++) {
27
+ if (Math.abs(before.data[a + c] - after.data[b + c]) > tolerance) {
28
+ isChanged = true;
29
+ break;
30
+ }
31
+ }
32
+ }
33
+ if (isChanged) {
34
+ changed++;
35
+ out.set(HIGHLIGHT, o);
36
+ }
37
+ else if (b !== undefined) {
38
+ // Unchanged: show the pixel faded to 25% over white so the changes stand out.
39
+ const alpha = after.data[b + 3] / 255;
40
+ for (let c = 0; c < 3; c++) {
41
+ const over = after.data[b + c] * alpha + 255 * (1 - alpha);
42
+ out[o + c] = Math.round(255 - (255 - over) * 0.25);
43
+ }
44
+ out[o + 3] = 255;
45
+ }
46
+ else {
47
+ out.set([255, 255, 255, 255], o);
48
+ }
49
+ }
50
+ }
51
+ const totalPixels = width * height;
52
+ return {
53
+ width,
54
+ height,
55
+ changedPixels: changed,
56
+ totalPixels,
57
+ ratio: totalPixels === 0 ? 0 : changed / totalPixels,
58
+ diff: { width, height, data: out },
59
+ };
60
+ }
61
+ //# sourceMappingURL=compare.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compare.js","sourceRoot":"","sources":["../../src/render/compare.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ /** Runs a program; must throw if it cannot be started or exits with an error. */
2
+ export type Exec = (command: string, args: string[]) => void;
3
+ export declare const defaultExec: Exec;
4
+ /** Renders one top-level frame of a design file to a PNG. */
5
+ export interface Renderer {
6
+ /** Returns the PNG bytes, or undefined when the frame does not exist in that file. */
7
+ renderFrame(file: string, page: string, frame: string, scale: number): Uint8Array | undefined;
8
+ }
9
+ /** Thrown when figcraft-cli itself cannot be run, as opposed to a frame that is missing. */
10
+ export declare class RendererUnavailableError extends Error {
11
+ }
12
+ /**
13
+ * Locate figcraft-cli. An explicit path wins; otherwise the FIGCRAFT_CLI environment
14
+ * variable, then `figcraft-cli` on the PATH. Returns undefined when it cannot be started.
15
+ */
16
+ export declare function findFigcraftCli(explicit?: string, exec?: Exec, env?: Record<string, string | undefined>): string | undefined;
17
+ /**
18
+ * Renderer backed by `figcraft-cli export`. The frame is selected by name; when the page
19
+ * is also passed and the CLI rejects that combination, it retries with the name alone.
20
+ */
21
+ export declare function createCliRenderer(bin: string, exec?: Exec): Renderer;
@@ -0,0 +1,63 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ export const defaultExec = (command, args) => {
6
+ execFileSync(command, args, { stdio: "ignore", timeout: 120_000 });
7
+ };
8
+ /** Thrown when figcraft-cli itself cannot be run, as opposed to a frame that is missing. */
9
+ export class RendererUnavailableError extends Error {
10
+ }
11
+ const isNotFound = (err) => err?.code === "ENOENT";
12
+ /**
13
+ * Locate figcraft-cli. An explicit path wins; otherwise the FIGCRAFT_CLI environment
14
+ * variable, then `figcraft-cli` on the PATH. Returns undefined when it cannot be started.
15
+ */
16
+ export function findFigcraftCli(explicit, exec = defaultExec, env = process.env) {
17
+ const candidates = explicit ? [explicit] : [env.FIGCRAFT_CLI, "figcraft-cli"];
18
+ for (const candidate of candidates) {
19
+ if (!candidate)
20
+ continue;
21
+ try {
22
+ exec(candidate, ["--help"]);
23
+ return candidate;
24
+ }
25
+ catch (err) {
26
+ // Exists but did not like --help: still usable. Only "not found" rules it out.
27
+ if (!isNotFound(err))
28
+ return candidate;
29
+ }
30
+ }
31
+ return undefined;
32
+ }
33
+ /**
34
+ * Renderer backed by `figcraft-cli export`. The frame is selected by name; when the page
35
+ * is also passed and the CLI rejects that combination, it retries with the name alone.
36
+ */
37
+ export function createCliRenderer(bin, exec = defaultExec) {
38
+ return {
39
+ renderFrame(file, page, frame, scale) {
40
+ const dir = mkdtempSync(join(tmpdir(), "figdiff-render-"));
41
+ const out = join(dir, "frame.png");
42
+ try {
43
+ const base = ["export", file, "-o", out, "--format", "png", "--scale", String(scale)];
44
+ for (const selector of [["--page", page, "--node", frame], ["--node", frame]]) {
45
+ try {
46
+ exec(bin, [...base, ...selector]);
47
+ return new Uint8Array(readFileSync(out));
48
+ }
49
+ catch (err) {
50
+ if (isNotFound(err))
51
+ throw new RendererUnavailableError(`Cannot run ${bin}`);
52
+ // Otherwise: this selector did not work, or the frame is not in this file.
53
+ }
54
+ }
55
+ return undefined;
56
+ }
57
+ finally {
58
+ rmSync(dir, { recursive: true, force: true });
59
+ }
60
+ },
61
+ };
62
+ }
63
+ //# sourceMappingURL=figcraft-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"figcraft-cli.js","sourceRoot":"","sources":["../../src/render/figcraft-cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAKjC,MAAM,CAAC,MAAM,WAAW,GAAS,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE;IACjD,YAAY,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;AACrE,CAAC,CAAC;AAQF,4FAA4F;AAC5F,MAAM,OAAO,wBAAyB,SAAQ,KAAK;CAAG;AAEtD,MAAM,UAAU,GAAG,CAAC,GAAY,EAAW,EAAE,CAAE,GAAgC,EAAE,IAAI,KAAK,QAAQ,CAAC;AAEnG;;;GAGG;AACH,MAAM,UAAU,eAAe,CAC7B,QAAiB,EACjB,OAAa,WAAW,EACxB,MAA0C,OAAO,CAAC,GAAG;IAErD,MAAM,UAAU,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC;IAC9E,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,CAAC,SAAS;YAAE,SAAS;QACzB,IAAI,CAAC;YACH,IAAI,CAAC,SAAS,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;YAC5B,OAAO,SAAS,CAAC;QACnB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,+EAA+E;YAC/E,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,OAAO,SAAS,CAAC;QACzC,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,OAAa,WAAW;IACrE,OAAO;QACL,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK;YAClC,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,iBAAiB,CAAC,CAAC,CAAC;YAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;YACnC,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;gBACtF,KAAK,MAAM,QAAQ,IAAI,CAAC,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;oBAC9E,IAAI,CAAC;wBACH,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC;wBAClC,OAAO,IAAI,UAAU,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC;oBAC3C,CAAC;oBAAC,OAAO,GAAG,EAAE,CAAC;wBACb,IAAI,UAAU,CAAC,GAAG,CAAC;4BAAE,MAAM,IAAI,wBAAwB,CAAC,cAAc,GAAG,EAAE,CAAC,CAAC;wBAC7E,2EAA2E;oBAC7E,CAAC;gBACH,CAAC;gBACD,OAAO,SAAS,CAAC;YACnB,CAAC;oBAAS,CAAC;gBACT,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAChD,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { Change } from "../diff/types.js";
2
+ import type { FrameImages } from "../html.js";
3
+ import type { Renderer } from "./figcraft-cli.js";
4
+ export interface RenderOptions {
5
+ /** Render scale passed to figcraft-cli. Default 1. */
6
+ scale?: number;
7
+ /** Most frames to render, to keep reports small and fast. Default 30. */
8
+ maxFrames?: number;
9
+ }
10
+ export interface RenderedFrames {
11
+ images: Record<string, FrameImages>;
12
+ warnings: string[];
13
+ }
14
+ /**
15
+ * Render every top-level frame that has changes, in both versions, and compare them.
16
+ * A frame missing from one version (added or removed) gets only the image that exists.
17
+ * Throws RendererUnavailableError if figcraft-cli cannot be run at all.
18
+ */
19
+ export declare function renderChangedFrames(changes: Change[], beforePath: string, afterPath: string, renderer: Renderer, options?: RenderOptions): RenderedFrames;
@@ -0,0 +1,53 @@
1
+ import { frameKey } from "../html.js";
2
+ import { compareImages } from "./compare.js";
3
+ import { decodePng, encodePng } from "./png.js";
4
+ const dataUri = (bytes) => `data:image/png;base64,${Buffer.from(bytes).toString("base64")}`;
5
+ /**
6
+ * Render every top-level frame that has changes, in both versions, and compare them.
7
+ * A frame missing from one version (added or removed) gets only the image that exists.
8
+ * Throws RendererUnavailableError if figcraft-cli cannot be run at all.
9
+ */
10
+ export function renderChangedFrames(changes, beforePath, afterPath, renderer, options = {}) {
11
+ const scale = options.scale ?? 1;
12
+ const maxFrames = options.maxFrames ?? 30;
13
+ const warnings = [];
14
+ const images = {};
15
+ const frames = new Map();
16
+ for (const c of changes) {
17
+ const [page, frame] = c.trail;
18
+ if (page !== undefined && frame !== undefined)
19
+ frames.set(frameKey(page, frame), [page, frame]);
20
+ }
21
+ let rendered = 0;
22
+ for (const [key, [page, frame]] of frames) {
23
+ if (rendered >= maxFrames) {
24
+ warnings.push(`Rendered ${maxFrames} of ${frames.size} changed frames; the rest have no images.`);
25
+ break;
26
+ }
27
+ rendered++;
28
+ const before = renderer.renderFrame(beforePath, page, frame, scale);
29
+ const after = renderer.renderFrame(afterPath, page, frame, scale);
30
+ if (!before && !after) {
31
+ warnings.push(`Could not render frame "${frame}" in either file.`);
32
+ continue;
33
+ }
34
+ const entry = {};
35
+ if (before)
36
+ entry.before = dataUri(before);
37
+ if (after)
38
+ entry.after = dataUri(after);
39
+ if (before && after) {
40
+ try {
41
+ const comparison = compareImages(decodePng(before), decodePng(after));
42
+ entry.diff = dataUri(encodePng(comparison.diff));
43
+ entry.changedRatio = comparison.ratio;
44
+ }
45
+ catch (err) {
46
+ warnings.push(`Could not compare images of frame "${frame}": ${err instanceof Error ? err.message : String(err)}`);
47
+ }
48
+ }
49
+ images[key] = entry;
50
+ }
51
+ return { images, warnings };
52
+ }
53
+ //# sourceMappingURL=frames.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frames.js","sourceRoot":"","sources":["../../src/render/frames.ts"],"names":[],"mappings":"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"}