@youtyan/code-viewer 0.9.2 → 0.10.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/README.md CHANGED
@@ -39,9 +39,19 @@ Requires Node.js 20 or newer when installed from npm. Development uses
39
39
  too, without needing a `docker-compose.yml`.
40
40
  Table descriptions appear inside expanded table entries and in the Schema
41
41
  tab header when the database provides them.
42
- - Read the built-in Help page for getting started, the `.code-viewer/`
43
- project files, AI annotations, datastores, the agent skill, and
44
- keybindings.
42
+ - Read the built-in Help page (last item in the header menu) for getting
43
+ started, the `.code-viewer/` project files, AI annotations, datastores,
44
+ the agent skill, and keybindings.
45
+ - Scratch on pasted text without leaving the current screen with the Tools
46
+ drawer (the `Tools` item in the header menu, or `?tools=<tool>` on any
47
+ URL): Markdown
48
+ preview (same renderer as file preview, so table of contents, task lists,
49
+ frontmatter, code highlighting and ` ```mermaid ` fences all work),
50
+ Mermaid preview with zoom and drag-pan, and a JSON / YAML tool that
51
+ auto-detects the input and re-emits it as formatted JSON or YAML (also a
52
+ validator and a JSON⇄YAML converter). Each tool keeps its own draft in
53
+ `.code-viewer/tools.json`, and the drawer width is draggable from its left
54
+ edge.
45
55
  - Inspect the runtime with the Environment Doctor (right-side sheet,
46
56
  toggled by the 🩺 icon in the header): runtime (Node / Bun / ABI),
47
57
  `@youtyan/code-viewer` version and execution origin (npx cache vs
@@ -129,6 +129,19 @@ function makeTimedId(prefix) {
129
129
  }
130
130
  var BASE36_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
131
131
 
132
+ // web-src/core/tools.ts
133
+ function isToolId(value) {
134
+ return TOOL_IDS.includes(value);
135
+ }
136
+ var TOOL_IDS, MIN_TOOLS_SHEET_WIDTH = 480, MAX_TOOLS_SHEET_WIDTH = 4000;
137
+ var init_tools = __esm(() => {
138
+ TOOL_IDS = [
139
+ "markdown",
140
+ "mermaid",
141
+ "json"
142
+ ];
143
+ });
144
+
132
145
  // web-src/core/routes.ts
133
146
  function assertNever(value) {
134
147
  throw new Error(`unhandled route: ${JSON.stringify(value)}`);
@@ -232,6 +245,7 @@ function buildRoute(route) {
232
245
  }
233
246
  var SPA_PATHS, APP_ENTRY_PATHS;
234
247
  var init_routes = __esm(() => {
248
+ init_tools();
235
249
  SPA_PATHS = [
236
250
  "/todif",
237
251
  "/todiff",
@@ -16021,35 +16035,105 @@ function mergeDbUiState(current, patch) {
16021
16035
  version: 1
16022
16036
  });
16023
16037
  }
16038
+ function emptyToolsState() {
16039
+ return { version: 1 };
16040
+ }
16041
+ function sanitizeToolsState(raw) {
16042
+ if (!isRecord(raw))
16043
+ return emptyToolsState();
16044
+ const out = { version: 1 };
16045
+ if (isToolId(raw.activeTool))
16046
+ out.activeTool = raw.activeTool;
16047
+ const width = optionalNumber(raw.width, MIN_TOOLS_SHEET_WIDTH, MAX_TOOLS_SHEET_WIDTH);
16048
+ if (width !== undefined)
16049
+ out.width = width;
16050
+ if (isRecord(raw.drafts)) {
16051
+ const drafts = {};
16052
+ for (const id of TOOL_IDS) {
16053
+ const draft = raw.drafts[id];
16054
+ if (typeof draft !== "string")
16055
+ continue;
16056
+ if (draft.length === 0 || draft.length > MAX_TOOL_DRAFT_LEN)
16057
+ continue;
16058
+ drafts[id] = draft;
16059
+ }
16060
+ if (Object.keys(drafts).length > 0)
16061
+ out.drafts = drafts;
16062
+ }
16063
+ return out;
16064
+ }
16065
+ function mergeToolsState(current, patch) {
16066
+ if (!isRecord(patch))
16067
+ return current;
16068
+ const next = { ...current, version: 1 };
16069
+ if ("activeTool" in patch) {
16070
+ if (patch.activeTool === null)
16071
+ delete next.activeTool;
16072
+ else if (isToolId(patch.activeTool))
16073
+ next.activeTool = patch.activeTool;
16074
+ }
16075
+ if ("width" in patch) {
16076
+ if (patch.width === null)
16077
+ delete next.width;
16078
+ else {
16079
+ const width = optionalNumber(patch.width, MIN_TOOLS_SHEET_WIDTH, MAX_TOOLS_SHEET_WIDTH);
16080
+ if (width !== undefined)
16081
+ next.width = width;
16082
+ }
16083
+ }
16084
+ if (isRecord(patch.drafts)) {
16085
+ const drafts = { ...next.drafts ?? {} };
16086
+ for (const id of TOOL_IDS) {
16087
+ if (!(id in patch.drafts))
16088
+ continue;
16089
+ const value = patch.drafts[id];
16090
+ if (value === null || value === "") {
16091
+ delete drafts[id];
16092
+ continue;
16093
+ }
16094
+ if (typeof value !== "string")
16095
+ continue;
16096
+ if (value.length > MAX_TOOL_DRAFT_LEN)
16097
+ throw new Error("tools state too large");
16098
+ drafts[id] = value;
16099
+ }
16100
+ next.drafts = drafts;
16101
+ }
16102
+ return sanitizeToolsState(next);
16103
+ }
16104
+ function patchJsonState(store, root, patch, merge) {
16105
+ return store.update(root, (state) => {
16106
+ const next = merge(state, patch);
16107
+ return { state: next, result: next };
16108
+ });
16109
+ }
16024
16110
  async function loadAppSettingsState(root) {
16025
16111
  return settingsStore.load(root);
16026
16112
  }
16027
16113
  async function patchAppSettingsState(root, patch) {
16028
- return settingsStore.update(root, (state) => {
16029
- const next = mergeSettings(state, patch);
16030
- return { state: next, result: next };
16031
- });
16114
+ return patchJsonState(settingsStore, root, patch, mergeSettings);
16032
16115
  }
16033
16116
  async function loadViewState(root) {
16034
16117
  return viewStateStore.load(root);
16035
16118
  }
16036
16119
  async function patchViewState(root, patch) {
16037
- return viewStateStore.update(root, (state) => {
16038
- const next = mergeViewState(state, patch);
16039
- return { state: next, result: next };
16040
- });
16120
+ return patchJsonState(viewStateStore, root, patch, mergeViewState);
16041
16121
  }
16042
16122
  async function loadDbUiState(root) {
16043
16123
  return dbUiStore.load(root);
16044
16124
  }
16045
16125
  async function patchDbUiState(root, patch) {
16046
- return dbUiStore.update(root, (state) => {
16047
- const next = mergeDbUiState(state, patch);
16048
- return { state: next, result: next };
16049
- });
16126
+ return patchJsonState(dbUiStore, root, patch, mergeDbUiState);
16127
+ }
16128
+ async function loadToolsState(root) {
16129
+ return toolsStore.load(root);
16050
16130
  }
16051
- var CODE_VIEWER_DIR2 = ".code-viewer", SETTINGS_FILE_NAME = "settings.json", VIEW_STATE_FILE_NAME = "view-state.json", DB_UI_FILE_NAME = "db-ui.json", MAX_SETTINGS_BYTES = 200000, MAX_VIEW_STATE_BYTES = 1e6, MAX_DB_UI_BYTES = 1e6, MAX_REF_LEN = 1024, MAX_KEY_LEN = 2048, MAX_VIEW_ITEMS = 20000, MAX_DB_UI_DBS = 200, MAX_DB_UI_TABLES = 500, MAX_DB_UI_COLUMNS = 1000, MAX_DB_UI_EXPANDED_SCOPES = 500, DB_UI_BOOL_PREF_KEYS, settingsStore, viewStateStore, dbUiStore;
16131
+ async function patchToolsState(root, patch) {
16132
+ return patchJsonState(toolsStore, root, patch, mergeToolsState);
16133
+ }
16134
+ var CODE_VIEWER_DIR2 = ".code-viewer", SETTINGS_FILE_NAME = "settings.json", VIEW_STATE_FILE_NAME = "view-state.json", DB_UI_FILE_NAME = "db-ui.json", TOOLS_FILE_NAME = "tools.json", MAX_SETTINGS_BYTES = 200000, MAX_VIEW_STATE_BYTES = 1e6, MAX_DB_UI_BYTES = 1e6, MAX_TOOL_DRAFT_LEN = 200000, MAX_TOOLS_BYTES = 4000000, MAX_REF_LEN = 1024, MAX_KEY_LEN = 2048, MAX_VIEW_ITEMS = 20000, MAX_DB_UI_DBS = 200, MAX_DB_UI_TABLES = 500, MAX_DB_UI_COLUMNS = 1000, MAX_DB_UI_EXPANDED_SCOPES = 500, DB_UI_BOOL_PREF_KEYS, settingsStore, viewStateStore, dbUiStore, toolsStore;
16052
16135
  var init_state_store = __esm(() => {
16136
+ init_tools();
16053
16137
  init_json_store();
16054
16138
  init_worktree_watcher();
16055
16139
  DB_UI_BOOL_PREF_KEYS = ["s3TooltipEnabled", "inferFkRails"];
@@ -16077,6 +16161,14 @@ var init_state_store = __esm(() => {
16077
16161
  backupSuffix: "corrupt",
16078
16162
  sizeErrorMessage: "db UI state too large"
16079
16163
  });
16164
+ toolsStore = createJsonFileStore({
16165
+ filePath: (root) => codeViewerPath(root, TOOLS_FILE_NAME),
16166
+ empty: emptyToolsState,
16167
+ sanitize: sanitizeToolsState,
16168
+ maxBytes: MAX_TOOLS_BYTES,
16169
+ backupSuffix: "corrupt",
16170
+ sizeErrorMessage: "tools state too large"
16171
+ });
16080
16172
  });
16081
16173
 
16082
16174
  // web-src/server/database/adapters/async-facade.ts
@@ -25433,50 +25525,48 @@ var exports_state_route = {};
25433
25525
  __export(exports_state_route, {
25434
25526
  handleStateRoute: () => handleStateRoute
25435
25527
  });
25436
- async function parseJsonBody(req) {
25437
- return parseBoundedJsonBody(req, MAX_STATE_PATCH_BODY_BYTES, "state body too large");
25528
+ async function parseJsonBody(req, maxBytes) {
25529
+ return parseBoundedJsonBody(req, maxBytes, "state body too large");
25438
25530
  }
25439
- async function handleSettingsGet(cwd) {
25440
- return jsonLoadResponse(() => loadAppSettingsState(cwd), "state", "failed to load settings state");
25441
- }
25442
- async function handleSettingsPatch(cwd, req, onChange) {
25443
- const body = await parseJsonBody(req);
25531
+ async function handleStatePatch(cwd, req, patchState, tooLargeMessage, saveFailedMessage, maxBodyBytes, onChange) {
25532
+ const body = await parseJsonBody(req, maxBodyBytes);
25444
25533
  if (body instanceof Response)
25445
25534
  return body;
25446
25535
  try {
25447
- const next = await patchAppSettingsState(cwd, body);
25536
+ const next = await patchState(cwd, body);
25448
25537
  if (onChange) {
25449
25538
  try {
25450
25539
  onChange(next);
25451
25540
  } catch (notifyErr) {
25452
- console.warn("[code-viewer] settings change notify failed:", notifyErr);
25541
+ console.warn("[code-viewer] state change notify failed:", notifyErr);
25453
25542
  }
25454
25543
  }
25455
25544
  return json(next);
25456
25545
  } catch (err) {
25457
25546
  const message = err instanceof Error ? err.message : String(err);
25458
- if (message === "settings state too large")
25547
+ if (message === tooLargeMessage)
25459
25548
  return textError(message, 413);
25460
25549
  console.error("[code-viewer] state error:", err);
25461
- return textError("failed to save settings state", 500);
25550
+ return textError(saveFailedMessage, 500);
25462
25551
  }
25463
25552
  }
25553
+ async function handleSettingsGet(cwd) {
25554
+ return jsonLoadResponse(() => loadAppSettingsState(cwd), "state", "failed to load settings state");
25555
+ }
25556
+ async function handleSettingsPatch(cwd, req, onChange) {
25557
+ return handleStatePatch(cwd, req, patchAppSettingsState, "settings state too large", "failed to save settings state", MAX_STATE_PATCH_BODY_BYTES, onChange);
25558
+ }
25464
25559
  async function handleViewGet(cwd) {
25465
25560
  return jsonLoadResponse(() => loadViewState(cwd), "state", "failed to load view state");
25466
25561
  }
25467
25562
  async function handleViewPatch(cwd, req) {
25468
- const body = await parseJsonBody(req);
25469
- if (body instanceof Response)
25470
- return body;
25471
- try {
25472
- return json(await patchViewState(cwd, body));
25473
- } catch (err) {
25474
- const message = err instanceof Error ? err.message : String(err);
25475
- if (message === "view state too large")
25476
- return textError(message, 413);
25477
- console.error("[code-viewer] state error:", err);
25478
- return textError("failed to save view state", 500);
25479
- }
25563
+ return handleStatePatch(cwd, req, patchViewState, "view state too large", "failed to save view state", MAX_STATE_PATCH_BODY_BYTES);
25564
+ }
25565
+ async function handleToolsGet(cwd) {
25566
+ return jsonLoadResponse(() => loadToolsState(cwd), "state", "failed to load tools state");
25567
+ }
25568
+ async function handleToolsPatch(cwd, req) {
25569
+ return handleStatePatch(cwd, req, patchToolsState, "tools state too large", "failed to save tools state", MAX_TOOLS_PATCH_BODY_BYTES);
25480
25570
  }
25481
25571
  async function handleStateRoute(req, url, cwd, sideEffectAllowed, options = {}) {
25482
25572
  return dispatchRoutes(req, url, {
@@ -25489,10 +25579,15 @@ async function handleStateRoute(req, url, cwd, sideEffectAllowed, options = {})
25489
25579
  methods: ["GET", "PATCH"],
25490
25580
  sideEffect: (method) => method !== "GET",
25491
25581
  handler: () => req.method === "GET" ? handleViewGet(cwd) : handleViewPatch(cwd, req)
25582
+ },
25583
+ "/_state/tools": {
25584
+ methods: ["GET", "PATCH"],
25585
+ sideEffect: (method) => method !== "GET",
25586
+ handler: () => req.method === "GET" ? handleToolsGet(cwd) : handleToolsPatch(cwd, req)
25492
25587
  }
25493
25588
  }, sideEffectAllowed, (res) => res, (err) => handleError("state", "handle state request", err));
25494
25589
  }
25495
- var MAX_STATE_PATCH_BODY_BYTES = 1e6;
25590
+ var MAX_STATE_PATCH_BODY_BYTES = 1e6, MAX_TOOLS_PATCH_BODY_BYTES = 4000000;
25496
25591
  var init_state_route = __esm(() => {
25497
25592
  init_handle_shared();
25498
25593
  init_state_store();
@@ -25718,6 +25813,7 @@ function staticFile(pathname) {
25718
25813
  "/app.js": ["app.js", "application/javascript; charset=utf-8"],
25719
25814
  "/mermaid.js": ["mermaid.js", "application/javascript; charset=utf-8"],
25720
25815
  "/shiki.js": ["shiki.js", "application/javascript; charset=utf-8"],
25816
+ "/yaml.js": ["yaml.js", "application/javascript; charset=utf-8"],
25721
25817
  "/vendor/diff2html/diff2html.min.css": [
25722
25818
  "vendor/diff2html/diff2html.min.css",
25723
25819
  "text/css; charset=utf-8"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,7 @@
28
28
  ],
29
29
  "scripts": {
30
30
  "build": "bun run build:web && bun run build:server",
31
- "build:web": "bun build --target=browser --format=iife --outfile=web/app.js web-src/app.ts && bun build --target=browser --format=esm --outfile=web/mermaid.js web-src/mermaid-entry.ts && bun build --target=browser --format=esm --outfile=web/shiki.js web-src/shiki-entry.ts && bun build --target=browser --format=iife --outfile=web/vendor/highlight.js/highlight.min.js web-src/highlight-entry.ts && node scripts/generate-third-party-notices.mjs",
31
+ "build:web": "bun build --target=browser --format=iife --outfile=web/app.js web-src/app.ts && bun build --target=browser --format=esm --outfile=web/mermaid.js web-src/mermaid-entry.ts && bun build --target=browser --format=esm --outfile=web/shiki.js web-src/shiki-entry.ts && bun build --target=browser --format=esm --outfile=web/yaml.js web-src/yaml-entry.ts && bun build --target=browser --format=iife --outfile=web/vendor/highlight.js/highlight.min.js web-src/highlight-entry.ts && node scripts/generate-third-party-notices.mjs",
32
32
  "build:server": "bun build --target=node --format=esm --outfile=dist/code-viewer.js --external pg --external mysql2/promise --external @redis/client web-src/server/cli.ts",
33
33
  "check": "bun run typecheck",
34
34
  "typecheck": "tsc --noEmit",
@@ -49,7 +49,7 @@
49
49
  "test:postgres:down": "docker compose -f data/test/postgresql/docker-compose.yml down -v",
50
50
  "test": "bun test",
51
51
  "lint": "biome lint web-src package.json biome.jsonc",
52
- "verify": "bun run check && bun run lint && bun run check:format && bun run build && bun run check:bundle && bun run test && node --check web/app.js && node --check web/mermaid.js && node --check web/shiki.js && node --check web/vendor/highlight.js/highlight.min.js && node --check dist/code-viewer.js && node dist/code-viewer.js --help && node scripts/node-smoke.mjs",
52
+ "verify": "bun run check && bun run lint && bun run check:format && bun run build && bun run check:bundle && bun run test && node --check web/app.js && node --check web/mermaid.js && node --check web/shiki.js && node --check web/yaml.js && node --check web/vendor/highlight.js/highlight.min.js && node --check dist/code-viewer.js && node dist/code-viewer.js --help && node scripts/node-smoke.mjs",
53
53
  "pack:dry": "npm pack --dry-run",
54
54
  "prepack": "bun run build",
55
55
  "prepublishOnly": "bun run verify"
@@ -75,7 +75,8 @@
75
75
  "markdown-it-footnote": "4.0.0",
76
76
  "mermaid": "11.14.0",
77
77
  "shiki": "4.0.2",
78
- "typescript": "5.9.3"
78
+ "typescript": "5.9.3",
79
+ "yaml": "2.9.0"
79
80
  },
80
81
  "optionalDependencies": {
81
82
  "better-sqlite3": "12.9.0"