@rooode/dsh-plugin-preview 0.1.3 → 0.1.5

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/lib/client.js CHANGED
@@ -33,8 +33,13 @@
33
33
  const SUPPORTED_EXTENSIONS = new Set([
34
34
  '.md', '.markdown', '.mdown', '.mkdn', '.mdwn',
35
35
  '.txt', '.log', '.json', '.yaml', '.yml',
36
- '.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss',
37
- '.py', '.sh', '.bash', '.ps1', '.sql', '.toml', '.xml'
36
+ '.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.less',
37
+ '.java', '.jav', '.jsp',
38
+ '.cpp', '.c', '.cc', '.cxx', '.h', '.hpp', '.hxx', '.inl',
39
+ '.py', '.pyw', '.ipynb',
40
+ '.go', '.rs', '.cs', '.kt',
41
+ '.sh', '.bash', '.zsh', '.ps1', '.bat', '.cmd',
42
+ '.sql', '.toml', '.xml', '.svg', '.ini', '.env', '.properties'
38
43
  ]);
39
44
 
40
45
  // =========================================================================
@@ -172,13 +177,49 @@
172
177
  if (ext === '.yaml' || ext === '.yml' || ext === '.toml' || ext === '.ini') return { icon: '⚙️', color: '#f97316' };
173
178
  if (ext === '.html' || ext === '.htm') return { icon: '🌐', color: '#ef4444' };
174
179
  if (ext === '.css' || ext === '.scss' || ext === '.less') return { icon: '#', color: '#06b6d4' };
175
- if (ext === '.py') return { icon: '🐍', color: '#38bdf8' };
176
- if (ext === '.sh' || ext === '.bash' || ext === '.ps1') return { icon: '⌨️', color: '#8b5cf6' };
180
+ if (ext === '.java' || ext === '.jav') return { icon: '', color: '#ea580c' };
181
+ if (['.cpp', '.cc', '.cxx', '.hpp', '.hxx'].includes(ext)) return { icon: 'C++', color: '#0284c7' };
182
+ if (ext === '.c' || ext === '.h') return { icon: 'C', color: '#64748b' };
183
+ if (ext === '.py' || ext === '.pyw') return { icon: '🐍', color: '#38bdf8' };
184
+ if (ext === '.go') return { icon: 'GO', color: '#00add8' };
185
+ if (ext === '.rs') return { icon: '🦀', color: '#ea580c' };
186
+ if (ext === '.cs') return { icon: 'C#', color: '#a855f7' };
187
+ if (ext === '.kt') return { icon: 'KT', color: '#7c3aed' };
188
+ if (ext === '.sh' || ext === '.bash' || ext === '.ps1' || ext === '.zsh') return { icon: '⌨️', color: '#8b5cf6' };
177
189
  if (['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp'].includes(ext)) return { icon: '🖼️', color: '#ec4899' };
178
190
  if (ext === '.log' || ext === '.txt') return { icon: '📄', color: '#64748b' };
179
191
  return { icon: '📄', color: '#94a3b8' };
180
192
  }
181
193
 
