@rooode/dsh-plugin-preview 0.1.3 → 0.1.4

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 (2) hide show
  1. package/lib/client.js +1282 -75
  2. package/package.json +2 -2
package/lib/client.js CHANGED
@@ -179,6 +179,30 @@
179
179
  return { icon: '📄', color: '#94a3b8' };
180
180
  }
181
181
 
182
+
183
+ function getFileTypeCategory(filePath) {
184
+ if (!filePath) return 'text';
185
+ const ext = getFileExtension(filePath).toLowerCase();
186
+ if (['.md', '.markdown', '.mdown', '.mkdn', '.mdwn'].includes(ext)) return 'markdown';
187
+ if (['.json', '.jsonc', '.json5', '.geojson', '.lock'].includes(ext)) return 'json';
188
+ if (['.yaml', '.yml'].includes(ext)) return 'yaml';
189
+ if (['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts'].includes(ext)) return 'javascript';
190
+ if (['.py', '.pyw'].includes(ext)) return 'python';
191
+ if (['.html', '.htm', '.svg', '.xml'].includes(ext)) return 'markup';
192
+ if (['.css', '.scss', '.less', '.sass'].includes(ext)) return 'css';
193
+ if (['.sh', '.bash', '.zsh', '.ps1', '.bat', '.cmd'].includes(ext)) return 'shell';
194
+ if (['.sql'].includes(ext)) return 'sql';
195
+ if (['.toml', '.ini', '.env', '.properties', '.conf', '.cfg', '.gitignore', '.gitattributes', '.editorconfig'].includes(ext)) return 'config';
196
+ return 'text';
197
+ }
198
+
199
+ function getDefaultViewMode(category) {
200
+ if (category === 'markdown') return 'preview';
201
+ if (category === 'json') return 'json-tree';
202
+ if (category === 'yaml') return 'code';
203
+ return 'code';
204
+ }
205
+
182
206
  function getCurrentWorkspaceRoot() {
183
207
  if (clientCtx && clientCtx.workspaces) {
184
208
  try {
@@ -248,12 +272,15 @@
248
272
  globalActiveTabId = globalTabs[existingIndex].id;
249
273
  globalTabs[existingIndex].lastActiveAt = Date.now();
250
274
  } else {
275
+ const category = getFileTypeCategory(fullPath);
276
+ const defaultMode = getDefaultViewMode(category);
251
277
  const newTab = {
252
278
  id: tabId,
253
279
  filePath: fullPath,
254
280
  title: fileName,
255
281
  extension: ext,
256
- viewMode: options.viewMode || 'preview',
282
+ category: category,
283
+ viewMode: options.viewMode || defaultMode,
257
284
  pinned: false,
258
285
  createdAt: Date.now(),
259
286
  lastActiveAt: Date.now(),
@@ -1053,10 +1080,735 @@
1053
1080
  }
1054
1081
 
1055
1082
  // =========================================================================
1083
+
1084
+ // =========================================================================
1085
+ // Multi-Language Syntax Highlighting & Tokenizers (Single-Pass Safe)
1086
+ // =========================================================================
1087
+ function highlightJSON(jsonStr) {
1088
+ if (!jsonStr) return '';
1089
+ const regex = /("(?:\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?|[{}\[\],:])/g;
1090
+ let lastIndex = 0;
1091
+ let out = '';
1092
+ let match;
1093
+
1094
+ while ((match = regex.exec(jsonStr)) !== null) {
1095
+ const raw = match[0];
1096
+ const index = match.index;
1097
+ if (index > lastIndex) {
1098
+ out += escapeHtml(jsonStr.slice(lastIndex, index));
1099
+ }
1100
+ lastIndex = regex.lastIndex;
1101
+
1102
+ if (raw.startsWith('"')) {
1103
+ if (raw.endsWith(':')) {
1104
+ const keyText = raw.slice(0, -1).trim();
1105
+ out += '<span class="tok-key">' + escapeHtml(keyText) + '</span>:';
1106
+ } else {
1107
+ out += '<span class="tok-string">' + escapeHtml(raw) + '</span>';
1108
+ }
1109
+ } else if (raw === 'true' || raw === 'false') {
1110
+ out += '<span class="tok-boolean">' + raw + '</span>';
1111
+ } else if (raw === 'null') {
1112
+ out += '<span class="tok-null">' + raw + '</span>';
1113
+ } else if (/^-?\d/.test(raw)) {
1114
+ out += '<span class="tok-number">' + raw + '</span>';
1115
+ } else if (/[{}\[\]]/.test(raw)) {
1116
+ out += '<span class="tok-bracket">' + escapeHtml(raw) + '</span>';
1117
+ } else if (/[:,]/.test(raw)) {
1118
+ out += '<span class="tok-punctuation">' + raw + '</span>';
1119
+ } else {
1120
+ out += escapeHtml(raw);
1121
+ }
1122
+ }
1123
+
1124
+ if (lastIndex < jsonStr.length) {
1125
+ out += escapeHtml(jsonStr.slice(lastIndex));
1126
+ }
1127
+ return out;
1128
+ }
1129
+
1130
+ function highlightYAML(yamlStr) {
1131
+ if (!yamlStr) return '';
1132
+ const lines = yamlStr.split('\n');
1133
+ return lines.map(line => {
1134
+ const commentIdx = line.indexOf('#');
1135
+ let mainPart = line;
1136
+ let commentPart = '';
1137
+ if (commentIdx !== -1) {
1138
+ mainPart = line.slice(0, commentIdx);
1139
+ commentPart = '<span class="tok-comment">' + escapeHtml(line.slice(commentIdx)) + '</span>';
1140
+ }
1141
+
1142
+ let highlightedMain = '';
1143
+ const kvMatch = mainPart.match(/^(\s*-\s+)?([a-zA-Z0-9_\-./@$]+)(\s*:\s*)(.*)$/);
1144
+ if (kvMatch) {
1145
+ const prefix = kvMatch[1] ? '<span class="tok-operator">' + escapeHtml(kvMatch[1]) + '</span>' : '';
1146
+ const key = '<span class="tok-key">' + escapeHtml(kvMatch[2]) + '</span>';
1147
+ const colon = '<span class="tok-punctuation">' + escapeHtml(kvMatch[3]) + '</span>';
1148
+ let val = kvMatch[4];
1149
+ const trimmedVal = val.trim();
1150
+ let valHtml = escapeHtml(val);
1151
+ if (/^(true|false|yes|no|on|off)$/i.test(trimmedVal)) {
1152
+ valHtml = '<span class="tok-boolean">' + escapeHtml(val) + '</span>';
1153
+ } else if (/^(null|~)$/i.test(trimmedVal)) {
1154
+ valHtml = '<span class="tok-null">' + escapeHtml(val) + '</span>';
1155
+ } else if (/^-?\d+(\.\d+)?$/.test(trimmedVal)) {
1156
+ valHtml = '<span class="tok-number">' + escapeHtml(val) + '</span>';
1157
+ } else if (/^["'].*["']$/.test(trimmedVal)) {
1158
+ valHtml = '<span class="tok-string">' + escapeHtml(val) + '</span>';
1159
+ } else if (trimmedVal.startsWith('&') || trimmedVal.startsWith('*')) {
1160
+ valHtml = '<span class="tok-type">' + escapeHtml(val) + '</span>';
1161
+ }
1162
+ highlightedMain = prefix + key + colon + valHtml;
1163
+ } else {
1164
+ const bulletMatch = mainPart.match(/^(\s*-\s+)(.*)$/);
1165
+ if (bulletMatch) {
1166
+ highlightedMain = '<span class="tok-operator">' + escapeHtml(bulletMatch[1]) + '</span>' + escapeHtml(bulletMatch[2]);
1167
+ } else {
1168
+ highlightedMain = escapeHtml(mainPart);
1169
+ }
1170
+ }
1171
+ return highlightedMain + commentPart;
1172
+ }).join('\n');
1173
+ }
1174
+
1175
+ function highlightJS(code) {
1176
+ if (!code) return '';
1177
+ const keywords = new Set([
1178
+ 'const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'do',
1179
+ 'switch', 'case', 'break', 'continue', 'default', 'new', 'delete', 'typeof', 'instanceof',
1180
+ 'void', 'yield', 'await', 'async', 'try', 'catch', 'finally', 'throw', 'class',
1181
+ 'extends', 'super', 'this', 'import', 'export', 'from', 'as', 'type', 'interface',
1182
+ 'enum', 'implements', 'declare', 'namespace', 'public', 'private', 'protected',
1183
+ 'readonly', 'static', 'abstract', 'get', 'set', 'of', 'in', 'debugger', 'constructor'
1184
+ ]);
1185
+ const types = new Set([
1186
+ 'Promise', 'Array', 'Object', 'String', 'Number', 'Boolean', 'Map', 'Set', 'WeakMap',
1187
+ 'WeakSet', 'JSON', 'Math', 'RegExp', 'Date', 'Error', 'Symbol', 'React', 'ReactDOM',
1188
+ 'null', 'undefined', 'true', 'false', 'any', 'unknown', 'never', 'void', 'string',
1189
+ 'number', 'boolean', 'bigint', 'Record', 'Partial', 'Required', 'Pick', 'Omit'
1190
+ ]);
1191
+
1192
+ const tokenRegex = /(\/\*[\s\S]*?\*\/|\/\/.*$|`(?:\[\s\S]|[^`\])*`|"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|\b(?:0x[0-9a-fA-F]+|\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?n?)\b|\b[a-zA-Z_$][a-zA-Z0-9_$]*\b|=>|===|!==|<=|>=|&&|\|\||\?\?|\?\.|[+\-*/%=!&|^~<>?:;,{}()\[\].])/gm;
1193
+
1194
+ let lastIndex = 0;
1195
+ let out = '';
1196
+ let match;
1197
+
1198
+ while ((match = tokenRegex.exec(code)) !== null) {
1199
+ const raw = match[0];
1200
+ const index = match.index;
1201
+ if (index > lastIndex) {
1202
+ out += escapeHtml(code.slice(lastIndex, index));
1203
+ }
1204
+ lastIndex = tokenRegex.lastIndex;
1205
+
1206
+ if (raw.startsWith('/*') || raw.startsWith('//')) {
1207
+ out += '<span class="tok-comment">' + escapeHtml(raw) + '</span>';
1208
+ } else if (raw.startsWith('`') || raw.startsWith('"') || raw.startsWith("'")) {
1209
+ out += '<span class="tok-string">' + escapeHtml(raw) + '</span>';
1210
+ } else if (/^\d|^0x/.test(raw)) {
1211
+ out += '<span class="tok-number">' + escapeHtml(raw) + '</span>';
1212
+ } else if (/^[a-zA-Z_$]/.test(raw)) {
1213
+ if (keywords.has(raw)) {
1214
+ out += '<span class="tok-keyword">' + escapeHtml(raw) + '</span>';
1215
+ } else if (types.has(raw)) {
1216
+ out += '<span class="tok-type">' + escapeHtml(raw) + '</span>';
1217
+ } else {
1218
+ const remainder = code.slice(lastIndex);
1219
+ if (/^\s*\(/.test(remainder)) {
1220
+ out += '<span class="tok-func">' + escapeHtml(raw) + '</span>';
1221
+ } else {
1222
+ out += escapeHtml(raw);
1223
+ }
1224
+ }
1225
+ } else if (/^(=>|===|!==|<=|>=|&&|\|\||\?\?|\?\.|[+\-*/%=!&|^~<>])$/.test(raw)) {
1226
+ out += '<span class="tok-operator">' + escapeHtml(raw) + '</span>';
1227
+ } else if (/^[{}()\[\]]$/.test(raw)) {
1228
+ out += '<span class="tok-bracket">' + escapeHtml(raw) + '</span>';
1229
+ } else if (/^[,;:?]$/.test(raw)) {
1230
+ out += '<span class="tok-punctuation">' + escapeHtml(raw) + '</span>';
1231
+ } else {
1232
+ out += escapeHtml(raw);
1233
+ }
1234
+ }
1235
+
1236
+ if (lastIndex < code.length) {
1237
+ out += escapeHtml(code.slice(lastIndex));
1238
+ }
1239
+ return out;
1240
+ }
1241
+
1242
+ function highlightPython(code) {
1243
+ if (!code) return '';
1244
+ const pyKeywords = new Set([
1245
+ 'def', 'class', 'import', 'from', 'return', 'if', 'elif', 'else', 'for', 'while',
1246
+ 'try', 'except', 'finally', 'raise', 'with', 'as', 'lambda', 'pass', 'yield',
1247
+ 'async', 'await', 'global', 'nonlocal', 'assert', 'del', 'in', 'is', 'not', 'and', 'or'
1248
+ ]);
1249
+ const pyBuiltins = new Set([
1250
+ 'True', 'False', 'None', 'self', 'cls', 'print', 'len', 'range', 'dict', 'list',
1251
+ 'set', 'tuple', 'str', 'int', 'float', 'bool', 'type', 'isinstance', 'enumerate', 'zip'
1252
+ ]);
1253
+
1254
+ const tokenRegex = /("""[\s\S]*?"""|'''[\s\S]*?'''|#.*$|f?"(?:\\[\s\S]|[^"\\])*"|f?'(?:\\[\s\S]|[^'\\])*'|\b\d+(?:\.\d+)?\b|\b[a-zA-Z_][a-zA-Z0-9_]*\b|==|!=|<=|>=|\+=|-=|\*=|\/=|->|[+\-*/%=&|^~<>?:;,{}()\[\].])/gm;
1255
+
1256
+ let lastIndex = 0;
1257
+ let out = '';
1258
+ let match;
1259
+
1260
+ while ((match = tokenRegex.exec(code)) !== null) {
1261
+ const raw = match[0];
1262
+ const index = match.index;
1263
+ if (index > lastIndex) {
1264
+ out += escapeHtml(code.slice(lastIndex, index));
1265
+ }
1266
+ lastIndex = tokenRegex.lastIndex;
1267
+
1268
+ if (raw.startsWith('#')) {
1269
+ out += '<span class="tok-comment">' + escapeHtml(raw) + '</span>';
1270
+ } else if (raw.startsWith('"""') || raw.startsWith("'''") || raw.startsWith('"') || raw.startsWith("'") || raw.startsWith('f"') || raw.startsWith("f'")) {
1271
+ out += '<span class="tok-string">' + escapeHtml(raw) + '</span>';
1272
+ } else if (/^\d/.test(raw)) {
1273
+ out += '<span class="tok-number">' + escapeHtml(raw) + '</span>';
1274
+ } else if (/^[a-zA-Z_]/.test(raw)) {
1275
+ if (pyKeywords.has(raw)) {
1276
+ out += '<span class="tok-keyword">' + escapeHtml(raw) + '</span>';
1277
+ } else if (pyBuiltins.has(raw)) {
1278
+ out += '<span class="tok-type">' + escapeHtml(raw) + '</span>';
1279
+ } else {
1280
+ const remainder = code.slice(lastIndex);
1281
+ if (/^\s*\(/.test(remainder)) {
1282
+ out += '<span class="tok-func">' + escapeHtml(raw) + '</span>';
1283
+ } else {
1284
+ out += escapeHtml(raw);
1285
+ }
1286
+ }
1287
+ } else if (/^(==|!=|<=|>=|\+=|-=|\*=|\/=|->|[+\-*/%=&|^~<>])$/.test(raw)) {
1288
+ out += '<span class="tok-operator">' + escapeHtml(raw) + '</span>';
1289
+ } else if (/^[{}()\[\]]$/.test(raw)) {
1290
+ out += '<span class="tok-bracket">' + escapeHtml(raw) + '</span>';
1291
+ } else if (/^[,;:?]$/.test(raw)) {
1292
+ out += '<span class="tok-punctuation">' + escapeHtml(raw) + '</span>';
1293
+ } else {
1294
+ out += escapeHtml(raw);
1295
+ }
1296
+ }
1297
+
1298
+ if (lastIndex < code.length) {
1299
+ out += escapeHtml(code.slice(lastIndex));
1300
+ }
1301
+ return out;
1302
+ }
1303
+
1304
+ function highlightHTML(code) {
1305
+ if (!code) return '';
1306
+ const tokenRegex = /(<!--[\s\S]*?-->|<\/?[a-zA-Z0-9\-:]+|(?:[a-zA-Z0-9\-:]+)=("[^"]*"|'[^']*')|\/?>)/g;
1307
+ let lastIndex = 0;
1308
+ let out = '';
1309
+ let match;
1310
+
1311
+ while ((match = tokenRegex.exec(code)) !== null) {
1312
+ const raw = match[0];
1313
+ const index = match.index;
1314
+ if (index > lastIndex) {
1315
+ out += escapeHtml(code.slice(lastIndex, index));
1316
+ }
1317
+ lastIndex = tokenRegex.lastIndex;
1318
+
1319
+ if (raw.startsWith('<!--')) {
1320
+ out += '<span class="tok-comment">' + escapeHtml(raw) + '</span>';
1321
+ } else if (raw.startsWith('<')) {
1322
+ out += '<span class="tok-bracket">&lt;</span><span class="tok-tag">' + escapeHtml(raw.slice(raw.startsWith('</') ? 2 : 1)) + '</span>';
1323
+ } else if (raw.endsWith('>')) {
1324
+ out += '<span class="tok-bracket">' + escapeHtml(raw) + '</span>';
1325
+ } else if (raw.includes('=')) {
1326
+ const eqIdx = raw.indexOf('=');
1327
+ const attrName = raw.slice(0, eqIdx);
1328
+ const attrVal = raw.slice(eqIdx + 1);
1329
+ out += '<span class="tok-attr">' + escapeHtml(attrName) + '</span>=<span class="tok-string">' + escapeHtml(attrVal) + '</span>';
1330
+ } else {
1331
+ out += escapeHtml(raw);
1332
+ }
1333
+ }
1334
+
1335
+ if (lastIndex < code.length) {
1336
+ out += escapeHtml(code.slice(lastIndex));
1337
+ }
1338
+ return out;
1339
+ }
1340
+
1341
+ function highlightGenericCode(code, category) {
1342
+ if (!code) return '';
1343
+ if (category === 'sql') {
1344
+ const sqlKeywords = /\b(SELECT|FROM|WHERE|INSERT|INTO|UPDATE|DELETE|JOIN|LEFT|RIGHT|INNER|OUTER|GROUP|BY|ORDER|HAVING|LIMIT|OFFSET|AS|ON|AND|OR|NOT|NULL|IS|CREATE|TABLE|DROP|ALTER|ADD|INDEX|PRIMARY|KEY)\b/gi;
1345
+ return escapeHtml(code)
1346
+ .replace(sqlKeywords, '<span class="tok-keyword">$1</span>')
1347
+ .replace(/(--.*$)/gm, '<span class="tok-comment">$1</span>')
1348
+ .replace(/('[^']*')/g, '<span class="tok-string">$1</span>');
1349
+ }
1350
+ const lines = code.split('\n');
1351
+ return lines.map(line => {
1352
+ const commentIdx = line.indexOf('#');
1353
+ let mainPart = line;
1354
+ let commentPart = '';
1355
+ if (commentIdx !== -1) {
1356
+ mainPart = line.slice(0, commentIdx);
1357
+ commentPart = '<span class="tok-comment">' + escapeHtml(line.slice(commentIdx)) + '</span>';
1358
+ }
1359
+ let h = escapeHtml(mainPart);
1360
+ h = h.replace(/^([a-zA-Z0-9_\-.]+)(\s*=\s*)(.*)$/, (m, k, eq, v) => {
1361
+ return '<span class="tok-key">' + k + '</span>' + eq + '<span class="tok-string">' + v + '</span>';
1362
+ });
1363
+ h = h.replace(/(\$[a-zA-Z0-9_{}]+)/g, '<span class="tok-type">$1</span>');
1364
+ h = h.replace(/\b(if|then|else|elif|fi|for|in|do|done|case|esac|echo|cd|export|npm|pnpm|node|git|curl)\b/g, '<span class="tok-keyword">$1</span>');
1365
+ return h + commentPart;
1366
+ }).join('\n');
1367
+ }
1368
+
1369
+ function highlightCode(rawCode, category) {
1370
+ if (!rawCode) return '';
1371
+ if (category === 'json') return highlightJSON(rawCode);
1372
+ if (category === 'yaml') return highlightYAML(rawCode);
1373
+ if (category === 'javascript') return highlightJS(rawCode);
1374
+ if (category === 'python') return highlightPython(rawCode);
1375
+ if (category === 'markup') return highlightHTML(rawCode);
1376
+ if (category === 'css') return highlightCSS(rawCode);
1377
+ if (category === 'shell' || category === 'config' || category === 'sql') return highlightGenericCode(rawCode, category);
1378
+ return escapeHtml(rawCode);
1379
+ }
1380
+
1381
+ // =========================================================================
1382
+ // Lightweight YAML to JSON Recursive Parser
1383
+ // =========================================================================
1384
+ function parseYamlScalar(val) {
1385
+ if (!val) return '';
1386
+ const v = val.trim();
1387
+ if (/^(true|yes|on)$/i.test(v)) return true;
1388
+ if (/^(false|no|off)$/i.test(v)) return false;
1389
+ if (/^(null|~)$/i.test(v)) return null;
1390
+ if (/^-?\d+$/.test(v)) return parseInt(v, 10);
1391
+ if (/^-?\d+\.\d+$/.test(v)) return parseFloat(v);
1392
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
1393
+ return v.slice(1, -1);
1394
+ }
1395
+ return v;
1396
+ }
1397
+
1398
+ function parseYamlToJson(yamlStr) {
1399
+ if (!yamlStr) return { ok: true, data: {} };
1400
+ try {
1401
+ const rawLines = yamlStr.split('\n');
1402
+ const lines = [];
1403
+ for (const raw of rawLines) {
1404
+ const cIdx = raw.indexOf('#');
1405
+ const l = cIdx !== -1 ? raw.slice(0, cIdx) : raw;
1406
+ if (l.trim()) {
1407
+ lines.push({ indent: l.search(/\S/), text: l.trim() });
1408
+ }
1409
+ }
1410
+ if (lines.length === 0) return { ok: true, data: {} };
1411
+
1412
+ let idx = 0;
1413
+ function parseBlock(currentIndent) {
1414
+ let isArr = false;
1415
+ if (idx < lines.length && lines[idx].text.startsWith('- ')) {
1416
+ isArr = true;
1417
+ }
1418
+ const container = isArr ? [] : {};
1419
+
1420
+ while (idx < lines.length) {
1421
+ const line = lines[idx];
1422
+ if (line.indent < currentIndent) break;
1423
+ if (line.indent > currentIndent) break;
1424
+
1425
+ idx++;
1426
+
1427
+ if (isArr) {
1428
+ if (line.text.startsWith('- ')) {
1429
+ const itemText = line.text.slice(2).trim();
1430
+ if (!itemText) {
1431
+ if (idx < lines.length && lines[idx].indent > line.indent) {
1432
+ container.push(parseBlock(lines[idx].indent));
1433
+ } else {
1434
+ container.push(null);
1435
+ }
1436
+ } else if (itemText.includes(':')) {
1437
+ const colonPos = itemText.indexOf(':');
1438
+ const k = itemText.slice(0, colonPos).trim().replace(/^['"]|['"]$/g, '');
1439
+ const v = itemText.slice(colonPos + 1).trim();
1440
+ const obj = {};
1441
+ if (!v) {
1442
+ if (idx < lines.length && lines[idx].indent > line.indent) {
1443
+ obj[k] = parseBlock(lines[idx].indent);
1444
+ } else {
1445
+ obj[k] = {};
1446
+ }
1447
+ } else {
1448
+ obj[k] = parseYamlScalar(v);
1449
+ }
1450
+ while (idx < lines.length && lines[idx].indent === line.indent + 2 && !lines[idx].text.startsWith('- ')) {
1451
+ const nextLine = lines[idx++];
1452
+ const cPos = nextLine.text.indexOf(':');
1453
+ if (cPos !== -1) {
1454
+ const nk = nextLine.text.slice(0, cPos).trim().replace(/^['"]|['"]$/g, '');
1455
+ const nv = nextLine.text.slice(cPos + 1).trim();
1456
+ if (!nv && idx < lines.length && lines[idx].indent > nextLine.indent) {
1457
+ obj[nk] = parseBlock(lines[idx].indent);
1458
+ } else {
1459
+ obj[nk] = parseYamlScalar(nv);
1460
+ }
1461
+ }
1462
+ }
1463
+ container.push(obj);
1464
+ } else {
1465
+ container.push(parseYamlScalar(itemText));
1466
+ }
1467
+ }
1468
+ } else {
1469
+ const colonPos = line.text.indexOf(':');
1470
+ if (colonPos !== -1) {
1471
+ const k = line.text.slice(0, colonPos).trim().replace(/^['"]|['"]$/g, '');
1472
+ const v = line.text.slice(colonPos + 1).trim();
1473
+ if (!v) {
1474
+ if (idx < lines.length && lines[idx].indent > line.indent) {
1475
+ container[k] = parseBlock(lines[idx].indent);
1476
+ } else {
1477
+ container[k] = {};
1478
+ }
1479
+ } else {
1480
+ container[k] = parseYamlScalar(v);
1481
+ }
1482
+ }
1483
+ }
1484
+ }
1485
+ return container;
1486
+ }
1487
+
1488
+ const root = parseBlock(lines[0].indent);
1489
+ return { ok: true, data: root };
1490
+ } catch (err) {
1491
+ return { ok: false, error: err.message };
1492
+ }
1493
+ }
1494
+
1495
+ // =========================================================================
1496
+ // Code Symbol Outline Extractor
1497
+ // =========================================================================
1498
+ function extractCodeSymbols(content, category) {
1499
+ if (!content) return [];
1500
+ const lines = content.split('\n');
1501
+ const symbols = [];
1502
+
1503
+ if (category === 'javascript') {
1504
+ lines.forEach((line, idx) => {
1505
+ const lineNum = idx + 1;
1506
+ const trimmed = line.trim();
1507
+ if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) return;
1508
+
1509
+ const classMatch = trimmed.match(/\bclass\s+([a-zA-Z0-9_$]+)/);
1510
+ if (classMatch) {
1511
+ symbols.push({ id: `line-${lineNum}`, name: classMatch[1], kind: 'class', line: lineNum, text: `🔷 class ${classMatch[1]}` });
1512
+ return;
1513
+ }
1514
+
1515
+ const funcMatch = trimmed.match(/\b(?:async\s+)?function\s*([a-zA-Z0-9_$]+)\s*\(/);
1516
+ if (funcMatch) {
1517
+ symbols.push({ id: `line-${lineNum}`, name: funcMatch[1], kind: 'function', line: lineNum, text: `⚡ fn ${funcMatch[1]}()` });
1518
+ return;
1519
+ }
1520
+
1521
+ const arrowMatch = trimmed.match(/(?:const|let|var)\s+([a-zA-Z0-9_$]+)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[a-zA-Z0-9_$]+)\s*=>/);
1522
+ if (arrowMatch) {
1523
+ symbols.push({ id: `line-${lineNum}`, name: arrowMatch[1], kind: 'function', line: lineNum, text: `⚡ fn ${arrowMatch[1]}()` });
1524
+ return;
1525
+ }
1526
+
1527
+ const typeMatch = trimmed.match(/\b(?:interface|type|enum)\s+([a-zA-Z0-9_$]+)/);
1528
+ if (typeMatch) {
1529
+ symbols.push({ id: `line-${lineNum}`, name: typeMatch[1], kind: 'type', line: lineNum, text: `🏷️ ${typeMatch[1]}` });
1530
+ return;
1531
+ }
1532
+
1533
+ if (trimmed.startsWith('export default')) {
1534
+ symbols.push({ id: `line-${lineNum}`, name: 'default export', kind: 'export', line: lineNum, text: `📦 export default` });
1535
+ }
1536
+ });
1537
+ } else if (category === 'python') {
1538
+ lines.forEach((line, idx) => {
1539
+ const lineNum = idx + 1;
1540
+ const classMatch = line.match(/^class\s+([a-zA-Z0-9_]+)/);
1541
+ if (classMatch) {
1542
+ symbols.push({ id: `line-${lineNum}`, name: classMatch[1], kind: 'class', line: lineNum, text: `🔷 class ${classMatch[1]}` });
1543
+ return;
1544
+ }
1545
+ const funcMatch = line.match(/^(\s*)(?:async\s+)?def\s+([a-zA-Z0-9_]+)/);
1546
+ if (funcMatch) {
1547
+ const prefix = funcMatch[1].length > 0 ? ' ' : '';
1548
+ symbols.push({ id: `line-${lineNum}`, name: funcMatch[2], kind: 'function', line: lineNum, text: `${prefix}⚡ def ${funcMatch[2]}()` });
1549
+ }
1550
+ });
1551
+ } else if (category === 'yaml') {
1552
+ lines.forEach((line, idx) => {
1553
+ const lineNum = idx + 1;
1554
+ const topKeyMatch = line.match(/^([a-zA-Z0-9_\-./@$]+)\s*:/);
1555
+ if (topKeyMatch && !line.startsWith('#')) {
1556
+ symbols.push({ id: `line-${lineNum}`, name: topKeyMatch[1], kind: 'key', line: lineNum, text: `🔑 ${topKeyMatch[1]}` });
1557
+ }
1558
+ });
1559
+ } else if (category === 'json') {
1560
+ lines.forEach((line, idx) => {
1561
+ const lineNum = idx + 1;
1562
+ const keyMatch = line.match(/^\s{2}"([a-zA-Z0-9_\-./@$]+)"\s*:/);
1563
+ if (keyMatch) {
1564
+ symbols.push({ id: `line-${lineNum}`, name: keyMatch[1], kind: 'key', line: lineNum, text: `🔑 "${keyMatch[1]}"` });
1565
+ }
1566
+ });
1567
+ }
1568
+ return symbols;
1569
+ }
1570
+
1571
+ // =========================================================================
1572
+ // React Component: JSONTreeNode & JSONTreeInspector
1573
+ // =========================================================================
1574
+ function JSONTreeNode({ name, value, path, depth, isArrayItem, index, expandedKeys, onToggle, filterQuery, onCopy }) {
1575
+ const isObject = value !== null && typeof value === 'object';
1576
+ const isArray = Array.isArray(value);
1577
+ const isExpanded = expandedKeys.has(path);
1578
+
1579
+ if (isObject) {
1580
+ const keys = Object.keys(value);
1581
+ const count = keys.length;
1582
+ const typeLabel = isArray ? `Array(${count})` : `Object{${count}}`;
1583
+
1584
+ if (filterQuery) {
1585
+ const matchSelf = (name && String(name).toLowerCase().includes(filterQuery)) || path.toLowerCase().includes(filterQuery);
1586
+ let matchChild = false;
1587
+ try { matchChild = JSON.stringify(value).toLowerCase().includes(filterQuery); } catch (e) {}
1588
+ if (!matchSelf && !matchChild) return null;
1589
+ }
1590
+
1591
+ const previewSummary = isArray
1592
+ ? `[ ${count === 0 ? '' : '...'} ]`
1593
+ : `{ ${keys.slice(0, 3).map(k => `${k}: ...`).join(', ')}${count > 3 ? ', ...' : ''} }`;
1594
+
1595
+ return h('div', { className: 'dsh-json-node-group' },
1596
+ h('div', {
1597
+ className: 'dsh-json-row dsh-json-row-expandable',
1598
+ style: { paddingLeft: `${depth * 16 + 6}px` },
1599
+ onClick: () => onToggle(path),
1600
+ },
1601
+ h('span', { className: `dsh-json-chevron ${isExpanded ? 'expanded' : ''}` }, isExpanded ? '▼' : '▶'),
1602
+ name ? h('span', { className: 'dsh-json-key' }, isArrayItem ? `[${index}]` : `"${name}":`) : null,
1603
+ h('span', { className: 'dsh-json-type-badge' }, typeLabel),
1604
+ !isExpanded ? h('span', { className: 'dsh-json-preview' }, previewSummary) : null,
1605
+ h('span', {
1606
+ className: 'dsh-json-copy-btn',
1607
+ title: `复制字段路径: ${path.replace(/^root\.?/, '')}`,
1608
+ onClick: (e) => {
1609
+ e.stopPropagation();
1610
+ onCopy(path.replace(/^root\.?/, ''), '字段路径');
1611
+ }
1612
+ }, '📋')
1613
+ ),
1614
+ isExpanded ? h('div', { className: 'dsh-json-children' },
1615
+ keys.map((k, i) => h(JSONTreeNode, {
1616
+ key: k,
1617
+ name: isArray ? null : k,
1618
+ value: value[k],
1619
+ path: `${path}.${k}`,
1620
+ depth: depth + 1,
1621
+ isArrayItem: isArray,
1622
+ index: i,
1623
+ expandedKeys,
1624
+ onToggle,
1625
+ filterQuery,
1626
+ onCopy,
1627
+ }))
1628
+ ) : null
1629
+ );
1630
+ }
1631
+
1632
+ // Primitive Value
1633
+ const valType = value === null ? 'null' : typeof value;
1634
+ let valDisplay = String(value);
1635
+ let valClass = `tok-${valType}`;
1636
+
1637
+ if (valType === 'string') {
1638
+ valDisplay = `"${value}"`;
1639
+ valClass = 'tok-string';
1640
+ } else if (valType === 'number') {
1641
+ valClass = 'tok-number';
1642
+ } else if (valType === 'boolean') {
1643
+ valClass = 'tok-boolean';
1644
+ } else if (valType === 'null') {
1645
+ valDisplay = 'null';
1646
+ valClass = 'tok-null';
1647
+ }
1648
+
1649
+ if (filterQuery) {
1650
+ const matchName = name && String(name).toLowerCase().includes(filterQuery);
1651
+ const matchVal = valDisplay.toLowerCase().includes(filterQuery);
1652
+ if (!matchName && !matchVal) return null;
1653
+ }
1654
+
1655
+ const isUrl = typeof value === 'string' && (value.startsWith('http://') || value.startsWith('https://'));
1656
+
1657
+ return h('div', {
1658
+ className: 'dsh-json-row dsh-json-row-leaf',
1659
+ style: { paddingLeft: `${depth * 16 + 22}px` },
1660
+ },
1661
+ name ? h('span', { className: 'dsh-json-key' }, isArrayItem ? `[${index}]:` : `"${name}":`) : null,
1662
+ isUrl ? h('a', {
1663
+ href: value,
1664
+ target: '_blank',
1665
+ rel: 'noopener noreferrer',
1666
+ className: 'dsh-json-link tok-string',
1667
+ onClick: (e) => e.stopPropagation(),
1668
+ }, valDisplay) : h('span', { className: valClass }, valDisplay),
1669
+ h('span', {
1670
+ className: 'dsh-json-copy-btn',
1671
+ title: '复制值',
1672
+ onClick: () => onCopy(String(value), '字段值')
1673
+ }, '📋')
1674
+ );
1675
+ }
1676
+
1677
+ function JSONTreeInspector({ data, rawContent, onFormat, onMinify }) {
1678
+ const [expandedKeys, setExpandedKeys] = useState(new Set(['root', 'root.scripts', 'root.dependencies', 'root.devDependencies']));
1679
+ const [filterQuery, setFilterQuery] = useState('');
1680
+
1681
+ const toggleKey = (path) => {
1682
+ setExpandedKeys(prev => {
1683
+ const next = new Set(prev);
1684
+ if (next.has(path)) next.delete(path);
1685
+ else next.add(path);
1686
+ return next;
1687
+ });
1688
+ };
1689
+
1690
+ const expandAll = () => {
1691
+ const all = new Set();
1692
+ const traverse = (val, p) => {
1693
+ all.add(p);
1694
+ if (val && typeof val === 'object') {
1695
+ for (const k of Object.keys(val)) {
1696
+ traverse(val[k], p ? `${p}.${k}` : k);
1697
+ }
1698
+ }
1699
+ };
1700
+ traverse(data, 'root');
1701
+ setExpandedKeys(all);
1702
+ };
1703
+
1704
+ const collapseAll = () => {
1705
+ setExpandedKeys(new Set(['root']));
1706
+ };
1707
+
1708
+ const handleCopy = (text, label) => {
1709
+ navigator.clipboard.writeText(text).then(() => {
1710
+ showToast(`已复制 ${label || '内容'}`, 'success');
1711
+ });
1712
+ };
1713
+
1714
+ return h('div', { className: 'dsh-json-inspector' },
1715
+ h('div', { className: 'dsh-json-header' },
1716
+ h('div', { className: 'dsh-json-search-box' },
1717
+ h('span', { className: 'dsh-json-search-icon' }, '🔍'),
1718
+ h('input', {
1719
+ type: 'text',
1720
+ placeholder: '过滤属性名或值...',
1721
+ value: filterQuery,
1722
+ onChange: (e) => setFilterQuery(e.target.value),
1723
+ className: 'dsh-json-search-input',
1724
+ }),
1725
+ filterQuery ? h('button', {
1726
+ className: 'dsh-search-clear-btn',
1727
+ onClick: () => setFilterQuery(''),
1728
+ }, '✕') : null
1729
+ ),
1730
+ h('div', { className: 'dsh-json-actions' },
1731
+ h('button', { type: 'button', className: 'dsh-json-btn', onClick: expandAll, title: '展开所有层级' }, '📂 展开全部'),
1732
+ h('button', { type: 'button', className: 'dsh-json-btn', onClick: collapseAll, title: '折叠到根节点' }, '📁 折叠全部'),
1733
+ onFormat ? h('button', { type: 'button', className: 'dsh-json-btn', onClick: onFormat, title: '格式化并复制' }, '⚡ 格式化') : null,
1734
+ onMinify ? h('button', { type: 'button', className: 'dsh-json-btn', onClick: onMinify, title: '压缩并复制' }, '🗜️ 压缩') : null
1735
+ )
1736
+ ),
1737
+ h('div', { className: 'dsh-json-tree-body' },
1738
+ h(JSONTreeNode, {
1739
+ name: Array.isArray(data) ? 'Array' : 'Object',
1740
+ value: data,
1741
+ path: 'root',
1742
+ depth: 0,
1743
+ expandedKeys,
1744
+ onToggle: toggleKey,
1745
+ filterQuery: filterQuery.trim().toLowerCase(),
1746
+ onCopy: handleCopy,
1747
+ })
1748
+ )
1749
+ );
1750
+ }
1751
+
1752
+ // =========================================================================
1753
+ // React Component: CodeEditorView
1754
+ // =========================================================================
1755
+ function CodeEditorView({ content, category, isHighlighted = true, isWordWrap = true, searchQuery = '', onLineClick }) {
1756
+ const lines = useMemo(() => (content ? content.split('\n') : []), [content]);
1757
+
1758
+ const highlightedHtml = useMemo(() => {
1759
+ if (!content) return '';
1760
+ if (!isHighlighted) {
1761
+ let escaped = escapeHtml(content);
1762
+ if (searchQuery.trim()) {
1763
+ const q = escapeHtml(searchQuery.trim());
1764
+ escaped = escaped.replace(new RegExp(`(${q})`, 'gi'), '<mark class="dsh-search-highlight">$1</mark>');
1765
+ }
1766
+ return escaped;
1767
+ }
1768
+ let html = highlightCode(content, category);
1769
+ if (searchQuery.trim()) {
1770
+ try {
1771
+ const q = escapeHtml(searchQuery.trim());
1772
+ const regex = new RegExp(`(${q})(?![^<]*>)`, 'gi');
1773
+ html = html.replace(regex, '<mark class="dsh-search-highlight">$1</mark>');
1774
+ } catch (e) {}
1775
+ }
1776
+ return html;
1777
+ }, [content, category, isHighlighted, searchQuery]);
1778
+
1779
+ return h('div', { className: `dsh-code-editor-view ${isWordWrap ? 'word-wrap' : ''}` },
1780
+ h('div', { className: 'dsh-code-gutter' },
1781
+ lines.map((_, i) => h('div', {
1782
+ key: i,
1783
+ id: `line-gutter-${i + 1}`,
1784
+ className: 'dsh-gutter-line',
1785
+ onClick: () => onLineClick && onLineClick(i + 1),
1786
+ title: `第 ${i + 1} 行 (点击定位)`
1787
+ }, i + 1))
1788
+ ),
1789
+ h('div', { className: 'dsh-code-content' },
1790
+ h('pre', { className: 'dsh-code-pre' },
1791
+ h('code', {
1792
+ className: `dsh-code-lang-${category}`,
1793
+ dangerouslySetInnerHTML: { __html: highlightedHtml }
1794
+ })
1795
+ )
1796
+ )
1797
+ );
1798
+ }
1799
+
1056
1800
  // React Components: MarkdownPreviewView
1057
1801
  // =========================================================================
1058
1802
  function MarkdownPreviewView({ tab, fileInfo, isLoading, error, isTreeOpen, onToggleTree, onRefresh, onOpenNative, onReveal }) {
1059
- const [viewMode, setViewMode] = useState(tab.viewMode || 'preview');
1803
+ const category = getFileTypeCategory(tab.filePath);
1804
+ const defaultMode = getDefaultViewMode(category);
1805
+ const [viewMode, setViewMode] = useState(tab.viewMode || defaultMode);
1806
+
1807
+ useEffect(() => {
1808
+ const mode = tab.viewMode || getDefaultViewMode(getFileTypeCategory(tab.filePath));
1809
+ setViewMode(mode);
1810
+ }, [tab.id, tab.filePath, tab.viewMode]);
1811
+ const [isWordWrap, setIsWordWrap] = useState(false);
1060
1812
  const [isTocOpen, setIsTocOpen] = useState(false);
1061
1813
  const [searchQuery, setSearchQuery] = useState('');
1062
1814
  const [isSearchOpen, setIsSearchOpen] = useState(false);
@@ -1064,10 +1816,35 @@
1064
1816
  const sourceRef = useRef(null);
1065
1817
 
1066
1818
  const content = fileInfo?.content || '';
1067
- const tocHeadings = useMemo(() => extractTocHeadings(content), [content]);
1068
1819
 
1069
- const renderedHtml = useMemo(() => {
1070
- if (!content) return '';
1820
+ // Outline extraction: markdown headings for md, symbols for code/json/yaml
1821
+ const outlineItems = useMemo(() => {
1822
+ if (!content) return [];
1823
+ if (category === 'markdown') {
1824
+ return extractTocHeadings(content);
1825
+ }
1826
+ return extractCodeSymbols(content, category);
1827
+ }, [content, category]);
1828
+
1829
+ // Parse JSON / YAML structure for tree mode
1830
+ const parsedTreeData = useMemo(() => {
1831
+ if (!content) return { ok: false, error: '空文件' };
1832
+ if (category === 'json' || viewMode === 'json-tree') {
1833
+ if (category === 'yaml') {
1834
+ return parseYamlToJson(content);
1835
+ }
1836
+ try {
1837
+ const data = JSON.parse(content);
1838
+ return { ok: true, data };
1839
+ } catch (e) {
1840
+ return { ok: false, error: e.message };
1841
+ }
1842
+ }
1843
+ return { ok: false };
1844
+ }, [content, category, viewMode]);
1845
+
1846
+ const renderedMarkdownHtml = useMemo(() => {
1847
+ if (!content || category !== 'markdown') return '';
1071
1848
  let html = renderMarkdownToHtml(content);
1072
1849
  if (searchQuery.trim()) {
1073
1850
  try {
@@ -1077,22 +1854,53 @@
1077
1854
  } catch (e) {}
1078
1855
  }
1079
1856
  return html;
1080
- }, [content, searchQuery]);
1857
+ }, [content, category, searchQuery]);
1081
1858
 
1082
1859
  const handleCopyAll = () => {
1083
1860
  if (!content) return;
1084
1861
  navigator.clipboard.writeText(content).then(() => {
1085
- showToast('已复制文档全文到剪贴板', 'success');
1862
+ showToast('已复制全文到剪贴板', 'success');
1086
1863
  }).catch(() => {
1087
1864
  showToast('复制失败', 'error');
1088
1865
  });
1089
1866
  };
1090
1867
 
1091
- const handleTocClick = (headingId) => {
1092
- if (!contentRef.current) return;
1093
- const el = contentRef.current.querySelector(`#${headingId}`);
1094
- if (el) {
1095
- el.scrollIntoView({ behavior: 'smooth', block: 'start' });
1868
+ const handleFormatJSON = () => {
1869
+ try {
1870
+ const obj = JSON.parse(content);
1871
+ const formatted = JSON.stringify(obj, null, 2);
1872
+ navigator.clipboard.writeText(formatted).then(() => {
1873
+ showToast('已格式化 JSON 并复制到剪贴板', 'success');
1874
+ });
1875
+ } catch (e) {
1876
+ showToast('JSON 语法错误,无法格式化', 'error');
1877
+ }
1878
+ };
1879
+
1880
+ const handleMinifyJSON = () => {
1881
+ try {
1882
+ const obj = JSON.parse(content);
1883
+ const minified = JSON.stringify(obj);
1884
+ navigator.clipboard.writeText(minified).then(() => {
1885
+ showToast('已压缩为单行 JSON 并复制到剪贴板', 'success');
1886
+ });
1887
+ } catch (e) {
1888
+ showToast('JSON 语法错误,无法压缩', 'error');
1889
+ }
1890
+ };
1891
+
1892
+ const handleOutlineClick = (item) => {
1893
+ if (category === 'markdown') {
1894
+ if (!contentRef.current) return;
1895
+ const el = contentRef.current.querySelector(`#${item.id}`);
1896
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
1897
+ } else {
1898
+ const el = document.getElementById(`line-gutter-${item.line}`);
1899
+ if (el) {
1900
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
1901
+ el.classList.add('dsh-line-highlight-flash');
1902
+ setTimeout(() => el.classList.remove('dsh-line-highlight-flash'), 1800);
1903
+ }
1096
1904
  }
1097
1905
  };
1098
1906
 
@@ -1113,44 +1921,128 @@
1113
1921
  title: isTreeOpen ? '隐藏工作区目录树 (Ctrl/Cmd+B)' : '展开工作区目录树 (Ctrl/Cmd+B)',
1114
1922
  }, '🗂️ 目录树'),
1115
1923
 
1116
- h('div', { className: 'dsh-preview-mode-group' },
1117
- h('button', {
1118
- type: 'button',
1119
- className: `dsh-preview-mode-btn ${viewMode === 'preview' ? 'active' : ''}`,
1120
- onClick: () => handleModeChange('preview'),
1121
- title: '渲染预览模式 (Markdown Render)',
1122
- }, '📖 预览'),
1123
- h('button', {
1124
- type: 'button',
1125
- className: `dsh-preview-mode-btn ${viewMode === 'source' ? 'active' : ''}`,
1126
- onClick: () => handleModeChange('source'),
1127
- title: '源码模式 (Raw Source)',
1128
- }, '📝 源码'),
1129
- h('button', {
1130
- type: 'button',
1131
- className: `dsh-preview-mode-btn ${viewMode === 'split' ? 'active' : ''}`,
1132
- onClick: () => handleModeChange('split'),
1133
- title: '分栏对比模式 (Split View)',
1134
- }, '🌓 分栏')
1924
+ // Adaptive View Mode buttons based on File Category
1925
+ category === 'markdown' ? (
1926
+ h('div', { className: 'dsh-preview-mode-group' },
1927
+ h('button', {
1928
+ type: 'button',
1929
+ className: `dsh-preview-mode-btn ${viewMode === 'preview' ? 'active' : ''}`,
1930
+ onClick: () => handleModeChange('preview'),
1931
+ title: 'Markdown 渲染预览 (Markdown Render)',
1932
+ }, '📖 预览'),
1933
+ h('button', {
1934
+ type: 'button',
1935
+ className: `dsh-preview-mode-btn ${viewMode === 'source' ? 'active' : ''}`,
1936
+ onClick: () => handleModeChange('source'),
1937
+ title: '源码高亮模式 (Source Code)',
1938
+ }, '📝 源码'),
1939
+ h('button', {
1940
+ type: 'button',
1941
+ className: `dsh-preview-mode-btn ${viewMode === 'split' ? 'active' : ''}`,
1942
+ onClick: () => handleModeChange('split'),
1943
+ title: '分栏对照模式 (Split View)',
1944
+ }, '🌓 分栏')
1945
+ )
1946
+ ) : category === 'json' ? (
1947
+ h('div', { className: 'dsh-preview-mode-group' },
1948
+ h('button', {
1949
+ type: 'button',
1950
+ className: `dsh-preview-mode-btn ${viewMode === 'json-tree' ? 'active' : ''}`,
1951
+ onClick: () => handleModeChange('json-tree'),
1952
+ title: '可视化结构树检查器 (JSON Tree)',
1953
+ }, '🌳 结构树'),
1954
+ h('button', {
1955
+ type: 'button',
1956
+ className: `dsh-preview-mode-btn ${viewMode === 'code' ? 'active' : ''}`,
1957
+ onClick: () => handleModeChange('code'),
1958
+ title: '语法高亮代码 (Code View)',
1959
+ }, '📜 代码'),
1960
+ h('button', {
1961
+ type: 'button',
1962
+ className: `dsh-preview-mode-btn ${viewMode === 'raw' ? 'active' : ''}`,
1963
+ onClick: () => handleModeChange('raw'),
1964
+ title: '纯文本模式 (Raw Text)',
1965
+ }, '📝 纯文本')
1966
+ )
1967
+ ) : category === 'yaml' ? (
1968
+ h('div', { className: 'dsh-preview-mode-group' },
1969
+ h('button', {
1970
+ type: 'button',
1971
+ className: `dsh-preview-mode-btn ${viewMode === 'code' ? 'active' : ''}`,
1972
+ onClick: () => handleModeChange('code'),
1973
+ title: 'YAML 语法高亮 (Syntax Highlighting)',
1974
+ }, '📜 高亮'),
1975
+ h('button', {
1976
+ type: 'button',
1977
+ className: `dsh-preview-mode-btn ${viewMode === 'json-tree' ? 'active' : ''}`,
1978
+ onClick: () => handleModeChange('json-tree'),
1979
+ title: '转为可视化结构树 (YAML as Tree)',
1980
+ }, '🌳 结构树'),
1981
+ h('button', {
1982
+ type: 'button',
1983
+ className: `dsh-preview-mode-btn ${viewMode === 'raw' ? 'active' : ''}`,
1984
+ onClick: () => handleModeChange('raw'),
1985
+ title: '纯文本模式 (Raw Text)',
1986
+ }, '📝 纯文本')
1987
+ )
1988
+ ) : (
1989
+ h('div', { className: 'dsh-preview-mode-group' },
1990
+ h('button', {
1991
+ type: 'button',
1992
+ className: `dsh-preview-mode-btn ${viewMode === 'code' ? 'active' : ''}`,
1993
+ onClick: () => handleModeChange('code'),
1994
+ title: '语法高亮模式 (Syntax Highlighting)',
1995
+ }, '📜 高亮'),
1996
+ h('button', {
1997
+ type: 'button',
1998
+ className: `dsh-preview-mode-btn ${viewMode === 'raw' ? 'active' : ''}`,
1999
+ onClick: () => handleModeChange('raw'),
2000
+ title: '纯文本模式 (Raw Text)',
2001
+ }, '📝 纯文本')
2002
+ )
1135
2003
  ),
1136
2004
 
1137
- tocHeadings.length > 0 ? h('button', {
2005
+ // Outline / TOC Button
2006
+ outlineItems.length > 0 ? h('button', {
1138
2007
  type: 'button',
1139
2008
  className: `dsh-preview-tool-btn ${isTocOpen ? 'active' : ''}`,
1140
2009
  onClick: () => setIsTocOpen(!isTocOpen),
1141
- title: isTocOpen ? '隐藏目录大纲' : '显示目录大纲 (TOC)',
1142
- }, `📑 大纲 (${tocHeadings.length})`) : null,
2010
+ title: isTocOpen ? '隐藏大纲侧栏' : '显示大纲侧栏',
2011
+ }, category === 'markdown' ? `📑 大纲 (${outlineItems.length})` : `📑 符号 (${outlineItems.length})`) : null,
2012
+
2013
+ // Word Wrap Toggle
2014
+ h('button', {
2015
+ type: 'button',
2016
+ className: `dsh-preview-tool-btn ${isWordWrap ? 'active' : ''}`,
2017
+ onClick: () => setIsWordWrap(!isWordWrap),
2018
+ title: isWordWrap ? '切换为不自动换行 (水平滚动)' : '切换为自动换行',
2019
+ }, '↩️ 换行'),
1143
2020
 
2021
+ // Search Button
1144
2022
  h('button', {
1145
2023
  type: 'button',
1146
2024
  className: `dsh-preview-tool-btn ${isSearchOpen ? 'active' : ''}`,
1147
2025
  onClick: () => setIsSearchOpen(!isSearchOpen),
1148
- title: '在文档中查找',
2026
+ title: '在文件中搜索',
1149
2027
  }, '🔍 查找')
1150
2028
  ),
1151
2029
 
1152
2030
  // Right: Action buttons
1153
2031
  h('div', { className: 'dsh-preview-toolbar-right' },
2032
+ category === 'json' ? h('button', {
2033
+ type: 'button',
2034
+ className: 'dsh-preview-tool-btn',
2035
+ onClick: handleFormatJSON,
2036
+ title: '格式化 JSON 并复制',
2037
+ }, '⚡ 格式化') : null,
2038
+
2039
+ category === 'json' ? h('button', {
2040
+ type: 'button',
2041
+ className: 'dsh-preview-tool-btn',
2042
+ onClick: handleMinifyJSON,
2043
+ title: '单行压缩 JSON 并复制',
2044
+ }, '🗜️ 压缩') : null,
2045
+
1154
2046
  h('button', {
1155
2047
  type: 'button',
1156
2048
  className: 'dsh-preview-tool-btn',
@@ -1199,12 +2091,12 @@
1199
2091
  }, '✕') : null
1200
2092
  ) : null,
1201
2093
 
1202
- // Body area with optional TOC sidebar + Main content
2094
+ // Body Layout
1203
2095
  h('div', { className: 'dsh-preview-body-layout' },
1204
- // TOC Sidebar
1205
- isTocOpen && tocHeadings.length > 0 ? h('div', { className: 'dsh-preview-toc-sidebar' },
2096
+ // Outline / TOC Sidebar
2097
+ isTocOpen && outlineItems.length > 0 ? h('div', { className: 'dsh-preview-toc-sidebar' },
1206
2098
  h('div', { className: 'dsh-preview-toc-header' },
1207
- h('span', { style: { fontWeight: 600, fontSize: 12 } }, '目录大纲 (TOC)'),
2099
+ h('span', { style: { fontWeight: 600, fontSize: 12 } }, category === 'markdown' ? '目录大纲 (TOC)' : '符号与属性大纲'),
1208
2100
  h('button', {
1209
2101
  type: 'button',
1210
2102
  className: 'dsh-toc-close-btn',
@@ -1212,73 +2104,95 @@
1212
2104
  }, '✕')
1213
2105
  ),
1214
2106
  h('div', { className: 'dsh-preview-toc-list' },
1215
- tocHeadings.map((hItem, idx) =>
2107
+ outlineItems.map((item, idx) =>
1216
2108
  h('div', {
1217
2109
  key: idx,
1218
- className: `dsh-toc-item dsh-toc-level-${hItem.level}`,
1219
- onClick: () => handleTocClick(hItem.id),
1220
- title: hItem.text,
2110
+ className: `dsh-toc-item ${item.level ? `dsh-toc-level-${item.level}` : 'dsh-symbol-item'}`,
2111
+ onClick: () => handleOutlineClick(item),
2112
+ title: item.text,
1221
2113
  },
1222
- h('span', { className: 'dsh-toc-bullet' }, '•'),
1223
- h('span', { className: 'dsh-toc-text' }, hItem.text)
2114
+ h('span', { className: 'dsh-toc-text' }, item.text)
1224
2115
  )
1225
2116
  )
1226
2117
  )
1227
2118
  ) : null,
1228
2119
 
1229
- // Main Viewer
2120
+ // Main View Content
1230
2121
  h('div', { className: 'dsh-preview-main-scroll' },
1231
2122
  isLoading ? h('div', { className: 'dsh-preview-loading' },
1232
2123
  h('div', { className: 'dsh-preview-spinner' }),
1233
- h('span', null, '正在加载文档内容...')
2124
+ h('span', null, '正在加载文件内容...')
1234
2125
  ) : (error || fileInfo?.error) ? h('div', { className: 'dsh-preview-error-card' },
1235
2126
  h('div', { className: 'dsh-error-icon' }, '⚠️'),
1236
- h('div', { className: 'dsh-error-title' }, '读取文档失败'),
2127
+ h('div', { className: 'dsh-error-title' }, '读取文件失败'),
1237
2128
  h('div', { className: 'dsh-error-desc' }, error || fileInfo?.error),
1238
2129
  h('div', { className: 'dsh-error-actions' },
1239
2130
  h('button', { type: 'button', className: 'dsh-btn-retry', onClick: onRefresh }, '重试'),
1240
2131
  h('button', { type: 'button', className: 'dsh-btn-native', onClick: () => onOpenNative(tab.filePath) }, '在外部打开')
1241
2132
  )
1242
- ) : viewMode === 'preview' ? (
1243
- // 1. Preview Mode
2133
+ ) : viewMode === 'json-tree' ? (
2134
+ // JSON / YAML Tree Inspector
2135
+ parsedTreeData.ok ? (
2136
+ h(JSONTreeInspector, {
2137
+ data: parsedTreeData.data,
2138
+ rawContent: content,
2139
+ onFormat: category === 'json' ? handleFormatJSON : null,
2140
+ onMinify: category === 'json' ? handleMinifyJSON : null,
2141
+ })
2142
+ ) : (
2143
+ h('div', { className: 'dsh-json-parse-error' },
2144
+ h('div', { style: { fontSize: 28, marginBottom: 8 } }, '⚠️'),
2145
+ h('div', { style: { fontWeight: 600, fontSize: 14, marginBottom: 4 } }, '结构解析失败'),
2146
+ h('div', { style: { fontSize: 12, color: '#ef4444', marginBottom: 12 } }, parsedTreeData.error || '无法按树状结构解析'),
2147
+ h('button', {
2148
+ type: 'button',
2149
+ className: 'dsh-btn-native',
2150
+ onClick: () => handleModeChange('code')
2151
+ }, '切换到代码高亮模式查看')
2152
+ )
2153
+ )
2154
+ ) : viewMode === 'preview' && category === 'markdown' ? (
2155
+ // Markdown Render View
1244
2156
  h('div', {
1245
2157
  ref: contentRef,
1246
2158
  className: 'dsh-markdown-body',
1247
- dangerouslySetInnerHTML: { __html: renderedHtml },
2159
+ dangerouslySetInnerHTML: { __html: renderedMarkdownHtml },
1248
2160
  })
1249
- ) : viewMode === 'source' ? (
1250
- // 2. Source Code Mode
1251
- h('div', { className: 'dsh-source-view' },
1252
- h('div', { className: 'dsh-source-line-numbers' },
1253
- (content.split('\n') || []).map((_, i) => h('div', { key: i }, i + 1))
1254
- ),
1255
- h('pre', { ref: sourceRef, className: 'dsh-source-pre' },
1256
- h('code', null, content)
1257
- )
1258
- )
1259
- ) : (
1260
- // 3. Split Mode
2161
+ ) : viewMode === 'split' && category === 'markdown' ? (
2162
+ // Markdown Split View
1261
2163
  h('div', { className: 'dsh-split-view' },
1262
2164
  h('div', { className: 'dsh-split-pane dsh-split-source' },
1263
- h('div', { className: 'dsh-split-pane-header' }, '📝 原始源码'),
1264
- h('div', { className: 'dsh-source-view' },
1265
- h('div', { className: 'dsh-source-line-numbers' },
1266
- (content.split('\n') || []).map((_, i) => h('div', { key: i }, i + 1))
1267
- ),
1268
- h('pre', { className: 'dsh-source-pre' },
1269
- h('code', null, content)
1270
- )
1271
- )
2165
+ h('div', { className: 'dsh-split-pane-header' }, '📝 源码'),
2166
+ h(CodeEditorView, {
2167
+ content,
2168
+ category: 'markdown',
2169
+ isHighlighted: true,
2170
+ isWordWrap,
2171
+ searchQuery,
2172
+ onLineClick: () => {},
2173
+ })
1272
2174
  ),
1273
2175
  h('div', { className: 'dsh-split-pane dsh-split-render' },
1274
2176
  h('div', { className: 'dsh-split-pane-header' }, '📖 渲染预览'),
1275
2177
  h('div', {
1276
2178
  ref: contentRef,
1277
2179
  className: 'dsh-markdown-body',
1278
- dangerouslySetInnerHTML: { __html: renderedHtml },
2180
+ dangerouslySetInnerHTML: { __html: renderedMarkdownHtml },
1279
2181
  })
1280
2182
  )
1281
2183
  )
2184
+ ) : (
2185
+ // Code View (for code/json/yaml/source/raw modes)
2186
+ h(CodeEditorView, {
2187
+ content,
2188
+ category,
2189
+ isHighlighted: viewMode !== 'raw',
2190
+ isWordWrap,
2191
+ searchQuery,
2192
+ onLineClick: (lineNum) => {
2193
+ showToast(`已选定第 ${lineNum} 行`, 'info');
2194
+ },
2195
+ })
1282
2196
  )
1283
2197
  )
1284
2198
  ),
@@ -1286,11 +2200,11 @@
1286
2200
  // Status Footer
1287
2201
  h('div', { className: 'dsh-preview-footer' },
1288
2202
  h('div', { className: 'dsh-footer-left', title: fileInfo?.path || tab.filePath },
1289
- h('span', { className: 'dsh-footer-ext' }, (fileInfo?.extension || tab.extension || '.md').toUpperCase().replace('.', '')),
2203
+ h('span', { className: 'dsh-footer-ext' }, (fileInfo?.extension || tab.extension || category).toUpperCase().replace('.', '')),
1290
2204
  h('span', { className: 'dsh-footer-path' }, fileInfo?.path || tab.filePath)
1291
2205
  ),
1292
2206
  h('div', { className: 'dsh-footer-right' },
1293
- fileInfo?.size ? h('span', { className: 'dsh-footer-stat' }, `${(fileInfo.size / 1024).toFixed(1)} KB`) : null,
2207
+ fileInfo?.size ? h('span', { className: 'dsh-footer-stat' }, formatBytes(fileInfo.size)) : null,
1294
2208
  fileInfo?.lineCount ? h('span', { className: 'dsh-footer-stat' }, `${fileInfo.lineCount} 行`) : null,
1295
2209
  fileInfo?.wordCount ? h('span', { className: 'dsh-footer-stat' }, `约 ${fileInfo.wordCount} 字`) : null
1296
2210
  )
@@ -1579,6 +2493,7 @@
1579
2493
 
1580
2494
  // Right: Content View or Welcome View
1581
2495
  activeTab ? h(MarkdownPreviewView, {
2496
+ key: activeTab.id,
1582
2497
  tab: activeTab,
1583
2498
  fileInfo: currentFileInfo,
1584
2499
  isLoading: currentLoading,
@@ -3048,6 +3963,298 @@
3048
3963
  .dsh-welcome-btn.primary:hover {
3049
3964
  opacity: 0.9;
3050
3965
  }
3966
+
3967
+ /* Syntax Highlighting Tokens (Light Theme) */
3968
+ .tok-keyword { color: #0000ff; font-weight: 600; }
3969
+ .tok-string { color: #a31515; }
3970
+ .tok-number { color: #098658; }
3971
+ .tok-boolean { color: #0000ff; font-weight: 600; }
3972
+ .tok-null { color: #708090; font-style: italic; }
3973
+ .tok-key { color: #001080; font-weight: 600; }
3974
+ .tok-comment { color: #008000; font-style: italic; }
3975
+ .tok-type { color: #267f99; }
3976
+ .tok-func { color: #795e26; }
3977
+ .tok-operator { color: #000000; font-weight: 500; }
3978
+ .tok-punctuation, .tok-bracket { color: #333333; }
3979
+ .tok-tag { color: #800000; font-weight: 600; }
3980
+ .tok-attr { color: #e50000; }
3981
+ .tok-prop { color: #001080; }
3982
+ .tok-selector { color: #800000; }
3983
+
3984
+ /* Syntax Highlighting Tokens (Dark Theme) */
3985
+ body[data-ds-dark-theme] .tok-keyword { color: #569cd6; }
3986
+ body[data-ds-dark-theme] .tok-string { color: #ce9178; }
3987
+ body[data-ds-dark-theme] .tok-number { color: #b5cea8; }
3988
+ body[data-ds-dark-theme] .tok-boolean { color: #569cd6; font-weight: 600; }
3989
+ body[data-ds-dark-theme] .tok-null { color: #808080; font-style: italic; }
3990
+ body[data-ds-dark-theme] .tok-key { color: #9cdcfe; font-weight: 600; }
3991
+ body[data-ds-dark-theme] .tok-comment { color: #6a9955; font-style: italic; }
3992
+ body[data-ds-dark-theme] .tok-type { color: #4ec9b0; }
3993
+ body[data-ds-dark-theme] .tok-func { color: #dcdcaa; }
3994
+ body[data-ds-dark-theme] .tok-operator { color: #d4d4d4; }
3995
+ body[data-ds-dark-theme] .tok-punctuation, body[data-ds-dark-theme] .tok-bracket { color: #d4d4d4; }
3996
+ body[data-ds-dark-theme] .tok-tag { color: #569cd6; }
3997
+ body[data-ds-dark-theme] .tok-attr { color: #9cdcfe; }
3998
+ body[data-ds-dark-theme] .tok-prop { color: #9cdcfe; }
3999
+ body[data-ds-dark-theme] .tok-selector { color: #d7ba7d; }
4000
+
4001
+ /* Code Editor Viewer */
4002
+ .dsh-code-editor-view {
4003
+ display: flex;
4004
+ min-height: 100%;
4005
+ font-family: "Cascadia Code", "Fira Code", Consolas, Menlo, monospace;
4006
+ font-size: 13px;
4007
+ line-height: 20px;
4008
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
4009
+ color: var(--dsw-alias-label-primary, #1e293b);
4010
+ }
4011
+ body[data-ds-dark-theme] .dsh-code-editor-view {
4012
+ background: #181b24;
4013
+ color: #d4d4d4;
4014
+ }
4015
+
4016
+ .dsh-code-gutter {
4017
+ width: 44px;
4018
+ min-width: 44px;
4019
+ padding: 12px 6px 12px 0;
4020
+ text-align: right;
4021
+ user-select: none;
4022
+ background: var(--dsw-alias-bg-layer-2, #f8fafc);
4023
+ border-right: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
4024
+ color: var(--dsw-alias-label-tertiary, #94a3b8);
4025
+ font-size: 12px;
4026
+ line-height: 20px;
4027
+ flex-shrink: 0;
4028
+ }
4029
+ body[data-ds-dark-theme] .dsh-code-gutter {
4030
+ background: #141720;
4031
+ border-right-color: #2a3140;
4032
+ color: #64748b;
4033
+ }
4034
+
4035
+ .dsh-gutter-line {
4036
+ height: 20px;
4037
+ cursor: pointer;
4038
+ padding-right: 8px;
4039
+ }
4040
+ .dsh-gutter-line:hover {
4041
+ color: var(--dsw-brand-primary, #0284c7);
4042
+ font-weight: bold;
4043
+ }
4044
+ .dsh-gutter-line.dsh-line-highlight-flash {
4045
+ background: rgba(14, 165, 233, 0.25);
4046
+ color: #0284c7;
4047
+ }
4048
+
4049
+ .dsh-code-content {
4050
+ flex: 1;
4051
+ min-width: 0;
4052
+ padding: 12px 16px;
4053
+ overflow: auto;
4054
+ }
4055
+ .dsh-code-pre {
4056
+ margin: 0;
4057
+ font-family: inherit;
4058
+ font-size: inherit;
4059
+ line-height: 20px;
4060
+ tab-size: 2;
4061
+ white-space: pre;
4062
+ }
4063
+ .dsh-code-editor-view.word-wrap .dsh-code-pre {
4064
+ white-space: pre-wrap;
4065
+ word-break: normal;
4066
+ overflow-wrap: break-word;
4067
+ }
4068
+ .dsh-code-editor-view {
4069
+ display: flex;
4070
+ min-height: 100%;
4071
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
4072
+ font-size: 12.5px;
4073
+ line-height: 20px;
4074
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
4075
+ color: var(--dsw-alias-label-primary, #1e293b);
4076
+ }
4077
+ .dsh-code-pre code {
4078
+ font-family: inherit;
4079
+ }
4080
+
4081
+ /* Interactive JSON Inspector */
4082
+ .dsh-json-inspector {
4083
+ display: flex;
4084
+ flex-direction: column;
4085
+ height: 100%;
4086
+ overflow: hidden;
4087
+ font-family: "Cascadia Code", Consolas, monospace;
4088
+ font-size: 12px;
4089
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
4090
+ }
4091
+ body[data-ds-dark-theme] .dsh-json-inspector {
4092
+ background: #181b24;
4093
+ color: #d4d4d4;
4094
+ }
4095
+
4096
+ .dsh-json-header {
4097
+ display: flex;
4098
+ align-items: center;
4099
+ justify-content: space-between;
4100
+ padding: 8px 12px;
4101
+ border-bottom: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
4102
+ background: var(--dsw-alias-bg-layer-2, #f8fafc);
4103
+ gap: 12px;
4104
+ flex-shrink: 0;
4105
+ }
4106
+ body[data-ds-dark-theme] .dsh-json-header {
4107
+ border-bottom-color: #2a3140;
4108
+ background: #141720;
4109
+ }
4110
+
4111
+ .dsh-json-search-box {
4112
+ position: relative;
4113
+ display: flex;
4114
+ align-items: center;
4115
+ flex: 1;
4116
+ max-width: 280px;
4117
+ }
4118
+ .dsh-json-search-icon {
4119
+ position: absolute;
4120
+ left: 8px;
4121
+ font-size: 11px;
4122
+ pointer-events: none;
4123
+ }
4124
+ .dsh-json-search-input {
4125
+ width: 100%;
4126
+ height: 26px;
4127
+ border-radius: 6px;
4128
+ border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
4129
+ padding: 0 24px 0 26px;
4130
+ font-size: 12px;
4131
+ background: #ffffff;
4132
+ color: inherit;
4133
+ outline: none;
4134
+ }
4135
+ body[data-ds-dark-theme] .dsh-json-search-input {
4136
+ background: #1e2330;
4137
+ border-color: #334155;
4138
+ color: #f1f5f9;
4139
+ }
4140
+ .dsh-json-actions {
4141
+ display: flex;
4142
+ gap: 6px;
4143
+ }
4144
+ .dsh-json-btn {
4145
+ padding: 3px 8px;
4146
+ border-radius: 4px;
4147
+ border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
4148
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
4149
+ color: var(--dsw-alias-label-secondary, #475569);
4150
+ cursor: pointer;
4151
+ font-size: 11px;
4152
+ transition: all 0.15s;
4153
+ }
4154
+ .dsh-json-btn:hover {
4155
+ background: var(--dsw-alias-bg-hover, rgba(0, 0, 0, 0.05));
4156
+ color: var(--dsw-alias-label-primary, #0f172a);
4157
+ }
4158
+ body[data-ds-dark-theme] .dsh-json-btn {
4159
+ background: #1e2330;
4160
+ border-color: #334155;
4161
+ color: #94a3b8;
4162
+ }
4163
+ body[data-ds-dark-theme] .dsh-json-btn:hover {
4164
+ background: #283042;
4165
+ color: #f1f5f9;
4166
+ }
4167
+
4168
+ .dsh-json-tree-body {
4169
+ flex: 1;
4170
+ overflow: auto;
4171
+ padding: 10px 12px;
4172
+ }
4173
+
4174
+ .dsh-json-row {
4175
+ display: flex;
4176
+ align-items: center;
4177
+ min-height: 22px;
4178
+ line-height: 22px;
4179
+ gap: 6px;
4180
+ border-radius: 4px;
4181
+ padding-right: 8px;
4182
+ transition: background 0.1s;
4183
+ }
4184
+ .dsh-json-row:hover {
4185
+ background: rgba(0, 0, 0, 0.03);
4186
+ }
4187
+ body[data-ds-dark-theme] .dsh-json-row:hover {
4188
+ background: rgba(255, 255, 255, 0.04);
4189
+ }
4190
+ .dsh-json-row-expandable {
4191
+ cursor: pointer;
4192
+ }
4193
+
4194
+ .dsh-json-chevron {
4195
+ font-size: 9px;
4196
+ color: #94a3b8;
4197
+ width: 12px;
4198
+ height: 12px;
4199
+ display: inline-flex;
4200
+ align-items: center;
4201
+ justify-content: center;
4202
+ flex-shrink: 0;
4203
+ user-select: none;
4204
+ }
4205
+ .dsh-json-key {
4206
+ color: #001080;
4207
+ font-weight: 600;
4208
+ flex-shrink: 0;
4209
+ }
4210
+ body[data-ds-dark-theme] .dsh-json-key {
4211
+ color: #9cdcfe;
4212
+ }
4213
+ .dsh-json-type-badge {
4214
+ font-size: 10px;
4215
+ color: #94a3b8;
4216
+ background: rgba(0, 0, 0, 0.05);
4217
+ padding: 0 5px;
4218
+ border-radius: 6px;
4219
+ user-select: none;
4220
+ }
4221
+ body[data-ds-dark-theme] .dsh-json-type-badge {
4222
+ background: rgba(255, 255, 255, 0.08);
4223
+ color: #64748b;
4224
+ }
4225
+ .dsh-json-preview {
4226
+ color: #64748b;
4227
+ font-style: italic;
4228
+ font-size: 11px;
4229
+ }
4230
+ body[data-ds-dark-theme] .dsh-json-preview {
4231
+ color: #94a3b8;
4232
+ }
4233
+ .dsh-json-copy-btn {
4234
+ opacity: 0;
4235
+ cursor: pointer;
4236
+ font-size: 10px;
4237
+ margin-left: auto;
4238
+ padding: 1px 4px;
4239
+ border-radius: 3px;
4240
+ transition: opacity 0.15s;
4241
+ }
4242
+ .dsh-json-row:hover .dsh-json-copy-btn {
4243
+ opacity: 0.75;
4244
+ }
4245
+ .dsh-json-copy-btn:hover {
4246
+ opacity: 1;
4247
+ background: rgba(0, 0, 0, 0.08);
4248
+ }
4249
+
4250
+ .dsh-json-parse-error {
4251
+ padding: 36px 16px;
4252
+ text-align: center;
4253
+ color: var(--dsw-alias-label-secondary, #64748b);
4254
+ }
4255
+ .dsh-symbol-item {
4256
+ padding-left: 10px;
4257
+ }
3051
4258
  `;
3052
4259
  document.head.appendChild(style);
3053
4260
  }