@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/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
+ }
@@ -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
+ }