@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.
- package/lib/client.js +1282 -75
- 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
|
-
|
|
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"><</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
|
|
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
|
-
|
|
1070
|
-
|
|
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('
|
|
1862
|
+
showToast('已复制全文到剪贴板', 'success');
|
|
1086
1863
|
}).catch(() => {
|
|
1087
1864
|
showToast('复制失败', 'error');
|
|
1088
1865
|
});
|
|
1089
1866
|
};
|
|
1090
1867
|
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
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
|
-
|
|
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 ? '
|
|
1142
|
-
}, `📑 大纲 (${
|
|
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
|
|
2094
|
+
// Body Layout
|
|
1203
2095
|
h('div', { className: 'dsh-preview-body-layout' },
|
|
1204
|
-
// TOC Sidebar
|
|
1205
|
-
isTocOpen &&
|
|
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
|
-
|
|
2107
|
+
outlineItems.map((item, idx) =>
|
|
1216
2108
|
h('div', {
|
|
1217
2109
|
key: idx,
|
|
1218
|
-
className: `dsh-toc-item dsh-toc-level-${
|
|
1219
|
-
onClick: () =>
|
|
1220
|
-
title:
|
|
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-
|
|
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
|
|
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 === '
|
|
1243
|
-
//
|
|
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:
|
|
2159
|
+
dangerouslySetInnerHTML: { __html: renderedMarkdownHtml },
|
|
1248
2160
|
})
|
|
1249
|
-
) : viewMode === '
|
|
1250
|
-
//
|
|
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(
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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:
|
|
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 ||
|
|
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' },
|
|
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
|
}
|