c063 1.6.4 → 1.7.0
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/dist/components/CodeBlock.js +15 -8
- package/dist/components/CodeLine.d.ts +1 -15
- package/dist/components/CodeLine.js +3 -1
- package/dist/components/CodeToken.d.ts +1 -16
- package/dist/components/CodeToken.js +3 -1
- package/dist/libs/index.d.ts +13 -13
- package/dist/libs/index.js +16 -1
- package/dist/libs/parser/configs/javascript.d.ts +2 -0
- package/dist/libs/parser/configs/javascript.js +82 -0
- package/dist/libs/parser/index.d.ts +4 -0
- package/dist/libs/parser/index.js +5 -0
- package/dist/types/index.d.ts +40 -2
- package/dist/utils/index.d.ts +24 -1
- package/dist/utils/index.js +138 -28
- package/package.json +1 -1
- package/src/components/CodeBlock.tsx +19 -10
- package/src/components/CodeLine.tsx +4 -1
- package/src/components/CodeToken.tsx +4 -1
- package/src/libs/index.tsx +16 -1
- package/src/libs/parser/configs/javascript.tsx +84 -0
- package/src/libs/parser/index.tsx +8 -0
- package/src/types/index.tsx +41 -14
- package/src/utils/index.tsx +165 -36
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { CodeLine } from "./CodeLine";
|
|
3
|
+
const preStyle = { margin: 0, padding: 0, overflowX: "auto" };
|
|
4
|
+
const tableStyle = { borderCollapse: "collapse", width: "100%" };
|
|
5
|
+
const rowStyle = { verticalAlign: "top" };
|
|
6
|
+
const codeCellStyle = { width: "100%" };
|
|
7
|
+
const lineNumberBaseStyle = {
|
|
8
|
+
paddingInline: "0.5rem",
|
|
9
|
+
textAlign: "right",
|
|
10
|
+
whiteSpace: "pre",
|
|
11
|
+
fontVariantNumeric: "tabular-nums",
|
|
12
|
+
color: "#888",
|
|
13
|
+
userSelect: "none",
|
|
14
|
+
};
|
|
3
15
|
/**
|
|
4
16
|
* 顯示完整程式碼區塊,支援多行語法 token 與行號顯示。
|
|
5
17
|
*
|
|
@@ -12,14 +24,9 @@ import { CodeLine } from "./CodeLine";
|
|
|
12
24
|
* @returns JSX 元素,呈現語法高亮的程式碼區塊
|
|
13
25
|
*/
|
|
14
26
|
export const CodeBlock = ({ tokenLines, showLineNumbers = true, lineNumberStyle, autoWrap, theme, ...rest }) => {
|
|
15
|
-
return (_jsx("pre", { ...rest, style:
|
|
16
|
-
|
|
17
|
-
textAlign: "right",
|
|
18
|
-
whiteSpace: "pre",
|
|
19
|
-
fontVariantNumeric: "tabular-nums",
|
|
20
|
-
color: "#888",
|
|
21
|
-
userSelect: "none",
|
|
27
|
+
return (_jsx("pre", { ...rest, style: preStyle, children: _jsx("table", { style: tableStyle, children: _jsx("tbody", { children: tokenLines.map((line, index) => (_jsxs("tr", { style: rowStyle, children: [showLineNumbers && (_jsx("td", { style: {
|
|
28
|
+
...lineNumberBaseStyle,
|
|
22
29
|
...lineNumberStyle,
|
|
23
|
-
}, children: index + 1 })), _jsx("td", { style:
|
|
30
|
+
}, children: index + 1 })), _jsx("td", { style: codeCellStyle, children: _jsx(CodeLine, { theme: theme, tokens: line, autoWrap: autoWrap }) })] }, index))) }) }) }));
|
|
24
31
|
};
|
|
25
32
|
CodeBlock.displayName = "CodeBlock";
|
|
@@ -1,16 +1,2 @@
|
|
|
1
1
|
import { CodeLineProps } from "../types/index";
|
|
2
|
-
|
|
3
|
-
* 渲染單一程式碼行,包含多個語法 token。
|
|
4
|
-
*
|
|
5
|
-
* @template T 元件渲染類型,例如 <code>、<span> 等
|
|
6
|
-
* @param props.tokens 該行所包含的語法 token 陣列
|
|
7
|
-
* @param props.style 自訂樣式,會與 whiteSpace: pre-wrap 合併
|
|
8
|
-
* @param props.theme 主題
|
|
9
|
-
* @param prop.autoWrap 是否自動換行
|
|
10
|
-
* @param rest 其他 HTMLAttributes
|
|
11
|
-
* @returns JSX 元素,呈現語法 token 的單行程式碼
|
|
12
|
-
*/
|
|
13
|
-
export declare const CodeLine: {
|
|
14
|
-
<T extends React.ElementType = "span">({ style, tokens, theme, autoWrap, ...rest }: CodeLineProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
15
|
-
displayName: string;
|
|
16
|
-
};
|
|
2
|
+
export declare const CodeLine: import("react").MemoExoticComponent<(<T extends React.ElementType = "span">({ style, tokens, theme, autoWrap, ...rest }: CodeLineProps<T>) => import("react/jsx-runtime").JSX.Element)>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from "react";
|
|
2
3
|
import { CodeToken } from "./CodeToken";
|
|
3
4
|
/**
|
|
4
5
|
* 渲染單一程式碼行,包含多個語法 token。
|
|
@@ -11,10 +12,11 @@ import { CodeToken } from "./CodeToken";
|
|
|
11
12
|
* @param rest 其他 HTMLAttributes
|
|
12
13
|
* @returns JSX 元素,呈現語法 token 的單行程式碼
|
|
13
14
|
*/
|
|
14
|
-
|
|
15
|
+
const CodeLineInner = ({ style, tokens, theme, autoWrap = true, ...rest }) => {
|
|
15
16
|
return (_jsx("code", { ...rest, style: {
|
|
16
17
|
whiteSpace: autoWrap ? "pre-wrap" : "nowrap",
|
|
17
18
|
...style,
|
|
18
19
|
}, children: tokens.map((token, index) => (_jsx(CodeToken, { theme: theme, ...token }, index))) }));
|
|
19
20
|
};
|
|
21
|
+
export const CodeLine = memo(CodeLineInner);
|
|
20
22
|
CodeLine.displayName = "CodeLine";
|
|
@@ -1,17 +1,2 @@
|
|
|
1
1
|
import { CodeTokenProps } from "../types/index";
|
|
2
|
-
|
|
3
|
-
* 渲染單一語法 token(例如關鍵字、字串、註解等),可指定標籤與樣式。
|
|
4
|
-
*
|
|
5
|
-
* @template T 元件渲染類型,預設為 <span>
|
|
6
|
-
* @param as 指定要渲染的 HTML 標籤或客製元件
|
|
7
|
-
* @param type 語法類型,用於對應不同顏色
|
|
8
|
-
* @param style 額外樣式,會與語法顏色合併
|
|
9
|
-
* @param children 顯示的程式碼字串
|
|
10
|
-
* @param theme 主題設定
|
|
11
|
-
* @param rest 其他 HTML 屬性
|
|
12
|
-
* @returns JSX 元素,顯示帶有語法顏色的 token
|
|
13
|
-
*/
|
|
14
|
-
export declare const CodeToken: {
|
|
15
|
-
<T extends React.ElementType = "span">({ as, style, children, className, type, theme, ...rest }: CodeTokenProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
16
|
-
displayName: string;
|
|
17
|
-
};
|
|
2
|
+
export declare const CodeToken: import("react").MemoExoticComponent<(<T extends React.ElementType = "span">({ as, style, children, className, type, theme, ...rest }: CodeTokenProps<T>) => import("react/jsx-runtime").JSX.Element)>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from "react";
|
|
2
3
|
import { themeMap } from "../libs/index";
|
|
3
4
|
/**
|
|
4
5
|
* 渲染單一語法 token(例如關鍵字、字串、註解等),可指定標籤與樣式。
|
|
@@ -12,11 +13,12 @@ import { themeMap } from "../libs/index";
|
|
|
12
13
|
* @param rest 其他 HTML 屬性
|
|
13
14
|
* @returns JSX 元素,顯示帶有語法顏色的 token
|
|
14
15
|
*/
|
|
15
|
-
|
|
16
|
+
const CodeTokenInner = ({ as, style, children, className, type = "default", theme = "default-dark-modern", ...rest }) => {
|
|
16
17
|
const Tag = as || "span";
|
|
17
18
|
return (_jsx(Tag, { ...rest, className: `c063-${type} ${className || ""}`, style: {
|
|
18
19
|
color: themeMap[theme][type],
|
|
19
20
|
...style,
|
|
20
21
|
}, children: children }));
|
|
21
22
|
};
|
|
23
|
+
export const CodeToken = memo(CodeTokenInner);
|
|
22
24
|
CodeToken.displayName = "CodeToken";
|
package/dist/libs/index.d.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import type { CodeTokenType, CodeTheme } from "../types";
|
|
2
2
|
declare const _themeRegistry: {
|
|
3
|
-
readonly "default-dark-modern": Record<
|
|
4
|
-
readonly "default-dark": Record<
|
|
5
|
-
readonly "default-dark-plus": Record<
|
|
6
|
-
readonly "visual-studio-light": Record<
|
|
7
|
-
readonly "default-light-plus": Record<
|
|
8
|
-
readonly "default-light-modern": Record<
|
|
9
|
-
readonly "github-light": Record<
|
|
10
|
-
readonly "github-light-default": Record<
|
|
11
|
-
readonly "github-light-colorblind": Record<
|
|
12
|
-
readonly "github-dark": Record<
|
|
13
|
-
readonly "github-dark-default": Record<
|
|
14
|
-
readonly "github-dark-colorblind": Record<
|
|
3
|
+
readonly "default-dark-modern": Record<"string" | "number" | "function" | "keyword1" | "keyword2" | "comment" | "type" | "variable" | "constant" | "brackets1" | "brackets2" | "brackets3" | "operator" | "default", import("csstype").Property.Color | undefined>;
|
|
4
|
+
readonly "default-dark": Record<"string" | "number" | "function" | "keyword1" | "keyword2" | "comment" | "type" | "variable" | "constant" | "brackets1" | "brackets2" | "brackets3" | "operator" | "default", import("csstype").Property.Color | undefined>;
|
|
5
|
+
readonly "default-dark-plus": Record<"string" | "number" | "function" | "keyword1" | "keyword2" | "comment" | "type" | "variable" | "constant" | "brackets1" | "brackets2" | "brackets3" | "operator" | "default", import("csstype").Property.Color | undefined>;
|
|
6
|
+
readonly "visual-studio-light": Record<"string" | "number" | "function" | "keyword1" | "keyword2" | "comment" | "type" | "variable" | "constant" | "brackets1" | "brackets2" | "brackets3" | "operator" | "default", import("csstype").Property.Color | undefined>;
|
|
7
|
+
readonly "default-light-plus": Record<"string" | "number" | "function" | "keyword1" | "keyword2" | "comment" | "type" | "variable" | "constant" | "brackets1" | "brackets2" | "brackets3" | "operator" | "default", import("csstype").Property.Color | undefined>;
|
|
8
|
+
readonly "default-light-modern": Record<"string" | "number" | "function" | "keyword1" | "keyword2" | "comment" | "type" | "variable" | "constant" | "brackets1" | "brackets2" | "brackets3" | "operator" | "default", import("csstype").Property.Color | undefined>;
|
|
9
|
+
readonly "github-light": Record<"string" | "number" | "function" | "keyword1" | "keyword2" | "comment" | "type" | "variable" | "constant" | "brackets1" | "brackets2" | "brackets3" | "operator" | "default", import("csstype").Property.Color | undefined>;
|
|
10
|
+
readonly "github-light-default": Record<"string" | "number" | "function" | "keyword1" | "keyword2" | "comment" | "type" | "variable" | "constant" | "brackets1" | "brackets2" | "brackets3" | "operator" | "default", import("csstype").Property.Color | undefined>;
|
|
11
|
+
readonly "github-light-colorblind": Record<"string" | "number" | "function" | "keyword1" | "keyword2" | "comment" | "type" | "variable" | "constant" | "brackets1" | "brackets2" | "brackets3" | "operator" | "default", import("csstype").Property.Color | undefined>;
|
|
12
|
+
readonly "github-dark": Record<"string" | "number" | "function" | "keyword1" | "keyword2" | "comment" | "type" | "variable" | "constant" | "brackets1" | "brackets2" | "brackets3" | "operator" | "default", import("csstype").Property.Color | undefined>;
|
|
13
|
+
readonly "github-dark-default": Record<"string" | "number" | "function" | "keyword1" | "keyword2" | "comment" | "type" | "variable" | "constant" | "brackets1" | "brackets2" | "brackets3" | "operator" | "default", import("csstype").Property.Color | undefined>;
|
|
14
|
+
readonly "github-dark-colorblind": Record<"string" | "number" | "function" | "keyword1" | "keyword2" | "comment" | "type" | "variable" | "constant" | "brackets1" | "brackets2" | "brackets3" | "operator" | "default", import("csstype").Property.Color | undefined>;
|
|
15
15
|
};
|
|
16
16
|
export declare const themes: (keyof typeof _themeRegistry)[];
|
|
17
17
|
export declare const themeMap: Record<CodeTheme, Record<CodeTokenType, React.CSSProperties["color"]>>;
|
|
18
|
-
export declare const
|
|
18
|
+
export declare const CODE_TOKEN_TYPES: readonly ["keyword1", "keyword2", "function", "string", "number", "comment", "type", "variable", "constant", "brackets1", "brackets2", "brackets3", "operator", "default"];
|
|
19
19
|
export {};
|
package/dist/libs/index.js
CHANGED
|
@@ -28,4 +28,19 @@ const _themeRegistry = {
|
|
|
28
28
|
export const themes = Object.keys(_themeRegistry);
|
|
29
29
|
// themeMap 保留給外部使用
|
|
30
30
|
export const themeMap = _themeRegistry;
|
|
31
|
-
export const
|
|
31
|
+
export const CODE_TOKEN_TYPES = [
|
|
32
|
+
"keyword1", // 關鍵字 1
|
|
33
|
+
"keyword2", // 關鍵字 2
|
|
34
|
+
"function", // 函式名稱
|
|
35
|
+
"string", // 字串常值
|
|
36
|
+
"number", // 數字常值
|
|
37
|
+
"comment", // 註解內容
|
|
38
|
+
"type", // 類型定義
|
|
39
|
+
"variable", // 變數名稱、函式名稱、類別名稱等識別符號
|
|
40
|
+
"constant", // 常數值,例如 enum 值、靜態屬性等
|
|
41
|
+
"brackets1", // 括號第一層
|
|
42
|
+
"brackets2", // 括號第二層
|
|
43
|
+
"brackets3", // 括號第三層
|
|
44
|
+
"operator", // 運算符號
|
|
45
|
+
"default", // 其他符號,例如逗號、分號、點號等
|
|
46
|
+
];
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export const config = {
|
|
2
|
+
patterns: [
|
|
3
|
+
{ type: "comment", regex: /^(\/\/.*|\/\*[\s\S]*?\*\/)/ },
|
|
4
|
+
{
|
|
5
|
+
type: "string",
|
|
6
|
+
regex: /^("([^"\\]|\\.)*"?|'([^'\\]|\\.)*'?|`([^`\\]|\\.)*`?)/,
|
|
7
|
+
},
|
|
8
|
+
{ type: "number", regex: /^\d+(\.\d+)?/ },
|
|
9
|
+
{
|
|
10
|
+
type: "operator",
|
|
11
|
+
regex: /^(===|==|!==|!=|<=|>=|=>|\.\.\.|&&|\|\||\+\+|--|\+=|-=|\*=|\/=|\+=|&=|\|=|\^=|<<=|>>=|>>>=|[+\-*/%=&|^~<>!?:,;.])/,
|
|
12
|
+
},
|
|
13
|
+
{ type: "brackets1", regex: /^[\(\)\[\]\{\}]/ },
|
|
14
|
+
{ type: "variable", regex: /^[a-zA-Z_$][a-zA-Z0-9_$]*/ },
|
|
15
|
+
{ type: "default", regex: /^[ \t\r]+/ },
|
|
16
|
+
],
|
|
17
|
+
keywords1: new Set([
|
|
18
|
+
"const",
|
|
19
|
+
"var",
|
|
20
|
+
"let",
|
|
21
|
+
"function",
|
|
22
|
+
"class",
|
|
23
|
+
"import",
|
|
24
|
+
"export",
|
|
25
|
+
"return",
|
|
26
|
+
"if",
|
|
27
|
+
"else",
|
|
28
|
+
"for",
|
|
29
|
+
"while",
|
|
30
|
+
"do",
|
|
31
|
+
"switch",
|
|
32
|
+
"case",
|
|
33
|
+
"break",
|
|
34
|
+
"continue",
|
|
35
|
+
"try",
|
|
36
|
+
"catch",
|
|
37
|
+
"finally",
|
|
38
|
+
"throw",
|
|
39
|
+
"new",
|
|
40
|
+
"this",
|
|
41
|
+
"super",
|
|
42
|
+
"extends",
|
|
43
|
+
"implements",
|
|
44
|
+
"interface",
|
|
45
|
+
"type",
|
|
46
|
+
"enum",
|
|
47
|
+
"async",
|
|
48
|
+
"await",
|
|
49
|
+
"from",
|
|
50
|
+
"as",
|
|
51
|
+
"declare",
|
|
52
|
+
"namespace",
|
|
53
|
+
"module",
|
|
54
|
+
"public",
|
|
55
|
+
"private",
|
|
56
|
+
"protected",
|
|
57
|
+
"static",
|
|
58
|
+
"readonly",
|
|
59
|
+
"abstract",
|
|
60
|
+
]),
|
|
61
|
+
keywords2: new Set([
|
|
62
|
+
"true",
|
|
63
|
+
"false",
|
|
64
|
+
"null",
|
|
65
|
+
"undefined",
|
|
66
|
+
"NaN",
|
|
67
|
+
"Infinity",
|
|
68
|
+
"void",
|
|
69
|
+
"typeof",
|
|
70
|
+
"instanceof",
|
|
71
|
+
"in",
|
|
72
|
+
"of",
|
|
73
|
+
"delete",
|
|
74
|
+
"this",
|
|
75
|
+
"console",
|
|
76
|
+
"window",
|
|
77
|
+
"document",
|
|
78
|
+
"global",
|
|
79
|
+
"process",
|
|
80
|
+
]),
|
|
81
|
+
detectFunctions: true,
|
|
82
|
+
};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { themes } from "../libs";
|
|
2
2
|
import { AsComponentProps, OverrideProps } from "./common";
|
|
3
|
+
import { CODE_TOKEN_TYPES } from "../libs/index";
|
|
4
|
+
import { parsableLanguages } from "../libs/parser";
|
|
3
5
|
/**
|
|
4
6
|
* 用於表示語法高亮中每個 token 的語意分類,對應於 `<CodeToken />` 中的 `type`。
|
|
5
7
|
*
|
|
@@ -22,7 +24,7 @@ import { AsComponentProps, OverrideProps } from "./common";
|
|
|
22
24
|
* const token: CodeTokenType = "keyword1";
|
|
23
25
|
* const token2: CodeTokenType = "string";
|
|
24
26
|
*/
|
|
25
|
-
export type CodeTokenType =
|
|
27
|
+
export type CodeTokenType = (typeof CODE_TOKEN_TYPES)[number];
|
|
26
28
|
/**
|
|
27
29
|
* 表示可用的語法高亮主題名稱。
|
|
28
30
|
* 對應 `themes` 陣列中定義的名稱,例如 `"vscode-dark"`。
|
|
@@ -124,3 +126,39 @@ export type CodeBlockProps<T extends React.ElementType = "span"> = OverrideProps
|
|
|
124
126
|
autoWrap?: boolean;
|
|
125
127
|
}>;
|
|
126
128
|
export type ParsableLanguage = (typeof parsableLanguages)[number];
|
|
129
|
+
/**
|
|
130
|
+
* 程式碼解析函式類型,接受原始程式碼字串,輸出 `CodeTokenProps` 的二維陣列。
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```tsx
|
|
134
|
+
* const tokens = parseTokens.javascript("const x = 1;");
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export type ParseTokensFunction = (code: string) => CodeTokenProps[][];
|
|
138
|
+
/**
|
|
139
|
+
* 為了方便擴充其他語言,我們將解析邏輯抽離為通用的 `createGenericParser` 工廠函式。
|
|
140
|
+
* 只要傳入該語言的一組 regex pattern 與關鍵字,即可產生對應的解析器。
|
|
141
|
+
*/
|
|
142
|
+
export type ParsableLanguageConfig = {
|
|
143
|
+
/**
|
|
144
|
+
* 該語言的 token 匹配規則,按優先順序排列。
|
|
145
|
+
* 注意:變數 (variable) 通常放在最後,作為 fallback。
|
|
146
|
+
*/
|
|
147
|
+
patterns: {
|
|
148
|
+
type: CodeTokenType;
|
|
149
|
+
regex: RegExp;
|
|
150
|
+
}[];
|
|
151
|
+
/**
|
|
152
|
+
* 第一類關鍵字集合 (例如 control flow: if, return, ...)
|
|
153
|
+
*/
|
|
154
|
+
keywords1?: Set<string>;
|
|
155
|
+
/**
|
|
156
|
+
* 第二類關鍵字集合 (例如 basic types, values: true, null, ...)
|
|
157
|
+
*/
|
|
158
|
+
keywords2?: Set<string>;
|
|
159
|
+
/**
|
|
160
|
+
* 是否啟用函式偵測 (當變數後方緊接 "(" 時視為 function)
|
|
161
|
+
* @default true
|
|
162
|
+
*/
|
|
163
|
+
detectFunctions?: boolean;
|
|
164
|
+
};
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { CodeTokenBuilder, CodeTokenProps, CodeTokenType } from "../types";
|
|
2
|
+
import { CodeTokenBuilder, CodeTokenProps, CodeTokenType, ParsableLanguage, ParseTokensFunction } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* 檢查給定的值是否為有效的 `CodeTokenType`。
|
|
5
|
+
* @param value 要檢查的值
|
|
6
|
+
* @returns 如果值是有效的 `CodeTokenType`,則返回 `true`,否則返回 `false`
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* isCodeTokenType("keyword1"); // true
|
|
10
|
+
* isCodeTokenType("invalidType"); // false
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
3
13
|
export declare const isCodeTokenType: (value: any) => value is CodeTokenType;
|
|
4
14
|
/**
|
|
5
15
|
* `c063` 是一組語法高亮 token 建構器集合。
|
|
@@ -55,3 +65,16 @@ export declare const isTokenEqual: <T extends React.ElementType>(a: CodeTokenPro
|
|
|
55
65
|
* @returns 分組後的 token 映射,key 為 `CodeTokenType`
|
|
56
66
|
*/
|
|
57
67
|
export declare const groupTokensByType: <T extends React.ElementType>(lines: CodeTokenProps<T>[][]) => Record<CodeTokenType, CodeTokenProps<T>[]>;
|
|
68
|
+
/**
|
|
69
|
+
* `parseTokens` 是語法解析器的代理集合,用來解析特定語言的程式碼字串。
|
|
70
|
+
*
|
|
71
|
+
* 每個 key 對應一種可解析語言(如 `"javascript"`、`"python"` 等),
|
|
72
|
+
* 傳入原始程式碼字串後,回傳解析後的 token 二維陣列(每行一組 token)。
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* const tokens = parseTokens.javascript("const x = 1;");
|
|
77
|
+
* ```
|
|
78
|
+
* @returns 以 `ParsableLanguage` 為 key 的解析函式集合。
|
|
79
|
+
*/
|
|
80
|
+
export declare const parseTokens: Record<ParsableLanguage, ParseTokensFunction>;
|
package/dist/utils/index.js
CHANGED
|
@@ -1,22 +1,33 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { parserConfigs } from "../libs/parser";
|
|
3
|
+
const CODE_TOKEN_TYPES = new Set([
|
|
4
|
+
"keyword1",
|
|
5
|
+
"keyword2",
|
|
6
|
+
"function",
|
|
7
|
+
"string",
|
|
8
|
+
"number",
|
|
9
|
+
"comment",
|
|
10
|
+
"type",
|
|
11
|
+
"variable",
|
|
12
|
+
"constant",
|
|
13
|
+
"brackets1",
|
|
14
|
+
"brackets2",
|
|
15
|
+
"brackets3",
|
|
16
|
+
"operator",
|
|
17
|
+
"default",
|
|
18
|
+
]);
|
|
19
|
+
/**
|
|
20
|
+
* 檢查給定的值是否為有效的 `CodeTokenType`。
|
|
21
|
+
* @param value 要檢查的值
|
|
22
|
+
* @returns 如果值是有效的 `CodeTokenType`,則返回 `true`,否則返回 `false`
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* isCodeTokenType("keyword1"); // true
|
|
26
|
+
* isCodeTokenType("invalidType"); // false
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
2
29
|
export const isCodeTokenType = (value) => {
|
|
3
|
-
|
|
4
|
-
"keyword1",
|
|
5
|
-
"keyword2",
|
|
6
|
-
"function",
|
|
7
|
-
"string",
|
|
8
|
-
"number",
|
|
9
|
-
"comment",
|
|
10
|
-
"type",
|
|
11
|
-
"variable",
|
|
12
|
-
"constant",
|
|
13
|
-
"brackets1",
|
|
14
|
-
"brackets2",
|
|
15
|
-
"brackets3",
|
|
16
|
-
"operator",
|
|
17
|
-
"default",
|
|
18
|
-
];
|
|
19
|
-
return codeTokenTypes.includes(value);
|
|
30
|
+
return CODE_TOKEN_TYPES.has(value);
|
|
20
31
|
};
|
|
21
32
|
/**
|
|
22
33
|
* `c063` 是一組語法高亮 token 建構器集合。
|
|
@@ -32,7 +43,7 @@ export const isCodeTokenType = (value) => {
|
|
|
32
43
|
* @returns 以 `CodeTokenType` 為 key 的建構器函式集合。
|
|
33
44
|
*/
|
|
34
45
|
const c063 = new Proxy({}, {
|
|
35
|
-
get: (
|
|
46
|
+
get: (_, prop) => {
|
|
36
47
|
/**
|
|
37
48
|
* 建立指定語法類型的 CodeToken。
|
|
38
49
|
*
|
|
@@ -42,7 +53,7 @@ const c063 = new Proxy({}, {
|
|
|
42
53
|
*/
|
|
43
54
|
const builder = (children, props) => {
|
|
44
55
|
if (!isCodeTokenType(prop)) {
|
|
45
|
-
|
|
56
|
+
throw new Error(`Invalid CodeTokenType: ${String(prop)}`);
|
|
46
57
|
}
|
|
47
58
|
return {
|
|
48
59
|
children,
|
|
@@ -109,6 +120,9 @@ export const extractTokenContent = (token) => {
|
|
|
109
120
|
* @returns 是否相等
|
|
110
121
|
*/
|
|
111
122
|
export const isTokenEqual = (a, b) => {
|
|
123
|
+
if (!isCodeTokenType(a.type) || !isCodeTokenType(b.type)) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
112
126
|
return a.type === b.type && extractTokenContent(a) === extractTokenContent(b);
|
|
113
127
|
};
|
|
114
128
|
/**
|
|
@@ -141,26 +155,122 @@ export const groupTokensByType = (lines) => {
|
|
|
141
155
|
return grouped;
|
|
142
156
|
};
|
|
143
157
|
/**
|
|
144
|
-
*
|
|
158
|
+
* 通用解析器工廠
|
|
159
|
+
*/
|
|
160
|
+
const createGenericParser = (config) => {
|
|
161
|
+
const { patterns, keywords1 = new Set(), keywords2 = new Set(), detectFunctions = true, } = config;
|
|
162
|
+
return (code) => {
|
|
163
|
+
const lines = [];
|
|
164
|
+
let currentLine = [];
|
|
165
|
+
let cursor = 0;
|
|
166
|
+
let bracketDepth = 0;
|
|
167
|
+
const getBracketType = (depth) => {
|
|
168
|
+
const types = ["brackets1", "brackets2", "brackets3"];
|
|
169
|
+
return types[depth % 3];
|
|
170
|
+
};
|
|
171
|
+
while (cursor < code.length) {
|
|
172
|
+
// 處理換行
|
|
173
|
+
if (code[cursor] === "\n") {
|
|
174
|
+
lines.push(currentLine);
|
|
175
|
+
currentLine = [];
|
|
176
|
+
cursor++;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
let bestMatch = null;
|
|
180
|
+
const remainingCode = code.slice(cursor);
|
|
181
|
+
for (const { type, regex } of patterns) {
|
|
182
|
+
const match = remainingCode.match(regex);
|
|
183
|
+
if (match) {
|
|
184
|
+
bestMatch = { type, value: match[0] };
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (bestMatch) {
|
|
189
|
+
let finalType = bestMatch.type;
|
|
190
|
+
// 針對 variable 類型進行關鍵字或函式檢查
|
|
191
|
+
if (finalType === "variable") {
|
|
192
|
+
if (keywords1.has(bestMatch.value)) {
|
|
193
|
+
finalType = "keyword1";
|
|
194
|
+
}
|
|
195
|
+
else if (keywords2.has(bestMatch.value)) {
|
|
196
|
+
finalType = "keyword2";
|
|
197
|
+
}
|
|
198
|
+
else if (detectFunctions) {
|
|
199
|
+
// 檢查後方是否緊接括號,若是則視為函式
|
|
200
|
+
let nextIdx = bestMatch.value.length;
|
|
201
|
+
while (nextIdx < remainingCode.length &&
|
|
202
|
+
/[ \t\r\n]/.test(remainingCode[nextIdx])) {
|
|
203
|
+
nextIdx++;
|
|
204
|
+
}
|
|
205
|
+
if (nextIdx < remainingCode.length &&
|
|
206
|
+
remainingCode[nextIdx] === "(") {
|
|
207
|
+
finalType = "function";
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// 處理括號顏色輪替
|
|
212
|
+
if (finalType === "brackets1") {
|
|
213
|
+
const char = bestMatch.value;
|
|
214
|
+
// 開括號增加深度,閉括號減少深度(簡單實作)
|
|
215
|
+
if (["(", "[", "{"].includes(char)) {
|
|
216
|
+
finalType = getBracketType(bracketDepth);
|
|
217
|
+
bracketDepth++;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
bracketDepth = Math.max(0, bracketDepth - 1);
|
|
221
|
+
finalType = getBracketType(bracketDepth);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// 處理多行 Token (如多行註解) 可能跨越換行的情況
|
|
225
|
+
if (bestMatch.value.includes("\n")) {
|
|
226
|
+
const subLines = bestMatch.value.split(/\r?\n/);
|
|
227
|
+
subLines.forEach((lineContent, index) => {
|
|
228
|
+
if (lineContent.length > 0) {
|
|
229
|
+
currentLine.push({ type: finalType, children: lineContent });
|
|
230
|
+
}
|
|
231
|
+
// 如果不是最後一段,表示遇到換行
|
|
232
|
+
if (index < subLines.length - 1) {
|
|
233
|
+
lines.push(currentLine);
|
|
234
|
+
currentLine = [];
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
currentLine.push({ type: finalType, children: bestMatch.value });
|
|
240
|
+
}
|
|
241
|
+
cursor += bestMatch.value.length;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
// 匹配失敗,作為預設文字推進一個字元
|
|
245
|
+
currentLine.push({ type: "default", children: code[cursor] });
|
|
246
|
+
cursor++;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// 處理最後一行
|
|
250
|
+
if (currentLine.length > 0 || lines.length === 0) {
|
|
251
|
+
lines.push(currentLine);
|
|
252
|
+
}
|
|
253
|
+
return lines;
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
/**
|
|
145
257
|
* `parseTokens` 是語法解析器的代理集合,用來解析特定語言的程式碼字串。
|
|
146
258
|
*
|
|
147
259
|
* 每個 key 對應一種可解析語言(如 `"javascript"`、`"python"` 等),
|
|
148
260
|
* 傳入原始程式碼字串後,回傳解析後的 token 二維陣列(每行一組 token)。
|
|
149
261
|
*
|
|
150
|
-
*
|
|
151
262
|
* @example
|
|
152
263
|
* ```ts
|
|
153
264
|
* const tokens = parseTokens.javascript("const x = 1;");
|
|
154
265
|
* ```
|
|
155
|
-
*
|
|
156
|
-
* @returns 語法高亮用的 `CodeTokenProps` 二維陣列
|
|
266
|
+
* @returns 以 `ParsableLanguage` 為 key 的解析函式集合。
|
|
157
267
|
*/
|
|
158
|
-
const parseTokens = new Proxy({}, {
|
|
268
|
+
export const parseTokens = new Proxy({}, {
|
|
159
269
|
get: (_, prop) => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
270
|
+
if (!(prop in parserConfigs)) {
|
|
271
|
+
throw new Error(`Unsupported language: ${String(prop)}`);
|
|
272
|
+
}
|
|
273
|
+
const parser = createGenericParser(parserConfigs[prop]);
|
|
164
274
|
return parser;
|
|
165
275
|
},
|
|
166
276
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "c063",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.7.0",
|
|
5
5
|
"description": "A React component for displaying code snippets with syntax highlighting.",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
+
import { CSSProperties } from "react";
|
|
1
2
|
import { CodeBlockProps } from "../types/index";
|
|
2
3
|
import { CodeLine } from "./CodeLine";
|
|
3
4
|
|
|
5
|
+
const preStyle: CSSProperties = { margin: 0, padding: 0, overflowX: "auto" };
|
|
6
|
+
const tableStyle: CSSProperties = { borderCollapse: "collapse", width: "100%" };
|
|
7
|
+
const rowStyle: CSSProperties = { verticalAlign: "top" };
|
|
8
|
+
const codeCellStyle: CSSProperties = { width: "100%" };
|
|
9
|
+
const lineNumberBaseStyle: CSSProperties = {
|
|
10
|
+
paddingInline: "0.5rem",
|
|
11
|
+
textAlign: "right",
|
|
12
|
+
whiteSpace: "pre",
|
|
13
|
+
fontVariantNumeric: "tabular-nums",
|
|
14
|
+
color: "#888",
|
|
15
|
+
userSelect: "none",
|
|
16
|
+
};
|
|
17
|
+
|
|
4
18
|
/**
|
|
5
19
|
* 顯示完整程式碼區塊,支援多行語法 token 與行號顯示。
|
|
6
20
|
*
|
|
@@ -21,27 +35,22 @@ export const CodeBlock = <T extends React.ElementType = "span">({
|
|
|
21
35
|
...rest
|
|
22
36
|
}: CodeBlockProps<T>) => {
|
|
23
37
|
return (
|
|
24
|
-
<pre {...rest} style={
|
|
25
|
-
<table style={
|
|
38
|
+
<pre {...rest} style={preStyle}>
|
|
39
|
+
<table style={tableStyle}>
|
|
26
40
|
<tbody>
|
|
27
41
|
{tokenLines.map((line, index) => (
|
|
28
|
-
<tr key={index} style={
|
|
42
|
+
<tr key={index} style={rowStyle}>
|
|
29
43
|
{showLineNumbers && (
|
|
30
44
|
<td
|
|
31
45
|
style={{
|
|
32
|
-
|
|
33
|
-
textAlign: "right",
|
|
34
|
-
whiteSpace: "pre",
|
|
35
|
-
fontVariantNumeric: "tabular-nums",
|
|
36
|
-
color: "#888",
|
|
37
|
-
userSelect: "none",
|
|
46
|
+
...lineNumberBaseStyle,
|
|
38
47
|
...lineNumberStyle,
|
|
39
48
|
}}
|
|
40
49
|
>
|
|
41
50
|
{index + 1}
|
|
42
51
|
</td>
|
|
43
52
|
)}
|
|
44
|
-
<td style={
|
|
53
|
+
<td style={codeCellStyle}>
|
|
45
54
|
<CodeLine theme={theme} tokens={line} autoWrap={autoWrap} />
|
|
46
55
|
</td>
|
|
47
56
|
</tr>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { memo } from "react";
|
|
1
2
|
import { CodeLineProps } from "../types/index";
|
|
2
3
|
import { CodeToken } from "./CodeToken";
|
|
3
4
|
/**
|
|
@@ -11,7 +12,7 @@ import { CodeToken } from "./CodeToken";
|
|
|
11
12
|
* @param rest 其他 HTMLAttributes
|
|
12
13
|
* @returns JSX 元素,呈現語法 token 的單行程式碼
|
|
13
14
|
*/
|
|
14
|
-
|
|
15
|
+
const CodeLineInner = <T extends React.ElementType = "span">({
|
|
15
16
|
style,
|
|
16
17
|
tokens,
|
|
17
18
|
theme,
|
|
@@ -33,4 +34,6 @@ export const CodeLine = <T extends React.ElementType = "span">({
|
|
|
33
34
|
);
|
|
34
35
|
};
|
|
35
36
|
|
|
37
|
+
export const CodeLine = memo(CodeLineInner);
|
|
38
|
+
|
|
36
39
|
CodeLine.displayName = "CodeLine";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { memo } from "react";
|
|
1
2
|
import { themeMap } from "../libs/index";
|
|
2
3
|
import { CodeTokenProps } from "../types/index";
|
|
3
4
|
|
|
@@ -13,7 +14,7 @@ import { CodeTokenProps } from "../types/index";
|
|
|
13
14
|
* @param rest 其他 HTML 屬性
|
|
14
15
|
* @returns JSX 元素,顯示帶有語法顏色的 token
|
|
15
16
|
*/
|
|
16
|
-
|
|
17
|
+
const CodeTokenInner = <T extends React.ElementType = "span">({
|
|
17
18
|
as,
|
|
18
19
|
style,
|
|
19
20
|
children,
|
|
@@ -38,4 +39,6 @@ export const CodeToken = <T extends React.ElementType = "span">({
|
|
|
38
39
|
);
|
|
39
40
|
};
|
|
40
41
|
|
|
42
|
+
export const CodeToken = memo(CodeTokenInner);
|
|
43
|
+
|
|
41
44
|
CodeToken.displayName = "CodeToken";
|
package/src/libs/index.tsx
CHANGED
|
@@ -38,4 +38,19 @@ export const themeMap: Record<
|
|
|
38
38
|
Record<CodeTokenType, React.CSSProperties["color"]>
|
|
39
39
|
> = _themeRegistry;
|
|
40
40
|
|
|
41
|
-
export const
|
|
41
|
+
export const CODE_TOKEN_TYPES = [
|
|
42
|
+
"keyword1", // 關鍵字 1
|
|
43
|
+
"keyword2", // 關鍵字 2
|
|
44
|
+
"function", // 函式名稱
|
|
45
|
+
"string", // 字串常值
|
|
46
|
+
"number", // 數字常值
|
|
47
|
+
"comment", // 註解內容
|
|
48
|
+
"type", // 類型定義
|
|
49
|
+
"variable", // 變數名稱、函式名稱、類別名稱等識別符號
|
|
50
|
+
"constant", // 常數值,例如 enum 值、靜態屬性等
|
|
51
|
+
"brackets1", // 括號第一層
|
|
52
|
+
"brackets2", // 括號第二層
|
|
53
|
+
"brackets3", // 括號第三層
|
|
54
|
+
"operator", // 運算符號
|
|
55
|
+
"default", // 其他符號,例如逗號、分號、點號等
|
|
56
|
+
] as const;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ParsableLanguageConfig } from "../../../types";
|
|
2
|
+
export const config: ParsableLanguageConfig = {
|
|
3
|
+
patterns: [
|
|
4
|
+
{ type: "comment", regex: /^(\/\/.*|\/\*[\s\S]*?\*\/)/ },
|
|
5
|
+
{
|
|
6
|
+
type: "string",
|
|
7
|
+
regex: /^("([^"\\]|\\.)*"?|'([^'\\]|\\.)*'?|`([^`\\]|\\.)*`?)/,
|
|
8
|
+
},
|
|
9
|
+
{ type: "number", regex: /^\d+(\.\d+)?/ },
|
|
10
|
+
{
|
|
11
|
+
type: "operator",
|
|
12
|
+
regex:
|
|
13
|
+
/^(===|==|!==|!=|<=|>=|=>|\.\.\.|&&|\|\||\+\+|--|\+=|-=|\*=|\/=|\+=|&=|\|=|\^=|<<=|>>=|>>>=|[+\-*/%=&|^~<>!?:,;.])/,
|
|
14
|
+
},
|
|
15
|
+
{ type: "brackets1", regex: /^[\(\)\[\]\{\}]/ },
|
|
16
|
+
{ type: "variable", regex: /^[a-zA-Z_$][a-zA-Z0-9_$]*/ },
|
|
17
|
+
{ type: "default", regex: /^[ \t\r]+/ },
|
|
18
|
+
],
|
|
19
|
+
keywords1: new Set([
|
|
20
|
+
"const",
|
|
21
|
+
"var",
|
|
22
|
+
"let",
|
|
23
|
+
"function",
|
|
24
|
+
"class",
|
|
25
|
+
"import",
|
|
26
|
+
"export",
|
|
27
|
+
"return",
|
|
28
|
+
"if",
|
|
29
|
+
"else",
|
|
30
|
+
"for",
|
|
31
|
+
"while",
|
|
32
|
+
"do",
|
|
33
|
+
"switch",
|
|
34
|
+
"case",
|
|
35
|
+
"break",
|
|
36
|
+
"continue",
|
|
37
|
+
"try",
|
|
38
|
+
"catch",
|
|
39
|
+
"finally",
|
|
40
|
+
"throw",
|
|
41
|
+
"new",
|
|
42
|
+
"this",
|
|
43
|
+
"super",
|
|
44
|
+
"extends",
|
|
45
|
+
"implements",
|
|
46
|
+
"interface",
|
|
47
|
+
"type",
|
|
48
|
+
"enum",
|
|
49
|
+
"async",
|
|
50
|
+
"await",
|
|
51
|
+
"from",
|
|
52
|
+
"as",
|
|
53
|
+
"declare",
|
|
54
|
+
"namespace",
|
|
55
|
+
"module",
|
|
56
|
+
"public",
|
|
57
|
+
"private",
|
|
58
|
+
"protected",
|
|
59
|
+
"static",
|
|
60
|
+
"readonly",
|
|
61
|
+
"abstract",
|
|
62
|
+
]),
|
|
63
|
+
keywords2: new Set([
|
|
64
|
+
"true",
|
|
65
|
+
"false",
|
|
66
|
+
"null",
|
|
67
|
+
"undefined",
|
|
68
|
+
"NaN",
|
|
69
|
+
"Infinity",
|
|
70
|
+
"void",
|
|
71
|
+
"typeof",
|
|
72
|
+
"instanceof",
|
|
73
|
+
"in",
|
|
74
|
+
"of",
|
|
75
|
+
"delete",
|
|
76
|
+
"this",
|
|
77
|
+
"console",
|
|
78
|
+
"window",
|
|
79
|
+
"document",
|
|
80
|
+
"global",
|
|
81
|
+
"process",
|
|
82
|
+
]),
|
|
83
|
+
detectFunctions: true,
|
|
84
|
+
};
|
package/src/types/index.tsx
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { themes } from "../libs";
|
|
2
2
|
import { AsComponentProps, OverrideProps } from "./common";
|
|
3
|
+
import { CODE_TOKEN_TYPES } from "../libs/index";
|
|
4
|
+
import { parsableLanguages } from "../libs/parser";
|
|
3
5
|
/**
|
|
4
6
|
* 用於表示語法高亮中每個 token 的語意分類,對應於 `<CodeToken />` 中的 `type`。
|
|
5
7
|
*
|
|
@@ -22,18 +24,7 @@ import { AsComponentProps, OverrideProps } from "./common";
|
|
|
22
24
|
* const token: CodeTokenType = "keyword1";
|
|
23
25
|
* const token2: CodeTokenType = "string";
|
|
24
26
|
*/
|
|
25
|
-
export type CodeTokenType =
|
|
26
|
-
| `keyword${1 | 2}` // 關鍵字,分兩種樣式
|
|
27
|
-
| "function" // 函式名
|
|
28
|
-
| "string" // 字串常值:'abc'、"hello"
|
|
29
|
-
| "number" // 數值常量:123、3.14
|
|
30
|
-
| "comment" // 註解內容:// 或 /* */
|
|
31
|
-
| "type" // 類型定義:type、interface、enum
|
|
32
|
-
| "variable" // 變數名、函式名、類別名等識別符號
|
|
33
|
-
| "constant" // 常數值:例如 enum 值、靜態屬性
|
|
34
|
-
| `brackets${1 | 2 | 3}` // 括號,多層巢狀不同樣式:{[()]}
|
|
35
|
-
| "operator" // 運算符號:=、+、*、===、<、>= 等
|
|
36
|
-
| "default"; // 其他符號:, ; . ? ! 等
|
|
27
|
+
export type CodeTokenType = (typeof CODE_TOKEN_TYPES)[number];
|
|
37
28
|
|
|
38
29
|
/**
|
|
39
30
|
* 表示可用的語法高亮主題名稱。
|
|
@@ -67,7 +58,7 @@ export type CodeTokenProps<T extends React.ElementType = "span"> =
|
|
|
67
58
|
|
|
68
59
|
export type CodeTokenBuilder = <T extends React.ElementType = "span">(
|
|
69
60
|
children: CodeTokenProps<T>["children"],
|
|
70
|
-
props?: CodeTokenProps<T
|
|
61
|
+
props?: CodeTokenProps<T>,
|
|
71
62
|
) => CodeTokenProps<T>;
|
|
72
63
|
|
|
73
64
|
/**
|
|
@@ -158,3 +149,39 @@ export type CodeBlockProps<T extends React.ElementType = "span"> =
|
|
|
158
149
|
>;
|
|
159
150
|
|
|
160
151
|
export type ParsableLanguage = (typeof parsableLanguages)[number];
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 程式碼解析函式類型,接受原始程式碼字串,輸出 `CodeTokenProps` 的二維陣列。
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```tsx
|
|
158
|
+
* const tokens = parseTokens.javascript("const x = 1;");
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export type ParseTokensFunction = (code: string) => CodeTokenProps[][];
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 為了方便擴充其他語言,我們將解析邏輯抽離為通用的 `createGenericParser` 工廠函式。
|
|
165
|
+
* 只要傳入該語言的一組 regex pattern 與關鍵字,即可產生對應的解析器。
|
|
166
|
+
*/
|
|
167
|
+
|
|
168
|
+
export type ParsableLanguageConfig = {
|
|
169
|
+
/**
|
|
170
|
+
* 該語言的 token 匹配規則,按優先順序排列。
|
|
171
|
+
* 注意:變數 (variable) 通常放在最後,作為 fallback。
|
|
172
|
+
*/
|
|
173
|
+
patterns: { type: CodeTokenType; regex: RegExp }[];
|
|
174
|
+
/**
|
|
175
|
+
* 第一類關鍵字集合 (例如 control flow: if, return, ...)
|
|
176
|
+
*/
|
|
177
|
+
keywords1?: Set<string>;
|
|
178
|
+
/**
|
|
179
|
+
* 第二類關鍵字集合 (例如 basic types, values: true, null, ...)
|
|
180
|
+
*/
|
|
181
|
+
keywords2?: Set<string>;
|
|
182
|
+
/**
|
|
183
|
+
* 是否啟用函式偵測 (當變數後方緊接 "(" 時視為 function)
|
|
184
|
+
* @default true
|
|
185
|
+
*/
|
|
186
|
+
detectFunctions?: boolean;
|
|
187
|
+
};
|
package/src/utils/index.tsx
CHANGED
|
@@ -4,28 +4,40 @@ import {
|
|
|
4
4
|
CodeTokenProps,
|
|
5
5
|
CodeTokenType,
|
|
6
6
|
ParsableLanguage,
|
|
7
|
+
ParsableLanguageConfig,
|
|
8
|
+
ParseTokensFunction,
|
|
7
9
|
} from "../types";
|
|
10
|
+
import { parserConfigs } from "../libs/parser";
|
|
8
11
|
|
|
12
|
+
const CODE_TOKEN_TYPES = new Set<CodeTokenType>([
|
|
13
|
+
"keyword1",
|
|
14
|
+
"keyword2",
|
|
15
|
+
"function",
|
|
16
|
+
"string",
|
|
17
|
+
"number",
|
|
18
|
+
"comment",
|
|
19
|
+
"type",
|
|
20
|
+
"variable",
|
|
21
|
+
"constant",
|
|
22
|
+
"brackets1",
|
|
23
|
+
"brackets2",
|
|
24
|
+
"brackets3",
|
|
25
|
+
"operator",
|
|
26
|
+
"default",
|
|
27
|
+
]);
|
|
28
|
+
/**
|
|
29
|
+
* 檢查給定的值是否為有效的 `CodeTokenType`。
|
|
30
|
+
* @param value 要檢查的值
|
|
31
|
+
* @returns 如果值是有效的 `CodeTokenType`,則返回 `true`,否則返回 `false`
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* isCodeTokenType("keyword1"); // true
|
|
35
|
+
* isCodeTokenType("invalidType"); // false
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
9
38
|
export const isCodeTokenType = (value: any): value is CodeTokenType => {
|
|
10
|
-
|
|
11
|
-
"keyword1",
|
|
12
|
-
"keyword2",
|
|
13
|
-
"function",
|
|
14
|
-
"string",
|
|
15
|
-
"number",
|
|
16
|
-
"comment",
|
|
17
|
-
"type",
|
|
18
|
-
"variable",
|
|
19
|
-
"constant",
|
|
20
|
-
"brackets1",
|
|
21
|
-
"brackets2",
|
|
22
|
-
"brackets3",
|
|
23
|
-
"operator",
|
|
24
|
-
"default",
|
|
25
|
-
];
|
|
26
|
-
return codeTokenTypes.includes(value);
|
|
39
|
+
return CODE_TOKEN_TYPES.has(value);
|
|
27
40
|
};
|
|
28
|
-
|
|
29
41
|
|
|
30
42
|
/**
|
|
31
43
|
* `c063` 是一組語法高亮 token 建構器集合。
|
|
@@ -43,7 +55,7 @@ export const isCodeTokenType = (value: any): value is CodeTokenType => {
|
|
|
43
55
|
const c063 = new Proxy(
|
|
44
56
|
{},
|
|
45
57
|
{
|
|
46
|
-
get: (
|
|
58
|
+
get: (_, prop: CodeTokenType) => {
|
|
47
59
|
/**
|
|
48
60
|
* 建立指定語法類型的 CodeToken。
|
|
49
61
|
*
|
|
@@ -53,10 +65,10 @@ const c063 = new Proxy(
|
|
|
53
65
|
*/
|
|
54
66
|
const builder = <T extends React.ElementType = "span">(
|
|
55
67
|
children: React.ReactNode,
|
|
56
|
-
props?: CodeTokenProps<T
|
|
68
|
+
props?: CodeTokenProps<T>,
|
|
57
69
|
) => {
|
|
58
70
|
if (!isCodeTokenType(prop)) {
|
|
59
|
-
|
|
71
|
+
throw new Error(`Invalid CodeTokenType: ${String(prop)}`);
|
|
60
72
|
}
|
|
61
73
|
return {
|
|
62
74
|
children,
|
|
@@ -66,7 +78,7 @@ const c063 = new Proxy(
|
|
|
66
78
|
};
|
|
67
79
|
return builder;
|
|
68
80
|
},
|
|
69
|
-
}
|
|
81
|
+
},
|
|
70
82
|
) as Record<CodeTokenType, CodeTokenBuilder>;
|
|
71
83
|
export default c063;
|
|
72
84
|
|
|
@@ -116,7 +128,7 @@ const _extractReactNode = (children: React.ReactNode): string => {
|
|
|
116
128
|
* ```
|
|
117
129
|
*/
|
|
118
130
|
export const extractTokenContent = <T extends React.ElementType>(
|
|
119
|
-
token: CodeTokenProps<T
|
|
131
|
+
token: CodeTokenProps<T>,
|
|
120
132
|
): string => {
|
|
121
133
|
return _extractReactNode(token.children);
|
|
122
134
|
};
|
|
@@ -130,8 +142,11 @@ export const extractTokenContent = <T extends React.ElementType>(
|
|
|
130
142
|
*/
|
|
131
143
|
export const isTokenEqual = <T extends React.ElementType>(
|
|
132
144
|
a: CodeTokenProps<T>,
|
|
133
|
-
b: CodeTokenProps<T
|
|
145
|
+
b: CodeTokenProps<T>,
|
|
134
146
|
): boolean => {
|
|
147
|
+
if (!isCodeTokenType(a.type) || !isCodeTokenType(b.type)) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
135
150
|
return a.type === b.type && extractTokenContent(a) === extractTokenContent(b);
|
|
136
151
|
};
|
|
137
152
|
|
|
@@ -142,7 +157,7 @@ export const isTokenEqual = <T extends React.ElementType>(
|
|
|
142
157
|
* @returns 分組後的 token 映射,key 為 `CodeTokenType`
|
|
143
158
|
*/
|
|
144
159
|
export const groupTokensByType = <T extends React.ElementType>(
|
|
145
|
-
lines: CodeTokenProps<T>[][]
|
|
160
|
+
lines: CodeTokenProps<T>[][],
|
|
146
161
|
): Record<CodeTokenType, CodeTokenProps<T>[]> => {
|
|
147
162
|
const grouped: Record<CodeTokenType, CodeTokenProps<T>[]> = {
|
|
148
163
|
keyword1: [],
|
|
@@ -169,30 +184,144 @@ export const groupTokensByType = <T extends React.ElementType>(
|
|
|
169
184
|
};
|
|
170
185
|
|
|
171
186
|
/**
|
|
172
|
-
*
|
|
187
|
+
* 通用解析器工廠
|
|
188
|
+
*/
|
|
189
|
+
const createGenericParser = (
|
|
190
|
+
config: ParsableLanguageConfig,
|
|
191
|
+
): ParseTokensFunction => {
|
|
192
|
+
const {
|
|
193
|
+
patterns,
|
|
194
|
+
keywords1 = new Set(),
|
|
195
|
+
keywords2 = new Set(),
|
|
196
|
+
detectFunctions = true,
|
|
197
|
+
} = config;
|
|
198
|
+
|
|
199
|
+
return (code: string) => {
|
|
200
|
+
const lines: CodeTokenProps<"span">[][] = [];
|
|
201
|
+
let currentLine: CodeTokenProps<"span">[] = [];
|
|
202
|
+
let cursor = 0;
|
|
203
|
+
let bracketDepth = 0;
|
|
204
|
+
|
|
205
|
+
const getBracketType = (depth: number): CodeTokenType => {
|
|
206
|
+
const types: CodeTokenType[] = ["brackets1", "brackets2", "brackets3"];
|
|
207
|
+
return types[depth % 3];
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
while (cursor < code.length) {
|
|
211
|
+
// 處理換行
|
|
212
|
+
if (code[cursor] === "\n") {
|
|
213
|
+
lines.push(currentLine);
|
|
214
|
+
currentLine = [];
|
|
215
|
+
cursor++;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let bestMatch: { type: CodeTokenType; value: string } | null = null;
|
|
220
|
+
const remainingCode = code.slice(cursor);
|
|
221
|
+
|
|
222
|
+
for (const { type, regex } of patterns) {
|
|
223
|
+
const match = remainingCode.match(regex);
|
|
224
|
+
if (match) {
|
|
225
|
+
bestMatch = { type, value: match[0] };
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (bestMatch) {
|
|
231
|
+
let finalType: CodeTokenType = bestMatch.type;
|
|
232
|
+
|
|
233
|
+
// 針對 variable 類型進行關鍵字或函式檢查
|
|
234
|
+
if (finalType === "variable") {
|
|
235
|
+
if (keywords1.has(bestMatch.value)) {
|
|
236
|
+
finalType = "keyword1";
|
|
237
|
+
} else if (keywords2.has(bestMatch.value)) {
|
|
238
|
+
finalType = "keyword2";
|
|
239
|
+
} else if (detectFunctions) {
|
|
240
|
+
// 檢查後方是否緊接括號,若是則視為函式
|
|
241
|
+
let nextIdx = bestMatch.value.length;
|
|
242
|
+
while (
|
|
243
|
+
nextIdx < remainingCode.length &&
|
|
244
|
+
/[ \t\r\n]/.test(remainingCode[nextIdx])
|
|
245
|
+
) {
|
|
246
|
+
nextIdx++;
|
|
247
|
+
}
|
|
248
|
+
if (
|
|
249
|
+
nextIdx < remainingCode.length &&
|
|
250
|
+
remainingCode[nextIdx] === "("
|
|
251
|
+
) {
|
|
252
|
+
finalType = "function";
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 處理括號顏色輪替
|
|
258
|
+
if (finalType === "brackets1") {
|
|
259
|
+
const char = bestMatch.value;
|
|
260
|
+
// 開括號增加深度,閉括號減少深度(簡單實作)
|
|
261
|
+
if (["(", "[", "{"].includes(char)) {
|
|
262
|
+
finalType = getBracketType(bracketDepth);
|
|
263
|
+
bracketDepth++;
|
|
264
|
+
} else {
|
|
265
|
+
bracketDepth = Math.max(0, bracketDepth - 1);
|
|
266
|
+
finalType = getBracketType(bracketDepth);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 處理多行 Token (如多行註解) 可能跨越換行的情況
|
|
271
|
+
if (bestMatch.value.includes("\n")) {
|
|
272
|
+
const subLines = bestMatch.value.split(/\r?\n/);
|
|
273
|
+
subLines.forEach((lineContent, index) => {
|
|
274
|
+
if (lineContent.length > 0) {
|
|
275
|
+
currentLine.push({ type: finalType, children: lineContent });
|
|
276
|
+
}
|
|
277
|
+
// 如果不是最後一段,表示遇到換行
|
|
278
|
+
if (index < subLines.length - 1) {
|
|
279
|
+
lines.push(currentLine);
|
|
280
|
+
currentLine = [];
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
} else {
|
|
284
|
+
currentLine.push({ type: finalType, children: bestMatch.value });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
cursor += bestMatch.value.length;
|
|
288
|
+
} else {
|
|
289
|
+
// 匹配失敗,作為預設文字推進一個字元
|
|
290
|
+
currentLine.push({ type: "default", children: code[cursor] });
|
|
291
|
+
cursor++;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 處理最後一行
|
|
296
|
+
if (currentLine.length > 0 || lines.length === 0) {
|
|
297
|
+
lines.push(currentLine);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return lines;
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
173
304
|
* `parseTokens` 是語法解析器的代理集合,用來解析特定語言的程式碼字串。
|
|
174
305
|
*
|
|
175
306
|
* 每個 key 對應一種可解析語言(如 `"javascript"`、`"python"` 等),
|
|
176
307
|
* 傳入原始程式碼字串後,回傳解析後的 token 二維陣列(每行一組 token)。
|
|
177
308
|
*
|
|
178
|
-
*
|
|
179
309
|
* @example
|
|
180
310
|
* ```ts
|
|
181
311
|
* const tokens = parseTokens.javascript("const x = 1;");
|
|
182
312
|
* ```
|
|
183
|
-
*
|
|
184
|
-
* @returns 語法高亮用的 `CodeTokenProps` 二維陣列
|
|
313
|
+
* @returns 以 `ParsableLanguage` 為 key 的解析函式集合。
|
|
185
314
|
*/
|
|
186
|
-
const parseTokens = new Proxy(
|
|
315
|
+
export const parseTokens = new Proxy(
|
|
187
316
|
{},
|
|
188
317
|
{
|
|
189
318
|
get: (_, prop: ParsableLanguage) => {
|
|
190
|
-
|
|
191
|
-
|
|
319
|
+
if (!(prop in parserConfigs)) {
|
|
320
|
+
throw new Error(`Unsupported language: ${String(prop)}`);
|
|
321
|
+
}
|
|
192
322
|
|
|
193
|
-
|
|
194
|
-
};
|
|
323
|
+
const parser = createGenericParser(parserConfigs[prop]);
|
|
195
324
|
return parser;
|
|
196
325
|
},
|
|
197
|
-
}
|
|
198
|
-
) as Record<ParsableLanguage,
|
|
326
|
+
},
|
|
327
|
+
) as Record<ParsableLanguage, ParseTokensFunction>;
|