openpuppet-language 2.0__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.
Files changed (40) hide show
  1. openpuppet_language-2.0.data/data/share/puppet/conformance/README.md +274 -0
  2. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/binding.json +238 -0
  3. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/capabilities.json +155 -0
  4. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/grammar.json +248 -0
  5. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/interaction.json +220 -0
  6. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/listen.json +133 -0
  7. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/platform.json +274 -0
  8. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/probes.json +117 -0
  9. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/render.json +180 -0
  10. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/tabs.json +39 -0
  11. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/templates.json +143 -0
  12. openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_bad_result.py +11 -0
  13. openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_basic.py +24 -0
  14. openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_deps_mismatch.py +11 -0
  15. openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_no_doc.py +10 -0
  16. openpuppet_language-2.0.data/data/share/puppet/conformance/runner.py +721 -0
  17. openpuppet_language-2.0.data/data/share/puppet/spec/01-grammar.md +283 -0
  18. openpuppet_language-2.0.data/data/share/puppet/spec/02-ir.md +77 -0
  19. openpuppet_language-2.0.data/data/share/puppet/spec/03-semantics.md +167 -0
  20. openpuppet_language-2.0.data/data/share/puppet/spec/04-vocabulary.md +227 -0
  21. openpuppet_language-2.0.data/data/share/puppet/spec/05-render-contract.md +126 -0
  22. openpuppet_language-2.0.data/data/share/puppet/spec/06-diagnostics.md +151 -0
  23. openpuppet_language-2.0.data/data/share/puppet/spec/README.md +73 -0
  24. openpuppet_language-2.0.dist-info/METADATA +50 -0
  25. openpuppet_language-2.0.dist-info/RECORD +40 -0
  26. openpuppet_language-2.0.dist-info/WHEEL +5 -0
  27. openpuppet_language-2.0.dist-info/entry_points.txt +2 -0
  28. openpuppet_language-2.0.dist-info/top_level.txt +1 -0
  29. puppet/__init__.py +19 -0
  30. puppet/adapter.py +112 -0
  31. puppet/capabilities.py +172 -0
  32. puppet/cli.py +132 -0
  33. puppet/diag.py +42 -0
  34. puppet/engine.py +1038 -0
  35. puppet/eval.py +243 -0
  36. puppet/ir.py +538 -0
  37. puppet/lang.py +755 -0
  38. puppet/raster_adapter.py +348 -0
  39. puppet/tk_adapter.py +374 -0
  40. puppet/vocab.py +147 -0
