gitxplain 0.1.0 → 0.1.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.
@@ -11,7 +11,15 @@ const ANSI = {
11
11
  };
12
12
 
13
13
  function supportsColor() {
14
- return Boolean(process.stdout?.isTTY) && process.env.NO_COLOR == null;
14
+ if (process.env.FORCE_COLOR != null && process.env.FORCE_COLOR !== "0") {
15
+ return true;
16
+ }
17
+
18
+ if (process.env.NO_COLOR != null) {
19
+ return false;
20
+ }
21
+
22
+ return Boolean(process.stdout?.isTTY);
15
23
  }
16
24
 
17
25
  function colorize(text, color) {
@@ -22,35 +30,199 @@ function colorize(text, color) {
22
30
  return `${color}${text}${ANSI.reset}`;
23
31
  }
24
32
 
33
+ function stripInlineMarkdown(text) {
34
+ return text
35
+ .replace(/`([^`]+)`/g, "$1")
36
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
37
+ .replace(/__([^_]+)__/g, "$1")
38
+ .replace(/\*([^*]+)\*/g, "$1")
39
+ .replace(/_([^_]+)_/g, "$1")
40
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)")
41
+ .trimEnd();
42
+ }
43
+
44
+ function normalizeMarkdownLine(line, state) {
45
+ const trimmed = line.trim();
46
+
47
+ if (/^```/.test(trimmed)) {
48
+ state.inCodeBlock = !state.inCodeBlock;
49
+ return "";
50
+ }
51
+
52
+ if (state.inCodeBlock) {
53
+ return ` ${line.replace(/^\s*/, "")}`;
54
+ }
55
+
56
+ if (/^---+$/.test(trimmed) || /^\*\*\*+$/.test(trimmed)) {
57
+ return "";
58
+ }
59
+
60
+ let normalizedHeading = trimmed
61
+ .replace(/^#{1,6}\s+/, "")
62
+ .replace(/^([0-9]+\.)\s+/, "");
63
+ normalizedHeading = stripInlineMarkdown(normalizedHeading).replace(/:\s*$/, "").trim();
64
+
65
+ if (
66
+ /^(summary|issues? fixed|issue|root cause|fix(?: explanation)?|impact|risk level|severity|technical breakdown|full analysis|line-by-line code walkthrough|code review|security review|security findings|review findings|suggestions|recommended mitigations)$/i.test(
67
+ normalizedHeading
68
+ )
69
+ ) {
70
+ return `${normalizedHeading}:`;
71
+ }
72
+
73
+ if (/^>\s*/.test(trimmed)) {
74
+ return stripInlineMarkdown(trimmed.replace(/^>\s*/, ""));
75
+ }
76
+
77
+ const bulletMatch = line.match(/^(\s*)([-*+]|\d+\.)\s+(.*)$/);
78
+ if (bulletMatch) {
79
+ const [, indent, marker, content] = bulletMatch;
80
+ return `${indent}${marker} ${stripInlineMarkdown(content)}`;
81
+ }
82
+
83
+ return stripInlineMarkdown(line);
84
+ }
85
+
25
86
  function formatTargetLabel(commitData) {
26
87
  return commitData.analysisType === "range" ? "Range" : "Commit";
27
88
  }
28
89
 
