@abelfubu/dv 0.1.0

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 (118) hide show
  1. package/dist/ansi-html.d.ts +42 -0
  2. package/dist/ansi-html.d.ts.map +1 -0
  3. package/dist/ansi-html.js +327 -0
  4. package/dist/ansi-output.d.ts +22 -0
  5. package/dist/ansi-output.d.ts.map +1 -0
  6. package/dist/ansi-output.js +154 -0
  7. package/dist/balance-delimiters.d.ts +25 -0
  8. package/dist/balance-delimiters.d.ts.map +1 -0
  9. package/dist/balance-delimiters.js +539 -0
  10. package/dist/balance-delimiters.test.d.ts +2 -0
  11. package/dist/balance-delimiters.test.d.ts.map +1 -0
  12. package/dist/balance-delimiters.test.js +1029 -0
  13. package/dist/cli-copy-notification.test.d.ts +2 -0
  14. package/dist/cli-copy-notification.test.d.ts.map +1 -0
  15. package/dist/cli-copy-notification.test.js +80 -0
  16. package/dist/cli-scroll.test.d.ts +2 -0
  17. package/dist/cli-scroll.test.d.ts.map +1 -0
  18. package/dist/cli-scroll.test.js +283 -0
  19. package/dist/cli.d.ts +9 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +976 -0
  22. package/dist/clipboard.d.ts +16 -0
  23. package/dist/clipboard.d.ts.map +1 -0
  24. package/dist/clipboard.js +128 -0
  25. package/dist/components/diff-view.d.ts +32 -0
  26. package/dist/components/diff-view.d.ts.map +1 -0
  27. package/dist/components/diff-view.js +123 -0
  28. package/dist/components/diff-view.test.d.ts +5 -0
  29. package/dist/components/diff-view.test.d.ts.map +1 -0
  30. package/dist/components/diff-view.test.js +312 -0
  31. package/dist/components/directory-tree-view.d.ts +33 -0
  32. package/dist/components/directory-tree-view.d.ts.map +1 -0
  33. package/dist/components/directory-tree-view.js +262 -0
  34. package/dist/components/index.d.ts +4 -0
  35. package/dist/components/index.d.ts.map +1 -0
  36. package/dist/components/index.js +5 -0
  37. package/dist/components/toast.d.ts +21 -0
  38. package/dist/components/toast.d.ts.map +1 -0
  39. package/dist/components/toast.js +47 -0
  40. package/dist/diff-cursor-utils.d.ts +20 -0
  41. package/dist/diff-cursor-utils.d.ts.map +1 -0
  42. package/dist/diff-cursor-utils.js +105 -0
  43. package/dist/diff-cursor-utils.test.d.ts +2 -0
  44. package/dist/diff-cursor-utils.test.d.ts.map +1 -0
  45. package/dist/diff-cursor-utils.test.js +40 -0
  46. package/dist/diff-surface-copy.d.ts +23 -0
  47. package/dist/diff-surface-copy.d.ts.map +1 -0
  48. package/dist/diff-surface-copy.js +64 -0
  49. package/dist/diff-surface-copy.test.d.ts +5 -0
  50. package/dist/diff-surface-copy.test.d.ts.map +1 -0
  51. package/dist/diff-surface-copy.test.js +142 -0
  52. package/dist/diff-utils.d.ts +196 -0
  53. package/dist/diff-utils.d.ts.map +1 -0
  54. package/dist/diff-utils.js +682 -0
  55. package/dist/diff-utils.test.d.ts +2 -0
  56. package/dist/diff-utils.test.d.ts.map +1 -0
  57. package/dist/diff-utils.test.js +727 -0
  58. package/dist/directory-tree.d.ts +72 -0
  59. package/dist/directory-tree.d.ts.map +1 -0
  60. package/dist/directory-tree.js +161 -0
  61. package/dist/directory-tree.test.d.ts +2 -0
  62. package/dist/directory-tree.test.d.ts.map +1 -0
  63. package/dist/directory-tree.test.js +383 -0
  64. package/dist/dropdown.d.ts +26 -0
  65. package/dist/dropdown.d.ts.map +1 -0
  66. package/dist/dropdown.js +172 -0
  67. package/dist/dropdown.test.d.ts +2 -0
  68. package/dist/dropdown.test.d.ts.map +1 -0
  69. package/dist/dropdown.test.js +106 -0
  70. package/dist/filter-submodule.e2e.test.d.ts +2 -0
  71. package/dist/filter-submodule.e2e.test.d.ts.map +1 -0
  72. package/dist/filter-submodule.e2e.test.js +109 -0
  73. package/dist/hooks/use-copy-selection.d.ts +29 -0
  74. package/dist/hooks/use-copy-selection.d.ts.map +1 -0
  75. package/dist/hooks/use-copy-selection.js +46 -0
  76. package/dist/kv-codec.d.ts +16 -0
  77. package/dist/kv-codec.d.ts.map +1 -0
  78. package/dist/kv-codec.js +36 -0
  79. package/dist/license.d.ts +14 -0
  80. package/dist/license.d.ts.map +1 -0
  81. package/dist/license.js +63 -0
  82. package/dist/logger.d.ts +9 -0
  83. package/dist/logger.d.ts.map +1 -0
  84. package/dist/logger.js +78 -0
  85. package/dist/monochrome.d.ts +34 -0
  86. package/dist/monochrome.d.ts.map +1 -0
  87. package/dist/monochrome.js +613 -0
  88. package/dist/monotone.d.ts +22 -0
  89. package/dist/monotone.d.ts.map +1 -0
  90. package/dist/monotone.js +185 -0
  91. package/dist/parsers-config.d.ts +19 -0
  92. package/dist/parsers-config.d.ts.map +1 -0
  93. package/dist/parsers-config.js +271 -0
  94. package/dist/patch-terminal-dimensions.d.ts +2 -0
  95. package/dist/patch-terminal-dimensions.d.ts.map +1 -0
  96. package/dist/patch-terminal-dimensions.js +45 -0
  97. package/dist/stdin-pager.test.d.ts +2 -0
  98. package/dist/stdin-pager.test.d.ts.map +1 -0
  99. package/dist/stdin-pager.test.js +497 -0
  100. package/dist/store.d.ts +16 -0
  101. package/dist/store.d.ts.map +1 -0
  102. package/dist/store.js +48 -0
  103. package/dist/themes/github.json +247 -0
  104. package/dist/themes.d.ts +59 -0
  105. package/dist/themes.d.ts.map +1 -0
  106. package/dist/themes.js +248 -0
  107. package/dist/tree-icons.d.ts +4 -0
  108. package/dist/tree-icons.d.ts.map +1 -0
  109. package/dist/tree-icons.js +18 -0
  110. package/dist/utils.d.ts +2 -0
  111. package/dist/utils.d.ts.map +1 -0
  112. package/dist/utils.js +13 -0
  113. package/dist/web-utils.d.ts +56 -0
  114. package/dist/web-utils.d.ts.map +1 -0
  115. package/dist/web-utils.js +363 -0
  116. package/package.json +37 -0
  117. package/public/jetbrains-mono-nerd.ttf +0 -0
  118. package/public/jetbrains-mono-nerd.woff2 +0 -0
