reslop 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +32 -19
  6. package/lib/capabilities.js +8 -8
  7. package/lib/cli.js +40 -74
  8. package/lib/clipboard.js +39 -47
  9. package/lib/dashboard/git.js +128 -0
  10. package/lib/dashboard/model.js +332 -0
  11. package/lib/dashboard/npm.js +193 -0
  12. package/lib/dashboard/tree.js +278 -0
  13. package/lib/deps-diff.js +197 -402
  14. package/lib/deps.js +113 -240
  15. package/lib/detect.js +8 -18
  16. package/lib/diff/align.js +41 -113
  17. package/lib/diff/diff.js +12 -43
  18. package/lib/diff/display.js +25 -36
  19. package/lib/diff/edit.js +6 -20
  20. package/lib/diff/inline.js +76 -144
  21. package/lib/diff/items.js +33 -47
  22. package/lib/diff/parse.js +28 -56
  23. package/lib/diff/patch.js +12 -33
  24. package/lib/diff/unit.js +79 -178
  25. package/lib/editor.js +52 -69
  26. package/lib/files.js +71 -89
  27. package/lib/git-branches.js +254 -416
  28. package/lib/git-deps.js +147 -272
  29. package/lib/git-reword-todo.js +5 -13
  30. package/lib/git-worktree.js +141 -212
  31. package/lib/git.js +21 -49
  32. package/lib/github.js +67 -181
  33. package/lib/gitlab.js +40 -90
  34. package/lib/highlight/bash.js +55 -177
  35. package/lib/highlight/core.js +155 -141
  36. package/lib/highlight/css.js +25 -58
  37. package/lib/highlight/csv.js +19 -37
  38. package/lib/highlight/dart.js +72 -213
  39. package/lib/highlight/dot.js +14 -14
  40. package/lib/highlight/html.js +42 -63
  41. package/lib/highlight/js.js +177 -506
  42. package/lib/highlight/json.js +17 -37
  43. package/lib/highlight/log.js +19 -25
  44. package/lib/highlight/txt.js +7 -10
  45. package/lib/highlight.js +17 -46
  46. package/lib/keys.js +49 -82
  47. package/lib/manifest.js +67 -126
  48. package/lib/npm-commands.js +120 -169
  49. package/lib/remote.js +140 -190
  50. package/lib/render/branches.js +63 -61
  51. package/lib/render/chrome.js +220 -333
  52. package/lib/render/commits.js +160 -361
  53. package/lib/render/dash-activity.js +212 -0
  54. package/lib/render/dash-blocks.js +245 -0
  55. package/lib/render/dash-table.js +290 -0
  56. package/lib/render/dashboard.js +62 -0
  57. package/lib/render/diff.js +123 -258
  58. package/lib/render/files.js +40 -85
  59. package/lib/render/frame.js +78 -117
  60. package/lib/render/notes.js +139 -210
  61. package/lib/render/npm.js +213 -177
  62. package/lib/render/present.js +4 -11
  63. package/lib/render/primitives.js +72 -174
  64. package/lib/render/render.js +5 -14
  65. package/lib/render/report.js +43 -0
  66. package/lib/render/tiles.js +260 -0
  67. package/lib/render/unit.js +15 -31
  68. package/lib/report-model.js +110 -33
  69. package/lib/report-parse.js +56 -37
  70. package/lib/report-render.js +102 -59
  71. package/lib/report-run.js +87 -77
  72. package/lib/report-test.js +221 -99
  73. package/lib/report-tools.js +96 -154
  74. package/lib/review-merge.js +33 -41
  75. package/lib/review.js +68 -129
  76. package/lib/runs.js +225 -0
  77. package/lib/select.js +53 -91
  78. package/lib/session/accessors.js +14 -21
  79. package/lib/session/actions.js +61 -43
  80. package/lib/session/branches.js +166 -222
  81. package/lib/session/change-actions.js +49 -119
  82. package/lib/session/commit.js +274 -355
  83. package/lib/session/compose.js +556 -714
  84. package/lib/session/dashboard.js +385 -0
  85. package/lib/session/dispatch.js +162 -170
  86. package/lib/session/files-pane.js +76 -44
  87. package/lib/session/items.js +6 -17
  88. package/lib/session/lifecycle.js +393 -466
  89. package/lib/session/load.js +1 -14
  90. package/lib/session/navigation.js +2 -2
  91. package/lib/session/npm.js +403 -420
  92. package/lib/session/ops.js +5 -13
  93. package/lib/session/pointer.js +16 -14
  94. package/lib/session/progress.js +39 -30
  95. package/lib/session/review.js +1 -3
  96. package/lib/session/tasks.js +276 -235
  97. package/lib/session/templates.js +51 -59
  98. package/lib/session/terminal.js +85 -132
  99. package/lib/session/view.js +47 -81
  100. package/lib/session/watch.js +196 -212
  101. package/lib/session.js +81 -161
  102. package/lib/update.js +44 -111
  103. package/lib/utilities.js +46 -88
  104. package/lib/wrap.js +29 -55
  105. package/package.json +1 -1
