@lvce-editor/output-view 2.10.1 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -991,6 +991,7 @@ const ToolBar = 'toolbar';
991
991
  const Button = 1;
992
992
  const Div = 4;
993
993
  const Input = 6;
994
+ const Span = 8;
994
995
  const Text$1 = 12;
995
996
  const A = 53;
996
997
  const Select$1 = 63;
@@ -1241,10 +1242,18 @@ const terminate = () => {
1241
1242
  globalThis.close();
1242
1243
  };
1243
1244
 
1245
+ const Text = 1;
1246
+ const Link = 2;
1247
+ const LogLevel = 3;
1248
+ const RepeatCount = 4;
1249
+
1244
1250
  const getTextFromParts = parts => {
1245
1251
  let result = '';
1246
1252
  for (const part of parts) {
1247
- result += part.value;
1253
+ if (part.type === RepeatCount) {
1254
+ continue;
1255
+ }
1256
+ result += part.type === Link ? part.label || part.value : part.value;
1248
1257
  }
1249
1258
  return result;
1250
1259
  };
@@ -1256,6 +1265,35 @@ const filterItems = (items, filterValue) => {
1256
1265
  return items.filter(parts => getTextFromParts(parts).toLowerCase().includes(normalizedFilterValue));
1257
1266
  };
1258
1267
 
1268
+ const getAggregationKey = line => {
1269
+ return JSON.stringify(line.filter(part => part.type !== RepeatCount));
1270
+ };
1271
+ const withRepeatCount = (line, count) => {
1272
+ const parts = line.filter(part => part.type !== RepeatCount);
1273
+ return [{
1274
+ type: RepeatCount,
1275
+ value: String(count)
1276
+ }, ...parts];
1277
+ };
1278
+ const aggregateLines = lines => {
1279
+ const result = [];
1280
+ let previousKey = '';
1281
+ let repeatCount = 0;
1282
+ for (const line of lines) {
1283
+ const key = getAggregationKey(line);
1284
+ if (result.length > 0 && key === previousKey) {
1285
+ repeatCount++;
1286
+ const previousIndex = result.length - 1;
1287
+ result[previousIndex] = withRepeatCount(result.at(-1), repeatCount);
1288
+ continue;
1289
+ }
1290
+ result.push(line);
1291
+ previousKey = key;
1292
+ repeatCount = 1;
1293
+ }
1294
+ return result;
1295
+ };
1296
+
1259
1297
  const isFileNotFoundError = error => {
1260
1298
  return error?.message?.includes('File not found') ?? false;
1261
1299
  };
@@ -1266,10 +1304,75 @@ const getLinkMatch = text => {
1266
1304
  return match ? match[0] : null;
1267
1305
  };
1268
1306
 
