@atlaspack/codeframe 2.12.1-canary.3354
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/LICENSE +201 -0
- package/lib/codeframe.js +36049 -0
- package/lib/codeframe.js.map +1 -0
- package/package.json +34 -0
- package/src/codeframe.js +302 -0
- package/test/codeframe.test.js +822 -0
- package/test/fixtures/a.js +13 -0
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atlaspack/codeframe",
|
|
3
|
+
"version": "2.12.1-canary.3354+7bb54d46a",
|
|
4
|
+
"description": "Blazing fast, zero configuration web application bundler",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/atlassian-labs/atlaspack.git"
|
|
12
|
+
},
|
|
13
|
+
"main": "lib/codeframe.js",
|
|
14
|
+
"source": "src/codeframe.js",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">= 16.0.0"
|
|
17
|
+
},
|
|
18
|
+
"targets": {
|
|
19
|
+
"main": {
|
|
20
|
+
"includeNodeModules": {
|
|
21
|
+
"chalk": false
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"chalk": "^4.1.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"emphasize": "^4.2.0",
|
|
30
|
+
"slice-ansi": "^4.0.0",
|
|
31
|
+
"string-width": "^4.2.0"
|
|
32
|
+
},
|
|
33
|
+
"gitHead": "7bb54d46a00c5ba9cdbc2ee426dcbe82c8d79a3e"
|
|
34
|
+
}
|
package/src/codeframe.js
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import type {DiagnosticCodeHighlight} from '@atlaspack/diagnostic';
|
|
3
|
+
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import emphasize from 'emphasize';
|
|
6
|
+
import stringWidth from 'string-width';
|
|
7
|
+
import sliceAnsi from 'slice-ansi';
|
|
8
|
+
|
|
9
|
+
type CodeFramePadding = {|
|
|
10
|
+
before: number,
|
|
11
|
+
after: number,
|
|
12
|
+
|};
|
|
13
|
+
|
|
14
|
+
type CodeFrameOptionsInput = $Shape<CodeFrameOptions>;
|
|
15
|
+
|
|
16
|
+
type CodeFrameOptions = {|
|
|
17
|
+
useColor: boolean,
|
|
18
|
+
syntaxHighlighting: boolean,
|
|
19
|
+
maxLines: number,
|
|
20
|
+
padding: CodeFramePadding,
|
|
21
|
+
terminalWidth: number,
|
|
22
|
+
language?: string,
|
|
23
|
+
|};
|
|
24
|
+
|
|
25
|
+
const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;
|
|
26
|
+
const TAB_REPLACE_REGEX = /\t/g;
|
|
27
|
+
const TAB_REPLACEMENT = ' ';
|
|
28
|
+
const DEFAULT_TERMINAL_WIDTH = 80;
|
|
29
|
+
|
|
30
|
+
const highlightSyntax = (txt: string, lang?: string): string => {
|
|
31
|
+
if (lang) {
|
|
32
|
+
try {
|
|
33
|
+
return emphasize.highlight(lang, txt).value;
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// fallback for unknown languages...
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return emphasize.highlightAuto(txt).value;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default function codeFrame(
|
|
43
|
+
code: string,
|
|
44
|
+
highlights: Array<DiagnosticCodeHighlight>,
|
|
45
|
+
inputOpts: CodeFrameOptionsInput = {},
|
|
46
|
+
): string {
|
|
47
|
+
if (highlights.length < 1) return '';
|
|
48
|
+
|
|
49
|
+
let opts: CodeFrameOptions = {
|
|
50
|
+
useColor: !!inputOpts.useColor,
|
|
51
|
+
syntaxHighlighting: !!inputOpts.syntaxHighlighting,
|
|
52
|
+
language: inputOpts.language,
|
|
53
|
+
maxLines: inputOpts.maxLines ?? 12,
|
|
54
|
+
terminalWidth: inputOpts.terminalWidth || DEFAULT_TERMINAL_WIDTH,
|
|
55
|
+
padding: inputOpts.padding || {
|
|
56
|
+
before: 1,
|
|
57
|
+
after: 2,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Highlights messages and prefixes when colors are enabled
|
|
62
|
+
const highlighter = (s: string, bold?: boolean) => {
|
|
63
|
+
if (opts.useColor) {
|
|
64
|
+
let redString = chalk.red(s);
|
|
65
|
+
return bold ? chalk.bold(redString) : redString;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return s;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Prefix lines with the line number
|
|
72
|
+
const lineNumberPrefixer = (params: {|
|
|
73
|
+
lineNumber?: string,
|
|
74
|
+
lineNumberLength: number,
|
|
75
|
+
isHighlighted: boolean,
|
|
76
|
+
|}) => {
|
|
77
|
+
let {lineNumber, lineNumberLength, isHighlighted} = params;
|
|
78
|
+
|
|
79
|
+
return `${isHighlighted ? highlighter('>') : ' '} ${
|
|
80
|
+
lineNumber
|
|
81
|
+
? lineNumber.padStart(lineNumberLength, ' ')
|
|
82
|
+
: ' '.repeat(lineNumberLength)
|
|
83
|
+
} | `;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Make columns/lines start at 1
|
|
87
|
+
let originalHighlights = highlights;
|
|
88
|
+
highlights = highlights.map(h => {
|
|
89
|
+
return {
|
|
90
|
+
start: {
|
|
91
|
+
column: h.start.column - 1,
|
|
92
|
+
line: h.start.line - 1,
|
|
93
|
+
},
|
|
94
|
+
end: {
|
|
95
|
+
column: h.end.column - 1,
|
|
96
|
+
line: h.end.line - 1,
|
|
97
|
+
},
|
|
98
|
+
message: h.message,
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Find first and last highlight
|
|
103
|
+
let firstHighlight =
|
|
104
|
+
highlights.length > 1
|
|
105
|
+
? highlights.sort((a, b) => a.start.line - b.start.line)[0]
|
|
106
|
+
: highlights[0];
|
|
107
|
+
let lastHighlight =
|
|
108
|
+
highlights.length > 1
|
|
109
|
+
? highlights.sort((a, b) => b.end.line - a.end.line)[0]
|
|
110
|
+
: highlights[0];
|
|
111
|
+
|
|
112
|
+
// Calculate first and last line index of codeframe
|
|
113
|
+
let startLine = firstHighlight.start.line - opts.padding.before;
|
|
114
|
+
startLine = startLine < 0 ? 0 : startLine;
|
|
115
|
+
let endLineIndex = lastHighlight.end.line + opts.padding.after;
|
|
116
|
+
let tail;
|
|
117
|
+
if (endLineIndex - startLine > opts.maxLines) {
|
|
118
|
+
let maxLine = startLine + opts.maxLines - 1;
|
|
119
|
+
highlights = highlights.filter(h => h.start.line < maxLine);
|
|
120
|
+
lastHighlight = highlights[0];
|
|
121
|
+
endLineIndex = Math.min(
|
|
122
|
+
maxLine,
|
|
123
|
+
lastHighlight.end.line + opts.padding.after,
|
|
124
|
+
);
|
|
125
|
+
tail = originalHighlights.filter(h => h.start.line > endLineIndex);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let lineNumberLength = (endLineIndex + 1).toString(10).length;
|
|
129
|
+
|
|
130
|
+
// Split input into lines and highlight syntax
|
|
131
|
+
let lines = code.split(NEWLINE);
|
|
132
|
+
let syntaxHighlightedLines = (
|
|
133
|
+
opts.syntaxHighlighting ? highlightSyntax(code, opts.language) : code
|
|
134
|
+
)
|
|
135
|
+
.replace(TAB_REPLACE_REGEX, TAB_REPLACEMENT)
|
|
136
|
+
.split(NEWLINE);
|
|
137
|
+
|
|
138
|
+
// Loop over all lines and create codeframe
|
|
139
|
+
let resultLines = [];
|
|
140
|
+
for (
|
|
141
|
+
let currentLineIndex = startLine;
|
|
142
|
+
currentLineIndex < syntaxHighlightedLines.length;
|
|
143
|
+
currentLineIndex++
|
|
144
|
+
) {
|
|
145
|
+
if (currentLineIndex > endLineIndex) break;
|
|
146
|
+
if (currentLineIndex > syntaxHighlightedLines.length - 1) break;
|
|
147
|
+
|
|
148
|
+
// Find highlights that need to get rendered on the current line
|
|
149
|
+
let lineHighlights = highlights
|
|
150
|
+
.filter(
|
|
151
|
+
highlight =>
|
|
152
|
+
highlight.start.line <= currentLineIndex &&
|
|
153
|
+
highlight.end.line >= currentLineIndex,
|
|
154
|
+
)
|
|
155
|
+
.sort(
|
|
156
|
+
(a, b) =>
|
|
157
|
+
(a.start.line < currentLineIndex ? 0 : a.start.column) -
|
|
158
|
+
(b.start.line < currentLineIndex ? 0 : b.start.column),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Check if this line has a full line highlight
|
|
162
|
+
let isWholeLine =
|
|
163
|
+
lineHighlights.length &&
|
|
164
|
+
!!lineHighlights.find(
|
|
165
|
+
h => h.start.line < currentLineIndex && h.end.line > currentLineIndex,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
let lineLengthLimit =
|
|
169
|
+
opts.terminalWidth > lineNumberLength + 7
|
|
170
|
+
? opts.terminalWidth - (lineNumberLength + 5)
|
|
171
|
+
: 10;
|
|
172
|
+
|
|
173
|
+
// Split the line into line parts that will fit the provided terminal width
|
|
174
|
+
let colOffset = 0;
|
|
175
|
+
let lineEndCol = lineLengthLimit;
|
|
176
|
+
let syntaxHighlightedLine = syntaxHighlightedLines[currentLineIndex];
|
|
177
|
+
if (stringWidth(syntaxHighlightedLine) > lineLengthLimit) {
|
|
178
|
+
if (lineHighlights.length > 0) {
|
|
179
|
+
if (lineHighlights[0].start.line === currentLineIndex) {
|
|
180
|
+
colOffset = lineHighlights[0].start.column - 5;
|
|
181
|
+
} else if (lineHighlights[0].end.line === currentLineIndex) {
|
|
182
|
+
colOffset = lineHighlights[0].end.column - 5;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
colOffset = colOffset > 0 ? colOffset : 0;
|
|
187
|
+
lineEndCol = colOffset + lineLengthLimit;
|
|
188
|
+
|
|
189
|
+
syntaxHighlightedLine = sliceAnsi(
|
|
190
|
+
syntaxHighlightedLine,
|
|
191
|
+
colOffset,
|
|
192
|
+
lineEndCol,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Write the syntax highlighted line part
|
|
197
|
+
resultLines.push(
|
|
198
|
+
lineNumberPrefixer({
|
|
199
|
+
lineNumber: (currentLineIndex + 1).toString(10),
|
|
200
|
+
lineNumberLength,
|
|
201
|
+
isHighlighted: lineHighlights.length > 0,
|
|
202
|
+
}) + syntaxHighlightedLine,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
let lineWidth = stringWidth(syntaxHighlightedLine);
|
|
206
|
+
let highlightLine = '';
|
|
207
|
+
if (isWholeLine) {
|
|
208
|
+
highlightLine = highlighter('^'.repeat(lineWidth));
|
|
209
|
+
} else if (lineHighlights.length > 0) {
|
|
210
|
+
let lastCol = 0;
|
|
211
|
+
let highlight = null;
|
|
212
|
+
let highlightHasEnded = false;
|
|
213
|
+
|
|
214
|
+
for (
|
|
215
|
+
let highlightIndex = 0;
|
|
216
|
+
highlightIndex < lineHighlights.length;
|
|
217
|
+
highlightIndex++
|
|
218
|
+
) {
|
|
219
|
+
// Set highlight to current highlight
|
|
220
|
+
highlight = lineHighlights[highlightIndex];
|
|
221
|
+
highlightHasEnded = false;
|
|
222
|
+
|
|
223
|
+
// Calculate the startColumn and get the real width by doing a substring of the original
|
|
224
|
+
// line and replacing tabs with our tab replacement to support tab handling
|
|
225
|
+
let startCol = 0;
|
|
226
|
+
if (
|
|
227
|
+
highlight.start.line === currentLineIndex &&
|
|
228
|
+
highlight.start.column > colOffset
|
|
229
|
+
) {
|
|
230
|
+
startCol = lines[currentLineIndex]
|
|
231
|
+
.substring(colOffset, highlight.start.column)
|
|
232
|
+
.replace(TAB_REPLACE_REGEX, TAB_REPLACEMENT).length;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Calculate the endColumn and get the real width by doing a substring of the original
|
|
236
|
+
// line and replacing tabs with our tab replacement to support tab handling
|
|
237
|
+
let endCol = lineWidth - 1;
|
|
238
|
+
if (highlight.end.line === currentLineIndex) {
|
|
239
|
+
endCol = lines[currentLineIndex]
|
|
240
|
+
.substring(colOffset, highlight.end.column)
|
|
241
|
+
.replace(TAB_REPLACE_REGEX, TAB_REPLACEMENT).length;
|
|
242
|
+
|
|
243
|
+
// If the endCol is too big for this line part, trim it so we can handle it in the next one
|
|
244
|
+
if (endCol > lineWidth) {
|
|
245
|
+
endCol = lineWidth - 1;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
highlightHasEnded = true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// If endcol is smaller than lastCol it overlaps with another highlight and is no longer visible, we can skip those
|
|
252
|
+
if (endCol >= lastCol) {
|
|
253
|
+
let characters = endCol - startCol + 1;
|
|
254
|
+
if (startCol > lastCol) {
|
|
255
|
+
// startCol is before lastCol, so add spaces as padding before the highlight indicators
|
|
256
|
+
highlightLine += ' '.repeat(startCol - lastCol);
|
|
257
|
+
} else if (lastCol > startCol) {
|
|
258
|
+
// If last column is larger than the start, there's overlap in highlights
|
|
259
|
+
// This line adjusts the characters count to ensure we don't add too many characters
|
|
260
|
+
characters += startCol - lastCol;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Don't crash (and swallow the original message) if the diagnostic is malformed (end is before start).
|
|
264
|
+
characters = Math.max(1, characters);
|
|
265
|
+
|
|
266
|
+
// Append the highlight indicators
|
|
267
|
+
highlightLine += highlighter('^'.repeat(characters));
|
|
268
|
+
|
|
269
|
+
// Set the lastCol equal to character count between start of line part and highlight end-column
|
|
270
|
+
lastCol = endCol + 1;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// There's no point in processing more highlights if we reached the end of the line
|
|
274
|
+
if (endCol >= lineEndCol - 1) {
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Append the highlight message if the current highlights ends on this line part
|
|
280
|
+
if (highlight && highlight.message && highlightHasEnded) {
|
|
281
|
+
highlightLine += ' ' + highlighter(highlight.message, true);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (highlightLine) {
|
|
286
|
+
resultLines.push(
|
|
287
|
+
lineNumberPrefixer({
|
|
288
|
+
lineNumberLength,
|
|
289
|
+
isHighlighted: true,
|
|
290
|
+
}) + highlightLine,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let result = resultLines.join('\n');
|
|
296
|
+
|
|
297
|
+
if (tail && tail.length > 0) {
|
|
298
|
+
result += '\n\n' + codeFrame(code, tail, inputOpts);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return result;
|
|
302
|
+
}
|