@@ -0,0 +1,42 @@
1
+ import type { CapturedFrame, CapturedLine, CapturedSpan } from "@opentuah/core";
2
+ export interface ToHtmlOptions {
3
+ /** Background color for the container */
4
+ backgroundColor?: string;
5
+ /** Text color for the container */
6
+ textColor?: string;
7
+ /** Font family for the output */
8
+ fontFamily?: string;
9
+ /** Trim empty lines from the end */
10
+ trimEmptyLines?: boolean;
11
+ /** Enable auto light/dark mode based on system preference */
12
+ autoTheme?: boolean;
13
+ /** HTML document title */
14
+ title?: string;
15
+ /** OG image URL for social media previews */
16
+ ogImageUrl?: string;
17
+ /** Custom line renderer - wraps or replaces the default <div class="line"> output per line.
18
+ * Generic hook: receives the default HTML, the captured line data, and the 0-based line index.
19
+ * Return a replacement HTML string. If not provided, the default <div class="line"> is used. */
20
+ renderLine?: (defaultHtml: string, line: CapturedLine, lineIndex: number) => string;
21
+ /** Extra CSS injected into the document style block */
22
+ extraCss?: string;
23
+ /** Extra JS injected as a separate script block before </body> */
24
+ extraJs?: string;
25
+ }
26
+ /**
27
+ * Converts captured frame to styled HTML.
28
+ * Renders HTML line by line from the CapturedFrame structure.
29
+ * Returns both the content HTML and a CSS block for deduplicated span styles.
30
+ */
31
+ export declare function frameToHtml(frame: CapturedFrame, options?: ToHtmlOptions): {
32
+ html: string;
33
+ spanCss: string;
34
+ };
35
+ /**
36
+ * Generates a complete HTML document from captured frame.
37
+ * Includes proper styling for terminal output display.
38
+ * Font size automatically adjusts to fit content within viewport.
39
+ */
40
+ export declare function frameToHtmlDocument(frame: CapturedFrame, options?: ToHtmlOptions): string;
41
+ export type { CapturedFrame, CapturedLine, CapturedSpan };
42
+ //# sourceMappingURL=ansi-html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ansi-html.d.ts","sourceRoot":"","sources":["../src/ansi-html.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAM/E,MAAM,WAAW,aAAa;IAC5B,yCAAyC;IACzC,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iCAAiC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,oCAAoC;IACpC,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,0BAA0B;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;qGAEiG;IACjG,UAAU,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,KAAK,MAAM,CAAA;IACnF,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAqHD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,GAAE,aAAkB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CA4BhH;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,CAyL7F;AAED,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,CAAA"}
@@ -0,0 +1,327 @@
1
+ // Terminal output to HTML converter for web preview generation.
2
+ // Uses opentui's test renderer to capture structured span data and generates responsive HTML documents
3
+ // with proper font scaling to fit terminal content within viewport width.
4
+ import { TextAttributes, rgbToHex } from "@opentuah/core";
5
+ import dedent from "string-dedent";
6
+ // Alias for syntax highlighting in editors (tagged template behaves identically)
7
+ const html = dedent;
8
+ /**
9
+ * Escape HTML special characters
10
+ */
11
+ function escapeHtml(text) {
12
+ return text
13
+ .replace(/&/g, "&amp;")
14
+ .replace(/</g, "&lt;")
15
+ .replace(/>/g, "&gt;")
16
+ .replace(/"/g, "&quot;");
17
+ }
18
+ function linkifyHtml(text) {
19
+ const urlRegex = /(https?:\/\/[^\s<]+)/g;
20
+ return text.replace(urlRegex, (url) => {
21
+ return `<a href="${url}" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: inherit;">${url}</a>`;
22
+ });
23
+ }
24
+ /**
25
+ * Convert RGBA to hex string, returning null for transparent colors
26
+ */
27
+ function rgbaToHexOrNull(rgba) {
28
+ if (rgba.a === 0)
29
+ return null;
30
+ return rgbToHex(rgba);
31
+ }
32
+ /**
33
+ * Build a CSS style key string from a span's visual properties.
34
+ * Used as the dedup key for the class map.
35
+ */
36
+ function spanStyleKey(span) {
37
+ const parts = [];
38
+ const fg = rgbaToHexOrNull(span.fg);
39
+ const bg = rgbaToHexOrNull(span.bg);
40
+ if (fg)
41
+ parts.push(`color:${fg}`);
42
+ if (bg)
43
+ parts.push(`background-color:${bg}`);
44
+ if (span.attributes & TextAttributes.BOLD)
45
+ parts.push("font-weight:bold");
46
+ if (span.attributes & TextAttributes.ITALIC)
47
+ parts.push("font-style:italic");
48
+ if (span.attributes & TextAttributes.UNDERLINE)
49
+ parts.push("text-decoration:underline");
50
+ if (span.attributes & TextAttributes.STRIKETHROUGH)
51
+ parts.push("text-decoration:line-through");
52
+ if (span.attributes & TextAttributes.DIM)
53
+ parts.push("opacity:0.5");
54
+ return parts.join(";");
55
+ }
56
+ /**
57
+ * Style class map — maps CSS declaration strings to short class names.
58
+ * Populated during frameToHtml, consumed by frameToHtmlDocument to emit
59
+ * a compact <style> block instead of repeating inline styles on every span.
60
+ *
61
+ * A typical diff has 50-200 unique style combos. Using classes instead of
62
+ * inline styles reduces HTML size by ~8MB on large diffs (185K spans ×
63
+ * ~55 bytes per inline style attribute).
64
+ */
65
+ class StyleClassMap {
66
+ map = new Map();
67
+ counter = 0;
68
+ /** Get or create a class name for a CSS declaration string */
69
+ getClass(styleKey) {
70
+ let cls = this.map.get(styleKey);
71
+ if (!cls) {
72
+ cls = `s${this.counter++}`;
73
+ this.map.set(styleKey, cls);
74
+ }
75
+ return cls;
76
+ }
77
+ /** Generate the CSS block for all collected classes */
78
+ toCss() {
79
+ const rules = [];
80
+ for (const [style, cls] of this.map) {
81
+ rules.push(`.${cls}{${style}}`);
82
+ }
83
+ return rules.join("\n");
84
+ }
85
+ }
86
+ /**
87
+ * Convert a single span to HTML using class-based styling.
88
+ * The classMap collects unique style combos; the actual CSS is emitted later.
89
+ */
90
+ function spanToHtml(span, classMap) {
91
+ const escapedText = linkifyHtml(escapeHtml(span.text));
92
+ const key = spanStyleKey(span);
93
+ if (key === "") {
94
+ return `<span>${escapedText}</span>`;
95
+ }
96
+ const cls = classMap.getClass(key);
97
+ return `<span class="${cls}">${escapedText}</span>`;
98
+ }
99
+ /**
100
+ * Convert a single line to HTML
101
+ */
102
+ function lineToHtml(line, classMap) {
103
+ if (line.spans.length === 0) {
104
+ return "";
105
+ }
106
+ return line.spans.map((span) => spanToHtml(span, classMap)).join("");
107
+ }
108
+ /**
109
+ * Check if a line is empty (no spans or only whitespace content)
110
+ */
111
+ function isLineEmpty(line) {
112
+ if (line.spans.length === 0)
113
+ return true;
114
+ // Check if all spans contain only whitespace
115
+ return line.spans.every(span => span.text.trim() === "");
116
+ }
117
+ /**
118
+ * Converts captured frame to styled HTML.
119
+ * Renders HTML line by line from the CapturedFrame structure.
120
+ * Returns both the content HTML and a CSS block for deduplicated span styles.
121
+ */
122
+ export function frameToHtml(frame, options = {}) {
123
+ const { trimEmptyLines = true } = options;
124
+ const classMap = new StyleClassMap();
125
+ let lines = frame.lines;
126
+ // Trim empty lines from the end
127
+ if (trimEmptyLines) {
128
+ while (lines.length > 0 && isLineEmpty(lines[lines.length - 1])) {
129
+ lines = lines.slice(0, -1);
130
+ }
131
+ }
132
+ // Render each line as a div
133
+ const htmlLines = lines.map((line, lineIndex) => {
134
+ const content = lineToHtml(line, classMap);
135
+ // Use a div for each line to ensure proper line breaks
136
+ // Empty lines get a span with nbsp for consistent flex behavior
137
+ const defaultHtml = `<div class="line">${content || "<span>&nbsp;</span>"}</div>`;
138
+ return options.renderLine
139
+ ? options.renderLine(defaultHtml, line, lineIndex)
140
+ : defaultHtml;
141
+ });
142
+ return {
143
+ html: htmlLines.join("\n"),
144
+ spanCss: classMap.toCss(),
145
+ };
146
+ }
147
+ /**
148
+ * Generates a complete HTML document from captured frame.
149
+ * Includes proper styling for terminal output display.
150
+ * Font size automatically adjusts to fit content within viewport.
151
+ */
152
+ export function frameToHtmlDocument(frame, options = {}) {
153
+ const { backgroundColor = "#ffffff", textColor = "#1a1a1a", fontFamily = "'JetBrains Mono Nerd', 'JetBrains Mono', 'Fira Code', Monaco, Menlo, 'Ubuntu Mono', Consolas, monospace", title = "Critique Diff", } = options;
154
+ const cols = frame.cols;
155
+ const { html: content, spanCss } = frameToHtml(frame, options);
156
+ const ogTags = options.ogImageUrl ? '\n' + html `
157
+ <meta property="og:title" content="${escapeHtml(title)}">
158
+ <meta property="og:type" content="website">
159
+ <meta property="og:image" content="${escapeHtml(options.ogImageUrl)}">
160
+ <meta property="og:image:width" content="1200">
161
+ <meta property="og:image:height" content="630">
162
+ <meta name="twitter:card" content="summary_large_image">
163
+ <meta name="twitter:title" content="${escapeHtml(title)}">
164
+ <meta name="twitter:image" content="${escapeHtml(options.ogImageUrl)}">
165
+ ` : '';
166
+ const autoThemeCss = options.autoTheme ? '\n' + html `
167
+ @media (prefers-color-scheme: light) {
168
+ html, body {
169
+ background-color: #ffffff;
170
+ }
171
+ #content {
172
+ filter: invert(1) hue-rotate(180deg);
173
+ }
174
+ }
175
+ ` : '';
176
+ const extraJsBlock = options.extraJs
177
+ ? `\n<script>\n${options.extraJs}\n</script>`
178
+ : '';
179
+ return html `
180
+ <!DOCTYPE html>
181
+ <html>
182
+ <head>
183
+ <meta charset="utf-8">
184
+ <meta name="viewport" content="width=device-width, initial-scale=1">
185
+ <link rel="icon" href="/favicon-dark.png" media="(prefers-color-scheme: dark)">
186
+ <link rel="icon" href="/favicon-light.png" media="(prefers-color-scheme: light)">
187
+ <link rel="icon" href="/favicon-dark.png">${ogTags}
188
+ <style>
189
+ @font-face {
190
+ font-family: 'JetBrains Mono Nerd';
191
+ src: url('/jetbrains-mono-nerd.woff2') format('woff2');
192
+ font-weight: normal;
193
+ font-style: normal;
194
+ font-display: swap;
195
+ }
196
+ </style>
197
+ <title>${escapeHtml(title)}</title>
198
+ <style>
199
+ /* Tailwind-style global defaults (safe for agentation widget) */
200
+ *, ::before, ::after {
201
+ box-sizing: border-box;
202
+ border-width: 0;
203
+ border-style: solid;
204
+ border-color: currentColor;
205
+ }
206
+ html {
207
+ -webkit-text-size-adjust: 100%;
208
+ text-size-adjust: 100%;
209
+ line-height: 1.5;
210
+ tab-size: 4;
211
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
212
+ }
213
+ html, body {
214
+ min-height: 100%;
215
+ margin: 0;
216
+ background-color: ${backgroundColor};
217
+ }
218
+ body {
219
+ overflow-x: clip;
220
+ overflow-y: auto;
221
+ max-width: 100vw;
222
+ }
223
+ img, video, svg {
224
+ display: block;
225
+ max-width: 100%;
226
+ }
227
+ /*
228
+ * Diff content styles scoped to #content so they don't interfere
229
+ * with the agentation widget which lives outside #content.
230
+ */
231
+ #content {
232
+ width: fit-content;
233
+ margin: 0 auto;
234
+ padding: 16px;
235
+ color: ${textColor};
236
+ font-family: ${fontFamily};
237
+ /*
238
+ * Font size scales to fit ${cols} columns within viewport.
239
+ * Formula: (viewport - padding) / (cols * char-ratio)
240
+ *
241
+ * The 0.6 char-ratio is the approximate width of 1ch relative to font-size
242
+ * in monospace fonts. Most monospace fonts (JetBrains Mono, Fira Code,
243
+ * Monaco, Consolas) have a ch/font-size ratio between 0.55-0.6.
244
+ * We use 0.6 as a safe upper bound to prevent overflow.
245
+ */
246
+ font-size: clamp(4px, calc((100vw - 32px) / (${cols} * 0.6)), 14px);
247
+ line-height: 1.7;
248
+ }
249
+ .line {
250
+ white-space: pre;
251
+ display: block;
252
+ content-visibility: auto;
253
+ contain-intrinsic-block-size: auto round(down, 1.7em, 1px);
254
+ background-color: ${backgroundColor};
255
+ transform: translateZ(0);
256
+ backface-visibility: hidden;
257
+ }
258
+ .line span {
259
+ white-space: pre;
260
+ display: inline;
261
+ line-height: 1.7;
262
+ padding-block: 0.35em;
263
+ }
264
+ /* Disable content-visibility on iOS Safari where it can cause rendering issues */
265
+ @supports (-webkit-touch-callout: none) {
266
+ .line {
267
+ content-visibility: visible;
268
+ }
269
+ }${autoThemeCss}
270
+ html {
271
+ scrollbar-width: thin;
272
+ scrollbar-color: #6b7280 #2d3748;
273
+ }
274
+ @media (prefers-color-scheme: light) {
275
+ html {
276
+ scrollbar-color: #a0aec0 #edf2f7;
277
+ }
278
+ }
279
+ ::-webkit-scrollbar {
280
+ width: 12px;
281
+ }
282
+ ::-webkit-scrollbar-track {
283
+ background: #2d3748;
284
+ }
285
+ ::-webkit-scrollbar-thumb {
286
+ background: #6b7280;
287
+ border-radius: 6px;
288
+ }
289
+ ::-webkit-scrollbar-thumb:hover {
290
+ background: #a0aec0;
291
+ }
292
+ @media (prefers-color-scheme: light) {
293
+ ::-webkit-scrollbar-track {
294
+ background: #edf2f7;
295
+ }
296
+ ::-webkit-scrollbar-thumb {
297
+ background: #a0aec0;
298
+ }
299
+ ::-webkit-scrollbar-thumb:hover {
300
+ background: #cbd5e1;
301
+ }
302
+ }
303
+ ${options.extraCss || ''}
304
+ ${spanCss}
305
+ </style>
306
+ </head>
307
+ <body>
308
+ <div id="content">
309
+ ${content}
310
+ </div>
311
+ <script>
312
+ // Redirect mobile devices to ?v=mobile for optimized view
313
+ (function() {
314
+ const params = new URLSearchParams(window.location.search);
315
+ if (!params.has('v')) {
316
+ const isMobile = /Mobile|iP(hone|od|ad)|Android|BlackBerry|IEMobile|Kindle|Opera M(obi|ini)|Windows Phone|webOS/i.test(navigator.userAgent);
317
+ if (isMobile) {
318
+ params.set('v', 'mobile');
319
+ window.location.replace(window.location.pathname + '?' + params.toString() + window.location.hash);
320
+ }
321
+ }
322
+ })();
323
+ </script>${extraJsBlock}
324
+ </body>
325
+ </html>
326
+ `;
327
+ }
@@ -0,0 +1,22 @@
1
+ import { type RGBA } from "@opentuah/core";
2
+ import type { CapturedFrame, CapturedSpan } from "@opentuah/core";
3
+ type ColorLevel = 0 | 1 | 2 | 3;
4
+ /**
5
+ * Detect terminal color support level.
6
+ * Returns 0 for non-TTY (piped output) to output plain text.
7
+ * Respects FORCE_COLOR and NO_COLOR environment variables.
8
+ */
9
+ export declare function getColorLevel(): ColorLevel;
10
+ /**
11
+ * Convert a single span to ANSI escape sequences.
12
+ */
13
+ export declare function spanToAnsi(span: CapturedSpan, level: ColorLevel, themeBg: RGBA | undefined): string;
14
+ /**
15
+ * Convert a CapturedFrame to ANSI-formatted string for terminal output.
16
+ * Trims empty lines from the end by default.
17
+ */
18
+ export declare function frameToAnsi(frame: CapturedFrame, themeBg: RGBA | undefined, options?: {
19
+ trimEmptyLines?: boolean;
20
+ }): string;
21
+ export {};
22
+ //# sourceMappingURL=ansi-output.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ansi-output.d.ts","sourceRoot":"","sources":["../src/ansi-output.ts"],"names":[],"mappings":"AAKA,OAAO,EAAkB,KAAK,IAAI,EAAE,MAAM,gBAAgB,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAgB,MAAM,gBAAgB,CAAA;AAG/E,KAAK,UAAU,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;AAE/B;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,UAAU,CAI1C;AA2DD;;GAEG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,YAAY,EAClB,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,IAAI,GAAG,SAAS,GACxB,MAAM,CAkDR;AAUD;;;GAGG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,IAAI,GAAG,SAAS,EACzB,OAAO,GAAE;IAAE,cAAc,CAAC,EAAE,OAAO,CAAA;CAAO,GACzC,MAAM,CAiBR"}
@@ -0,0 +1,154 @@
1
+ // ANSI escape sequence output for terminal scrollback rendering.
2
+ // Detects terminal color capabilities and outputs appropriate escape codes.
3
+ // Falls back gracefully: truecolor → 256 → 16 → plain text.
4
+ import supportsColor from "supports-color";
5
+ import { TextAttributes } from "@opentuah/core";
6
+ /**
7
+ * Detect terminal color support level.
8
+ * Returns 0 for non-TTY (piped output) to output plain text.
9
+ * Respects FORCE_COLOR and NO_COLOR environment variables.
10
+ */
11
+ export function getColorLevel() {
12
+ // supports-color handles FORCE_COLOR, NO_COLOR, and TTY detection internally
13
+ if (!supportsColor.stdout)
14
+ return 0;
15
+ return supportsColor.stdout.level;
16
+ }
17
+ /**
18
+ * Convert RGB (0-1 floats) to nearest 256-color palette index.
19
+ * Uses the 6x6x6 color cube (indices 16-231) and grayscale ramp (232-255).
20
+ */
21
+ function rgbTo256(r, g, b) {
22
+ // Convert 0-1 floats to 0-255
23
+ const r8 = Math.round(r * 255);
24
+ const g8 = Math.round(g * 255);
25
+ const b8 = Math.round(b * 255);
26
+ // Grayscale detection - if all channels are close
27
+ if (Math.abs(r8 - g8) < 8 && Math.abs(g8 - b8) < 8) {
28
+ const gray = (r8 + g8 + b8) / 3;
29
+ if (gray < 8)
30
+ return 16; // Black
31
+ if (gray > 248)
32
+ return 231; // White
33
+ // Grayscale ramp: 232-255 (24 shades)
34
+ return Math.round((gray - 8) / 10) + 232;
35
+ }
36
+ // 6x6x6 color cube: indices 16-231
37
+ const ri = Math.round(r * 5);
38
+ const gi = Math.round(g * 5);
39
+ const bi = Math.round(b * 5);
40
+ return 16 + 36 * ri + 6 * gi + bi;
41
+ }
42
+ /**
43
+ * Convert RGB (0-1 floats) to nearest 16-color ANSI index.
44
+ * Returns 0-7 for normal colors, 8-15 for bright colors.
45
+ */
46
+ function rgbTo16(r, g, b) {
47
+ // Determine brightness
48
+ const brightness = (r + g + b) / 3;
49
+ const bright = brightness > 0.5 ? 8 : 0;
50
+ // Determine which channels are "on"
51
+ const threshold = 0.33;
52
+ const rb = r > threshold ? 1 : 0;
53
+ const gb = g > threshold ? 2 : 0;
54
+ const bb = b > threshold ? 4 : 0;
55
+ return bright + rb + gb + bb;
56
+ }
57
+ /**
58
+ * Blend a color with the background based on alpha.
59
+ * Terminals don't support alpha, so we pre-blend.
60
+ */
61
+ function blendWithBackground(color, bg) {
62
+ const a = color.a;
63
+ return [
64
+ color.r * a + bg.r * (1 - a),
65
+ color.g * a + bg.g * (1 - a),
66
+ color.b * a + bg.b * (1 - a),
67
+ ];
68
+ }
69
+ /**
70
+ * Convert a single span to ANSI escape sequences.
71
+ */
72
+ export function spanToAnsi(span, level, themeBg) {
73
+ // No colors - return plain text
74
+ if (level === 0)
75
+ return span.text;
76
+ const codes = [];
77
+ // Foreground color
78
+ if (span.fg.a > 0.01) {
79
+ const [r, g, b] = themeBg ? blendWithBackground(span.fg, themeBg) : [span.fg.r, span.fg.g, span.fg.b];
80
+ if (level === 3) {
81
+ // Truecolor
82
+ codes.push(`38;2;${Math.round(r * 255)};${Math.round(g * 255)};${Math.round(b * 255)}`);
83
+ }
84
+ else if (level === 2) {
85
+ // 256 colors
86
+ codes.push(`38;5;${rgbTo256(r, g, b)}`);
87
+ }
88
+ else {
89
+ // 16 colors
90
+ const idx = rgbTo16(r, g, b);
91
+ codes.push(idx >= 8 ? `9${idx - 8}` : `3${idx}`);
92
+ }
93
+ }
94
+ // Background color
95
+ if (span.bg.a > 0.01) {
96
+ const [r, g, b] = themeBg ? blendWithBackground(span.bg, themeBg) : [span.bg.r, span.bg.g, span.bg.b];
97
+ if (level === 3) {
98
+ // Truecolor
99
+ codes.push(`48;2;${Math.round(r * 255)};${Math.round(g * 255)};${Math.round(b * 255)}`);
100
+ }
101
+ else if (level === 2) {
102
+ // 256 colors
103
+ codes.push(`48;5;${rgbTo256(r, g, b)}`);
104
+ }
105
+ else {
106
+ // 16 colors
107
+ const idx = rgbTo16(r, g, b);
108
+ codes.push(idx >= 8 ? `10${idx - 8}` : `4${idx}`);
109
+ }
110
+ }
111
+ // Text attributes
112
+ if (span.attributes & TextAttributes.BOLD)
113
+ codes.push("1");
114
+ if (span.attributes & TextAttributes.DIM)
115
+ codes.push("2");
116
+ if (span.attributes & TextAttributes.ITALIC)
117
+ codes.push("3");
118
+ if (span.attributes & TextAttributes.UNDERLINE)
119
+ codes.push("4");
120
+ if (span.attributes & TextAttributes.STRIKETHROUGH)
121
+ codes.push("9");
122
+ // If no styling, return plain text
123
+ if (codes.length === 0)
124
+ return span.text;
125
+ // Wrap text with ANSI codes and reset
126
+ return `\x1b[${codes.join(";")}m${span.text}\x1b[0m`;
127
+ }
128
+ /**
129
+ * Check if a line is empty (no spans or only whitespace).
130
+ */
131
+ function isLineEmpty(line) {
132
+ if (line.spans.length === 0)
133
+ return true;
134
+ return line.spans.every((span) => span.text.trim() === "");
135
+ }
136
+ /**
137
+ * Convert a CapturedFrame to ANSI-formatted string for terminal output.
138
+ * Trims empty lines from the end by default.
139
+ */
140
+ export function frameToAnsi(frame, themeBg, options = {}) {
141
+ const { trimEmptyLines = true } = options;
142
+ const level = getColorLevel();
143
+ let lines = frame.lines;
144
+ // Trim empty lines from the end
145
+ if (trimEmptyLines) {
146
+ while (lines.length > 0 && isLineEmpty(lines[lines.length - 1])) {
147
+ lines = lines.slice(0, -1);
148
+ }
149
+ }
150
+ // Convert each line to ANSI
151
+ return lines
152
+ .map((line) => line.spans.map((span) => spanToAnsi(span, level, themeBg)).join(""))
153
+ .join("\n");
154
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Count unescaped occurrences of a delimiter in a code string.
3
+ *
4
+ * Walks character by character. Backslash skips the next character,
5
+ * otherwise checks for the delimiter at the current position.
6
+ * Handles both single-char (`) and multi-char (""") delimiters.
7
+ */
8
+ export declare function countDelimiter(code: string, delimiter: string): number;
9
+ /**
10
+ * Balance paired delimiters in a unified diff patch for correct syntax
11
+ * highlighting.
12
+ *
13
+ * Pass 1 (tokenize): for each hunk, extract content lines and count
14
+ * delimiter occurrences.
15
+ *
16
+ * Pass 2 (repair): if a hunk has an odd count for any symmetric delimiter,
17
+ * classify the unmatched boundary token as a likely opener or closer and add
18
+ * the missing paired token on an existing content line.
19
+ *
20
+ * Pass 3 (hunk isolation): if a hunk leaves an asymmetric delimiter open,
21
+ * append its closing token to the last content line so the next hunk starts
22
+ * from a clean parser state.
23
+ */
24
+ export declare function balanceDelimiters(rawDiff: string, filetype?: string): string;
25
+ //# sourceMappingURL=balance-delimiters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"balance-delimiters.d.ts","sourceRoot":"","sources":["../src/balance-delimiters.ts"],"names":[],"mappings":"AA+FA;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEtE;AAseD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CA4E5E"}