1269
- const Text = 1;
1270
- const Link = 2;
1307
+ const Filter$2 = 'Filter';
1308
+ const IconButton = 'IconButton';
1309
+ const InputBox = 'InputBox';
1310
+ const MaskIcon = 'MaskIcon';
1311
+ const FilterInput = 'FilterInput';
1312
+ const OutputContent = 'OutputContent';
1313
+ const Actions = 'Actions';
1314
+ const Message = 'Message';
1315
+ const Viewlet = 'Viewlet';
1316
+ const Output$1 = 'Output';
1317
+ const Option = 'Option';
1318
+ const Select = 'Select';
1319
+ const Error$1 = 'Error';
1320
+ const Line = 'Line';
1321
+ const OutputRepeatCount = 'OutputRepeatCount';
1322
+ const OutputSourceLink = 'OutputSourceLink';
1323
+
1324
+ const isStructuredLogEntry = value => {
1325
+ return value?.category === 'Window' && typeof value.level === 'string' && typeof value.line === 'number' && typeof value.message === 'string' && typeof value.source === 'string' && typeof value.timestamp === 'string';
1326
+ };
1327
+ const getSourceLabel = (source, line) => {
1328
+ const withoutQueryOrFragment = source.split(/[?#]/, 1)[0];
1329
+ const slashIndex = withoutQueryOrFragment.lastIndexOf('/');
1330
+ const name = slashIndex === -1 ? withoutQueryOrFragment : withoutQueryOrFragment.slice(slashIndex + 1);
1331
+ return line > 0 ? `${name}:${line}` : name;
1332
+ };
1333
+ const normalizeLevel = level => {
1334
+ return level === 'warn' ? 'warning' : level;
1335
+ };
1336
+ const parseStructuredLogLine = line => {
1337
+ if (!line.startsWith('{')) {
1338
+ return undefined;
1339
+ }
1340
+ let parsed;
1341
+ try {
1342
+ parsed = JSON.parse(line);
1343
+ } catch {
1344
+ return undefined;
1345
+ }
1346
+ if (!isStructuredLogEntry(parsed)) {
1347
+ return undefined;
1348
+ }
1349
+ const parts = [{
1350
+ type: LogLevel,
1351
+ value: normalizeLevel(parsed.level)
1352
+ }, {
1353
+ type: Text,
1354
+ value: parsed.message
1355
+ }];
1356
+ if (!parsed.source) {
1357
+ return parts;
1358
+ }
1359
+ const href = parsed.line > 0 ? `${parsed.source}:${parsed.line}` : parsed.source;
1360
+ return [...parts, {
1361
+ type: Text,
1362
+ value: ' '
1363
+ }, {
1364
+ className: OutputSourceLink,
1365
+ label: getSourceLabel(parsed.source, parsed.line),
1366
+ type: Link,
1367
+ value: href
1368
+ }];
1369
+ };
1271
1370
 
1272
1371
  const parseLine = line => {
1372
+ const structuredLine = parseStructuredLogLine(line);
1373
+ if (structuredLine) {
1374
+ return structuredLine;
1375
+ }
1273
1376
  const parts = [];
1274
1377
  let rest = line;
1275
1378
  while (rest.length > 0) {
@@ -1309,7 +1412,7 @@ const loadLines = async uri => {
1309
1412
  try {
1310
1413
  // TODO use log stream, updating the output when the file is changed
1311
1414
  const content = await readFile(uri);
1312
- const lines = content.split('\n').map(parseLine);
1415
+ const lines = aggregateLines(content.split('\n').map(parseLine));
1313
1416
  return {
1314
1417
  code: 0,
1315
1418
  error: '',
@@ -1550,8 +1653,8 @@ const getKeyBindings = () => {
1550
1653
  }];
1551
1654
  };
1552
1655
 
1553
- const Filter$2 = 'filter';
1554
- const Output$1 = 'output';
1656
+ const Filter$1 = 'filter';
1657
+ const Output = 'output';
1555
1658
  const MainProcess = 'MainProcess';
1556
1659
  const SharedProcess = 'SharedProcess';
1557
1660
  const Window = 'Window';
@@ -1762,7 +1865,7 @@ const i18nString = (key, placeholders = emptyObject) => {
1762
1865
  };
1763
1866
 
1764
1867
  const ClearOutput = 'clear output';
1765
- const Filter$1 = 'Filter';
1868
+ const Filter = 'Filter';
1766
1869
  const LogFileNotFound$1 = 'The log file was not found.';
1767
1870
  const Settings = 'Settings';
1768
1871
  const TurnOffAutoScroll = 'Turn auto scrolling off';
@@ -1780,7 +1883,7 @@ const logFileNotFound = () => {
1780
1883
  return i18nString(LogFileNotFound$1);
1781
1884
  };
1782
1885
  const filter = () => {
1783
- return i18nString(Filter$1);
1886
+ return i18nString(Filter);
1784
1887
  };
1785
1888
 
1786
1889
  const loadButtons = () => {
@@ -1972,47 +2075,69 @@ const refresh = async state => {
1972
2075
  };
1973
2076
 
1974
2077
  const renderFilterValue = (oldState, newState) => {
1975
- return ['Viewlet.setValueByName', Filter$2, newState.filterValue];
2078
+ return ['Viewlet.setValueByName', Filter$1, newState.filterValue];
1976
2079
  };
1977
2080
 
1978
- const Filter = 'Filter';
1979
- const IconButton = 'IconButton';
1980
- const InputBox = 'InputBox';
1981
- const MaskIcon = 'MaskIcon';
1982
- const FilterInput = 'FilterInput';
1983
- const OutputContent = 'OutputContent';
1984
- const Actions = 'Actions';
1985
- const Message = 'Message';
1986
- const Viewlet = 'Viewlet';
1987
- const Output = 'Output';
1988
- const Option = 'Option';
1989
- const Select = 'Select';
1990
- const Error$1 = 'Error';
1991
- const Line = 'Line';
1992
-
1993
- const parentNode = {
2081
+ const noNodes = [];
2082
+ const repeatCountNode = {
1994
2083
  childCount: 1,
1995
- className: Line,
1996
- type: Div
2084
+ className: OutputRepeatCount,
2085
+ type: Span
2086
+ };
2087
+ const spaceNode = text(' ');
2088
+ const getLevel = parts => {
2089
+ return parts.find(part => part.type === LogLevel)?.value || '';
2090
+ };
2091
+ const getLinePartDom = part => {
2092
+ switch (part.type) {
2093
+ case Link:
2094
+ {
2095
+ const linkNode = {
2096
+ childCount: 1,
2097
+ href: part.value,
2098
+ rel: 'noopener noreferrer',
2099
+ target: '_blank',
2100
+ type: A,
2101
+ ...(part.className && {
2102
+ className: part.className
2103
+ })
2104
+ };
2105
+ return {
2106
+ childCount: 1,
2107
+ nodes: [linkNode, text(part.label || part.value)]
2108
+ };
2109
+ }
2110
+ case RepeatCount:
2111
+ return {
2112
+ childCount: 2,
2113
+ nodes: [repeatCountNode, text(part.value), spaceNode]
2114
+ };
2115
+ case Text:
2116
+ return {
2117
+ childCount: 1,
2118
+ nodes: [text(part.value)]
2119
+ };
2120
+ default:
2121
+ return {
2122
+ childCount: 0,
2123
+ nodes: noNodes
2124
+ };
2125
+ }
1997
2126
  };
1998
2127
  const getLineDom = parts => {
1999
2128
  const children = [];
2129
+ let childCount = 0;
2000
2130
  for (const part of parts) {
2001
- if (part.type === Text) {
2002
- children.push(text(part.value));
2003
- } else {
2004
- children.push({
2005
- childCount: 1,
2006
- href: part.value,
2007
- rel: 'noopener noreferrer',
2008
- target: '_blank',
2009
- type: A
2010
- }, text(part.value));
2011
- }
2131
+ const partDom = getLinePartDom(part);
2132
+ children.push(...partDom.nodes);
2133
+ childCount += partDom.childCount;
2012
2134
  }
2135
+ const level = getLevel(parts);
2136
+ const className = level ? mergeClassNames(Line, level) : Line;
2013
2137
  const lineNode = {
2014
- ...parentNode,
2015
- childCount: children.length
2138
+ childCount,
2139
+ className,
2140
+ type: Div
2016
2141
  };
2017
2142
  return [lineNode, ...children];
2018
2143
  };
@@ -2060,12 +2185,13 @@ const getErrorDom = (errorCode, error) => {
2060
2185
  return [errorNode, text(error)];
2061
2186
  };
2062
2187
 
2188
+ const outputNode = {
2189
+ childCount: 1,
2190
+ className: mergeClassNames(Viewlet, Output$1),
2191
+ type: Div
2192
+ };
2063
2193
  const getOutputVirtualDom = (lines, errorCode, error) => {
2064
- return [{
2065
- childCount: 1,
2066
- className: mergeClassNames(Viewlet, Output),
2067
- type: Div
2068
- }, ...getContentDom(lines, error), ...getErrorDom(errorCode, error)];
2194
+ return [outputNode, ...getContentDom(lines, error), ...getErrorDom(errorCode, error)];
2069
2195
  };
2070
2196
 
2071
2197
  const renderItems = (oldState, newState) => {
@@ -2075,7 +2201,7 @@ const renderItems = (oldState, newState) => {
2075
2201
 
2076
2202
  const renderSelectedItem = (oldState, newState) => {
2077
2203
  const value = newState.selectedOption;
2078
- const name = Output$1;
2204
+ const name = Output;
2079
2205
  return ['Viewlet.setValueByName', name, value];
2080
2206
  };
2081
2207
 
@@ -2144,7 +2270,7 @@ const getActionButtonsVirtualDom = buttons => {
2144
2270
 
2145
2271
  const filterNode = {
2146
2272
  childCount: 1,
2147
- className: Filter,
2273
+ className: Filter$2,
2148
2274
  type: Div
2149
2275
  };
2150
2276
  const getFilterVirtualDom = () => {
@@ -2152,7 +2278,7 @@ const getFilterVirtualDom = () => {
2152
2278
  return [filterNode, {
2153
2279
  childCount: 0,
2154
2280
  className: mergeClassNames(InputBox, FilterInput),
2155
- name: Filter$2,
2281
+ name: Filter$1,
2156
2282
  onInput: HandleFilterInput,
2157
2283
  placeholder,
2158
2284
  type: Input
@@ -2176,7 +2302,7 @@ const getSelectVirtualDom = options => {
2176
2302
  return [{
2177
2303
  childCount: options.length,
2178
2304
  className: Select,
2179
- name: Output$1,
2305
+ name: Output,
2180
2306
  onChange: HandleSelect,
2181
2307
  type: Select$1
2182
2308
  }, ...options.flatMap(getOptionVirtualDom)];
@@ -2241,7 +2367,10 @@ const resize = (state, dimensions) => {
2241
2367
  };
2242
2368
 
2243
2369
  const serializeLinePart = part => {
2244
- return part.value;
2370
+ if (part.type === LogLevel || part.type === RepeatCount) {
2371
+ return '';
2372
+ }
2373
+ return part.type === Link ? part.label || part.value : part.value;
2245
2374
  };
2246
2375
  const serializeLineParts = parts => {
2247
2376
  return parts.map(serializeLinePart).join('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/output-view",
3
- "version": "2.10.1",
3
+ "version": "2.12.0",
4
4
  "description": "Output View Worker",
5
5
  "repository": {
6
6
  "type": "git",