29
- function highlightLine(line) {
30
- if (/^([0-9]+\.)?\s*(Summary|Issue|Root Cause|Fix|Impact|Risk Level|Technical Breakdown|Security Findings|Suggestions|Review Findings):/i.test(line)) {
31
- return colorize(line, ANSI.bold + ANSI.cyan);
90
+ function normalizeHeading(line) {
91
+ const match = line.match(/^([0-9]+\.)?\s*(Summary|Issues? Fixed|Issue|Root Cause|Fix(?: Explanation)?|Impact|Risk Level|Severity|Technical Breakdown|Full Analysis|Line-by-Line Code Walkthrough|Code Review|Security Review|Security Findings|Review Findings|Suggestions|Recommended Mitigations)\s*:?\s*$/i);
92
+
93
+ if (!match) {
94
+ return null;
95
+ }
96
+
97
+ return `${match[2]}:`;
98
+ }
99
+
100
+ function isFileHeading(line) {
101
+ return /^(?:File|Path)\s*:/i.test(line) || /^[A-Za-z0-9_./-]+\.[A-Za-z0-9]+:\s*$/.test(line);
102
+ }
103
+
104
+ function classifyTone(line) {
105
+ if (/^\s*low(?:\b|[.:])/i.test(line)) {
106
+ return "good";
107
+ }
108
+
109
+ if (/^\s*medium(?:\b|[.:])/i.test(line)) {
110
+ return "neutral";
32
111
  }
33
112
 
34
- if (/risk/i.test(line) && /\blow\b/i.test(line)) {
35
- return colorize(line, ANSI.green);
113
+ if (/^\s*high(?:\b|[.:])/i.test(line)) {
114
+ return "bad";
36
115
  }
37
116
 
38
- if (/risk/i.test(line) && /\bmedium\b/i.test(line)) {
39
- return colorize(line, ANSI.yellow);
117
+ if (
118
+ /\b(no significant findings|no security findings|none apparent|looks good|safe|improved|improvement|fixed|resolved|successful|passes?|low risk|low severity)\b/i.test(
119
+ line
120
+ )
121
+ ) {
122
+ return "good";
40
123
  }
41
124
 
42
- if (/risk/i.test(line) && /\bhigh\b/i.test(line)) {
43
- return colorize(line, ANSI.red);
125
+ if (
126
+ /\b(issue|issues|bug|broken|failure|failing|risk|risky|severity|vulnerability|insecure|regression|warning|problem|bad|missing|error|high risk|high severity)\b/i.test(
127
+ line
128
+ )
129
+ ) {
130
+ return "bad";
44
131
  }
45
132
 
133
+ if (/\b(suggestion|consider|follow-up|todo|medium risk|medium severity)\b/i.test(line)) {
134
+ return "neutral";
135
+ }
136
+
137
+ return null;
138
+ }
139
+
140
+ function colorizeByTone(line, tone) {
46
141
  return line;
47
142
  }
48
143
 
144
+ function formatBulletLine(line) {
145
+ const match = line.match(/^(\s*)([-*]|\d+\.)\s+(.*)$/);
146
+
147
+ if (!match) {
148
+ return null;
149
+ }
150
+
151
+ const [, indent, marker, content] = match;
152
+ return `${indent}${colorize(marker, ANSI.cyan)} ${content}`;
153
+ }
154
+
155
+ function formatSeverityLine(line) {
156
+ if (/\brisk level\b|\bseverity\b/i.test(line) === false) {
157
+ return null;
158
+ }
159
+
160
+ return line;
161
+ }
162
+
163
+ function formatLine(line) {
164
+ const trimmed = line.trim();
165
+
166
+ if (trimmed === "") {
167
+ return "";
168
+ }
169
+
170
+ const normalizedHeading = normalizeHeading(trimmed);
171
+
172
+ if (normalizedHeading) {
173
+ return colorize(normalizedHeading, ANSI.bold + ANSI.cyan);
174
+ }
175
+
176
+ if (isFileHeading(trimmed)) {
177
+ return colorize(trimmed, ANSI.bold + ANSI.cyan);
178
+ }
179
+
180
+ const bulletLine = formatBulletLine(line);
181
+
182
+ if (bulletLine) {
183
+ return bulletLine;
184
+ }
185
+
186
+ const severityLine = formatSeverityLine(trimmed);
187
+
188
+ if (severityLine) {
189
+ return severityLine;
190
+ }
191
+
192
+ return colorizeByTone(line, classifyTone(line));
193
+ }
194
+
49
195
  function formatExplanation(explanation) {
50
- return explanation
196
+ const state = { inCodeBlock: false };
197
+ const lines = explanation
198
+ .replaceAll("\r\n", "\n")
51
199
  .split("\n")
52
- .map((line) => highlightLine(line))
53
- .join("\n");
200
+ .map((line) => normalizeMarkdownLine(line, state));
201
+ const formatted = [];
202
+ let previousWasBlank = false;
203
+
204
+ for (const line of lines) {
205
+ const trimmed = line.trim();
206
+ const formattedLine = formatLine(line);
207
+ const isHeading = normalizeHeading(trimmed) != null || isFileHeading(trimmed);
208
+
209
+ if (trimmed === "") {
210
+ if (!previousWasBlank && formatted.length > 0) {
211
+ formatted.push("");
212
+ }
213
+ previousWasBlank = true;
214
+ continue;
215
+ }
216
+
217
+ if (isHeading && formatted.length > 0 && !previousWasBlank) {
218
+ formatted.push("");
219
+ }
220
+
221
+ formatted.push(formattedLine);
222
+ previousWasBlank = false;
223
+ }
224
+
225
+ return formatted.join("\n").trimEnd();
54
226
  }
55
227
 
56
228
  export function formatPreamble({ mode, commitData, options, promptMeta }) {