larkvs 0.1__tar.gz
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.
- larkvs-0.1/PKG-INFO +108 -0
- larkvs-0.1/larkvs/__init__.py +50 -0
- larkvs-0.1/larkvs/claude_helper.py +106 -0
- larkvs-0.1/larkvs/colorparser.py +403 -0
- larkvs-0.1/larkvs/colorpicker.py +127 -0
- larkvs-0.1/larkvs/common.py +1047 -0
- larkvs-0.1/larkvs/completer.py +410 -0
- larkvs-0.1/larkvs/execution.py +710 -0
- larkvs-0.1/larkvs/highlighters.py +364 -0
- larkvs-0.1/larkvs/home.py +408 -0
- larkvs-0.1/larkvs/logos/__init__.py +0 -0
- larkvs-0.1/larkvs/logos/larkvs-logo.png +0 -0
- larkvs-0.1/larkvs/logos/nahida-close.png +0 -0
- larkvs-0.1/larkvs/logos/nahida-new.png +0 -0
- larkvs-0.1/larkvs/logos/nahida-new2.png +0 -0
- larkvs-0.1/larkvs/logos/nahida-open.png +0 -0
- larkvs-0.1/larkvs/logos/nahida-open2.png +0 -0
- larkvs-0.1/larkvs/main.py +1697 -0
- larkvs-0.1/larkvs/mcp_ipc.py +206 -0
- larkvs-0.1/larkvs/mcp_server.py +419 -0
- larkvs-0.1/larkvs/mcp_tools.py +48 -0
- larkvs-0.1/larkvs/panels.py +701 -0
- larkvs-0.1/larkvs/projectio.py +316 -0
- larkvs-0.1/larkvs/settings.py +1170 -0
- larkvs-0.1/larkvs/settingsmgr.py +206 -0
- larkvs-0.1/larkvs/settingsui.py +239 -0
- larkvs-0.1/larkvs/test_mcp_e2e.py +120 -0
- larkvs-0.1/larkvs/test_mcp_ipc.py +118 -0
- larkvs-0.1/larkvs/test_mcp_multi.py +63 -0
- larkvs-0.1/larkvs/test_mcp_stdio.py +114 -0
- larkvs-0.1/larkvs/theme.py +926 -0
- larkvs-0.1/larkvs/workers.py +343 -0
- larkvs-0.1/larkvs/workspace.py +2066 -0
- larkvs-0.1/larkvs/xml_tree_canvas.py +1310 -0
- larkvs-0.1/larkvs.egg-info/PKG-INFO +108 -0
- larkvs-0.1/larkvs.egg-info/SOURCES.txt +40 -0
- larkvs-0.1/larkvs.egg-info/dependency_links.txt +1 -0
- larkvs-0.1/larkvs.egg-info/entry_points.txt +2 -0
- larkvs-0.1/larkvs.egg-info/requires.txt +4 -0
- larkvs-0.1/larkvs.egg-info/top_level.txt +1 -0
- larkvs-0.1/setup.cfg +4 -0
- larkvs-0.1/setup.py +39 -0
larkvs-0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: larkvs
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: A visual debugging tool for Lark grammars, VS Code-style desktop IDE.
|
|
5
|
+
Author-email: 2229066748@qq.com
|
|
6
|
+
Maintainer: Eagle'sBaby
|
|
7
|
+
Maintainer-email: 2229066748@qq.com
|
|
8
|
+
License: Apache Licence 2.0
|
|
9
|
+
Keywords: python,lark,ui
|
|
10
|
+
Classifier: Programming Language :: Python
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: PyQt6>6.4
|
|
15
|
+
Requires-Dist: lark>=1.1.0
|
|
16
|
+
Requires-Dist: qtawesome
|
|
17
|
+
Requires-Dist: fastmcp<3,>2
|
|
18
|
+
Dynamic: author-email
|
|
19
|
+
Dynamic: classifier
|
|
20
|
+
Dynamic: description
|
|
21
|
+
Dynamic: description-content-type
|
|
22
|
+
Dynamic: keywords
|
|
23
|
+
Dynamic: license
|
|
24
|
+
Dynamic: maintainer
|
|
25
|
+
Dynamic: maintainer-email
|
|
26
|
+
Dynamic: requires-dist
|
|
27
|
+
Dynamic: requires-python
|
|
28
|
+
Dynamic: summary
|
|
29
|
+
|
|
30
|
+
# Lark Visual Studio
|
|
31
|
+
|
|
32
|
+
A visual debugging tool for Lark grammars, VS Code-style desktop IDE.
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- **Grammar Editing** — Lark syntax highlighting, code completion, color configuration (`#RRGGBB` inline/block)
|
|
37
|
+
- **Parse Tree Visualization** — Tree/text dual view, node color mapping, click-to-locate source
|
|
38
|
+
- **Execution Engine** — Write and run Transformer/Visitor code, variable monitor, safe sandbox
|
|
39
|
+
- **Compile & Run Pipeline** — Compile (F6) parse only; Run (F5) parse + compile + execute
|
|
40
|
+
- **Project Management** — `.larkvs` project directories, recent projects, import/export `.plark`
|
|
41
|
+
- **Theme System** — Light/dark real-time switching
|
|
42
|
+
- **Settings System** — General/Appearance/Editor/Shortcuts/Interpreter, global and project-level
|
|
43
|
+
- **MCP Integration** — Claude Code can read/write grammar, trigger compile/run via MCP
|
|
44
|
+
- **Built-in Examples** — Arithmetic, JSON, CSV, lexical analysis, Lua interpreter, and more
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install larkvs
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Launch GUI (default)
|
|
56
|
+
larkvs
|
|
57
|
+
|
|
58
|
+
# Open a project file
|
|
59
|
+
larkvs -f <path/to/project>/.larkvs
|
|
60
|
+
|
|
61
|
+
# Start MCP Server (stdio transport)
|
|
62
|
+
larkvs -m
|
|
63
|
+
|
|
64
|
+
# Show help
|
|
65
|
+
larkvs -h
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| Flag | Description |
|
|
69
|
+
|------|-------------|
|
|
70
|
+
| `-m`, `--mcp` | Start MCP Server (stdio) for AI clients like Claude Code |
|
|
71
|
+
| `-f`, `--file` <path> | Open the specified `.larkvs` project file |
|
|
72
|
+
| `-h` | Show help and exit |
|
|
73
|
+
|
|
74
|
+
## Shortcuts
|
|
75
|
+
|
|
76
|
+
| Shortcut | Action |
|
|
77
|
+
|----------|--------|
|
|
78
|
+
| F6 | Compile (parse only) |
|
|
79
|
+
| F5 | Run (compile + execute) |
|
|
80
|
+
| Ctrl+, | Open settings |
|
|
81
|
+
| Ctrl+Shift+F | Global search |
|
|
82
|
+
| Double Shift | Search dialog |
|
|
83
|
+
|
|
84
|
+
## Project Structure
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
main.py Main window + entry point
|
|
88
|
+
common.py Base components (CodeEditor, Icons, StatusBar)
|
|
89
|
+
panels.py Feature panels (grammar editor, test input, parse result, console)
|
|
90
|
+
execution.py Execution engine (ExecutionPanel, VariableMonitor, ProblemPanel)
|
|
91
|
+
workspace.py Workspace (ActivityBar, TabWorkspace, BottomPanel, search dialog)
|
|
92
|
+
workers.py Background threads (ParseWorker, ExecutionWorker)
|
|
93
|
+
highlighters.py Syntax highlighters
|
|
94
|
+
colorparser.py Color configuration parser
|
|
95
|
+
colorpicker.py Color picker popup
|
|
96
|
+
completer.py Code completion
|
|
97
|
+
home.py Home page (recent project buttons)
|
|
98
|
+
projectio.py Project file I/O
|
|
99
|
+
settings.py Settings dialog
|
|
100
|
+
settingsmgr.py Settings manager
|
|
101
|
+
settingsui.py Settings UI components
|
|
102
|
+
theme.py Theme/examples/templates
|
|
103
|
+
xml_tree_canvas.py XML tree visualization
|
|
104
|
+
claude_helper.py Claude Code integration
|
|
105
|
+
mcp_ipc.py MCP TCP server
|
|
106
|
+
mcp_server.py MCP standalone server
|
|
107
|
+
mcp_tools.py MCP method constants
|
|
108
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Lark Visual Studio — Lark 语法可视化调试工具"""
|
|
2
|
+
import sys
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def FN_OpenUI(filepath=None):
|
|
8
|
+
"""启动 Lark Visual Studio GUI。"""
|
|
9
|
+
from PyQt6.QtWidgets import QApplication
|
|
10
|
+
from PyQt6.QtGui import QFont
|
|
11
|
+
from larkvs.main import MainWindow
|
|
12
|
+
|
|
13
|
+
app = QApplication(sys.argv)
|
|
14
|
+
app.setApplicationName("Lark Visual Studio")
|
|
15
|
+
app.setStyle("Fusion")
|
|
16
|
+
|
|
17
|
+
_font = QFont("Segoe UI", 10)
|
|
18
|
+
_font.setStyleHint(QFont.StyleHint.SansSerif)
|
|
19
|
+
app.setFont(_font)
|
|
20
|
+
|
|
21
|
+
window = MainWindow()
|
|
22
|
+
|
|
23
|
+
if filepath:
|
|
24
|
+
_path = Path(filepath).resolve()
|
|
25
|
+
if _path.exists():
|
|
26
|
+
window._loadProjectFromPath(str(_path))
|
|
27
|
+
else:
|
|
28
|
+
print(f"项目文件不存在: {_path}")
|
|
29
|
+
|
|
30
|
+
window.show()
|
|
31
|
+
sys.exit(app.exec())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def FN_StartMCP():
|
|
35
|
+
"""启动 larkvs MCP Server (stdio 传输)。"""
|
|
36
|
+
from larkvs.mcp_server import mcp
|
|
37
|
+
mcp.run(transport="stdio")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def CMD_Enter():
|
|
41
|
+
"""larkvs 入口:默认启动 GUI,-m 启动 MCP Server。"""
|
|
42
|
+
_parser = argparse.ArgumentParser(description="Lark Visual Studio")
|
|
43
|
+
_parser.add_argument("-m", "--mcp", action="store_true", help="启动 MCP Server (stdio)")
|
|
44
|
+
_parser.add_argument("-f", "--file", dest="file", default=None, help="打开指定的 .larkvs 项目文件")
|
|
45
|
+
_args = _parser.parse_args()
|
|
46
|
+
|
|
47
|
+
if _args.mcp:
|
|
48
|
+
FN_StartMCP()
|
|
49
|
+
else:
|
|
50
|
+
FN_OpenUI(_args.file)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Claude Code 辅助集成"""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ClaudeHelperMixin:
|
|
14
|
+
"""提供“帮助 claude”菜单功能:检测 claude、生成 MCP 配置并启动。"""
|
|
15
|
+
|
|
16
|
+
def _onHelpClaude(self):
|
|
17
|
+
"""检查 claude 是否可用,若可用则创建临时目录、写入 MCP 配置并启动 claude。"""
|
|
18
|
+
claude_cmd = shutil.which("claude")
|
|
19
|
+
if claude_cmd is None:
|
|
20
|
+
QMessageBox.warning(
|
|
21
|
+
self,
|
|
22
|
+
"claude 未找到",
|
|
23
|
+
"未在 PATH 中找到 claude 命令。\n\n"
|
|
24
|
+
"请确保 Claude Code CLI 已安装并添加到系统 PATH 中。\n"
|
|
25
|
+
"安装指南: https://docs.anthropic.com/en/docs/claude-code/setup",
|
|
26
|
+
)
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
project_root = Path(__file__).parent.resolve()
|
|
30
|
+
mcp_server_path = project_root / "mcp_server.py"
|
|
31
|
+
if not mcp_server_path.exists():
|
|
32
|
+
QMessageBox.critical(
|
|
33
|
+
self,
|
|
34
|
+
"错误",
|
|
35
|
+
f"未找到 MCP 服务器脚本:\n{mcp_server_path}\n\n"
|
|
36
|
+
"请确保项目文件完整。",
|
|
37
|
+
)
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
temp_dir = Path(tempfile.mkdtemp(prefix="larkvs_claude_"))
|
|
41
|
+
claude_dir = temp_dir / ".claude"
|
|
42
|
+
try:
|
|
43
|
+
claude_dir.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
except Exception as e:
|
|
45
|
+
QMessageBox.critical(
|
|
46
|
+
self,
|
|
47
|
+
"错误",
|
|
48
|
+
f"无法创建临时目录:\n{claude_dir}\n\n{str(e)}",
|
|
49
|
+
)
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
settings = {
|
|
53
|
+
"mcpServers": {
|
|
54
|
+
"larkvs": {
|
|
55
|
+
"command": sys.executable,
|
|
56
|
+
"args": [str(mcp_server_path)],
|
|
57
|
+
"env": {
|
|
58
|
+
"LARKVS_PID": str(os.getpid()),
|
|
59
|
+
"PYTHONIOENCODING": "utf-8",
|
|
60
|
+
"PYTHONUNBUFFERED": "1",
|
|
61
|
+
},
|
|
62
|
+
"type": "stdio",
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
settings_path = claude_dir / "settings.json"
|
|
67
|
+
try:
|
|
68
|
+
settings_path.write_text(
|
|
69
|
+
json.dumps(settings, ensure_ascii=False, indent=2),
|
|
70
|
+
encoding="utf-8",
|
|
71
|
+
)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
QMessageBox.critical(
|
|
74
|
+
self,
|
|
75
|
+
"错误",
|
|
76
|
+
f"无法写入 Claude 设置文件:\n{settings_path}\n\n{str(e)}",
|
|
77
|
+
)
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
if hasattr(self, "_console"):
|
|
81
|
+
self._console.appendInfo(f"正在启动 claude,临时目录: {temp_dir}")
|
|
82
|
+
if hasattr(self, "_statusBar"):
|
|
83
|
+
self._statusBar.showMessage("正在启动 claude...", 5000)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
kwargs = {}
|
|
87
|
+
if os.name == "nt":
|
|
88
|
+
kwargs["creationflags"] = subprocess.CREATE_NEW_CONSOLE
|
|
89
|
+
|
|
90
|
+
subprocess.Popen(
|
|
91
|
+
[claude_cmd],
|
|
92
|
+
cwd=str(temp_dir),
|
|
93
|
+
env={**os.environ},
|
|
94
|
+
**kwargs,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if hasattr(self, "_statusBar"):
|
|
98
|
+
self._statusBar.showMessage(
|
|
99
|
+
f"claude 已启动 ({temp_dir.name})", 5000
|
|
100
|
+
)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
QMessageBox.critical(
|
|
103
|
+
self,
|
|
104
|
+
"启动失败",
|
|
105
|
+
f"无法启动 claude:\n\n{str(e)}",
|
|
106
|
+
)
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""颜色配置解析器:从语法文本中提取颜色配置,剥离写法2配置块"""
|
|
2
|
+
import re
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# ==================== 常量 ====================
|
|
7
|
+
COLOR_PATTERN = re.compile(r'#[0-9A-Fa-f]{6}(?::(?:light|dark))?')
|
|
8
|
+
HEX_PREFIX = re.compile(r'#[0-9A-Fa-f]{6}')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ==================== 数据类 ====================
|
|
12
|
+
@dataclass
|
|
13
|
+
class ColorConfig:
|
|
14
|
+
"""颜色配置项(纯数据,不含方法)"""
|
|
15
|
+
light: str | None = None
|
|
16
|
+
dark: str | None = None
|
|
17
|
+
tokens: set = field(default_factory=set)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ==================== 解析函数 ====================
|
|
21
|
+
def ParseColorConfigs(text):
|
|
22
|
+
"""从语法文本中解析颜色配置,返回 list[ColorConfig]
|
|
23
|
+
|
|
24
|
+
识别两种写法:
|
|
25
|
+
- 写法1(行内/向上匹配):#color 附加到规则行末尾或独立行
|
|
26
|
+
- 写法2(事后描述):#color { tokens } 或多行变体
|
|
27
|
+
"""
|
|
28
|
+
lines = text.split('\n')
|
|
29
|
+
configs = []
|
|
30
|
+
i = 0
|
|
31
|
+
n = len(lines)
|
|
32
|
+
|
|
33
|
+
while i < n:
|
|
34
|
+
stripped = lines[i].strip()
|
|
35
|
+
|
|
36
|
+
# 跳过空行和注释行
|
|
37
|
+
if not stripped or stripped.startswith('//'):
|
|
38
|
+
i += 1
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
# 尝试写法2(事后描述)
|
|
42
|
+
result = _tryPostDesc(lines, i)
|
|
43
|
+
if result is not None:
|
|
44
|
+
config, nextI = result
|
|
45
|
+
configs.append(config)
|
|
46
|
+
i = nextI
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
# 尝试写法1行内(规则行末尾带 #color)
|
|
50
|
+
config = _tryInline(lines[i])
|
|
51
|
+
if config is not None:
|
|
52
|
+
configs.append(config)
|
|
53
|
+
i += 1
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
# 尝试写法1向上匹配(独立颜色行,无 { 跟随)
|
|
57
|
+
config = _tryUpward(lines, i)
|
|
58
|
+
if config is not None:
|
|
59
|
+
configs.append(config)
|
|
60
|
+
i += 1
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
i += 1
|
|
64
|
+
|
|
65
|
+
return configs
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def StripColorConfigs(text):
|
|
69
|
+
"""剥离写法2配置块,返回纯 Lark 语法
|
|
70
|
+
|
|
71
|
+
写法1的 #color 是 Lark 注释,无需剥离。
|
|
72
|
+
写法2的 {...} 和 (...) 非注释,必须剥离。
|
|
73
|
+
"""
|
|
74
|
+
lines = text.split('\n')
|
|
75
|
+
result = []
|
|
76
|
+
i = 0
|
|
77
|
+
n = len(lines)
|
|
78
|
+
|
|
79
|
+
while i < n:
|
|
80
|
+
stripped = lines[i].strip()
|
|
81
|
+
|
|
82
|
+
# 空行和注释行保留
|
|
83
|
+
if not stripped or stripped.startswith('//'):
|
|
84
|
+
result.append(lines[i])
|
|
85
|
+
i += 1
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
# 检查是否为写法2块起始
|
|
89
|
+
blockEnd = _findPostDescEnd(lines, i)
|
|
90
|
+
if blockEnd is not None:
|
|
91
|
+
i = blockEnd + 1
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
result.append(lines[i])
|
|
95
|
+
i += 1
|
|
96
|
+
|
|
97
|
+
return '\n'.join(result)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ==================== 内部函数 ====================
|
|
101
|
+
def _extractColors(text):
|
|
102
|
+
"""从文本中提取颜色值,返回 dict: {'light': str|None, 'dark': str|None}"""
|
|
103
|
+
result = {'light': None, 'dark': None}
|
|
104
|
+
for m in COLOR_PATTERN.finditer(text):
|
|
105
|
+
full = m.group(0)
|
|
106
|
+
hexval = full[:7] # #RRGGBB
|
|
107
|
+
suffix = full[7:] # :light, :dark, or ''
|
|
108
|
+
if suffix == ':light':
|
|
109
|
+
result['light'] = hexval
|
|
110
|
+
elif suffix == ':dark':
|
|
111
|
+
result['dark'] = hexval
|
|
112
|
+
else:
|
|
113
|
+
# 无后缀:同时应用于 light 和 dark
|
|
114
|
+
if result['light'] is None:
|
|
115
|
+
result['light'] = hexval
|
|
116
|
+
if result['dark'] is None:
|
|
117
|
+
result['dark'] = hexval
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _extractTokenNames(text):
|
|
122
|
+
"""从文本中提取 TOKEN 名称(终结符名和字面量)"""
|
|
123
|
+
tokens = set()
|
|
124
|
+
# 字符串字面量: "..."
|
|
125
|
+
for m in re.finditer(r'"([^"]*)"', text):
|
|
126
|
+
tokens.add(m.group(1))
|
|
127
|
+
# 大写标识符(终结符名)
|
|
128
|
+
for m in re.finditer(r'\b[A-Z_][A-Z_0-9]*\b', text):
|
|
129
|
+
tokens.add(m.group(0))
|
|
130
|
+
return tokens
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _extractTokensFromRule(line):
|
|
134
|
+
"""从规则行中提取终结符名和字面量"""
|
|
135
|
+
# 先移除颜色值
|
|
136
|
+
cleaned = COLOR_PATTERN.sub('', line)
|
|
137
|
+
return _extractTokenNames(cleaned)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _isRuleLine(text):
|
|
141
|
+
"""判断是否为规则行(含 : 或以 | 开头)"""
|
|
142
|
+
# 先移除颜色值,避免 :light/:dark 后缀导致误判
|
|
143
|
+
cleaned = COLOR_PATTERN.sub('', text).strip()
|
|
144
|
+
return ':' in cleaned or cleaned.startswith('|')
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _tryPostDesc(lines, startIdx):
|
|
148
|
+
"""尝试解析写法2(事后描述),返回 (ColorConfig, nextIdx) 或 None"""
|
|
149
|
+
idx = startIdx
|
|
150
|
+
line = lines[idx].strip()
|
|
151
|
+
|
|
152
|
+
# 情况1:以 ( 开头的颜色组
|
|
153
|
+
if line.startswith('('):
|
|
154
|
+
return _tryPostDescParen(lines, idx)
|
|
155
|
+
|
|
156
|
+
# 情况2:以 #color 开头
|
|
157
|
+
if not HEX_PREFIX.match(line):
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
colors = _extractColors(line)
|
|
161
|
+
afterColors = COLOR_PATTERN.sub('', line).strip()
|
|
162
|
+
|
|
163
|
+
# #color { ... } 同行
|
|
164
|
+
if afterColors.startswith('{'):
|
|
165
|
+
tokens = _parseBraceInline(afterColors)
|
|
166
|
+
if tokens is None:
|
|
167
|
+
# 多行 { },查找闭合 }
|
|
168
|
+
endIdx = _findClosingBrace(lines, idx)
|
|
169
|
+
if endIdx is None:
|
|
170
|
+
return None
|
|
171
|
+
tokens = _parseBraceBlock(lines, idx, endIdx)
|
|
172
|
+
return ColorConfig(colors['light'], colors['dark'], tokens), endIdx + 1
|
|
173
|
+
# } 也在同行
|
|
174
|
+
return ColorConfig(colors['light'], colors['dark'], tokens), idx + 1
|
|
175
|
+
|
|
176
|
+
# #color 独立行,查找下一非空行是否有 {
|
|
177
|
+
if not afterColors:
|
|
178
|
+
nextIdx = idx + 1
|
|
179
|
+
while nextIdx < len(lines):
|
|
180
|
+
s = lines[nextIdx].strip()
|
|
181
|
+
if not s or s.startswith('//'):
|
|
182
|
+
nextIdx += 1
|
|
183
|
+
continue
|
|
184
|
+
if s.startswith('{'):
|
|
185
|
+
endIdx = _findClosingBrace(lines, nextIdx)
|
|
186
|
+
if endIdx is None:
|
|
187
|
+
return None
|
|
188
|
+
tokens = _parseBraceBlock(lines, nextIdx, endIdx)
|
|
189
|
+
return ColorConfig(colors['light'], colors['dark'], tokens), endIdx + 1
|
|
190
|
+
return None # 非 {,不是写法2
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _tryPostDescParen(lines, startIdx):
|
|
197
|
+
"""处理以 ( 开头的颜色组写法2"""
|
|
198
|
+
idx = startIdx
|
|
199
|
+
# 收集直到 ) 的文本
|
|
200
|
+
groupEnd = idx
|
|
201
|
+
while groupEnd < len(lines):
|
|
202
|
+
if ')' in lines[groupEnd]:
|
|
203
|
+
break
|
|
204
|
+
groupEnd += 1
|
|
205
|
+
if groupEnd >= len(lines):
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
groupText = ' '.join(lines[j].strip() for j in range(idx, groupEnd + 1))
|
|
209
|
+
# 检查组内是否有颜色
|
|
210
|
+
if not HEX_PREFIX.search(groupText):
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
colors = _extractColors(groupText)
|
|
214
|
+
|
|
215
|
+
# 检查 ) 后是否有 {
|
|
216
|
+
lineWithParen = lines[groupEnd].strip()
|
|
217
|
+
parenEnd = lineWithParen.index(')')
|
|
218
|
+
afterParen = lineWithParen[parenEnd + 1:].strip()
|
|
219
|
+
|
|
220
|
+
if afterParen.startswith('{'):
|
|
221
|
+
endIdx = _findClosingBrace(lines, groupEnd)
|
|
222
|
+
if endIdx is None:
|
|
223
|
+
return None
|
|
224
|
+
tokens = _parseBraceBlock(lines, groupEnd, endIdx)
|
|
225
|
+
return ColorConfig(colors['light'], colors['dark'], tokens), endIdx + 1
|
|
226
|
+
|
|
227
|
+
if not afterParen:
|
|
228
|
+
# 查找下一非空行
|
|
229
|
+
nextIdx = groupEnd + 1
|
|
230
|
+
while nextIdx < len(lines):
|
|
231
|
+
s = lines[nextIdx].strip()
|
|
232
|
+
if not s or s.startswith('//'):
|
|
233
|
+
nextIdx += 1
|
|
234
|
+
continue
|
|
235
|
+
if s.startswith('{'):
|
|
236
|
+
endIdx = _findClosingBrace(lines, nextIdx)
|
|
237
|
+
if endIdx is None:
|
|
238
|
+
return None
|
|
239
|
+
tokens = _parseBraceBlock(lines, nextIdx, endIdx)
|
|
240
|
+
return ColorConfig(colors['light'], colors['dark'], tokens), endIdx + 1
|
|
241
|
+
return None
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _findClosingBrace(lines, startIdx):
|
|
248
|
+
"""从 startIdx 开始查找包含 } 的行索引,返回索引或 None"""
|
|
249
|
+
for j in range(startIdx, len(lines)):
|
|
250
|
+
if '}' in lines[j]:
|
|
251
|
+
return j
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _parseBraceInline(text):
|
|
256
|
+
"""解析同行 { ... } 内的 tokens。返回 set 或 None(如果 } 不在同行)"""
|
|
257
|
+
braceStart = text.index('{')
|
|
258
|
+
braceEnd = text.find('}', braceStart)
|
|
259
|
+
if braceEnd == -1:
|
|
260
|
+
return None # } 不在同行
|
|
261
|
+
inner = text[braceStart + 1:braceEnd].strip()
|
|
262
|
+
return _extractTokenNames(inner)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _parseBraceBlock(lines, braceIdx, endIdx):
|
|
266
|
+
"""解析多行 { ... } 块内的 tokens"""
|
|
267
|
+
tokens = set()
|
|
268
|
+
for j in range(braceIdx, endIdx + 1):
|
|
269
|
+
s = lines[j].strip()
|
|
270
|
+
if j == braceIdx:
|
|
271
|
+
# 第一行:移除 { 前的内容
|
|
272
|
+
if '{' in s:
|
|
273
|
+
s = s[s.index('{') + 1:].strip()
|
|
274
|
+
if j == endIdx:
|
|
275
|
+
# 最后一行:移除 } 及之后的内容
|
|
276
|
+
if '}' in s:
|
|
277
|
+
s = s[:s.index('}')].strip()
|
|
278
|
+
if not s or s.startswith('//'):
|
|
279
|
+
continue
|
|
280
|
+
tokens.update(_extractTokenNames(s))
|
|
281
|
+
return tokens
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _tryInline(line):
|
|
285
|
+
"""尝试解析写法1行内(规则行末尾带 #color)"""
|
|
286
|
+
stripped = line.strip()
|
|
287
|
+
|
|
288
|
+
colors = _extractColors(stripped)
|
|
289
|
+
if colors['light'] is None and colors['dark'] is None:
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
# 移除颜色后的内容
|
|
293
|
+
afterColors = COLOR_PATTERN.sub('', stripped).strip()
|
|
294
|
+
|
|
295
|
+
# 必须是规则行
|
|
296
|
+
if not _isRuleLine(afterColors):
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
tokens = _extractTokensFromRule(afterColors)
|
|
300
|
+
return ColorConfig(colors['light'], colors['dark'], tokens)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _tryUpward(lines, idx):
|
|
304
|
+
"""尝试解析写法1向上匹配(独立颜色行,应用于上一规则行)"""
|
|
305
|
+
stripped = lines[idx].strip()
|
|
306
|
+
|
|
307
|
+
colors = _extractColors(stripped)
|
|
308
|
+
if colors['light'] is None and colors['dark'] is None:
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
# 移除颜色后必须为空(纯颜色行)
|
|
312
|
+
afterColors = COLOR_PATTERN.sub('', stripped).strip()
|
|
313
|
+
if afterColors:
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
# 查找上一非空、非注释行(跳过颜色行)
|
|
317
|
+
prevIdx = idx - 1
|
|
318
|
+
while prevIdx >= 0:
|
|
319
|
+
prevLine = lines[prevIdx].strip()
|
|
320
|
+
if not prevLine or prevLine.startswith('//'):
|
|
321
|
+
prevIdx -= 1
|
|
322
|
+
continue
|
|
323
|
+
# 跳过独立的颜色行
|
|
324
|
+
prevAfterColors = COLOR_PATTERN.sub('', prevLine).strip()
|
|
325
|
+
prevColors = _extractColors(prevLine)
|
|
326
|
+
if not prevAfterColors and (prevColors['light'] is not None or prevColors['dark'] is not None):
|
|
327
|
+
prevIdx -= 1
|
|
328
|
+
continue
|
|
329
|
+
if _isRuleLine(prevLine):
|
|
330
|
+
tokens = _extractTokensFromRule(prevLine)
|
|
331
|
+
return ColorConfig(colors['light'], colors['dark'], tokens)
|
|
332
|
+
break
|
|
333
|
+
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _findPostDescEnd(lines, startIdx):
|
|
338
|
+
"""如果 lines[startIdx] 开始一个写法2块,返回块最后一行的索引;否则 None"""
|
|
339
|
+
idx = startIdx
|
|
340
|
+
line = lines[idx].strip()
|
|
341
|
+
|
|
342
|
+
# 以 ( 开头的颜色组
|
|
343
|
+
if line.startswith('('):
|
|
344
|
+
# 查找 )
|
|
345
|
+
groupEnd = idx
|
|
346
|
+
while groupEnd < len(lines):
|
|
347
|
+
if ')' in lines[groupEnd]:
|
|
348
|
+
break
|
|
349
|
+
groupEnd += 1
|
|
350
|
+
if groupEnd >= len(lines):
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
groupText = ' '.join(lines[j].strip() for j in range(idx, groupEnd + 1))
|
|
354
|
+
if not HEX_PREFIX.search(groupText):
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
lineWithParen = lines[groupEnd].strip()
|
|
358
|
+
parenEnd = lineWithParen.index(')')
|
|
359
|
+
afterParen = lineWithParen[parenEnd + 1:].strip()
|
|
360
|
+
|
|
361
|
+
if afterParen.startswith('{'):
|
|
362
|
+
endIdx = _findClosingBrace(lines, groupEnd)
|
|
363
|
+
return endIdx if endIdx is not None else None
|
|
364
|
+
|
|
365
|
+
if not afterParen:
|
|
366
|
+
nextIdx = groupEnd + 1
|
|
367
|
+
while nextIdx < len(lines):
|
|
368
|
+
s = lines[nextIdx].strip()
|
|
369
|
+
if not s or s.startswith('//'):
|
|
370
|
+
nextIdx += 1
|
|
371
|
+
continue
|
|
372
|
+
if s.startswith('{'):
|
|
373
|
+
endIdx = _findClosingBrace(lines, nextIdx)
|
|
374
|
+
return endIdx if endIdx is not None else None
|
|
375
|
+
return None
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
# 以 #color 开头
|
|
379
|
+
if not HEX_PREFIX.match(line):
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
afterColors = COLOR_PATTERN.sub('', line).strip()
|
|
383
|
+
|
|
384
|
+
# #color { ... } 同行
|
|
385
|
+
if afterColors.startswith('{'):
|
|
386
|
+
endIdx = _findClosingBrace(lines, idx)
|
|
387
|
+
return endIdx if endIdx is not None else idx
|
|
388
|
+
|
|
389
|
+
# #color 独立行,查找下一非空行
|
|
390
|
+
if not afterColors:
|
|
391
|
+
nextIdx = idx + 1
|
|
392
|
+
while nextIdx < len(lines):
|
|
393
|
+
s = lines[nextIdx].strip()
|
|
394
|
+
if not s or s.startswith('//'):
|
|
395
|
+
nextIdx += 1
|
|
396
|
+
continue
|
|
397
|
+
if s.startswith('{'):
|
|
398
|
+
endIdx = _findClosingBrace(lines, nextIdx)
|
|
399
|
+
return endIdx if endIdx is not None else None
|
|
400
|
+
return None
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
return None
|