@@ -1,47 +1,31 @@
1
1
  'use strict';
2
2
 
3
3
  const detect = require('../detect.js');
4
- const { detectLang } = detect;
4
+ const diffLib = require('../diff/diff.js');
5
5
  const primitives = require('./primitives.js');
6
6
  const diff = require('./diff.js');
7
7
 
8
- const { paneResult } = primitives;
9
- const { paintDiffLine, codeCursorInRows, codeEditHits } = diff;
10
- const { expandSoftRows } = diff;
11
- const diffLib = require('../diff/diff.js');
8
+ const { detectLang } = detect;
12
9
  const { sameHunk } = diffLib;
10
+ const { paneResult } = primitives;
11
+ const { paintDiffLine, expandSoftRows, codeHits } = diff;
13
12
 
14
13
  const paintBodyUnit = (view, width, color) => {
15
- const lines = view.unitLines ?? [];
16
- const item = view.item;
14
+ const { item, codeOverlay: overlay } = view;
17
15
  const lang = detectLang(view.reviewPath ?? '');
18
- const overlay = view.codeOverlay;
19
16
  const editing =
20
- view.mode === 'compose' && overlay && typeof overlay.cursor === 'number';
21
- const picked = editing ? overlay.scrollCol : 0;
22
- const scrollCol = picked ?? 0;
17
+ view.mode === 'compose' && typeof overlay?.cursor === 'number';
18
+ const scrollCol = (editing ? overlay.scrollCol : 0) ?? 0;
19
+ const lines = view.unitLines ?? [];
23
20
  const source = editing ? lines : expandSoftRows(lines, width, false, true);
24
- const { length } = source;
25
- const body = new Array(length);
26
- for (let i = 0; i < length; i++) {
27
- const line = source[i];
28
- const isBlock = item && sameHunk(line.item, item);
29
- const row = { ...line };
30
- if (editing) {
31
- row.mark = ' ';
32
- } else if ((row.type === 'add' || row.type === 'del') && !isBlock) {
33
- row.mark = ' ';
34
- }
35
- body[i] = paintDiffLine(row, width, color, row.origin, lang, scrollCol);
36
- }
37
- const edit = overlay ? overlay.cursor : null;
38
- const cursor = codeCursorInRows(source, false, width, edit, scrollCol);
39
- const editHits = editing ? codeEditHits(source, false, width, scrollCol) : [];
40
- return paneResult({
41
- body,
42
- cursor,
43
- editHits,
21
+ const body = source.map((line) => {
22
+ const changed = line.type === 'add' || line.type === 'del';
23
+ const outside = changed && !(item && sameHunk(line.item, item));
24
+ const row = editing || outside ? { ...line, mark: ' ' } : line;
25
+ return paintDiffLine(row, width, color, row.origin, lang, scrollCol);
44
26
  });
27
+ const hits = codeHits(source, false, width, overlay, editing, scrollCol);
28
+ return paneResult({ body, ...hits });
45
29
  };
46
30
 
47
31
  module.exports = { paintBodyUnit };
@@ -4,7 +4,7 @@ const ansi = require('./ansi.js');
4
4
  const { stripAnsi } = ansi;
5
5
  const commands = require('./npm-commands.js');
6
6
 
7
- const { withoutNodeFrame, indentStack, relativize } = commands;
7
+ const { traceLine, relativize } = commands;
8
8
 
9
9
  const summary = () => ({
10
10
  tests: null,
@@ -23,8 +23,10 @@ const problem = (fields = {}) => {
23
23
  severity: fields.severity || 'error',
24
24
  name: fields.name || '',
25
25
  message: fields.message || '',
26
+ key: fields.key || '',
26
27
  trace,
27
28
  traces,
29
+ related: fields.related || [],
28
30
  duration: fields.duration || '',
29
31
  code: fields.code || '',
30
32
  operator: fields.operator || '',
@@ -44,15 +46,16 @@ const report = (tool, fields = {}) => ({
44
46
  });
45
47
 
46
48
  const userTrace = (text, root) => {
47
- const lines = stripAnsi(`${text ?? ''}`).split(/\r?\n/);
48
49
  const kept = [];
49
- for (const line of lines) {
50
- const next = withoutNodeFrame(line);
51
- if (next === null) continue;
52
- kept.push(relativize(indentStack(next), root).trimEnd());
50
+ for (const line of stripAnsi(`${text ?? ''}`).split(/\r?\n/)) {
51
+ const next = traceLine(line, root);
52
+ if (next !== null) kept.push(next.trimEnd());
53
53
  }
54
- while (kept.length && kept[kept.length - 1] === '') kept.pop();
55
- return kept.join('\n').trim();
54
+ let start = 0;
55
+ while (start < kept.length && kept[start].trim() === '') start += 1;
56
+ let end = kept.length;
57
+ while (end > start && kept[end - 1] === '') end -= 1;
58
+ return kept.slice(start, end).join('\n');
56
59
  };
57
60
 
58
61
  const normalizeMessage = (message) => {
@@ -60,33 +63,106 @@ const normalizeMessage = (message) => {
60
63
  return text.replace(/\s+\S+:\d+:\d+$/, '');
61
64
  };
62
65
 
63
- const groupKey = (item) =>
64
- [
66
+ // Build identity from raw diagnostics, before userTrace removes Node frames.
67
+ const errorKey = (fields, stack, root) => {
68
+ const text = stripAnsi(fields.message || '');
69
+ const header = /^([\w.]*Error)(?: \[[^\]]+\])?:\s*/.exec(text);
70
+ const type = fields.type || (header ? header[1] : '');
71
+ const message = header ? text.slice(header[0].length) : text;
72
+ let origin = '';
73
+ for (const row of stripAnsi(stack).split(/\r?\n/)) {
74
+ const line = row.trim().replace(/^at\s+/, '');
75
+ if (/^[\w.]*Error(?: \[[^\]]+\])?:/.test(line)) continue;
76
+ if (line.includes('node:') || !/:\d+:\d+\)?$/.test(line)) continue;
77
+ const location = line.replace(/^.*\((.*:\d+:\d+)\)$/, '$1');
78
+ origin = relativize(location.replace(/file:\/\//g, ''), root).replace(
79
+ /\\/g,
80
+ '/',
81
+ );
82
+ break;
83
+ }
84
+ return JSON.stringify([
85
+ fields.severity || 'error',
86
+ type,
87
+ fields.code || '',
88
+ fields.operator || '',
89
+ message.replace(/\r\n/g, '\n').trim(),
90
+ fields.expected || '',
91
+ fields.actual || '',
92
+ origin,
93
+ ]);
94
+ };
95
+
96
+ const normalizeTrace = (trace) => {
97
+ if (!trace) return '';
98
+ return `${trace}`
99
+ .trim()
100
+ .split(/\r?\n/)
101
+ .map((line) => line.trim())
102
+ .filter(Boolean)
103
+ .map((line) => line.replace(/\\/g, '/').replace(/:\d+(?::\d+)?/g, ':X:Y'))
104
+ .join('\n');
105
+ };
106
+
107
+ const groupKey = (item) => {
108
+ if (item.key) return item.key;
109
+ const trace = normalizeTrace(item.trace);
110
+ return [
65
111
  item.severity,
66
112
  item.code,
67
113
  item.operator,
68
- item.expected,
69
- item.actual,
70
- normalizeMessage(item.message),
114
+ trace || normalizeMessage(item.message),
71
115
  ].join('\0');
116
+ };
117
+
118
+ const relatedProblem = (item, group) => {
119
+ const frames = item.trace.split(/\r?\n/).map((line) => line.trim());
120
+ const sample = (group.traces[0] || '')
121
+ .split(/\r?\n/)
122
+ .map((line) => line.trim());
123
+ let index = 0;
124
+ while (index < frames.length && frames[index] === sample[index]) index++;
125
+ if (index === frames.length && item.name === group.name) return '';
126
+ const rows = [item.name];
127
+ const branch = frames[index];
128
+ const last = frames.at(-1) || '';
129
+ let location = '';
130
+ if (/:\d+:\d+\)?$/.test(last)) {
131
+ const inner = last.replace(/^.*\((.*:\d+:\d+)\)$/, '$1');
132
+ location = inner.replace(/^at\s+/, '');
133
+ }
134
+ if (branch) {
135
+ let frame = branch.replace(/^at\s+/, '');
136
+ if (index === frames.length - 1 && location) frame = location;
137
+ rows.push(frame);
138
+ }
139
+ if (index < frames.length - 1 && location) rows.push(location);
140
+ return rows.filter(Boolean).join(' | ');
141
+ };
72
142
 
73
143
  const groupProblems = (problems) => {
74
144
  const groups = new Map();
75
- const order = [];
76
145
  for (const item of problems) {
77
146
  const key = groupKey(item);
78
147
  let group = groups.get(key);
79
148
  if (!group) {
80
- group = problem({ ...item, count: 0, traces: [], omitted: 0 });
149
+ group = problem({ ...item, count: 0, traces: [], related: [] });
81
150
  groups.set(key, group);
82
- order.push(group);
83
151
  }
84
152
  group.count += 1;
85
- if (item.trace && group.traces.length < 3) group.traces.push(item.trace);
153
+ if (item.trace && !group.traces.length) {
154
+ group.traces.push(item.trace);
155
+ } else if (group.count > 1 && group.related.length < 2) {
156
+ const related = relatedProblem(item, group);
157
+ if (related && !group.related.includes(related)) {
158
+ group.related.push(related);
159
+ }
160
+ }
86
161
  }
162
+ const order = [...groups.values()];
87
163
  for (const group of order) {
88
164
  group.trace = group.traces[0] || '';
89
- group.omitted = Math.max(0, group.count - group.traces.length);
165
+ group.omitted = Math.max(0, group.count - 1 - group.related.length);
90
166
  }
91
167
  return order;
92
168
  };
@@ -98,23 +174,25 @@ const groupReport = (item) =>
98
174
  problems: groupProblems(item.problems),
99
175
  });
100
176
 
101
- const reduceStderr = (text, root) => userTrace(text, root);
177
+ const toolFailed = (item) => {
178
+ const stats = item.summary;
179
+ if (stats.failed || stats.errors) return true;
180
+ return item.problems.some((entry) => entry.severity !== 'warning');
181
+ };
102
182
 
103
- const shortTail = (text) => {
104
- const lines = `${text ?? ''}`.trim().split(/\r?\n/);
105
- if (lines.length <= 20) return lines.join('\n');
106
- return lines.slice(-20).join('\n');
183
+ const keepReport = (item) => {
184
+ const stats = item.summary;
185
+ if (stats.tests || stats.passed || stats.failed || stats.skipped) return true;
186
+ return !!(stats.errors || stats.warnings) || item.problems.length > 0;
107
187
  };
108
188
 
109
189
  const envelope = (reports, exit, stderr) => {
110
- const status = exit === null || exit === undefined ? 1 : exit;
111
- const ok = status === 0;
112
- const items = new Array(reports.length);
113
- for (let i = 0; i < reports.length; i++) {
114
- const item = reports[i];
115
- items[i] = report(item.tool, { ...item, ok });
116
- }
117
- return { ok, exit: status, reports: items, stderr: stderr || '' };
190
+ const status = exit ?? 1;
191
+ const kept = reports.filter(keepReport);
192
+ const items = kept.map((item) =>
193
+ report(item.tool, { ...item, ok: !toolFailed(item) }),
194
+ );
195
+ return { ok: status === 0, exit: status, reports: items, stderr };
118
196
  };
119
197
 
120
198
  module.exports = {
@@ -122,8 +200,7 @@ module.exports = {
122
200
  problem,
123
201
  report,
124
202
  userTrace,
203
+ errorKey,
125
204
  groupReport,
126
- reduceStderr,
127
- shortTail,
128
205
  envelope,
129
206
  };
@@ -6,31 +6,26 @@ const model = require('./report-model.js');
6
6
  const testParse = require('./report-test.js');
7
7
  const toolParse = require('./report-tools.js');
8
8
 
9
- const { problem, report, groupReport } = model;
10
- const { reduceStderr, shortTail, envelope } = model;
9
+ const { problem, report, userTrace, groupReport, envelope } = model;
11
10
  const { parseTap, parseNodeJsonl, parseTestJson, parseSpec } = testParse;
12
- const { parseToolJson, parseToolText } = toolParse;
11
+ const { parseToolJson, parseToolText, stderrRest } = toolParse;
13
12
 
14
- const KNOWN = ['test', 'tsc', 'eslint', 'prettier', 'audit', 'outdated', 'ls'];
13
+ const COUNTS = ['tests', 'passed', 'failed', 'skipped', 'errors', 'warnings'];
15
14
 
16
15
  const toolHint = (program, args) => {
17
- const name = `${program ?? ''}`.split(/[/\\]/).pop();
18
- const list = args || [];
19
- const joined = [name, ...list].join(' ');
20
- if (name === 'eslint') return 'eslint';
21
- if (name === 'tsc') return 'tsc';
22
- if (name === 'prettier') return 'prettier';
16
+ const name = program.split(/[/\\]/).pop();
17
+ const joined = [name, ...args].join(' ');
18
+ if (name === 'eslint' || name === 'tsc' || name === 'prettier') return name;
23
19
  if (/\baudit\b/.test(joined)) return 'audit';
24
20
  if (/\boutdated\b/.test(joined)) return 'outdated';
25
- if (name === 'npm' || name === 'pnpm' || name === 'npm.cmd') {
26
- if (/\b(ls|list)\b/.test(joined)) return 'ls';
27
- }
28
- if (name === 'node' && (args || []).includes('--test')) return 'test';
21
+ const isNpm = name === 'npm' || name === 'pnpm' || name === 'npm.cmd';
22
+ if (isNpm && /\b(ls|list)\b/.test(joined)) return 'ls';
23
+ if (name === 'node' && args.includes('--test')) return 'test';
29
24
  return '';
30
25
  };
31
26
 
32
27
  const parseJson = (text) => {
33
- const trimmed = `${text ?? ''}`.trim();
28
+ const trimmed = text.trim();
34
29
  if (!trimmed || (trimmed[0] !== '{' && trimmed[0] !== '[')) return null;
35
30
  try {
36
31
  return JSON.parse(trimmed);
@@ -46,40 +41,64 @@ const parseJsonReports = (text, root) => {
46
41
  if (!data) return null;
47
42
  const test = parseTestJson(data, root);
48
43
  if (test) return [test];
49
- const tools = parseToolJson(data, root);
50
- if (tools) return tools;
51
- return [];
44
+ return parseToolJson(data, root) || [];
52
45
  };
53
46
 
54
47
  const parseReports = (text, root) => {
55
48
  const json = parseJsonReports(text, root);
56
49
  if (json) return json;
57
- const reports = [];
58
50
  const tap = parseTap(text, root);
59
- if (tap) reports.push(tap);
60
- const tools = parseToolText(text, root);
61
- for (const item of tools) reports.push(item);
62
- if (!tap) {
63
- const spec = parseSpec(text, root);
64
- if (spec) reports.push(spec);
65
- }
66
- return reports;
51
+ const spec = tap ? null : parseSpec(text, root);
52
+ const reports = [tap, ...parseToolText(text, root), spec];
53
+ return reports.filter(Boolean);
67
54
  };
68
55
 
56
+ const shortTail = (text) => text.trim().split(/\r?\n/).slice(-20).join('\n');
57
+
69
58
  const withHint = (reports, text, exit, hint) => {
70
- if (reports.length || !`${text ?? ''}`.trim() || exit === 0) return reports;
71
- const tool = KNOWN.includes(hint) ? hint : 'test';
72
- return [report(tool, { problems: [problem({ message: shortTail(text) })] })];
59
+ if (reports.length || !text.trim() || exit === 0) return reports;
60
+ const fallback = problem({ message: shortTail(text) });
61
+ return [report(hint || 'test', { problems: [fallback] })];
73
62
  };
74
63
 
75
- const buildDocument = (stdout, stderr, root, exit, hint) => {
76
- const text = stripAnsi(stdout);
77
- const reports = withHint(parseReports(text, root), text, exit, hint);
78
- const grouped = new Array(reports.length);
79
- for (let i = 0; i < reports.length; i++) {
80
- grouped[i] = groupReport(reports[i]);
64
+ const mergeSummary = (left, right) => {
65
+ const stats = { ...left };
66
+ for (const key of COUNTS) {
67
+ if (right[key] === null) continue;
68
+ stats[key] = (stats[key] ?? 0) + right[key];
69
+ }
70
+ if (!stats.duration && right.duration) stats.duration = right.duration;
71
+ return stats;
72
+ };
73
+
74
+ const mergeReports = (left, right) => {
75
+ const merged = left.slice();
76
+ for (const item of right) {
77
+ const at = merged.findIndex((entry) => entry.tool === item.tool);
78
+ if (at < 0) {
79
+ merged.push(item);
80
+ continue;
81
+ }
82
+ const prev = merged[at];
83
+ merged[at] = report(item.tool, {
84
+ summary: mergeSummary(prev.summary, item.summary),
85
+ problems: prev.problems.concat(item.problems),
86
+ });
81
87
  }
82
- return envelope(grouped, exit, reduceStderr(stderr, root));
88
+ return merged;
89
+ };
90
+
91
+ const buildDocument = (stdout, stderr, root, exit, hint) => {
92
+ const out = stripAnsi(stdout);
93
+ const err = stripAnsi(stderr);
94
+ const reports = mergeReports(
95
+ parseReports(out, root),
96
+ parseReports(err, root),
97
+ );
98
+ const text = out.trim() ? out : err;
99
+ const grouped = withHint(reports, text, exit, hint).map(groupReport);
100
+ const rest = stderrRest(err, grouped);
101
+ return envelope(grouped, exit, userTrace(rest, root));
83
102
  };
84
103
 
85
104
  module.exports = {
@@ -1,78 +1,111 @@
1
1
  'use strict';
2
2
 
3
+ const utilities = require('./utilities.js');
4
+ const { unquote } = utilities;
5
+
3
6
  const NOTICE_HEAD =
4
7
  'reslop: reduced output; passing results are omitted ' +
5
8
  'and similar problems are grouped';
6
9
 
7
- const NOTICE = [
8
- NOTICE_HEAD,
9
- 'If reduced output hides information needed for debugging,',
10
- 'rerun the same command with RESLOP_OUTPUT=raw.',
11
- ].join('\n');
10
+ const notice = (rawFile) => {
11
+ const target = rawFile
12
+ ? `the raw log \`.log/${rawFile}\``
13
+ : 'the matching raw log under `.log/`';
14
+ const hint = `Agent: if this report hides a needed detail, read ${target}.`;
15
+ return `${NOTICE_HEAD}\n${hint}`;
16
+ };
12
17
 
13
- const SUMMARY_ROWS = 6;
14
- const PROBLEM_FIELDS = 7;
18
+ const addRow = (rows, key, value) => {
19
+ if (value === null || value === undefined || value === '') return;
20
+ rows.push([key, `${value}`]);
21
+ };
15
22
 
16
- const writeRow = (rows, index, key, value) => {
17
- if (value === null || value === undefined || value === '') return index;
18
- rows[index] = [key, `${value}`];
19
- return index + 1;
23
+ const isComparison = (line) => {
24
+ const match = /^(.*) (?:!==|===|!=|==) (\S.*)$/.exec(line.trim());
25
+ return !!match && match[1].trim() !== '';
20
26
  };
21
27
 
22
- const summaryRows = (stats) => {
23
- const rows = new Array(SUMMARY_ROWS);
24
- let index = 0;
25
- index = writeRow(rows, index, 'passed', stats.passed);
26
- index = writeRow(rows, index, 'failed', stats.failed);
27
- index = writeRow(rows, index, 'skipped', stats.skipped);
28
- index = writeRow(rows, index, 'errors', stats.errors);
29
- index = writeRow(rows, index, 'warnings', stats.warnings);
30
- index = writeRow(rows, index, 'duration', stats.duration);
31
- rows.length = index;
32
- return rows;
28
+ const isDiffHeader = (line) => {
29
+ const text = line.trim().replace(/^[+-]\s*/, '');
30
+ return /^actual\s+[+-]?\s*expected$/.test(text);
33
31
  };
34
32
 
35
- const problemRows = (item) => {
36
- const extra = item.extra;
37
- let size = PROBLEM_FIELDS + extra.length;
38
- if (item.count > 1) size += 1;
39
- if (item.omitted > 0) size += 1;
40
- const rows = new Array(size);
41
- let index = 0;
42
- index = writeRow(rows, index, 'severity', item.severity);
43
- index = writeRow(rows, index, 'duration', item.duration);
44
- index = writeRow(rows, index, 'code', item.code);
45
- index = writeRow(rows, index, 'operator', item.operator);
46
- index = writeRow(rows, index, 'expected', item.expected);
47
- index = writeRow(rows, index, 'actual', item.actual);
48
- index = writeRow(rows, index, 'message', item.message);
49
- for (let i = 0; i < extra.length; i++) {
50
- const pair = extra[i];
51
- index = writeRow(rows, index, pair[0], pair[1]);
33
+ const isMarkedDiff = (line) => /^[+-](?:\s|$)/.test(line.trim());
34
+
35
+ const sameValue = (line, value) => {
36
+ if (!value) return false;
37
+ const text = line.trim();
38
+ return text === value || unquote(text) === value;
39
+ };
40
+
41
+ const startsDiff = (line, item) =>
42
+ isComparison(line) ||
43
+ isDiffHeader(line) ||
44
+ isMarkedDiff(line) ||
45
+ sameValue(line, item.expected) ||
46
+ sameValue(line, item.actual);
47
+
48
+ const displayMessage = (item) => {
49
+ const values = item.expected !== '' || item.actual !== '';
50
+ const lines = [];
51
+ for (const line of `${item.message}`.split('\n')) {
52
+ if (line.trim() === '') continue;
53
+ if (values && startsDiff(line, item)) break;
54
+ lines.push(line);
52
55
  }
53
- if (item.count > 1) index = writeRow(rows, index, 'count', item.count);
54
- if (item.omitted > 0) {
55
- index = writeRow(rows, index, 'omitted', item.omitted);
56
+ if (lines.length && lines[0].trimEnd().endsWith(':')) {
57
+ lines[0] = lines[0].trimEnd().slice(0, -1);
56
58
  }
57
- rows.length = index;
59
+ if (lines.length && lines[0].trim() === '') lines.shift();
60
+ return lines.join('\n');
61
+ };
62
+
63
+ const summaryRows = (stats) => {
64
+ const rows = [];
65
+ addRow(rows, 'passed', stats.passed);
66
+ addRow(rows, 'failed', stats.failed);
67
+ addRow(rows, 'skipped', stats.skipped);
68
+ addRow(rows, 'errors', stats.errors);
69
+ addRow(rows, 'warnings', stats.warnings);
70
+ addRow(rows, 'duration', stats.duration);
58
71
  return rows;
59
72
  };
60
73
 
61
- const markdownValue = (value) => {
62
- const text = `${value}`;
63
- if (!text.includes('\n')) return text;
64
- return `\n${text}`;
74
+ const problemRows = (item) => {
75
+ const rows = [];
76
+ if (item.severity !== 'error') addRow(rows, 'severity', item.severity);
77
+ addRow(rows, 'duration', item.duration);
78
+ addRow(rows, 'code', item.code);
79
+ addRow(rows, 'operator', item.operator);
80
+ addRow(rows, 'expected', item.expected);
81
+ addRow(rows, 'actual', item.actual);
82
+ for (const pair of item.extra) addRow(rows, pair[0], pair[1]);
83
+ if (item.count > 1) addRow(rows, 'count', item.count);
84
+ if (item.omitted > 0) addRow(rows, 'omitted', item.omitted);
85
+ return rows;
65
86
  };
66
87
 
88
+ const prefixLines = (prefix, text) =>
89
+ `${text}`
90
+ .replace(/\n+$/, '')
91
+ .split('\n')
92
+ .map((line) => `${prefix}${line}`);
93
+
94
+ const quoteLines = (text) => prefixLines('│ ', text);
95
+
67
96
  const markdownProblem = (item) => {
68
97
  const title = item.name || item.message || item.code || 'problem';
69
98
  const lines = ['', `## ${title}`, ''];
70
99
  for (const row of problemRows(item)) {
71
- lines.push(`- ${row[0]}: ${markdownValue(row[1])}`);
100
+ const value = row[1].includes('\n') ? `\n${row[1]}` : row[1];
101
+ lines.push(`- ${row[0]}: ${value}`);
72
102
  }
73
- for (const trace of item.traces) {
74
- if (!trace) continue;
75
- lines.push(`- trace:\n${trace}`);
103
+ const message = displayMessage(item);
104
+ if (message) lines.push('', ...prefixLines('> ', message), '');
105
+ if (item.trace) lines.push(`- trace:\n${item.trace}`);
106
+ if (item.related.length) {
107
+ lines.push('- related:');
108
+ for (const row of item.related) lines.push(` - ${row}`);
76
109
  }
77
110
  return lines;
78
111
  };
@@ -82,23 +115,33 @@ const markdownReport = (item) => {
82
115
  for (const row of summaryRows(item.summary)) {
83
116
  lines.push(`${row[0]}: ${row[1]}`);
84
117
  }
85
- for (const problem of item.problems) {
86
- lines.push(...markdownProblem(problem));
87
- }
118
+ for (const entry of item.problems) lines.push(...markdownProblem(entry));
88
119
  return lines;
89
120
  };
90
121
 
91
- const renderMarkdown = (doc) => {
92
- const lines = [NOTICE, '', `exit: ${doc.exit}`];
122
+ const renderDocument = (doc, rawFile) => {
123
+ const lines = [notice(rawFile)];
93
124
  for (const item of doc.reports) lines.push(...markdownReport(item));
94
125
  if (doc.stderr) lines.push('', '# stderr', '', doc.stderr);
126
+ lines.push('', `exit: ${doc.exit}`);
95
127
  return `${lines.join('\n').replace(/\n+$/, '')}\n`;
96
128
  };
97
129
 
98
- const renderDocument = (doc) => renderMarkdown(doc);
130
+ const loggedText = (text, rawFile) => {
131
+ const body = `${text ?? ''}`;
132
+ if (body.startsWith(NOTICE_HEAD)) return body;
133
+ const head = notice(rawFile);
134
+ if (!body.trim()) return `${head}\n`;
135
+ return `${head}\n\n${body}`;
136
+ };
99
137
 
100
138
  module.exports = {
101
- NOTICE,
102
- renderMarkdown,
139
+ NOTICE_HEAD,
140
+ notice,
141
+ summaryRows,
142
+ problemRows,
143
+ displayMessage,
144
+ quoteLines,
145
+ loggedText,
103
146
  renderDocument,
104
147
  };