@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/cordis.patch.yml +12 -0
- package/lib/client.js +1529 -79
- package/package.json +2 -2
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
|
-
'.
|
|
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 === '.
|
|
176
|
-
if (
|
|
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
|
-
|
|
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"><</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
|
|
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
|
-
|
|
1070
|
-
|
|
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('
|
|
2105
|
+
showToast('已复制全文到剪贴板', 'success');
|
|
1086
2106
|
}).catch(() => {
|
|
1087
2107
|
showToast('复制失败', 'error');
|
|
1088
2108
|
});
|
|
1089
2109
|
};
|
|
1090
2110
|
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
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
|
-
|
|
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 ? '
|
|
1142
|
-
}, `📑 大纲 (${
|
|
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
|
|
2337
|
+
// Body Layout
|
|
1203
2338
|
h('div', { className: 'dsh-preview-body-layout' },
|
|
1204
|
-
// TOC Sidebar
|
|
1205
|
-
isTocOpen &&
|
|
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
|
-
|
|
2350
|
+
outlineItems.map((item, idx) =>
|
|
1216
2351
|
h('div', {
|
|
1217
2352
|
key: idx,
|
|
1218
|
-
className: `dsh-toc-item dsh-toc-level-${
|
|
1219
|
-
onClick: () =>
|
|
1220
|
-
title:
|
|
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-
|
|
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
|
|
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 === '
|
|
1243
|
-
//
|
|
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:
|
|
2402
|
+
dangerouslySetInnerHTML: { __html: renderedMarkdownHtml },
|
|
1248
2403
|
})
|
|
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
|
|
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(
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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:
|
|
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 ||
|
|
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' },
|
|
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
|
}
|