bubbleUI 0.4.4a1__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.
- bubble/QUI.py +4 -0
- bubble/__init__.py +8 -0
- bubble/cards.py +1503 -0
- bubble/dev_navview.py +4334 -0
- bubble/dev_titlebar.py +2900 -0
- bubble/glasskit/__init__.py +94 -0
- bubble/glasskit/config.py +77 -0
- bubble/glasskit/energy.py +202 -0
- bubble/glasskit/glass.py +1034 -0
- bubble/glasskit/gpu.py +43 -0
- bubble/glasskit/imaging.py +823 -0
- bubble/glasskit/settings.py +82 -0
- bubble/i18n.py +27 -0
- bubble/icon.py +261 -0
- bubble/levels/__init__.py +32 -0
- bubble/levels/component.py +106 -0
- bubble/levels/manager.py +334 -0
- bubble/styles.py +282 -0
- bubble/uiWindow.py +262 -0
- bubble/widgets.py +962 -0
- bubbleui-0.4.4a1.dist-info/METADATA +18 -0
- bubbleui-0.4.4a1.dist-info/RECORD +25 -0
- bubbleui-0.4.4a1.dist-info/WHEEL +5 -0
- bubbleui-0.4.4a1.dist-info/licenses/LICENSE +21 -0
- bubbleui-0.4.4a1.dist-info/top_level.txt +1 -0
bubble/levels/manager.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""pyside6_levels.manager —— “组件层级(level)”核心实现。
|
|
2
|
+
|
|
3
|
+
术语
|
|
4
|
+
====
|
|
5
|
+
- contribution(层级贡献):一个组件为它的**子控件**增加的层级,默认 0。
|
|
6
|
+
内置按钮等普通控件贡献为 0;自制卡片组件通常贡献为 1。
|
|
7
|
+
- raw / base level(基础层级):控件自身的层级 = 祖先链上所有组件贡献之和。
|
|
8
|
+
组件**自己的贡献不计入自身**,只作用于后代。
|
|
9
|
+
- defined levels(断点):用户在 QSS 里通过 ``[level="N"]`` 声明过的层级集合,
|
|
10
|
+
永远包含 0(默认配色)。
|
|
11
|
+
- style level(样式层级):按“就近向下填充”规则映射后的层级,写入控件的
|
|
12
|
+
动态属性 ``level``,供 QSS 选择器匹配;真实层级写入 ``rawLevel`` 便于调试。
|
|
13
|
+
|
|
14
|
+
填充规则
|
|
15
|
+
========
|
|
16
|
+
对某个断点集合 D(例如 {0, 1, 3})和控件的基础层级 r:
|
|
17
|
+
|
|
18
|
+
style(r) = max { d ∈ D | d <= r }
|
|
19
|
+
|
|
20
|
+
即:层级 1、3 有定义时,2 使用 1 的配色,4 使用 3 的配色;D 中只有 0
|
|
21
|
+
(用户没写任何额外断点)时,所有层级都落到 0,也就是默认规则。
|
|
22
|
+
|
|
23
|
+
为什么必须运行时计算
|
|
24
|
+
====================
|
|
25
|
+
QSS 没有“最近的更低断点”选择器;``[level=1]`` 这种**不带引号的数字值**
|
|
26
|
+
在 Qt 的 CSS 解析器里是非法语法,会导致整份样式表解析失败。因此本模块在
|
|
27
|
+
``setStyleSheet()`` 中把 ``[level=1]`` 规范化为 ``[level="1"]``,再把每个
|
|
28
|
+
控件解析后的样式层级写进 ``level`` 动态属性并重新 polish,从而让用户仍可
|
|
29
|
+
用自然写法书写选择器。
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import re
|
|
35
|
+
from typing import Iterable, Optional
|
|
36
|
+
|
|
37
|
+
from PySide6.QtCore import QEvent, QObject, Qt, QTimer, Signal
|
|
38
|
+
from PySide6.QtWidgets import QApplication, QWidget
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"LevelManager",
|
|
42
|
+
"normalize_level_selectors",
|
|
43
|
+
"parse_defined_levels",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
# 私有状态挂在控件实例上(不用动态属性,避免污染 QSS 选择器命名空间)
|
|
47
|
+
_CONTRIB_ATTR = "_pyside6_level_contribution"
|
|
48
|
+
_OVERRIDE_ATTR = "_pyside6_level_override"
|
|
49
|
+
_MANAGER_ATTR = "_pyside6_level_manager"
|
|
50
|
+
|
|
51
|
+
#: 不带引号的层级选择器(Qt 不接受,需要规范化)
|
|
52
|
+
_UNQUOTED_LEVEL_RE = re.compile(r"\[\s*level\s*=\s*(-?\d+)\s*\]")
|
|
53
|
+
#: 任意写法的层级选择器(用于收集断点)
|
|
54
|
+
_LEVEL_VALUE_RE = re.compile(
|
|
55
|
+
r"""\[\s*level\s*=\s*(?:"(-?\d+)"|'(-?\d+)'|(-?\d+))\s*\]"""
|
|
56
|
+
)
|
|
57
|
+
_QSS_COMMENT_RE = re.compile(r"/\*.*?\*/", re.S)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _strip_comments(qss: str) -> str:
|
|
61
|
+
"""去掉 QSS 注释,避免把注释里的示例选择器当成断点。"""
|
|
62
|
+
return _QSS_COMMENT_RE.sub("", qss)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def normalize_level_selectors(qss: str) -> str:
|
|
66
|
+
"""把 Qt 不接受的 ``[level=1]`` 改写成 ``[level="1"]``。
|
|
67
|
+
|
|
68
|
+
对已经带引号、以及非 level 的属性选择器不做任何改动。
|
|
69
|
+
"""
|
|
70
|
+
return _UNQUOTED_LEVEL_RE.sub(
|
|
71
|
+
lambda m: '[level="%d"]' % max(0, int(m.group(1))), qss
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_defined_levels(qss: str) -> "list[int]":
|
|
76
|
+
"""从 QSS 文本中收集所有 ``[level=N]`` 断点,排序并保证包含 0。"""
|
|
77
|
+
text = _strip_comments(qss)
|
|
78
|
+
levels = {0}
|
|
79
|
+
for match in _LEVEL_VALUE_RE.finditer(text):
|
|
80
|
+
for group in match.groups():
|
|
81
|
+
if group is not None:
|
|
82
|
+
levels.add(max(0, int(group)))
|
|
83
|
+
break
|
|
84
|
+
return sorted(levels)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class LevelManager(QObject):
|
|
88
|
+
"""应用级单例:维护控件层级、解析断点、刷新 QSS 样式。"""
|
|
89
|
+
|
|
90
|
+
#: 任意控件的层级或断点变化、并完成一次重算后发出(可用于调试面板刷新)
|
|
91
|
+
levels_changed = Signal()
|
|
92
|
+
|
|
93
|
+
def __init__(self, app: Optional[QApplication] = None) -> None:
|
|
94
|
+
app = app or QApplication.instance()
|
|
95
|
+
if app is None:
|
|
96
|
+
raise RuntimeError("请先创建 QApplication,再使用 LevelManager")
|
|
97
|
+
super().__init__(app)
|
|
98
|
+
self._app: QApplication = app
|
|
99
|
+
self._levels: "list[int]" = [0]
|
|
100
|
+
self._raw_stylesheet: str = ""
|
|
101
|
+
self._in_recompute: bool = False
|
|
102
|
+
self._pending: bool = False
|
|
103
|
+
|
|
104
|
+
self._timer = QTimer(self)
|
|
105
|
+
self._timer.setSingleShot(True)
|
|
106
|
+
self._timer.setInterval(0)
|
|
107
|
+
self._timer.timeout.connect(self.recompute)
|
|
108
|
+
|
|
109
|
+
app.installEventFilter(self)
|
|
110
|
+
# 接管之前已经创建的控件
|
|
111
|
+
self.recompute()
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------------------------------ 单例
|
|
114
|
+
@classmethod
|
|
115
|
+
def instance(cls) -> "LevelManager":
|
|
116
|
+
"""取得(必要时创建)当前 QApplication 对应的管理器。"""
|
|
117
|
+
app = QApplication.instance()
|
|
118
|
+
if app is None:
|
|
119
|
+
raise RuntimeError("请先创建 QApplication,再使用 LevelManager")
|
|
120
|
+
manager = getattr(app, _MANAGER_ATTR, None)
|
|
121
|
+
if manager is None:
|
|
122
|
+
manager = cls(app)
|
|
123
|
+
setattr(app, _MANAGER_ATTR, manager)
|
|
124
|
+
return manager
|
|
125
|
+
|
|
126
|
+
# ------------------------------------------------------------ 样式表/断点
|
|
127
|
+
@property
|
|
128
|
+
def stylesheet(self) -> str:
|
|
129
|
+
"""最近一次通过 :meth:`setStyleSheet` 传入的原始 QSS。"""
|
|
130
|
+
return self._raw_stylesheet
|
|
131
|
+
|
|
132
|
+
def defined_levels(self) -> "tuple[int, ...]":
|
|
133
|
+
"""当前断点集合(升序,必含 0)。"""
|
|
134
|
+
return tuple(self._levels)
|
|
135
|
+
|
|
136
|
+
def set_defined_levels(self, levels: Iterable[int]) -> None:
|
|
137
|
+
"""手动指定断点集合。
|
|
138
|
+
|
|
139
|
+
适用于样式表不由 :meth:`setStyleSheet` 管理、或需要额外断点的场景。
|
|
140
|
+
调用 :meth:`setStyleSheet` 会以 QSS 中解析出的断点**替换**本集合。
|
|
141
|
+
"""
|
|
142
|
+
new_levels = sorted({0} | {max(0, int(level)) for level in levels})
|
|
143
|
+
if new_levels != self._levels:
|
|
144
|
+
self._levels = new_levels
|
|
145
|
+
self.recompute()
|
|
146
|
+
|
|
147
|
+
def setStyleSheet(self, qss: str) -> None:
|
|
148
|
+
"""设置应用样式表:收集断点 → 规范化选择器 → 设置样式表 → 重算。
|
|
149
|
+
|
|
150
|
+
这是推荐的唯一入口。若直接调用 ``app.setStyleSheet()``,管理器无法
|
|
151
|
+
知道断点,也无法把 ``[level=1]`` 规范化成 Qt 认识的写法。
|
|
152
|
+
"""
|
|
153
|
+
self._raw_stylesheet = qss
|
|
154
|
+
normalized = normalize_level_selectors(qss)
|
|
155
|
+
self._levels = parse_defined_levels(normalized)
|
|
156
|
+
# Qt 在 setStyleSheet 时会对已有控件整体重新应用样式;随后 recompute
|
|
157
|
+
# 把新算出的 level 写进控件,并对发生变化的控件重新 polish。
|
|
158
|
+
self._app.setStyleSheet(normalized)
|
|
159
|
+
self.recompute()
|
|
160
|
+
|
|
161
|
+
# ------------------------------------------------------------------ 层级
|
|
162
|
+
def contribution(self, widget: QWidget) -> int:
|
|
163
|
+
"""控件为其子控件增加的层级(默认 0)。"""
|
|
164
|
+
return int(getattr(widget, _CONTRIB_ATTR, 0))
|
|
165
|
+
|
|
166
|
+
def set_contribution(self, widget: QWidget, contribution: int) -> None:
|
|
167
|
+
"""设置控件的层级贡献(≥0)。自制组件一般在 ``__init__`` 里调用。"""
|
|
168
|
+
contribution = max(0, int(contribution))
|
|
169
|
+
if getattr(widget, _CONTRIB_ATTR, None) != contribution:
|
|
170
|
+
setattr(widget, _CONTRIB_ATTR, contribution)
|
|
171
|
+
self.schedule()
|
|
172
|
+
|
|
173
|
+
def override(self, widget: QWidget) -> Optional[int]:
|
|
174
|
+
"""控件的绝对基础层级覆盖;未设置时为 None。"""
|
|
175
|
+
return getattr(widget, _OVERRIDE_ATTR, None)
|
|
176
|
+
|
|
177
|
+
def set_override(self, widget: QWidget, level: Optional[int]) -> None:
|
|
178
|
+
"""强制指定控件的基础层级(绝对覆盖,不看父链)。
|
|
179
|
+
|
|
180
|
+
典型用途:
|
|
181
|
+
- 独立创建、尚无 parent 的控件;
|
|
182
|
+
- 挂在主窗口下、但视觉上属于另一个层级的弹窗/浮层;
|
|
183
|
+
- 组件内单独调整某个子控件的层级。
|
|
184
|
+
传 ``None`` 取消覆盖。
|
|
185
|
+
"""
|
|
186
|
+
if level is None:
|
|
187
|
+
if getattr(widget, _OVERRIDE_ATTR, None) is not None:
|
|
188
|
+
delattr(widget, _OVERRIDE_ATTR)
|
|
189
|
+
self.schedule()
|
|
190
|
+
return
|
|
191
|
+
level = max(0, int(level))
|
|
192
|
+
if getattr(widget, _OVERRIDE_ATTR, None) != level:
|
|
193
|
+
setattr(widget, _OVERRIDE_ATTR, level)
|
|
194
|
+
self.schedule()
|
|
195
|
+
|
|
196
|
+
def raw_level(self, widget: QWidget) -> int:
|
|
197
|
+
"""控件自身的基础层级(祖先贡献之和;覆盖优先;不含自身贡献)。"""
|
|
198
|
+
return self._raw_level(widget, {})
|
|
199
|
+
|
|
200
|
+
def style_level(self, raw_level: int) -> int:
|
|
201
|
+
"""把基础层级按断点填充规则映射为样式层级。"""
|
|
202
|
+
raw_level = max(0, int(raw_level))
|
|
203
|
+
result = 0
|
|
204
|
+
for level in self._levels:
|
|
205
|
+
if level <= raw_level:
|
|
206
|
+
result = level
|
|
207
|
+
else:
|
|
208
|
+
break
|
|
209
|
+
return result
|
|
210
|
+
|
|
211
|
+
def level(self, widget: QWidget) -> int:
|
|
212
|
+
"""控件当前应使用的样式层级(实时计算,不依赖事件循环)。"""
|
|
213
|
+
return self.style_level(self.raw_level(widget))
|
|
214
|
+
|
|
215
|
+
# ------------------------------------------------------------------ 刷新
|
|
216
|
+
def schedule(self) -> None:
|
|
217
|
+
"""请求一次合并重算(0ms 单次定时器,适合 parent 尚未设置等异步场景)。"""
|
|
218
|
+
if not self._timer.isActive():
|
|
219
|
+
self._timer.start()
|
|
220
|
+
|
|
221
|
+
def flush(self) -> None:
|
|
222
|
+
"""立即执行挂起的重算(测试、截图、需要同步读取 ``level`` 属性时使用)。"""
|
|
223
|
+
self._timer.stop()
|
|
224
|
+
self._pending = False
|
|
225
|
+
self.recompute()
|
|
226
|
+
|
|
227
|
+
def recompute(self) -> None:
|
|
228
|
+
"""重算所有控件的 rawLevel / level 动态属性,并在变化时重新 polish。"""
|
|
229
|
+
if self._in_recompute:
|
|
230
|
+
self._pending = True
|
|
231
|
+
return
|
|
232
|
+
self._timer.stop()
|
|
233
|
+
self._in_recompute = True
|
|
234
|
+
changed_widgets: "list[QWidget]" = []
|
|
235
|
+
try:
|
|
236
|
+
raw_cache: "dict[int, int]" = {}
|
|
237
|
+
for widget in QApplication.allWidgets():
|
|
238
|
+
try:
|
|
239
|
+
raw = self._raw_level(widget, raw_cache)
|
|
240
|
+
target = self.style_level(raw)
|
|
241
|
+
old_raw = widget.property("rawLevel")
|
|
242
|
+
old_level = widget.property("level")
|
|
243
|
+
if old_raw != raw:
|
|
244
|
+
widget.setProperty("rawLevel", raw)
|
|
245
|
+
if old_level != target:
|
|
246
|
+
widget.setProperty("level", target)
|
|
247
|
+
if old_raw != raw or old_level != target:
|
|
248
|
+
changed_widgets.append(widget)
|
|
249
|
+
except RuntimeError: # 底层 C++ 对象已销毁
|
|
250
|
+
continue
|
|
251
|
+
finally:
|
|
252
|
+
self._in_recompute = False
|
|
253
|
+
|
|
254
|
+
if changed_widgets:
|
|
255
|
+
# 除控件自身外,还要刷新它的后代:祖先的属性可能参与
|
|
256
|
+
# `QFrame[level="1"] QPushButton` 这类后代选择器。
|
|
257
|
+
self._refresh_styles(changed_widgets)
|
|
258
|
+
self.levels_changed.emit()
|
|
259
|
+
if self._pending:
|
|
260
|
+
self._pending = False
|
|
261
|
+
self.schedule()
|
|
262
|
+
|
|
263
|
+
def _refresh_styles(self, changed_widgets: "list[QWidget]") -> None:
|
|
264
|
+
"""对“自身或祖先发生变化、且已经 polish 过”的控件重新 polish。
|
|
265
|
+
|
|
266
|
+
从未 polish 过的控件不用处理:首次 show / 挂载 / grab 时的 polish
|
|
267
|
+
会读到最新的 ``level`` 属性。遍历整棵控件树一次,避免逐个子树重复遍历。
|
|
268
|
+
"""
|
|
269
|
+
changed_ids = {id(widget) for widget in changed_widgets}
|
|
270
|
+
children: "dict[int, list[QWidget]]" = {}
|
|
271
|
+
roots: "list[QWidget]" = []
|
|
272
|
+
for widget in QApplication.allWidgets():
|
|
273
|
+
try:
|
|
274
|
+
parent = widget.parentWidget()
|
|
275
|
+
except RuntimeError:
|
|
276
|
+
continue
|
|
277
|
+
if parent is None:
|
|
278
|
+
roots.append(widget)
|
|
279
|
+
else:
|
|
280
|
+
children.setdefault(id(parent), []).append(widget)
|
|
281
|
+
|
|
282
|
+
stack = [(root, False) for root in roots]
|
|
283
|
+
while stack:
|
|
284
|
+
widget, ancestor_changed = stack.pop()
|
|
285
|
+
dirty = ancestor_changed or id(widget) in changed_ids
|
|
286
|
+
if dirty:
|
|
287
|
+
try:
|
|
288
|
+
if widget.testAttribute(Qt.WidgetAttribute.WA_WState_Polished):
|
|
289
|
+
self._repolish(widget)
|
|
290
|
+
except RuntimeError:
|
|
291
|
+
continue
|
|
292
|
+
for child in children.get(id(widget), ()):
|
|
293
|
+
stack.append((child, dirty))
|
|
294
|
+
|
|
295
|
+
# ------------------------------------------------------------------ 内部
|
|
296
|
+
def _raw_level(self, widget: QWidget, cache: "dict[int, int]") -> int:
|
|
297
|
+
key = id(widget)
|
|
298
|
+
cached = cache.get(key)
|
|
299
|
+
if cached is not None:
|
|
300
|
+
return cached
|
|
301
|
+
override = self.override(widget)
|
|
302
|
+
if override is not None:
|
|
303
|
+
value = override
|
|
304
|
+
else:
|
|
305
|
+
parent = widget.parentWidget()
|
|
306
|
+
if parent is None:
|
|
307
|
+
value = 0
|
|
308
|
+
else:
|
|
309
|
+
value = self._raw_level(parent, cache) + self.contribution(parent)
|
|
310
|
+
cache[key] = value
|
|
311
|
+
return value
|
|
312
|
+
|
|
313
|
+
@staticmethod
|
|
314
|
+
def _repolish(widget: QWidget) -> None:
|
|
315
|
+
"""动态属性变化后必须重新 polish,QSS 属性选择器才会重新匹配。"""
|
|
316
|
+
try:
|
|
317
|
+
style = widget.style()
|
|
318
|
+
style.unpolish(widget)
|
|
319
|
+
style.polish(widget)
|
|
320
|
+
widget.update()
|
|
321
|
+
except RuntimeError:
|
|
322
|
+
pass
|
|
323
|
+
|
|
324
|
+
# ------------------------------------------------------------------ 事件
|
|
325
|
+
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
|
|
326
|
+
# parent 变化、子控件增删都会改变层级;用 0ms 定时器合并,避免构造期抖动
|
|
327
|
+
event_type = event.type()
|
|
328
|
+
if event_type in (
|
|
329
|
+
QEvent.Type.ParentChange,
|
|
330
|
+
QEvent.Type.ChildAdded,
|
|
331
|
+
QEvent.Type.ChildRemoved,
|
|
332
|
+
):
|
|
333
|
+
self.schedule()
|
|
334
|
+
return False
|
bubble/styles.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
'''
|
|
2
|
+
组件样式
|
|
3
|
+
'''
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from .QUI import *
|
|
8
|
+
|
|
9
|
+
__all__ = ['StyleReplaceMode', 'StyleSheet', 'styles', 'indexes', 'maps', 'style_path']
|
|
10
|
+
|
|
11
|
+
class StyleReplaceMode(Enum):
|
|
12
|
+
'''
|
|
13
|
+
特殊样式替换模式
|
|
14
|
+
'''
|
|
15
|
+
ALL = 'all' # 全部替换
|
|
16
|
+
|
|
17
|
+
class StyleSheet:
|
|
18
|
+
'''CSS序列化工具类,支持CSS与JSON互相转换'''
|
|
19
|
+
|
|
20
|
+
def __init__(self, css_text):
|
|
21
|
+
'''
|
|
22
|
+
初始化CSS序列化器
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
css_text: CSS文本
|
|
26
|
+
'''
|
|
27
|
+
# 编译正则表达式以提高性能
|
|
28
|
+
self.property_pattern = re.compile(r'\s*([^:]+)\s*:\s*([^;]+)\s*(?:;|$)')
|
|
29
|
+
self.comment_pattern = re.compile(r'/\*.*?\*/', re.DOTALL)
|
|
30
|
+
self.empty_line_pattern = re.compile(r'\n\s*\n')
|
|
31
|
+
|
|
32
|
+
# 变量
|
|
33
|
+
self.css_text = css_text
|
|
34
|
+
self.css_data = self.serialize(css_text)
|
|
35
|
+
|
|
36
|
+
def _clean_css(self, css: str) -> str:
|
|
37
|
+
'''
|
|
38
|
+
清理CSS:移除注释、多余空格和空行
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
css: 原始CSS字符串
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
清理后的CSS字符串
|
|
45
|
+
'''
|
|
46
|
+
# 移除CSS注释
|
|
47
|
+
css = self.comment_pattern.sub('', css)
|
|
48
|
+
|
|
49
|
+
# 移除多余空格(保留必要的空格,如属性值中的空格)
|
|
50
|
+
# 这里我们只移除选择器和属性名周围的空格
|
|
51
|
+
css = css.strip()
|
|
52
|
+
|
|
53
|
+
# 移除空行
|
|
54
|
+
css = self.empty_line_pattern.sub('\n', css)
|
|
55
|
+
|
|
56
|
+
return css
|
|
57
|
+
|
|
58
|
+
def _parse_rule(self, selector: str, rule_body: str) -> dict[str, str]:
|
|
59
|
+
'''
|
|
60
|
+
解析单个CSS规则体
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
selector: CSS选择器
|
|
64
|
+
rule_body: 规则体内容
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
解析后的属性字典
|
|
68
|
+
'''
|
|
69
|
+
properties = {}
|
|
70
|
+
|
|
71
|
+
# 查找所有属性
|
|
72
|
+
matches = self.property_pattern.findall(rule_body)
|
|
73
|
+
|
|
74
|
+
for prop_name, prop_value in matches:
|
|
75
|
+
# 清理属性名和值
|
|
76
|
+
prop_name = prop_name.strip()
|
|
77
|
+
prop_value = prop_value.strip()
|
|
78
|
+
|
|
79
|
+
if prop_name and prop_value:
|
|
80
|
+
properties[prop_name] = prop_value
|
|
81
|
+
|
|
82
|
+
return properties
|
|
83
|
+
|
|
84
|
+
def _get_value_by_indices(self, indices, multi_list):
|
|
85
|
+
'''
|
|
86
|
+
根据索引列表逐层访问多维列表
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
indices: 一维索引列表,如 [0, 1]
|
|
90
|
+
multi_list: 多维列表,如 [[1, 2], [3, 4]]
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
对应位置的值
|
|
94
|
+
'''
|
|
95
|
+
result = multi_list
|
|
96
|
+
for index in indices:
|
|
97
|
+
result = result[index]
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
def _update_nested_dict(self, keys, nested_dict, new_value):
|
|
101
|
+
'''
|
|
102
|
+
根据键列表更新嵌套字典中的值,中间层级不存在时会自动创建
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
keys: 键列表,如 ['a', 'b']
|
|
106
|
+
nested_dict: 嵌套字典,如 {'a': {'b': 'c'}}
|
|
107
|
+
new_value: 要设置的新值,如 'd'
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
修改后的字典(原字典不会被修改)
|
|
111
|
+
'''
|
|
112
|
+
result = nested_dict.copy()
|
|
113
|
+
current = result
|
|
114
|
+
|
|
115
|
+
# 遍历到倒数第二个键,缺失时自动创建
|
|
116
|
+
for key in keys[:-1]:
|
|
117
|
+
child = current.get(key)
|
|
118
|
+
if not isinstance(child, dict):
|
|
119
|
+
child = {}
|
|
120
|
+
else:
|
|
121
|
+
child = child.copy()
|
|
122
|
+
current[key] = child
|
|
123
|
+
current = child
|
|
124
|
+
|
|
125
|
+
# 用最后一个键设置新值
|
|
126
|
+
current[keys[-1]] = new_value
|
|
127
|
+
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
def serialize_to_jsonstr(self, css: str, indent: None | int = None) -> str:
|
|
131
|
+
'''
|
|
132
|
+
将CSS序列化为JSON字符串
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
css: CSS字符串
|
|
136
|
+
indent: JSON缩进,None表示不缩进
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
JSON格式字符串
|
|
140
|
+
'''
|
|
141
|
+
result = {}
|
|
142
|
+
|
|
143
|
+
# 清理CSS
|
|
144
|
+
css = self._clean_css(css)
|
|
145
|
+
|
|
146
|
+
# 分割规则块
|
|
147
|
+
# 查找所有规则:选择器 { 规则体 }
|
|
148
|
+
rule_start = 0
|
|
149
|
+
depth = 0 # 花括号深度
|
|
150
|
+
current_selector = None
|
|
151
|
+
current_rule_start = 0
|
|
152
|
+
|
|
153
|
+
for i, char in enumerate(css):
|
|
154
|
+
if char == '{':
|
|
155
|
+
if depth == 0:
|
|
156
|
+
# 找到选择器
|
|
157
|
+
selector_text = css[rule_start:i].strip()
|
|
158
|
+
if selector_text:
|
|
159
|
+
current_selector = selector_text
|
|
160
|
+
current_rule_start = i + 1
|
|
161
|
+
depth += 1
|
|
162
|
+
elif char == '}':
|
|
163
|
+
depth -= 1
|
|
164
|
+
if depth == 0 and current_selector:
|
|
165
|
+
# 规则结束
|
|
166
|
+
rule_body = css[current_rule_start:i]
|
|
167
|
+
|
|
168
|
+
# 解析规则
|
|
169
|
+
properties = self._parse_rule(current_selector, rule_body)
|
|
170
|
+
if properties:
|
|
171
|
+
# 处理多个选择器(如 'p, a, div')
|
|
172
|
+
selectors = [s.strip() for s in current_selector.split(',')]
|
|
173
|
+
for selector in selectors:
|
|
174
|
+
if selector:
|
|
175
|
+
if selector in result:
|
|
176
|
+
# 合并重复选择器的属性
|
|
177
|
+
result[selector].update(properties)
|
|
178
|
+
else:
|
|
179
|
+
result[selector] = properties.copy()
|
|
180
|
+
|
|
181
|
+
# 重置
|
|
182
|
+
current_selector = None
|
|
183
|
+
rule_start = i + 1
|
|
184
|
+
|
|
185
|
+
return json.dumps(result, indent=indent)
|
|
186
|
+
|
|
187
|
+
def serialize(self, css: str) -> dict:
|
|
188
|
+
'''
|
|
189
|
+
将CSS序列化为字典
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
css: CSS字符串
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
序列化后的字典
|
|
196
|
+
'''
|
|
197
|
+
json_str = self.serialize_to_jsonstr(css)
|
|
198
|
+
try:
|
|
199
|
+
return json.loads(json_str)
|
|
200
|
+
except json.JSONDecodeError:
|
|
201
|
+
raise ValueError(f'无效的JSON格式: {json_str}')
|
|
202
|
+
|
|
203
|
+
def deserialize(
|
|
204
|
+
self,
|
|
205
|
+
json_input: str | dict,
|
|
206
|
+
) -> str:
|
|
207
|
+
'''
|
|
208
|
+
将JSON反序列化为CSS
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
json_input: JSON字符串或字典
|
|
212
|
+
style: CSS格式化样式
|
|
213
|
+
indent: 缩进空格数(仅当style为PRETTY时有效)
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
CSS字符串
|
|
217
|
+
'''
|
|
218
|
+
try:
|
|
219
|
+
# 解析JSON输入
|
|
220
|
+
if isinstance(json_input, str):
|
|
221
|
+
data = json.loads(json_input)
|
|
222
|
+
elif isinstance(json_input, dict):
|
|
223
|
+
data = json_input
|
|
224
|
+
else:
|
|
225
|
+
raise ValueError('输入必须是JSON字符串或字典')
|
|
226
|
+
|
|
227
|
+
css_rules = []
|
|
228
|
+
|
|
229
|
+
for selector, properties in data.items():
|
|
230
|
+
if not isinstance(properties, dict):
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
# 最小化格式:无空格
|
|
234
|
+
rule = f'{selector}{{'
|
|
235
|
+
rule += ';'.join([f'{prop}:{value}' for prop, value in properties.items()])
|
|
236
|
+
rule += ';}'
|
|
237
|
+
css_rules.append(rule)
|
|
238
|
+
return ''.join(css_rules)
|
|
239
|
+
except json.JSONDecodeError as e:
|
|
240
|
+
raise ValueError(f'无效的JSON格式: {e}')
|
|
241
|
+
|
|
242
|
+
def refresh(self, css_text: str):
|
|
243
|
+
'''
|
|
244
|
+
刷新CSS文本并重新解析
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
css_text: 新的CSS文本
|
|
248
|
+
'''
|
|
249
|
+
self.css_text = css_text
|
|
250
|
+
self.css_data = self.serialize(css_text)
|
|
251
|
+
|
|
252
|
+
def replace(self, index, old_value: str | StyleReplaceMode, new_value: str, output_json: bool = True) -> dict:
|
|
253
|
+
'''
|
|
254
|
+
替换CSS数据中的值
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
index: 要替换值的索引
|
|
258
|
+
old_value: 旧值;为 StyleReplaceMode.ALL 时直接覆盖,键不存在时自动创建
|
|
259
|
+
new_value: 新值
|
|
260
|
+
output_json: True 返回字典,False 返回 StyleSheet
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
新的CSS数据字典或 StyleSheet
|
|
264
|
+
'''
|
|
265
|
+
data = self.css_data.copy()
|
|
266
|
+
|
|
267
|
+
if old_value == StyleReplaceMode.ALL:
|
|
268
|
+
# 全匹配:即使键不存在也会创建
|
|
269
|
+
new_data = self._update_nested_dict(index, data, new_value)
|
|
270
|
+
if output_json:
|
|
271
|
+
return new_data
|
|
272
|
+
return StyleSheet(self.deserialize(new_data))
|
|
273
|
+
|
|
274
|
+
value = self._get_value_by_indices(index, data)
|
|
275
|
+
|
|
276
|
+
if not isinstance(value, str):
|
|
277
|
+
raise ValueError(f'索引{index}处的值不是字符串')
|
|
278
|
+
|
|
279
|
+
new_data = self._update_nested_dict(index, data, value.replace(old_value, new_value))
|
|
280
|
+
if output_json:
|
|
281
|
+
return new_data
|
|
282
|
+
return StyleSheet(self.deserialize(new_data))
|