@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,363 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "@opentuah/react/jsx-runtime";
|
|
2
|
+
// Web preview generation utilities for uploading diffs to critique.work.
|
|
3
|
+
// Renders diff components using opentui test renderer, converts to HTML with responsive layout,
|
|
4
|
+
// and uploads desktop/mobile versions for shareable diff viewing.
|
|
5
|
+
import { getResolvedTheme, rgbaToHex } from "./themes.js";
|
|
6
|
+
import { buildDirectoryTree } from "./directory-tree.js";
|
|
7
|
+
import { DiffRenderable } from "@opentuah/core";
|
|
8
|
+
/**
|
|
9
|
+
* Calculate the actual content height from root's children after layout.
|
|
10
|
+
* Returns the maximum bottom edge (top + height) of all children.
|
|
11
|
+
*/
|
|
12
|
+
function getContentHeight(root) {
|
|
13
|
+
const children = root.getChildren();
|
|
14
|
+
if (children.length === 0)
|
|
15
|
+
return 0;
|
|
16
|
+
let maxBottom = 0;
|
|
17
|
+
for (const child of children) {
|
|
18
|
+
const layout = child.getLayoutNode().getComputedLayout();
|
|
19
|
+
const bottom = layout.top + layout.height;
|
|
20
|
+
if (bottom > maxBottom) {
|
|
21
|
+
maxBottom = bottom;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return Math.ceil(maxBottom);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Find all DiffRenderable instances in the renderer tree.
|
|
28
|
+
* Walks the tree recursively checking instanceof DiffRenderable.
|
|
29
|
+
*/
|
|
30
|
+
function findDiffRenderables(root) {
|
|
31
|
+
const results = [];
|
|
32
|
+
function walk(node) {
|
|
33
|
+
if (!node.getChildren)
|
|
34
|
+
return;
|
|
35
|
+
for (const child of node.getChildren()) {
|
|
36
|
+
if (child instanceof DiffRenderable) {
|
|
37
|
+
results.push(child);
|
|
38
|
+
}
|
|
39
|
+
walk(child);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
walk(root);
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Wait for tree-sitter syntax highlighting to complete on all diff elements,
|
|
47
|
+
* then wait for rendering to stabilize (no more requestRender calls).
|
|
48
|
+
*
|
|
49
|
+
* Two-phase approach:
|
|
50
|
+
* 1. Wait for DiffRenderable.isHighlighting to become false on all diffs
|
|
51
|
+
* (deterministic, exits as soon as tree-sitter is done)
|
|
52
|
+
* 2. Wait for render idle — no new requestRender calls for idleMs
|
|
53
|
+
* (catches deferred rebuilds like DiffRenderable.requestRebuild which
|
|
54
|
+
* uses queueMicrotask to schedule buildView + requestRender after
|
|
55
|
+
* highlighting completes)
|
|
56
|
+
*
|
|
57
|
+
* Phase 2 is critical because DiffRenderable's split view rebuild happens
|
|
58
|
+
* asynchronously via microtask AFTER isHighlighting goes false. Without it,
|
|
59
|
+
* the captured frame may have concealed (unhighlighted) content on one side.
|
|
60
|
+
*/
|
|
61
|
+
async function waitForHighlightAndRenderStabilization(renderer, renderOnce, maxMs = 2000) {
|
|
62
|
+
const startTime = Date.now();
|
|
63
|
+
const pollMs = 20;
|
|
64
|
+
const idleMs = 80;
|
|
65
|
+
// Track render requests to detect when rendering has quiesced
|
|
66
|
+
let lastRenderTime = Date.now();
|
|
67
|
+
const originalRequestRender = renderer.root.requestRender.bind(renderer.root);
|
|
68
|
+
renderer.root.requestRender = function () {
|
|
69
|
+
lastRenderTime = Date.now();
|
|
70
|
+
originalRequestRender();
|
|
71
|
+
};
|
|
72
|
+
// Do one render cycle to kick off highlighting
|
|
73
|
+
await renderOnce();
|
|
74
|
+
// Phase 1: wait for isHighlighting to become false on all diffs
|
|
75
|
+
while (Date.now() - startTime < maxMs) {
|
|
76
|
+
const diffs = findDiffRenderables(renderer.root);
|
|
77
|
+
if (diffs.length === 0 || diffs.every(d => !d.isHighlighting)) {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
await new Promise(resolve => setTimeout(resolve, pollMs));
|
|
81
|
+
await renderOnce();
|
|
82
|
+
}
|
|
83
|
+
// Phase 2: wait for render to stabilize (catches deferred rebuilds)
|
|
84
|
+
// Reset the render timestamp so we wait for any post-highlight renders
|
|
85
|
+
lastRenderTime = Date.now();
|
|
86
|
+
await renderOnce();
|
|
87
|
+
while (Date.now() - startTime < maxMs) {
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
if (now - lastRenderTime >= idleMs)
|
|
90
|
+
break;
|
|
91
|
+
await new Promise(resolve => setTimeout(resolve, pollMs));
|
|
92
|
+
await renderOnce();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Render diff to CapturedFrame using opentui test renderer.
|
|
97
|
+
* Uses content-fitting: renders with initial height, measures actual content,
|
|
98
|
+
* then resizes to exact content height to avoid wasting memory.
|
|
99
|
+
*/
|
|
100
|
+
async function renderDiffToFrameWithSectionPositions(diffContent, options) {
|
|
101
|
+
const { createTestRenderer } = await import("@opentuah/core/testing");
|
|
102
|
+
const { createRoot } = await import("@opentuah/react");
|
|
103
|
+
const { getTreeSitterClient } = await import("@opentuah/core");
|
|
104
|
+
const { parsePatch, formatPatch } = await import("diff");
|
|
105
|
+
// Pre-initialize TreeSitter client to ensure syntax highlighting works
|
|
106
|
+
const tsClient = getTreeSitterClient();
|
|
107
|
+
await tsClient.initialize();
|
|
108
|
+
const { DiffView, DirectoryTreeView } = await import("./components/index.js");
|
|
109
|
+
const { getFileName, getOldFileName, countChanges, getFileStatus, getViewMode, processFiles, detectFiletype, stripSubmoduleHeaders, parseGitDiffFiles, } = await import("./diff-utils.js");
|
|
110
|
+
const { themeNames, defaultThemeName } = await import("./themes.js");
|
|
111
|
+
const themeName = options.themeName && themeNames.includes(options.themeName)
|
|
112
|
+
? options.themeName
|
|
113
|
+
: defaultThemeName;
|
|
114
|
+
// Parse the diff (with rename detection support)
|
|
115
|
+
const files = parseGitDiffFiles(stripSubmoduleHeaders(diffContent), parsePatch);
|
|
116
|
+
const filesWithRawDiff = processFiles(files, formatPatch);
|
|
117
|
+
const fileNames = filesWithRawDiff.map((file) => getFileName(file));
|
|
118
|
+
const treeFiles = filesWithRawDiff.map((file, idx) => {
|
|
119
|
+
const { additions, deletions } = countChanges(file.hunks);
|
|
120
|
+
return {
|
|
121
|
+
path: getFileName(file),
|
|
122
|
+
status: getFileStatus(file),
|
|
123
|
+
additions,
|
|
124
|
+
deletions,
|
|
125
|
+
fileIndex: idx,
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
const treeFileOrder = buildDirectoryTree(treeFiles)
|
|
129
|
+
.filter((node) => node.isFile && node.fileIndex !== undefined)
|
|
130
|
+
.map((node) => node.fileIndex);
|
|
131
|
+
if (filesWithRawDiff.length === 0) {
|
|
132
|
+
throw new Error("No files to display");
|
|
133
|
+
}
|
|
134
|
+
// Store refs to each file section container so we can map file headers
|
|
135
|
+
// to exact rendered line indexes (avoids text-regex false positives).
|
|
136
|
+
const fileSectionRefs = new Map();
|
|
137
|
+
// Get theme colors
|
|
138
|
+
const webTheme = getResolvedTheme(themeName);
|
|
139
|
+
const webBg = webTheme.background;
|
|
140
|
+
const webText = rgbaToHex(webTheme.text);
|
|
141
|
+
const webMuted = rgbaToHex(webTheme.textMuted);
|
|
142
|
+
const showNotice = options.showNotice === true;
|
|
143
|
+
// Create the diff view component
|
|
144
|
+
// NOTE: No height: "100%" - let content determine its natural height
|
|
145
|
+
function WebApp() {
|
|
146
|
+
return (_jsxs("box", { style: {
|
|
147
|
+
flexDirection: "column",
|
|
148
|
+
backgroundColor: webBg,
|
|
149
|
+
}, children: [_jsx("box", { style: { marginBottom: 2 }, children: _jsx(DirectoryTreeView, { files: treeFiles, themeName: themeName }) }), filesWithRawDiff.map((file, idx) => {
|
|
150
|
+
const fileName = getFileName(file);
|
|
151
|
+
const oldFileName = getOldFileName(file);
|
|
152
|
+
const filetype = detectFiletype(fileName);
|
|
153
|
+
const { additions, deletions } = countChanges(file.hunks);
|
|
154
|
+
// Use forced viewMode if set, otherwise auto-detect (higher threshold 150 for web vs TUI 100)
|
|
155
|
+
const viewMode = options.viewMode || getViewMode(additions, deletions, options.cols, 150);
|
|
156
|
+
return (_jsxs("box", { ref: (r) => {
|
|
157
|
+
if (r)
|
|
158
|
+
fileSectionRefs.set(idx, r);
|
|
159
|
+
else
|
|
160
|
+
fileSectionRefs.delete(idx);
|
|
161
|
+
}, style: { flexDirection: "column", marginBottom: 2 }, children: [_jsxs("box", { style: {
|
|
162
|
+
paddingBottom: 1,
|
|
163
|
+
paddingLeft: 1,
|
|
164
|
+
paddingRight: 1,
|
|
165
|
+
flexShrink: 0,
|
|
166
|
+
flexDirection: "row",
|
|
167
|
+
alignItems: "center",
|
|
168
|
+
}, children: [oldFileName ? (_jsxs(_Fragment, { children: [_jsx("text", { fg: webMuted, children: oldFileName.trim() }), _jsx("text", { fg: webMuted, children: " \u2192 " }), _jsx("text", { fg: webText, children: fileName.trim() })] })) : (_jsx("text", { fg: webText, children: fileName.trim() })), _jsxs("text", { fg: "#2d8a47", children: [" +", additions] }), _jsxs("text", { fg: "#c53b53", children: ["-", deletions] })] }), _jsx(DiffView, { diff: file.rawDiff || "", view: viewMode, filetype: filetype, themeName: themeName, wrapMode: options.wrapMode })] }, idx));
|
|
169
|
+
})] }));
|
|
170
|
+
}
|
|
171
|
+
// Content-fitting rendering:
|
|
172
|
+
// 1. Start with small initial height
|
|
173
|
+
// 2. If content is clipped (content height == buffer height), double the buffer
|
|
174
|
+
// 3. Repeat until content fits or we hit max
|
|
175
|
+
// 4. Shrink to exact content height
|
|
176
|
+
let currentHeight = 100; // Start small
|
|
177
|
+
const { renderer, renderOnce, resize } = await createTestRenderer({
|
|
178
|
+
width: options.cols,
|
|
179
|
+
height: currentHeight,
|
|
180
|
+
});
|
|
181
|
+
// Mount and do initial render
|
|
182
|
+
createRoot(renderer).render(_jsx(WebApp, {}));
|
|
183
|
+
await renderOnce();
|
|
184
|
+
// Wait for React to mount components (may take a few render cycles)
|
|
185
|
+
let contentHeight = getContentHeight(renderer.root);
|
|
186
|
+
while (contentHeight === 0) {
|
|
187
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
188
|
+
await renderOnce();
|
|
189
|
+
contentHeight = getContentHeight(renderer.root);
|
|
190
|
+
}
|
|
191
|
+
// If content height == buffer height, content is clipped - double until it fits
|
|
192
|
+
while (contentHeight >= currentHeight && currentHeight < options.maxRows) {
|
|
193
|
+
currentHeight = Math.min(currentHeight * 2, options.maxRows);
|
|
194
|
+
resize(options.cols, currentHeight);
|
|
195
|
+
await renderOnce();
|
|
196
|
+
contentHeight = getContentHeight(renderer.root);
|
|
197
|
+
}
|
|
198
|
+
// Shrink to exact content height (remove empty space at bottom)
|
|
199
|
+
const finalHeight = Math.min(Math.max(contentHeight, 1), options.maxRows);
|
|
200
|
+
if (finalHeight < renderer.height) {
|
|
201
|
+
resize(options.cols, finalHeight);
|
|
202
|
+
await renderOnce();
|
|
203
|
+
}
|
|
204
|
+
// Wait for tree-sitter highlighting + render stabilization
|
|
205
|
+
await waitForHighlightAndRenderStabilization(renderer, renderOnce, options.stabilizeMs ?? 2000);
|
|
206
|
+
const sectionPositions = [];
|
|
207
|
+
for (let idx = 0; idx < fileNames.length; idx++) {
|
|
208
|
+
const section = fileSectionRefs.get(idx);
|
|
209
|
+
if (!section)
|
|
210
|
+
continue;
|
|
211
|
+
const layout = section.getLayoutNode().getComputedLayout();
|
|
212
|
+
sectionPositions.push({
|
|
213
|
+
lineIndex: Math.max(0, Math.round(layout.top)),
|
|
214
|
+
fileName: fileNames[idx],
|
|
215
|
+
fileIndex: idx,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
// Capture the final frame
|
|
219
|
+
const buffer = renderer.currentRenderBuffer;
|
|
220
|
+
const cursorState = renderer.getCursorState();
|
|
221
|
+
const frame = {
|
|
222
|
+
cols: buffer.width,
|
|
223
|
+
rows: buffer.height,
|
|
224
|
+
cursor: [cursorState.x, cursorState.y],
|
|
225
|
+
lines: buffer.getSpanLines(),
|
|
226
|
+
};
|
|
227
|
+
renderer.destroy();
|
|
228
|
+
return {
|
|
229
|
+
frame,
|
|
230
|
+
sectionPositions,
|
|
231
|
+
treeFileOrder,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
export async function renderDiffToFrame(diffContent, options) {
|
|
235
|
+
const { frame } = await renderDiffToFrameWithSectionPositions(diffContent, options);
|
|
236
|
+
return frame;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Convert a file path to a URL-safe anchor slug.
|
|
240
|
+
* e.g. "src/components/foo-bar.tsx" → "src-components-foo-bar-tsx"
|
|
241
|
+
*/
|
|
242
|
+
export function slugifyFileName(name) {
|
|
243
|
+
return name
|
|
244
|
+
.trim()
|
|
245
|
+
.toLowerCase()
|
|
246
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
247
|
+
.replace(/^-|-$/g, "");
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Return a unique ID, appending -2, -3, etc. if the base already exists.
|
|
251
|
+
*/
|
|
252
|
+
function dedupeId(id, usedIds) {
|
|
253
|
+
if (!usedIds.has(id))
|
|
254
|
+
return id;
|
|
255
|
+
let i = 2;
|
|
256
|
+
while (usedIds.has(`${id}-${i}`))
|
|
257
|
+
i++;
|
|
258
|
+
return `${id}-${i}`;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Extract line numbers from a captured diff line's spans.
|
|
262
|
+
* In split view each row has two line number columns (old + new):
|
|
263
|
+
* " " "29" " - " ...content... " " "29" " + " ...content...
|
|
264
|
+
* In unified view there's only one.
|
|
265
|
+
*
|
|
266
|
+
* Returns the new-file (right) line number when available,
|
|
267
|
+
* falling back to the old-file (left) number for deleted-only rows.
|
|
268
|
+
* Returns null for non-diff lines (headers, hunk markers, etc.).
|
|
269
|
+
*/
|
|
270
|
+
export function extractLineNumber(line) {
|
|
271
|
+
let firstNum = null;
|
|
272
|
+
let secondNum = null;
|
|
273
|
+
let foundNonEmpty = false;
|
|
274
|
+
for (const span of line.spans) {
|
|
275
|
+
const trimmed = span.text.trim();
|
|
276
|
+
if (trimmed === "")
|
|
277
|
+
continue;
|
|
278
|
+
if (/^\d+$/.test(trimmed)) {
|
|
279
|
+
if (!firstNum) {
|
|
280
|
+
firstNum = trimmed;
|
|
281
|
+
}
|
|
282
|
+
else if (!secondNum) {
|
|
283
|
+
secondNum = trimmed;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else if (!foundNonEmpty) {
|
|
287
|
+
// First non-empty span is not a number — not a diff line
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
foundNonEmpty = true;
|
|
291
|
+
}
|
|
292
|
+
// Prefer new-file (right/second) number; fall back to old-file (left/first)
|
|
293
|
+
return secondNum ?? firstNum;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Match a rendered tree file row and extract the file path label.
|
|
297
|
+
* Examples:
|
|
298
|
+
* "│ ├── index.ts (+5,-2)"
|
|
299
|
+
* "└── README.md (-15)"
|
|
300
|
+
*/
|
|
301
|
+
export function extractTreeFilePath(lineText) {
|
|
302
|
+
const match = lineText.match(/^\s*[│ ]*[├└]──\s+(.+?)\s+\([^)]*\)\s*$/);
|
|
303
|
+
if (!match || !match[1])
|
|
304
|
+
return null;
|
|
305
|
+
return match[1].trim();
|
|
306
|
+
}
|
|
307
|
+
function escapeHtmlAttribute(value) {
|
|
308
|
+
return value
|
|
309
|
+
.replace(/&/g, "&")
|
|
310
|
+
.replace(/"/g, """)
|
|
311
|
+
.replace(/</g, "<")
|
|
312
|
+
.replace(/>/g, ">");
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Build line-indexed anchors from file section layout positions.
|
|
316
|
+
* This avoids regex detection on rendered text, which can produce
|
|
317
|
+
* false positives when code lines mimic file-header patterns.
|
|
318
|
+
*/
|
|
319
|
+
export function buildAnchorMap(sections) {
|
|
320
|
+
const anchors = new Map();
|
|
321
|
+
const usedIds = new Set();
|
|
322
|
+
for (const section of sections) {
|
|
323
|
+
if (!Number.isFinite(section.lineIndex) || section.lineIndex < 0)
|
|
324
|
+
continue;
|
|
325
|
+
if (anchors.has(section.lineIndex))
|
|
326
|
+
continue;
|
|
327
|
+
const label = section.fileName.trim();
|
|
328
|
+
const baseSlug = slugifyFileName(label) || "file";
|
|
329
|
+
const id = dedupeId(baseSlug, usedIds);
|
|
330
|
+
usedIds.add(id);
|
|
331
|
+
anchors.set(section.lineIndex, { id, label });
|
|
332
|
+
}
|
|
333
|
+
return anchors;
|
|
334
|
+
}
|
|
335
|
+
// CSS for file section anchors — injected via extraCss hook.
|
|
336
|
+
// Clicking a file link copies the filename to clipboard and updates the URL hash.
|
|
337
|
+
const SECTION_ANCHOR_CSS = `
|
|
338
|
+
.file-section { scroll-margin-top: 16px; }
|
|
339
|
+
.file-link { color: inherit; text-decoration: none; cursor: copy; }
|
|
340
|
+
.file-link:hover { text-decoration: underline; }
|
|
341
|
+
.tree-file-link { color: inherit; text-decoration: none; }
|
|
342
|
+
.tree-file-link:hover { text-decoration: underline; }
|
|
343
|
+
`;
|
|
344
|
+
// JS: scroll to hash fragment on page load + click-to-copy filename on .file-link click.
|
|
345
|
+
// On click: copies the filename text to clipboard and updates the URL hash.
|
|
346
|
+
const SECTION_ANCHOR_JS = `
|
|
347
|
+
// Scroll to hash on page load
|
|
348
|
+
if (location.hash) {
|
|
349
|
+
var el = document.getElementById(location.hash.slice(1));
|
|
350
|
+
if (el) setTimeout(function () { el.scrollIntoView({ behavior: 'smooth' }) }, 100);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Click file link: copy filename to clipboard + update URL hash
|
|
354
|
+
document.addEventListener('click', function (e) {
|
|
355
|
+
var link = e.target.closest('.file-link');
|
|
356
|
+
if (!link) return;
|
|
357
|
+
e.preventDefault();
|
|
358
|
+
var text = link.textContent;
|
|
359
|
+
var section = link.closest('.file-section');
|
|
360
|
+
if (section && section.id) history.replaceState(null, '', '#' + section.id);
|
|
361
|
+
navigator.clipboard.writeText(text);
|
|
362
|
+
});
|
|
363
|
+
`;
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@abelfubu/dv",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": "./dist/cli.js",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"public"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"cli": "bun src/cli.tsx",
|
|
16
|
+
"cli:watch": "bun --watch src/cli.tsx",
|
|
17
|
+
"build": "rm -rf dist && tsc",
|
|
18
|
+
"prepublishOnly": "bun run build"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/bun": "1.3.5",
|
|
22
|
+
"tuistory": "^0.0.13",
|
|
23
|
+
"typescript": "^5.9.3"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@opentuah/core": "0.1.97",
|
|
27
|
+
"@opentuah/react": "0.1.97",
|
|
28
|
+
"@parcel/watcher": "^2.5.6",
|
|
29
|
+
"diff": "^8.0.2",
|
|
30
|
+
"goke": "^6.6.1",
|
|
31
|
+
"picocolors": "^1.1.1",
|
|
32
|
+
"react": "^19.2.0",
|
|
33
|
+
"strip-ansi": "^7.1.2",
|
|
34
|
+
"supports-color": "^10.2.2",
|
|
35
|
+
"zustand": "^5.0.8"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
Binary file
|
|
Binary file
|