c063 1.6.5 → 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.
@@ -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: { margin: 0, padding: 0, overflowX: "auto" }, children: _jsx("table", { style: { borderCollapse: "collapse", width: "100%" }, children: _jsx("tbody", { children: tokenLines.map((line, index) => (_jsxs("tr", { style: { verticalAlign: "top" }, children: [showLineNumbers && (_jsx("td", { style: {
16
- paddingInline: "0.5rem",
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: { width: "100%" }, children: _jsx(CodeLine, { theme: theme, tokens: line, autoWrap: autoWrap }) })] }, index))) }) }) }));
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
- export const CodeLine = ({ style, tokens, theme, autoWrap = true, ...rest }) => {
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
- export const CodeToken = ({ as, style, children, className, type = "default", theme = "default-dark-modern", ...rest }) => {
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";
@@ -1,19 +1,19 @@
1
1
  import type { CodeTokenType, CodeTheme } from "../types";
2
2
  declare const _themeRegistry: {
3
- readonly "default-dark-modern": Record<CodeTokenType, import("csstype").Property.Color | undefined>;
4
- readonly "default-dark": Record<CodeTokenType, import("csstype").Property.Color | undefined>;
5
- readonly "default-dark-plus": Record<CodeTokenType, import("csstype").Property.Color | undefined>;
6
- readonly "visual-studio-light": Record<CodeTokenType, import("csstype").Property.Color | undefined>;
7
- readonly "default-light-plus": Record<CodeTokenType, import("csstype").Property.Color | undefined>;
8
- readonly "default-light-modern": Record<CodeTokenType, import("csstype").Property.Color | undefined>;
9
- readonly "github-light": Record<CodeTokenType, import("csstype").Property.Color | undefined>;
10
- readonly "github-light-default": Record<CodeTokenType, import("csstype").Property.Color | undefined>;
11
- readonly "github-light-colorblind": Record<CodeTokenType, import("csstype").Property.Color | undefined>;
12
- readonly "github-dark": Record<CodeTokenType, import("csstype").Property.Color | undefined>;
13
- readonly "github-dark-default": Record<CodeTokenType, import("csstype").Property.Color | undefined>;
14
- readonly "github-dark-colorblind": Record<CodeTokenType, import("csstype").Property.Color | undefined>;
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 parsableLanguages: readonly ["javascript"];
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 {};
@@ -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 parsableLanguages = ["javascript"];
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,2 @@
1
+ import { ParsableLanguageConfig } from "../../../types";
2
+ export declare const config: ParsableLanguageConfig;
@@ -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
+ };
@@ -0,0 +1,4 @@
1
+ export declare const parsableLanguages: readonly ["javascript"];
2
+ export declare const parserConfigs: {
3
+ javascript: import("../..").ParsableLanguageConfig;
4
+ };
@@ -0,0 +1,5 @@
1
+ import { config as javascript } from "./configs/javascript";
2
+ export const parsableLanguages = ["javascript"];
3
+ export const parserConfigs = {
4
+ javascript,
5
+ };
@@ -1,5 +1,7 @@
1
- import { parsableLanguages, themes } from "../libs";
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 = `keyword${1 | 2}` | "function" | "string" | "number" | "comment" | "type" | "variable" | "constant" | `brackets${1 | 2 | 3}` | "operator" | "default";
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
+ };
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { CodeTokenBuilder, CodeTokenProps, CodeTokenType } from "../types";
2
+ import { CodeTokenBuilder, CodeTokenProps, CodeTokenType, ParsableLanguage, ParseTokensFunction } from "../types";
3
3
  /**
4
4
  * 檢查給定的值是否為有效的 `CodeTokenType`。
5
5
  * @param value 要檢查的值
@@ -65,3 +65,16 @@ export declare const isTokenEqual: <T extends React.ElementType>(a: CodeTokenPro
65
65
  * @returns 分組後的 token 映射,key 為 `CodeTokenType`
66
66
  */
67
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>;
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import { parserConfigs } from "../libs/parser";
2
3
  const CODE_TOKEN_TYPES = new Set([
3
4
  "keyword1",
4
5
  "keyword2",
@@ -154,26 +155,122 @@ export const groupTokensByType = (lines) => {
154
155
  return grouped;
155
156
  };
156
157
  /**
157
- * 待實作
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
+ /**
158
257
  * `parseTokens` 是語法解析器的代理集合,用來解析特定語言的程式碼字串。
159
258
  *
160
259
  * 每個 key 對應一種可解析語言(如 `"javascript"`、`"python"` 等),
161
260
  * 傳入原始程式碼字串後,回傳解析後的 token 二維陣列(每行一組 token)。
162
261
  *
163
- *
164
262
  * @example
165
263
  * ```ts
166
264
  * const tokens = parseTokens.javascript("const x = 1;");
167
265
  * ```
168
- *
169
- * @returns 語法高亮用的 `CodeTokenProps` 二維陣列
266
+ * @returns 以 `ParsableLanguage` 為 key 的解析函式集合。
170
267
  */
171
- const parseTokens = new Proxy({}, {
268
+ export const parseTokens = new Proxy({}, {
172
269
  get: (_, prop) => {
173
- const parser = (content) => {
174
- const result = [];
175
- return result;
176
- };
270
+ if (!(prop in parserConfigs)) {
271
+ throw new Error(`Unsupported language: ${String(prop)}`);
272
+ }
273
+ const parser = createGenericParser(parserConfigs[prop]);
177
274
  return parser;
178
275
  },
179
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.6.5",
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={{ margin: 0, padding: 0, overflowX: "auto" }}>
25
- <table style={{ borderCollapse: "collapse", width: "100%" }}>
38
+ <pre {...rest} style={preStyle}>
39
+ <table style={tableStyle}>
26
40
  <tbody>
27
41
  {tokenLines.map((line, index) => (
28
- <tr key={index} style={{ verticalAlign: "top" }}>
42
+ <tr key={index} style={rowStyle}>
29
43
  {showLineNumbers && (
30
44
  <td
31
45
  style={{
32
- paddingInline: "0.5rem",
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={{ width: "100%" }}>
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
- export const CodeLine = <T extends React.ElementType = "span">({
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
- export const CodeToken = <T extends React.ElementType = "span">({
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";
@@ -38,4 +38,19 @@ export const themeMap: Record<
38
38
  Record<CodeTokenType, React.CSSProperties["color"]>
39
39
  > = _themeRegistry;
40
40
 
41
- export const parsableLanguages = ["javascript"] as 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
+ };
@@ -0,0 +1,8 @@
1
+ import { config as javascript } from "./configs/javascript";
2
+
3
+ export const parsableLanguages = ["javascript"] as const;
4
+ export const parserConfigs = {
5
+ javascript,
6
+ };
7
+
8
+
@@ -1,5 +1,7 @@
1
- import { parsableLanguages, themes } from "../libs";
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
+ };
@@ -4,7 +4,10 @@ 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
 
9
12
  const CODE_TOKEN_TYPES = new Set<CodeTokenType>([
10
13
  "keyword1",
@@ -62,7 +65,7 @@ const c063 = new Proxy(
62
65
  */
63
66
  const builder = <T extends React.ElementType = "span">(
64
67
  children: React.ReactNode,
65
- props?: CodeTokenProps<T>
68
+ props?: CodeTokenProps<T>,
66
69
  ) => {
67
70
  if (!isCodeTokenType(prop)) {
68
71
  throw new Error(`Invalid CodeTokenType: ${String(prop)}`);
@@ -75,7 +78,7 @@ const c063 = new Proxy(
75
78
  };
76
79
  return builder;
77
80
  },
78
- }
81
+ },
79
82
  ) as Record<CodeTokenType, CodeTokenBuilder>;
80
83
  export default c063;
81
84
 
@@ -125,7 +128,7 @@ const _extractReactNode = (children: React.ReactNode): string => {
125
128
  * ```
126
129
  */
127
130
  export const extractTokenContent = <T extends React.ElementType>(
128
- token: CodeTokenProps<T>
131
+ token: CodeTokenProps<T>,
129
132
  ): string => {
130
133
  return _extractReactNode(token.children);
131
134
  };
@@ -139,7 +142,7 @@ export const extractTokenContent = <T extends React.ElementType>(
139
142
  */
140
143
  export const isTokenEqual = <T extends React.ElementType>(
141
144
  a: CodeTokenProps<T>,
142
- b: CodeTokenProps<T>
145
+ b: CodeTokenProps<T>,
143
146
  ): boolean => {
144
147
  if (!isCodeTokenType(a.type) || !isCodeTokenType(b.type)) {
145
148
  return false;
@@ -154,7 +157,7 @@ export const isTokenEqual = <T extends React.ElementType>(
154
157
  * @returns 分組後的 token 映射,key 為 `CodeTokenType`
155
158
  */
156
159
  export const groupTokensByType = <T extends React.ElementType>(
157
- lines: CodeTokenProps<T>[][]
160
+ lines: CodeTokenProps<T>[][],
158
161
  ): Record<CodeTokenType, CodeTokenProps<T>[]> => {
159
162
  const grouped: Record<CodeTokenType, CodeTokenProps<T>[]> = {
160
163
  keyword1: [],
@@ -181,30 +184,144 @@ export const groupTokensByType = <T extends React.ElementType>(
181
184
  };
182
185
 
183
186
  /**
184
- * 待實作
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
+ /**
185
304
  * `parseTokens` 是語法解析器的代理集合,用來解析特定語言的程式碼字串。
186
305
  *
187
306
  * 每個 key 對應一種可解析語言(如 `"javascript"`、`"python"` 等),
188
307
  * 傳入原始程式碼字串後,回傳解析後的 token 二維陣列(每行一組 token)。
189
308
  *
190
- *
191
309
  * @example
192
310
  * ```ts
193
311
  * const tokens = parseTokens.javascript("const x = 1;");
194
312
  * ```
195
- *
196
- * @returns 語法高亮用的 `CodeTokenProps` 二維陣列
313
+ * @returns 以 `ParsableLanguage` 為 key 的解析函式集合。
197
314
  */
198
- const parseTokens = new Proxy(
315
+ export const parseTokens = new Proxy(
199
316
  {},
200
317
  {
201
318
  get: (_, prop: ParsableLanguage) => {
202
- const parser = (content: string): CodeTokenProps<"span">[][] => {
203
- const result: CodeTokenProps<"span">[][] = [];
319
+ if (!(prop in parserConfigs)) {
320
+ throw new Error(`Unsupported language: ${String(prop)}`);
321
+ }
204
322
 
205
- return result;
206
- };
323
+ const parser = createGenericParser(parserConfigs[prop]);
207
324
  return parser;
208
325
  },
209
- }
210
- ) as Record<ParsableLanguage, (content: string) => CodeTokenProps<"span">[][]>;
326
+ },
327
+ ) as Record<ParsableLanguage, ParseTokensFunction>;