docs: close M0b public contract drift
This commit is contained in:
parent
162f4b83fd
commit
46ffc69c5f
@ -64,7 +64,7 @@ Properties must be complete before the first child. A child can be a string, a n
|
||||
(text :font-weight 'bold "Hello")
|
||||
(text
|
||||
:color "#687386"
|
||||
(expr :value (if ready "Ready" "Waiting")))))
|
||||
(expr (if ready "Ready" "Waiting")))))
|
||||
```
|
||||
|
||||
An attribute appearing after a child is invalid because the two regions may
|
||||
@ -77,18 +77,17 @@ not be interleaved.
|
||||
The rule is simple:
|
||||
|
||||
1. Structural View positions are not quoted. This includes `etaf-view`, Hosts, Component calls, children, slot forms, and static Component styles.
|
||||
2. Elisp expression positions follow normal Elisp evaluation. This includes attribute values, `:key`, `:on-*`, `:use`, `expr :value`, `:setup`, Context values, Behavior constructors, and Actions.
|
||||
2. Elisp expression positions follow normal Elisp evaluation. This includes attribute values, `:key`, `:on-*`, `:use`, `expr`, `:setup`, Context values, Behavior constructors, and Actions.
|
||||
|
||||
```elisp
|
||||
(etaf-view
|
||||
(text
|
||||
:color (if dark "#F4F6FB" "#1F2328")
|
||||
(expr :value label)))
|
||||
(expr label)))
|
||||
|
||||
(etaf-view
|
||||
(column
|
||||
(expr
|
||||
:value
|
||||
(when open
|
||||
(etaf-view
|
||||
(text :font-weight 'bold "Details"))))))
|
||||
@ -103,30 +102,37 @@ Attribute values do not need an `expr` wrapper. `expr` exists only because the c
|
||||
`expr` accepts exactly one property and no children:
|
||||
|
||||
```elisp
|
||||
(expr :value ELISP-EXPRESSION)
|
||||
(expr ELISP-EXPRESSION)
|
||||
```
|
||||
|
||||
It evaluates the expression, then accepts a string, View, sequence, or `nil`. It creates no Ebox wrapper, identity, lifecycle, watcher, or effect. `if`, `when`, `cond`, `let`, `mapcar`, `cl-loop`, and other Elisp forms remain ordinary Elisp inside `:value`.
|
||||
It evaluates the expression, then accepts a string, typed View, proper typed
|
||||
View sequence, or `nil`. It creates no Ebox wrapper, identity, lifecycle,
|
||||
watcher, or effect. `if`, `when`, `cond`, `let`, `mapcar`, `cl-loop`, and other
|
||||
Elisp forms remain ordinary Elisp inside the form.
|
||||
|
||||
The current core has no file-facing `.etaf` pair loader. `etaf-define-component` is the structure/style/behavior unit: its View defines structure, `:styles` owns presentation rules, and `:setup` owns retained state, events, and lifecycle behavior. A future `.etaf` single-file component format belongs in a compiler layer that lowers into this same public View and Component contract; it is not a second runtime grammar.
|
||||
|
||||
## 4. Components
|
||||
|
||||
The public definition macro has exactly three keywords:
|
||||
The public definition macro has four keywords. Choose exactly one frontend;
|
||||
the other two clauses are optional:
|
||||
|
||||
```elisp
|
||||
```text
|
||||
(etaf-define-component NAME (&key PROPS)
|
||||
DOCSTRING?
|
||||
:setup OPAQUE-STATE-FORM
|
||||
:view VIEW
|
||||
:styles (styles RULE...))
|
||||
|
||||
(etaf-define-component NAME (&key PROPS)
|
||||
DOCSTRING?
|
||||
:setup SETUP
|
||||
:setup OPAQUE-STATE-FORM
|
||||
:render ORDINARY-ELISP
|
||||
:styles (styles RULE...))
|
||||
```
|
||||
|
||||
`:view` and `:setup` are mutually exclusive. `:styles` is optional and may appear once. Props are the only declared business inputs; ordinary trailing children and named slots are normalized separately into the Component's slot collection.
|
||||
`:view` and `:render` are mutually exclusive and exactly one is required.
|
||||
`:setup` and `:styles` are optional and may each appear once. Props are the only declared business inputs; ordinary trailing children and named slots are normalized separately into the Component's slot collection.
|
||||
|
||||
The Component definition is the current structure/style/behavior boundary. Keep dynamic state, Action callbacks, and lifecycle work in `:setup`; keep static presentation in `:styles`. A future `.etaf` SFC compiler may produce these definitions, but the Runtime does not load `.etaf` files directly.
|
||||
|
||||
@ -138,31 +144,34 @@ The Component definition is the current structure/style/behavior boundary. Keep
|
||||
:view
|
||||
(text
|
||||
:font-weight 'bold
|
||||
(expr :value label)))
|
||||
(expr label)))
|
||||
```
|
||||
|
||||
```elisp
|
||||
(etaf-define-component disclosure (&key title)
|
||||
"Render a retained disclosure."
|
||||
:setup
|
||||
(let ((open (etaf-ref nil)))
|
||||
(lambda ()
|
||||
(etaf-view
|
||||
(column
|
||||
(text
|
||||
:role 'button
|
||||
:on-press
|
||||
(lambda ()
|
||||
(setf (etaf-value open)
|
||||
(not (etaf-value open))))
|
||||
(expr :value (if (etaf-value open) "Hide" "Show")))
|
||||
(expr
|
||||
:value
|
||||
(when (etaf-value open)
|
||||
(etaf-view (text (expr :value title))))))))))
|
||||
(etaf-ref nil)
|
||||
:view
|
||||
(column
|
||||
(text
|
||||
:role 'button
|
||||
:on-press
|
||||
(let ((open (etaf-state)))
|
||||
(lambda ()
|
||||
(setf (etaf-value open)
|
||||
(not (etaf-value open)))))
|
||||
(expr (if (etaf-value (etaf-state)) "Hide" "Show")))
|
||||
(expr
|
||||
(when (etaf-value (etaf-state))
|
||||
(etaf-view (text (expr title)))))))
|
||||
```
|
||||
|
||||
`:setup` runs once for a retained Component instance and must return a zero-argument render function. Re-render reads current props and refs without rerunning setup. Setup is the owner for local refs, computed values, watches, Effects, and cleanup registration.
|
||||
`:setup` runs once for a retained Component instance and returns one opaque state
|
||||
value. `etaf-state` returns that exact value during `:view` or `:render`.
|
||||
Re-render reads current props and state without rerunning setup. Setup is the
|
||||
owner for local refs, computed values, watches, Effects, and cleanup
|
||||
registration; it never returns a render function.
|
||||
|
||||
`:key` is stable identity metadata, not a business prop. On a Component call it selects the retained Component instance within the sibling scope; on a Host it is forwarded as the Ebox node key. If a render candidate fails, the Runtime restores the previous instance, handlers, behaviors, and buffer.
|
||||
|
||||
@ -296,6 +305,19 @@ Use `etaf-action-define` and `etaf-dispatch` when the mutation is named and shar
|
||||
|
||||
`etaf-behavior-create` accepts the reserved `:install` attribute for an optional zero-argument installer. The installer may return a cleanup function; `etaf-current-behavior-context` exposes the current Runtime, structural path, and Host props while it runs. Installer state is disposed when the Behavior is replaced or its owner is unmounted.
|
||||
|
||||
Interaction composition is deterministic. An explicit Host `:on-*` callback
|
||||
runs first, followed by Behavior callbacks in declaration order; an error
|
||||
short-circuits the remaining callbacks. For non-event attributes the explicit
|
||||
Host value wins, otherwise the first declaring Behavior wins. Behavior names
|
||||
must be unique on one Host before any installer runs. Stable installer identity
|
||||
is reused, and every installed cleanup runs exactly once. Events dispatch only
|
||||
to the exact Host reference: ETAF has no capture or bubble phase.
|
||||
|
||||
Action names should be application- or feature-prefixed symbols. Duplicate
|
||||
registration is an error by default. `etaf-action-redefine-run` is the explicit
|
||||
authoring/reload boundary; replacement affects future name-based dispatch and
|
||||
does not flush a mounted Runtime.
|
||||
|
||||
Runtime events are dispatched through `etaf-dispatch-event`, and focus/hit testing use public Ebox Host-reference queries through `etaf-activate`, `etaf-focus`, `etaf-focus-next`, `etaf-host-ref-bounds`, and `etaf-host-ref-position`.
|
||||
|
||||
## 9. Context, Theme, Data, and Resource
|
||||
@ -310,14 +332,13 @@ Context is an inherited Component Scope environment:
|
||||
:setup
|
||||
(let ((service (etaf-ref "demo-service")))
|
||||
(etaf-provide 'service service)
|
||||
(lambda () (etaf-view (slot)))))
|
||||
service)
|
||||
:view (slot))
|
||||
|
||||
(etaf-define-component service-consumer ()
|
||||
"Read the inherited service."
|
||||
:setup
|
||||
(let ((service (etaf-inject 'service nil t)))
|
||||
(lambda ()
|
||||
(etaf-view (text (expr :value (etaf-value service)))))))
|
||||
:setup (etaf-inject 'service nil t)
|
||||
:view (text (expr (etaf-value (etaf-state)))))
|
||||
|
||||
(etaf-view (service-provider (service-consumer)))
|
||||
```
|
||||
@ -328,10 +349,9 @@ Keys are stable ordinary symbols. The nearest ancestor wins; a missing required
|
||||
(etaf-define-component themed-shell ()
|
||||
"Provide default text colors to a subtree."
|
||||
:setup
|
||||
(progn
|
||||
(etaf-theme-provide
|
||||
'(:color "#F4F6FB" :bgcolor "#202634"))
|
||||
(lambda () (etaf-view (slot)))))
|
||||
(etaf-theme-provide
|
||||
'(:color "#F4F6FB" :bgcolor "#202634"))
|
||||
:view (slot))
|
||||
```
|
||||
|
||||
Palette resolution remains a Theme concern, not a UI catalog concern. Core
|
||||
|
||||
@ -64,7 +64,7 @@ ATTRIBUTE = :KEY VALUE
|
||||
(text :font-weight 'bold "Hello")
|
||||
(text
|
||||
:color "#687386"
|
||||
(expr :value (if ready "Ready" "Waiting")))))
|
||||
(expr (if ready "Ready" "Waiting")))))
|
||||
```
|
||||
|
||||
属性出现在子节点之后时,属性区和子节点区被交错,属于非法结构。
|
||||
@ -76,18 +76,17 @@ ATTRIBUTE = :KEY VALUE
|
||||
规则只有两条:
|
||||
|
||||
1. 结构性 View 位置不需要 quote,包括 `etaf-view`、Host、Component 调用、子节点、slot 和静态 Component styles。
|
||||
2. Elisp 表达式位置遵循普通 Elisp 求值,包括属性值、`:key`、`:on-*`、`:use`、`expr :value`、`:setup`、Context 值、Behavior 构造器和 Action。
|
||||
2. Elisp 表达式位置遵循普通 Elisp 求值,包括属性值、`:key`、`:on-*`、`:use`、`expr`、`:setup`、Context 值、Behavior 构造器和 Action。
|
||||
|
||||
```elisp
|
||||
(etaf-view
|
||||
(text
|
||||
:color (if dark "#F4F6FB" "#1F2328")
|
||||
(expr :value label)))
|
||||
(expr label)))
|
||||
|
||||
(etaf-view
|
||||
(column
|
||||
(expr
|
||||
:value
|
||||
(when open
|
||||
(etaf-view
|
||||
(text :font-weight 'bold "Details"))))))
|
||||
@ -102,30 +101,34 @@ ATTRIBUTE = :KEY VALUE
|
||||
`expr` 只接受一个属性且不能有子节点:
|
||||
|
||||
```elisp
|
||||
(expr :value ELISP-EXPRESSION)
|
||||
(expr ELISP-EXPRESSION)
|
||||
```
|
||||
|
||||
它执行表达式,然后接受字符串、View、View 序列或 `nil`。它不创建 Ebox wrapper、identity、生命周期、watch 或 effect。`if`、`when`、`cond`、`let`、`mapcar`、`cl-loop` 等 Elisp 形式仍然只是 `:value` 中的普通 Elisp。
|
||||
它执行表达式,然后接受字符串、typed View、typed View proper sequence 或 `nil`。
|
||||
它不创建 Ebox wrapper、identity、生命周期、watch 或 effect。`if`、`when`、`cond`、
|
||||
`let`、`mapcar`、`cl-loop` 等仍是 form 中的普通 Elisp。
|
||||
|
||||
当前 core 没有面向文件的 `.etaf` pair loader。`etaf-define-component` 才是结构/样式/行为单元:View 定义结构,`:styles` 负责 presentation,`:setup` 负责 retained state、事件和生命周期行为。未来的 `.etaf` SFC 属于把结果 lowering 到同一套公共 View/Component 契约的 compiler layer,而不是第二套 Runtime 语法。
|
||||
|
||||
## 4. Component
|
||||
|
||||
公共定义宏只有三个关键字:
|
||||
公共定义宏有四个关键字。两个 frontend 必须二选一,另外两个 clause 可选:
|
||||
|
||||
```elisp
|
||||
```text
|
||||
(etaf-define-component NAME (&key PROPS)
|
||||
DOCSTRING?
|
||||
:setup OPAQUE-STATE-FORM
|
||||
:view VIEW
|
||||
:styles (styles RULE...))
|
||||
|
||||
(etaf-define-component NAME (&key PROPS)
|
||||
DOCSTRING?
|
||||
:setup SETUP
|
||||
:setup OPAQUE-STATE-FORM
|
||||
:render ORDINARY-ELISP
|
||||
:styles (styles RULE...))
|
||||
```
|
||||
|
||||
`:view` 和 `:setup` 互斥;`:styles` 可选且最多出现一次。Props 是唯一需要声明的业务输入;普通尾部子节点和命名 slot 会被规范化为 Component 的 slot 集合。
|
||||
`:view` 和 `:render` 互斥且必须恰好出现一个;`:setup` 与 `:styles` 可选且各最多出现一次。Props 是唯一需要声明的业务输入;普通尾部子节点和命名 slot 会被规范化为 Component 的 slot 集合。
|
||||
|
||||
Component definition 是当前结构/样式/行为边界:动态状态、Action callback 和生命周期工作放进 `:setup`,静态 presentation 放进 `:styles`。未来 `.etaf` SFC compiler 可以生成这些 definition,但 Runtime 不会直接加载 `.etaf` 文件。
|
||||
|
||||
@ -137,31 +140,33 @@ Component definition 是当前结构/样式/行为边界:动态状态、Action
|
||||
:view
|
||||
(text
|
||||
:font-weight 'bold
|
||||
(expr :value label)))
|
||||
(expr label)))
|
||||
```
|
||||
|
||||
```elisp
|
||||
(etaf-define-component disclosure (&key title)
|
||||
"Render a retained disclosure."
|
||||
:setup
|
||||
(let ((open (etaf-ref nil)))
|
||||
(lambda ()
|
||||
(etaf-view
|
||||
(column
|
||||
(text
|
||||
:role 'button
|
||||
:on-press
|
||||
(lambda ()
|
||||
(setf (etaf-value open)
|
||||
(not (etaf-value open))))
|
||||
(expr :value (if (etaf-value open) "Hide" "Show")))
|
||||
(expr
|
||||
:value
|
||||
(when (etaf-value open)
|
||||
(etaf-view (text (expr :value title))))))))))
|
||||
(etaf-ref nil)
|
||||
:view
|
||||
(column
|
||||
(text
|
||||
:role 'button
|
||||
:on-press
|
||||
(let ((open (etaf-state)))
|
||||
(lambda ()
|
||||
(setf (etaf-value open)
|
||||
(not (etaf-value open)))))
|
||||
(expr (if (etaf-value (etaf-state)) "Hide" "Show")))
|
||||
(expr
|
||||
(when (etaf-value (etaf-state))
|
||||
(etaf-view (text (expr title)))))))
|
||||
```
|
||||
|
||||
`:setup` 对一个 retained Component instance 只运行一次,并且必须返回零参数 render 函数。重新渲染读取当前 props 和 ref,不重新运行 setup。setup 负责局部 ref、computed、watch、Effect 和 cleanup 的创建。
|
||||
`:setup` 对一个 retained Component instance 只运行一次,返回一个 opaque 状态值。
|
||||
`:view` 或 `:render` 中用 `etaf-state` 取得这个准确值。重新渲染读取当前 props 和
|
||||
state,不重新运行 setup。setup 负责局部 ref、computed、watch、Effect 和 cleanup,
|
||||
不返回 render 函数。
|
||||
|
||||
`:key` 是稳定的 identity metadata,不是业务 prop。放在 Component 调用上时,它选择同级作用域内要保留的 Component instance;放在 Host 上时,它会作为 Ebox node key 向下传递。候选渲染失败时,Runtime 恢复旧 instance、handlers、Behaviors 和 buffer。
|
||||
|
||||
@ -295,6 +300,16 @@ Behavior = 安装多个非视觉能力的可复用 bundle
|
||||
|
||||
`etaf-behavior-create` 接受保留的 `:install` 属性,用于可选的零参数 installer。Installer 可以返回 cleanup;运行期间可通过 `etaf-current-behavior-context` 读取当前 Runtime、结构路径和 Host props。Behavior 被替换或 owner 卸载时,installer 状态会被释放。
|
||||
|
||||
交互组合是确定的:显式 Host `:on-*` callback 先运行,然后按声明顺序运行 Behavior
|
||||
callback;任一错误会 short-circuit 后续 callback。非事件属性由显式 Host 值优先,
|
||||
否则第一个声明该属性的 Behavior 获胜(first-wins)。同一 Host 的 Behavior name 必须在任何
|
||||
installer 运行前保持唯一。稳定 installer identity 会复用,每个已安装 cleanup
|
||||
exactly-once。事件只 dispatch 给准确 Host ref;ETAF 没有 capture 或 bubble 阶段。
|
||||
|
||||
Action name 应使用 application/feature-prefixed symbol。重复注册默认报错。
|
||||
`etaf-action-redefine-run` 是显式 authoring/reload 边界;替换只影响未来按 name 的
|
||||
dispatch,不会 flush 已挂载 Runtime。
|
||||
|
||||
Runtime 事件通过 `etaf-dispatch-event` 进入;命中测试和 focus 通过 Ebox Host 引用查询,并由 `etaf-activate`、`etaf-focus`、`etaf-focus-next`、`etaf-host-ref-bounds` 和 `etaf-host-ref-position` 提供公共入口。
|
||||
|
||||
## 9. Context、Theme、Data 与 Resource
|
||||
@ -309,14 +324,13 @@ Context 是继承的 Component Scope 环境:
|
||||
:setup
|
||||
(let ((service (etaf-ref "demo-service")))
|
||||
(etaf-provide 'service service)
|
||||
(lambda () (etaf-view (slot)))))
|
||||
service)
|
||||
:view (slot))
|
||||
|
||||
(etaf-define-component service-consumer ()
|
||||
"Read the inherited service."
|
||||
:setup
|
||||
(let ((service (etaf-inject 'service nil t)))
|
||||
(lambda ()
|
||||
(etaf-view (text (expr :value (etaf-value service)))))))
|
||||
:setup (etaf-inject 'service nil t)
|
||||
:view (text (expr (etaf-value (etaf-state)))))
|
||||
|
||||
(etaf-view (service-provider (service-consumer)))
|
||||
```
|
||||
@ -327,10 +341,9 @@ key 使用稳定的普通 symbol。最近的祖先优先,缺失的 required ke
|
||||
(etaf-define-component themed-shell ()
|
||||
"Provide default text colors to a subtree."
|
||||
:setup
|
||||
(progn
|
||||
(etaf-theme-provide
|
||||
'(:color "#F4F6FB" :bgcolor "#202634"))
|
||||
(lambda () (etaf-view (slot)))))
|
||||
(etaf-theme-provide
|
||||
'(:color "#F4F6FB" :bgcolor "#202634"))
|
||||
:view (slot))
|
||||
```
|
||||
|
||||
Palette 解析属于 Theme,而不是 UI 目录。core 提供
|
||||
|
||||
@ -18,7 +18,7 @@ The repository is complete for the agreed unified architecture when the mandator
|
||||
|
||||
| Milestone | Delivered responsibility | Evidence |
|
||||
| --- | --- | --- |
|
||||
| P0 grammar | Unified View shape, property-first parsing, `etaf-view`, `expr :value`, core Hosts, aliases | `tests/etaf-tests.el` structural and syntax tests |
|
||||
| P0 grammar | Unified View shape, property-first parsing, `etaf-view`, `(expr FORM)`, core Hosts, aliases | `tests/etaf-tests.el` structural and syntax tests |
|
||||
| P1 Components | `:view`, `:setup`, props, default/named slots, retained instances, lifecycle, `:key`, raw Ebox escape | Component, slot, mount, prop-update, raw-node, and rollback tests |
|
||||
| P2 Runtime | refs, computed, effects, watches, Scope cleanup, Context, Theme, Behaviors, events, focus, Actions | Mounted event/focus tests, reactive failure rollback, cleanup tests |
|
||||
| P3 presentation | scoped styles, selector matching, Theme precedence, inline text runs, Resource and error boundary | Style, Theme, text-surface, Resource, and error tests |
|
||||
@ -55,16 +55,19 @@ Do not split a file merely to create a shorter name. Split only when a stable re
|
||||
- The only structural form is `(NAME :PROPERTY VALUE ... CHILD ...)`.
|
||||
- All properties precede children.
|
||||
- Attribute values are ordinary Elisp expressions.
|
||||
- The only child computation bridge is `(expr :value ELISP-EXPRESSION)`.
|
||||
- The only child computation bridge is `(expr ELISP-EXPRESSION)`; its result
|
||||
may be text, a typed View/proper typed View sequence, or `nil`.
|
||||
- A dynamic View returned by Elisp is constructed with `(etaf-view ...)` inside `expr`; quoted structural data is never executed.
|
||||
- The six visible Host forms are `text`, `box`, `row`, `column`, `flex`, and `grid`; `fragment`, `slot`, and `expr` are transparent framework structure.
|
||||
- There is no raw Ebox View escape hatch. Framework integrations use Ebox's evaluated typed-constructor port outside the author DSL.
|
||||
|
||||
### 4.2 Component and slots
|
||||
|
||||
- `etaf-define-component` accepts only `:view`, `:setup`, and optional `:styles`.
|
||||
- `:view` and `:setup` are mutually exclusive.
|
||||
- `:setup` runs once per retained identity and returns a zero-argument render function.
|
||||
- `etaf-define-component` chooses exactly one of `:view` or `:render`; `:setup`
|
||||
and `:styles` are optional.
|
||||
- `:view` and `:render` are mutually exclusive frontends.
|
||||
- `:setup` runs once per retained identity and returns opaque state read with
|
||||
`etaf-state` during either frontend.
|
||||
- Props update render without rerunning setup.
|
||||
- Trailing children are `slots.default`; named slot input uses `(slot :name 'NAME CHILD...)`.
|
||||
- Default outlet shorthand is `(slot)` or `(slot FALLBACK...)`.
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
|
||||
| 里程碑 | 已交付职责 | 证据 |
|
||||
| --- | --- | --- |
|
||||
| P0 grammar | 统一 View 形状、属性优先解析、`etaf-view`、`expr :value`、核心 Host、alias | `tests/etaf-tests.el` 结构和语法测试 |
|
||||
| P0 grammar | 统一 View 形状、属性优先解析、`etaf-view`、`(expr FORM)`、核心 Host、alias | `tests/etaf-tests.el` 结构和语法测试 |
|
||||
| P1 Component | `:view`、`:setup`、props、默认/命名 slot、retained instance、生命周期、`:key`、raw Ebox 出口 | Component、slot、mount、prop 更新、raw node、rollback 测试 |
|
||||
| P2 Runtime | ref、computed、effect、watch、Scope cleanup、Context、Theme、Behavior、事件、focus、Action | 挂载事件/focus、响应式失败回滚和 cleanup 测试 |
|
||||
| P3 presentation | 作用域样式、selector、Theme 优先级、inline text runs、Resource 和 error boundary | 样式、Theme、文本 surface、Resource、error 测试 |
|
||||
@ -55,16 +55,19 @@
|
||||
- 唯一结构形式是 `(NAME :PROPERTY VALUE ... CHILD ...)`。
|
||||
- 所有属性必须先于所有子节点。
|
||||
- 属性值是普通 Elisp 表达式。
|
||||
- 唯一的子节点计算桥接是 `(expr :value ELISP-EXPRESSION)`。
|
||||
- 唯一的子节点计算桥接是 `(expr ELISP-EXPRESSION)`;结果可以是文本、typed View、
|
||||
typed View proper sequence 或 `nil`。
|
||||
- Elisp 返回动态 View 时,在 `expr` 中用 `(etaf-view ...)` 构造;被 quote 的结构数据永远不会被执行。
|
||||
- 六个可见 Host form 是 `text`、`box`、`row`、`column`、`flex`、`grid`;`fragment`、`slot`、`expr` 是透明的框架结构。
|
||||
- View 作者 DSL 不提供 raw Ebox 逃生口;框架集成在作者 DSL 之外使用 Ebox evaluated typed-constructor 端口。
|
||||
|
||||
### 4.2 Component 与 slot
|
||||
|
||||
- `etaf-define-component` 只接受 `:view`、`:setup` 和可选 `:styles`。
|
||||
- `:view` 与 `:setup` 互斥。
|
||||
- `:setup` 对每个 retained identity 只运行一次,返回零参数 render 函数。
|
||||
- `etaf-define-component` 必须在 `:view` 与 `:render` 中恰好选择一个;`:setup` 与
|
||||
`:styles` 可选。
|
||||
- `:view` 与 `:render` 是互斥 frontend。
|
||||
- `:setup` 对每个 retained identity 只运行一次,返回 opaque 状态,并由两个 frontend
|
||||
中的 `etaf-state` 读取。
|
||||
- Props 更新只重新 render,不重新运行 setup。
|
||||
- 尾部 children 是 `slots.default`;命名 slot 输入使用 `(slot :name 'NAME CHILD...)`。
|
||||
- 默认 outlet 简写是 `(slot)` 或 `(slot FALLBACK...)`。
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
# ETAF Component 定义:目标设计
|
||||
# ETAF Component 定义:历史目标设计
|
||||
|
||||
> 状态:已完成架构审查的目标设计,尚未实现。
|
||||
> **SUPERSEDED / historical:本文不再是当前公共合同。**
|
||||
>
|
||||
> 本文只定义目标公共契约,不代表当前版本已经支持这些写法。当前实现差距见
|
||||
> “实现状态与迁移”一节。审查通过并完成代码、测试、GUI 与性能门禁后,稳定内容
|
||||
> 才能进入用户指南。
|
||||
> 当前公共合同以 [docs/architecture.zh.md](../architecture.zh.md) 与
|
||||
> [docs/user-guide.zh.md](../user-guide.zh.md) 为准。本文保留为设计决策历史,不再以
|
||||
> 创建时的“当前状态”表覆盖已经交付的实现。
|
||||
|
||||
本文只拥有 **Component 作者语法和作者可见语义**。属性 owner、跨包依赖、
|
||||
transaction 权限、Ebox/TP/Rust 边界仍以
|
||||
@ -1016,11 +1016,12 @@ grammar,也不要求 IR 保存 frontend provenance。
|
||||
| topology compiler | 是 | 否 |
|
||||
| Runtime/identity/lifecycle | 同一套 | 同一套 |
|
||||
|
||||
## 21. 实现状态与迁移
|
||||
## 21. 历史实现快照(非当前状态)
|
||||
|
||||
截至本文创建时,当前代码的真实状态是:
|
||||
下表只记录本文创建时的历史差距,不能用于判断当前版本。当前实现与迁移状态见
|
||||
[implementation-plan.zh.md](../implementation-plan.zh.md)。
|
||||
|
||||
| 能力 | 当前状态 | 目标动作 |
|
||||
| 能力 | 历史状态 | 当时目标动作 |
|
||||
| --- | --- | --- |
|
||||
| `etaf-define-component :view` | 已实现 | 按新 DSL grammar 收敛。 |
|
||||
| `:setup` | 已实现,但返回零参数 render function | 删除旧语义;改为只返回 state。 |
|
||||
|
||||
@ -824,7 +824,10 @@ complete chain, for example:
|
||||
|
||||
`etaf-component`:
|
||||
|
||||
- defines Component props, `:view`, `:setup`, and `:styles`;
|
||||
- defines Component props, mutually exclusive `:view`/`:render` frontends,
|
||||
optional `:setup` returning opaque state, the sole `etaf-state` accessor,
|
||||
and `:styles`;
|
||||
- defines the `etaf-node` code frontend at the same typed View ABI boundary;
|
||||
- does not schedule Runtime or call Ebox.
|
||||
|
||||
`etaf-runtime`:
|
||||
|
||||
@ -80,7 +80,7 @@ Attribute values are ordinary Elisp expressions. They do not need an extra `expr
|
||||
(text
|
||||
:color (if dark "#F4F6FB" "#1F2328")
|
||||
:background-color "#20242B"
|
||||
(expr :value label))))
|
||||
(expr label))))
|
||||
```
|
||||
|
||||
The child region is structural. `expr` is the one explicit bridge for ordinary Elisp computation:
|
||||
@ -88,14 +88,15 @@ The child region is structural. `expr` is the one explicit bridge for ordinary E
|
||||
```elisp
|
||||
(etaf-view
|
||||
(column
|
||||
(expr :value (if loading "Loading..." "Ready"))
|
||||
(expr (if loading "Loading..." "Ready"))
|
||||
(expr
|
||||
:value
|
||||
(when open
|
||||
(etaf-view (text :font-style 'italic "Details"))))))
|
||||
```
|
||||
|
||||
`expr` accepts exactly `:value` and no children. Its result can be a string, View, sequence, or `nil`. `if`, `when`, `cond`, `let`, `mapcar`, and `cl-loop` remain normal Elisp inside the value.
|
||||
`expr` accepts exactly one ordinary Elisp form and no structural children. Its
|
||||
result can be a string, typed View, proper typed View sequence, or `nil`.
|
||||
`if`, `when`, `cond`, `let`, `mapcar`, and `cl-loop` remain normal Elisp.
|
||||
|
||||
Quote has one ordinary Elisp meaning:
|
||||
|
||||
@ -134,7 +135,7 @@ The beginner form is a stateless `:view` Component:
|
||||
:view
|
||||
(text
|
||||
:font-weight 'bold
|
||||
(expr :value label)))
|
||||
(expr label)))
|
||||
|
||||
(etaf-mount
|
||||
"*etaf-status*"
|
||||
@ -154,8 +155,9 @@ The definition macro accepts only these keywords:
|
||||
|
||||
| Keyword | Meaning |
|
||||
| --- | --- |
|
||||
| `:view` | The stateless View producer; mutually exclusive with `:setup` |
|
||||
| `:setup` | One-time Component initialization returning a zero-argument render function |
|
||||
| `:view` | Declarative View frontend; mutually exclusive with `:render` |
|
||||
| `:render` | Ordinary-Elisp frontend returning one typed View through `etaf-node` |
|
||||
| `:setup` | Optional one-time initialization returning opaque state read with `etaf-state` |
|
||||
| `:styles` | Optional static scoped style declaration |
|
||||
|
||||
There is no separate declaration block for children, slots, events, state, or variants. Props are declared with `(&key ...)`; children and slots are implicit content.
|
||||
@ -173,18 +175,24 @@ Use `:setup` when the Component owns local state:
|
||||
(lambda () (message "%s mounted" title)))
|
||||
(etaf-on-unmounted
|
||||
(lambda () (message "%s unmounted" title)))
|
||||
(lambda ()
|
||||
(etaf-view
|
||||
(column
|
||||
(text :font-weight 'bold (expr :value title))
|
||||
(text (expr :value (format "Count: %d" (etaf-value count))))
|
||||
(text
|
||||
:role 'button
|
||||
:on-press (lambda () (cl-incf (etaf-value count)))
|
||||
"Increment"))))))
|
||||
count)
|
||||
:view
|
||||
(column
|
||||
(text :font-weight 'bold (expr title))
|
||||
(text (expr (format "Count: %d" (etaf-value (etaf-state)))))
|
||||
(text
|
||||
:role 'button
|
||||
:on-press
|
||||
(let ((count (etaf-state)))
|
||||
(lambda () (cl-incf (etaf-value count))))
|
||||
"Increment")))
|
||||
```
|
||||
|
||||
Setup runs once for the retained instance. Its returned render function runs on each update. `etaf-on-mounted`, `etaf-on-updated`, and `etaf-on-unmounted` register lifecycle callbacks for that Component instance. Scope disposal automatically stops reactive effects and cleanup.
|
||||
Setup runs once for the retained instance and returns one opaque state value.
|
||||
The selected `:view` or `:render` frontend runs on each update and reads that
|
||||
exact value with `etaf-state`. `etaf-on-mounted`, `etaf-on-updated`, and
|
||||
`etaf-on-unmounted` register lifecycle callbacks for that Component instance.
|
||||
Scope disposal automatically stops reactive effects and cleanup.
|
||||
|
||||
The small reactive API is:
|
||||
|
||||
@ -219,7 +227,7 @@ Trailing children are the anonymous/default slot:
|
||||
"Render a titled panel."
|
||||
:view
|
||||
(column
|
||||
(text :font-weight 'bold (expr :value title))
|
||||
(text :font-weight 'bold (expr title))
|
||||
(slot (text :color "#687386" "No content"))))
|
||||
|
||||
(etaf-view
|
||||
@ -236,7 +244,7 @@ Named slots use `:name` and must use a stable non-keyword symbol:
|
||||
:view
|
||||
(column
|
||||
(slot :name 'header
|
||||
(text :font-weight 'bold (expr :value title)))
|
||||
(text :font-weight 'bold (expr title)))
|
||||
(slot (text :color "#687386" "No body"))))
|
||||
|
||||
(etaf-view
|
||||
@ -248,6 +256,22 @@ Named slots use `:name` and must use a stable non-keyword symbol:
|
||||
|
||||
The two default-slot shorthands are `(slot)` and `(slot FALLBACK...)`. The normalized spelling is `(slot :name 'default FALLBACK...)`. At a call site, ordinary children fill `default`; a named input uses `(slot :name 'header CHILD...)`. An explicit empty `(slot :name 'header)` suppresses the fallback. Strings, numbers, variables, and runtime expressions are not valid slot names.
|
||||
|
||||
`expr` may return a typed View or a proper sequence of typed Views at a
|
||||
structural boundary. It never exposes or accepts ETAF's private structs. The
|
||||
same Component can combine a keyed `:for`, a structural expression, and a
|
||||
named footer slot:
|
||||
|
||||
```elisp
|
||||
(etaf-define-component etaf-docs-collection-card (&key items footer-view)
|
||||
"Render keyed rows, one dynamic typed View, and a footer slot."
|
||||
:view
|
||||
(column
|
||||
(row :for (item items) :key (car item)
|
||||
(text (expr (cdr item))))
|
||||
(expr footer-view)
|
||||
(slot :name 'footer (text "No footer"))))
|
||||
```
|
||||
|
||||
## 7. Styles and themes
|
||||
|
||||
Static Component styles use one declaration form:
|
||||
@ -291,10 +315,9 @@ Theme is a Context convenience, not another runtime object:
|
||||
(etaf-define-component themed-shell ()
|
||||
"Provide default text colors to a subtree."
|
||||
:setup
|
||||
(progn
|
||||
(etaf-theme-provide
|
||||
'(:color "#F4F6FB" :bgcolor "#202634"))
|
||||
(lambda () (etaf-view (slot)))))
|
||||
(etaf-theme-provide
|
||||
'(:color "#F4F6FB" :bgcolor "#202634"))
|
||||
:view (slot))
|
||||
```
|
||||
|
||||
For a light/dark application palette, keep the semantic roles in one palette
|
||||
@ -372,6 +395,18 @@ For a reusable installer, reserve `:install` for the cleanup-producing part of t
|
||||
|
||||
The installer can call `etaf-current-behavior-context` when it needs the current Runtime or Host path. Replacing the Behavior runs the old cleanup before the new state becomes current. Behavior equality keeps function and reactive-value identity with `eq`; a newly-created installer closure is therefore a deliberate replacement, not an accidental reuse. The replacement is staged under the mounted resource registry and becomes authoritative only when its generation commits.
|
||||
|
||||
Composition rules are fixed: the Host callback runs before Behavior callbacks,
|
||||
Behaviors follow declaration order, and a callback error short-circuits the
|
||||
rest. For non-event attributes the Host wins, then the first Behavior wins.
|
||||
Duplicate Behavior names fail before installation. Stable installers are reused
|
||||
and each installed cleanup runs exactly once. Dispatch targets one exact Host;
|
||||
there is no capture or bubble phase.
|
||||
|
||||
Use application- or feature-prefixed Action names. Duplicate Action
|
||||
registration is an error. During deliberate reload, wrap the replacement in
|
||||
`etaf-action-redefine-run`; it changes future name-based dispatch without
|
||||
flushing the mounted Runtime.
|
||||
|
||||
Focus and hit testing are Runtime operations:
|
||||
|
||||
```elisp
|
||||
@ -394,15 +429,14 @@ Use Context for a dependency shared across component depth, not for ordinary pro
|
||||
:setup
|
||||
(let ((service (etaf-ref "demo-service")))
|
||||
(etaf-provide 'service service)
|
||||
(lambda () (etaf-view (slot)))))
|
||||
service)
|
||||
:view (slot))
|
||||
|
||||
(etaf-define-component service-label ()
|
||||
"Read the inherited service."
|
||||
:setup
|
||||
(let ((service (etaf-inject 'service nil t)))
|
||||
(lambda ()
|
||||
(etaf-view
|
||||
(text (expr :value (format "Service: %s" (etaf-value service)))))))
|
||||
:setup (etaf-inject 'service nil t)
|
||||
:view
|
||||
(text (expr (format "Service: %s" (etaf-value (etaf-state))))))
|
||||
|
||||
(etaf-mount
|
||||
"*etaf-context*"
|
||||
@ -469,8 +503,9 @@ updates only when that identity enters or leaves the main selection, including
|
||||
when application code writes `etaf-data-selection` directly. The default
|
||||
DataGrid path uses these refs with keyed retained row owners, so a single-select
|
||||
change invalidates the old and new rows rather than the complete visible page.
|
||||
Custom `:row-selected-p` and `:selected-key` contracts remain available when
|
||||
selection is owned outside the Controller.
|
||||
Custom `:row-selected-p` remains available when selection is owned outside the
|
||||
Controller. Stable identity still comes from the required `:row-key`; there is
|
||||
no second selection-key DataGrid prop.
|
||||
|
||||
`etaf-data-controller` accepts `:item-key` for stable selected-row lookup. When
|
||||
created inside a Component setup, its internal effect Scope is owned by the
|
||||
@ -662,7 +697,7 @@ Most applications need only `etaf-view`, `etaf-mount`, `etaf-define-component`,
|
||||
- Put every property before the first child.
|
||||
- Use `:font-weight 'bold`, not `:font-weight :bold`; the weight is an Elisp symbol value, not a property keyword.
|
||||
- Do not quote a structural View form.
|
||||
- Use `expr :value` for `if`, `when`, `let`, `mapcar`, or a View returned by ordinary Elisp.
|
||||
- Use `(expr FORM)` for `if`, `when`, `let`, `mapcar`, or a typed View returned by ordinary Elisp.
|
||||
- Use `(slot)` or `(slot FALLBACK...)` for the default outlet; use `:name 'header` for named slot content.
|
||||
- Keep writes out of rendering; use an event, Action, watch callback, or Effect.
|
||||
- Use `etaf-ui` Components for product controls; core Hosts are the structural foundation.
|
||||
|
||||
@ -80,7 +80,7 @@ View 含有状态型 Component、响应式数据、事件或生命周期时,
|
||||
(text
|
||||
:color (if dark "#F4F6FB" "#1F2328")
|
||||
:background-color "#20242B"
|
||||
(expr :value label))))
|
||||
(expr label))))
|
||||
```
|
||||
|
||||
子节点区是结构语法。`expr` 是执行普通 Elisp 的唯一明确桥接:
|
||||
@ -88,14 +88,15 @@ View 含有状态型 Component、响应式数据、事件或生命周期时,
|
||||
```elisp
|
||||
(etaf-view
|
||||
(column
|
||||
(expr :value (if loading "Loading..." "Ready"))
|
||||
(expr (if loading "Loading..." "Ready"))
|
||||
(expr
|
||||
:value
|
||||
(when open
|
||||
(etaf-view (text :font-style 'italic "Details"))))))
|
||||
```
|
||||
|
||||
`expr` 只接受 `:value`,不能有子节点。返回值可以是字符串、View、序列或 `nil`。`if`、`when`、`cond`、`let`、`mapcar` 和 `cl-loop` 仍然是 value 中的普通 Elisp。
|
||||
`expr` 只接受一个普通 Elisp form,不能有结构子节点。返回值可以是字符串、typed
|
||||
View、typed View proper sequence 或 `nil`。`if`、`when`、`cond`、`let`、`mapcar` 和
|
||||
`cl-loop` 仍是普通 Elisp。
|
||||
|
||||
quote 只有普通 Elisp 的含义:
|
||||
|
||||
@ -134,7 +135,7 @@ quote 只有普通 Elisp 的含义:
|
||||
:view
|
||||
(text
|
||||
:font-weight 'bold
|
||||
(expr :value label)))
|
||||
(expr label)))
|
||||
|
||||
(etaf-mount
|
||||
"*etaf-status*"
|
||||
@ -154,8 +155,9 @@ Component 的规范名称可以带 `etaf-` 前缀:
|
||||
|
||||
| 关键字 | 作用 |
|
||||
| --- | --- |
|
||||
| `:view` | 无状态 Component 的 View 生产者,与 `:setup` 互斥 |
|
||||
| `:setup` | 一次性初始化,返回零参数 render 函数 |
|
||||
| `:view` | 声明式 View frontend,与 `:render` 互斥 |
|
||||
| `:render` | 普通 Elisp frontend,通过 `etaf-node` 返回一个 typed View |
|
||||
| `:setup` | 可选的一次性初始化,返回由 `etaf-state` 读取的 opaque 状态 |
|
||||
| `:styles` | 可选的静态作用域样式声明 |
|
||||
|
||||
没有单独的 children、slot、event、state 或 variant 声明块。业务 props 通过 `(&key ...)` 声明,children 和 slot 是隐式内容。
|
||||
@ -173,18 +175,23 @@ Component 自己拥有状态时使用 `:setup`:
|
||||
(lambda () (message "%s mounted" title)))
|
||||
(etaf-on-unmounted
|
||||
(lambda () (message "%s unmounted" title)))
|
||||
(lambda ()
|
||||
(etaf-view
|
||||
(column
|
||||
(text :font-weight 'bold (expr :value title))
|
||||
(text (expr :value (format "Count: %d" (etaf-value count))))
|
||||
(text
|
||||
:role 'button
|
||||
:on-press (lambda () (cl-incf (etaf-value count)))
|
||||
"Increment"))))))
|
||||
count)
|
||||
:view
|
||||
(column
|
||||
(text :font-weight 'bold (expr title))
|
||||
(text (expr (format "Count: %d" (etaf-value (etaf-state)))))
|
||||
(text
|
||||
:role 'button
|
||||
:on-press
|
||||
(let ((count (etaf-state)))
|
||||
(lambda () (cl-incf (etaf-value count))))
|
||||
"Increment")))
|
||||
```
|
||||
|
||||
Setup 对 retained instance 只执行一次,返回的 render 函数在更新时重复运行。`etaf-on-mounted`、`etaf-on-updated` 和 `etaf-on-unmounted` 注册该 Component 的生命周期 callback。Scope 释放时会自动停止响应式 effect 并运行 cleanup。
|
||||
Setup 对 retained instance 只执行一次,返回一个 opaque 状态值。选定的 `:view` 或
|
||||
`:render` frontend 在更新时运行,并通过 `etaf-state` 读取这个准确值。
|
||||
`etaf-on-mounted`、`etaf-on-updated` 和 `etaf-on-unmounted` 注册该 Component 的生命周期
|
||||
callback。Scope 释放时会自动停止响应式 effect 并运行 cleanup。
|
||||
|
||||
响应式 API 只有一套模型:
|
||||
|
||||
@ -219,7 +226,7 @@ Setup 对 retained instance 只执行一次,返回的 render 函数在更新
|
||||
"Render a titled panel."
|
||||
:view
|
||||
(column
|
||||
(text :font-weight 'bold (expr :value title))
|
||||
(text :font-weight 'bold (expr title))
|
||||
(slot (text :color "#687386" "No content"))))
|
||||
|
||||
(etaf-view
|
||||
@ -236,7 +243,7 @@ Setup 对 retained instance 只执行一次,返回的 render 函数在更新
|
||||
:view
|
||||
(column
|
||||
(slot :name 'header
|
||||
(text :font-weight 'bold (expr :value title)))
|
||||
(text :font-weight 'bold (expr title)))
|
||||
(slot (text :color "#687386" "No body"))))
|
||||
|
||||
(etaf-view
|
||||
@ -248,6 +255,21 @@ Setup 对 retained instance 只执行一次,返回的 render 函数在更新
|
||||
|
||||
默认 slot 的两个用户简写是 `(slot)` 和 `(slot FALLBACK...)`。内部统一形式是 `(slot :name 'default FALLBACK...)`。调用处的普通子节点填充 `default`;命名内容写成 `(slot :name 'header CHILD...)`。显式空的 `(slot :name 'header)` 会抑制 fallback。字符串、数字、变量和运行时表达式都不是合法 slot name。
|
||||
|
||||
在结构边界,`expr` 可以返回 typed View 或 typed View 的 proper sequence,但它不暴露、
|
||||
也不接受 ETAF 私有 struct。同一个 Component 可以组合 keyed `:for`、结构表达式和
|
||||
命名 footer slot:
|
||||
|
||||
```elisp
|
||||
(etaf-define-component etaf-docs-collection-card (&key items footer-view)
|
||||
"Render keyed rows, one dynamic typed View, and a footer slot."
|
||||
:view
|
||||
(column
|
||||
(row :for (item items) :key (car item)
|
||||
(text (expr (cdr item))))
|
||||
(expr footer-view)
|
||||
(slot :name 'footer (text "No footer"))))
|
||||
```
|
||||
|
||||
## 7. 样式与 Theme
|
||||
|
||||
静态 Component 样式只有一种声明形式:
|
||||
@ -290,10 +312,9 @@ Theme 是 Context 的便捷形式,不是另一个 Runtime 对象:
|
||||
(etaf-define-component themed-shell ()
|
||||
"Provide default text colors to a subtree."
|
||||
:setup
|
||||
(progn
|
||||
(etaf-theme-provide
|
||||
'(:color "#F4F6FB" :bgcolor "#202634"))
|
||||
(lambda () (etaf-view (slot)))))
|
||||
(etaf-theme-provide
|
||||
'(:color "#F4F6FB" :bgcolor "#202634"))
|
||||
:view (slot))
|
||||
```
|
||||
|
||||
如果应用有亮/暗两套 palette,应把语义 role 集中放在一份 palette plist 中,
|
||||
@ -369,6 +390,15 @@ Behavior 用来打包可复用的非视觉属性和 cleanup:
|
||||
|
||||
Installer 需要 Runtime 或 Host path 时,可以调用 `etaf-current-behavior-context`。Behavior 被替换时,旧 cleanup 会在新状态成为当前状态前运行。Behavior equality 对 function 和 reactive value 使用 `eq`;因此新建的 installer closure 会被视为有意替换,而不是错误复用。替换状态先以 mounted resource registry 的 staged resource 保存,generation commit 后才成为 authority。
|
||||
|
||||
组合规则固定:Host callback 先于 Behavior callback,Behavior 按声明顺序运行,callback
|
||||
error 会 short-circuit 剩余 callback。非事件属性先由 Host 获胜,否则由第一个
|
||||
Behavior 获胜(first-wins)。重复 Behavior name 在 installer 前失败;稳定 installer 会复用,每个
|
||||
已安装 cleanup exactly-once。dispatch 只命中准确 Host,不存在 capture 或 bubble。
|
||||
|
||||
Action name 使用 application/feature-prefixed symbol;重复 Action 注册默认报错。显式
|
||||
reload 用 `etaf-action-redefine-run` 包住替换,它只改变未来按 name 的 dispatch,不会
|
||||
flush 已挂载 Runtime。
|
||||
|
||||
Focus 和 hit testing 是 Runtime 操作:
|
||||
|
||||
```elisp
|
||||
@ -391,15 +421,14 @@ Context 适合跨多层共享依赖,不适合普通 label:
|
||||
:setup
|
||||
(let ((service (etaf-ref "demo-service")))
|
||||
(etaf-provide 'service service)
|
||||
(lambda () (etaf-view (slot)))))
|
||||
service)
|
||||
:view (slot))
|
||||
|
||||
(etaf-define-component service-label ()
|
||||
"Read the inherited service."
|
||||
:setup
|
||||
(let ((service (etaf-inject 'service nil t)))
|
||||
(lambda ()
|
||||
(etaf-view
|
||||
(text (expr :value (format "Service: %s" (etaf-value service)))))))
|
||||
:setup (etaf-inject 'service nil t)
|
||||
:view
|
||||
(text (expr (format "Service: %s" (etaf-value (etaf-state))))))
|
||||
|
||||
(etaf-mount
|
||||
"*etaf-context*"
|
||||
@ -465,8 +494,8 @@ Controller 通过 `etaf-data-items`、`etaf-data-status`、`etaf-data-error`、`
|
||||
identity 进入或离开主 selection 时它才会变化,应用直接写
|
||||
`etaf-data-selection` 时也一样。DataGrid 默认把这些 ref 与 keyed retained row
|
||||
owner 配合使用,因此单选变化只会使旧行和新行失效,而不是整个可见页。
|
||||
selection 由 Controller 外部拥有时,仍可使用自定义 `:row-selected-p` 与
|
||||
`:selected-key` 契约。
|
||||
selection 由 Controller 外部拥有时,仍可使用自定义 `:row-selected-p`。稳定 identity
|
||||
继续来自必选的 `:row-key`;DataGrid 没有第二个 selection-key prop。
|
||||
|
||||
`etaf-data-controller` 支持用于稳定选中行查找的 `:item-key`。在 Component
|
||||
setup 中创建时,它的内部 effect Scope 会自动归当前 Component Scope 所有;如果
|
||||
@ -650,7 +679,7 @@ observer,不修改任何函数。
|
||||
- 所有属性必须放在第一个子节点之前。
|
||||
- 使用 `:font-weight 'bold`,不要使用 `:font-weight :bold`;weight 是 Elisp symbol 值,不是属性 keyword。
|
||||
- 结构性 View form 不要 quote。
|
||||
- `if`、`when`、`let`、`mapcar` 或 Elisp 返回 View 时,使用 `expr :value`。
|
||||
- `if`、`when`、`let`、`mapcar` 或 Elisp 返回 typed View 时,使用 `(expr FORM)`。
|
||||
- 默认 outlet 使用 `(slot)` 或 `(slot FALLBACK...)`;命名内容使用 `:name 'header`。
|
||||
- 不要在 render 中写状态;使用事件、Action、watch callback 或 Effect。
|
||||
- 产品级控件使用 `etaf-ui` Component;core Host 只是结构基础。
|
||||
|
||||
111
scripts/etaf-m0b-component-manifest.el
Normal file
111
scripts/etaf-m0b-component-manifest.el
Normal file
@ -0,0 +1,111 @@
|
||||
;;; etaf-m0b-component-manifest.el --- Observed Component contract manifest -*- lexical-binding: t; -*-
|
||||
|
||||
;; SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Emit a machine-readable M0b manifest that keeps Component business props,
|
||||
;; forwarded Host attributes, and the single-root forwarding guarantee in
|
||||
;; separate fields. The manifest is emitted only after a real mounted probe.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'json)
|
||||
(require 'etaf)
|
||||
|
||||
(defconst etaf-m0b-component-manifest--dsl-name
|
||||
'etaf-m0b-manifest-dsl-card)
|
||||
|
||||
(defconst etaf-m0b-component-manifest--render-name
|
||||
'etaf-m0b-manifest-render-card)
|
||||
|
||||
(defun etaf-m0b-component-manifest--define-fixtures ()
|
||||
"Define the two manifest probe Components through the public authoring API."
|
||||
(etaf-component-redefine-run
|
||||
(lambda ()
|
||||
(eval
|
||||
'(etaf-define-component etaf-m0b-manifest-dsl-card (&key title)
|
||||
:view
|
||||
(box :class "definition" (text (expr title)))))
|
||||
(eval
|
||||
'(etaf-define-component etaf-m0b-manifest-render-card (&key label)
|
||||
:render
|
||||
(etaf-node 'box (list :class "definition")
|
||||
(list (etaf-node 'text nil (list label)))))))))
|
||||
|
||||
(defun etaf-m0b-component-manifest--spec-props (name)
|
||||
"Return declared business props for Component NAME."
|
||||
(let ((spec (gethash name etaf--view-registry)))
|
||||
(unless (etaf--component-spec-p spec)
|
||||
(error "Manifest fixture is not a Component: %S" name))
|
||||
(mapcar #'symbol-name (etaf--component-spec-props spec))))
|
||||
|
||||
(defun etaf-m0b-component-manifest--mounted-probe (name prop value ref text)
|
||||
"Mount NAME with PROP VALUE and verify forwarded attrs at REF and TEXT."
|
||||
(let ((buffer (generate-new-buffer-name " *etaf-m0b-manifest*")))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(etaf-mount
|
||||
buffer
|
||||
(etaf-node name
|
||||
(list prop value :class "caller"
|
||||
:aria-label "manifest-probe" :ref ref)
|
||||
nil))
|
||||
(let* ((runtime (etaf-runtime-for-buffer buffer))
|
||||
(props (etaf-runtime-host-props-for runtime ref)))
|
||||
(unless (and (string-match-p (regexp-quote text)
|
||||
(with-current-buffer buffer
|
||||
(buffer-string)))
|
||||
(member "caller" (plist-get props :class))
|
||||
(equal "manifest-probe"
|
||||
(plist-get props :aria-label)))
|
||||
(error "Mounted Component forwarding probe failed: %S" name)))
|
||||
t)
|
||||
(when-let* ((runtime (etaf-runtime-for-buffer buffer)))
|
||||
(etaf-unmount runtime))
|
||||
(when-let* ((live (get-buffer buffer)))
|
||||
(kill-buffer live)))))
|
||||
|
||||
(defun etaf-m0b-component-manifest-data ()
|
||||
"Return the observed M0b Component manifest as an alist."
|
||||
(etaf-m0b-component-manifest--define-fixtures)
|
||||
(let ((dsl-ok
|
||||
(etaf-m0b-component-manifest--mounted-probe
|
||||
etaf-m0b-component-manifest--dsl-name :title "DSL" 'manifest-dsl
|
||||
"DSL"))
|
||||
(render-ok
|
||||
(etaf-m0b-component-manifest--mounted-probe
|
||||
etaf-m0b-component-manifest--render-name :label "Render"
|
||||
'manifest-render "Render"))
|
||||
(attrs '("class" "style/presentation" "layout" "ref"
|
||||
"role/tab-index" "aria-*" "on-*" "use")))
|
||||
`((schema-version . 1)
|
||||
(evidence-mode . "observed")
|
||||
(components
|
||||
. (((name . ,(symbol-name etaf-m0b-component-manifest--dsl-name))
|
||||
(frontend . "view")
|
||||
(declared-business-props
|
||||
. ,(etaf-m0b-component-manifest--spec-props
|
||||
etaf-m0b-component-manifest--dsl-name))
|
||||
(forwarded-host-attrs . ,attrs)
|
||||
(root-shape-forwarding-guarantee
|
||||
. "single attr-capable root only; fragment, text, and multi-root results reject forwarded attrs")
|
||||
(mounted-validation . ,dsl-ok))
|
||||
((name . ,(symbol-name etaf-m0b-component-manifest--render-name))
|
||||
(frontend . "render")
|
||||
(declared-business-props
|
||||
. ,(etaf-m0b-component-manifest--spec-props
|
||||
etaf-m0b-component-manifest--render-name))
|
||||
(forwarded-host-attrs . ,attrs)
|
||||
(root-shape-forwarding-guarantee
|
||||
. "single attr-capable root only; fragment, text, and multi-root results reject forwarded attrs")
|
||||
(mounted-validation . ,render-ok)))))))
|
||||
|
||||
(defun etaf-m0b-component-manifest-write-json ()
|
||||
"Write the observed M0b Component manifest as JSON."
|
||||
(princ (json-encode (etaf-m0b-component-manifest-data))))
|
||||
|
||||
(provide 'etaf-m0b-component-manifest)
|
||||
|
||||
;;; etaf-m0b-component-manifest.el ends here
|
||||
@ -6,6 +6,12 @@
|
||||
|
||||
(require 'ert)
|
||||
(require 'cl-lib)
|
||||
(require 'json)
|
||||
(require 'macroexp)
|
||||
(require 'etaf)
|
||||
|
||||
(declare-function etaf-m0b-component-manifest-write-json
|
||||
"etaf-m0b-component-manifest" ())
|
||||
|
||||
(defconst etaf-docs-test--root
|
||||
(file-name-directory
|
||||
@ -30,7 +36,7 @@
|
||||
(unless (re-search-forward "^```[[:space:]]*$" nil t)
|
||||
(error "Unclosed Elisp documentation block"))
|
||||
(push (buffer-substring-no-properties start
|
||||
(line-beginning-position))
|
||||
(match-beginning 0))
|
||||
blocks))))
|
||||
(nreverse blocks)))
|
||||
|
||||
@ -47,6 +53,42 @@
|
||||
(end-of-file count)
|
||||
(error (error "Cannot read documentation form: %S" err))))))
|
||||
|
||||
(defun etaf-docs-test--forms (source)
|
||||
"Read and return every form in documentation SOURCE."
|
||||
(with-temp-buffer
|
||||
(insert source)
|
||||
(goto-char (point-min))
|
||||
(let (forms)
|
||||
(condition-case nil
|
||||
(while t (push (read (current-buffer)) forms))
|
||||
(end-of-file (nreverse forms))))))
|
||||
|
||||
(defun etaf-docs-test--mounted-collection-probe (forms)
|
||||
"Load exact documentation FORMS and mount the collection composition."
|
||||
(etaf-component-redefine-run
|
||||
(lambda ()
|
||||
(dolist (form forms)
|
||||
(eval form t))))
|
||||
(let ((buffer (generate-new-buffer " *etaf-docs-m0b*")))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(let ((view
|
||||
(eval
|
||||
'(etaf-view
|
||||
(etaf-docs-collection-card
|
||||
:items '((1 . "One") (2 . "Two"))
|
||||
:footer-view (etaf-view (text "Dynamic"))
|
||||
(slot :name 'footer (text "Footer"))))
|
||||
t)))
|
||||
(etaf-mount buffer view))
|
||||
(let ((text (with-current-buffer buffer (buffer-string))))
|
||||
(dolist (expected '("One" "Two" "Dynamic" "Footer"))
|
||||
(should (string-match-p expected text)))))
|
||||
(when-let* ((runtime (etaf-runtime-for-buffer buffer)))
|
||||
(etaf-unmount runtime))
|
||||
(when (buffer-live-p buffer)
|
||||
(kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest etaf-docs-have-paired-long-lived-files ()
|
||||
"Keep the public architecture, guide, and plan in both languages."
|
||||
(dolist (file '("README.md"
|
||||
@ -95,6 +137,119 @@
|
||||
(etaf-docs-test--read file)))
|
||||
(should (> (etaf-docs-test--read-all block) 0)))))
|
||||
|
||||
(ert-deftest etaf-docs-component-contract-is-current ()
|
||||
"Keep public Component prose aligned with the authoritative macro contract."
|
||||
(dolist (file '("docs/architecture.en.md" "docs/architecture.zh.md"
|
||||
"docs/user-guide.en.md" "docs/user-guide.zh.md"
|
||||
"docs/implementation-plan.en.md"
|
||||
"docs/implementation-plan.zh.md"))
|
||||
(let ((contents (etaf-docs-test--read file)))
|
||||
(should (string-match-p (regexp-quote ":view") contents))
|
||||
(should (string-match-p (regexp-quote ":render") contents))
|
||||
(should (string-match-p (regexp-quote ":setup") contents))
|
||||
(should (string-match-p (regexp-opt '("opaque state" "opaque 状态"))
|
||||
contents))
|
||||
(should-not
|
||||
(string-match-p
|
||||
(regexp-opt '("setup returns a zero-argument render function"
|
||||
":setup` runs once per retained identity and returns a zero-argument render function"
|
||||
":setup` runs once for a retained Component instance and must return a zero-argument render function"
|
||||
"返回零参数 render 函数"
|
||||
"必须返回零参数 render 函数"))
|
||||
contents)))))
|
||||
|
||||
(ert-deftest etaf-docs-data-grid-selection-contract-is-current ()
|
||||
"Document only selection inputs actually declared by `etaf-data-grid'."
|
||||
(dolist (file '("docs/user-guide.en.md" "docs/user-guide.zh.md"))
|
||||
(let ((contents (etaf-docs-test--read file)))
|
||||
(should (string-match-p (regexp-quote ":row-selected-p") contents))
|
||||
(should-not (string-match-p (regexp-quote ":selected-key") contents)))))
|
||||
|
||||
(ert-deftest etaf-docs-superseded-proposals-not-current-contract ()
|
||||
"Keep the old Component proposal explicitly historical."
|
||||
(let ((contents
|
||||
(etaf-docs-test--read "docs/proposals/component-definition.zh.md")))
|
||||
(should (string-match-p "SUPERSEDED" contents))
|
||||
(should (string-match-p "historical" contents))
|
||||
(should (string-match-p (regexp-quote "docs/architecture.zh.md") contents))
|
||||
(should-not (string-match-p "尚未实现" contents))))
|
||||
|
||||
(ert-deftest etaf-docs-module-boundaries-target-vocabulary-is-paired ()
|
||||
"Keep the English and Chinese target Component vocabulary equivalent."
|
||||
(dolist (file '("docs/proposals/module-boundaries.en.md"
|
||||
"docs/proposals/module-boundaries.zh.md"))
|
||||
(let ((contents (etaf-docs-test--read file)))
|
||||
(dolist (token '(":view" ":render" ":setup" "etaf-state" "etaf-node"
|
||||
"opaque"))
|
||||
(should (string-match-p (regexp-quote token) contents))))))
|
||||
|
||||
(ert-deftest etaf-docs-interaction-contract-is-current ()
|
||||
"Keep the public Action/Behavior/event composition contract explicit."
|
||||
(dolist (file '("docs/architecture.en.md" "docs/architecture.zh.md"
|
||||
"docs/user-guide.en.md" "docs/user-guide.zh.md"))
|
||||
(let ((contents (etaf-docs-test--read file)))
|
||||
(dolist (token '("Host" "Behavior" "short-circuit" "first"
|
||||
"capture" "bubble" "application" "feature"
|
||||
"etaf-action-redefine-run" "dispatch"))
|
||||
(should (string-match-p (regexp-quote token) contents))))))
|
||||
|
||||
(ert-deftest etaf-docs-component-manifest-is-machine-readable ()
|
||||
"Validate the three-column Component manifest and observed evidence."
|
||||
(let* ((script (expand-file-name "scripts/etaf-m0b-component-manifest.el"
|
||||
etaf-docs-test--root))
|
||||
(json-object-type 'alist)
|
||||
(json-array-type 'list)
|
||||
(manifest
|
||||
(with-temp-buffer
|
||||
(let ((standard-output (current-buffer)))
|
||||
(load script nil t)
|
||||
(etaf-m0b-component-manifest-write-json))
|
||||
(goto-char (point-min))
|
||||
(json-read))))
|
||||
(should (equal 1 (alist-get 'schema-version manifest)))
|
||||
(should (equal "observed" (alist-get 'evidence-mode manifest)))
|
||||
(dolist (entry (alist-get 'components manifest))
|
||||
(should (alist-get 'declared-business-props entry))
|
||||
(should (alist-get 'forwarded-host-attrs entry))
|
||||
(should (alist-get 'root-shape-forwarding-guarantee entry))
|
||||
(should (eq t (alist-get 'mounted-validation entry))))))
|
||||
|
||||
(ert-deftest etaf-docs-executable-suite-is-fail-closed ()
|
||||
"Classify exact blocks before macroexpansion, loading, or mounted smoke."
|
||||
(let ((fixture (expand-file-name
|
||||
"tests/fixtures/etaf-m0b-doc-examples.sexp"
|
||||
etaf-docs-test--root)))
|
||||
(should (file-readable-p fixture))
|
||||
(let ((records (with-temp-buffer
|
||||
(insert-file-contents fixture)
|
||||
(goto-char (point-min))
|
||||
(read (current-buffer)))))
|
||||
(should records)
|
||||
(dolist (record records)
|
||||
(let* ((file (plist-get record :file))
|
||||
(index (plist-get record :block))
|
||||
(classification (plist-get record :classification))
|
||||
(blocks (etaf-docs-test--elisp-blocks
|
||||
(etaf-docs-test--read file)))
|
||||
(source (nth (1- index) blocks)))
|
||||
(should source)
|
||||
(should (equal (secure-hash 'sha256 source)
|
||||
(plist-get record :sha256)))
|
||||
(should (memq classification
|
||||
'(macroexpand-only load-safe mounted-smoke)))
|
||||
(pcase classification
|
||||
('macroexpand-only
|
||||
(should (> (etaf-docs-test--read-all source) 0)))
|
||||
((or 'load-safe 'mounted-smoke)
|
||||
(let ((forms (etaf-docs-test--forms source)))
|
||||
(dolist (form forms)
|
||||
(should (macroexpand-all (copy-tree form))))
|
||||
(pcase (plist-get record :probe)
|
||||
('collection-composition
|
||||
(etaf-docs-test--mounted-collection-probe forms))
|
||||
('nil (dolist (form forms) (eval form t)))
|
||||
(_ (ert-fail "Unknown safe documentation probe")))))))))))
|
||||
|
||||
(ert-deftest etaf-source-uses-only-public-ebox-names ()
|
||||
"Keep the ETAF implementation independent of Ebox private functions."
|
||||
(dolist (file
|
||||
|
||||
26
tests/etaf-m0b-component-manifest-tests.el
Normal file
26
tests/etaf-m0b-component-manifest-tests.el
Normal file
@ -0,0 +1,26 @@
|
||||
;;; etaf-m0b-component-manifest-tests.el --- M0b manifest probes -*- lexical-binding: t; -*-
|
||||
|
||||
;; SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
(require 'etaf-m0b-component-manifest)
|
||||
|
||||
(ert-deftest etaf-m0b-component-manifest-is-derived-and-mounted ()
|
||||
"Derive business props and prove Host-attribute forwarding by mounting."
|
||||
(let* ((manifest (etaf-m0b-component-manifest-data))
|
||||
(components (alist-get 'components manifest)))
|
||||
(should (= 2 (length components)))
|
||||
(should (equal '("title")
|
||||
(alist-get 'declared-business-props (car components))))
|
||||
(should (equal '("label")
|
||||
(alist-get 'declared-business-props (cadr components))))
|
||||
(dolist (entry components)
|
||||
(should (alist-get 'forwarded-host-attrs entry))
|
||||
(should (string-match-p "single attr-capable root"
|
||||
(alist-get 'root-shape-forwarding-guarantee
|
||||
entry)))
|
||||
(should (eq t (alist-get 'mounted-validation entry))))))
|
||||
|
||||
;;; etaf-m0b-component-manifest-tests.el ends here
|
||||
1
tests/fixtures/etaf-m0a-document-examples.sexp
vendored
Normal file
1
tests/fixtures/etaf-m0a-document-examples.sexp
vendored
Normal file
File diff suppressed because one or more lines are too long
5
tests/fixtures/etaf-m0b-doc-examples.sexp
vendored
Normal file
5
tests/fixtures/etaf-m0b-doc-examples.sexp
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
((:file "docs/user-guide.en.md"
|
||||
:block 18
|
||||
:sha256 "79390863a3fdf604969cc4f424b36a9a466e569bd0ac740e61c580b2518cb2da"
|
||||
:classification mounted-smoke
|
||||
:probe collection-composition))
|
||||
Loading…
Reference in New Issue
Block a user