如何封装 @babel:code-frame ?
# 0.前言
本篇博客快速上手 @babel/code-frame
的实现原理,示例仓库 (opens new window)
# 1.最终效果?
import { describe, expect, it } from "vitest";
const { codeFrameColumns } = require("@babel/code-frame");
const code = `
const a = 1;
const b = 2;
`;
describe("使用 code-frame 实现错误提示", () => {
it("01.打印出标记相应位置代码的 code frame", () => {
const res = codeFrameColumns(
code,
{
start: { line: 2, column: 1 },
end: { line: 3, column: 5 },
},
{
/* 此部分为 options */
highlightCode: true,
message: "这里出错了",
/* forceColor: true, */
},
);
expect(res).toMatchInlineSnapshot(`
" 1 |
> 2 | const a = 1;
| ^^^^^^^^^^^^
> 3 | const b = 2;
| ^^^^^ 这里出错了
4 |"
`);
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
在 terminal
中显示:
# 2. 分析实现方案?
详细使用见官方说明文档:https://babel.dev/docs/babel-code-frame
需要实现的 API
:codeFrameColumns
const res = codeFrameColumns(
code,
{
start: { line: 2, column: 1 },
end: { line: 3, column: 5 },
},
{
/* 此部分为 options */
highlightCode: true,
message: "这里出错了",
forceColor: true,
linesAbove: 2 /* 打印当前上2行 */,
linesBelow: 3 /* 打印当前下2行 */,
},
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
其中:
- 第一个参数:输入原始的
code
。 - 第二个参数:提示的范围。
- 第三个参数:
opts
配置选项。
# 3. 数据流向视角
这里并不想简单的把源码的实现给贴上来,而是思考如何如果让我来实现这个函数,数据是如何流转的?
下面以 数据流向 的视角来概述整个代码的功能。
输入 code
的代码是基于 ES6
的模板父子串实现的:
`
const a = 1;
const b = 2;
`;
2
3
4
实际获取到的是:
"\nconst a = 1;\nconst b = 2;\n";
通过正则的手段,将字符串处理如下:
["", "const a = 1;", "const b = 2;", ""];
需要构造一个临时的结构,将需要修改的行标识出来:
{
"2": [1,12],
"3": [0,5],
}
2
3
4
根据临时结构对数组进行如下修改:
- 添加序号(
gutter
) ,如> rowNumber |
。 - 添加一行
\n | ^^^^^^^^^^^^
其中^
波浪行的长度取决于col
。 - 添加颜色。
示例结构:
[
" 1 |",
"> 2 | const a = 1;\n | ^^^^^^^^^^^^",
"> 3 | const b = 2;\n | ^^^^^ 这里出错了",
" 4 |",
].join("\n");
2
3
4
5
6
# 4.函数设计如下:
通过上述数据流基本上很清晰了,本节将从函数的实现步骤进行分析:
首先将
rawLines
转化为数组const NEWLINE = /\r\n|[\n\r\u2028\u2029]/; const lines = rawLines.split(NEWLINE);
1
2读取配置信息:
是否需要高亮?
const highlighted = opts.highlightCode;
1对
chalk
进行二次封装function getDefs(chalk) { return { gutter: chalk.grey, marker: chalk.red.bold, message: chalk.red.bold, }; } const defs = getDefs(chalk); /* 将 chalk 更加语义化 */
1
2
3
4
5
6
7
8
现在开始思考如何处理下面的数组。
[ "", "const a = 1;" /* 整行需要标识 */, "const b = 2;" /* 前5个字符需要标识 */, "", ];
1
2
3
4
5
6此时我们需要三个信息:
- 打印的函数范围?起始行数以及终止行数。
- 记录标记的第
k
行的,第i
列到第j
列。
此时,需要抽象出
getMarkerLines
函数,用于生成一个临时结构:const { start /* 需要打印的初始行数,与 aboveLines 与 belowLinse 这两个 opts 有关 */, end /* 需要打印的结束结束行数,与 aboveLines 与 belowLinse 这两个 opts 有关 */, markerLines /* key-当前行数,value-数组【startCol,len】 */, } = getMarkerLines(loc, lines, opts);
1
2
3
4
5其中,
markerLines
结构如下:{ "2": [ 1,12 ], "3": [ 0,5,], }
1
2
3
4使用正则转为数组,只截取
start
到end
即可,后续在.map
中编写核心逻辑highlightedLines.split(NEWLINE, end).slice(start, end).map(()=>{....})
1循环体内:
打印行数:
number = start + 1 + index
通过
markerLines[number]
可以判读当前行是否需要标记。为了保证打印的格式化,通过
paddingNumber
控制是否需要打印多余空格。计算需要需要的标记符重复次数:
\n
+gutter.replace(/\d/g, " ")
+"^".repeat(numberOfMarkers)
判断是否为最后一行,如果是最后一行,则打印
message
:markerLine += " " + opts.message;
1
最终处理结果如下:
[ " 1 |", ["> 2 | const a = 1;", "\n | ^^^^^^^^^^^^"].join(""), ["> 3 | const b = 2;", "\n | ^^^^^ 这里出错了"].join(""), " 4 |", ];
1
2
3
4
5
6
循环体内的代码,完整见 @babel/code-frame
代码:
let frame = highlightedLines
.split(NEWLINE, end)
.slice(start, end)
.map((line, index) => {
const number = start + 1 + index;
const paddedNumber = ` ${number}`.slice(-numberMaxWidth);
const gutter = ` ${paddedNumber} |`;
const hasMarker = markerLines[number];
const lastMarkerLine = !markerLines[number + 1];
if (hasMarker) {
let markerLine = "";
if (Array.isArray(hasMarker)) {
const markerSpacing = line
.slice(0, Math.max(hasMarker[0] - 1, 0))
.replace(/[^\t]/g, " ");
const numberOfMarkers = hasMarker[1] || 1;
markerLine = [
"\n ",
maybeHighlight(defs.gutter, gutter.replace(/\d/g, " ")),
" ",
markerSpacing,
maybeHighlight(defs.marker, "^").repeat(numberOfMarkers),
].join("");
if (lastMarkerLine && opts.message) {
markerLine += " " + maybeHighlight(defs.message, opts.message);
}
}
return [
maybeHighlight(defs.marker, ">"),
maybeHighlight(defs.gutter, gutter),
line.length > 0 ? ` ${line}` : "",
markerLine,
].join("");
} else {
return ` ${maybeHighlight(defs.gutter, gutter)}${
line.length > 0 ? ` ${line}` : ""
}`;
}
})
.join("\n");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 5. 使用 ASCII 码打印颜色
若想要在控制台内打印出颜色,可以使用 ESC
让 ASCII
码进入控制字符模式:
格式如下:
ESC
(进入控制模式) + [
(开始) + 前景色 + ;
+ 背景色
+ ;
+ 加粗 + ;
+ 下划线 + m
(结束)
在 shell
终端执行如下代码:
# 字符串格式
echo -e "\e[36;1;4mjiasheng"
# 16 进制
echo -e "\033[36;1;4mjiasheng"
# 8 进制
echo -e "\0x1b[36;4mjiasheng"
2
3
4
5
6
其中,36 - 青色 | 1 - 加粗 | 4 - 下划线
同样使用: \e[0m
可以添加去除样式,如在 jiasheng 字符串后接上一个不带格式的 wang
,可以