194
+
195
+ function getFileTypeCategory(filePath) {
196
+ if (!filePath) return 'text';
197
+ const ext = getFileExtension(filePath).toLowerCase();
198
+ if (['.md', '.markdown', '.mdown', '.mkdn', '.mdwn'].includes(ext)) return 'markdown';
199
+ if (['.json', '.jsonc', '.json5', '.geojson', '.lock'].includes(ext)) return 'json';
200
+ if (['.yaml', '.yml'].includes(ext)) return 'yaml';
201
+ if (['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts'].includes(ext)) return 'javascript';
202
+ if (['.java', '.jav', '.jsp'].includes(ext)) return 'java';
203
+ if (['.cpp', '.c', '.cc', '.cxx', '.h', '.hpp', '.hxx', '.inl'].includes(ext)) return 'cpp';
204
+ if (['.py', '.pyw', '.ipynb'].includes(ext)) return 'python';
205
+ if (['.go'].includes(ext)) return 'go';
206
+ if (['.rs'].includes(ext)) return 'rust';
207
+ if (['.cs'].includes(ext)) return 'csharp';
208
+ if (['.html', '.htm', '.svg', '.xml'].includes(ext)) return 'markup';
209
+ if (['.css', '.scss', '.less', '.sass'].includes(ext)) return 'css';
210
+ if (['.sh', '.bash', '.zsh', '.ps1', '.bat', '.cmd'].includes(ext)) return 'shell';
211
+ if (['.sql'].includes(ext)) return 'sql';
212
+ if (['.toml', '.ini', '.env', '.properties', '.conf', '.cfg', '.gitignore', '.gitattributes', '.editorconfig'].includes(ext)) return 'config';
213
+ return 'text';
214
+ }
215
+
216
+ function getDefaultViewMode(category) {
217
+ if (category === 'markdown') return 'preview';
218
+ if (category === 'json') return 'json-tree';
219
+ if (category === 'yaml') return 'code';
220
+ return 'code';
221
+ }
222
+
182
223
  function getCurrentWorkspaceRoot() {
183
224
  if (clientCtx && clientCtx.workspaces) {
184
225
  try {
@@ -248,12 +289,15 @@
248
289
  globalActiveTabId = globalTabs[existingIndex].id;
249
290
  globalTabs[existingIndex].lastActiveAt = Date.now();
250
291
  } else {
292
+ const category = getFileTypeCategory(fullPath);
293
+ const defaultMode = getDefaultViewMode(category);
251
294
  const newTab = {
252
295
  id: tabId,
253
296
  filePath: fullPath,
254
297
  title: fileName,
255
298
  extension: ext,
256
- viewMode: options.viewMode || 'preview',
299
+ category: category,
300
+ viewMode: options.viewMode || defaultMode,
257
301
  pinned: false,
258
302
  createdAt: Date.now(),
259
303
  lastActiveAt: Date.now(),
@@ -1053,10 +1097,961 @@
1053
1097
  }
1054
1098
 
1055
1099
  // =========================================================================
1100
+
1101
+ // =========================================================================
1102
+ // Multi-Language Syntax Highlighting & Tokenizers (Single-Pass Safe)
1103
+ // =========================================================================
1104
+ function highlightJSON(jsonStr) {
1105
+ if (!jsonStr) return '';
1106
+ const regex = /("(?:\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?|[{}\[\],:])/g;
1107
+ let lastIndex = 0;
1108
+ let out = '';
1109
+ let match;
1110
+
1111
+ while ((match = regex.exec(jsonStr)) !== null) {
1112
+ const raw = match[0];
1113
+ const index = match.index;
1114
+ if (index > lastIndex) {
1115
+ out += escapeHtml(jsonStr.slice(lastIndex, index));
1116
+ }
1117
+ lastIndex = regex.lastIndex;
1118
+
1119
+ if (raw.startsWith('"')) {
1120
+ if (raw.endsWith(':')) {
1121
+ const keyText = raw.slice(0, -1).trim();
1122
+ out += '<span class="tok-key">' + escapeHtml(keyText) + '</span>:';
1123
+ } else {
1124
+ out += '<span class="tok-string">' + escapeHtml(raw) + '</span>';
1125
+ }
1126
+ } else if (raw === 'true' || raw === 'false') {
1127
+ out += '<span class="tok-boolean">' + raw + '</span>';
1128
+ } else if (raw === 'null') {
1129
+ out += '<span class="tok-null">' + raw + '</span>';
1130
+ } else if (/^-?\d/.test(raw)) {
1131
+ out += '<span class="tok-number">' + raw + '</span>';
1132
+ } else if (/[{}\[\]]/.test(raw)) {
1133
+ out += '<span class="tok-bracket">' + escapeHtml(raw) + '</span>';
1134
+ } else if (/[:,]/.test(raw)) {
1135
+ out += '<span class="tok-punctuation">' + raw + '</span>';
1136
+ } else {
1137
+ out += escapeHtml(raw);
1138
+ }
1139
+ }
1140
+
1141
+ if (lastIndex < jsonStr.length) {
1142
+ out += escapeHtml(jsonStr.slice(lastIndex));
1143
+ }
1144
+ return out;
1145
+ }
1146
+
1147
+ function highlightYAML(yamlStr) {
1148
+ if (!yamlStr) return '';
1149
+ const lines = yamlStr.split('\n');
1150
+ return lines.map(line => {
1151
+ const commentIdx = line.indexOf('#');
1152
+ let mainPart = line;
1153
+ let commentPart = '';
1154
+ if (commentIdx !== -1) {
1155
+ mainPart = line.slice(0, commentIdx);
1156
+ commentPart = '<span class="tok-comment">' + escapeHtml(line.slice(commentIdx)) + '</span>';
1157
+ }
1158
+
1159
+ let highlightedMain = '';
1160
+ const kvMatch = mainPart.match(/^(\s*-\s+)?([a-zA-Z0-9_\-./@$]+)(\s*:\s*)(.*)$/);
1161
+ if (kvMatch) {
1162
+ const prefix = kvMatch[1] ? '<span class="tok-operator">' + escapeHtml(kvMatch[1]) + '</span>' : '';
1163
+ const key = '<span class="tok-key">' + escapeHtml(kvMatch[2]) + '</span>';
1164
+ const colon = '<span class="tok-punctuation">' + escapeHtml(kvMatch[3]) + '</span>';
1165
+ let val = kvMatch[4];
1166
+ const trimmedVal = val.trim();
1167
+ let valHtml = escapeHtml(val);
1168
+ if (/^(true|false|yes|no|on|off)$/i.test(trimmedVal)) {
1169
+ valHtml = '<span class="tok-boolean">' + escapeHtml(val) + '</span>';
1170
+ } else if (/^(null|~)$/i.test(trimmedVal)) {
1171
+ valHtml = '<span class="tok-null">' + escapeHtml(val) + '</span>';
1172
+ } else if (/^-?\d+(\.\d+)?$/.test(trimmedVal)) {
1173
+ valHtml = '<span class="tok-number">' + escapeHtml(val) + '</span>';
1174
+ } else if (/^["'].*["']$/.test(trimmedVal)) {
1175
+ valHtml = '<span class="tok-string">' + escapeHtml(val) + '</span>';
1176
+ } else if (trimmedVal.startsWith('&') || trimmedVal.startsWith('*')) {
1177
+ valHtml = '<span class="tok-type">' + escapeHtml(val) + '</span>';
1178
+ }
1179
+ highlightedMain = prefix + key + colon + valHtml;
1180
+ } else {
1181
+ const bulletMatch = mainPart.match(/^(\s*-\s+)(.*)$/);
1182
+ if (bulletMatch) {
1183
+ highlightedMain = '<span class="tok-operator">' + escapeHtml(bulletMatch[1]) + '</span>' + escapeHtml(bulletMatch[2]);
1184
+ } else {
1185
+ highlightedMain = escapeHtml(mainPart);
1186
+ }
1187
+ }
1188
+ return highlightedMain + commentPart;
1189
+ }).join('\n');
1190
+ }
1191
+
1192
+ function highlightJS(code) {
1193
+ if (!code) return '';
1194
+ const keywords = new Set([
1195
+ 'const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'do',
1196
+ 'switch', 'case', 'break', 'continue', 'default', 'new', 'delete', 'typeof', 'instanceof',
1197
+ 'void', 'yield', 'await', 'async', 'try', 'catch', 'finally', 'throw', 'class',
1198
+ 'extends', 'super', 'this', 'import', 'export', 'from', 'as', 'type', 'interface',
1199
+ 'enum', 'implements', 'declare', 'namespace', 'public', 'private', 'protected',
1200
+ 'readonly', 'static', 'abstract', 'get', 'set', 'of', 'in', 'debugger', 'constructor'
1201
+ ]);
1202
+ const types = new Set([
1203
+ 'Promise', 'Array', 'Object', 'String', 'Number', 'Boolean', 'Map', 'Set', 'WeakMap',
1204
+ 'WeakSet', 'JSON', 'Math', 'RegExp', 'Date', 'Error', 'Symbol', 'React', 'ReactDOM',
1205
+ 'null', 'undefined', 'true', 'false', 'any', 'unknown', 'never', 'void', 'string',
1206
+ 'number', 'boolean', 'bigint', 'Record', 'Partial', 'Required', 'Pick', 'Omit'
1207
+ ]);
1208
+
1209
+ 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;
1210
+
1211
+ let lastIndex = 0;
1212
+ let out = '';
1213
+ let match;
1214
+
1215
+ while ((match = tokenRegex.exec(code)) !== null) {
1216
+ const raw = match[0];
1217
+ const index = match.index;
1218
+ if (index > lastIndex) {
1219
+ out += escapeHtml(code.slice(lastIndex, index));
1220
+ }
1221
+ lastIndex = tokenRegex.lastIndex;
1222
+
1223
+ if (raw.startsWith('/*') || raw.startsWith('//')) {
1224
+ out += '<span class="tok-comment">' + escapeHtml(raw) + '</span>';
1225
+ } else if (raw.startsWith('`') || raw.startsWith('"') || raw.startsWith("'")) {
1226
+ out += '<span class="tok-string">' + escapeHtml(raw) + '</span>';
1227
+ } else if (/^\d|^0x/.test(raw)) {
1228
+ out += '<span class="tok-number">' + escapeHtml(raw) + '</span>';
1229
+ } else if (/^[a-zA-Z_$]/.test(raw)) {
1230
+ if (keywords.has(raw)) {
1231
+ out += '<span class="tok-keyword">' + escapeHtml(raw) + '</span>';
1232
+ } else if (types.has(raw)) {
1233
+ out += '<span class="tok-type">' + escapeHtml(raw) + '</span>';
1234
+ } else {
1235
+ const remainder = code.slice(lastIndex);
1236
+ if (/^\s*\(/.test(remainder)) {
1237
+ out += '<span class="tok-func">' + escapeHtml(raw) + '</span>';
1238
+ } else {
1239
+ out += escapeHtml(raw);
1240
+ }
1241
+ }
1242
+ } else if (/^(=>|===|!==|<=|>=|&&|\|\||\?\?|\?\.|[+\-*/%=!&|^~<>])$/.test(raw)) {
1243
+ out += '<span class="tok-operator">' + escapeHtml(raw) + '</span>';
1244
+ } else if (/^[{}()\[\]]$/.test(raw)) {
1245
+ out += '<span class="tok-bracket">' + escapeHtml(raw) + '</span>';
1246
+ } else if (/^[,;:?]$/.test(raw)) {
1247
+ out += '<span class="tok-punctuation">' + escapeHtml(raw) + '</span>';
1248
+ } else {
1249
+ out += escapeHtml(raw);
1250
+ }
1251
+ }
1252
+
1253
+ if (lastIndex < code.length) {
1254
+ out += escapeHtml(code.slice(lastIndex));
1255
+ }
1256
+ return out;
1257
+ }
1258
+
1259
+ function highlightJava(code) {
1260
+ if (!code) return '';
1261
+ const keywords = new Set([
1262
+ 'public', 'private', 'protected', 'class', 'interface', 'enum', 'record', 'extends',
1263
+ 'implements', 'static', 'final', 'abstract', 'void', 'return', 'if', 'else', 'for',
1264
+ 'while', 'do', 'switch', 'case', 'break', 'continue', 'default', 'new', 'this', 'super',
1265
+ 'try', 'catch', 'finally', 'throw', 'throws', 'import', 'package', 'synchronized',
1266
+ 'volatile', 'transient', 'native', 'strictfp', 'instanceof', 'assert', 'yield', 'sealed',
1267
+ 'permits', 'var', 'const', 'goto'
1268
+ ]);
1269
+ const types = new Set([
1270
+ 'int', 'long', 'short', 'byte', 'float', 'double', 'boolean', 'char', 'void',
1271
+ 'String', 'Object', 'Integer', 'Long', 'Boolean', 'Double', 'Float', 'Byte', 'Short', 'Character',
1272
+ 'List', 'Map', 'Set', 'ArrayList', 'HashMap', 'HashSet', 'LinkedList', 'TreeMap', 'TreeSet',
1273
+ 'Optional', 'Stream', 'Arrays', 'Collections', 'System', 'Thread', 'Exception', 'RuntimeException',
1274
+ 'Throwable', 'Error', 'Class', 'StringBuilder', 'StringBuffer', 'CompletableFuture', 'Future',
1275
+ 'null', 'true', 'false'
1276
+ ]);
1277
+
1278
+ const tokenRegex = /(\/\*[\s\S]*?\*\/|\/\/.*$|"""[\s\S]*?"""|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|@[a-zA-Z0-9_.]+|\b(?:0x[0-9a-fA-F]+|\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?[fFdDlL]?)\b|\b[a-zA-Z_$][a-zA-Z0-9_$]*\b|->|::|==|!=|<=|>=|&&|\|\||\+\+|--|\+=|-=|\*=|\/=|%=|&=|\|=|\^=|>>>=|>>=|<<=|[+\-*/%=!&|^~<>?:;,{}()\[\].])/gm;
1279
+
1280
+ let lastIndex = 0;
1281
+ let out = '';
1282
+ let match;
1283
+
1284
+ while ((match = tokenRegex.exec(code)) !== null) {
1285
+ const raw = match[0];
1286
+ const index = match.index;
1287
+ if (index > lastIndex) {
1288
+ out += escapeHtml(code.slice(lastIndex, index));
1289
+ }
1290
+ lastIndex = tokenRegex.lastIndex;
1291
+
1292
+ if (raw.startsWith('/*') || raw.startsWith('//')) {
1293
+ out += '<span class="tok-comment">' + escapeHtml(raw) + '</span>';
1294
+ } else if (raw.startsWith('"""') || raw.startsWith('"') || raw.startsWith("'")) {
1295
+ out += '<span class="tok-string">' + escapeHtml(raw) + '</span>';
1296
+ } else if (raw.startsWith('@')) {
1297
+ out += '<span class="tok-attr">' + escapeHtml(raw) + '</span>';
1298
+ } else if (/^\d|^0x/.test(raw)) {
1299
+ out += '<span class="tok-number">' + escapeHtml(raw) + '</span>';
1300
+ } else if (/^[a-zA-Z_$]/.test(raw)) {
1301
+ if (keywords.has(raw)) {
1302
+ out += '<span class="tok-keyword">' + escapeHtml(raw) + '</span>';
1303
+ } else if (types.has(raw)) {
1304
+ out += '<span class="tok-type">' + escapeHtml(raw) + '</span>';
1305
+ } else {
1306
+ const remainder = code.slice(lastIndex);
1307
+ if (/^\s*\(/.test(remainder)) {
1308
+ out += '<span class="tok-func">' + escapeHtml(raw) + '</span>';
1309
+ } else {
1310
+ out += escapeHtml(raw);
1311
+ }
1312
+ }
1313
+ } else if (/^(->|::|==|!=|<=|>=|&&|\|\||\+\+|--|\+=|-=|\*=|\/=|%=|&=|\|=|\^=|>>>=|>>=|<<=|[+\-*/%=!&|^~<>])$/.test(raw)) {
1314
+ out += '<span class="tok-operator">' + escapeHtml(raw) + '</span>';
1315
+ } else if (/^[{}()\[\]]$/.test(raw)) {
1316
+ out += '<span class="tok-bracket">' + escapeHtml(raw) + '</span>';
1317
+ } else if (/^[,;:?]$/.test(raw)) {
1318
+ out += '<span class="tok-punctuation">' + escapeHtml(raw) + '</span>';
1319
+ } else {
1320
+ out += escapeHtml(raw);
1321
+ }
1322
+ }
1323
+
1324
+ if (lastIndex < code.length) {
1325
+ out += escapeHtml(code.slice(lastIndex));
1326
+ }
1327
+ return out;
1328
+ }
1329
+
1330
+ function highlightCpp(code) {
1331
+ if (!code) return '';
1332
+ const keywords = new Set([
1333
+ 'class', 'struct', 'union', 'enum', 'template', 'typename', 'namespace', 'using',
1334
+ 'typedef', 'public', 'private', 'protected', 'virtual', 'override', 'final', 'const',
1335
+ 'constexpr', 'consteval', 'constinit', 'static', 'inline', 'auto', 'void', 'return',
1336
+ 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue', 'default',
1337
+ 'new', 'delete', 'this', 'try', 'catch', 'throw', 'noexcept', 'explicit', 'friend',
1338
+ 'mutable', 'volatile', 'concept', 'requires', 'co_await', 'co_yield', 'co_return',
1339
+ 'decltype', 'sizeof', 'alignas', 'alignof', 'static_cast', 'dynamic_cast',
1340
+ 'reinterpret_cast', 'const_cast', 'operator', 'export', 'import', 'module'
1341
+ ]);
1342
+ const types = new Set([
1343
+ 'int', 'long', 'short', 'char', 'float', 'double', 'bool', 'void', 'wchar_t', 'char8_t',
1344
+ 'char16_t', 'char32_t', 'size_t', 'int8_t', 'int16_t', 'int32_t', 'int64_t',
1345
+ 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', 'nullptr', 'true', 'false',
1346
+ 'string', 'wstring', 'vector', 'map', 'unordered_map', 'set', 'unordered_set',
1347
+ 'pair', 'tuple', 'unique_ptr', 'shared_ptr', 'weak_ptr', 'make_unique', 'make_shared',
1348
+ 'std', 'cout', 'cin', 'cerr', 'endl', 'array', 'deque', 'list', 'queue', 'stack'
1349
+ ]);
1350
+
1351
+ const tokenRegex = /(\/\*[\s\S]*?\*\/|\/\/.*$|#(?:include|define|ifdef|ifndef|endif|if|else|elif|pragma|undef|error)\b.*$|R"\(.*?\)"|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\b(?:0x[0-9a-fA-F]+|\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?[uUlLfF]?)\b|\b[a-zA-Z_][a-zA-Z0-9_]*\b|::|->\*|->|==|!=|<=|>=|<=>|&&|\|\||\+\+|--|\+=|-=|\*=|\/=|%=|&=|\|=|\^=|>>=|<<=|<<|>>|[+\-*/%=!&|^~<>?:;,{}()\[\].])/gm;
1352
+
1353
+ let lastIndex = 0;
1354
+ let out = '';
1355
+ let match;
1356
+
1357
+ while ((match = tokenRegex.exec(code)) !== null) {
1358
+ const raw = match[0];
1359
+ const index = match.index;
1360
+ if (index > lastIndex) {
1361
+ out += escapeHtml(code.slice(lastIndex, index));
1362
+ }
1363
+ lastIndex = tokenRegex.lastIndex;
1364
+
1365
+ if (raw.startsWith('/*') || raw.startsWith('//')) {
1366
+ out += '<span class="tok-comment">' + escapeHtml(raw) + '</span>';
1367
+ } else if (raw.startsWith('#')) {
1368
+ const spaceIdx = raw.indexOf(' ');
1369
+ if (spaceIdx !== -1) {
1370
+ const dir = raw.slice(0, spaceIdx);
1371
+ const rest = raw.slice(spaceIdx);
1372
+ out += '<span class="tok-keyword">' + escapeHtml(dir) + '</span><span class="tok-string">' + escapeHtml(rest) + '</span>';
1373
+ } else {
1374
+ out += '<span class="tok-keyword">' + escapeHtml(raw) + '</span>';
1375
+ }
1376
+ } else if (raw.startsWith('R"') || raw.startsWith('"') || raw.startsWith("'")) {
1377
+ out += '<span class="tok-string">' + escapeHtml(raw) + '</span>';
1378
+ } else if (/^\d|^0x/.test(raw)) {
1379
+ out += '<span class="tok-number">' + escapeHtml(raw) + '</span>';
1380
+ } else if (/^[a-zA-Z_]/.test(raw)) {
1381
+ if (keywords.has(raw)) {
1382
+ out += '<span class="tok-keyword">' + escapeHtml(raw) + '</span>';
1383
+ } else if (types.has(raw)) {
1384
+ out += '<span class="tok-type">' + escapeHtml(raw) + '</span>';
1385
+ } else {
1386
+ const remainder = code.slice(lastIndex);
1387
+ if (/^\s*\(/.test(remainder)) {
1388
+ out += '<span class="tok-func">' + escapeHtml(raw) + '</span>';
1389
+ } else {
1390
+ out += escapeHtml(raw);
1391
+ }
1392
+ }
1393
+ } else if (/^(::|->\*|->|==|!=|<=|>=|<=>|&&|\|\||\+\+|--|\+=|-=|\*=|\/=|%=|&=|\|=|\^=|>>=|<<=|<<|>>|[+\-*/%=!&|^~<>])$/.test(raw)) {
1394
+ out += '<span class="tok-operator">' + escapeHtml(raw) + '</span>';
1395
+ } else if (/^[{}()\[\]]$/.test(raw)) {
1396
+ out += '<span class="tok-bracket">' + escapeHtml(raw) + '</span>';
1397
+ } else if (/^[,;:?]$/.test(raw)) {
1398
+ out += '<span class="tok-punctuation">' + escapeHtml(raw) + '</span>';
1399
+ } else {
1400
+ out += escapeHtml(raw);
1401
+ }
1402
+ }
1403
+
1404
+ if (lastIndex < code.length) {
1405
+ out += escapeHtml(code.slice(lastIndex));
1406
+ }
1407
+ return out;
1408
+ }
1409
+
1410
+ function highlightPython(code) {
1411
+ if (!code) return '';
1412
+ const pyKeywords = new Set([
1413
+ 'def', 'class', 'import', 'from', 'return', 'if', 'elif', 'else', 'for', 'while',
1414
+ 'try', 'except', 'finally', 'raise', 'with', 'as', 'lambda', 'pass', 'yield',
1415
+ 'async', 'await', 'global', 'nonlocal', 'assert', 'del', 'in', 'is', 'not', 'and', 'or',
1416
+ 'match', 'case'
1417
+ ]);
1418
+ const pyBuiltins = new Set([
1419
+ 'True', 'False', 'None', 'self', 'cls', 'print', 'len', 'range', 'dict', 'list',
1420
+ 'set', 'tuple', 'str', 'int', 'float', 'bool', 'type', 'isinstance', 'enumerate', 'zip',
1421
+ 'super', 'sum', 'min', 'max', 'any', 'all', 'open', 'map', 'filter'
1422
+ ]);
1423
+
1424
+ const tokenRegex = /("""[\s\S]*?"""|'''[\s\S]*?'''|#.*$|@[a-zA-Z0-9_.]+|f?"(?:\\[\s\S]|[^"\\])*"|f?'(?:\\[\s\S]|[^'\\])*'|\b\d+(?:\.\d+)?\b|\b[a-zA-Z_][a-zA-Z0-9_]*\b|==|!=|<=|>=|\+=|-=|\*=|\/=|->|[+\-*/%=&|^~<>?:;,{}()\[\].])/gm;
1425
+
1426
+ let lastIndex = 0;
1427
+ let out = '';
1428
+ let match;
1429
+
1430
+ while ((match = tokenRegex.exec(code)) !== null) {
1431
+ const raw = match[0];
1432
+ const index = match.index;
1433
+ if (index > lastIndex) {
1434
+ out += escapeHtml(code.slice(lastIndex, index));
1435
+ }
1436
+ lastIndex = tokenRegex.lastIndex;
1437
+
1438
+ if (raw.startsWith('#')) {
1439
+ out += '<span class="tok-comment">' + escapeHtml(raw) + '</span>';
1440
+ } else if (raw.startsWith('"""') || raw.startsWith("'''") || raw.startsWith('"') || raw.startsWith("'") || raw.startsWith('f"') || raw.startsWith("f'")) {
1441
+ out += '<span class="tok-string">' + escapeHtml(raw) + '</span>';
1442
+ } else if (raw.startsWith('@')) {
1443
+ out += '<span class="tok-attr">' + escapeHtml(raw) + '</span>';
1444
+ } else if (/^\d/.test(raw)) {
1445
+ out += '<span class="tok-number">' + escapeHtml(raw) + '</span>';
1446
+ } else if (/^[a-zA-Z_]/.test(raw)) {
1447
+ if (pyKeywords.has(raw)) {
1448
+ out += '<span class="tok-keyword">' + escapeHtml(raw) + '</span>';
1449
+ } else if (pyBuiltins.has(raw)) {
1450
+ out += '<span class="tok-type">' + escapeHtml(raw) + '</span>';
1451
+ } else {
1452
+ const remainder = code.slice(lastIndex);
1453
+ if (/^\s*\(/.test(remainder)) {
1454
+ out += '<span class="tok-func">' + escapeHtml(raw) + '</span>';
1455
+ } else {
1456
+ out += escapeHtml(raw);
1457
+ }
1458
+ }
1459
+ } else if (/^(==|!=|<=|>=|\+=|-=|\*=|\/=|->|[+\-*/%=&|^~<>])$/.test(raw)) {
1460
+ out += '<span class="tok-operator">' + escapeHtml(raw) + '</span>';
1461
+ } else if (/^[{}()\[\]]$/.test(raw)) {
1462
+ out += '<span class="tok-bracket">' + escapeHtml(raw) + '</span>';
1463
+ } else if (/^[,;:?]$/.test(raw)) {
1464
+ out += '<span class="tok-punctuation">' + escapeHtml(raw) + '</span>';
1465
+ } else {
1466
+ out += escapeHtml(raw);
1467
+ }
1468
+ }
1469
+
1470
+ if (lastIndex < code.length) {
1471
+ out += escapeHtml(code.slice(lastIndex));
1472
+ }
1473
+ return out;
1474
+ }
1475
+
1476
+ function highlightHTML(code) {
1477
+ if (!code) return '';
1478
+ const tokenRegex = /(<!--[\s\S]*?-->|<\/?[a-zA-Z0-9\-:]+|(?:[a-zA-Z0-9\-:]+)=("[^"]*"|'[^']*')|\/?>)/g;
1479
+ let lastIndex = 0;
1480
+ let out = '';
1481
+ let match;
1482
+
1483
+ while ((match = tokenRegex.exec(code)) !== null) {
1484
+ const raw = match[0];
1485
+ const index = match.index;
1486
+ if (index > lastIndex) {
1487
+ out += escapeHtml(code.slice(lastIndex, index));
1488
+ }
1489
+ lastIndex = tokenRegex.lastIndex;
1490
+
1491
+ if (raw.startsWith('<!--')) {
1492
+ out += '<span class="tok-comment">' + escapeHtml(raw) + '</span>';
1493
+ } else if (raw.startsWith('<')) {
1494
+ out += '<span class="tok-bracket">&lt;</span><span class="tok-tag">' + escapeHtml(raw.slice(raw.startsWith('</') ? 2 : 1)) + '</span>';
1495
+ } else if (raw.endsWith('>')) {
1496
+ out += '<span class="tok-bracket">' + escapeHtml(raw) + '</span>';
1497
+ } else if (raw.includes('=')) {
1498
+ const eqIdx = raw.indexOf('=');
1499
+ const attrName = raw.slice(0, eqIdx);
1500
+ const attrVal = raw.slice(eqIdx + 1);
1501
+ out += '<span class="tok-attr">' + escapeHtml(attrName) + '</span>=<span class="tok-string">' + escapeHtml(attrVal) + '</span>';
1502
+ } else {
1503
+ out += escapeHtml(raw);
1504
+ }
1505
+ }
1506
+
1507
+ if (lastIndex < code.length) {
1508
+ out += escapeHtml(code.slice(lastIndex));
1509
+ }
1510
+ return out;
1511
+ }
1512
+
1513
+ function highlightGenericCode(code, category) {
1514
+ if (!code) return '';
1515
+ if (category === 'sql') {
1516
+ 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;
1517
+ return escapeHtml(code)
1518
+ .replace(sqlKeywords, '<span class="tok-keyword">$1</span>')
1519
+ .replace(/(--.*$)/gm, '<span class="tok-comment">$1</span>')
1520
+ .replace(/('[^']*')/g, '<span class="tok-string">$1</span>');
1521
+ }
1522
+ if (category === 'go') {
1523
+ const goKeywords = /\b(package|import|func|return|var|const|type|struct|interface|map|chan|go|defer|if|else|for|range|switch|case|default|select|break|continue|fallthrough)\b/g;
1524
+ return escapeHtml(code)
1525
+ .replace(/(\b(?:string|int|int64|int32|uint|uint64|byte|rune|bool|float32|float64|error|nil|true|false)\b)/g, '<span class="tok-type">$1</span>')
1526
+ .replace(goKeywords, '<span class="tok-keyword">$1</span>')
1527
+ .replace(/(\/\/.*$)/gm, '<span class="tok-comment">$1</span>')
1528
+ .replace(/("(?:\\[\s\S]|[^"\\])*")/g, '<span class="tok-string">$1</span>');
1529
+ }
1530
+ if (category === 'rust') {
1531
+ const rsKeywords = /\b(fn|let|mut|pub|struct|enum|impl|trait|type|use|mod|crate|return|if|else|match|loop|while|for|in|break|continue|async|await|where|unsafe|as|ref|move)\b/g;
1532
+ return escapeHtml(code)
1533
+ .replace(/(\b(?:i8|i16|i32|i64|i128|isize|u8|u16|u32|u64|u128|usize|f32|f64|bool|char|str|String|Vec|Option|Result|Some|None|Ok|Err|true|false)\b)/g, '<span class="tok-type">$1</span>')
1534
+ .replace(rsKeywords, '<span class="tok-keyword">$1</span>')
1535
+ .replace(/(\/\/.*$)/gm, '<span class="tok-comment">$1</span>')
1536
+ .replace(/("(?:\\[\s\S]|[^"\\])*")/g, '<span class="tok-string">$1</span>');
1537
+ }
1538
+ const lines = code.split('\n');
1539
+ return lines.map(line => {
1540
+ const commentIdx = line.indexOf('#');
1541
+ let mainPart = line;
1542
+ let commentPart = '';
1543
+ if (commentIdx !== -1) {
1544
+ mainPart = line.slice(0, commentIdx);
1545
+ commentPart = '<span class="tok-comment">' + escapeHtml(line.slice(commentIdx)) + '</span>';
1546
+ }
1547
+ let h = escapeHtml(mainPart);
1548
+ h = h.replace(/^([a-zA-Z0-9_\-.]+)(\s*=\s*)(.*)$/, (m, k, eq, v) => {
1549
+ return '<span class="tok-key">' + k + '</span>' + eq + '<span class="tok-string">' + v + '</span>';
1550
+ });
1551
+ h = h.replace(/(\$[a-zA-Z0-9_{}]+)/g, '<span class="tok-type">$1</span>');
1552
+ 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>');
1553
+ return h + commentPart;
1554
+ }).join('\n');
1555
+ }
1556
+
1557
+ function highlightCode(rawCode, category) {
1558
+ if (!rawCode) return '';
1559
+ if (category === 'json') return highlightJSON(rawCode);
1560
+ if (category === 'yaml') return highlightYAML(rawCode);
1561
+ if (category === 'javascript') return highlightJS(rawCode);
1562
+ if (category === 'java') return highlightJava(rawCode);
1563
+ if (category === 'cpp') return highlightCpp(rawCode);
1564
+ if (category === 'python') return highlightPython(rawCode);
1565
+ if (category === 'markup') return highlightHTML(rawCode);
1566
+ if (category === 'css') return highlightCSS(rawCode);
1567
+ if (category === 'shell' || category === 'config' || category === 'sql' || category === 'go' || category === 'rust' || category === 'csharp') return highlightGenericCode(rawCode, category);
1568
+ return escapeHtml(rawCode);
1569
+ }
1570
+
1571
+ // =========================================================================
1572
+ // Lightweight YAML to JSON Recursive Parser
1573
+ // =========================================================================
1574
+ function parseYamlScalar(val) {
1575
+ if (!val) return '';
1576
+ const v = val.trim();
1577
+ if (/^(true|yes|on)$/i.test(v)) return true;
1578
+ if (/^(false|no|off)$/i.test(v)) return false;
1579
+ if (/^(null|~)$/i.test(v)) return null;
1580
+ if (/^-?\d+$/.test(v)) return parseInt(v, 10);
1581
+ if (/^-?\d+\.\d+$/.test(v)) return parseFloat(v);
1582
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
1583
+ return v.slice(1, -1);
1584
+ }
1585
+ return v;
1586
+ }
1587
+
1588
+ function parseYamlToJson(yamlStr) {
1589
+ if (!yamlStr) return { ok: true, data: {} };
1590
+ try {
1591
+ const rawLines = yamlStr.split('\n');
1592
+ const lines = [];
1593
+ for (const raw of rawLines) {
1594
+ const cIdx = raw.indexOf('#');
1595
+ const l = cIdx !== -1 ? raw.slice(0, cIdx) : raw;
1596
+ if (l.trim()) {
1597
+ lines.push({ indent: l.search(/\S/), text: l.trim() });
1598
+ }
1599
+ }
1600
+ if (lines.length === 0) return { ok: true, data: {} };
1601
+
1602
+ let idx = 0;
1603
+ function parseBlock(currentIndent) {
1604
+ let isArr = false;
1605
+ if (idx < lines.length && lines[idx].text.startsWith('- ')) {
1606
+ isArr = true;
1607
+ }
1608
+ const container = isArr ? [] : {};
1609
+
1610
+ while (idx < lines.length) {
1611
+ const line = lines[idx];
1612
+ if (line.indent < currentIndent) break;
1613
+ if (line.indent > currentIndent) break;
1614
+
1615
+ idx++;
1616
+
1617
+ if (isArr) {
1618
+ if (line.text.startsWith('- ')) {
1619
+ const itemText = line.text.slice(2).trim();
1620
+ if (!itemText) {
1621
+ if (idx < lines.length && lines[idx].indent > line.indent) {
1622
+ container.push(parseBlock(lines[idx].indent));
1623
+ } else {
1624
+ container.push(null);
1625
+ }
1626
+ } else if (itemText.includes(':')) {
1627
+ const colonPos = itemText.indexOf(':');
1628
+ const k = itemText.slice(0, colonPos).trim().replace(/^['"]|['"]$/g, '');
1629
+ const v = itemText.slice(colonPos + 1).trim();
1630
+ const obj = {};
1631
+ if (!v) {
1632
+ if (idx < lines.length && lines[idx].indent > line.indent) {
1633
+ obj[k] = parseBlock(lines[idx].indent);
1634
+ } else {
1635
+ obj[k] = {};
1636
+ }
1637
+ } else {
1638
+ obj[k] = parseYamlScalar(v);
1639
+ }
1640
+ while (idx < lines.length && lines[idx].indent === line.indent + 2 && !lines[idx].text.startsWith('- ')) {
1641
+ const nextLine = lines[idx++];
1642
+ const cPos = nextLine.text.indexOf(':');
1643
+ if (cPos !== -1) {
1644
+ const nk = nextLine.text.slice(0, cPos).trim().replace(/^['"]|['"]$/g, '');
1645
+ const nv = nextLine.text.slice(cPos + 1).trim();
1646
+ if (!nv && idx < lines.length && lines[idx].indent > nextLine.indent) {
1647
+ obj[nk] = parseBlock(lines[idx].indent);
1648
+ } else {
1649
+ obj[nk] = parseYamlScalar(nv);
1650
+ }
1651
+ }
1652
+ }
1653
+ container.push(obj);
1654
+ } else {
1655
+ container.push(parseYamlScalar(itemText));
1656
+ }
1657
+ }
1658
+ } else {
1659
+ const colonPos = line.text.indexOf(':');
1660
+ if (colonPos !== -1) {
1661
+ const k = line.text.slice(0, colonPos).trim().replace(/^['"]|['"]$/g, '');
1662
+ const v = line.text.slice(colonPos + 1).trim();
1663
+ if (!v) {
1664
+ if (idx < lines.length && lines[idx].indent > line.indent) {
1665
+ container[k] = parseBlock(lines[idx].indent);
1666
+ } else {
1667
+ container[k] = {};
1668
+ }
1669
+ } else {
1670
+ container[k] = parseYamlScalar(v);
1671
+ }
1672
+ }
1673
+ }
1674
+ }
1675
+ return container;
1676
+ }
1677
+
1678
+ const root = parseBlock(lines[0].indent);
1679
+ return { ok: true, data: root };
1680
+ } catch (err) {
1681
+ return { ok: false, error: err.message };
1682
+ }
1683
+ }
1684
+
1685
+ // =========================================================================
1686
+ // Code Symbol Outline Extractor
1687
+ // =========================================================================
1688
+ function extractCodeSymbols(content, category) {
1689
+ if (!content) return [];
1690
+ const lines = content.split('\n');
1691
+ const symbols = [];
1692
+
1693
+ if (category === 'javascript') {
1694
+ lines.forEach((line, idx) => {
1695
+ const lineNum = idx + 1;
1696
+ const trimmed = line.trim();
1697
+ if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*') || trimmed.startsWith('return ')) return;
1698
+
1699
+ const classMatch = trimmed.match(/\bclass\s+([a-zA-Z0-9_$]+)/);
1700
+ if (classMatch) {
1701
+ symbols.push({ id: `line-${lineNum}`, name: classMatch[1], kind: 'class', line: lineNum, text: `🔷 class ${classMatch[1]}` });
1702
+ return;
1703
+ }
1704
+
1705
+ const funcMatch = trimmed.match(/\b(?:async\s+)?function\s*([a-zA-Z0-9_$]+)\s*\(/);
1706
+ if (funcMatch) {
1707
+ symbols.push({ id: `line-${lineNum}`, name: funcMatch[1], kind: 'function', line: lineNum, text: `⚡ fn ${funcMatch[1]}()` });
1708
+ return;
1709
+ }
1710
+
1711
+ const arrowMatch = trimmed.match(/(?:const|let|var)\s+([a-zA-Z0-9_$]+)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[a-zA-Z0-9_$]+)\s*=>/);
1712
+ if (arrowMatch) {
1713
+ symbols.push({ id: `line-${lineNum}`, name: arrowMatch[1], kind: 'function', line: lineNum, text: `⚡ fn ${arrowMatch[1]}()` });
1714
+ return;
1715
+ }
1716
+
1717
+ const typeMatch = trimmed.match(/\b(?:interface|type|enum)\s+([a-zA-Z0-9_$]+)/);
1718
+ if (typeMatch) {
1719
+ symbols.push({ id: `line-${lineNum}`, name: typeMatch[1], kind: 'type', line: lineNum, text: `🏷️ ${typeMatch[1]}` });
1720
+ return;
1721
+ }
1722
+
1723
+ if (trimmed.startsWith('export default')) {
1724
+ symbols.push({ id: `line-${lineNum}`, name: 'default export', kind: 'export', line: lineNum, text: `📦 export default` });
1725
+ }
1726
+ });
1727
+ } else if (category === 'java') {
1728
+ lines.forEach((line, idx) => {
1729
+ const lineNum = idx + 1;
1730
+ const trimmed = line.trim();
1731
+ if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*') || trimmed.startsWith('throw ') || trimmed.startsWith('return ')) return;
1732
+
1733
+ const pkgMatch = trimmed.match(/^package\s+([a-zA-Z0-9_.]+);/);
1734
+ if (pkgMatch) {
1735
+ symbols.push({ id: `line-${lineNum}`, name: pkgMatch[1], kind: 'package', line: lineNum, text: `📦 package ${pkgMatch[1]}` });
1736
+ return;
1737
+ }
1738
+
1739
+ const classMatch = trimmed.match(/\b(?:public|protected|private|static|final|abstract\s+)*(class|interface|enum|record)\s+([a-zA-Z0-9_]+)/);
1740
+ if (classMatch) {
1741
+ const typeIcon = classMatch[1] === 'interface' ? '🏷️' : classMatch[1] === 'enum' ? '🔢' : '🔷';
1742
+ symbols.push({ id: `line-${lineNum}`, name: classMatch[2], kind: classMatch[1], line: lineNum, text: `${typeIcon} ${classMatch[1]} ${classMatch[2]}` });
1743
+ return;
1744
+ }
1745
+
1746
+ const methodMatch = trimmed.match(/^(?:@\w+\s+)*(?:public|protected|private|static|final|synchronized|abstract|default|native|\s)*(?:<[^>]+>\s+)?([a-zA-Z0-9_<>[],.\s]+(?:\s*\[\s*\])?\s+)?([a-zA-Z0-9_]+)\s*\([^)]*\)\s*(?:throws\s+[a-zA-Z0-9_,\s]+)?\s*\{?$/);
1747
+ if (methodMatch) {
1748
+ const name = methodMatch[2];
1749
+ if (!['if', 'for', 'while', 'switch', 'catch', 'return', 'throw', 'new', 'this', 'super'].includes(name)) {
1750
+ symbols.push({ id: `line-${lineNum}`, name, kind: 'method', line: lineNum, text: `⚡ ${name}()` });
1751
+ }
1752
+ }
1753
+ });
1754
+ } else if (category === 'cpp') {
1755
+ lines.forEach((line, idx) => {
1756
+ const lineNum = idx + 1;
1757
+ const trimmed = line.trim();
1758
+ if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*') || trimmed.startsWith('#') || trimmed.startsWith('throw ') || trimmed.startsWith('return ')) return;
1759
+
1760
+ const nsMatch = trimmed.match(/^namespace\s+([a-zA-Z0-9_:]+)/);
1761
+ if (nsMatch) {
1762
+ symbols.push({ id: `line-${lineNum}`, name: nsMatch[1], kind: 'namespace', line: lineNum, text: `📦 namespace ${nsMatch[1]}` });
1763
+ return;
1764
+ }
1765
+
1766
+ const classMatch = trimmed.match(/\b(class|struct|union|enum\s+class|enum)\s+([a-zA-Z0-9_]+)/);
1767
+ if (classMatch && !trimmed.endsWith(';')) {
1768
+ symbols.push({ id: `line-${lineNum}`, name: classMatch[2], kind: classMatch[1], line: lineNum, text: `🔷 ${classMatch[1]} ${classMatch[2]}` });
1769
+ return;
1770
+ }
1771
+
1772
+ const funcMatch = trimmed.match(/^(?:(?:inline|virtual|static|constexpr|const|explicit|friend|auto|[a-zA-Z0-9_:<>&*~]+)\s+)+([a-zA-Z0-9_~]+)\s*\([^)]*\)\s*(?:const|noexcept|override|final|\s)*\s*\{?$/);
1773
+ if (funcMatch && !['if', 'for', 'while', 'switch', 'catch', 'return', 'else', 'sizeof', 'decltype'].includes(funcMatch[1])) {
1774
+ symbols.push({ id: `line-${lineNum}`, name: funcMatch[1], kind: 'function', line: lineNum, text: `⚡ fn ${funcMatch[1]}()` });
1775
+ }
1776
+ });
1777
+ } else if (category === 'python') {
1778
+ lines.forEach((line, idx) => {
1779
+ const lineNum = idx + 1;
1780
+ const trimmed = line.trim();
1781
+ if (trimmed.startsWith('#')) return;
1782
+
1783
+ const classMatch = trimmed.match(/^classs+([a-zA-Z0-9_]+)/);
1784
+ if (classMatch) {
1785
+ symbols.push({ id: `line-${lineNum}`, name: classMatch[1], kind: 'class', line: lineNum, text: `🔷 class ${classMatch[1]}` });
1786
+ return;
1787
+ }
1788
+ const funcMatch = line.match(/^(\s*)(?:async\s+)?defs+([a-zA-Z0-9_]+)/);
1789
+ if (funcMatch) {
1790
+ const prefix = funcMatch[1].length > 0 ? ' ' : '';
1791
+ symbols.push({ id: `line-${lineNum}`, name: funcMatch[2], kind: 'function', line: lineNum, text: `${prefix}⚡ def ${funcMatch[2]}()` });
1792
+ }
1793
+ });
1794
+ } else if (category === 'yaml') {
1795
+ lines.forEach((line, idx) => {
1796
+ const lineNum = idx + 1;
1797
+ const topKeyMatch = line.match(/^([a-zA-Z0-9_\-./@$]+)\s*:/);
1798
+ if (topKeyMatch && !line.startsWith('#')) {
1799
+ symbols.push({ id: `line-${lineNum}`, name: topKeyMatch[1], kind: 'key', line: lineNum, text: `🔑 ${topKeyMatch[1]}` });
1800
+ }
1801
+ });
1802
+ } else if (category === 'json') {
1803
+ lines.forEach((line, idx) => {
1804
+ const lineNum = idx + 1;
1805
+ const keyMatch = line.match(/^\s{2}"([a-zA-Z0-9_\-./@$]+)"\s*:/);
1806
+ if (keyMatch) {
1807
+ symbols.push({ id: `line-${lineNum}`, name: keyMatch[1], kind: 'key', line: lineNum, text: `🔑 "${keyMatch[1]}"` });
1808
+ }
1809
+ });
1810
+ }
1811
+ return symbols;
1812
+ }
1813
+
1814
+ // =========================================================================
1815
+ // React Component: JSONTreeNode & JSONTreeInspector
1816
+ // =========================================================================
1817
+ function JSONTreeNode({ name, value, path, depth, isArrayItem, index, expandedKeys, onToggle, filterQuery, onCopy }) {
1818
+ const isObject = value !== null && typeof value === 'object';
1819
+ const isArray = Array.isArray(value);
1820
+ const isExpanded = expandedKeys.has(path);
1821
+
1822
+ if (isObject) {
1823
+ const keys = Object.keys(value);
1824
+ const count = keys.length;
1825
+ const typeLabel = isArray ? `Array(${count})` : `Object{${count}}`;
1826
+
1827
+ if (filterQuery) {
1828
+ const matchSelf = (name && String(name).toLowerCase().includes(filterQuery)) || path.toLowerCase().includes(filterQuery);
1829
+ let matchChild = false;
1830
+ try { matchChild = JSON.stringify(value).toLowerCase().includes(filterQuery); } catch (e) {}
1831
+ if (!matchSelf && !matchChild) return null;
1832
+ }
1833
+
1834
+ const previewSummary = isArray
1835
+ ? `[ ${count === 0 ? '' : '...'} ]`
1836
+ : `{ ${keys.slice(0, 3).map(k => `${k}: ...`).join(', ')}${count > 3 ? ', ...' : ''} }`;
1837
+
1838
+ return h('div', { className: 'dsh-json-node-group' },
1839
+ h('div', {
1840
+ className: 'dsh-json-row dsh-json-row-expandable',
1841
+ style: { paddingLeft: `${depth * 16 + 6}px` },
1842
+ onClick: () => onToggle(path),
1843
+ },
1844
+ h('span', { className: `dsh-json-chevron ${isExpanded ? 'expanded' : ''}` }, isExpanded ? '▼' : '▶'),
1845
+ name ? h('span', { className: 'dsh-json-key' }, isArrayItem ? `[${index}]` : `"${name}":`) : null,
1846
+ h('span', { className: 'dsh-json-type-badge' }, typeLabel),
1847
+ !isExpanded ? h('span', { className: 'dsh-json-preview' }, previewSummary) : null,
1848
+ h('span', {
1849
+ className: 'dsh-json-copy-btn',
1850
+ title: `复制字段路径: ${path.replace(/^root\.?/, '')}`,
1851
+ onClick: (e) => {
1852
+ e.stopPropagation();
1853
+ onCopy(path.replace(/^root\.?/, ''), '字段路径');
1854
+ }
1855
+ }, '📋')
1856
+ ),
1857
+ isExpanded ? h('div', { className: 'dsh-json-children' },
1858
+ keys.map((k, i) => h(JSONTreeNode, {
1859
+ key: k,
1860
+ name: isArray ? null : k,
1861
+ value: value[k],
1862
+ path: `${path}.${k}`,
1863
+ depth: depth + 1,
1864
+ isArrayItem: isArray,
1865
+ index: i,
1866
+ expandedKeys,
1867
+ onToggle,
1868
+ filterQuery,
1869
+ onCopy,
1870
+ }))
1871
+ ) : null
1872
+ );
1873
+ }
1874
+
1875
+ // Primitive Value
1876
+ const valType = value === null ? 'null' : typeof value;
1877
+ let valDisplay = String(value);
1878
+ let valClass = `tok-${valType}`;
1879
+
1880
+ if (valType === 'string') {
1881
+ valDisplay = `"${value}"`;
1882
+ valClass = 'tok-string';
1883
+ } else if (valType === 'number') {
1884
+ valClass = 'tok-number';
1885
+ } else if (valType === 'boolean') {
1886
+ valClass = 'tok-boolean';
1887
+ } else if (valType === 'null') {
1888
+ valDisplay = 'null';
1889
+ valClass = 'tok-null';
1890
+ }
1891
+
1892
+ if (filterQuery) {
1893
+ const matchName = name && String(name).toLowerCase().includes(filterQuery);
1894
+ const matchVal = valDisplay.toLowerCase().includes(filterQuery);
1895
+ if (!matchName && !matchVal) return null;
1896
+ }
1897
+
1898
+ const isUrl = typeof value === 'string' && (value.startsWith('http://') || value.startsWith('https://'));
1899
+
1900
+ return h('div', {
1901
+ className: 'dsh-json-row dsh-json-row-leaf',
1902
+ style: { paddingLeft: `${depth * 16 + 22}px` },
1903
+ },
1904
+ name ? h('span', { className: 'dsh-json-key' }, isArrayItem ? `[${index}]:` : `"${name}":`) : null,
1905
+ isUrl ? h('a', {
1906
+ href: value,
1907
+ target: '_blank',
1908
+ rel: 'noopener noreferrer',
1909
+ className: 'dsh-json-link tok-string',
1910
+ onClick: (e) => e.stopPropagation(),
1911
+ }, valDisplay) : h('span', { className: valClass }, valDisplay),
1912
+ h('span', {
1913
+ className: 'dsh-json-copy-btn',
1914
+ title: '复制值',
1915
+ onClick: () => onCopy(String(value), '字段值')
1916
+ }, '📋')
1917
+ );
1918
+ }
1919
+
1920
+ function JSONTreeInspector({ data, rawContent, onFormat, onMinify }) {
1921
+ const [expandedKeys, setExpandedKeys] = useState(new Set(['root', 'root.scripts', 'root.dependencies', 'root.devDependencies']));
1922
+ const [filterQuery, setFilterQuery] = useState('');
1923
+
1924
+ const toggleKey = (path) => {
1925
+ setExpandedKeys(prev => {
1926
+ const next = new Set(prev);
1927
+ if (next.has(path)) next.delete(path);
1928
+ else next.add(path);
1929
+ return next;
1930
+ });
1931
+ };
1932
+
1933
+ const expandAll = () => {
1934
+ const all = new Set();
1935
+ const traverse = (val, p) => {
1936
+ all.add(p);
1937
+ if (val && typeof val === 'object') {
1938
+ for (const k of Object.keys(val)) {
1939
+ traverse(val[k], p ? `${p}.${k}` : k);
1940
+ }
1941
+ }
1942
+ };
1943
+ traverse(data, 'root');
1944
+ setExpandedKeys(all);
1945
+ };
1946
+
1947
+ const collapseAll = () => {
1948
+ setExpandedKeys(new Set(['root']));
1949
+ };
1950
+
1951
+ const handleCopy = (text, label) => {
1952
+ navigator.clipboard.writeText(text).then(() => {
1953
+ showToast(`已复制 ${label || '内容'}`, 'success');
1954
+ });
1955
+ };
1956
+
1957
+ return h('div', { className: 'dsh-json-inspector' },
1958
+ h('div', { className: 'dsh-json-header' },
1959
+ h('div', { className: 'dsh-json-search-box' },
1960
+ h('span', { className: 'dsh-json-search-icon' }, '🔍'),
1961
+ h('input', {
1962
+ type: 'text',
1963
+ placeholder: '过滤属性名或值...',
1964
+ value: filterQuery,
1965
+ onChange: (e) => setFilterQuery(e.target.value),
1966
+ className: 'dsh-json-search-input',
1967
+ }),
1968
+ filterQuery ? h('button', {
1969
+ className: 'dsh-search-clear-btn',
1970
+ onClick: () => setFilterQuery(''),
1971
+ }, '✕') : null
1972
+ ),
1973
+ h('div', { className: 'dsh-json-actions' },
1974
+ h('button', { type: 'button', className: 'dsh-json-btn', onClick: expandAll, title: '展开所有层级' }, '📂 展开全部'),
1975
+ h('button', { type: 'button', className: 'dsh-json-btn', onClick: collapseAll, title: '折叠到根节点' }, '📁 折叠全部'),
1976
+ onFormat ? h('button', { type: 'button', className: 'dsh-json-btn', onClick: onFormat, title: '格式化并复制' }, '⚡ 格式化') : null,
1977
+ onMinify ? h('button', { type: 'button', className: 'dsh-json-btn', onClick: onMinify, title: '压缩并复制' }, '🗜️ 压缩') : null
1978
+ )
1979
+ ),
1980
+ h('div', { className: 'dsh-json-tree-body' },
1981
+ h(JSONTreeNode, {
1982
+ name: Array.isArray(data) ? 'Array' : 'Object',
1983
+ value: data,
1984
+ path: 'root',
1985
+ depth: 0,
1986
+ expandedKeys,
1987
+ onToggle: toggleKey,
1988
+ filterQuery: filterQuery.trim().toLowerCase(),
1989
+ onCopy: handleCopy,
1990
+ })
1991
+ )
1992
+ );
1993
+ }
1994
+
1995
+ // =========================================================================
1996
+ // React Component: CodeEditorView
1997
+ // =========================================================================
1998
+ function CodeEditorView({ content, category, isHighlighted = true, isWordWrap = true, searchQuery = '', onLineClick }) {
1999
+ const lines = useMemo(() => (content ? content.split('\n') : []), [content]);
2000
+
2001
+ const highlightedHtml = useMemo(() => {
2002
+ if (!content) return '';
2003
+ if (!isHighlighted) {
2004
+ let escaped = escapeHtml(content);
2005
+ if (searchQuery.trim()) {
2006
+ const q = escapeHtml(searchQuery.trim());
2007
+ escaped = escaped.replace(new RegExp(`(${q})`, 'gi'), '<mark class="dsh-search-highlight">$1</mark>');
2008
+ }
2009
+ return escaped;
2010
+ }
2011
+ let html = highlightCode(content, category);
2012
+ if (searchQuery.trim()) {
2013
+ try {
2014
+ const q = escapeHtml(searchQuery.trim());
2015
+ const regex = new RegExp(`(${q})(?![^<]*>)`, 'gi');
2016
+ html = html.replace(regex, '<mark class="dsh-search-highlight">$1</mark>');
2017
+ } catch (e) {}
2018
+ }
2019
+ return html;
2020
+ }, [content, category, isHighlighted, searchQuery]);
2021
+
2022
+ return h('div', { className: `dsh-code-editor-view ${isWordWrap ? 'word-wrap' : ''}` },
2023
+ h('div', { className: 'dsh-code-gutter' },
2024
+ lines.map((_, i) => h('div', {
2025
+ key: i,
2026
+ id: `line-gutter-${i + 1}`,
2027
+ className: 'dsh-gutter-line',
2028
+ onClick: () => onLineClick && onLineClick(i + 1),
2029
+ title: `第 ${i + 1} 行 (点击定位)`
2030
+ }, i + 1))
2031
+ ),
2032
+ h('div', { className: 'dsh-code-content' },
2033
+ h('pre', { className: 'dsh-code-pre' },
2034
+ h('code', {
2035
+ className: `dsh-code-lang-${category}`,
2036
+ dangerouslySetInnerHTML: { __html: highlightedHtml }
2037
+ })
2038
+ )
2039
+ )
2040
+ );
2041
+ }
2042
+
1056
2043
  // React Components: MarkdownPreviewView
1057
2044
  // =========================================================================
1058
2045
  function MarkdownPreviewView({ tab, fileInfo, isLoading, error, isTreeOpen, onToggleTree, onRefresh, onOpenNative, onReveal }) {
1059
- const [viewMode, setViewMode] = useState(tab.viewMode || 'preview');
2046
+ const category = getFileTypeCategory(tab.filePath);
2047
+ const defaultMode = getDefaultViewMode(category);
2048
+ const [viewMode, setViewMode] = useState(tab.viewMode || defaultMode);
2049
+
2050
+ useEffect(() => {
2051
+ const mode = tab.viewMode || getDefaultViewMode(getFileTypeCategory(tab.filePath));
2052
+ setViewMode(mode);
2053
+ }, [tab.id, tab.filePath, tab.viewMode]);
2054
+ const [isWordWrap, setIsWordWrap] = useState(false);
1060
2055
  const [isTocOpen, setIsTocOpen] = useState(false);
1061
2056
  const [searchQuery, setSearchQuery] = useState('');
1062
2057
  const [isSearchOpen, setIsSearchOpen] = useState(false);
@@ -1064,10 +2059,35 @@
1064
2059
  const sourceRef = useRef(null);
1065
2060
 
1066
2061
  const content = fileInfo?.content || '';
1067
- const tocHeadings = useMemo(() => extractTocHeadings(content), [content]);
1068
2062
 
1069
- const renderedHtml = useMemo(() => {
1070
- if (!content) return '';
2063
+ // Outline extraction: markdown headings for md, symbols for code/json/yaml
2064
+ const outlineItems = useMemo(() => {
2065
+ if (!content) return [];
2066
+ if (category === 'markdown') {
2067
+ return extractTocHeadings(content);
2068
+ }
2069
+ return extractCodeSymbols(content, category);
2070
+ }, [content, category]);
2071
+
2072
+ // Parse JSON / YAML structure for tree mode
2073
+ const parsedTreeData = useMemo(() => {
2074
+ if (!content) return { ok: false, error: '空文件' };
2075
+ if (category === 'json' || viewMode === 'json-tree') {
2076
+ if (category === 'yaml') {
2077
+ return parseYamlToJson(content);
2078
+ }
2079
+ try {
2080
+ const data = JSON.parse(content);
2081
+ return { ok: true, data };
2082
+ } catch (e) {
2083
+ return { ok: false, error: e.message };
2084
+ }
2085
+ }
2086
+ return { ok: false };
2087
+ }, [content, category, viewMode]);
2088
+
2089
+ const renderedMarkdownHtml = useMemo(() => {
2090
+ if (!content || category !== 'markdown') return '';
1071
2091
  let html = renderMarkdownToHtml(content);
1072
2092
  if (searchQuery.trim()) {
1073
2093
  try {
@@ -1077,22 +2097,53 @@
1077
2097
  } catch (e) {}
1078
2098
  }
1079
2099
  return html;
1080
- }, [content, searchQuery]);
2100
+ }, [content, category, searchQuery]);
1081
2101
 
1082
2102
  const handleCopyAll = () => {
1083
2103
  if (!content) return;
1084
2104
  navigator.clipboard.writeText(content).then(() => {
1085
- showToast('已复制文档全文到剪贴板', 'success');
2105
+ showToast('已复制全文到剪贴板', 'success');
1086
2106
  }).catch(() => {
1087
2107
  showToast('复制失败', 'error');
1088
2108
  });
1089
2109
  };
1090
2110
 
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' });
2111
+ const handleFormatJSON = () => {
2112
+ try {
2113
+ const obj = JSON.parse(content);
2114
+ const formatted = JSON.stringify(obj, null, 2);
2115
+ navigator.clipboard.writeText(formatted).then(() => {
2116
+ showToast('已格式化 JSON 并复制到剪贴板', 'success');
2117
+ });
2118
+ } catch (e) {
2119
+ showToast('JSON 语法错误,无法格式化', 'error');
2120
+ }
2121
+ };
2122
+
2123
+ const handleMinifyJSON = () => {
2124
+ try {
2125
+ const obj = JSON.parse(content);
2126
+ const minified = JSON.stringify(obj);
2127
+ navigator.clipboard.writeText(minified).then(() => {
2128
+ showToast('已压缩为单行 JSON 并复制到剪贴板', 'success');
2129
+ });
2130
+ } catch (e) {
2131
+ showToast('JSON 语法错误,无法压缩', 'error');
2132
+ }
2133
+ };
2134
+
2135
+ const handleOutlineClick = (item) => {
2136
+ if (category === 'markdown') {
2137
+ if (!contentRef.current) return;
2138
+ const el = contentRef.current.querySelector(`#${item.id}`);
2139
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
2140
+ } else {
2141
+ const el = document.getElementById(`line-gutter-${item.line}`);
2142
+ if (el) {
2143
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
2144
+ el.classList.add('dsh-line-highlight-flash');
2145
+ setTimeout(() => el.classList.remove('dsh-line-highlight-flash'), 1800);
2146
+ }
1096
2147
  }
1097
2148
  };
1098
2149
 
@@ -1113,44 +2164,128 @@
1113
2164
  title: isTreeOpen ? '隐藏工作区目录树 (Ctrl/Cmd+B)' : '展开工作区目录树 (Ctrl/Cmd+B)',
1114
2165
  }, '🗂️ 目录树'),
1115
2166
 
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
- }, '🌓 分栏')
2167
+ // Adaptive View Mode buttons based on File Category
2168
+ category === 'markdown' ? (
2169
+ h('div', { className: 'dsh-preview-mode-group' },
2170
+ h('button', {
2171
+ type: 'button',
2172
+ className: `dsh-preview-mode-btn ${viewMode === 'preview' ? 'active' : ''}`,
2173
+ onClick: () => handleModeChange('preview'),
2174
+ title: 'Markdown 渲染预览 (Markdown Render)',
2175
+ }, '📖 预览'),
2176
+ h('button', {
2177
+ type: 'button',
2178
+ className: `dsh-preview-mode-btn ${viewMode === 'source' ? 'active' : ''}`,
2179
+ onClick: () => handleModeChange('source'),
2180
+ title: '源码高亮模式 (Source Code)',
2181
+ }, '📝 源码'),
2182
+ h('button', {
2183
+ type: 'button',
2184
+ className: `dsh-preview-mode-btn ${viewMode === 'split' ? 'active' : ''}`,
2185
+ onClick: () => handleModeChange('split'),
2186
+ title: '分栏对照模式 (Split View)',
2187
+ }, '🌓 分栏')
2188
+ )
2189
+ ) : category === 'json' ? (
2190
+ h('div', { className: 'dsh-preview-mode-group' },
2191
+ h('button', {
2192
+ type: 'button',
2193
+ className: `dsh-preview-mode-btn ${viewMode === 'json-tree' ? 'active' : ''}`,
2194
+ onClick: () => handleModeChange('json-tree'),
2195
+ title: '可视化结构树检查器 (JSON Tree)',
2196
+ }, '🌳 结构树'),
2197
+ h('button', {
2198
+ type: 'button',
2199
+ className: `dsh-preview-mode-btn ${viewMode === 'code' ? 'active' : ''}`,
2200
+ onClick: () => handleModeChange('code'),
2201
+ title: '语法高亮代码 (Code View)',
2202
+ }, '📜 代码'),
2203
+ h('button', {
2204
+ type: 'button',
2205
+ className: `dsh-preview-mode-btn ${viewMode === 'raw' ? 'active' : ''}`,
2206
+ onClick: () => handleModeChange('raw'),
2207
+ title: '纯文本模式 (Raw Text)',
2208
+ }, '📝 纯文本')
2209
+ )
2210
+ ) : category === 'yaml' ? (
2211
+ h('div', { className: 'dsh-preview-mode-group' },
2212
+ h('button', {
2213
+ type: 'button',
2214
+ className: `dsh-preview-mode-btn ${viewMode === 'code' ? 'active' : ''}`,
2215
+ onClick: () => handleModeChange('code'),
2216
+ title: 'YAML 语法高亮 (Syntax Highlighting)',
2217
+ }, '📜 高亮'),
2218
+ h('button', {
2219
+ type: 'button',
2220
+ className: `dsh-preview-mode-btn ${viewMode === 'json-tree' ? 'active' : ''}`,
2221
+ onClick: () => handleModeChange('json-tree'),
2222
+ title: '转为可视化结构树 (YAML as Tree)',
2223
+ }, '🌳 结构树'),
2224
+ h('button', {
2225
+ type: 'button',
2226
+ className: `dsh-preview-mode-btn ${viewMode === 'raw' ? 'active' : ''}`,
2227
+ onClick: () => handleModeChange('raw'),
2228
+ title: '纯文本模式 (Raw Text)',
2229
+ }, '📝 纯文本')
2230
+ )
2231
+ ) : (
2232
+ h('div', { className: 'dsh-preview-mode-group' },
2233
+ h('button', {
2234
+ type: 'button',
2235
+ className: `dsh-preview-mode-btn ${viewMode === 'code' ? 'active' : ''}`,
2236
+ onClick: () => handleModeChange('code'),
2237
+ title: '语法高亮模式 (Syntax Highlighting)',
2238
+ }, '📜 高亮'),
2239
+ h('button', {
2240
+ type: 'button',
2241
+ className: `dsh-preview-mode-btn ${viewMode === 'raw' ? 'active' : ''}`,
2242
+ onClick: () => handleModeChange('raw'),
2243
+ title: '纯文本模式 (Raw Text)',
2244
+ }, '📝 纯文本')
2245
+ )
1135
2246
  ),
1136
2247
 
1137
- tocHeadings.length > 0 ? h('button', {
2248
+ // Outline / TOC Button
2249
+ outlineItems.length > 0 ? h('button', {
1138
2250
  type: 'button',
1139
2251
  className: `dsh-preview-tool-btn ${isTocOpen ? 'active' : ''}`,
1140
2252
  onClick: () => setIsTocOpen(!isTocOpen),
1141
- title: isTocOpen ? '隐藏目录大纲' : '显示目录大纲 (TOC)',
1142
- }, `📑 大纲 (${tocHeadings.length})`) : null,
2253
+ title: isTocOpen ? '隐藏大纲侧栏' : '显示大纲侧栏',
2254
+ }, category === 'markdown' ? `📑 大纲 (${outlineItems.length})` : `📑 符号 (${outlineItems.length})`) : null,
2255
+
2256
+ // Word Wrap Toggle
2257
+ h('button', {
2258
+ type: 'button',
2259
+ className: `dsh-preview-tool-btn ${isWordWrap ? 'active' : ''}`,
2260
+ onClick: () => setIsWordWrap(!isWordWrap),
2261
+ title: isWordWrap ? '切换为不自动换行 (水平滚动)' : '切换为自动换行',
2262
+ }, '↩️ 换行'),
1143
2263
 
2264
+ // Search Button
1144
2265
  h('button', {
1145
2266
  type: 'button',
1146
2267
  className: `dsh-preview-tool-btn ${isSearchOpen ? 'active' : ''}`,
1147
2268
  onClick: () => setIsSearchOpen(!isSearchOpen),
1148
- title: '在文档中查找',
2269
+ title: '在文件中搜索',
1149
2270
  }, '🔍 查找')
1150
2271
  ),
1151
2272
 
1152
2273
  // Right: Action buttons
1153
2274
  h('div', { className: 'dsh-preview-toolbar-right' },
2275
+ category === 'json' ? h('button', {
2276
+ type: 'button',
2277
+ className: 'dsh-preview-tool-btn',
2278
+ onClick: handleFormatJSON,
2279
+ title: '格式化 JSON 并复制',
2280
+ }, '⚡ 格式化') : null,
2281
+
2282
+ category === 'json' ? h('button', {
2283
+ type: 'button',
2284
+ className: 'dsh-preview-tool-btn',
2285
+ onClick: handleMinifyJSON,
2286
+ title: '单行压缩 JSON 并复制',
2287
+ }, '🗜️ 压缩') : null,
2288
+
1154
2289
  h('button', {
1155
2290
  type: 'button',
1156
2291
  className: 'dsh-preview-tool-btn',
@@ -1199,12 +2334,12 @@
1199
2334
  }, '✕') : null
1200
2335
  ) : null,
1201
2336
 
1202
- // Body area with optional TOC sidebar + Main content
2337
+ // Body Layout
1203
2338
  h('div', { className: 'dsh-preview-body-layout' },
1204
- // TOC Sidebar
1205
- isTocOpen && tocHeadings.length > 0 ? h('div', { className: 'dsh-preview-toc-sidebar' },
2339
+ // Outline / TOC Sidebar
2340
+ isTocOpen && outlineItems.length > 0 ? h('div', { className: 'dsh-preview-toc-sidebar' },
1206
2341
  h('div', { className: 'dsh-preview-toc-header' },
1207
- h('span', { style: { fontWeight: 600, fontSize: 12 } }, '目录大纲 (TOC)'),
2342
+ h('span', { style: { fontWeight: 600, fontSize: 12 } }, category === 'markdown' ? '目录大纲 (TOC)' : '符号与属性大纲'),
1208
2343
  h('button', {
1209
2344
  type: 'button',
1210
2345
  className: 'dsh-toc-close-btn',
@@ -1212,73 +2347,95 @@
1212
2347
  }, '✕')
1213
2348
  ),
1214
2349
  h('div', { className: 'dsh-preview-toc-list' },
1215
- tocHeadings.map((hItem, idx) =>
2350
+ outlineItems.map((item, idx) =>
1216
2351
  h('div', {
1217
2352
  key: idx,
1218
- className: `dsh-toc-item dsh-toc-level-${hItem.level}`,
1219
- onClick: () => handleTocClick(hItem.id),
1220
- title: hItem.text,
2353
+ className: `dsh-toc-item ${item.level ? `dsh-toc-level-${item.level}` : 'dsh-symbol-item'}`,
2354
+ onClick: () => handleOutlineClick(item),
2355
+ title: item.text,
1221
2356
  },
1222
- h('span', { className: 'dsh-toc-bullet' }, '•'),
1223
- h('span', { className: 'dsh-toc-text' }, hItem.text)
2357
+ h('span', { className: 'dsh-toc-text' }, item.text)
1224
2358
  )
1225
2359
  )
1226
2360
  )
1227
2361
  ) : null,
1228
2362
 
1229
- // Main Viewer
2363
+ // Main View Content
1230
2364
  h('div', { className: 'dsh-preview-main-scroll' },
1231
2365
  isLoading ? h('div', { className: 'dsh-preview-loading' },
1232
2366
  h('div', { className: 'dsh-preview-spinner' }),
1233
- h('span', null, '正在加载文档内容...')
2367
+ h('span', null, '正在加载文件内容...')
1234
2368
  ) : (error || fileInfo?.error) ? h('div', { className: 'dsh-preview-error-card' },
1235
2369
  h('div', { className: 'dsh-error-icon' }, '⚠️'),
1236
- h('div', { className: 'dsh-error-title' }, '读取文档失败'),
2370
+ h('div', { className: 'dsh-error-title' }, '读取文件失败'),
1237
2371
  h('div', { className: 'dsh-error-desc' }, error || fileInfo?.error),
1238
2372
  h('div', { className: 'dsh-error-actions' },
1239
2373
  h('button', { type: 'button', className: 'dsh-btn-retry', onClick: onRefresh }, '重试'),
1240
2374
  h('button', { type: 'button', className: 'dsh-btn-native', onClick: () => onOpenNative(tab.filePath) }, '在外部打开')
1241
2375
  )
1242
- ) : viewMode === 'preview' ? (
1243
- // 1. Preview Mode
2376
+ ) : viewMode === 'json-tree' ? (
2377
+ // JSON / YAML Tree Inspector
2378
+ parsedTreeData.ok ? (
2379
+ h(JSONTreeInspector, {
2380
+ data: parsedTreeData.data,
2381
+ rawContent: content,
2382
+ onFormat: category === 'json' ? handleFormatJSON : null,
2383
+ onMinify: category === 'json' ? handleMinifyJSON : null,
2384
+ })
2385
+ ) : (
2386
+ h('div', { className: 'dsh-json-parse-error' },
2387
+ h('div', { style: { fontSize: 28, marginBottom: 8 } }, '⚠️'),
2388
+ h('div', { style: { fontWeight: 600, fontSize: 14, marginBottom: 4 } }, '结构解析失败'),
2389
+ h('div', { style: { fontSize: 12, color: '#ef4444', marginBottom: 12 } }, parsedTreeData.error || '无法按树状结构解析'),
2390
+ h('button', {
2391
+ type: 'button',
2392
+ className: 'dsh-btn-native',
2393
+ onClick: () => handleModeChange('code')
2394
+ }, '切换到代码高亮模式查看')
2395
+ )
2396
+ )
2397
+ ) : viewMode === 'preview' && category === 'markdown' ? (
2398
+ // Markdown Render View
1244
2399
  h('div', {
1245
2400
  ref: contentRef,
1246
2401
  className: 'dsh-markdown-body',
1247
- dangerouslySetInnerHTML: { __html: renderedHtml },
2402
+ dangerouslySetInnerHTML: { __html: renderedMarkdownHtml },
1248
2403
  })
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
2404
+ ) : viewMode === 'split' && category === 'markdown' ? (
2405
+ // Markdown Split View
1261
2406
  h('div', { className: 'dsh-split-view' },
1262
2407
  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
- )
2408
+ h('div', { className: 'dsh-split-pane-header' }, '📝 源码'),
2409
+ h(CodeEditorView, {
2410
+ content,
2411
+ category: 'markdown',
2412
+ isHighlighted: true,
2413
+ isWordWrap,
2414
+ searchQuery,
2415
+ onLineClick: () => {},
2416
+ })
1272
2417
  ),
1273
2418
  h('div', { className: 'dsh-split-pane dsh-split-render' },
1274
2419
  h('div', { className: 'dsh-split-pane-header' }, '📖 渲染预览'),
1275
2420
  h('div', {
1276
2421
  ref: contentRef,
1277
2422
  className: 'dsh-markdown-body',
1278
- dangerouslySetInnerHTML: { __html: renderedHtml },
2423
+ dangerouslySetInnerHTML: { __html: renderedMarkdownHtml },
1279
2424
  })
1280
2425
  )
1281
2426
  )
2427
+ ) : (
2428
+ // Code View (for code/json/yaml/source/raw modes)
2429
+ h(CodeEditorView, {
2430
+ content,
2431
+ category,
2432
+ isHighlighted: viewMode !== 'raw',
2433
+ isWordWrap,
2434
+ searchQuery,
2435
+ onLineClick: (lineNum) => {
2436
+ showToast(`已选定第 ${lineNum} 行`, 'info');
2437
+ },
2438
+ })
1282
2439
  )
1283
2440
  )
1284
2441
  ),
@@ -1286,11 +2443,11 @@
1286
2443
  // Status Footer
1287
2444
  h('div', { className: 'dsh-preview-footer' },
1288
2445
  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('.', '')),
2446
+ h('span', { className: 'dsh-footer-ext' }, (fileInfo?.extension || tab.extension || category).toUpperCase().replace('.', '')),
1290
2447
  h('span', { className: 'dsh-footer-path' }, fileInfo?.path || tab.filePath)
1291
2448
  ),
1292
2449
  h('div', { className: 'dsh-footer-right' },
1293
- fileInfo?.size ? h('span', { className: 'dsh-footer-stat' }, `${(fileInfo.size / 1024).toFixed(1)} KB`) : null,
2450
+ fileInfo?.size ? h('span', { className: 'dsh-footer-stat' }, formatBytes(fileInfo.size)) : null,
1294
2451
  fileInfo?.lineCount ? h('span', { className: 'dsh-footer-stat' }, `${fileInfo.lineCount} 行`) : null,
1295
2452
  fileInfo?.wordCount ? h('span', { className: 'dsh-footer-stat' }, `约 ${fileInfo.wordCount} 字`) : null
1296
2453
  )
@@ -1579,6 +2736,7 @@
1579
2736
 
1580
2737
  // Right: Content View or Welcome View
1581
2738
  activeTab ? h(MarkdownPreviewView, {
2739
+ key: activeTab.id,
1582
2740
  tab: activeTab,
1583
2741
  fileInfo: currentFileInfo,
1584
2742
  isLoading: currentLoading,
@@ -3048,6 +4206,298 @@
3048
4206
  .dsh-welcome-btn.primary:hover {
3049
4207
  opacity: 0.9;
3050
4208
  }
4209
+
4210
+ /* Syntax Highlighting Tokens (Light Theme) */
4211
+ .tok-keyword { color: #0000ff; font-weight: 600; }
4212
+ .tok-string { color: #a31515; }
4213
+ .tok-number { color: #098658; }
4214
+ .tok-boolean { color: #0000ff; font-weight: 600; }
4215
+ .tok-null { color: #708090; font-style: italic; }
4216
+ .tok-key { color: #001080; font-weight: 600; }
4217
+ .tok-comment { color: #008000; font-style: italic; }
4218
+ .tok-type { color: #267f99; }
4219
+ .tok-func { color: #795e26; }
4220
+ .tok-operator { color: #000000; font-weight: 500; }
4221
+ .tok-punctuation, .tok-bracket { color: #333333; }
4222
+ .tok-tag { color: #800000; font-weight: 600; }
4223
+ .tok-attr { color: #e50000; }
4224
+ .tok-prop { color: #001080; }
4225
+ .tok-selector { color: #800000; }
4226
+
4227
+ /* Syntax Highlighting Tokens (Dark Theme) */
4228
+ body[data-ds-dark-theme] .tok-keyword { color: #569cd6; }
4229
+ body[data-ds-dark-theme] .tok-string { color: #ce9178; }
4230
+ body[data-ds-dark-theme] .tok-number { color: #b5cea8; }
4231
+ body[data-ds-dark-theme] .tok-boolean { color: #569cd6; font-weight: 600; }
4232
+ body[data-ds-dark-theme] .tok-null { color: #808080; font-style: italic; }
4233
+ body[data-ds-dark-theme] .tok-key { color: #9cdcfe; font-weight: 600; }
4234
+ body[data-ds-dark-theme] .tok-comment { color: #6a9955; font-style: italic; }
4235
+ body[data-ds-dark-theme] .tok-type { color: #4ec9b0; }
4236
+ body[data-ds-dark-theme] .tok-func { color: #dcdcaa; }
4237
+ body[data-ds-dark-theme] .tok-operator { color: #d4d4d4; }
4238
+ body[data-ds-dark-theme] .tok-punctuation, body[data-ds-dark-theme] .tok-bracket { color: #d4d4d4; }
4239
+ body[data-ds-dark-theme] .tok-tag { color: #569cd6; }
4240
+ body[data-ds-dark-theme] .tok-attr { color: #9cdcfe; }
4241
+ body[data-ds-dark-theme] .tok-prop { color: #9cdcfe; }
4242
+ body[data-ds-dark-theme] .tok-selector { color: #d7ba7d; }
4243
+
4244
+ /* Code Editor Viewer */
4245
+ .dsh-code-editor-view {
4246
+ display: flex;
4247
+ min-height: 100%;
4248
+ font-family: "Cascadia Code", "Fira Code", Consolas, Menlo, monospace;
4249
+ font-size: 13px;
4250
+ line-height: 20px;
4251
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
4252
+ color: var(--dsw-alias-label-primary, #1e293b);
4253
+ }
4254
+ body[data-ds-dark-theme] .dsh-code-editor-view {
4255
+ background: #181b24;
4256
+ color: #d4d4d4;
4257
+ }
4258
+
4259
+ .dsh-code-gutter {
4260
+ width: 44px;
4261
+ min-width: 44px;
4262
+ padding: 12px 6px 12px 0;
4263
+ text-align: right;
4264
+ user-select: none;
4265
+ background: var(--dsw-alias-bg-layer-2, #f8fafc);
4266
+ border-right: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
4267
+ color: var(--dsw-alias-label-tertiary, #94a3b8);
4268
+ font-size: 12px;
4269
+ line-height: 20px;
4270
+ flex-shrink: 0;
4271
+ }
4272
+ body[data-ds-dark-theme] .dsh-code-gutter {
4273
+ background: #141720;
4274
+ border-right-color: #2a3140;
4275
+ color: #64748b;
4276
+ }
4277
+
4278
+ .dsh-gutter-line {
4279
+ height: 20px;
4280
+ cursor: pointer;
4281
+ padding-right: 8px;
4282
+ }
4283
+ .dsh-gutter-line:hover {
4284
+ color: var(--dsw-brand-primary, #0284c7);
4285
+ font-weight: bold;
4286
+ }
4287
+ .dsh-gutter-line.dsh-line-highlight-flash {
4288
+ background: rgba(14, 165, 233, 0.25);
4289
+ color: #0284c7;
4290
+ }
4291
+
4292
+ .dsh-code-content {
4293
+ flex: 1;
4294
+ min-width: 0;
4295
+ padding: 12px 16px;
4296
+ overflow: auto;
4297
+ }
4298
+ .dsh-code-pre {
4299
+ margin: 0;
4300
+ font-family: inherit;
4301
+ font-size: inherit;
4302
+ line-height: 20px;
4303
+ tab-size: 2;
4304
+ white-space: pre;
4305
+ }
4306
+ .dsh-code-editor-view.word-wrap .dsh-code-pre {
4307
+ white-space: pre-wrap;
4308
+ word-break: normal;
4309
+ overflow-wrap: break-word;
4310
+ }
4311
+ .dsh-code-editor-view {
4312
+ display: flex;
4313
+ min-height: 100%;
4314
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
4315
+ font-size: 12.5px;
4316
+ line-height: 20px;
4317
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
4318
+ color: var(--dsw-alias-label-primary, #1e293b);
4319
+ }
4320
+ .dsh-code-pre code {
4321
+ font-family: inherit;
4322
+ }
4323
+
4324
+ /* Interactive JSON Inspector */
4325
+ .dsh-json-inspector {
4326
+ display: flex;
4327
+ flex-direction: column;
4328
+ height: 100%;
4329
+ overflow: hidden;
4330
+ font-family: "Cascadia Code", Consolas, monospace;
4331
+ font-size: 12px;
4332
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
4333
+ }
4334
+ body[data-ds-dark-theme] .dsh-json-inspector {
4335
+ background: #181b24;
4336
+ color: #d4d4d4;
4337
+ }
4338
+
4339
+ .dsh-json-header {
4340
+ display: flex;
4341
+ align-items: center;
4342
+ justify-content: space-between;
4343
+ padding: 8px 12px;
4344
+ border-bottom: 1px solid var(--dsw-alias-border-l3, #e2e8f0);
4345
+ background: var(--dsw-alias-bg-layer-2, #f8fafc);
4346
+ gap: 12px;
4347
+ flex-shrink: 0;
4348
+ }
4349
+ body[data-ds-dark-theme] .dsh-json-header {
4350
+ border-bottom-color: #2a3140;
4351
+ background: #141720;
4352
+ }
4353
+
4354
+ .dsh-json-search-box {
4355
+ position: relative;
4356
+ display: flex;
4357
+ align-items: center;
4358
+ flex: 1;
4359
+ max-width: 280px;
4360
+ }
4361
+ .dsh-json-search-icon {
4362
+ position: absolute;
4363
+ left: 8px;
4364
+ font-size: 11px;
4365
+ pointer-events: none;
4366
+ }
4367
+ .dsh-json-search-input {
4368
+ width: 100%;
4369
+ height: 26px;
4370
+ border-radius: 6px;
4371
+ border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
4372
+ padding: 0 24px 0 26px;
4373
+ font-size: 12px;
4374
+ background: #ffffff;
4375
+ color: inherit;
4376
+ outline: none;
4377
+ }
4378
+ body[data-ds-dark-theme] .dsh-json-search-input {
4379
+ background: #1e2330;
4380
+ border-color: #334155;
4381
+ color: #f1f5f9;
4382
+ }
4383
+ .dsh-json-actions {
4384
+ display: flex;
4385
+ gap: 6px;
4386
+ }
4387
+ .dsh-json-btn {
4388
+ padding: 3px 8px;
4389
+ border-radius: 4px;
4390
+ border: 1px solid var(--dsw-alias-border-l2, #cbd5e1);
4391
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
4392
+ color: var(--dsw-alias-label-secondary, #475569);
4393
+ cursor: pointer;
4394
+ font-size: 11px;
4395
+ transition: all 0.15s;
4396
+ }
4397
+ .dsh-json-btn:hover {
4398
+ background: var(--dsw-alias-bg-hover, rgba(0, 0, 0, 0.05));
4399
+ color: var(--dsw-alias-label-primary, #0f172a);
4400
+ }
4401
+ body[data-ds-dark-theme] .dsh-json-btn {
4402
+ background: #1e2330;
4403
+ border-color: #334155;
4404
+ color: #94a3b8;
4405
+ }
4406
+ body[data-ds-dark-theme] .dsh-json-btn:hover {
4407
+ background: #283042;
4408
+ color: #f1f5f9;
4409
+ }
4410
+
4411
+ .dsh-json-tree-body {
4412
+ flex: 1;
4413
+ overflow: auto;
4414
+ padding: 10px 12px;
4415
+ }
4416
+
4417
+ .dsh-json-row {
4418
+ display: flex;
4419
+ align-items: center;
4420
+ min-height: 22px;
4421
+ line-height: 22px;
4422
+ gap: 6px;
4423
+ border-radius: 4px;
4424
+ padding-right: 8px;
4425
+ transition: background 0.1s;
4426
+ }
4427
+ .dsh-json-row:hover {
4428
+ background: rgba(0, 0, 0, 0.03);
4429
+ }
4430
+ body[data-ds-dark-theme] .dsh-json-row:hover {
4431
+ background: rgba(255, 255, 255, 0.04);
4432
+ }
4433
+ .dsh-json-row-expandable {
4434
+ cursor: pointer;
4435
+ }
4436
+
4437
+ .dsh-json-chevron {
4438
+ font-size: 9px;
4439
+ color: #94a3b8;
4440
+ width: 12px;
4441
+ height: 12px;
4442
+ display: inline-flex;
4443
+ align-items: center;
4444
+ justify-content: center;
4445
+ flex-shrink: 0;
4446
+ user-select: none;
4447
+ }
4448
+ .dsh-json-key {
4449
+ color: #001080;
4450
+ font-weight: 600;
4451
+ flex-shrink: 0;
4452
+ }
4453
+ body[data-ds-dark-theme] .dsh-json-key {
4454
+ color: #9cdcfe;
4455
+ }
4456
+ .dsh-json-type-badge {
4457
+ font-size: 10px;
4458
+ color: #94a3b8;
4459
+ background: rgba(0, 0, 0, 0.05);
4460
+ padding: 0 5px;
4461
+ border-radius: 6px;
4462
+ user-select: none;
4463
+ }
4464
+ body[data-ds-dark-theme] .dsh-json-type-badge {
4465
+ background: rgba(255, 255, 255, 0.08);
4466
+ color: #64748b;
4467
+ }
4468
+ .dsh-json-preview {
4469
+ color: #64748b;
4470
+ font-style: italic;
4471
+ font-size: 11px;
4472
+ }
4473
+ body[data-ds-dark-theme] .dsh-json-preview {
4474
+ color: #94a3b8;
4475
+ }
4476
+ .dsh-json-copy-btn {
4477
+ opacity: 0;
4478
+ cursor: pointer;
4479
+ font-size: 10px;
4480
+ margin-left: auto;
4481
+ padding: 1px 4px;
4482
+ border-radius: 3px;
4483
+ transition: opacity 0.15s;
4484
+ }
4485
+ .dsh-json-row:hover .dsh-json-copy-btn {
4486
+ opacity: 0.75;
4487
+ }
4488
+ .dsh-json-copy-btn:hover {
4489
+ opacity: 1;
4490
+ background: rgba(0, 0, 0, 0.08);
4491
+ }
4492
+
4493
+ .dsh-json-parse-error {
4494
+ padding: 36px 16px;
4495
+ text-align: center;
4496
+ color: var(--dsw-alias-label-secondary, #64748b);
4497
+ }
4498
+ .dsh-symbol-item {
4499
+ padding-left: 10px;
4500
+ }
3051
4501
  `;
3052
4502
  document.head.appendChild(style);
3053
4503
  }