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.
@@ -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,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>;
@@ -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
- const codeTokenTypes = [
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: (target, prop, receiver) => {
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
- return Reflect.get(target, prop, receiver);
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
- const parser = (content) => {
161
- const result = [];
162
- return result;
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.6.4",
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,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
- const codeTokenTypes: CodeTokenType[] = [
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: (target, prop: CodeTokenType, receiver) => {
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
- return Reflect.get(target, prop, receiver);
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
- const parser = (content: string): CodeTokenProps<"span">[][] => {
191
- const result: CodeTokenProps<"span">[][] = [];
319
+ if (!(prop in parserConfigs)) {
320
+ throw new Error(`Unsupported language: ${String(prop)}`);
321
+ }
192
322
 
193
- return result;
194
- };
323
+ const parser = createGenericParser(parserConfigs[prop]);
195
324
  return parser;
196
325
  },
197
- }
198
- ) as Record<ParsableLanguage, (content: string) => CodeTokenProps<"span">[][]>;
326
+ },
327
+ ) as Record<ParsableLanguage, ParseTokensFunction>;