kk-codeimg 0.0.1__py3-none-any.whl

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.
kk_codeimg/__init__.py ADDED
@@ -0,0 +1,126 @@
1
+ """kk_codeimg —— 把代码渲染成精美的代码图片。
2
+
3
+ 最简用法:
4
+
5
+ from kk_codeimg import code_generator
6
+ code_generator(code, 'python', output='out.png')
7
+ code_generator(code, 'python', style='tokyo', output='out.png')
8
+
9
+ 不传 output 时返回 PIL.Image.Image。
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Optional, Union
15
+
16
+ from PIL import Image
17
+
18
+ from .palette import BACKGROUND_CHOICES, GRADIENTS, THEMES
19
+ from .renderer import CodeImageRenderer, RenderResult
20
+ from .styles import STYLES, Style, get_style, list_styles, resolve, resolve_for_render
21
+
22
+ __all__ = [
23
+ "code_generator",
24
+ "CodeImageRenderer",
25
+ "RenderResult",
26
+ "STYLES",
27
+ "Style",
28
+ "get_style",
29
+ "list_styles",
30
+ "THEMES",
31
+ "GRADIENTS",
32
+ "BACKGROUND_CHOICES",
33
+ "list_themes",
34
+ "list_backgrounds",
35
+ ]
36
+
37
+ __version__ = "0.0.1"
38
+
39
+
40
+ def list_themes() -> list:
41
+ """列出全部主题名。"""
42
+ return sorted(THEMES)
43
+
44
+
45
+ def list_backgrounds() -> list:
46
+ """列出全部背景名(含 single / transparent)。"""
47
+ return list(GRADIENTS) + ["single", "transparent"]
48
+
49
+
50
+ def code_generator(
51
+ code: str,
52
+ language: str = "python",
53
+ output: Optional[str] = None,
54
+ *,
55
+ style: Optional[str] = None,
56
+ theme: Optional[str] = None,
57
+ background: Optional[str] = None,
58
+ window_controls: Optional[str] = None,
59
+ font: Optional[str] = None,
60
+ font_size: Optional[int] = None,
61
+ line_height: Optional[float] = None,
62
+ padding: Optional[int] = None,
63
+ line_numbers: Optional[bool] = None,
64
+ transparent_editor: Optional[bool] = None,
65
+ border_radius: Optional[int] = None,
66
+ scrim_alpha: Optional[float] = None,
67
+ title: Optional[str] = None,
68
+ title_alpha: float = 0.72,
69
+ title_size: Optional[Union[int, str]] = None,
70
+ scale: Optional[int] = None,
71
+ ) -> Union[Image.Image, RenderResult]:
72
+ """把代码渲染成一张精美的代码图片。
73
+
74
+ 参数
75
+ ----
76
+ code : 源代码文本
77
+ language : 语言名,如 'python' / 'javascript' / 'sql'(Pygments 别名)
78
+ output : PNG 保存路径;None 则只返回 Image
79
+
80
+ style : 预设风格名,一次套用一组参数;显式参数优先级更高。
81
+ 可选:default / carbon / dracula / tokyo / github / light-frosted /
82
+ frosted / minimal / neon / share / print
83
+
84
+ theme : 12 套主题之一,默认随 style(default 风格为 'one-dark')
85
+ background : 渐变名 / 'single' / 'transparent' / '#hex'
86
+ window_controls : 'color' / 'gray' / 'gray-light' / 'outline' / 'none'
87
+ font : 字体名或 .ttf 路径;None 时按 Cascadia → JetBrains → DejaVu 顺序探测
88
+ font_size : 字号,默认 15
89
+ line_height : 行高倍数,默认 1.45
90
+ padding : 图片四周留白,默认 64
91
+ line_numbers : 是否显示行号,默认 True
92
+ transparent_editor : 编辑器区域透明,默认 False
93
+ border_radius : 编辑器圆角,默认 9
94
+ scrim_alpha : 透明模式下的底色不透明度 0~1;None 时按主题明暗自动决定
95
+ (浅色主题自动垫白底,保证深字可读)
96
+ title : 窗口标题,显示在窗口栏居中(控制点右侧)
97
+ title_alpha : 标题不透明度,默认 0.72(退居次要,不与代码抢焦点)
98
+ title_size : 标题字号。`None`(默认)/`'same'` = 与代码同字号;
99
+ 传数字则单独指定(超出窗口栏容量会自动钳制)。
100
+ **字体始终与代码一致**,只有字号可不同。
101
+ scale : 渲染倍率,默认 2(高清输出)
102
+
103
+ 返回
104
+ ----
105
+ 传了 output -> RenderResult(.image / .path / .editor_box / .code_box);
106
+ 否则 -> PIL.Image.Image
107
+ """
108
+ if not isinstance(code, str):
109
+ raise TypeError("code 必须是字符串")
110
+ if not code.strip():
111
+ raise ValueError("code 不能为空")
112
+
113
+ opts = resolve_for_render(
114
+ style,
115
+ theme=theme, background=background, window_controls=window_controls,
116
+ border_radius=border_radius, font=font, font_size=font_size,
117
+ line_height=line_height, padding=padding, line_numbers=line_numbers,
118
+ transparent_editor=transparent_editor, scale=scale,
119
+ )
120
+
121
+ renderer = CodeImageRenderer(
122
+ title=title, scrim_alpha=scrim_alpha,
123
+ title_alpha=title_alpha, title_size=title_size, **opts
124
+ )
125
+ result = renderer.render(code, language, output=output)
126
+ return result if output else result.image
kk_codeimg/contrast.py ADDED
@@ -0,0 +1,115 @@
1
+ """对比度评估:判断「主题 + 背景」组合下代码是否清晰可读。
2
+
3
+ 用途:透明编辑器下,浅色主题的深色文字直接压在背景上极易糊掉
4
+ (实测 github-light 的 keyword 压在 leaf 渐变上仅 1.24:1)。
5
+ 本模块用 WCAG 相对亮度公式给出可读性判定,并提供磨砂底色建议。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Dict, Iterable, List, Tuple
11
+
12
+ RGB = Tuple[int, int, int]
13
+
14
+ # WCAG 阈值
15
+ AA_TEXT = 4.5
16
+ AA_LARGE = 3.0
17
+
18
+
19
+ def _lin(c: int) -> float:
20
+ c = c / 255.0
21
+ return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
22
+
23
+
24
+ def relative_luminance(rgb: RGB) -> float:
25
+ r, g, b = rgb
26
+ return 0.2126 * _lin(r) + 0.7152 * _lin(g) + 0.0722 * _lin(b)
27
+
28
+
29
+ def contrast_ratio(fg: RGB, bg: RGB) -> float:
30
+ la, lb = relative_luminance(fg), relative_luminance(bg)
31
+ hi, lo = max(la, lb), min(la, lb)
32
+ return (hi + 0.05) / (lo + 0.05)
33
+
34
+
35
+ def hex_to_rgb(value: str) -> RGB:
36
+ v = value.strip().lstrip("#")
37
+ if len(v) == 3:
38
+ v = "".join(c * 2 for c in v)
39
+ return (int(v[0:2], 16), int(v[2:4], 16), int(v[4:6], 16))
40
+
41
+
42
+ def sample_gradient(c1: str, c2: str, steps: int = 9) -> List[RGB]:
43
+ """在渐变上取样若干点(含两端),用于评估最差情况。"""
44
+ a, b = hex_to_rgb(c1), hex_to_rgb(c2)
45
+ return [
46
+ tuple(round(a[i] + (b[i] - a[i]) * (i_step / (steps - 1))) for i in range(3)) # type: ignore[misc]
47
+ for i_step in range(steps)
48
+ ]
49
+
50
+
51
+ def gradient_extremes(c1: str, c2: str) -> List[RGB]:
52
+ """只取渐变两端与中点——最差对比度通常出现在两端。"""
53
+ return sample_gradient(c1, c2, steps=3)
54
+
55
+
56
+ def blend(fg: RGB, bg: RGB, alpha: float) -> RGB:
57
+ """把 fg 以 alpha 叠在 bg 上,得到实际呈现色。"""
58
+ return tuple(round(fg[i] * alpha + bg[i] * (1 - alpha)) for i in range(3)) # type: ignore[return-value]
59
+
60
+
61
+ def evaluate(
62
+ theme_colors: Dict[str, str],
63
+ background_samples: Iterable[RGB],
64
+ fallback_fg: RGB,
65
+ ) -> Tuple[float, str, List[Tuple[str, float]]]:
66
+ """评估主题在给定背景上的可读性。
67
+
68
+ 返回 (最差对比度, 最差槽位名, [(槽位, 对比度), ...])。
69
+ """
70
+ per_slot: List[Tuple[str, float]] = []
71
+ for slot, color in theme_colors.items():
72
+ fg = hex_to_rgb(color)
73
+ worst = min(contrast_ratio(fg, bg) for bg in background_samples)
74
+ per_slot.append((slot, worst))
75
+ if not per_slot:
76
+ return contrast_ratio(fallback_fg, next(iter(background_samples))), "", []
77
+ per_slot.sort(key=lambda kv: kv[1])
78
+ return per_slot[0][1], per_slot[0][0], per_slot
79
+
80
+
81
+ def suggest_scrim_alpha(
82
+ theme_bg: RGB,
83
+ theme_fg: RGB,
84
+ background_samples: Iterable[RGB],
85
+ target: float = AA_TEXT,
86
+ ) -> float:
87
+ """求出让主题文字达到目标对比度所需的「底色不透明度」。
88
+
89
+ 在底色上叠一层 theme_bg 的不透明层 alpha,
90
+ 返回 0~1 之间的最小可用 alpha。
91
+ """
92
+ samples = list(background_samples)
93
+ best = 1.0
94
+ # 以 2% 步长搜索
95
+ alpha = 0.0
96
+ while alpha <= 1.0:
97
+ ok = True
98
+ for bg in samples:
99
+ composited = blend(theme_bg, bg, alpha)
100
+ if contrast_ratio(theme_fg, composited) < target:
101
+ ok = False
102
+ break
103
+ if ok:
104
+ best = alpha
105
+ break
106
+ alpha += 0.02
107
+ return round(best, 2)
108
+
109
+
110
+ def readability_label(ratio: float) -> str:
111
+ if ratio >= AA_TEXT:
112
+ return "✅ 清晰"
113
+ if ratio >= AA_LARGE:
114
+ return "⚠️ 偏低(仅大字可读)"
115
+ return "❌ 难以辨认"
kk_codeimg/palette.py ADDED
@@ -0,0 +1,347 @@
1
+ """配色预设。
2
+
3
+ 色值取自社区公开的编辑器主题方案(MIT / CC 授权),
4
+ 并在此基础上按 WCAG 对比度标准做了修正,非目测近似。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import colorsys
10
+ from dataclasses import dataclass, field
11
+ from typing import Dict, Optional, Tuple
12
+
13
+
14
+ def _hex_to_rgb(value: str) -> Tuple[int, int, int]:
15
+ """'#rrggbb' / '#rgb' -> (r, g, b);也接受 'rgb(r,g,b)'。"""
16
+ value = value.strip()
17
+ if value.startswith("rgb"):
18
+ inner = value[value.index("(") + 1:value.rindex(")")]
19
+ parts = [p for p in inner.replace("/", ",").split(",") if p.strip()]
20
+ out = []
21
+ for p in parts[:3]:
22
+ p = p.strip()
23
+ out.append(int(float(p[: p.index("%")]) * 255 / 100) if "%" in p else int(float(p)))
24
+ return tuple(out) # type: ignore[return-value]
25
+ value = value.lstrip("#")
26
+ if len(value) == 3:
27
+ value = "".join(ch * 2 for ch in value)
28
+ return tuple(int(value[i: i + 2], 16) for i in (0, 2, 4)) # type: ignore[return-value]
29
+
30
+
31
+ def _hsl_to_rgb(h: int, s: int, l: int) -> Tuple[int, int, int]:
32
+ """部分主题用了 hsl(...) 字面量,这里做等价换算。"""
33
+ r, g, b = colorsys.hls_to_rgb(h / 360.0, l / 100.0, s / 100.0)
34
+ return round(r * 255), round(g * 255), round(b * 255)
35
+
36
+
37
+ # --------------------------------------------------------------------------
38
+ # 语义 token:把 CodeMirror 的 tag 体系收敛成 Pygments 能映射的一套 key
39
+ # --------------------------------------------------------------------------
40
+ @dataclass(frozen=True)
41
+ class Theme:
42
+ name: str
43
+ dark: bool
44
+ bg: str
45
+ fg: str
46
+ gutter: str
47
+ # 语义槽位 -> CSS 颜色(hsl 字面量已预先换算成 hex)
48
+ colors: Dict[str, str] = field(default_factory=dict)
49
+
50
+ def bg_rgb(self) -> Tuple[int, int, int]:
51
+ return _hex_to_rgb(self.bg)
52
+
53
+ def fg_rgb(self) -> Tuple[int, int, int]:
54
+ return _hex_to_rgb(self.fg)
55
+
56
+ def gutter_rgb(self) -> Tuple[int, int, int]:
57
+ return _hex_to_rgb(self.gutter)
58
+
59
+ def color(self, slot: str) -> Tuple[int, int, int]:
60
+ """取槽位颜色。
61
+
62
+ 并非每套主题都定义了全部槽位(如 Xcode 系列只定义了 7 个),
63
+ 缺槽时按语义近邻回退,避免整片 token 退化成前景色(看起来像没高亮)。
64
+ """
65
+ seen = set()
66
+ cur = slot
67
+ while cur and cur not in seen:
68
+ seen.add(cur)
69
+ v = self.colors.get(cur)
70
+ if v:
71
+ return _hex_to_rgb(v)
72
+ cur = _FALLBACK.get(cur, "")
73
+ return _hex_to_rgb(self.fg)
74
+
75
+
76
+ # 槽位缺失时的语义回退链:左列缺了就用右列的值。
77
+ # 依据 CodeMirror 默认高亮语义(keyword/name/string/number/comment 五大类)。
78
+ _FALLBACK = {
79
+ "module_keyword": "keyword",
80
+ "definition_keyword": "keyword",
81
+ "definition_variable": "variable_name",
82
+ "definition_type": "type_name",
83
+ "definition_property": "property_name",
84
+ "control_keyword": "keyword",
85
+ "operator_keyword": "keyword",
86
+ "type_name": "class_name",
87
+ "class_name": "type_name",
88
+ "namespace": "class_name",
89
+ "tag_name": "class_name",
90
+ "label_name": "name",
91
+ "name": "variable_name",
92
+ "variable_name": "name",
93
+ "property_name": "attribute_name",
94
+ "attribute_name": "property_name",
95
+ "constant": "number",
96
+ "bool": "number",
97
+ "null": "number",
98
+ "modifier": "keyword",
99
+ "meta": "comment",
100
+ "invalid": "comment",
101
+ "function": "name",
102
+ "angle_bracket": "punctuation",
103
+ "punctuation": "operator",
104
+ "separator": "punctuation",
105
+ "brace": "string",
106
+ "quote": "string",
107
+ "regexp": "string",
108
+ "link": "string",
109
+ "special_property": "string",
110
+ "special_brace": "string",
111
+ "atom": "constant",
112
+ "bracket": "punctuation",
113
+ "heading": "keyword",
114
+ "deleted": "invalid",
115
+ "character": "string",
116
+ "macro_name": "string",
117
+ "annotation": "comment",
118
+ "changed": "number",
119
+ "color": "constant",
120
+ "self": "keyword",
121
+ "deref_operator": "operator",
122
+ "square_bracket": "punctuation",
123
+ "process_instruction": "string",
124
+ "inserted": "string",
125
+ }
126
+
127
+
128
+ def _t(name, dark, bg, fg, gutter, colors) -> Theme:
129
+ return Theme(name, dark, bg, fg, gutter, colors)
130
+
131
+
132
+ # 12 套主题,字段与各编辑器的 CodeMirror styles 数组一一对应
133
+ THEMES: Dict[str, Theme] = {
134
+ "dracula": _t(
135
+ "Dracula", True, "#282a36", "#f8f8f2", "#7d8799",
136
+ {
137
+ # comment 原为 #6272a4(对比度 3.03),提亮至 #8390b7 达 AA
138
+ "comment": "#8390b7", "string": "#f1fa8c", "brace": "#f1fa8c",
139
+ "number": "#bd93f9", "bool": "#bd93f9", "null": "#bd93f9",
140
+ "keyword": "#ff79c6", "operator": "#ff79c6",
141
+ "definition_keyword": "#8be9fd", "type_name": "#8be9fd",
142
+ "definition_type": "#f8f8f2", "class_name": "#50fa7b",
143
+ "function": "#50fa7b", "attribute_name": "#50fa7b",
144
+ },
145
+ ),
146
+ "one-dark": _t(
147
+ "One Dark", True, "#282c34", "#d4d4d4", "#7d8799",
148
+ {
149
+ "function": "#61afef", "tag_name": "#e17078", "heading": "#e17078",
150
+ "operator": "#d4d4d4", "punctuation": "#d4d4d4", "regexp": "#d4d4d4",
151
+ # comment 原为 #54636d(对比度仅 2.25,注释几乎不可读),提亮至 #8495a0
152
+ "comment": "#8495a0", "property_name": "#abb2bf", # hsl(220,14%,71%)
153
+ "attribute_name": "#d19a66", "number": "#d19a66", # hsl(29,54%,61%)
154
+ "class_name": "#e5c07b", "keyword": "#c678dd", # hsl(39/286,...)
155
+ "string": "#98c379", "special_property": "#98c379",
156
+ },
157
+ ),
158
+ "vscode": _t(
159
+ "VS Code", True, "#1e1e1e", "#9cdcfe", "#838383",
160
+ {
161
+ "keyword": "#569cd6", "operator_keyword": "#569cd6", "modifier": "#569cd6",
162
+ "constant": "#569cd6", "bool": "#569cd6", "special_brace": "#569cd6",
163
+ "control_keyword": "#c586c0", "module_keyword": "#c586c0",
164
+ "name": "#9cdcfe", "property_name": "#9cdcfe", "variable_name": "#9cdcfe",
165
+ "type_name": "#4ec9b0", "class_name": "#4ec9b0", "tag_name": "#4ec9b0",
166
+ "namespace": "#4ec9b0", "annotation": "#4ec9b0",
167
+ "function": "#dcdcaa", "number": "#b5cea8",
168
+ "operator": "#d4d4d4", "punctuation": "#d4d4d4",
169
+ "regexp": "#d16969", "string": "#ce9178", "meta": "#ce9178",
170
+ "angle_bracket": "#858585", "comment": "#6a9955", "link": "#6a9955",
171
+ "invalid": "#ff2e2e",
172
+ },
173
+ ),
174
+ "ayu-light": _t(
175
+ "Ayu Light", False, "#fcfcfc", "#5c6166", "#8a919966",
176
+ {
177
+ # 原配色为亮色系(number 1.85:1),在白底上几乎不可读。
178
+ # 保持色相、压暗明度至 WCAG AA。
179
+ "comment": "#72757a", "string": "#5f7f00", "regexp": "#2f8267",
180
+ "number": "#aa6300", "bool": "#aa6300", "null": "#aa6300",
181
+ "variable_name": "#5c6166", "definition_keyword": "#c15405",
182
+ "modifier": "#c15405", "keyword": "#c15405",
183
+ "operator": "#c45117", "punctuation": "#5c6166",
184
+ "function": "#a5670c", "attribute_name": "#a5670c",
185
+ "class_name": "#147cb1", "definition_type": "#147cb1",
186
+ "type_name": "#277e9b", "tag_name": "#277e9b", "label_name": "#277e9b",
187
+ },
188
+ ),
189
+ "github-light": _t(
190
+ "Github Light", False, "#ffffff", "#24292e", "#6a737d",
191
+ {
192
+ "tag_name": "#116329", "comment": "#6a737d", "bracket": "#6a737d",
193
+ "class_name": "#6f42c1", "property_name": "#6f42c1",
194
+ "variable_name": "#005cc5", "attribute_name": "#005cc5",
195
+ "number": "#005cc5", "operator": "#005cc5",
196
+ "keyword": "#d73a49", "type_name": "#d73a49",
197
+ "string": "#032f62", "meta": "#032f62", "regexp": "#032f62",
198
+ "name": "#22863a", "bool": "#c45508", "null": "#c45508",
199
+ "link": "#032f62", "invalid": "#b31d28",
200
+ },
201
+ ),
202
+ "github-dark": _t(
203
+ "GitHub Dark", True, "#0d1117", "#c9d1d9", "#8b949e",
204
+ {
205
+ "tag_name": "#7ee787", "comment": "#8b949e", "bracket": "#8b949e",
206
+ "class_name": "#d2a8ff", "property_name": "#d2a8ff",
207
+ "variable_name": "#79c0ff", "attribute_name": "#79c0ff",
208
+ "number": "#79c0ff", "operator": "#79c0ff",
209
+ "keyword": "#ff7b72", "type_name": "#ff7b72",
210
+ "string": "#a5d6ff", "meta": "#a5d6ff", "regexp": "#a5d6ff",
211
+ "name": "#7ee787", "bool": "#ffab70", "null": "#ffab70",
212
+ "invalid": "#ffdcd7",
213
+ },
214
+ ),
215
+ "tokyo-night": _t(
216
+ "Tokyo Night", True, "#1a1b26", "#c0caf5", "#565f89",
217
+ {
218
+ "keyword": "#bb9af7", "name": "#c0caf5", "property_name": "#7aa2f7",
219
+ "string": "#9ece6a", "function": "#7aa2f7", "label_name": "#7aa2f7",
220
+ "constant": "#bb9af7", "separator": "#c0caf5",
221
+ "class_name": "#c0caf5", "number": "#ff9e64", "modifier": "#ff9e64",
222
+ "type_name": "#0db9d7", "operator": "#bb9af7", "operator_keyword": "#bb9af7",
223
+ "regexp": "#b4f9f8", "link": "#b4f9f8",
224
+ "comment": "#7981ab", "meta": "#7981ab", "invalid": "#ff5370",
225
+ },
226
+ ),
227
+ "xcode-dark": _t(
228
+ "Xcode Dark", True, "#1f1f24", "#ACF2E4", "#7F8C98",
229
+ {
230
+ "comment": "#7F8C98", "quote": "#7F8C98", "meta": "#7F8C98",
231
+ "keyword": "#FF7AB2", "string": "#FF8170",
232
+ "type_name": "#DABAFF", "definition_variable": "#6BDFFF",
233
+ "name": "#6BAA9F", "variable_name": "#ACF2E4",
234
+ # 原主题未定义以下槽位,这里按 Xcode 默认配色补齐,
235
+ # 避免数字/运算符等退化成前景色(视觉上像没高亮)
236
+ "number": "#DABAFF", "bool": "#DABAFF", "null": "#DABAFF",
237
+ "operator": "#ACF2E4", "punctuation": "#ACF2E4",
238
+ "class_name": "#DABAFF", "function": "#6BDFFF",
239
+ "property_name": "#6BAA9F", "attribute_name": "#6BAA9F",
240
+ },
241
+ ),
242
+ "xcode-light": _t(
243
+ "Xcode Light", False, "#ffffff", "#3D3D3D", "#707F8D",
244
+ {
245
+ "comment": "#6a7886", "quote": "#6a7886", "meta": "#6a7886",
246
+ "type_name": "#522BB2", "keyword": "#aa0d91",
247
+ "string": "#D23423", "name": "#032f62",
248
+ "variable_name": "#23575C", "definition_variable": "#327A9E",
249
+ # 同上,补齐原主题未定义的基础槽位
250
+ "number": "#1C00CF", "bool": "#1C00CF", "null": "#1C00CF",
251
+ "operator": "#3D3D3D", "punctuation": "#3D3D3D",
252
+ "class_name": "#522BB2", "function": "#327A9E",
253
+ "property_name": "#032f62", "attribute_name": "#032f62",
254
+ },
255
+ ),
256
+ "amy": _t(
257
+ "Amy", True, "#200020", "#D0D0FF", "#C080C0",
258
+ {
259
+ "comment": "#7373b9", "string": "#999999", "regexp": "#999999",
260
+ "number": "#7090B0", "bool": "#8080A0", "null": "#8080A0",
261
+ "punctuation": "#9f669f", "keyword": "#60B0FF",
262
+ "definition_keyword": "#B0FFF0", "module_keyword": "#60B0FF",
263
+ "operator": "#A0A0FF", "operator_keyword": "#A0A0FF",
264
+ "variable_name": "#008888", "control_keyword": "#80A0FF",
265
+ "class_name": "#70E080", "property_name": "#50A0A0",
266
+ "function": "#50A0A0", "tag_name": "#009090", "modifier": "#B0FFF0",
267
+ },
268
+ ),
269
+ "aura": _t(
270
+ "Aura", True, "#21202e", "#edecee", "#edecee",
271
+ {
272
+ "keyword": "#a277ff", "name": "#edecee", "property_name": "#ffca85",
273
+ "string": "#61ffca", "function": "#ffca85", "label_name": "#ffca85",
274
+ "constant": "#61ffca", "separator": "#edecee",
275
+ "class_name": "#82e2ff", "number": "#61ffca", "modifier": "#61ffca",
276
+ "type_name": "#82e2ff", "operator": "#a277ff",
277
+ "operator_keyword": "#a277ff", "regexp": "#61ffca",
278
+ "comment": "#888888", "meta": "#888888", "invalid": "#ff6767",
279
+ },
280
+ ),
281
+ "cool-glow": _t(
282
+ "Cool Glow", True, "#060521", "#E0E0E0", "#E0E0E090",
283
+ {
284
+ "comment": "#AEAEAE", "string": "#8DFF8E", "special_brace": "#8DFF8E",
285
+ "regexp": "#8DFF8E", "class_name": "#A3EBFF", "function": "#A3EBFF",
286
+ "definition_type": "#A3EBFF", "number": "#62E9BD",
287
+ "bool": "#62E9BD", "null": "#62E9BD",
288
+ "keyword": "#2BF1DC", "operator": "#2BF1DC",
289
+ "definition_keyword": "#F8FBB1", "modifier": "#F8FBB1",
290
+ "variable_name": "#B683CA", "type_name": "#60A4F1",
291
+ "property_name": "#60A4F1", "tag_name": "#60A4F1",
292
+ },
293
+ ),
294
+ }
295
+
296
+ # 10 套社区常用渐变,值为 linear-gradient(90deg, c1, c2)
297
+ GRADIENTS: Dict[str, Tuple[str, str]] = {
298
+ "mystic": ("#b993d6", "#8ca6db"), # 默认背景
299
+ "aruba": ("#42afa1", "#78d4a8"),
300
+ "jungle": ("#c1b777", "#79c08d"),
301
+ "tropical": ("#2bc0e4", "#eaecc6"),
302
+ "aurora": ("#5ee7df", "#b490ca"),
303
+ "candy": ("#d8848b", "#cb96da"),
304
+ "peach": ("#de6262", "#ffb88c"),
305
+ "bananas": ("#f9de70", "#fffbab"),
306
+ "leaf": ("#1d976c", "#93f9b9"),
307
+ "ocean": ("#24c6dc", "#514a9d"),
308
+ }
309
+
310
+ # 窗口控制点配色(对应 CSS 的 win-ctrl-style-*)
311
+ WINDOW_CONTROLS: Dict[str, Dict[str, Optional[str]]] = {
312
+ "color": {"exit": "#ff5f56", "min": "#ffbd2e", "max": "#27c93f", "border": None},
313
+ "gray": {"exit": "#ffffff40", "min": "#ffffff40", "max": "#ffffff40", "border": None},
314
+ "gray-light": {"exit": "#00000018", "min": "#00000018", "max": "#00000018", "border": None},
315
+ "outline": {"exit": None, "min": None, "max": None, "border": "#ffffff40"},
316
+ "none": {"exit": None, "min": None, "max": None, "border": None},
317
+ }
318
+
319
+ # 背景可选取值
320
+ BACKGROUND_CHOICES = tuple(GRADIENTS) + ("single", "transparent")
321
+
322
+
323
+ def get_theme(name: str) -> Theme:
324
+ """按名称取主题,接受大小写与空格/下划线差异。"""
325
+ key = name.strip().lower().replace("_", "-").replace(" ", "-")
326
+ aliases = {
327
+ "one-dark": "one-dark", "onedark": "one-dark", "dark": "one-dark",
328
+ "dracula": "dracula", "vscode": "vscode", "vs-code": "vscode",
329
+ "ayu-light": "ayu-light", "ayulight": "ayu-light", "light": "github-light",
330
+ "github-light": "github-light", "githublight": "github-light",
331
+ "github-dark": "github-dark", "githubdark": "github-dark",
332
+ "tokyo-night": "tokyo-night", "tokyonight": "tokyo-night", "tokyo": "tokyo-night",
333
+ "xcode-dark": "xcode-dark", "xcodedark": "xcode-dark",
334
+ "xcode-light": "xcode-light", "xcodelight": "xcode-light",
335
+ "amy": "amy", "aura": "aura", "cool-glow": "cool-glow", "coolglow": "cool-glow",
336
+ }
337
+ resolved = aliases.get(key, key)
338
+ if resolved not in THEMES:
339
+ raise ValueError(
340
+ f"未知主题 {name!r};可选:{', '.join(sorted(THEMES))}"
341
+ )
342
+ return THEMES[resolved]
343
+
344
+
345
+ def get_gradient(name: str) -> Optional[Tuple[str, str]]:
346
+ key = name.strip().lower()
347
+ return GRADIENTS.get(key)
kk_codeimg/py.typed ADDED
File without changes