@youtyan/code-viewer 0.1.16 → 0.1.18

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/web/style.css CHANGED
@@ -2575,6 +2575,18 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
2575
2575
  font-size: 16px;
2576
2576
  line-height: 1.75;
2577
2577
  }
2578
+ .gdp-html-preview {
2579
+ width: 100%;
2580
+ min-height: min(78vh, 920px);
2581
+ background: var(--bg);
2582
+ }
2583
+ .gdp-html-preview iframe {
2584
+ display: block;
2585
+ width: 100%;
2586
+ min-height: min(78vh, 920px);
2587
+ border: 0;
2588
+ background: #fff;
2589
+ }
2578
2590
  .gdp-markdown-layout {
2579
2591
  display: grid;
2580
2592
  grid-template-columns: 260px minmax(0, 1fr);
package/web-src/routes.ts DELETED
@@ -1,148 +0,0 @@
1
- export type DiffRange = {
2
- from: string;
3
- to: string;
4
- };
5
-
6
- export type SourceLineRange = {
7
- start: number;
8
- end: number;
9
- };
10
-
11
- export type SourceLineTarget = number | SourceLineRange;
12
-
13
- export type SourceFileTarget = {
14
- path: string;
15
- ref: string;
16
- };
17
-
18
- export type AppRoute =
19
- | { screen: 'repo'; ref: string; path: string; range: DiffRange }
20
- | { screen: 'diff'; range: DiffRange; path?: string; line?: SourceLineTarget }
21
- | { screen: 'file'; path: string; ref: string; range: DiffRange; view?: 'blob' | 'detail'; line?: SourceLineTarget }
22
- | { screen: 'help'; range: DiffRange; lang: string; section: string }
23
- | { screen: 'unknown'; reason: 'unknown-pathname' | 'missing-path'; rawPathname: string; rawSearch: string; range: DiffRange };
24
-
25
- export const SPA_PATHS = ['/todif', '/todiff', '/file', '/help'] as const;
26
- export const APP_ENTRY_PATHS = ['/', '/index.html'] as const;
27
-
28
- export function assertNever(value: never): never {
29
- throw new Error('unhandled route: ' + JSON.stringify(value));
30
- }
31
-
32
- function parseLegacyRange(value: string | null | undefined, fallback: DiffRange): DiffRange {
33
- const raw = value || '';
34
- const sep = raw.indexOf('..');
35
- if (sep < 0) return fallback;
36
- return {
37
- from: raw.slice(0, sep) || fallback.from,
38
- to: raw.slice(sep + 2) || fallback.to,
39
- };
40
- }
41
-
42
- function parseLineTarget(value: string | null | undefined): SourceLineTarget | undefined {
43
- const raw = value || '';
44
- const range = /^(\d+)-(\d+)$/.exec(raw);
45
- if (range) {
46
- const a = Number(range[1]);
47
- const b = Number(range[2]);
48
- const start = Math.min(a, b);
49
- const end = Math.max(a, b);
50
- if (start > 0) return { start, end };
51
- return undefined;
52
- }
53
- const line = Number(raw);
54
- return Number.isInteger(line) && line > 0 ? line : undefined;
55
- }
56
-
57
- function formatLineTarget(line: SourceLineTarget): string {
58
- return typeof line === 'number' ? String(line) : line.start + '-' + line.end;
59
- }
60
-
61
- export function parseRoute(pathname: string, search: string, fallbackRange: DiffRange): AppRoute {
62
- const params = new URLSearchParams(search);
63
- const legacyRange = parseLegacyRange(params.get('range'), fallbackRange);
64
- const range = {
65
- from: params.get('from') || legacyRange.from,
66
- to: params.get('to') || legacyRange.to,
67
- };
68
- switch (pathname) {
69
- case '/':
70
- case '/index.html':
71
- return {
72
- screen: 'repo',
73
- ref: params.get('ref') || params.get('target') || 'worktree',
74
- path: params.get('path') || '',
75
- range,
76
- };
77
- case '/todif':
78
- case '/todiff':
79
- return {
80
- screen: 'diff',
81
- range,
82
- ...(params.get('path') ? { path: params.get('path') || '' } : {}),
83
- ...(parseLineTarget(params.get('line')) ? { line: parseLineTarget(params.get('line')) } : {}),
84
- };
85
- case '/file': {
86
- const path = params.get('path') || '';
87
- const target = params.get('target') || '';
88
- const ref = target || params.get('ref') || 'worktree';
89
- const line = parseLineTarget(params.get('line'));
90
- if (!path) return { screen: 'unknown', reason: 'missing-path', rawPathname: pathname, rawSearch: search, range };
91
- return { screen: 'file', path, ref, range, view: target ? 'blob' : 'detail', ...(line ? { line } : {}) };
92
- }
93
- case '/help':
94
- return {
95
- screen: 'help',
96
- range,
97
- lang: params.get('lang') || 'en',
98
- section: params.get('section') || 'keybindings',
99
- };
100
- default:
101
- return { screen: 'unknown', reason: 'unknown-pathname', rawPathname: pathname, rawSearch: search, range };
102
- }
103
- }
104
-
105
- export function buildRoute(route: AppRoute): string {
106
- switch (route.screen) {
107
- case 'repo': {
108
- const params = new URLSearchParams();
109
- if (route.ref && route.ref !== 'worktree') params.set('ref', route.ref);
110
- if (route.path) params.set('path', route.path);
111
- const qs = params.toString();
112
- return '/' + (qs ? '?' + qs : '');
113
- }
114
- case 'file':
115
- if (route.view === 'blob') {
116
- return '/file?path=' + encodeURIComponent(route.path) +
117
- '&target=' + encodeURIComponent(route.ref || 'worktree') +
118
- (route.line ? '&line=' + encodeURIComponent(formatLineTarget(route.line)) : '');
119
- }
120
- return '/file?path=' + encodeURIComponent(route.path) +
121
- '&ref=' + encodeURIComponent(route.ref || 'worktree') +
122
- '&from=' + encodeURIComponent(route.range.from || '') +
123
- '&to=' + encodeURIComponent(route.range.to || 'worktree') +
124
- (route.line ? '&line=' + encodeURIComponent(formatLineTarget(route.line)) : '');
125
- case 'diff':
126
- return '/todif?from=' + encodeURIComponent(route.range.from || '') +
127
- '&to=' + encodeURIComponent(route.range.to || 'worktree') +
128
- (route.path ? '&path=' + encodeURIComponent(route.path) : '') +
129
- (route.line ? '&line=' + encodeURIComponent(formatLineTarget(route.line)) : '');
130
- case 'help': {
131
- const params = new URLSearchParams();
132
- if (route.lang && route.lang !== 'en') params.set('lang', route.lang);
133
- if (route.section && route.section !== 'keybindings') params.set('section', route.section);
134
- const qs = params.toString();
135
- return '/help' + (qs ? '?' + qs : '');
136
- }
137
- case 'unknown':
138
- return '/todif?from=' + encodeURIComponent(route.range.from || '') +
139
- '&to=' + encodeURIComponent(route.range.to || 'worktree');
140
- default:
141
- return assertNever(route);
142
- }
143
- }
144
-
145
- export function buildRawFileUrl(target: SourceFileTarget): string {
146
- return '/_file?path=' + encodeURIComponent(target.path) +
147
- '&ref=' + encodeURIComponent(target.ref || 'worktree');
148
- }
@@ -1,64 +0,0 @@
1
- import { lstatSync } from 'node:fs';
2
- import { join } from 'node:path';
3
-
4
- // Short enough that a browser reload self-heals stale git data, while still
5
- // coalescing bursts from one render pass.
6
- export const CACHE_TTL_MS = 1500;
7
- export const MAX_TIMED_CACHE_ENTRIES = 200;
8
-
9
- export type TimedCacheEntry<T> = T & { storedAt: number };
10
-
11
- export function cacheFresh<T>(
12
- cached: TimedCacheEntry<T> | undefined,
13
- now = Date.now(),
14
- ttlMs = CACHE_TTL_MS,
15
- ): cached is TimedCacheEntry<T> {
16
- return !!cached && now - cached.storedAt <= ttlMs;
17
- }
18
-
19
- export function setTimedCacheEntry<T>(
20
- cache: Map<string, TimedCacheEntry<T>>,
21
- key: string,
22
- value: T,
23
- now = Date.now(),
24
- maxEntries = MAX_TIMED_CACHE_ENTRIES,
25
- ): void {
26
- cache.set(key, { ...value, storedAt: now });
27
- while (cache.size > maxEntries) {
28
- const oldest = cache.keys().next().value;
29
- if (oldest === undefined) break;
30
- cache.delete(oldest);
31
- }
32
- }
33
-
34
- export function worktreeFileSignature(path: string, cwd: string): string {
35
- try {
36
- const stats = lstatSync(join(cwd, path));
37
- const inode = 'ino' in stats ? stats.ino : 0;
38
- return `state:file|size:${stats.size}|mtime:${stats.mtimeMs}|ctime:${stats.ctimeMs}|ino:${inode}`;
39
- } catch {
40
- return 'state:missing';
41
- }
42
- }
43
-
44
- export function fileDiffCacheKey(options: {
45
- path: string;
46
- oldPath?: string | null;
47
- isUntracked: boolean;
48
- range: { from?: string; to?: string };
49
- extras: string[];
50
- args: string[];
51
- cwd: string;
52
- }): string {
53
- const worktreeTarget = options.range.from === 'worktree' || !options.range.to || options.range.to === 'worktree';
54
- if (options.isUntracked && !worktreeTarget) {
55
- throw new Error('untracked file diffs require a worktree range');
56
- }
57
- const signature = worktreeTarget
58
- ? `\0${worktreeFileSignature(options.path, options.cwd)}`
59
- : '';
60
- if (options.isUntracked) {
61
- return `u\0${options.path}${signature}\0${options.extras.join('\0')}`;
62
- }
63
- return `t\0${options.path}\0${options.oldPath || ''}${signature}\0${[...options.extras, ...options.args].join('\0')}`;
64
- }
@@ -1,37 +0,0 @@
1
- import { basename } from 'node:path';
2
-
3
- export type WatchFn = (
4
- path: string,
5
- options: { persistent?: boolean },
6
- listener: (eventType: string, filename: string | Buffer | null) => void,
7
- ) => unknown;
8
-
9
- type DevAssetReloadOptions = {
10
- enabled: boolean;
11
- webRoot: string;
12
- watchedFiles: readonly string[];
13
- watch: WatchFn;
14
- sendReload: () => void;
15
- setTimeoutFn?: typeof setTimeout;
16
- clearTimeoutFn?: typeof clearTimeout;
17
- debounceMs?: number;
18
- };
19
-
20
- export function startDevAssetReload(options: DevAssetReloadOptions): boolean {
21
- if (!options.enabled) return false;
22
- const watched = new Set(options.watchedFiles);
23
- const setTimer = options.setTimeoutFn || setTimeout;
24
- const clearTimer = options.clearTimeoutFn || clearTimeout;
25
- const debounceMs = options.debounceMs ?? 150;
26
- let timer: ReturnType<typeof setTimeout> | null = null;
27
-
28
- options.watch(options.webRoot, { persistent: false }, (_event, filename) => {
29
- if (!filename || !watched.has(basename(filename.toString()))) return;
30
- if (timer) clearTimer(timer);
31
- timer = setTimer(() => {
32
- timer = null;
33
- options.sendReload();
34
- }, debounceMs);
35
- });
36
- return true;
37
- }
@@ -1,100 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- import { readdirSync, statSync } from 'node:fs';
4
- import { join, normalize } from 'node:path';
5
-
6
- const ROOT = normalize(join(import.meta.dir, '..', '..'));
7
- const SERVER_ROOT = join(ROOT, 'web-src', 'server');
8
- const DEFAULT_DEV_PORT = 64160;
9
-
10
- type ChildProcess = {
11
- kill(signal?: string): void;
12
- exited: Promise<number>;
13
- };
14
-
15
- let server: ChildProcess | null = null;
16
- let build: ChildProcess | null = null;
17
- let restarting = false;
18
- let firstStart = true;
19
-
20
- function withDefaultPort(args: string[]) {
21
- if (args.includes('--port')) return args;
22
- return ['--port', String(DEFAULT_DEV_PORT), ...args];
23
- }
24
-
25
- function withoutOpen(args: string[]) {
26
- return args.filter((arg) => arg !== '--open');
27
- }
28
-
29
- function serverArgs() {
30
- const args = withDefaultPort(process.argv.slice(2));
31
- return firstStart ? args : withoutOpen(args);
32
- }
33
-
34
- function watchedFiles() {
35
- return readdirSync(SERVER_ROOT)
36
- .filter((name) => name.endsWith('.ts') && name !== 'runtime.d.ts')
37
- .map((name) => join(SERVER_ROOT, name))
38
- .concat(join(ROOT, 'web-src', 'types.ts'));
39
- }
40
-
41
- function watchSignature() {
42
- return watchedFiles()
43
- .map((file) => `${file}:${statSync(file).mtimeMs}`)
44
- .join('|');
45
- }
46
-
47
- function startBuild() {
48
- build = Bun.spawn([
49
- 'bun', 'build', '--watch', '--target=browser', '--format=iife',
50
- '--outfile=web/app.js', 'web-src/app.ts',
51
- ], { cwd: ROOT, stdout: 'inherit', stderr: 'inherit' }) as ChildProcess;
52
- }
53
-
54
- function startServer() {
55
- const args = serverArgs();
56
- firstStart = false;
57
- server = Bun.spawn([
58
- 'bun', 'run', 'web-src/server/preview.ts', ...args,
59
- ], {
60
- cwd: ROOT,
61
- stdout: 'inherit',
62
- stderr: 'inherit',
63
- env: { ...process.env, CODE_VIEWER_DEV: '1' },
64
- }) as ChildProcess;
65
- }
66
-
67
- async function restartServer() {
68
- if (restarting) return;
69
- restarting = true;
70
- const old = server;
71
- server = null;
72
- if (old) {
73
- old.kill();
74
- await old.exited.catch(() => 1);
75
- }
76
- startServer();
77
- restarting = false;
78
- }
79
-
80
- function shutdown() {
81
- if (server) server.kill();
82
- if (build) build.kill();
83
- process.exit(0);
84
- }
85
-
86
- process.on('SIGINT', shutdown);
87
- process.on('SIGTERM', shutdown);
88
-
89
- console.log(`code-viewer dev server watching ${SERVER_ROOT}`);
90
- startBuild();
91
- startServer();
92
-
93
- let sig = watchSignature();
94
- setInterval(() => {
95
- const next = watchSignature();
96
- if (next === sig) return;
97
- sig = next;
98
- console.log('server source changed; restarting preview server');
99
- restartServer();
100
- }, 500);