@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.
- package/dist/ansi-html.d.ts +42 -0
- package/dist/ansi-html.d.ts.map +1 -0
- package/dist/ansi-html.js +327 -0
- package/dist/ansi-output.d.ts +22 -0
- package/dist/ansi-output.d.ts.map +1 -0
- package/dist/ansi-output.js +154 -0
- package/dist/balance-delimiters.d.ts +25 -0
- package/dist/balance-delimiters.d.ts.map +1 -0
- package/dist/balance-delimiters.js +539 -0
- package/dist/balance-delimiters.test.d.ts +2 -0
- package/dist/balance-delimiters.test.d.ts.map +1 -0
- package/dist/balance-delimiters.test.js +1029 -0
- package/dist/cli-copy-notification.test.d.ts +2 -0
- package/dist/cli-copy-notification.test.d.ts.map +1 -0
- package/dist/cli-copy-notification.test.js +80 -0
- package/dist/cli-scroll.test.d.ts +2 -0
- package/dist/cli-scroll.test.d.ts.map +1 -0
- package/dist/cli-scroll.test.js +283 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +976 -0
- package/dist/clipboard.d.ts +16 -0
- package/dist/clipboard.d.ts.map +1 -0
- package/dist/clipboard.js +128 -0
- package/dist/components/diff-view.d.ts +32 -0
- package/dist/components/diff-view.d.ts.map +1 -0
- package/dist/components/diff-view.js +123 -0
- package/dist/components/diff-view.test.d.ts +5 -0
- package/dist/components/diff-view.test.d.ts.map +1 -0
- package/dist/components/diff-view.test.js +312 -0
- package/dist/components/directory-tree-view.d.ts +33 -0
- package/dist/components/directory-tree-view.d.ts.map +1 -0
- package/dist/components/directory-tree-view.js +262 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +5 -0
- package/dist/components/toast.d.ts +21 -0
- package/dist/components/toast.d.ts.map +1 -0
- package/dist/components/toast.js +47 -0
- package/dist/diff-cursor-utils.d.ts +20 -0
- package/dist/diff-cursor-utils.d.ts.map +1 -0
- package/dist/diff-cursor-utils.js +105 -0
- package/dist/diff-cursor-utils.test.d.ts +2 -0
- package/dist/diff-cursor-utils.test.d.ts.map +1 -0
- package/dist/diff-cursor-utils.test.js +40 -0
- package/dist/diff-surface-copy.d.ts +23 -0
- package/dist/diff-surface-copy.d.ts.map +1 -0
- package/dist/diff-surface-copy.js +64 -0
- package/dist/diff-surface-copy.test.d.ts +5 -0
- package/dist/diff-surface-copy.test.d.ts.map +1 -0
- package/dist/diff-surface-copy.test.js +142 -0
- package/dist/diff-utils.d.ts +196 -0
- package/dist/diff-utils.d.ts.map +1 -0
- package/dist/diff-utils.js +682 -0
- package/dist/diff-utils.test.d.ts +2 -0
- package/dist/diff-utils.test.d.ts.map +1 -0
- package/dist/diff-utils.test.js +727 -0
- package/dist/directory-tree.d.ts +72 -0
- package/dist/directory-tree.d.ts.map +1 -0
- package/dist/directory-tree.js +161 -0
- package/dist/directory-tree.test.d.ts +2 -0
- package/dist/directory-tree.test.d.ts.map +1 -0
- package/dist/directory-tree.test.js +383 -0
- package/dist/dropdown.d.ts +26 -0
- package/dist/dropdown.d.ts.map +1 -0
- package/dist/dropdown.js +172 -0
- package/dist/dropdown.test.d.ts +2 -0
- package/dist/dropdown.test.d.ts.map +1 -0
- package/dist/dropdown.test.js +106 -0
- package/dist/filter-submodule.e2e.test.d.ts +2 -0
- package/dist/filter-submodule.e2e.test.d.ts.map +1 -0
- package/dist/filter-submodule.e2e.test.js +109 -0
- package/dist/hooks/use-copy-selection.d.ts +29 -0
- package/dist/hooks/use-copy-selection.d.ts.map +1 -0
- package/dist/hooks/use-copy-selection.js +46 -0
- package/dist/kv-codec.d.ts +16 -0
- package/dist/kv-codec.d.ts.map +1 -0
- package/dist/kv-codec.js +36 -0
- package/dist/license.d.ts +14 -0
- package/dist/license.d.ts.map +1 -0
- package/dist/license.js +63 -0
- package/dist/logger.d.ts +9 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +78 -0
- package/dist/monochrome.d.ts +34 -0
- package/dist/monochrome.d.ts.map +1 -0
- package/dist/monochrome.js +613 -0
- package/dist/monotone.d.ts +22 -0
- package/dist/monotone.d.ts.map +1 -0
- package/dist/monotone.js +185 -0
- package/dist/parsers-config.d.ts +19 -0
- package/dist/parsers-config.d.ts.map +1 -0
- package/dist/parsers-config.js +271 -0
- package/dist/patch-terminal-dimensions.d.ts +2 -0
- package/dist/patch-terminal-dimensions.d.ts.map +1 -0
- package/dist/patch-terminal-dimensions.js +45 -0
- package/dist/stdin-pager.test.d.ts +2 -0
- package/dist/stdin-pager.test.d.ts.map +1 -0
- package/dist/stdin-pager.test.js +497 -0
- package/dist/store.d.ts +16 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +48 -0
- package/dist/themes/github.json +247 -0
- package/dist/themes.d.ts +59 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +248 -0
- package/dist/tree-icons.d.ts +4 -0
- package/dist/tree-icons.d.ts.map +1 -0
- package/dist/tree-icons.js +18 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +13 -0
- package/dist/web-utils.d.ts +56 -0
- package/dist/web-utils.d.ts.map +1 -0
- package/dist/web-utils.js +363 -0
- package/package.json +37 -0
- package/public/jetbrains-mono-nerd.ttf +0 -0
- 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, "&")
|
|
14
|
+
.replace(/</g, "<")
|
|
15
|
+
.replace(/>/g, ">")
|
|
16
|
+
.replace(/"/g, """);
|
|
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> </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"}
|