puppet/tk_adapter.py ADDED
@@ -0,0 +1,374 @@
1
+ """conformance 适配器(Tk 渲染器):行分隔 JSON over stdio + **真实几何**。
2
+
3
+ 与 `adapter.py` 同一协议(`conformance/README.md` 第 4 节)。区别:
4
+
5
+ - `hello` 声明 `geometry=true`(`winfo` 实测矩形)与一个**子集**能力声明——
6
+ 这正是"声明即 oracle"的靶子:未声明的词汇必须可见降级(引擎按声明自动发)。
7
+ - 布局按规范 05 第 2 节的可观察语义实现:col/row 主轴、gap(相邻间隔)、
8
+ flex(剩余空间)、w/h(固定尺寸)、x/y(脱离流动)、visible=false(不占位)。
9
+ - Tk 是标准库(零第三方依赖不破);需要显示器,故 `headless=false`。
10
+
11
+ 用法:
12
+ python conformance/runner.py --impl-module puppet.tk_adapter
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import sys
19
+
20
+ from . import adapter as base
21
+ from .diag import ERROR, INFO, Diagnostic
22
+ from .engine import Engine
23
+ from .lang import Lit
24
+
25
+ try:
26
+ import tkinter as tk
27
+ except ImportError: # pragma: no cover - 无 Tcl/Tk 的环境
28
+ tk = None
29
+
30
+ # 能力声明:**只声明真实现了的**。规范允许子集实现——代价是未声明词汇
31
+ # 必须可见降级(引擎按这份声明自动产生 DEGRADED_FEATURE)。
32
+ RENDERING = {
33
+ "controls": ["window", "col", "row", "template",
34
+ "text", "button", "spacer", "divider", "input", "checkbox"],
35
+ "attributes": ["gap", "w", "h", "x", "y", "bgcolor", "fg",
36
+ "text", "title", "flex"],
37
+ "animations": [],
38
+ "icons": [],
39
+ "geometry": True,
40
+ "snapshot": False,
41
+ "interaction": True,
42
+ "headless": False,
43
+ "notes": "Tk 参考渲染器:容器与窗口的 w/h 按像素实现;文本类控件按内容自尺寸;"
44
+ "图标不做位图(一律降级);未声明的词汇一律可见降级;"
45
+ "用户动作经真实 Tk 事件投递为引擎事件。",
46
+ }
47
+
48
+ _CONTAINERS = ("window", "col", "row")
49
+ _GAP_DEFAULT = 8 # 规范 04 第 3.2 节:col/row/list 默认 gap=8
50
+
51
+
52
+ class TkRenderer:
53
+ """把程序 IR 镜像成 Tk 部件树,并量出真实矩形。每次请求整体重建。"""
54
+
55
+ def __init__(self, engine: Engine):
56
+ if tk is None:
57
+ raise RuntimeError("本环境没有 Tcl/Tk,无法运行 Tk 渲染器")
58
+ self.engine = engine
59
+ self.root = tk.Tk()
60
+ self.root.withdraw()
61
+ self.widgets: dict = {}
62
+ self.vars: dict = {}
63
+ self.values: dict = {}
64
+ self.origin = None
65
+ # 能力声明替身可关掉几何(双向"按声明行事")。
66
+ self.geometry_on = True
67
+ # 观测替身(load.renderGeometry):一旦提供,它就是真值——
68
+ # 与业务替身 capabilities 同一待遇,优先于真实测量。
69
+ self.double = None
70
+
71
+ def close(self):
72
+ try:
73
+ self.root.destroy()
74
+ except Exception: # noqa: BLE001
75
+ pass
76
+
77
+ def pump(self):
78
+ """让 Tk 把布局算完(不进 mainloop,事件驱动全靠它)。"""
79
+ self.root.update()
80
+
81
+ # ------------------------------------------------------------ 镜像
82
+
83
+ def sync(self):
84
+ for w in self.widgets.values():
85
+ try:
86
+ w.destroy()
87
+ except Exception: # noqa: BLE001
88
+ pass
89
+ self.widgets = {}
90
+ self.origin = None
91
+ # 求值后的属性:渲染器必须反映**绑定值**,而不是源码里的字面量
92
+ self.values = self.engine._observe_attrs()
93
+ program = self.engine.program
94
+ root = program.nodes.get("root")
95
+ if root is None:
96
+ return
97
+ for wid in root.children:
98
+ node = program.nodes.get(wid)
99
+ if node is not None and node.type == "window":
100
+ widget = self._build(node, None)
101
+ if self.origin is None and widget is not None:
102
+ self.origin = widget
103
+
104
+ def _build(self, node, parent):
105
+ typ = node.type
106
+ if typ == "template":
107
+ return None # 页面内不渲染(规范 04 第 2 节);行实例由 list 承担
108
+ if typ == "window":
109
+ widget = tk.Toplevel(self.root)
110
+ title = self._static(node, "title")
111
+ if isinstance(title, str):
112
+ widget.title(title)
113
+ width, height = self._static(node, "w"), self._static(node, "h")
114
+ if isinstance(width, (int, float)) and isinstance(height, (int, float)):
115
+ widget.geometry("%dx%d" % (int(width), int(height)))
116
+ self._style(node, widget)
117
+ else:
118
+ widget = self._make(typ, node, parent)
119
+ self._style(node, widget)
120
+ self.widgets[node.id] = widget
121
+ self._bind_events(node, widget)
122
+ for cid in node.children:
123
+ child = self.engine.program.nodes.get(cid)
124
+ if child is None or child.type == "template":
125
+ continue
126
+ self._build(child, widget)
127
+ if typ in _CONTAINERS:
128
+ self._layout_children(node, widget)
129
+ return widget
130
+
131
+ def _make(self, typ, node, parent):
132
+ key = "#" + node.id
133
+ text = self.values.get(key + ".text")
134
+ if text is None:
135
+ text = self._static(node, "text")
136
+ label = str(text) if text is not None else ""
137
+ if typ == "text":
138
+ return tk.Label(parent, text=label)
139
+ if typ == "button":
140
+ return tk.Button(parent, text=label)
141
+ if typ == "input":
142
+ entry = tk.Entry(parent)
143
+ value = self.values.get(key + ".value")
144
+ if value not in (None, ""):
145
+ entry.insert(0, str(value))
146
+ return entry
147
+ if typ == "checkbox":
148
+ return tk.Checkbutton(parent, text=label)
149
+ if typ == "divider":
150
+ return tk.Frame(parent, height=1, bd=0, relief="flat", bg="#e2e8f0")
151
+ if typ == "spacer":
152
+ return tk.Frame(parent, width=1, height=1)
153
+ # 未声明支持的控件(list/tabs/slider/…)与未知类型:降级为占位容器——
154
+ # 可见降级由引擎按声明发 DEGRADED_FEATURE;子树继续参与布局,
155
+ # 禁止"渲染成空白后丢弃子树"。
156
+ return tk.Frame(parent)
157
+
158
+ def _bind_events(self, node, widget):
159
+ """把 Tk 事件翻译成引擎事件——这正是规范 05 第 4 节要求的**渲染器职责**。
160
+
161
+ 用户动作 → Tk 事件 → 本绑定 → `engine.fire` → 处理器。缺这一步,交互就只是
162
+ "引擎内部调用",规范要求的可观察触发条件就无人守护。
163
+ """
164
+ nid = node.id
165
+ if node.type == "button":
166
+ widget.bind("<Button-1>", lambda _e, n=nid: self._fire(n, "click"))
167
+ elif node.type == "checkbox":
168
+ var = tk.BooleanVar(master=widget,
169
+ value=bool(self.values.get("#" + nid + ".value")))
170
+ widget.configure(variable=var)
171
+ self.vars[nid] = var
172
+ widget.configure(
173
+ command=lambda n=nid: self._fire(n, "change", self.vars[n].get()))
174
+ elif node.type == "input":
175
+ widget.bind("<Return>",
176
+ lambda _e, n=nid: self._fire(n, "submit", self._entry_text(widget)))
177
+ widget.bind("<<Modified>>",
178
+ lambda _e, n=nid: self._fire(n, "change", self._entry_text(widget)))
179
+ widget.bind("<FocusIn>", lambda _e, n=nid: self._fire(n, "focus"))
180
+ widget.bind("<FocusOut>", lambda _e, n=nid: self._fire(n, "blur"))
181
+
182
+ def _fire(self, nid, event, value=None):
183
+ """渲染器把事件交给引擎;诊断进观察流,与用户操作同一条路。"""
184
+ self.engine.queue(self.engine.fire(nid, event, None, value))
185
+
186
+ @staticmethod
187
+ def _entry_text(widget):
188
+ try:
189
+ return widget.get()
190
+ except tk.TclError:
191
+ return ""
192
+
193
+ def deliver(self, target, action, value=None):
194
+ """把一次"用户动作"投递给**真实部件**(而不是直接调引擎)。
195
+
196
+ 返回 `{"delivered": bool, "diagnostics": [...]}`;投递不了就必须可见降级。
197
+ """
198
+ nid = (target or "").lstrip("#")
199
+ widget = self.widgets.get(nid)
200
+ if widget is None or nid not in self.engine.program.nodes:
201
+ return {"delivered": False, "diagnostics": [Diagnostic(
202
+ "TARGET_MISSING", ERROR,
203
+ "节点 #%s 不存在或不可见" % nid).to_dict()]}
204
+ try:
205
+ if action == "click":
206
+ x = max(widget.winfo_width() // 2, 1)
207
+ y = max(widget.winfo_height() // 2, 1)
208
+ widget.event_generate("<Button-1>", x=x, y=y)
209
+ widget.event_generate("<ButtonRelease-1>", x=x, y=y)
210
+ elif action == "submit":
211
+ widget.event_generate("<Return>")
212
+ elif action == "change":
213
+ var = self.vars.get(nid)
214
+ if var is not None: # 勾选类:需要时翻转并触发 command
215
+ # `invoke()` 自身会翻转,故只在**当前值与目标不同**时才调;
216
+ # 先 `set` 再 `invoke` 会翻回去——送出的值又变回原值。
217
+ if bool(var.get()) != bool(value):
218
+ widget.invoke()
219
+ else: # 输入类:改文本并送一次修改事件
220
+ widget.delete(0, "end")
221
+ if value not in (None, ""):
222
+ widget.insert(0, str(value))
223
+ widget.event_generate("<<Modified>>")
224
+ elif action in ("focus", "blur"):
225
+ widget.focus_set()
226
+ widget.event_generate("<FocusIn>" if action == "focus" else "<FocusOut>")
227
+ else:
228
+ return {"delivered": False, "diagnostics": [Diagnostic(
229
+ "DEGRADED_FEATURE", INFO,
230
+ "本渲染器不支持交互动作 %s" % action,
231
+ feature="interaction").to_dict()]}
232
+ except tk.TclError as ex:
233
+ return {"delivered": False, "diagnostics": [Diagnostic(
234
+ "DEGRADED_FEATURE", INFO, "交互投递失败:%s" % ex,
235
+ feature="interaction").to_dict()]}
236
+ self.root.update()
237
+ return {"delivered": True}
238
+
239
+ def _style(self, node, widget):
240
+ color = self._static(node, "bgcolor")
241
+ if isinstance(color, str):
242
+ try:
243
+ widget.configure(bg=color)
244
+ except tk.TclError:
245
+ pass
246
+ fg = self._static(node, "fg")
247
+ if isinstance(fg, str):
248
+ try:
249
+ widget.configure(fg=fg)
250
+ except tk.TclError:
251
+ pass
252
+ width, height = self._static(node, "w"), self._static(node, "h")
253
+ if isinstance(widget, tk.Frame) and (
254
+ isinstance(width, (int, float)) or isinstance(height, (int, float))):
255
+ if isinstance(width, (int, float)):
256
+ widget.configure(width=int(width))
257
+ if isinstance(height, (int, float)):
258
+ widget.configure(height=int(height))
259
+ widget.pack_propagate(False)
260
+
261
+ def _layout_children(self, node, widget):
262
+ horizontal = node.type == "row"
263
+ gap = self._static(node, "gap")
264
+ gap = gap if isinstance(gap, (int, float)) else _GAP_DEFAULT
265
+ kids = [self.engine.program.nodes.get(cid) for cid in node.children]
266
+ kids = [k for k in kids if k is not None and k.type != "template"]
267
+ last = len(kids) - 1
268
+ for idx, child in enumerate(kids):
269
+ if not self.engine.flag(child.id, "visible"):
270
+ continue # visible=false 不占位(规范 05 第 2 节)
271
+ target = self.widgets.get(child.id)
272
+ if target is None:
273
+ continue
274
+ x, y = self._static(child, "x"), self._static(child, "y")
275
+ if isinstance(x, (int, float)) and isinstance(y, (int, float)):
276
+ target.place(x=int(x), y=int(y)) # 绝对定位脱离流动
277
+ continue
278
+ edge = int(gap / 2.0) # 相邻子节点共享一个 gap,首尾不留半格
279
+ pad = (0 if idx == 0 else edge, 0 if idx == last else edge)
280
+ flex = self._static(child, "flex")
281
+ expand = isinstance(flex, (int, float)) and flex > 0
282
+ if horizontal:
283
+ target.pack(side="left", padx=pad,
284
+ fill="both" if expand else "y", expand=expand)
285
+ else:
286
+ target.pack(side="top", pady=pad,
287
+ fill="both" if expand else "x", expand=expand)
288
+
289
+ # ------------------------------------------------------------ 测量
290
+
291
+ def measure(self) -> dict:
292
+ """量出真实矩形:相对第一个窗口客户区左上角,浮点像素(规范 05 第 8 节)。"""
293
+ out: dict = {}
294
+ if self.origin is None:
295
+ return out
296
+ try:
297
+ ox = self.origin.winfo_rootx()
298
+ oy = self.origin.winfo_rooty()
299
+ except tk.TclError:
300
+ return out
301
+ for nid, widget in self.widgets.items():
302
+ try:
303
+ if not widget.winfo_ismapped():
304
+ continue # 未映射 = 不可见 / 未布局,不提供几何
305
+ out["#" + nid] = {
306
+ "x": widget.winfo_rootx() - ox,
307
+ "y": widget.winfo_rooty() - oy,
308
+ "width": widget.winfo_width(),
309
+ "height": widget.winfo_height(),
310
+ }
311
+ except tk.TclError:
312
+ continue
313
+ return out
314
+
315
+ def _static(self, node, name):
316
+ expr = node.attrs.get(name)
317
+ return expr.value if isinstance(expr, Lit) else None
318
+
319
+
320
+ def handle(engine: Engine, renderer, request: dict) -> dict:
321
+ if request.get("op") == "hello":
322
+ return {"protocol": "1", "rendering": RENDERING}
323
+ if request.get("op") == "interact":
324
+ return renderer.deliver(request.get("target", ""),
325
+ request.get("action", ""), request.get("value"))
326
+ return base.handle(engine, request)
327
+
328
+
329
+ def main() -> int:
330
+ try:
331
+ sys.stdin.reconfigure(encoding="utf-8", errors="replace")
332
+ sys.stdout.reconfigure(encoding="ascii", errors="backslashreplace")
333
+ except Exception: # noqa: BLE001 - 老环境没有 reconfigure 也不致命
334
+ pass
335
+ engine = Engine(rendering=RENDERING)
336
+ renderer = TkRenderer(engine)
337
+ for line in sys.stdin:
338
+ line = line.strip()
339
+ if not line:
340
+ continue
341
+ try:
342
+ request = json.loads(line)
343
+ except json.JSONDecodeError as ex:
344
+ base._emit({"error": "请求不是合法 JSON:%s" % ex})
345
+ continue
346
+ if request.get("op") == "load":
347
+ # 每次装载都是新用例:先回到渲染器自身的声明,再让声明替身覆盖。
348
+ renderer.geometry_on = True
349
+ renderer.double = request.get("renderGeometry")
350
+ if isinstance(request.get("rendering"), dict):
351
+ declared = request["rendering"].get("geometry")
352
+ if declared is not None:
353
+ renderer.geometry_on = bool(declared)
354
+ # 几何来源优先级:观测替身 > 真实测量。先镜像并让 Tk 算完,
355
+ # 再应答——观察与探针(含批内 where)拿到的都是当前矩形。
356
+ renderer.sync()
357
+ renderer.pump()
358
+ if renderer.double is not None:
359
+ engine.render_geometry = renderer.double
360
+ else:
361
+ engine.render_geometry = renderer.measure() if renderer.geometry_on else {}
362
+ try:
363
+ response = handle(engine, renderer, request)
364
+ except Exception as ex: # noqa: BLE001 - 适配器绝不静默:错误原样回传
365
+ response = {"error": "%s: %s" % (type(ex).__name__, ex)}
366
+ base._emit(response)
367
+ if request.get("op") == "quit":
368
+ break
369
+ renderer.close()
370
+ return 0
371
+
372
+
373
+ if __name__ == "__main__":
374
+ raise SystemExit(main())
puppet/vocab.py ADDED
@@ -0,0 +1,147 @@
1
+ """词汇表:`spec/04-vocabulary.md` 的机器可读副本。
2
+
3
+ 实现与规范共用一份词汇,避免"文档说一套、代码做一套"。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ NODE_TYPES = (
9
+ "window", "col", "row", "navbar", "list", "tabs", "template",
10
+ "text", "icon", "divider", "spacer", "progress", "image", "avatar",
11
+ "button", "input", "checkbox", "switch", "slider", "dropdown",
12
+ )
13
+
14
+ STATE_FLAGS = ("hover", "focus", "pressed", "error", "visible", "disabled")
15
+
16
+ BOX_ATTRS = ("pad", "margin", "bgcolor", "gradient", "radius", "border", "shadow", "opacity")
17
+ LAYOUT_ATTRS = ("gap", "justify", "align", "wrap", "flex", "scroll", "w", "h",
18
+ "x", "y", "offset", "scale", "rotate")
19
+ TYPO_ATTRS = ("fg", "size", "weight", "italic", "font", "tooltip")
20
+ CONTENT_ATTRS = ("text", "icon", "src", "fit", "initials", "value", "selected",
21
+ "min", "max", "step", "placeholder", "title",
22
+ "option_label", "option_value", "primary")
23
+ REF_ATTRS = ("source", "template", "options")
24
+ META_ATTRS = ("states", "animate", "duration", "curve")
25
+
26
+ ALL_ATTRS = frozenset(
27
+ BOX_ATTRS + LAYOUT_ATTRS + TYPO_ATTRS + CONTENT_ATTRS + REF_ATTRS
28
+ + META_ATTRS + STATE_FLAGS
29
+ )
30
+
31
+ # `states` 里允许出现的外观属性
32
+ STATE_APPEARANCE = frozenset(
33
+ ("bgcolor", "fg", "gradient", "border", "radius", "shadow", "opacity",
34
+ "size", "weight", "italic", "align")
35
+ )
36
+ # `states` 里出现这些属性即 STATE_LAYOUT_ATTR
37
+ # (注意:`align` 属于外观白名单,不在此列——规范 04 第 3.6 节)
38
+ STATE_APPEARANCE_LAYOUT = frozenset(
39
+ ("pad", "margin", "gap", "flex", "w", "h", "x", "y", "scroll", "justify", "wrap")
40
+ )
41
+
42
+ # 只适用于特定类型的属性(用于 ATTR_ON_TYPE)
43
+ TYPE_ONLY_ATTRS = {
44
+ "placeholder": ("input",),
45
+ "selected": ("dropdown", "tabs"),
46
+ "options": ("dropdown",),
47
+ "option_label": ("dropdown",),
48
+ "option_value": ("dropdown",),
49
+ "min": ("slider", "progress"),
50
+ "max": ("slider", "progress"),
51
+ "step": ("slider",),
52
+ "fit": ("image",),
53
+ "initials": ("avatar",),
54
+ "title": ("window", "navbar"),
55
+ "source": ("list",),
56
+ "template": ("list",),
57
+ }
58
+
59
+ INTERACTION_EVENTS = frozenset(("click", "change", "submit", "focus", "blur"))
60
+
61
+ # 控件的主交互事件:用户对它做的"那个动作"。
62
+ # 这些事件没有任何处理器时,用户操作它不会有任何反应——必须被说出来。
63
+ # `input` 刻意不在表内:能打字本身不是需要被响应的动作。
64
+ PRIMARY_EVENT = {
65
+ "button": "click",
66
+ "checkbox": "change",
67
+ "switch": "change",
68
+ "slider": "change",
69
+ "dropdown": "change",
70
+ # `tabs` 刻意不在表内:切换选项卡本身就有渲染效果(换显示哪一页),
71
+ # 不需要处理器。见规范 04 第 5.1 节。
72
+ }
73
+
74
+ # 输入类控件"总有一个值":未声明时也应有确定值,否则 `#id.value` 会报错,
75
+ # 而"没写 value"是极常见的情形(此问题由写真实示例暴露)。
76
+ VALUE_DEFAULTS = {
77
+ "input": "", "checkbox": False, "switch": False, "slider": 0,
78
+ "progress": None, "dropdown": None, "tabs": 0,
79
+ }
80
+
81
+
82
+ def default_value(node_type: str, field: str):
83
+ """返回天然默认值;无默认则返回 None 并由调用方区分(用 _MISSING 哨兵)。"""
84
+ if field == "value":
85
+ return VALUE_DEFAULTS.get(node_type, _MISSING)
86
+ if field == "selected" and node_type in ("dropdown", "tabs"):
87
+ return 0
88
+ return _MISSING
89
+
90
+
91
+ class _Missing:
92
+ def __repr__(self):
93
+ return "<无默认值>"
94
+
95
+
96
+ _MISSING = _Missing()
97
+
98
+ ANIMATABLE = frozenset((
99
+ "opacity", "bgcolor", "fg", "gradient", "border",
100
+ "w", "h", "size", "radius", "pad", "margin", "gap",
101
+ "x", "y", "align", "justify", "offset", "scale", "rotate", "value",
102
+ ))
103
+
104
+ EVENTS = frozenset(("click", "change", "submit", "focus", "blur",
105
+ "pending", "done", "error", "timeout", "cancel"))
106
+
107
+ # 需要 `as` 绑定名的类型
108
+ TEMPLATE_TYPES = frozenset(("template",))
109
+
110
+ # 内置函数:名字 → (最少参数, 最多参数);None 表示不限
111
+ BUILTINS = {
112
+ "count": (1, 1), "len": (1, 1),
113
+ "upper": (1, 1), "lower": (1, 1), "trim": (1, 1),
114
+ "contains": (2, 2), "num": (1, 1), "str": (1, 1),
115
+ "abs": (1, 1), "min": (2, 2), "max": (2, 2), "sum": (1, 1), "round": (1, 2),
116
+ "join": (2, 2), "split": (2, 2),
117
+ "at": (2, 2), "first": (1, 1), "last": (1, 1), "slice": (3, 3),
118
+ "keys": (1, 1), "values": (1, 1), "has": (2, 2),
119
+ "fmt": (1, None),
120
+ }
121
+
122
+ # 必须支持的核心图标子集
123
+ CORE_ICONS = frozenset("""
124
+ add remove delete edit save close check cancel search settings
125
+ home menu more refresh download upload share copy filter sort
126
+ star favorite user users lock unlock mail phone calendar clock
127
+ location image camera play pause stop file folder document list
128
+ grid chart cart payment bell warning info error success help
129
+ arrow_up arrow_down arrow_left arrow_right chevron_up chevron_down
130
+ chevron_left chevron_right plus minus eye eye_off link tag flag
131
+ """.split())
132
+
133
+
134
+ def attr_kind(name: str) -> str:
135
+ """返回值类 / 引用类。"""
136
+ return "ref" if name in REF_ATTRS else "value"
137
+
138
+
139
+ def suggest_from(name: str, candidates) -> str:
140
+ """给未知名字找一个近似候选(用于诊断建议)。"""
141
+ best, score = "", 0.0
142
+ for cand in candidates:
143
+ common = len(set(name) & set(cand))
144
+ ratio = common / max(len(set(name) | set(cand)), 1)
145
+ if ratio > score:
146
+ best, score = cand, ratio
147
+ return best if score >= 0.5 else ""