refactor: own author facts behind opaque source handles
This commit is contained in:
parent
31046ad8c9
commit
cb0e1823d3
7
Makefile
7
Makefile
@ -10,7 +10,7 @@ NATIVE_MANIFEST = native/Cargo.toml
|
||||
NATIVE_TARGET ?= $(shell $(EMACS_BATCH) -l ebox-native-reflow.el --eval '(princ (ebox-native-reflow--rust-target))')
|
||||
NATIVE_RELEASE_DIR = native/target/$(NATIVE_TARGET)/release
|
||||
|
||||
.PHONY: all check ci load compile test checkdoc core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests docs-contract-tests ci-contract-tests performance-evaluator visual-check native-rust-tests native-build diff-check clean package-lint package-lint-install
|
||||
.PHONY: all check ci load compile test checkdoc source-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests docs-contract-tests ci-contract-tests performance-evaluator visual-check native-rust-tests native-build diff-check clean package-lint package-lint-install
|
||||
|
||||
all: check
|
||||
|
||||
@ -25,7 +25,10 @@ compile:
|
||||
rm -f *.elc tests/*.elc scripts/*.elc
|
||||
$(EMACS_BATCH) --eval '(setq byte-compile-error-on-warn t byte-compile-warnings (quote (not obsolete)))' -l ebox.el --eval '(ebox-byte-compile)'
|
||||
|
||||
test: core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests docs-contract-tests ci-contract-tests
|
||||
test: source-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests docs-contract-tests ci-contract-tests
|
||||
|
||||
source-tests:
|
||||
$(EMACS_BATCH) -l tests/ebox-source-tests.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
core-tests:
|
||||
$(EMACS_TEST) -l tests/ebox-core-render-tests.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
11
README.md
11
README.md
@ -41,8 +41,9 @@ field-based child syntax.
|
||||
```
|
||||
|
||||
Use `ebox-build` for the public author DSL. Framework integrations may instead
|
||||
construct typed nodes with `ebox-text-create`, a typed LayoutConfig constructor,
|
||||
and `ebox-box-create`; that evaluated API is not a second author grammar.
|
||||
assemble typed nodes with one `ebox-source-builder`, then seal the forest and
|
||||
its source generation as one `CanonicalEboxInput`; that evaluated API is not a
|
||||
second author grammar.
|
||||
|
||||
## Layout choices
|
||||
|
||||
@ -59,12 +60,12 @@ do not require a wrapper node.
|
||||
|
||||
- `ebox-render` returns propertized text without publishing a live buffer.
|
||||
- `ebox-render-to-buffer` mounts a retained surface.
|
||||
- `ebox-commit` atomically publishes a newly built root.
|
||||
- `ebox-commit` atomically publishes a newly built canonical input.
|
||||
- `ebox-buffer-update-report` returns the last successful update report.
|
||||
- `ebox-rerender-buffer-with-context` applies an explicit viewport change.
|
||||
|
||||
Ebox copies author input before assigning runtime identity, so one built tree
|
||||
may be mounted in multiple buffers without sharing live ownership.
|
||||
Ebox copies canonical input before assigning runtime identity, so one built
|
||||
value may be mounted in multiple buffers without sharing live ownership.
|
||||
|
||||
## Optional native module
|
||||
|
||||
|
||||
@ -37,9 +37,9 @@ Ebox 需要 Emacs 29.1 或更高版本,并依赖 ECSS 与 TP。包管理器应
|
||||
(box "Right")))))
|
||||
```
|
||||
|
||||
普通 author DSL 统一通过 `ebox-build`。框架集成可以直接组合
|
||||
`ebox-text-create`、typed LayoutConfig constructor 和 `ebox-box-create`;这个
|
||||
evaluated API 不是第二套 author 语法。
|
||||
普通 author DSL 统一通过 `ebox-build`。框架集成可以用一个
|
||||
`ebox-source-builder` 组合 typed node,再把 forest 与同一代 source facts 封装为一个
|
||||
`CanonicalEboxInput`;这个 evaluated API 不是第二套 author 语法。
|
||||
|
||||
## 如何选择布局
|
||||
|
||||
@ -55,12 +55,12 @@ Flex/Grid participation property 直接属于子 `box`,不需要额外 wrapper
|
||||
|
||||
- `ebox-render` 返回带属性文本,不发布 live buffer;
|
||||
- `ebox-render-to-buffer` 挂载 retained surface;
|
||||
- `ebox-commit` 原子发布重新构建的 root;
|
||||
- `ebox-commit` 原子发布重新构建的 canonical input;
|
||||
- `ebox-buffer-update-report` 返回最近一次成功更新报告;
|
||||
- `ebox-rerender-buffer-with-context` 应用显式 viewport 变化。
|
||||
|
||||
Ebox 会在分配 runtime identity 前复制 author 输入,因此同一棵已构建树可以挂载到
|
||||
多个 buffer,而不会共享 live ownership。
|
||||
Ebox 会在分配 runtime identity 前复制 canonical input,因此同一个 built value 可以
|
||||
挂载到多个 buffer,而不会共享 live ownership。
|
||||
|
||||
## 可选 native 模块
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ This is the maintainer entry point for the standalone Ebox repository. It descri
|
||||
| --- | --- |
|
||||
| `ebox.el` | Public facade, construction helpers, rendering, TP-backed buffer entry points, scrolling, commit, and byte compilation. |
|
||||
| `ebox-cache.el` | Measurement/render cache records, invalidation, and cache reports. |
|
||||
| `ebox-source.el` | Opaque source handles, immutable author source records, and candidate source indexes. |
|
||||
| `ebox-style.el` | ECSS property schemas, declarations and cascade, shorthand expansion, computed style, colors, borders, and dirty effects. |
|
||||
| `ebox-tree.el` | Node traversal, logical child access, ECSS subject adaptation, identity, parent paths, keys, and tree snapshots. |
|
||||
| `ebox-child-range.el` | Immutable weighted segment trie, sparse persistent key trie, Range replacement, and Gate A metrics. |
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
| --- | --- |
|
||||
| `ebox.el` | 公共门面、构造辅助函数、渲染、基于 TP 的 buffer 入口、滚动、commit 与 byte compile。 |
|
||||
| `ebox-cache.el` | 测量/渲染缓存记录、失效和缓存报告。 |
|
||||
| `ebox-source.el` | opaque source handle、不可变 author source record 与 candidate source index。 |
|
||||
| `ebox-style.el` | ECSS property schema、declaration 与 cascade、shorthand 展开、computed style、颜色、border 和 dirty effect。 |
|
||||
| `ebox-tree.el` | 节点遍历、逻辑子节点访问、ECSS subject 适配、identity、父路径、key 和树 snapshot。 |
|
||||
| `ebox-child-range.el` | 不可变 weighted segment trie、稀疏持久 key trie、Range replacement 与 Gate A metrics。 |
|
||||
|
||||
@ -31,6 +31,11 @@ one string payload. Every Box form accepts zero or more directly nested Text
|
||||
or Box children. The form name selects Normal, Row, Column, Flex, or Grid child
|
||||
layout; authors do not pass a layout selector as a property.
|
||||
|
||||
`ebox-build` returns one opaque `CanonicalEboxInput`. It atomically carries the
|
||||
ordered canonical forest and the source generation that owns its author facts.
|
||||
Ordinary author code passes that value unchanged to Ebox render/publication
|
||||
functions; it does not extract a node or construct a source index.
|
||||
|
||||
```elisp
|
||||
(ebox-build
|
||||
'(column :padding (1 2)
|
||||
@ -73,28 +78,87 @@ to Ebox and invalid context/value combinations signal an error.
|
||||
|
||||
## 2. Evaluated typed construction
|
||||
|
||||
Frameworks that already own author parsing can construct canonical nodes
|
||||
directly:
|
||||
Frameworks that already own author parsing can use the evaluated integration
|
||||
API. This API is not a second author grammar. One `ebox-source-builder` owns
|
||||
the complete source generation; every typed node carries a handle from that
|
||||
builder and node-owned facts projected from the same normalized declarations.
|
||||
The final `CanonicalEboxInput` transports the forest and sealed source index as
|
||||
one value:
|
||||
|
||||
```elisp
|
||||
(let ((layout (ebox-column-layout-create
|
||||
:item-gap 1 :cross-align 'stretch)))
|
||||
(ebox-box-create
|
||||
:layout layout
|
||||
:children (list (ebox-text-create :value "One")
|
||||
(ebox-text-create :value "Two"))))
|
||||
(let* ((builder (ebox-source-builder-create))
|
||||
(root-declarations nil)
|
||||
(left-declarations nil)
|
||||
(right-declarations nil)
|
||||
(root-handle
|
||||
(ebox-source-builder-bind
|
||||
builder :declarations root-declarations))
|
||||
(left-handle
|
||||
(ebox-source-builder-bind
|
||||
builder :declarations left-declarations))
|
||||
(right-handle
|
||||
(ebox-source-builder-bind
|
||||
builder :declarations right-declarations))
|
||||
(left
|
||||
(ebox-text-create
|
||||
:value "Left"
|
||||
:source-handle left-handle
|
||||
:owned-facts
|
||||
(ebox-canonical-facts-from-declarations
|
||||
'text left-declarations)))
|
||||
(right
|
||||
(ebox-text-create
|
||||
:value "Right"
|
||||
:source-handle right-handle
|
||||
:owned-facts
|
||||
(ebox-canonical-facts-from-declarations
|
||||
'text right-declarations)))
|
||||
(root
|
||||
(ebox-box-create
|
||||
:layout (ebox-row-layout-create
|
||||
:item-gap 1 :cross-align 'center)
|
||||
:children (list left right)
|
||||
:source-handle root-handle
|
||||
:owned-facts
|
||||
(ebox-canonical-facts-from-declarations
|
||||
'row root-declarations))))
|
||||
(ebox-canonical-input-create
|
||||
(list root)
|
||||
(ebox-source-builder-finish builder)))
|
||||
```
|
||||
|
||||
- `ebox-text-create` requires `:value STRING`.
|
||||
- `ebox-source-builder-create` begins exactly one mutable source assembly.
|
||||
- `ebox-source-builder-bind` stores normalized source facts and returns an
|
||||
identity-only opaque handle.
|
||||
- `ebox-source-builder-finish` detaches and seals the builder's immutable fact
|
||||
index. A sealed builder rejects further mutation.
|
||||
- `ebox-canonical-facts-from-declarations` projects the node-owned facts from
|
||||
the same normalized declarations passed to the source builder.
|
||||
- `ebox-text-create` requires `:value`, `:source-handle`, and `:owned-facts`.
|
||||
- `ebox-normal-layout-create` creates Normal LayoutConfig.
|
||||
- `ebox-row-layout-create` and `ebox-column-layout-create` accept
|
||||
`:item-gap` and `:cross-align`.
|
||||
- `ebox-flex-layout-create` creates a validated Flex LayoutConfig.
|
||||
- `ebox-grid-layout-create` creates a validated Grid LayoutConfig.
|
||||
- `ebox-box-create` requires one typed `:layout` value and accepts a
|
||||
`:children` list of typed nodes.
|
||||
- `ebox-box-create` requires `:layout`, `:source-handle`, and `:owned-facts`;
|
||||
it accepts a `:children` list of typed nodes.
|
||||
- `ebox-canonical-input-create` combines an ordered node forest with the one
|
||||
source index that owns every referenced handle; this boundary validates the
|
||||
exact set and seals source order from the forest's final preorder, independent
|
||||
of bottom-up construction order.
|
||||
- `ebox-canonical-input-root-host-ref` returns the detached publication address
|
||||
of a validated single-root input without exposing its source handle.
|
||||
- `ebox-canonical-input-roots` returns a shallow, read-only copy of the exact
|
||||
top-level roots. It does not import source facts into another candidate.
|
||||
- `ebox-canonical-input-import-roots` accepts only exact, unique top-level roots
|
||||
from that input and an open source builder. It imports facts only for the
|
||||
selected subtrees, preserves their node, source-handle, and immutable-record
|
||||
identities, and never adopts facts from unselected roots.
|
||||
- `ebox-canonical-input-equal-p` compares the complete canonical forest, Range
|
||||
anchors, source identities, and immutable source facts for no-op proofs.
|
||||
|
||||
The last two fields belong only to this evaluated integration contract. They
|
||||
These source builders, fact/input constructors, and typed node/layout
|
||||
constructors belong only to the evaluated integration contract. Their fields
|
||||
are not author properties or alternate DSL syntax.
|
||||
|
||||
Node inspection functions are `ebox-node-kind`, `ebox-text-node-p`,
|
||||
@ -107,10 +171,10 @@ though the facade inventory below focuses on application entry points.
|
||||
|
||||
| Function | Contract |
|
||||
| --- | --- |
|
||||
| `ebox-render` | Return a propertized string without publishing a live buffer. |
|
||||
| `ebox-render-to-buffer` | Copy the built root, mount a retained TP surface, enable `ebox-buffer-mode`, and return the buffer. Its optional plist accepts only `:observer FUNCTION`. |
|
||||
| `ebox-display-buffer` | Render through the retained path, delete other windows, and switch to the result. |
|
||||
| `ebox-commit` | Atomically publish a newly built root or one-shot logical candidate. |
|
||||
| `ebox-render` | Accept a single-root `CanonicalEboxInput` and return a propertized string without publishing a live buffer. |
|
||||
| `ebox-render-to-buffer` | Accept a single-root `CanonicalEboxInput`, mount a retained TP surface, enable `ebox-buffer-mode`, and return the buffer. Its optional plist accepts only `:observer FUNCTION`. |
|
||||
| `ebox-display-buffer` | Accept a single-root `CanonicalEboxInput`, render through the retained path, delete other windows, and switch to the result. |
|
||||
| `ebox-commit` | Atomically publish a single-root `CanonicalEboxInput` or a one-shot logical candidate returned by `ebox-candidate-begin`. |
|
||||
| `ebox-buffer-set-observer` | Set or remove one function-valued observer on an already mounted buffer. |
|
||||
| `ebox-buffer-update-report` | Return a defensive copy of the last successful update report. |
|
||||
| `ebox-rerender-buffer-with-context` | Apply explicit viewport width and optional height while retaining identity. |
|
||||
@ -154,13 +218,13 @@ They do not publish by themselves.
|
||||
| `ebox-region-ids` | Return current low-level region ids in document order. |
|
||||
| `ebox-region-resolve` | Resolve one logical `:id` in a mounted buffer to an opaque surface-scoped handle. |
|
||||
| `ebox-region-update` | Apply supported mutable style or scroll properties through one retained transaction. |
|
||||
| `ebox-child-range` | Build a transparent, addressable child Range descriptor from typed nodes. |
|
||||
| `ebox-child-range` | Framework integration only: build a nonvisual, addressable child Range descriptor while assembling a typed canonical input. |
|
||||
| `ebox-candidate-begin` | Capture the exact current mounted generation in a one-shot candidate. |
|
||||
| `ebox-candidate-replace` | Replace one candidate node address. |
|
||||
| `ebox-candidate-replace-range-ref` | Replace one transparent child Range payload. |
|
||||
| `ebox-candidate-replace-root` | Replace the candidate's private root address. |
|
||||
| `ebox-candidate-replace-host-ref` | Replace one framework-owned opaque host reference. |
|
||||
| `ebox-candidate-patch-host-paint` | Record a proven paint-only host patch or return nil when replacement is required. |
|
||||
| `ebox-candidate-replace` | Replace one candidate node address from a single-root `CanonicalEboxInput`. |
|
||||
| `ebox-candidate-replace-range-ref` | Replace one transparent child Range payload from a forest-valued `CanonicalEboxInput`. |
|
||||
| `ebox-candidate-replace-root` | Replace the candidate's private root address from a single-root `CanonicalEboxInput`. |
|
||||
| `ebox-candidate-replace-host-ref` | Replace one framework-owned opaque host reference from a single-root `CanonicalEboxInput`. |
|
||||
| `ebox-candidate-patch-host-paint` | Compare previous and next single-root canonical inputs; record a proven paint-only host patch or return nil when replacement is required. |
|
||||
| `ebox-host-ref-bounds` | Return current margin-free bounds for an opaque host reference. |
|
||||
| `ebox-host-ref-position` | Return its first current position. |
|
||||
|
||||
@ -168,6 +232,10 @@ Handles and reported positions belong to one live publication generation.
|
||||
Resolve them again after their object is removed. A candidate is sealed by
|
||||
commit and cannot be reused.
|
||||
|
||||
The author DSL has no Range form. Ordinary authors compose direct Text/Box
|
||||
children and never call `ebox-child-range`; Range addresses exist only for
|
||||
frameworks that already own semantic child ranges and candidate updates.
|
||||
|
||||
## 5. Selectors
|
||||
|
||||
ECSS is the sole selector parser and matcher. Ebox supplies logical tree
|
||||
@ -216,7 +284,7 @@ names every entry:
|
||||
- Construction: `ebox-build`, `ebox-text-create`,
|
||||
`ebox-normal-layout-create`, `ebox-row-layout-create`,
|
||||
`ebox-column-layout-create`, `ebox-flex-layout-create`,
|
||||
`ebox-grid-layout-create`, `ebox-box-create`, `ebox-child-range`.
|
||||
`ebox-grid-layout-create`, `ebox-box-create`.
|
||||
- Render/publication: `ebox-render`, `ebox-render-to-buffer`,
|
||||
`ebox-display-buffer`, `ebox-commit`, `ebox-buffer-set-observer`,
|
||||
`ebox-buffer-update-report`,
|
||||
@ -226,6 +294,7 @@ names every entry:
|
||||
- Candidate/identity: `ebox-candidate-begin`, `ebox-candidate-replace`,
|
||||
`ebox-candidate-replace-range-ref`, `ebox-candidate-replace-root`,
|
||||
`ebox-candidate-replace-host-ref`, `ebox-candidate-patch-host-paint`,
|
||||
`ebox-child-range`,
|
||||
`ebox-region-ids`, `ebox-region-resolve`, `ebox-region-update`,
|
||||
`ebox-host-ref-bounds`, `ebox-host-ref-position`.
|
||||
- Selectors: `ebox-selector-parse`, `ebox-selector-match-node-p`,
|
||||
|
||||
@ -29,6 +29,10 @@ STRING
|
||||
每种 Box form 都接受零个或多个直接嵌套的 Text/Box 子节点。form 名称选择 Normal、
|
||||
Row、Column、Flex 或 Grid 子布局;author 不通过 property 传入布局选择器。
|
||||
|
||||
`ebox-build` 返回一个不透明的 `CanonicalEboxInput`,以原子整体携带有序 canonical
|
||||
forest 和拥有其 author facts 的 source generation。普通 author 代码只把这个值原样
|
||||
传给 Ebox 渲染/发布函数,不取出 node,也不构造 source index。
|
||||
|
||||
```elisp
|
||||
(ebox-build
|
||||
'(column :padding (1 2)
|
||||
@ -69,27 +73,83 @@ Ebox;context/value 组合无效时会直接报错。
|
||||
|
||||
## 2. Evaluated typed construction
|
||||
|
||||
已经拥有 author parsing 的框架可以直接构造 canonical node:
|
||||
已经拥有 author parsing 的框架可以使用 evaluated 集成 API;它不是第二套 author
|
||||
语法。一个 `ebox-source-builder` 拥有完整 source generation;每个 typed node 都携带
|
||||
该 builder 返回的 handle,以及由同一份 normalized declarations 投影出的 node-owned
|
||||
facts。最后用一个 `CanonicalEboxInput` 同时传递 forest 与封存的 source index:
|
||||
|
||||
```elisp
|
||||
(let ((layout (ebox-column-layout-create
|
||||
:item-gap 1 :cross-align 'stretch)))
|
||||
(ebox-box-create
|
||||
:layout layout
|
||||
:children (list (ebox-text-create :value "One")
|
||||
(ebox-text-create :value "Two"))))
|
||||
(let* ((builder (ebox-source-builder-create))
|
||||
(root-declarations nil)
|
||||
(left-declarations nil)
|
||||
(right-declarations nil)
|
||||
(root-handle
|
||||
(ebox-source-builder-bind
|
||||
builder :declarations root-declarations))
|
||||
(left-handle
|
||||
(ebox-source-builder-bind
|
||||
builder :declarations left-declarations))
|
||||
(right-handle
|
||||
(ebox-source-builder-bind
|
||||
builder :declarations right-declarations))
|
||||
(left
|
||||
(ebox-text-create
|
||||
:value "Left"
|
||||
:source-handle left-handle
|
||||
:owned-facts
|
||||
(ebox-canonical-facts-from-declarations
|
||||
'text left-declarations)))
|
||||
(right
|
||||
(ebox-text-create
|
||||
:value "Right"
|
||||
:source-handle right-handle
|
||||
:owned-facts
|
||||
(ebox-canonical-facts-from-declarations
|
||||
'text right-declarations)))
|
||||
(root
|
||||
(ebox-box-create
|
||||
:layout (ebox-row-layout-create
|
||||
:item-gap 1 :cross-align 'center)
|
||||
:children (list left right)
|
||||
:source-handle root-handle
|
||||
:owned-facts
|
||||
(ebox-canonical-facts-from-declarations
|
||||
'row root-declarations))))
|
||||
(ebox-canonical-input-create
|
||||
(list root)
|
||||
(ebox-source-builder-finish builder)))
|
||||
```
|
||||
|
||||
- `ebox-text-create` 要求 `:value STRING`;
|
||||
- `ebox-source-builder-create` 开始且只开始一次 mutable source assembly;
|
||||
- `ebox-source-builder-bind` 保存 normalized source facts,并返回只含 identity 的
|
||||
opaque handle;
|
||||
- `ebox-source-builder-finish` 分离并封存 builder 的 immutable fact index;封存后
|
||||
builder 拒绝继续修改;
|
||||
- `ebox-canonical-facts-from-declarations` 从传给 source builder 的同一份 normalized
|
||||
declarations 投影 node-owned facts;
|
||||
- `ebox-text-create` 要求 `:value`、`:source-handle` 与 `:owned-facts`;
|
||||
- `ebox-normal-layout-create` 构造 Normal LayoutConfig;
|
||||
- `ebox-row-layout-create` 与 `ebox-column-layout-create` 接受
|
||||
`:item-gap` 和 `:cross-align`;
|
||||
- `ebox-flex-layout-create` 构造经过验证的 Flex LayoutConfig;
|
||||
- `ebox-grid-layout-create` 构造经过验证的 Grid LayoutConfig;
|
||||
- `ebox-box-create` 要求一个 typed `:layout` 值,并接受 typed node 的
|
||||
`:children` list。
|
||||
- `ebox-box-create` 要求 `:layout`、`:source-handle` 与 `:owned-facts`,并接受 typed
|
||||
node 的 `:children` list;
|
||||
- `ebox-canonical-input-create` 把有序 node forest 与拥有其中全部 handle 的唯一
|
||||
source index 组合为一个值;这个边界验证精确 handle 集合,并从 forest 的最终
|
||||
preorder 封存 source order,不依赖 bottom-up 构造顺序。
|
||||
- `ebox-canonical-input-root-host-ref` 返回经过验证的 single-root input 的分离发布
|
||||
地址,不暴露其中的 source handle;
|
||||
- `ebox-canonical-input-roots` 返回 exact top-level roots 的 shallow、只读副本,
|
||||
不会把 source facts 导入另一个 candidate;
|
||||
- `ebox-canonical-input-import-roots` 只接受该 input 中 exact、唯一的 top-level
|
||||
roots 与一个 open source builder;它只导入选中 subtree 的 facts,保留 node、
|
||||
source-handle 与 immutable record identity,绝不接管未选 root 的 facts;
|
||||
- `ebox-canonical-input-equal-p` 比较完整 canonical forest、Range anchor、source
|
||||
identity 和 immutable source facts,用于 no-op 证明。
|
||||
|
||||
最后两个 field 只属于 evaluated 集成契约,不是 author property,也不是另一套 DSL。
|
||||
这些 source builder、fact/input constructor 与 typed node/layout constructor 只属于
|
||||
evaluated 集成契约;其中的 field 不是 author property,也不是另一套 DSL。
|
||||
|
||||
typed 模块还公开 `ebox-node-kind`、`ebox-text-node-p`、`ebox-box-node-p`、
|
||||
`ebox-node-source-handle`、`ebox-text-node-value`、`ebox-box-node-layout`、
|
||||
@ -100,10 +160,10 @@ typed 模块还公开 `ebox-node-kind`、`ebox-text-node-p`、`ebox-box-node-p`
|
||||
|
||||
| 函数 | 契约 |
|
||||
| --- | --- |
|
||||
| `ebox-render` | 返回带属性字符串,不发布 live buffer。 |
|
||||
| `ebox-render-to-buffer` | 复制 built root,挂载 retained TP surface,启用 `ebox-buffer-mode` 并返回 buffer;可选 plist 只接受 `:observer FUNCTION`。 |
|
||||
| `ebox-display-buffer` | 走 retained 路径渲染,删除其他 window 后切换到结果。 |
|
||||
| `ebox-commit` | 原子发布重新构建的 root 或 one-shot logical candidate。 |
|
||||
| `ebox-render` | 接收 single-root `CanonicalEboxInput`,返回带属性字符串,不发布 live buffer。 |
|
||||
| `ebox-render-to-buffer` | 接收 single-root `CanonicalEboxInput`,挂载 retained TP surface,启用 `ebox-buffer-mode` 并返回 buffer;可选 plist 只接受 `:observer FUNCTION`。 |
|
||||
| `ebox-display-buffer` | 接收 single-root `CanonicalEboxInput`,走 retained 路径渲染,删除其他 window 后切换到结果。 |
|
||||
| `ebox-commit` | 原子发布 single-root `CanonicalEboxInput` 或 `ebox-candidate-begin` 返回的 one-shot logical candidate。 |
|
||||
| `ebox-buffer-set-observer` | 给已经 mounted 的 buffer 设置或移除一个 function observer。 |
|
||||
| `ebox-buffer-update-report` | 返回最近一次成功更新报告的防御性副本。 |
|
||||
| `ebox-rerender-buffer-with-context` | 应用显式 viewport width 与可选 height,同时保留 identity。 |
|
||||
@ -142,19 +202,23 @@ observer 为 nil 时,Ebox 不创建 observation context,不取 timestamp/GC
|
||||
| `ebox-region-ids` | 按文档顺序返回当前底层 region id。 |
|
||||
| `ebox-region-resolve` | 把 mounted buffer 中一个逻辑 `:id` 解析为不透明、surface-scoped 的 handle。 |
|
||||
| `ebox-region-update` | 通过一次 retained transaction 应用支持的可变样式或滚动 property。 |
|
||||
| `ebox-child-range` | 从 typed node 构造透明、可寻址的 child Range descriptor。 |
|
||||
| `ebox-child-range` | 仅供框架集成:在组装 typed canonical input 时构造非视觉、可寻址的 child Range descriptor。 |
|
||||
| `ebox-candidate-begin` | 在 one-shot candidate 中捕获当前准确 mounted generation。 |
|
||||
| `ebox-candidate-replace` | 替换一个 candidate node address。 |
|
||||
| `ebox-candidate-replace-range-ref` | 替换一个透明 child Range payload。 |
|
||||
| `ebox-candidate-replace-root` | 替换 candidate 的私有 root address。 |
|
||||
| `ebox-candidate-replace-host-ref` | 替换一个 framework-owned opaque host reference。 |
|
||||
| `ebox-candidate-patch-host-paint` | 记录经过证明的 paint-only host patch;需要 replacement 时返回 nil。 |
|
||||
| `ebox-candidate-replace` | 用 single-root `CanonicalEboxInput` 替换一个 candidate node address。 |
|
||||
| `ebox-candidate-replace-range-ref` | 用 forest-valued `CanonicalEboxInput` 替换一个透明 child Range payload。 |
|
||||
| `ebox-candidate-replace-root` | 用 single-root `CanonicalEboxInput` 替换 candidate 的私有 root address。 |
|
||||
| `ebox-candidate-replace-host-ref` | 用 single-root `CanonicalEboxInput` 替换一个 framework-owned opaque host reference。 |
|
||||
| `ebox-candidate-patch-host-paint` | 比较 previous/next single-root canonical input;记录经过证明的 paint-only host patch,需要 replacement 时返回 nil。 |
|
||||
| `ebox-host-ref-bounds` | 返回 opaque host reference 当前不含 margin 的 bounds。 |
|
||||
| `ebox-host-ref-position` | 返回其当前第一个位置。 |
|
||||
|
||||
handle 和位置只属于一个 live publication generation;对象被删除后要重新解析。
|
||||
candidate 被 commit 封存后不能复用。
|
||||
|
||||
author DSL 没有 Range form。普通 author 只组合直接嵌套的 Text/Box child,不调用
|
||||
`ebox-child-range`;Range address 只服务已经拥有 semantic child range 与 candidate
|
||||
update 的框架。
|
||||
|
||||
## 5. Selector
|
||||
|
||||
ECSS 是唯一 selector parser 与 matcher;Ebox 只提供 logical tree relation 和
|
||||
@ -198,8 +262,7 @@ native 模块只是加速器,不是正确性依赖。加载包不会构建它
|
||||
|
||||
- 构造:`ebox-build`、`ebox-text-create`、`ebox-normal-layout-create`、
|
||||
`ebox-row-layout-create`、`ebox-column-layout-create`、
|
||||
`ebox-flex-layout-create`、`ebox-grid-layout-create`、`ebox-box-create`、
|
||||
`ebox-child-range`;
|
||||
`ebox-flex-layout-create`、`ebox-grid-layout-create`、`ebox-box-create`;
|
||||
- 渲染/发布:`ebox-render`、`ebox-render-to-buffer`、`ebox-display-buffer`、
|
||||
`ebox-commit`、`ebox-buffer-set-observer`、`ebox-buffer-update-report`、
|
||||
`ebox-rerender-buffer-with-context`、`ebox-viewport-window-width`、
|
||||
@ -208,6 +271,7 @@ native 模块只是加速器,不是正确性依赖。加载包不会构建它
|
||||
- Candidate/identity:`ebox-candidate-begin`、`ebox-candidate-replace`、
|
||||
`ebox-candidate-replace-range-ref`、`ebox-candidate-replace-root`、
|
||||
`ebox-candidate-replace-host-ref`、`ebox-candidate-patch-host-paint`、
|
||||
`ebox-child-range`、
|
||||
`ebox-region-ids`、`ebox-region-resolve`、`ebox-region-update`、
|
||||
`ebox-host-ref-bounds`、`ebox-host-ref-position`;
|
||||
- Selector:`ebox-selector-parse`、`ebox-selector-match-node-p`、
|
||||
|
||||
@ -35,7 +35,7 @@ Children are always nested directly. A layout form is a Box with a selected
|
||||
child-layout algorithm, not a different kind of visual object.
|
||||
|
||||
```elisp
|
||||
(defvar ebox-guide-root
|
||||
(defvar ebox-guide-input
|
||||
(ebox-build
|
||||
'(column :padding (1 2)
|
||||
:border ((1) solid "#8A93A6")
|
||||
@ -45,6 +45,11 @@ child-layout algorithm, not a different kind of visual object.
|
||||
(box :background-color "#EEF2FF" "Archive")))))
|
||||
```
|
||||
|
||||
`ebox-build` returns an opaque `CanonicalEboxInput`. It keeps the canonical
|
||||
forest and its source generation together. Pass that value unchanged to the
|
||||
render and publication functions; ordinary author code does not extract or
|
||||
reassemble its internal nodes.
|
||||
|
||||
Use `box` with geometry but no child when an empty rectangular area is needed.
|
||||
No extra node type is necessary.
|
||||
|
||||
@ -125,13 +130,15 @@ parent. The child-layout algorithm still comes from the form name.
|
||||
|
||||
`ebox-render` returns propertized text without changing a live buffer.
|
||||
`ebox-render-to-buffer` mounts a retained TP surface and returns its buffer.
|
||||
Both functions, and `ebox-display-buffer`, accept the opaque value returned by
|
||||
`ebox-build`.
|
||||
|
||||
```elisp
|
||||
(ebox-render ebox-guide-root)
|
||||
(ebox-render-to-buffer "*Ebox Guide*" ebox-guide-root)
|
||||
(ebox-render ebox-guide-input)
|
||||
(ebox-render-to-buffer "*Ebox Guide*" ebox-guide-input)
|
||||
```
|
||||
|
||||
Build a fresh root and use `ebox-commit` for an atomic update:
|
||||
Build a fresh canonical input and use `ebox-commit` for an atomic update:
|
||||
|
||||
```elisp
|
||||
(ebox-commit
|
||||
@ -139,7 +146,7 @@ Build a fresh root and use `ebox-commit` for an atomic update:
|
||||
(ebox-build
|
||||
'(column :padding (1 2)
|
||||
(text "Updated notes")
|
||||
(box :key body "The new root is caller-owned."))))
|
||||
(box :key body "The new input is caller-owned."))))
|
||||
```
|
||||
|
||||
Validation, rendering, or publication failure leaves the previous buffer and
|
||||
@ -198,19 +205,61 @@ examples.
|
||||
|
||||
## 9. Typed integration API
|
||||
|
||||
Frameworks that already normalize author input may bypass the list DSL. They
|
||||
construct one TextNode with `ebox-text-create`, one typed LayoutConfig with the
|
||||
matching layout constructor, then one BoxNode with `ebox-box-create`.
|
||||
Frameworks that already normalize author input may bypass the list DSL. This
|
||||
is an integration API, not a second author grammar. One source builder owns
|
||||
the complete source generation. Each TextNode or BoxNode receives an opaque
|
||||
handle from that builder and node-owned facts projected from the same
|
||||
normalized declarations. The framework then seals the builder and transports
|
||||
the forest and source index together as one `CanonicalEboxInput`.
|
||||
|
||||
```elisp
|
||||
(ebox-box-create
|
||||
:layout (ebox-row-layout-create :item-gap 1 :cross-align 'center)
|
||||
:children (list (ebox-text-create :value "Left")
|
||||
(ebox-text-create :value "Right")))
|
||||
(let* ((builder (ebox-source-builder-create))
|
||||
(root-declarations nil)
|
||||
(left-declarations nil)
|
||||
(right-declarations nil)
|
||||
(root-handle
|
||||
(ebox-source-builder-bind
|
||||
builder :declarations root-declarations))
|
||||
(left-handle
|
||||
(ebox-source-builder-bind
|
||||
builder :declarations left-declarations))
|
||||
(right-handle
|
||||
(ebox-source-builder-bind
|
||||
builder :declarations right-declarations))
|
||||
(left
|
||||
(ebox-text-create
|
||||
:value "Left"
|
||||
:source-handle left-handle
|
||||
:owned-facts
|
||||
(ebox-canonical-facts-from-declarations
|
||||
'text left-declarations)))
|
||||
(right
|
||||
(ebox-text-create
|
||||
:value "Right"
|
||||
:source-handle right-handle
|
||||
:owned-facts
|
||||
(ebox-canonical-facts-from-declarations
|
||||
'text right-declarations)))
|
||||
(root
|
||||
(ebox-box-create
|
||||
:layout (ebox-row-layout-create
|
||||
:item-gap 1 :cross-align 'center)
|
||||
:children (list left right)
|
||||
:source-handle root-handle
|
||||
:owned-facts
|
||||
(ebox-canonical-facts-from-declarations
|
||||
'row root-declarations))))
|
||||
(ebox-canonical-input-create
|
||||
(list root)
|
||||
(ebox-source-builder-finish builder)))
|
||||
```
|
||||
|
||||
Here `:layout` and `:children` are fields of the evaluated typed constructor.
|
||||
They are not author properties and do not extend the seven-entry grammar.
|
||||
`ebox-source-builder-create`, `ebox-source-builder-bind`,
|
||||
`ebox-source-builder-finish`, `ebox-canonical-facts-from-declarations`, the
|
||||
typed node/layout constructors, and `ebox-canonical-input-create` belong to
|
||||
this integration boundary. Here `:layout`, `:children`, `:source-handle`, and
|
||||
`:owned-facts` are evaluated constructor fields. They are not author
|
||||
properties and do not extend the seven-entry grammar.
|
||||
|
||||
## 10. Optional native reflow and verification
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@ Ebox 文档由 Text 和 Box 节点组成。author 语法只有七个入口:
|
||||
子节点始终直接嵌套。布局 form 只是选择了子布局算法的 Box,不是另一种视觉对象。
|
||||
|
||||
```elisp
|
||||
(defvar ebox-guide-root
|
||||
(defvar ebox-guide-input
|
||||
(ebox-build
|
||||
'(column :padding (1 2)
|
||||
:border ((1) solid "#8A93A6")
|
||||
@ -41,6 +41,10 @@ Ebox 文档由 Text 和 Box 节点组成。author 语法只有七个入口:
|
||||
(box :background-color "#EEF2FF" "Archive")))))
|
||||
```
|
||||
|
||||
`ebox-build` 返回不透明的 `CanonicalEboxInput`,把 canonical forest 与对应的 source
|
||||
generation 保持为一个整体。普通 author 代码只需把这个值原样传给渲染和发布函数,
|
||||
不要取出或重新组装其中的内部 node。
|
||||
|
||||
需要空矩形区域时,使用只有几何 property、没有子节点的 `box`,不需要额外节点类型。
|
||||
|
||||
## 3. 用布局名称直接表达意图
|
||||
@ -115,14 +119,15 @@ placement property 同样直接属于子 `box`。
|
||||
## 5. 渲染与发布
|
||||
|
||||
`ebox-render` 返回带属性文本,不修改 live buffer;`ebox-render-to-buffer` 挂载
|
||||
retained TP surface 并返回 buffer。
|
||||
retained TP surface 并返回 buffer。它们和 `ebox-display-buffer` 都接收
|
||||
`ebox-build` 返回的不透明值。
|
||||
|
||||
```elisp
|
||||
(ebox-render ebox-guide-root)
|
||||
(ebox-render-to-buffer "*Ebox Guide*" ebox-guide-root)
|
||||
(ebox-render ebox-guide-input)
|
||||
(ebox-render-to-buffer "*Ebox Guide*" ebox-guide-input)
|
||||
```
|
||||
|
||||
重新构建 root 后,用 `ebox-commit` 原子更新:
|
||||
重新构建 canonical input 后,用 `ebox-commit` 原子更新:
|
||||
|
||||
```elisp
|
||||
(ebox-commit
|
||||
@ -130,7 +135,7 @@ retained TP surface 并返回 buffer。
|
||||
(ebox-build
|
||||
'(column :padding (1 2)
|
||||
(text "Updated notes")
|
||||
(box :key body "The new root is caller-owned."))))
|
||||
(box :key body "The new input is caller-owned."))))
|
||||
```
|
||||
|
||||
验证、渲染或发布失败时,旧 buffer 与 runtime 状态保持不变。
|
||||
@ -184,19 +189,59 @@ retained TP surface 并返回 buffer。
|
||||
|
||||
## 9. Typed 集成 API
|
||||
|
||||
已经完成 author normalization 的框架可以绕过 list DSL:用 `ebox-text-create`
|
||||
构造 TextNode,用对应 layout constructor 构造 typed LayoutConfig,再用
|
||||
`ebox-box-create` 构造 BoxNode。
|
||||
已经完成 author normalization 的框架可以绕过 list DSL;这是集成 API,不是第二套
|
||||
author 语法。一个 source builder 拥有完整 source generation。每个 TextNode 或
|
||||
BoxNode 都取得该 builder 返回的不透明 handle,并使用由同一份 normalized
|
||||
declarations 投影出的 node-owned facts。最后封存 builder,把 forest 与 source index
|
||||
作为一个 `CanonicalEboxInput` 传递。
|
||||
|
||||
```elisp
|
||||
(ebox-box-create
|
||||
:layout (ebox-row-layout-create :item-gap 1 :cross-align 'center)
|
||||
:children (list (ebox-text-create :value "Left")
|
||||
(ebox-text-create :value "Right")))
|
||||
(let* ((builder (ebox-source-builder-create))
|
||||
(root-declarations nil)
|
||||
(left-declarations nil)
|
||||
(right-declarations nil)
|
||||
(root-handle
|
||||
(ebox-source-builder-bind
|
||||
builder :declarations root-declarations))
|
||||
(left-handle
|
||||
(ebox-source-builder-bind
|
||||
builder :declarations left-declarations))
|
||||
(right-handle
|
||||
(ebox-source-builder-bind
|
||||
builder :declarations right-declarations))
|
||||
(left
|
||||
(ebox-text-create
|
||||
:value "Left"
|
||||
:source-handle left-handle
|
||||
:owned-facts
|
||||
(ebox-canonical-facts-from-declarations
|
||||
'text left-declarations)))
|
||||
(right
|
||||
(ebox-text-create
|
||||
:value "Right"
|
||||
:source-handle right-handle
|
||||
:owned-facts
|
||||
(ebox-canonical-facts-from-declarations
|
||||
'text right-declarations)))
|
||||
(root
|
||||
(ebox-box-create
|
||||
:layout (ebox-row-layout-create
|
||||
:item-gap 1 :cross-align 'center)
|
||||
:children (list left right)
|
||||
:source-handle root-handle
|
||||
:owned-facts
|
||||
(ebox-canonical-facts-from-declarations
|
||||
'row root-declarations))))
|
||||
(ebox-canonical-input-create
|
||||
(list root)
|
||||
(ebox-source-builder-finish builder)))
|
||||
```
|
||||
|
||||
这里的 `:layout` 和 `:children` 是 evaluated typed constructor 的字段,不是 author
|
||||
property,也没有扩展七入口语法。
|
||||
`ebox-source-builder-create`、`ebox-source-builder-bind`、
|
||||
`ebox-source-builder-finish`、`ebox-canonical-facts-from-declarations`、typed
|
||||
node/layout constructor 与 `ebox-canonical-input-create` 只属于集成边界。这里的
|
||||
`:layout`、`:children`、`:source-handle` 与 `:owned-facts` 是 evaluated constructor
|
||||
字段,不是 author property,也没有扩展七入口语法。
|
||||
|
||||
## 10. 可选 native reflow 与验证
|
||||
|
||||
|
||||
@ -12,9 +12,11 @@
|
||||
(require 'cl-lib)
|
||||
(require 'subr-x)
|
||||
(require 'ebox-layout-config)
|
||||
(require 'ebox-source)
|
||||
(require 'ebox-style)
|
||||
(require 'ebox-node-factory)
|
||||
(require 'ebox-child-range)
|
||||
(require 'ebox-tree)
|
||||
|
||||
(defun ebox-canonical--validate-plist (plist context)
|
||||
"Validate PLIST shape for error CONTEXT and return PLIST."
|
||||
@ -53,39 +55,191 @@
|
||||
(error "%s accepts %S at most once" context field))
|
||||
(and values (cons t (car values)))))
|
||||
|
||||
(defconst ebox-canonical--source-metadata-fields
|
||||
'(:source-handle :key :class :id)
|
||||
"Source facts accepted by both canonical Text and Box constructors.")
|
||||
(defconst ebox-canonical--source-fields
|
||||
'(:source-handle)
|
||||
"Opaque source input accepted by canonical Text and Box constructors.")
|
||||
|
||||
(defvar ebox-canonical--declaration-fact-cache
|
||||
(make-hash-table :test #'eq :weakness 'key)
|
||||
"Declaration-identity keyed validated canonical node facts.")
|
||||
|
||||
(defun ebox-canonical--source-metadata (plist context)
|
||||
"Return detached source metadata from PLIST for CONTEXT.
|
||||
Every metadata field is optional but may occur at most once."
|
||||
(let (metadata)
|
||||
(dolist (field ebox-canonical--source-metadata-fields)
|
||||
(when-let* ((entry (ebox-canonical--optional-field
|
||||
plist field context)))
|
||||
(setq metadata
|
||||
(append metadata (list field (cdr entry))))))
|
||||
metadata))
|
||||
(defvar ebox-canonical--source-builder nil
|
||||
"Transaction-local source builder available during typed tree assembly.")
|
||||
|
||||
(defun ebox-canonical--apply-source-metadata (node metadata)
|
||||
"Attach canonical source METADATA to runtime NODE and return NODE."
|
||||
(while metadata
|
||||
(let ((field (pop metadata))
|
||||
(value (pop metadata)))
|
||||
(plist-put node
|
||||
(pcase field
|
||||
(:source-handle :ebox-source-handle)
|
||||
(_ field))
|
||||
value)
|
||||
;; `:host-ref' is the private retained-geometry projection of the
|
||||
;; source-owned handle. It is not a second public identity field.
|
||||
(when (and (eq field :source-handle) value)
|
||||
(plist-put node :host-ref value))))
|
||||
(cl-defstruct (ebox-canonical-facts
|
||||
(:constructor ebox-canonical--make-facts))
|
||||
"Already computed node-owned projection of one source declaration set."
|
||||
engine-properties style-required-p specified-property-ids)
|
||||
|
||||
(cl-defstruct (ebox-canonical-input
|
||||
(:constructor ebox-canonical--make-input)
|
||||
(:conc-name ebox-canonical-input--))
|
||||
"Atomic transport of a canonical forest and its source generation."
|
||||
nodes source-index range-anchors)
|
||||
|
||||
(defun ebox-canonical--validate-input-source-ownership (nodes source-index)
|
||||
"Validate that NODES reference exactly SOURCE-INDEX's source generation."
|
||||
(let ((seen-nodes (make-hash-table :test #'eq))
|
||||
(seen-handles (make-hash-table :test #'eq))
|
||||
preorder-handles)
|
||||
(cl-labels
|
||||
((visit
|
||||
(node)
|
||||
(unless (or (ebox-text-node-p node) (ebox-box-node-p node))
|
||||
(error "Ebox canonical input contains a noncanonical node: %S" node))
|
||||
(when (gethash node seen-nodes)
|
||||
(error "Ebox canonical input reuses one node object"))
|
||||
(puthash node t seen-nodes)
|
||||
(let ((handle (ebox-node-source-handle node)))
|
||||
(unless (and (ebox-source-handle-p handle)
|
||||
(ebox-source-index-handle-member-p
|
||||
source-index handle))
|
||||
(error
|
||||
"Ebox canonical source is absent from its input generation"))
|
||||
(when (gethash handle seen-handles)
|
||||
(error "Ebox canonical input reuses one source handle"))
|
||||
(puthash handle t seen-handles)
|
||||
(push handle preorder-handles))
|
||||
(when (ebox-box-node-p node)
|
||||
(mapc #'visit (ebox-box-node-children node)))))
|
||||
(mapc #'visit nodes))
|
||||
(setq preorder-handles (nreverse preorder-handles))
|
||||
(unless (= (hash-table-count seen-handles)
|
||||
(ebox-source--table-size
|
||||
(ebox-source--index-handle-records source-index)))
|
||||
(error "Ebox canonical input contains unreferenced source facts"))
|
||||
(ebox-source--with-order source-index preorder-handles)))
|
||||
|
||||
(defun ebox-canonical-input-create (nodes source-index &optional range-anchors)
|
||||
"Return one opaque canonical input for ordered forest NODES.
|
||||
SOURCE-INDEX owns every source fact referenced by NODES. RANGE-ANCHORS are
|
||||
nonvisual structural addresses transported with the same generation."
|
||||
(unless (and (proper-list-p nodes)
|
||||
(cl-every (lambda (node)
|
||||
(and (listp node) (not (stringp node))))
|
||||
nodes))
|
||||
(error "Ebox canonical input nodes must be a proper node forest: %S"
|
||||
nodes))
|
||||
(unless (ebox-source-index-p source-index)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-index-p source-index)))
|
||||
(setq source-index
|
||||
(ebox-canonical--validate-input-source-ownership nodes source-index))
|
||||
(ebox-canonical--make-input
|
||||
:nodes (copy-sequence nodes)
|
||||
:source-index source-index
|
||||
:range-anchors (copy-tree range-anchors)))
|
||||
|
||||
(defun ebox-canonical-input-roots (input)
|
||||
"Return a shallow copy of canonical INPUT's ordered forest roots.
|
||||
This is a read-only structural view; it does not adopt INPUT's source facts
|
||||
into another candidate."
|
||||
(unless (ebox-canonical-input-p input)
|
||||
(signal 'wrong-type-argument (list 'ebox-canonical-input-p input)))
|
||||
(copy-sequence (ebox-canonical-input--nodes input)))
|
||||
|
||||
(defun ebox-canonical-input-import-roots (input roots builder)
|
||||
"Import INPUT facts owned by selected top-level ROOTS into BUILDER.
|
||||
Each ROOT must be an exact, unique member of INPUT's forest. The selected
|
||||
subtrees retain their existing nodes, handles, and immutable records without
|
||||
adopting facts from unselected roots. Return a shallow copy of ROOTS."
|
||||
(unless (ebox-canonical-input-p input)
|
||||
(signal 'wrong-type-argument (list 'ebox-canonical-input-p input)))
|
||||
(ebox-source--builder-assert-open builder)
|
||||
(unless (proper-list-p roots)
|
||||
(error "Ebox canonical selected roots must be a proper list: %S" roots))
|
||||
(let ((available (make-hash-table :test #'eq))
|
||||
(selected (make-hash-table :test #'eq))
|
||||
(source-index (ebox-canonical-input--source-index input)))
|
||||
(dolist (root (ebox-canonical-input--nodes input))
|
||||
(puthash root t available))
|
||||
(cl-labels
|
||||
((import-node
|
||||
(node)
|
||||
(let* ((handle (ebox-node-source-handle node))
|
||||
(record (ebox-source--index-record source-index handle)))
|
||||
(unless record
|
||||
(error "Ebox canonical selected root has no source facts"))
|
||||
(ebox-source--builder-add-record builder handle record))
|
||||
(when (ebox-box-node-p node)
|
||||
(mapc #'import-node (ebox-box-node-children node)))))
|
||||
(dolist (root roots)
|
||||
(unless (gethash root available)
|
||||
(error "Ebox canonical selected root is outside its input"))
|
||||
(when (gethash root selected)
|
||||
(error "Ebox canonical selected root occurs more than once"))
|
||||
(puthash root t selected)
|
||||
(import-node root))))
|
||||
(copy-sequence roots))
|
||||
|
||||
(defun ebox-canonical-input--single-root (input context)
|
||||
"Return INPUT's only root node, or signal for error CONTEXT."
|
||||
(unless (ebox-canonical-input-p input)
|
||||
(signal 'wrong-type-argument (list 'ebox-canonical-input-p input)))
|
||||
(let ((nodes (ebox-canonical-input--nodes input)))
|
||||
(unless (= (length nodes) 1)
|
||||
(error "%s requires exactly one canonical root" context))
|
||||
(car nodes)))
|
||||
|
||||
(defun ebox-canonical-input-root-host-ref (input)
|
||||
"Return INPUT's detached single-root publication reference.
|
||||
This framework-integration port keeps `ebox-source-handle' opaque: Ebox first
|
||||
validates that the canonical root's exact handle belongs to INPUT's immutable
|
||||
source generation, then returns only the author-owned publication reference."
|
||||
(let* ((root (ebox-canonical-input--single-root
|
||||
input "ebox-canonical-input-root-host-ref"))
|
||||
(handle (plist-get root :ebox-source-handle))
|
||||
(source-index (ebox-canonical-input--source-index input)))
|
||||
(unless (and (ebox-source-handle-p handle)
|
||||
(ebox-source-index-handle-member-p source-index handle))
|
||||
(error "Ebox canonical root source is absent from its input generation"))
|
||||
(ebox-source-handle-id handle)))
|
||||
|
||||
(defun ebox-canonical-input-equal-p (left right)
|
||||
"Return non-nil when canonical inputs LEFT and RIGHT are exactly equivalent.
|
||||
The comparison includes the ordered forest, transparent Range anchors, source
|
||||
identity order, and every immutable author fact. Derived selector/runtime
|
||||
indexes are deliberately excluded because they are projections of these
|
||||
canonical inputs, not additional source authority."
|
||||
(and (ebox-canonical-input-p left)
|
||||
(ebox-canonical-input-p right)
|
||||
(equal-including-properties
|
||||
(ebox-canonical-input--nodes left)
|
||||
(ebox-canonical-input--nodes right))
|
||||
(equal-including-properties
|
||||
(ebox-canonical-input--range-anchors left)
|
||||
(ebox-canonical-input--range-anchors right))
|
||||
(let ((left-handles
|
||||
(ebox-source-index-handles
|
||||
(ebox-canonical-input--source-index left)))
|
||||
(right-handles
|
||||
(ebox-source-index-handles
|
||||
(ebox-canonical-input--source-index right)))
|
||||
(left-index (ebox-canonical-input--source-index left))
|
||||
(right-index (ebox-canonical-input--source-index right)))
|
||||
(and (= (length left-handles) (length right-handles))
|
||||
(cl-every
|
||||
(lambda (left-handle right-handle)
|
||||
(and
|
||||
(equal-including-properties
|
||||
(ebox-source--handle-id-view left-handle)
|
||||
(ebox-source--handle-id-view right-handle))
|
||||
(equal-including-properties
|
||||
(ebox-source--index-record left-index left-handle)
|
||||
(ebox-source--index-record right-index right-handle))))
|
||||
left-handles right-handles)))))
|
||||
|
||||
(defun ebox-canonical--source-handle (plist context)
|
||||
"Return the one opaque source handle from PLIST for CONTEXT."
|
||||
(let ((handle (ebox-canonical--required-field
|
||||
plist :source-handle context)))
|
||||
(unless (ebox-source-handle-p handle)
|
||||
(error "%s :source-handle must be opaque Ebox source: %S"
|
||||
context handle))
|
||||
handle))
|
||||
|
||||
(defun ebox-canonical--apply-source-handle (node handle)
|
||||
"Attach opaque source HANDLE to runtime NODE and return NODE."
|
||||
(plist-put node :ebox-source-handle handle)
|
||||
node)
|
||||
|
||||
(defun ebox-canonical--text-measurement-property-p (property)
|
||||
@ -171,6 +325,25 @@ and author TAG."
|
||||
'(paint typography))))))
|
||||
nil))))
|
||||
|
||||
(defun ebox-canonical-facts-from-declarations (tag declarations)
|
||||
"Return immutable node-owned facts for TAG from DECLARATIONS.
|
||||
Source declarations stay behind the source handle; the returned value contains
|
||||
only the current canonical-node projection needed before G4c separates paint."
|
||||
(let ((predicate
|
||||
(if (eq tag 'text)
|
||||
#'ebox-canonical--text-measurement-property-p
|
||||
#'ebox-canonical--box-frame-property-p)))
|
||||
(pcase-let ((`(,canonical ,engine ,style-required-p)
|
||||
(ebox-canonical--node-properties
|
||||
nil (and declarations (cons t declarations))
|
||||
predicate
|
||||
(if (eq tag 'text) "Ebox Text" "Ebox Box") tag)))
|
||||
(ebox-canonical--make-facts
|
||||
:engine-properties engine
|
||||
:style-required-p style-required-p
|
||||
:specified-property-ids
|
||||
(cl-loop for (id _value) on canonical by #'cddr collect id)))))
|
||||
|
||||
(defun ebox-node-kind (node)
|
||||
"Return canonical NODE kind, or nil for a legacy runtime node."
|
||||
(and (listp node) (plist-get node :ebox-kind)))
|
||||
@ -247,41 +420,57 @@ and author TAG."
|
||||
children))
|
||||
(push (cons nil (list entry)) segments)
|
||||
(push entry material)))
|
||||
(list (nreverse material)
|
||||
(and range-p
|
||||
(ebox-child-range--build (nreverse segments) nil t)))))
|
||||
(setq material (nreverse material)
|
||||
segments (nreverse segments))
|
||||
(when (and range-p (not (ebox-source-builder-p
|
||||
ebox-canonical--source-builder)))
|
||||
(error "Ebox Range children require one canonical source builder"))
|
||||
(let ((source-builder (and range-p ebox-canonical--source-builder)))
|
||||
(list material
|
||||
(and range-p
|
||||
(ebox-child-range--build
|
||||
segments
|
||||
(lambda (node)
|
||||
(when-let* ((record
|
||||
(ebox-source--builder-record
|
||||
source-builder
|
||||
(ebox-node-source-handle node))))
|
||||
(ebox-source-record-key record)))
|
||||
nil t))))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-text-create (&rest plist)
|
||||
"Create a canonical TextNode from evaluated PLIST.
|
||||
|
||||
`:value' is required and must be one string. `:source-handle', `:key',
|
||||
`:class', and `:id' are optional source facts. Direct properties are limited
|
||||
to text measurement. `:declarations' may carry one already-normalized Text
|
||||
author fact set so paint is projected without reparsing."
|
||||
`:value' is required and must be one string. `:source-handle' is required and
|
||||
is the only author source input. Direct properties are limited to already
|
||||
computed Text measurement facts."
|
||||
(ebox-canonical--validate-plist plist "ebox-text-create")
|
||||
(let* ((value (ebox-canonical--required-field
|
||||
plist :value "Ebox Text"))
|
||||
(declarations-field (ebox-canonical--optional-field
|
||||
plist :declarations "Ebox Text"))
|
||||
(source-metadata (ebox-canonical--source-metadata plist "Ebox Text"))
|
||||
(source-handle (ebox-canonical--source-handle plist "Ebox Text"))
|
||||
(facts (ebox-canonical--required-field
|
||||
plist :owned-facts "Ebox Text"))
|
||||
(props (ebox-canonical--without-keys
|
||||
plist (append '(:value :declarations)
|
||||
ebox-canonical--source-metadata-fields))))
|
||||
plist
|
||||
(append '(:value :owned-facts)
|
||||
ebox-canonical--source-fields))))
|
||||
(unless (stringp value)
|
||||
(error "Ebox Text :value must be a string: %S" value))
|
||||
(pcase-let* ((`(,declarations ,engine-props ,style-required-p)
|
||||
(ebox-canonical--node-properties
|
||||
props declarations-field
|
||||
#'ebox-canonical--text-measurement-property-p
|
||||
"Ebox Text" 'text))
|
||||
(node
|
||||
(ebox-node-factory--create-text-expanded
|
||||
(append engine-props (list :content value))
|
||||
declarations)))
|
||||
(when props
|
||||
(error "Ebox Text accepts only normalized owned facts"))
|
||||
(when (not (ebox-canonical-facts-p facts))
|
||||
(error "Ebox Text :owned-facts must be canonical facts: %S" facts))
|
||||
(pcase-let* ((engine-props
|
||||
(ebox-canonical-facts-engine-properties facts))
|
||||
(style-required-p
|
||||
(ebox-canonical-facts-style-required-p facts))
|
||||
(node
|
||||
(ebox-node-factory--create-text-expanded
|
||||
(append engine-props (list :content value)))))
|
||||
(plist-put node :ebox-kind 'text)
|
||||
(plist-put node :ebox-text-value value)
|
||||
(ebox-canonical--apply-source-metadata node source-metadata)
|
||||
(ebox-canonical--apply-source-handle node source-handle)
|
||||
(plist-put node :display '(inline flow))
|
||||
(plist-put node :ebox-author-style-count
|
||||
(if style-required-p 1 0))
|
||||
@ -295,24 +484,24 @@ author fact set so paint is projected without reparsing."
|
||||
|
||||
`:layout' must occur exactly once as an `ebox-layout-config'.
|
||||
`:children' is a list of canonical nodes. Every Layout accepts zero or more
|
||||
children according to its own formatting algorithm. `:declarations' may
|
||||
carry one already-normalized Box author fact set."
|
||||
children according to its own formatting algorithm. `:source-handle' is the
|
||||
only author source input."
|
||||
(ebox-canonical--validate-plist plist "ebox-box-create")
|
||||
(let* ((layout (ebox-canonical--required-field
|
||||
plist :layout "Ebox Box"))
|
||||
(children-field (ebox-canonical--optional-field
|
||||
plist :children "Ebox Box"))
|
||||
(declarations-field (ebox-canonical--optional-field
|
||||
plist :declarations "Ebox Box"))
|
||||
(outer-field (ebox-canonical--optional-field
|
||||
plist :outer "Ebox Box"))
|
||||
(source-metadata (ebox-canonical--source-metadata plist "Ebox Box"))
|
||||
(source-handle (ebox-canonical--source-handle plist "Ebox Box"))
|
||||
(facts (ebox-canonical--required-field
|
||||
plist :owned-facts "Ebox Box"))
|
||||
(children (if children-field (cdr children-field) nil))
|
||||
child-sequence
|
||||
(outer (if outer-field (cdr outer-field) 'block))
|
||||
(props (ebox-canonical--without-keys
|
||||
plist (append '(:layout :children :outer :declarations)
|
||||
ebox-canonical--source-metadata-fields))))
|
||||
plist (append '(:layout :children :outer :owned-facts)
|
||||
ebox-canonical--source-fields))))
|
||||
(unless (ebox-layout-config-p layout)
|
||||
(error "Ebox Box :layout must be a typed layout config: %S"
|
||||
layout))
|
||||
@ -329,22 +518,28 @@ carry one already-normalized Box author fact set."
|
||||
'(normal row column flex grid))
|
||||
(error "Ebox Box Layout is not implemented: %S"
|
||||
(ebox-layout-config-kind layout)))
|
||||
(pcase-let* ((tag (if (eq (ebox-layout-config-kind layout) 'normal)
|
||||
'box
|
||||
(ebox-layout-config-kind layout)))
|
||||
(`(,declarations ,engine-props ,style-required-p)
|
||||
(ebox-canonical--node-properties
|
||||
props declarations-field
|
||||
#'ebox-canonical--box-frame-property-p
|
||||
"Ebox Box" tag))
|
||||
(node (ebox-node-factory--create-expanded
|
||||
engine-props declarations)))
|
||||
(when props
|
||||
(error "Ebox Box accepts only normalized owned facts"))
|
||||
(when (not (ebox-canonical-facts-p facts))
|
||||
(error "Ebox Box :owned-facts must be canonical facts: %S" facts))
|
||||
(pcase-let* ((`(,canonical-declarations ,engine-props ,style-required-p)
|
||||
(list
|
||||
(ebox-canonical-facts-specified-property-ids facts)
|
||||
(ebox-canonical-facts-engine-properties facts)
|
||||
(ebox-canonical-facts-style-required-p facts)))
|
||||
(node (ebox-node-factory--create-expanded engine-props)))
|
||||
(plist-put node :ebox-kind 'box)
|
||||
(plist-put node :ebox-layout-config layout)
|
||||
(plist-put node :children children)
|
||||
(when (plist-member canonical-declarations
|
||||
(ebox-style-schema-id :min-width))
|
||||
(plist-put node :ebox-explicit-min-width-p t))
|
||||
(when (plist-member canonical-declarations
|
||||
(ebox-style-schema-id :min-height))
|
||||
(plist-put node :ebox-explicit-min-height-p t))
|
||||
(when child-sequence
|
||||
(plist-put node :ebox-child-sequence child-sequence))
|
||||
(ebox-canonical--apply-source-metadata node source-metadata)
|
||||
(ebox-canonical--apply-source-handle node source-handle)
|
||||
(plist-put node :display
|
||||
(list outer
|
||||
(pcase (ebox-layout-config-kind layout)
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
|
||||
(cl-defstruct (ebox-child-range--segment
|
||||
(:constructor ebox-child-range--make-segment))
|
||||
ref payload)
|
||||
ref payload keys)
|
||||
|
||||
(cl-defstruct (ebox-child-range--segment-node
|
||||
(:constructor ebox-child-range--make-segment-node))
|
||||
@ -208,12 +208,17 @@ shallow copy owned by the persistent sequence."
|
||||
(ebox-child-range--make-descriptor :ref ref :items items))
|
||||
|
||||
(defun ebox-child-range--build
|
||||
(segments &optional hash-function retain-item-identities-p)
|
||||
(segments key-function &optional hash-function retain-item-identities-p
|
||||
segment-key-vectors)
|
||||
"Build an immutable child sequence from SEGMENTS.
|
||||
Each entry is `(REF . ITEMS)'; nil REF denotes one static child item.
|
||||
HASH-FUNCTION optionally supplies the stable key hash. When
|
||||
KEY-FUNCTION returns each item's source-owned author key, or nil when this
|
||||
sequence is deliberately unkeyed. HASH-FUNCTION optionally supplies the
|
||||
stable key hash. When
|
||||
RETAIN-ITEM-IDENTITIES-P is non-nil, ITEMS are already-owned canonical nodes
|
||||
and the sequence retains those exact objects instead of copying them."
|
||||
and the sequence retains those exact objects instead of copying them.
|
||||
SEGMENT-KEY-VECTORS, when non-nil, supplies the already-derived key vector for
|
||||
each segment so structural copies never recover keys from source metadata."
|
||||
(unless (proper-list-p segments)
|
||||
(signal 'wrong-type-argument (list 'proper-list-p segments)))
|
||||
(let* ((count (length segments))
|
||||
@ -222,7 +227,13 @@ and the sequence retains those exact objects instead of copying them."
|
||||
(metrics (ebox-child-range--make-metrics))
|
||||
root key-root (refs (make-hash-table :test #'equal)) (index 0))
|
||||
(when (zerop count) (error "Ebox child sequence requires a segment"))
|
||||
(dolist (entry segments)
|
||||
(when (and segment-key-vectors
|
||||
(/= (length segment-key-vectors) (length segments)))
|
||||
(error "Ebox child sequence key vectors are misaligned"))
|
||||
(cl-loop for entry in segments
|
||||
for explicit-keys in (or segment-key-vectors
|
||||
(make-list (length segments) nil))
|
||||
do
|
||||
(let* ((ref (car entry))
|
||||
(items (cdr entry)))
|
||||
(unless (proper-list-p items)
|
||||
@ -232,16 +243,27 @@ and the sequence retains those exact objects instead of copying them."
|
||||
(when (and ref (gethash ref refs))
|
||||
(error "Ebox child Range ref is not unique: %S" ref))
|
||||
(when ref (puthash ref index refs))
|
||||
(let ((payload (vconcat
|
||||
(mapcar (lambda (item)
|
||||
(ebox-child-range--item-copy
|
||||
item metrics retain-item-identities-p))
|
||||
items))))
|
||||
(let* ((payload (vconcat
|
||||
(mapcar (lambda (item)
|
||||
(ebox-child-range--item-copy
|
||||
item metrics retain-item-identities-p))
|
||||
items)))
|
||||
(keys
|
||||
(if explicit-keys
|
||||
(copy-sequence explicit-keys)
|
||||
(vconcat
|
||||
(mapcar (lambda (item)
|
||||
(and key-function
|
||||
(funcall key-function item)))
|
||||
items)))))
|
||||
(unless (= (length keys) (length payload))
|
||||
(error "Ebox child sequence keys do not match payload"))
|
||||
(setq root (ebox-child-range--segment-insert
|
||||
root height index
|
||||
(ebox-child-range--make-segment :ref ref :payload payload)))
|
||||
(ebox-child-range--make-segment
|
||||
:ref ref :payload payload :keys keys)))
|
||||
(dotimes (offset (length payload))
|
||||
(let* ((item (aref payload offset)) (key (plist-get item :key)))
|
||||
(let ((key (aref keys offset)))
|
||||
(when key
|
||||
(setq key-root
|
||||
(ebox-child-range--hash-change
|
||||
@ -252,14 +274,17 @@ and the sequence retains those exact objects instead of copying them."
|
||||
:root root :height height :count count :ref-index refs
|
||||
:key-root key-root :hash-function hash-function)))
|
||||
|
||||
(defun ebox-child-range--replace (sequence ref items)
|
||||
(defun ebox-child-range--replace (sequence ref items key-function)
|
||||
"Return `(NEW-SEQUENCE . METRICS)' for SEQUENCE after replacing REF ITEMS."
|
||||
(ebox-child-range--replace-many sequence (list (cons ref items))))
|
||||
(ebox-child-range--replace-many
|
||||
sequence (list (cons ref items)) key-function))
|
||||
|
||||
(defun ebox-child-range--replace-many (sequence replacements &optional reuse-maps)
|
||||
(defun ebox-child-range--replace-many
|
||||
(sequence replacements key-function &optional reuse-maps)
|
||||
"Replace RANGE REPLACEMENTS in SEQUENCE with one two-phase transaction.
|
||||
REUSE-MAPS is an alist from Range ref to proven `(NEW . OLD)' item indexes;
|
||||
those NEW items already name canonical published objects and are not copied."
|
||||
those NEW items already name canonical published objects and are not copied.
|
||||
KEY-FUNCTION returns the source-owned key for every replacement item."
|
||||
(let ((metrics (ebox-child-range--make-metrics))
|
||||
(missing (make-symbol "missing-range-ref"))
|
||||
(seen (make-hash-table :test #'equal)) records)
|
||||
@ -278,13 +303,18 @@ those NEW items already name canonical published objects and are not copied."
|
||||
(when (eq index missing)
|
||||
(error "Ebox child Range ref does not exist: %S" (car entry)))
|
||||
(let ((reuse-map (cdr (assoc (car entry) reuse-maps #'equal))))
|
||||
(push (list (car entry) index
|
||||
(vconcat
|
||||
(cl-loop for item in (cdr entry) for offset from 0
|
||||
collect
|
||||
(if (assq offset reuse-map)
|
||||
item
|
||||
(ebox-child-range--item-copy item metrics)))))
|
||||
(push (list
|
||||
(car entry) index
|
||||
(vconcat
|
||||
(cl-loop for item in (cdr entry) for offset from 0
|
||||
collect
|
||||
(if (assq offset reuse-map)
|
||||
item
|
||||
(ebox-child-range--item-copy item metrics))))
|
||||
(vconcat
|
||||
(mapcar (lambda (item)
|
||||
(and key-function (funcall key-function item)))
|
||||
(cdr entry))))
|
||||
records))))
|
||||
(setq records (nreverse records))
|
||||
(let ((key-root (ebox-child-range--sequence-key-root sequence))
|
||||
@ -300,24 +330,29 @@ those NEW items already name canonical published objects and are not copied."
|
||||
root (ebox-child-range--sequence-height sequence)
|
||||
(nth 1 record)
|
||||
(ebox-child-range--make-segment
|
||||
:ref (car record) :payload (nth 2 record))
|
||||
:ref (car record) :payload (nth 2 record)
|
||||
:keys (nth 3 record))
|
||||
metrics)))
|
||||
(setq root (car result))
|
||||
(list (car record) (nth 1 record) (cdr result)
|
||||
(nth 2 record))))
|
||||
(nth 2 record) (nth 3 record))))
|
||||
records))
|
||||
(dolist (record records)
|
||||
(let ((payload (ebox-child-range--segment-payload (nth 2 record))))
|
||||
(let* ((segment (nth 2 record))
|
||||
(payload (ebox-child-range--segment-payload segment))
|
||||
(keys (ebox-child-range--segment-keys segment)))
|
||||
(dotimes (offset (length payload))
|
||||
(ebox-child-range--metric-add metrics 'old-affected-payload-visits)
|
||||
(when-let* ((key (plist-get (aref payload offset) :key)))
|
||||
(when-let* ((key (aref keys offset)))
|
||||
(setq key-root
|
||||
(ebox-child-range--hash-change
|
||||
key-root key nil (funcall hash-function key) 0 t metrics))))))
|
||||
(dolist (record records)
|
||||
(let ((index (nth 1 record)) (payload (nth 3 record)))
|
||||
(let ((index (nth 1 record))
|
||||
(payload (nth 3 record))
|
||||
(keys (nth 4 record)))
|
||||
(dotimes (offset (length payload))
|
||||
(when-let* ((key (plist-get (aref payload offset) :key)))
|
||||
(when-let* ((key (aref keys offset)))
|
||||
(setq key-root
|
||||
(ebox-child-range--hash-change
|
||||
key-root key (cons index offset)
|
||||
@ -330,16 +365,17 @@ those NEW items already name canonical published objects and are not copied."
|
||||
metrics))))
|
||||
|
||||
(defun ebox-child-range--replace-item-at
|
||||
(sequence segment-index offset old-item new-item)
|
||||
(sequence segment-index offset old-item new-item new-key)
|
||||
"Persistently replace OLD-ITEM by NEW-ITEM in SEQUENCE.
|
||||
SEGMENT-INDEX and OFFSET identify its exact material-child location."
|
||||
SEGMENT-INDEX and OFFSET identify its exact material-child location. NEW-KEY
|
||||
is the replacement's source-owned author key."
|
||||
(let* ((segment (ebox-child-range--segment-at sequence segment-index))
|
||||
(payload (copy-sequence (ebox-child-range--segment-payload segment)))
|
||||
(keys (copy-sequence (ebox-child-range--segment-keys segment)))
|
||||
(key-root (ebox-child-range--sequence-key-root sequence))
|
||||
(hash-function (ebox-child-range--sequence-hash-function sequence))
|
||||
(metrics (ebox-child-range--make-metrics))
|
||||
(old-key (plist-get old-item :key))
|
||||
(new-key (plist-get new-item :key)))
|
||||
(old-key (aref keys offset)))
|
||||
(unless (eq (aref payload offset) old-item)
|
||||
(error "Ebox child sequence location is stale"))
|
||||
(when old-key
|
||||
@ -351,12 +387,14 @@ SEGMENT-INDEX and OFFSET identify its exact material-child location."
|
||||
key-root new-key (cons segment-index offset)
|
||||
(funcall hash-function new-key) 0 nil metrics)))
|
||||
(aset payload offset new-item)
|
||||
(aset keys offset new-key)
|
||||
(let ((result
|
||||
(ebox-child-range--segment-replace
|
||||
(ebox-child-range--sequence-root sequence)
|
||||
(ebox-child-range--sequence-height sequence) segment-index
|
||||
(ebox-child-range--make-segment
|
||||
:ref (ebox-child-range--segment-ref segment) :payload payload)
|
||||
:ref (ebox-child-range--segment-ref segment)
|
||||
:payload payload :keys keys)
|
||||
metrics)))
|
||||
(ebox-child-range--make-sequence
|
||||
:root (car result) :height (ebox-child-range--sequence-height sequence)
|
||||
|
||||
91
ebox-dsl.el
91
ebox-dsl.el
@ -9,6 +9,7 @@
|
||||
;;; Code:
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'ebox-source)
|
||||
(require 'ebox-style)
|
||||
(require 'ebox-canonical)
|
||||
(require 'ebox-flex)
|
||||
@ -19,10 +20,11 @@
|
||||
(:constructor ebox-dsl--make-properties))
|
||||
"Normalized properties for one canonical Text or Box author source."
|
||||
tag
|
||||
source-metadata
|
||||
source-handle
|
||||
outer
|
||||
layout
|
||||
declarations)
|
||||
declarations
|
||||
owned-facts)
|
||||
|
||||
(defconst ebox-dsl--box-tags
|
||||
'(box row column flex grid)
|
||||
@ -32,10 +34,6 @@
|
||||
'(:key :class :id)
|
||||
"Shared source metadata accepted by Ebox author surfaces.")
|
||||
|
||||
(defun ebox-dsl--source-handle ()
|
||||
"Return one opaque identity for a normalized author node."
|
||||
(make-symbol "ebox-source"))
|
||||
|
||||
(defun ebox-dsl--validate-plist (plist context)
|
||||
"Validate PLIST shape for CONTEXT and return PLIST."
|
||||
(unless (and (proper-list-p plist) (zerop (% (length plist) 2)))
|
||||
@ -70,16 +68,10 @@
|
||||
when (memq key keys)
|
||||
append (list key value)))
|
||||
|
||||
(defun ebox-dsl--source-metadata
|
||||
(properties source-handle context)
|
||||
"Return source metadata from PROPERTIES and SOURCE-HANDLE for CONTEXT."
|
||||
(let ((metadata (list :source-handle source-handle)))
|
||||
(dolist (field ebox-dsl--source-fields)
|
||||
(when-let* ((entry (ebox-dsl--optional-field
|
||||
properties field context)))
|
||||
(setq metadata
|
||||
(append metadata (list field (cdr entry))))))
|
||||
metadata))
|
||||
(defun ebox-dsl--source-field (properties field context)
|
||||
"Return optional source FIELD value from PROPERTIES for CONTEXT."
|
||||
(when-let* ((entry (ebox-dsl--optional-field properties field context)))
|
||||
(cdr entry)))
|
||||
|
||||
(defun ebox-dsl--compile-style (tag properties context)
|
||||
"Compile and validate TAG author PROPERTIES for CONTEXT."
|
||||
@ -110,8 +102,8 @@
|
||||
(error "%s rejected layout config: %s"
|
||||
context (error-message-string err))))))
|
||||
|
||||
(defun ebox-dsl--normalize-properties (tag properties source-handle)
|
||||
"Project evaluated TAG PROPERTIES for opaque SOURCE-HANDLE.
|
||||
(defun ebox-dsl--normalize-properties (tag properties source source-builder)
|
||||
"Project evaluated TAG PROPERTIES for author SOURCE.
|
||||
|
||||
TAG is `text', `box', `row', `column', `flex', or `grid'. The returned
|
||||
`ebox-dsl--properties' contains typed constructor inputs, source metadata,
|
||||
@ -148,14 +140,22 @@ function."
|
||||
tag style-properties context)))
|
||||
(ebox-dsl--make-properties
|
||||
:tag tag
|
||||
:source-metadata
|
||||
(ebox-dsl--source-metadata
|
||||
properties source-handle context)
|
||||
:source-handle
|
||||
(ebox-source-builder-bind
|
||||
source-builder
|
||||
:key (ebox-dsl--source-field properties :key context)
|
||||
:id (ebox-dsl--source-field properties :id context)
|
||||
:class (ebox-dsl--source-field properties :class context)
|
||||
:declarations declarations
|
||||
:provenance (list :adapter 'ebox-dsl :tag tag
|
||||
:source-present-p (not (null source))))
|
||||
:outer outer
|
||||
:layout
|
||||
(ebox-dsl--layout-config
|
||||
tag declarations extensions context)
|
||||
:declarations declarations))))
|
||||
:declarations declarations
|
||||
:owned-facts
|
||||
(ebox-canonical-facts-from-declarations tag declarations)))))
|
||||
|
||||
(defconst ebox-dsl--tags '(text box row column flex grid)
|
||||
"The complete Ebox author tag set.")
|
||||
@ -196,7 +196,7 @@ function."
|
||||
(setq children (append children (list item))))))
|
||||
(cons properties children)))
|
||||
|
||||
(defun ebox-dsl--build-text (_source items)
|
||||
(defun ebox-dsl--build-text (source items source-builder)
|
||||
"Build one canonical TextNode from author ITEMS."
|
||||
(let* ((split (ebox-dsl--split items))
|
||||
(properties (car split))
|
||||
@ -206,53 +206,66 @@ function."
|
||||
(unless (stringp (car payloads))
|
||||
(error "Ebox text payload must be a string: %S" (car payloads)))
|
||||
(let* ((plan (ebox-dsl--normalize-properties
|
||||
'text properties (ebox-dsl--source-handle)))
|
||||
'text properties source source-builder))
|
||||
(node
|
||||
(apply #'ebox-text-create
|
||||
(append (list :value (car payloads))
|
||||
(ebox-dsl--properties-source-metadata plan)
|
||||
(list :declarations
|
||||
(ebox-dsl--properties-declarations plan))))))
|
||||
(ebox-text-create
|
||||
:value (car payloads)
|
||||
:owned-facts (ebox-dsl--properties-owned-facts plan)
|
||||
:source-handle (ebox-dsl--properties-source-handle plan))))
|
||||
node)))
|
||||
|
||||
(defun ebox-dsl--build-box (tag _source items)
|
||||
(defun ebox-dsl--build-box (tag source items source-builder)
|
||||
"Build canonical BoxNode TAG from author ITEMS."
|
||||
(let* ((split (ebox-dsl--split items))
|
||||
(properties (car split))
|
||||
(child-forms (cdr split))
|
||||
(plan (ebox-dsl--normalize-properties
|
||||
tag properties (ebox-dsl--source-handle)))
|
||||
(children (mapcar #'ebox-dsl-build child-forms))
|
||||
tag properties source source-builder))
|
||||
(children
|
||||
(mapcar (lambda (child)
|
||||
(ebox-dsl-build child source-builder))
|
||||
child-forms))
|
||||
(node
|
||||
(apply #'ebox-box-create
|
||||
(append
|
||||
(list :layout (ebox-dsl--properties-layout plan)
|
||||
:outer (ebox-dsl--properties-outer plan)
|
||||
:children children
|
||||
:declarations
|
||||
(ebox-dsl--properties-declarations plan))
|
||||
(ebox-dsl--properties-source-metadata plan)))))
|
||||
:owned-facts (ebox-dsl--properties-owned-facts plan)
|
||||
:source-handle
|
||||
(ebox-dsl--properties-source-handle plan))))))
|
||||
node))
|
||||
|
||||
(defun ebox-dsl-build (dsl)
|
||||
(defun ebox-dsl-build (dsl source-builder)
|
||||
"Build one canonical Ebox node from inert author DSL.
|
||||
|
||||
The complete grammar is String plus `text', `box', `row', `column', `flex',
|
||||
and `grid'. Strings normalize to Text. The five Box forms select a typed
|
||||
LayoutConfig; no form accepts `:content', `:layout', or a raw runtime node."
|
||||
(unless (ebox-source-builder-p source-builder)
|
||||
(signal 'wrong-type-argument
|
||||
(list 'ebox-source-builder-p source-builder)))
|
||||
(cond
|
||||
((stringp dsl)
|
||||
(ebox-dsl--build-text dsl (list dsl)))
|
||||
(ebox-dsl--build-text dsl (list dsl) source-builder))
|
||||
((not (and (consp dsl) (symbolp (car dsl))))
|
||||
(error "Ebox build: invalid author node %S" dsl))
|
||||
((not (memq (car dsl) ebox-dsl--tags))
|
||||
(error "Ebox build: unknown author tag %S" (car dsl)))
|
||||
((eq (car dsl) 'text)
|
||||
(ebox-dsl--build-text
|
||||
dsl (ebox-dsl--unquote-properties (cdr dsl))))
|
||||
dsl (ebox-dsl--unquote-properties (cdr dsl)) source-builder))
|
||||
(t
|
||||
(ebox-dsl--build-box
|
||||
(car dsl) dsl (ebox-dsl--unquote-properties (cdr dsl))))))
|
||||
(car dsl) dsl (ebox-dsl--unquote-properties (cdr dsl))
|
||||
source-builder))))
|
||||
|
||||
(defun ebox-dsl-build-input (dsl)
|
||||
"Compile inert DSL into one atomic canonical Ebox input."
|
||||
(let* ((builder (ebox-source-builder-create))
|
||||
(root (ebox-dsl-build dsl builder))
|
||||
(source-index (ebox-source-builder-finish builder)))
|
||||
(ebox-canonical-input-create (list root) source-index)))
|
||||
|
||||
(provide 'ebox-dsl)
|
||||
|
||||
|
||||
22
ebox-flex.el
22
ebox-flex.el
@ -639,8 +639,10 @@ grapheme."
|
||||
|
||||
(defun ebox--flex-explicit-min-main-p (box axis)
|
||||
"Return non-nil when BOX declares a main-axis minimum explicitly."
|
||||
(let ((property (if (eq axis 'row) 'ebox/min-width 'ebox/min-height)))
|
||||
(plist-member (plist-get box :ebox-style-declarations) property)))
|
||||
(plist-get box
|
||||
(if (eq axis 'row)
|
||||
:ebox-explicit-min-width-p
|
||||
:ebox-explicit-min-height-p)))
|
||||
|
||||
(defun ebox--flex-box-main-constraint (box axis property)
|
||||
"Return BOX's total main-axis size constraint from PROPERTY."
|
||||
@ -746,14 +748,8 @@ grapheme."
|
||||
(- target outer-size))))
|
||||
|
||||
(defun ebox--flex-copy-put-size (copy property value)
|
||||
"Set COPY size PROPERTY to VALUE in engine fields and ECSS declarations."
|
||||
(plist-put copy property value)
|
||||
(plist-put
|
||||
copy :ebox-style-declarations
|
||||
(ebox-style-merge-declarations
|
||||
(plist-get copy :ebox-style-declarations)
|
||||
(ebox-style-compile-declarations
|
||||
(list property (if (eq property :width) (list value) value))))))
|
||||
"Set COPY's layout-computed size PROPERTY to VALUE."
|
||||
(plist-put copy property value))
|
||||
|
||||
(defun ebox--flex-copy-node-for-size (node axis main cross stretch)
|
||||
"Return NODE or a copy with flex-computed MAIN/CROSS size."
|
||||
@ -1142,7 +1138,7 @@ Return a sized render entry containing the rendered string and dimensions."
|
||||
(t
|
||||
(ebox--flex-bind-source-viewport
|
||||
source item-viewport
|
||||
(lambda () (ebox-render (or node source))))))))
|
||||
(lambda () (ebox--render-node (or node source))))))))
|
||||
(ebox--flex-recache-source-boxes source)
|
||||
(when (and (not fragment-entry)
|
||||
main
|
||||
@ -1646,7 +1642,7 @@ metadata and rendering decisions."
|
||||
(plist-put copy :content rendered)
|
||||
(plist-put copy :ebox-content-node nil)
|
||||
(plist-put copy :ebox-content-width-exact-p t)
|
||||
(prog1 (ebox-render copy)
|
||||
(prog1 (ebox--render-node copy)
|
||||
(puthash region-id box ebox--region-box-table)))
|
||||
rendered))
|
||||
|
||||
@ -1891,7 +1887,7 @@ a prefix of the eventual full wrapper render."
|
||||
(plist-put copy :content content)
|
||||
(plist-put copy :ebox-content-node nil)
|
||||
(plist-put copy :ebox-content-width-exact-p t)
|
||||
(prog1 (ebox-render copy)
|
||||
(prog1 (ebox--render-node copy)
|
||||
(puthash region-id box ebox--region-box-table)))
|
||||
content))
|
||||
(lines (ebox-string-lines rendered)))
|
||||
|
||||
@ -773,7 +773,7 @@ size for start/center/end alignment unless it would overflow its track."
|
||||
(plist-put copy :content rendered)
|
||||
(plist-put copy :ebox-content-node nil)
|
||||
(plist-put copy :ebox-content-width-exact-p t)
|
||||
(prog1 (ebox-render copy)
|
||||
(prog1 (ebox--render-node copy)
|
||||
(puthash region-id box ebox--region-box-table)))
|
||||
rendered))
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -64,6 +64,15 @@
|
||||
(defvar ebox--render-runtime-revision nil
|
||||
"Current buffer runtime revision for render-local cache validation.")
|
||||
|
||||
(defvar ebox--render-source-index nil
|
||||
"Read-only source generation for the current private render extent.")
|
||||
|
||||
(defvar ebox--render-source-generations nil
|
||||
"Ordered read-only source generations available to one proof transaction.")
|
||||
|
||||
(defvar ebox--render-source-states nil
|
||||
"Ordered retained runtime states available to one proof transaction.")
|
||||
|
||||
(defvar ebox--region-id-counter)
|
||||
(defvar ebox--runtime-node-id-counter)
|
||||
(defvar ebox--region-box-table)
|
||||
@ -362,7 +371,7 @@ and its formatted content exactly fills the used height."
|
||||
(declare-function ebox-surface--candidate-root "ebox-surface"
|
||||
(source preserve-identities-p))
|
||||
(declare-function ebox-surface--inline-inheritance-required-p "ebox-surface"
|
||||
(root))
|
||||
(root source-index))
|
||||
(declare-function ebox--add-render-face!
|
||||
"ebox-render-context" (string start end face &optional append))
|
||||
|
||||
@ -1387,7 +1396,7 @@ an inherited viewport."
|
||||
:scroll-lookahead
|
||||
ebox--scroll-window-initial-lookahead-lines-override
|
||||
:scroll-disabled ebox--scroll-window-render-disabled)))
|
||||
(ebox-render node))))))
|
||||
(ebox--render-node node))))))
|
||||
|
||||
(defun ebox--resolve-size-content-pixel (box value fallback)
|
||||
"Resolve a horizontal size VALUE to BOX content pixels.
|
||||
@ -1958,12 +1967,40 @@ region/node identities and render side tables for this one materialization."
|
||||
(ebox--render-cache-scroll-state-retained-cost-cache nil))
|
||||
(ebox--render-layout candidate))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-render (node)
|
||||
"Render layout NODE to a multi-line string through a TP materialization.
|
||||
NODE accepts every layout node supported by `ebox--render-layout'. The
|
||||
materialized surface is ephemeral and creates no live buffer state."
|
||||
(let ((rendered
|
||||
(defun ebox--render-node (node &optional base-source-index)
|
||||
"Render private layout NODE with optional BASE-SOURCE-INDEX.
|
||||
NODE is already below the canonical input boundary. Internal layout paths may
|
||||
also pass a pre-rendered string while an active surface owns runtime facts."
|
||||
(let* ((explicit-source-index-p (and base-source-index t))
|
||||
(handle (and (listp node) (ebox-tree-node-source-handle node)))
|
||||
(node-id (and (listp node) (plist-get node :node-id)))
|
||||
(base-source-index
|
||||
(or base-source-index
|
||||
(and node-id
|
||||
(cl-loop
|
||||
for state in ebox--render-source-states
|
||||
for table = (plist-get state :node-table)
|
||||
when (and (hash-table-p table)
|
||||
(eq node (gethash node-id table)))
|
||||
return (plist-get state :source-index)))
|
||||
(and handle
|
||||
(cl-find-if
|
||||
(lambda (index)
|
||||
(and index
|
||||
(ebox-source-index-handle-member-p index handle)))
|
||||
ebox--render-source-generations))
|
||||
ebox--render-source-index))
|
||||
(inherited-source-index-p
|
||||
(and (not explicit-source-index-p) base-source-index))
|
||||
(ebox--render-source-index base-source-index)
|
||||
(source-index
|
||||
(and (not (stringp node))
|
||||
(not ebox--surface-materialization-active)
|
||||
(not ebox--render-runtime-revision)
|
||||
(ebox-tree-source-index
|
||||
node t ebox--render-root-parent-kind base-source-index
|
||||
inherited-source-index-p)))
|
||||
(rendered
|
||||
(if (or ebox--surface-materialization-active
|
||||
ebox--render-runtime-revision)
|
||||
(ebox--render-layout node)
|
||||
@ -1971,7 +2008,8 @@ materialized surface is ephemeral and creates no live buffer state."
|
||||
(if (and (not (stringp node))
|
||||
(not (ebox-style-cascade-active-p))
|
||||
(zerop (ebox-tree-author-style-count node))
|
||||
(not (ebox-surface--inline-inheritance-required-p node)))
|
||||
(not (ebox-surface--inline-inheritance-required-p
|
||||
node source-index)))
|
||||
(let ((ebox--region-id-counter ebox--region-id-counter)
|
||||
(ebox--runtime-node-id-counter ebox--runtime-node-id-counter))
|
||||
(ebox--render-ephemeral-static node))
|
||||
@ -1979,11 +2017,25 @@ materialized surface is ephemeral and creates no live buffer state."
|
||||
(ebox--region-id-counter ebox--region-id-counter)
|
||||
(ebox--runtime-node-id-counter ebox--runtime-node-id-counter))
|
||||
(tp-surface-materialize-string
|
||||
(ebox-surface-producer node nil t)))))))
|
||||
(ebox-surface-producer
|
||||
node nil t
|
||||
(and base-source-index
|
||||
(list :source-base-index base-source-index)))))))))
|
||||
(unless ebox--paint-origin-capture-p
|
||||
(ebox--strip-paint-origins! rendered))
|
||||
rendered))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-render (input)
|
||||
"Render canonical INPUT to a multi-line string through TP.
|
||||
INPUT atomically carries one typed Text/Box root and its source generation.
|
||||
The materialized surface is ephemeral and creates no live buffer state."
|
||||
(unless (ebox-canonical-input-p input)
|
||||
(signal 'wrong-type-argument (list 'ebox-canonical-input-p input)))
|
||||
(ebox--render-node
|
||||
(ebox-canonical-input--single-root input "ebox-render")
|
||||
(ebox-canonical-input--source-index input)))
|
||||
|
||||
(defun ebox--width-pad (string extra-pixels)
|
||||
"Append a display-space of EXTRA-PIXELS to the right of every line in STRING.
|
||||
Used by `ebox--render-stack' to equalise rows to the same total width."
|
||||
@ -2878,7 +2930,7 @@ are always resolved from BOX so no closure can retain a replaced runtime."
|
||||
(plist-put copy :content content)
|
||||
(plist-put copy :ebox-content-node nil)
|
||||
(plist-put copy :ebox-content-width-exact-p content-width-exact-p)
|
||||
(prog1 (ebox-render copy)
|
||||
(prog1 (ebox--render-node copy)
|
||||
(puthash region-id wrapper ebox--region-box-table))))
|
||||
|
||||
(defun ebox--window-render-flat-wrapper-chunk-lines
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
(declare-function ebox--literal-root-pixel-width "ebox" (box))
|
||||
(declare-function ebox-get "ebox" (box key))
|
||||
(declare-function ebox-style-cascade-active-p "ebox-style" ())
|
||||
(declare-function ebox-tree-node-key "ebox-tree" (node))
|
||||
(declare-function ebox-tree-node-key "ebox-tree" (source-index node))
|
||||
(declare-function ebox-tree--children-raw "ebox-tree" (node))
|
||||
(declare-function ebox--node-direct-viewport-width-dependent-p
|
||||
"ebox-incremental" (node))
|
||||
@ -77,8 +77,11 @@
|
||||
(gethash node-id new-parents missing))
|
||||
(eq (plist-get old-node :ebox-type)
|
||||
(plist-get node :ebox-type))
|
||||
(equal (ebox-tree-node-key old-node)
|
||||
(ebox-tree-node-key node)))
|
||||
(equal
|
||||
(ebox-tree-node-key
|
||||
(plist-get old-state :source-index) old-node)
|
||||
(ebox-tree-node-key
|
||||
(plist-get candidate-state :source-index) node)))
|
||||
(setq stable nil))))
|
||||
new-nodes))
|
||||
(when stable
|
||||
@ -144,8 +147,11 @@ type, key, parent, and sibling position are identical."
|
||||
(plist-get new-root :node-id))
|
||||
(eq (plist-get old-root :ebox-type)
|
||||
(plist-get new-root :ebox-type))
|
||||
(equal (ebox-tree-node-key old-root)
|
||||
(ebox-tree-node-key new-root))
|
||||
(equal
|
||||
(ebox-tree-node-key
|
||||
(plist-get old-state :source-index) old-root)
|
||||
(ebox-tree-node-key
|
||||
(plist-get candidate-state :source-index) new-root))
|
||||
(hash-table-p old-nodes) (hash-table-p new-nodes)
|
||||
(hash-table-p old-objects)))
|
||||
result)
|
||||
@ -178,8 +184,13 @@ type, key, parent, and sibling position are identical."
|
||||
(null old-object)
|
||||
(not (eq (plist-get old-node :ebox-type)
|
||||
(plist-get node :ebox-type)))
|
||||
(not (equal (ebox-tree-node-key old-node)
|
||||
(ebox-tree-node-key node)))))
|
||||
(not
|
||||
(equal
|
||||
(ebox-tree-node-key
|
||||
(plist-get old-state :source-index) old-node)
|
||||
(ebox-tree-node-key
|
||||
(plist-get candidate-state :source-index)
|
||||
node)))))
|
||||
(touched-p
|
||||
(or replacement-p
|
||||
(gethash node-id requested)
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
;;; ebox-node-factory.el --- Runtime box node factory -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Commentary:
|
||||
;; Owns the legacy runtime box encoding used by render/layout modules during
|
||||
;; the typed canonical migration. Higher programmatic constructors may lower
|
||||
;; through this factory, but author DSL parsing and buffer publication do not
|
||||
;; belong here.
|
||||
;; Owns private runtime box storage used by render/layout modules. Higher
|
||||
;; programmatic constructors may lower through this factory, but author source
|
||||
;; facts, DSL parsing, and buffer publication do not belong here.
|
||||
|
||||
;;; Code:
|
||||
|
||||
@ -63,8 +62,8 @@ leaving them absent would make the canonical node violate the Box contract.")
|
||||
(plist-put box key value)))
|
||||
box))
|
||||
|
||||
(defun ebox-node-factory--create-expanded (expanded declarations)
|
||||
"Create one runtime box from EXPANDED engine facts and DECLARATIONS."
|
||||
(defun ebox-node-factory--create-expanded (expanded)
|
||||
"Create one runtime box from EXPANDED engine facts."
|
||||
(let ((controlled-scroll-offset-p
|
||||
(plist-member expanded :scroll-offset)))
|
||||
(let ((box (copy-sequence ebox-node-factory--box-longhand)))
|
||||
@ -72,32 +71,23 @@ leaving them absent would make the canonical node violate the Box contract.")
|
||||
do (plist-put box property value))
|
||||
(plist-put box :ebox-type 'box)
|
||||
(plist-put box :display '(block flow))
|
||||
(plist-put box :ebox-style-declarations (copy-sequence declarations))
|
||||
(when controlled-scroll-offset-p
|
||||
(plist-put box :ebox-scroll-offset-controlled-p t))
|
||||
box)))
|
||||
|
||||
(defun ebox-node-factory--create-text-expanded (expanded declarations)
|
||||
"Create canonical Text storage from EXPANDED facts and DECLARATIONS."
|
||||
(defun ebox-node-factory--create-text-expanded (expanded)
|
||||
"Create canonical Text storage from EXPANDED facts."
|
||||
(let ((node (copy-sequence ebox-node-factory--text-longhand)))
|
||||
(cl-loop for (property value) on expanded by #'cddr
|
||||
do (plist-put node property value))
|
||||
(plist-put node :ebox-type 'box)
|
||||
(plist-put node :display '(inline flow))
|
||||
(plist-put node :ebox-style-declarations (copy-sequence declarations))
|
||||
node))
|
||||
|
||||
(defun ebox-node-factory--create (plist declarations)
|
||||
"Create one runtime box from normalized PLIST and DECLARATIONS.
|
||||
PLIST contains already-validated public engine inputs; DECLARATIONS are the
|
||||
single canonical author fact set retained for style projection."
|
||||
(ebox-node-factory--create-expanded
|
||||
(ebox-style-expand-ebox-plist plist) declarations))
|
||||
|
||||
(defun ebox-node-factory--create-from-properties (&rest plist)
|
||||
"Create one private runtime box from evaluated engine PLIST."
|
||||
(ebox-node-factory--create
|
||||
plist (ebox-style-compile-declarations plist)))
|
||||
(ebox-node-factory--create-expanded
|
||||
(ebox-style-expand-ebox-plist plist)))
|
||||
|
||||
(provide 'ebox-node-factory)
|
||||
|
||||
|
||||
@ -115,10 +115,23 @@
|
||||
(ebox--ensure-region-id box)))
|
||||
(_ nil)))
|
||||
|
||||
(defun ebox-selector--root-and-index (value context)
|
||||
"Return `(ROOT . SOURCE-INDEX)' from canonical input VALUE for CONTEXT."
|
||||
(if (ebox-canonical-input-p value)
|
||||
(cons (ebox-canonical-input--single-root value context)
|
||||
(ebox-canonical-input--source-index value))
|
||||
(error "%s requires one canonical Ebox input" context)))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-selector-match-node-p (node selector)
|
||||
"Return non-nil when NODE matches structured ECSS SELECTOR."
|
||||
(ecss-selector-match-p selector (ebox-tree-node-subject node)))
|
||||
(pcase-let* ((`(,root . ,source-index)
|
||||
(ebox-selector--root-and-index
|
||||
node "ebox-selector-match-node-p"))
|
||||
(source-index
|
||||
(ebox-tree-source-index root nil nil source-index)))
|
||||
(ecss-selector-match-p
|
||||
selector (ebox-tree-node-subject source-index root))))
|
||||
|
||||
(defun ebox-selector--match-handle (node path selector)
|
||||
"Return a selector match handle for NODE on PATH."
|
||||
@ -128,12 +141,12 @@
|
||||
:path path
|
||||
:selector selector))
|
||||
|
||||
(defun ebox-selector--matching-subject-entries (root ast)
|
||||
"Return semantic ROOT entries whose ECSS subjects match AST."
|
||||
(defun ebox-selector--matching-subject-entries (root ast &optional source-index)
|
||||
"Return semantic ROOT entries in SOURCE-INDEX whose subjects match AST."
|
||||
(cl-remove-if-not
|
||||
(lambda (entry)
|
||||
(ecss-selector-match-p ast (plist-get entry :subject)))
|
||||
(plist-get (ebox-tree-subject-index root) :entries)))
|
||||
(plist-get (ebox-tree-subject-index root source-index) :entries)))
|
||||
|
||||
(defun ebox-selector--subject-entry-handle (entry selector)
|
||||
"Return a match handle for semantic subject ENTRY and SELECTOR."
|
||||
@ -143,10 +156,16 @@
|
||||
;;;###autoload
|
||||
(defun ebox-selector-query-all (root selector)
|
||||
"Return document-ordered selector match handles under ROOT."
|
||||
(let ((ast (ebox-selector-parse selector)))
|
||||
(pcase-let* ((`(,root . ,source-index)
|
||||
(ebox-selector--root-and-index
|
||||
root "ebox-selector-query-all"))
|
||||
(source-index
|
||||
(ebox-tree-source-index root nil nil source-index))
|
||||
(ast (ebox-selector-parse selector)))
|
||||
(mapcar (lambda (entry)
|
||||
(ebox-selector--subject-entry-handle entry selector))
|
||||
(ebox-selector--matching-subject-entries root ast))))
|
||||
(ebox-selector--matching-subject-entries
|
||||
root ast source-index))))
|
||||
|
||||
(defun ebox-selector--resolve-buffer (buffer)
|
||||
"Return live buffer object for BUFFER, or signal a user error."
|
||||
@ -163,11 +182,13 @@
|
||||
(and region-id
|
||||
(gethash region-id
|
||||
(plist-get state :region-surface-object-table))))
|
||||
(source-index (plist-get state :source-index))
|
||||
(region-handle
|
||||
(and object
|
||||
(ebox-selector--live-region-handle
|
||||
buffer object
|
||||
(ebox-tree-node-id (plist-get handle :node))))))
|
||||
(ebox-tree-node-id
|
||||
source-index (plist-get handle :node))))))
|
||||
(append handle
|
||||
(list :buffer buffer :region-handle region-handle))))
|
||||
|
||||
@ -234,12 +255,12 @@
|
||||
(when (or id classes type)
|
||||
(cons t (ebox-selector--shortest-candidates candidates)))))
|
||||
|
||||
(defun ebox-selector--indexed-entry-matches-p (entry ast)
|
||||
"Return non-nil when indexed ENTRY matches ECSS selector AST."
|
||||
(defun ebox-selector--indexed-entry-matches-p (source-index entry ast)
|
||||
"Return non-nil when SOURCE-INDEX ENTRY matches ECSS selector AST."
|
||||
(let ((subject
|
||||
(if (ebox-selector--relational-p ast)
|
||||
(ebox-tree-subject-for-path (cdr entry))
|
||||
(ebox-tree-node-subject (car entry)))))
|
||||
(ebox-tree-subject-for-path source-index (cdr entry))
|
||||
(ebox-tree-node-subject source-index (car entry)))))
|
||||
(ecss-selector-match-p ast subject)))
|
||||
|
||||
(defun ebox-selector--entries-to-buffer-handles (entries selector buffer)
|
||||
@ -255,21 +276,25 @@
|
||||
"Return `(SUPPORTED . HANDLES)' for an indexed AST query in BUFFER."
|
||||
(when (ebox-selector--descendant-indexable-p ast)
|
||||
(when-let* ((indexed (ebox-selector--indexed-candidates buffer ast)))
|
||||
(cons
|
||||
t
|
||||
(ebox-selector--entries-to-buffer-handles
|
||||
(cl-remove-if-not
|
||||
(lambda (entry) (ebox-selector--indexed-entry-matches-p entry ast))
|
||||
(cdr indexed))
|
||||
selector buffer)))))
|
||||
(let ((source-index (ebox--buffer-source-index buffer)))
|
||||
(cons
|
||||
t
|
||||
(ebox-selector--entries-to-buffer-handles
|
||||
(cl-remove-if-not
|
||||
(lambda (entry)
|
||||
(ebox-selector--indexed-entry-matches-p
|
||||
source-index entry ast))
|
||||
(cdr indexed))
|
||||
selector buffer))))))
|
||||
|
||||
(defun ebox-selector--query-buffer-tree (root selector ast buffer)
|
||||
"Return full semantic tree matches for ROOT, SELECTOR, AST, and BUFFER."
|
||||
(defun ebox-selector--query-buffer-tree
|
||||
(root source-index selector ast buffer)
|
||||
"Return semantic ROOT matches from SOURCE-INDEX for SELECTOR in BUFFER."
|
||||
(mapcar
|
||||
(lambda (entry)
|
||||
(ebox-selector--handle-with-buffer
|
||||
(ebox-selector--subject-entry-handle entry selector) buffer))
|
||||
(ebox-selector--matching-subject-entries root ast)))
|
||||
(ebox-selector--matching-subject-entries root ast source-index)))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-selector-query-buffer (buffer selector)
|
||||
@ -278,7 +303,8 @@ Selector lookup reads the stored runtime root, not rendered buffer text
|
||||
properties. Each returned handle includes `:buffer' in addition to the
|
||||
tree-query handle fields."
|
||||
(let* ((resolved-buffer (ebox-selector--resolve-buffer buffer))
|
||||
(root (ebox--buffer-root-node resolved-buffer)))
|
||||
(root (ebox--buffer-root-node resolved-buffer))
|
||||
(source-index (ebox--buffer-source-index resolved-buffer)))
|
||||
(unless root
|
||||
(user-error "ebox-selector: buffer has no Ebox runtime state: %S"
|
||||
resolved-buffer))
|
||||
@ -288,7 +314,7 @@ tree-query handle fields."
|
||||
(if indexed
|
||||
(cdr indexed)
|
||||
(ebox-selector--query-buffer-tree
|
||||
root selector ast resolved-buffer)))))
|
||||
root source-index selector ast resolved-buffer)))))
|
||||
|
||||
(defun ebox-selector--skip-handle (handle reason)
|
||||
"Return a structured skip entry for HANDLE and REASON."
|
||||
|
||||
769
ebox-source.el
Normal file
769
ebox-source.el
Normal file
@ -0,0 +1,769 @@
|
||||
;;; ebox-source.el --- Immutable author source facts -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Commentary:
|
||||
;; Owns opaque author source handles, immutable source records, and candidate
|
||||
;; source indexes. It does not own Ebox nodes, tree traversal, selector
|
||||
;; matching, layout, paint, buffers, or publication.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'subr-x)
|
||||
|
||||
(cl-defstruct (ebox-source-record
|
||||
(:constructor ebox-source--make-record))
|
||||
"Immutable facts supplied by one author source."
|
||||
key id classes attributes states declarations provenance)
|
||||
|
||||
(cl-defstruct (ebox-source-handle
|
||||
(:constructor ebox-source--make-handle)
|
||||
(:conc-name ebox-source--handle-))
|
||||
"Opaque identity for one author source."
|
||||
identity)
|
||||
|
||||
(cl-defstruct (ebox-source-index
|
||||
(:constructor ebox-source--make-index)
|
||||
(:conc-name ebox-source--index-))
|
||||
"Immutable candidate-local mapping from source handles to records."
|
||||
records handle-records order subjects node-subjects entries root-subject
|
||||
selector-id-table selector-class-table selector-type-table host-ref-table
|
||||
derived-stale-p)
|
||||
|
||||
(cl-defstruct (ebox-source-builder
|
||||
(:constructor ebox-source--make-builder)
|
||||
(:conc-name ebox-source--builder-))
|
||||
"Private mutable source assembly sealed into one immutable source index."
|
||||
records handle-records order sealed-p)
|
||||
|
||||
(cl-defstruct (ebox-source-table
|
||||
(:constructor ebox-source--make-table))
|
||||
"Persistent immutable table overlay with a derived lookup memo."
|
||||
base added removed count test depth memo)
|
||||
|
||||
(cl-defstruct (ebox-source-order
|
||||
(:constructor ebox-source--make-order))
|
||||
"Bounded persistent document-order handle delta."
|
||||
base replacements removed appended local-handles handles depth)
|
||||
|
||||
(defconst ebox-source--persistent-max-depth 32
|
||||
"Maximum SourceIndex overlay depth before exact compaction.")
|
||||
|
||||
(defconst ebox-source--memo-missing (make-symbol "ebox-source-memo-missing")
|
||||
"Private marker for a memoized absent persistent-table key.")
|
||||
|
||||
(defconst ebox-source--memo-unset (make-symbol "ebox-source-memo-unset")
|
||||
"Private marker for a persistent-table key not present in its memo.")
|
||||
|
||||
(defun ebox-source--flat-table (hash test)
|
||||
"Seal HASH as a flat immutable source table using TEST."
|
||||
(ebox-source--make-table
|
||||
:base nil :added hash :removed (make-hash-table :test test)
|
||||
:count (hash-table-count hash) :test test :depth 0 :memo nil))
|
||||
|
||||
(defun ebox-source--table-seal (table test)
|
||||
"Return TABLE as an immutable persistent table using TEST."
|
||||
(cond
|
||||
((null table) nil)
|
||||
((ebox-source-table-p table) table)
|
||||
((hash-table-p table) (ebox-source--flat-table table test))
|
||||
(t (signal 'wrong-type-argument (list 'hash-table-p table)))))
|
||||
|
||||
(defun ebox-source--table-value (table key)
|
||||
"Return KEY's value from persistent or flat TABLE, or nil."
|
||||
(cond
|
||||
((ebox-source-table-p table)
|
||||
(let ((missing (make-symbol "ebox-source-table-missing")))
|
||||
(let ((value (ebox-source--table-get table key missing)))
|
||||
(unless (eq value missing) value))))
|
||||
((hash-table-p table) (gethash key table))
|
||||
((null table) nil)
|
||||
(t (signal 'wrong-type-argument (list 'hash-table-p table)))))
|
||||
|
||||
(defun ebox-source--table-size (table)
|
||||
"Return the number of bindings retained by TABLE."
|
||||
(cond
|
||||
((ebox-source-table-p table) (ebox-source-table-count table))
|
||||
((hash-table-p table) (hash-table-count table))
|
||||
((null table) 0)
|
||||
(t (signal 'wrong-type-argument (list 'hash-table-p table)))))
|
||||
|
||||
(defun ebox-source--table-get (table key missing)
|
||||
"Return KEY from persistent TABLE, or MISSING."
|
||||
(let* ((memo (ebox-source-table-memo table))
|
||||
(memoized
|
||||
(and memo (gethash key memo ebox-source--memo-unset))))
|
||||
(if (and memo (not (eq memoized ebox-source--memo-unset)))
|
||||
(if (eq memoized ebox-source--memo-missing) missing memoized)
|
||||
(let* ((local-missing (make-symbol "ebox-source-local-missing"))
|
||||
(local
|
||||
(gethash key (ebox-source-table-added table) local-missing))
|
||||
(resolved
|
||||
(cond
|
||||
((not (eq local local-missing)) local)
|
||||
((gethash key (ebox-source-table-removed table))
|
||||
ebox-source--memo-missing)
|
||||
((ebox-source-table-base table)
|
||||
(ebox-source--table-get
|
||||
(ebox-source-table-base table)
|
||||
key ebox-source--memo-missing))
|
||||
(t ebox-source--memo-missing))))
|
||||
;; Facts remain immutable. The memo only path-compresses a lookup whose
|
||||
;; answer is fixed by this immutable overlay and its immutable base.
|
||||
(when memo (puthash key resolved memo))
|
||||
(if (eq resolved ebox-source--memo-missing) missing resolved)))))
|
||||
|
||||
(defun ebox-source--table-materialize (table)
|
||||
"Return a fresh flat hash table with persistent TABLE's exact bindings."
|
||||
(let ((result
|
||||
(if (ebox-source-table-base table)
|
||||
(ebox-source--table-materialize (ebox-source-table-base table))
|
||||
(make-hash-table :test (ebox-source-table-test table)))))
|
||||
(maphash (lambda (key _value) (remhash key result))
|
||||
(ebox-source-table-removed table))
|
||||
(maphash (lambda (key value) (puthash key value result))
|
||||
(ebox-source-table-added table))
|
||||
result))
|
||||
|
||||
(defun ebox-source--table-member-p (table key)
|
||||
"Return non-nil when persistent TABLE contains KEY."
|
||||
(let ((missing (make-symbol "ebox-source-missing")))
|
||||
(not (eq (ebox-source--table-get table key missing) missing))))
|
||||
|
||||
(defun ebox-source--table-overlay (base added removed count)
|
||||
"Return bounded persistent BASE overlaid by ADDED and REMOVED with COUNT."
|
||||
(let ((depth (1+ (ebox-source-table-depth base))))
|
||||
(if (< depth ebox-source--persistent-max-depth)
|
||||
(ebox-source--make-table
|
||||
:base base :added added :removed removed :count count
|
||||
:test (ebox-source-table-test base) :depth depth
|
||||
:memo (make-hash-table :test (ebox-source-table-test base)))
|
||||
(let ((flat (ebox-source--table-materialize base)))
|
||||
(maphash (lambda (key _value) (remhash key flat)) removed)
|
||||
(maphash (lambda (key value) (puthash key value flat)) added)
|
||||
(unless (= (hash-table-count flat) count)
|
||||
(error "Ebox persistent source table count is inconsistent"))
|
||||
(ebox-source--flat-table flat (ebox-source-table-test base))))))
|
||||
|
||||
(defun ebox-source--order-apply (handles replacements removed appended)
|
||||
"Apply order delta to HANDLES and return a fresh exact order."
|
||||
(let ((missing (make-symbol "ebox-source-order-missing")) result)
|
||||
(dolist (handle handles)
|
||||
(let ((replacement (gethash handle replacements missing)))
|
||||
(cond
|
||||
((not (eq replacement missing)) (push replacement result))
|
||||
((gethash handle removed) nil)
|
||||
(t (push handle result)))))
|
||||
(append (nreverse result) (copy-sequence appended))))
|
||||
|
||||
(defun ebox-source--order-handles (order)
|
||||
"Materialize persistent source ORDER."
|
||||
(cond
|
||||
((listp order) (copy-sequence order))
|
||||
((ebox-source-order-handles order)
|
||||
(copy-sequence (ebox-source-order-handles order)))
|
||||
(t
|
||||
(ebox-source--order-apply
|
||||
(ebox-source--order-handles (ebox-source-order-base order))
|
||||
(ebox-source-order-replacements order)
|
||||
(ebox-source-order-removed order)
|
||||
(ebox-source-order-appended order)))))
|
||||
|
||||
(defun ebox-source--order-overlay
|
||||
(base replacements removed appended local-handles)
|
||||
"Return a bounded persistent order delta over BASE."
|
||||
(let* ((base-depth (if (ebox-source-order-p base)
|
||||
(or (ebox-source-order-depth base) 0)
|
||||
0))
|
||||
(depth (1+ base-depth)))
|
||||
(if (< depth ebox-source--persistent-max-depth)
|
||||
(ebox-source--make-order
|
||||
:base base :replacements replacements :removed removed
|
||||
:appended appended :local-handles (copy-sequence local-handles)
|
||||
:depth depth)
|
||||
(ebox-source--make-order
|
||||
:handles
|
||||
(ebox-source--order-apply
|
||||
(ebox-source--order-handles base) replacements removed appended)
|
||||
:local-handles (copy-sequence local-handles)
|
||||
:depth 0))))
|
||||
|
||||
(defun ebox-source--copy-data (value)
|
||||
"Return a detached copy of mutable source VALUE.
|
||||
Functions and records are opaque values and retain identity."
|
||||
(cond
|
||||
((functionp value) value)
|
||||
((hash-table-p value)
|
||||
(let ((copy (copy-hash-table value)))
|
||||
(clrhash copy)
|
||||
(maphash
|
||||
(lambda (key item)
|
||||
(puthash (ebox-source--copy-data key)
|
||||
(ebox-source--copy-data item)
|
||||
copy))
|
||||
value)
|
||||
copy))
|
||||
((consp value)
|
||||
(cons (ebox-source--copy-data (car value))
|
||||
(ebox-source--copy-data (cdr value))))
|
||||
((stringp value) (copy-sequence value))
|
||||
((bool-vector-p value) (copy-sequence value))
|
||||
((recordp value) value)
|
||||
((vectorp value)
|
||||
(let ((copy (copy-sequence value)))
|
||||
(dotimes (index (length copy))
|
||||
(aset copy index
|
||||
(ebox-source--copy-data (aref copy index))))
|
||||
copy))
|
||||
(t value)))
|
||||
|
||||
(defun ebox-source--metadata-string (value)
|
||||
"Return VALUE normalized to a selector metadata string."
|
||||
(cond
|
||||
((null value) nil)
|
||||
((symbolp value) (symbol-name value))
|
||||
((stringp value) (copy-sequence value))
|
||||
(t (format "%s" value))))
|
||||
|
||||
(defun ebox-source--metadata-tokens (value)
|
||||
"Return VALUE as unique normalized selector metadata tokens."
|
||||
(delete-dups
|
||||
(delq nil
|
||||
(mapcar #'ebox-source--metadata-string
|
||||
(if (listp value) value (list value))))))
|
||||
|
||||
(defun ebox-source--attribute-key (key)
|
||||
"Return canonical keyword selector attribute KEY."
|
||||
(cond
|
||||
((keywordp key) key)
|
||||
((symbolp key) (intern (concat ":" (symbol-name key))))
|
||||
((stringp key)
|
||||
(intern (if (string-prefix-p ":" key) key (concat ":" key))))
|
||||
(t (error "Ebox source attribute name is invalid: %S" key))))
|
||||
|
||||
(defun ebox-source--attributes (attributes)
|
||||
"Return detached canonical selector ATTRIBUTES."
|
||||
(unless (or (null attributes) (proper-list-p attributes))
|
||||
(error "Ebox source attributes must be an alist: %S" attributes))
|
||||
(mapcar
|
||||
(lambda (attribute)
|
||||
(unless (consp attribute)
|
||||
(error "Ebox source attributes must be an alist: %S" attributes))
|
||||
(cons (ebox-source--attribute-key (car attribute))
|
||||
(ebox-source--copy-data (cdr attribute))))
|
||||
attributes))
|
||||
|
||||
(defun ebox-source--declarations (declarations)
|
||||
"Return detached canonical DECLARATIONS after shape validation."
|
||||
(unless (and (proper-list-p declarations)
|
||||
(zerop (% (length declarations) 2)))
|
||||
(error "Ebox source declarations must be an even plist: %S"
|
||||
declarations))
|
||||
(ebox-source--copy-data declarations))
|
||||
|
||||
(defun ebox-source-builder-create ()
|
||||
"Return a mutable source builder for one canonical input."
|
||||
(ebox-source--make-builder
|
||||
:records (make-hash-table :test #'equal)
|
||||
:handle-records (make-hash-table :test #'eq)
|
||||
:order nil
|
||||
:sealed-p nil))
|
||||
|
||||
(defun ebox-source--builder-assert-open (builder)
|
||||
"Signal unless BUILDER is a live mutable source builder."
|
||||
(unless (ebox-source-builder-p builder)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-builder-p builder)))
|
||||
(when (ebox-source--builder-sealed-p builder)
|
||||
(error "Ebox source builder is already sealed")))
|
||||
|
||||
(defun ebox-source--builder-add-record (builder handle record)
|
||||
"Add identity-only HANDLE and immutable RECORD to open BUILDER."
|
||||
(ebox-source--builder-assert-open builder)
|
||||
(unless (ebox-source-handle-p handle)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-handle-p handle)))
|
||||
(unless (ebox-source-record-p record)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-record-p record)))
|
||||
(let* ((identity (ebox-source--handle-id-view handle))
|
||||
(records (ebox-source--builder-records builder)))
|
||||
(when (gethash identity records)
|
||||
(error "Ebox source identity occurs more than once: %S" identity))
|
||||
(puthash identity record records)
|
||||
;; Both indexes retain the same immutable record object. Identity lookup
|
||||
;; owns uniqueness; exact-handle lookup owns hot-path resolution.
|
||||
(puthash handle record (ebox-source--builder-handle-records builder))
|
||||
(push handle (ebox-source--builder-order builder))
|
||||
handle))
|
||||
|
||||
(cl-defun ebox-source-builder-bind
|
||||
(builder &key identity key id class selector-attributes selector-state
|
||||
declarations provenance)
|
||||
"Bind normalized source facts in BUILDER and return an opaque handle.
|
||||
The returned handle contains identity only; all facts are owned by BUILDER
|
||||
until `ebox-source-builder-finish' seals them into a source index."
|
||||
(let ((handle
|
||||
(ebox-source--make-handle
|
||||
:identity (ebox-source--copy-data
|
||||
(or identity (make-symbol "ebox-source")))))
|
||||
(record
|
||||
(ebox-source--make-record
|
||||
:key (ebox-source--copy-data key)
|
||||
:id (ebox-source--metadata-string id)
|
||||
:classes (ebox-source--metadata-tokens class)
|
||||
:attributes (ebox-source--attributes selector-attributes)
|
||||
:states (ebox-source--metadata-tokens selector-state)
|
||||
:declarations (ebox-source--declarations declarations)
|
||||
:provenance (ebox-source--copy-data provenance))))
|
||||
(ebox-source--builder-add-record builder handle record)))
|
||||
|
||||
(defun ebox-source--builder-record (builder handle)
|
||||
"Return open BUILDER's immutable record for exact HANDLE."
|
||||
(ebox-source--builder-assert-open builder)
|
||||
(gethash handle (ebox-source--builder-handle-records builder)))
|
||||
|
||||
(defun ebox-source-builder-import (builder index)
|
||||
"Import INDEX's immutable source facts into open BUILDER.
|
||||
The exact handles and records are shared; duplicate source identity is an
|
||||
error because one canonical forest cannot contain the same source twice."
|
||||
(ebox-source--builder-assert-open builder)
|
||||
(unless (ebox-source-index-p index)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-index-p index)))
|
||||
(dolist (handle (ebox-source-index-handles index))
|
||||
(ebox-source--builder-add-record
|
||||
builder handle
|
||||
(or (ebox-source--index-record index handle)
|
||||
(error "Ebox imported source handle is absent from its index"))))
|
||||
builder)
|
||||
|
||||
(cl-defun ebox-source-index-rebind
|
||||
(index handle &key
|
||||
(key nil key-p) (id nil id-p) (class nil class-p)
|
||||
(selector-attributes nil attributes-p)
|
||||
(selector-state nil states-p)
|
||||
(declarations nil declarations-p)
|
||||
(provenance nil provenance-p))
|
||||
"Return `(NEW-INDEX . NEW-HANDLE)' after rebinding HANDLE in INDEX.
|
||||
The semantic identity and all omitted immutable facts are retained. A fresh
|
||||
exact handle makes the candidate generation change explicit without placing
|
||||
facts on the canonical node."
|
||||
(let ((record (or (ebox-source--index-record index handle)
|
||||
(error "Ebox source rebind handle is absent")))
|
||||
(builder (ebox-source-builder-create)))
|
||||
(let* ((new-handle
|
||||
(ebox-source-builder-bind
|
||||
builder
|
||||
:identity (ebox-source--handle-id-view handle)
|
||||
:key (if key-p key (ebox-source-record-key record))
|
||||
:id (if id-p id (ebox-source-record-id record))
|
||||
:class (if class-p class (ebox-source-record-classes record))
|
||||
:selector-attributes
|
||||
(if attributes-p
|
||||
selector-attributes
|
||||
(ebox-source-record-attributes record))
|
||||
:selector-state
|
||||
(if states-p selector-state (ebox-source-record-states record))
|
||||
:declarations (if declarations-p
|
||||
declarations
|
||||
(ebox-source-record-declarations record))
|
||||
:provenance
|
||||
(if provenance-p provenance
|
||||
(ebox-source-record-provenance record))))
|
||||
(incoming (ebox-source-builder-finish builder))
|
||||
(sources (make-hash-table :test #'eq)))
|
||||
(puthash new-handle incoming sources)
|
||||
(let* ((candidate
|
||||
(ebox-source--index-replace-handles
|
||||
index (list handle) (list new-handle) sources
|
||||
(list (cons handle new-handle))))
|
||||
(node-subjects (ebox-source--index-node-subjects index))
|
||||
(selector-stable-p
|
||||
(not (or id-p class-p attributes-p states-p)))
|
||||
(subject (and selector-stable-p node-subjects
|
||||
(ebox-source--table-value node-subjects handle))))
|
||||
(cond
|
||||
(subject
|
||||
(let ((added (make-hash-table :test #'eq))
|
||||
(removed (make-hash-table :test #'eq)))
|
||||
(puthash new-handle subject added)
|
||||
(puthash handle t removed)
|
||||
(setq candidate
|
||||
(ebox-source--with-derived
|
||||
candidate
|
||||
:subjects (ebox-source--index-subjects index)
|
||||
:node-subjects
|
||||
(ebox-source--table-overlay
|
||||
node-subjects added removed
|
||||
(ebox-source--table-size node-subjects))
|
||||
:entries (ebox-source--index-entries index)
|
||||
:root-subject (ebox-source--index-root-subject index)
|
||||
:selector-id-table
|
||||
(ebox-source--index-selector-id-table index)
|
||||
:selector-class-table
|
||||
(ebox-source--index-selector-class-table index)
|
||||
:selector-type-table
|
||||
(ebox-source--index-selector-type-table index)
|
||||
:host-ref-table (ebox-source--index-host-ref-table index)
|
||||
:derived-stale-p t))))
|
||||
(node-subjects
|
||||
;; Selector metadata changes invalidate subject identity and ancestry;
|
||||
;; the tree owner must derive them again from the candidate root.
|
||||
(setq candidate
|
||||
(ebox-source--with-derived
|
||||
candidate :host-ref-table
|
||||
(ebox-source--index-host-ref-table index)
|
||||
:derived-stale-p t))))
|
||||
(cons candidate new-handle)))))
|
||||
|
||||
(defun ebox-source-builder-finish (builder)
|
||||
"Seal BUILDER and return its fact-only immutable source index."
|
||||
(ebox-source--builder-assert-open builder)
|
||||
(let ((records (copy-hash-table (ebox-source--builder-records builder)))
|
||||
(handle-records
|
||||
(copy-hash-table (ebox-source--builder-handle-records builder)))
|
||||
(order (nreverse (copy-sequence (ebox-source--builder-order builder)))))
|
||||
(setf (ebox-source--builder-sealed-p builder) t
|
||||
(ebox-source--builder-records builder) nil
|
||||
(ebox-source--builder-handle-records builder) nil
|
||||
(ebox-source--builder-order builder) nil)
|
||||
(ebox-source--make-index
|
||||
:records (ebox-source--flat-table records #'equal)
|
||||
:handle-records (ebox-source--flat-table handle-records #'eq)
|
||||
:order order)))
|
||||
|
||||
(defun ebox-source-handle-id (handle)
|
||||
"Return a detached copy of HANDLE's opaque diagnostic identity."
|
||||
(ebox-source--copy-data (ebox-source--handle-id-view handle)))
|
||||
|
||||
(defun ebox-source--handle-id-view (handle)
|
||||
"Return HANDLE's private immutable identity view."
|
||||
(unless (ebox-source-handle-p handle)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-handle-p handle)))
|
||||
(ebox-source--handle-identity handle))
|
||||
|
||||
(cl-defun ebox-source--with-derived
|
||||
(index &key subjects node-subjects entries root-subject
|
||||
selector-id-table selector-class-table selector-type-table
|
||||
host-ref-table derived-stale-p)
|
||||
"Return INDEX completed with immutable tree-derived query facts."
|
||||
(unless (ebox-source-index-p index)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-index-p index)))
|
||||
(ebox-source--make-index
|
||||
:records (ebox-source--index-records index)
|
||||
:handle-records (ebox-source--index-handle-records index)
|
||||
:order (ebox-source--index-order index)
|
||||
:subjects (ebox-source--table-seal subjects #'equal)
|
||||
:node-subjects (ebox-source--table-seal node-subjects #'eq)
|
||||
:entries entries :root-subject root-subject
|
||||
:selector-id-table selector-id-table
|
||||
:selector-class-table selector-class-table
|
||||
:selector-type-table selector-type-table
|
||||
:host-ref-table host-ref-table
|
||||
:derived-stale-p derived-stale-p))
|
||||
|
||||
(defun ebox-source--with-order (index handles)
|
||||
"Return INDEX sharing all facts with exact document-order HANDLES."
|
||||
(unless (ebox-source-index-p index)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-index-p index)))
|
||||
(let* ((order (ebox-source--index-order index))
|
||||
(local-handles
|
||||
(and (ebox-source-order-p order)
|
||||
(copy-sequence (ebox-source-order-local-handles order)))))
|
||||
(ebox-source--make-index
|
||||
:records (ebox-source--index-records index)
|
||||
:handle-records (ebox-source--index-handle-records index)
|
||||
:order (if local-handles
|
||||
(ebox-source--make-order
|
||||
:handles (copy-sequence handles)
|
||||
:local-handles local-handles :depth 0)
|
||||
(copy-sequence handles))
|
||||
:subjects (ebox-source--index-subjects index)
|
||||
:node-subjects (ebox-source--index-node-subjects index)
|
||||
:entries (ebox-source--index-entries index)
|
||||
:root-subject (ebox-source--index-root-subject index)
|
||||
:selector-id-table (ebox-source--index-selector-id-table index)
|
||||
:selector-class-table (ebox-source--index-selector-class-table index)
|
||||
:selector-type-table (ebox-source--index-selector-type-table index)
|
||||
:host-ref-table (ebox-source--index-host-ref-table index)
|
||||
:derived-stale-p (ebox-source--index-derived-stale-p index))))
|
||||
|
||||
(defun ebox-source--with-runtime-hosts (index host-ref-table)
|
||||
"Return INDEX sharing its source facts with runtime HOST-REF-TABLE."
|
||||
(unless (ebox-source-index-p index)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-index-p index)))
|
||||
(ebox-source--make-index
|
||||
:records (ebox-source--index-records index)
|
||||
:handle-records (ebox-source--index-handle-records index)
|
||||
:order (ebox-source--index-order index)
|
||||
:subjects (ebox-source--index-subjects index)
|
||||
:node-subjects (ebox-source--index-node-subjects index)
|
||||
:entries (ebox-source--index-entries index)
|
||||
:root-subject (ebox-source--index-root-subject index)
|
||||
:selector-id-table (ebox-source--index-selector-id-table index)
|
||||
:selector-class-table (ebox-source--index-selector-class-table index)
|
||||
:selector-type-table (ebox-source--index-selector-type-table index)
|
||||
:host-ref-table host-ref-table
|
||||
:derived-stale-p (ebox-source--index-derived-stale-p index)))
|
||||
|
||||
(defun ebox-source--index-replace-handles
|
||||
(index removed added incoming-source-indexes
|
||||
&optional replacement-pairs final-order)
|
||||
"Return candidate INDEX after two-phase REMOVED and ADDED handle changes.
|
||||
Source records and exact handle membership are updated persistently. Tree and
|
||||
selector derivations remain shared but are marked stale until explicitly
|
||||
rebuilt or patched by the tree owner. REPLACEMENT-PAIRS preserve exact order
|
||||
for nonstructural changes; structural changes supply FINAL-ORDER."
|
||||
(unless (ebox-source-index-p index)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-index-p index)))
|
||||
(let ((removed-identities (make-hash-table :test #'equal))
|
||||
(removed-handles (make-hash-table :test #'eq))
|
||||
(added-records (make-hash-table :test #'equal))
|
||||
(added-handle-records (make-hash-table :test #'eq))
|
||||
(replacement-table (make-hash-table :test #'eq))
|
||||
(replacement-values (make-hash-table :test #'eq))
|
||||
added-order)
|
||||
(dolist (handle removed)
|
||||
(when (ebox-source-handle-p handle)
|
||||
(unless (ebox-source-index-handle-member-p index handle)
|
||||
(error "Ebox removed source handle is absent"))
|
||||
(puthash handle t removed-handles)
|
||||
(puthash (ebox-source--handle-id-view handle) t
|
||||
removed-identities)))
|
||||
(dolist (handle added)
|
||||
(unless (ebox-source-handle-p handle)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-handle-p handle)))
|
||||
(let ((identity (ebox-source--handle-id-view handle)))
|
||||
(when (or (gethash identity added-records)
|
||||
(and (ebox-source--table-member-p
|
||||
(ebox-source--index-records index) identity)
|
||||
(not (gethash identity removed-identities))))
|
||||
(error "Ebox source identity occurs more than once: %S" identity))
|
||||
(let* ((incoming-index
|
||||
(and (hash-table-p incoming-source-indexes)
|
||||
(gethash handle incoming-source-indexes)))
|
||||
(record
|
||||
(and incoming-index
|
||||
(ebox-source--index-record incoming-index handle))))
|
||||
(unless record
|
||||
(error "Ebox candidate source is absent from incoming input"))
|
||||
(puthash identity record added-records)
|
||||
(puthash handle record added-handle-records))
|
||||
(push handle added-order)))
|
||||
(setq added-order (nreverse added-order))
|
||||
(dolist (pair replacement-pairs)
|
||||
(unless (and (gethash (car pair) removed-handles)
|
||||
(gethash (cdr pair) added-handle-records))
|
||||
(error "Ebox source order replacement is not part of its fact delta"))
|
||||
(puthash (car pair) (cdr pair) replacement-table)
|
||||
(puthash (cdr pair) t replacement-values))
|
||||
(unless final-order
|
||||
(maphash
|
||||
(lambda (handle _value)
|
||||
(unless (gethash handle replacement-table)
|
||||
(error "Structural source removal requires a final document order")))
|
||||
removed-handles)
|
||||
(dolist (handle added-order)
|
||||
(unless (gethash handle replacement-values)
|
||||
(error "Structural source insertion requires a final document order"))))
|
||||
(ebox-source--make-index
|
||||
:records
|
||||
(ebox-source--table-overlay
|
||||
(ebox-source--index-records index) added-records removed-identities
|
||||
(+ (- (ebox-source-table-count (ebox-source--index-records index))
|
||||
(hash-table-count removed-identities))
|
||||
(hash-table-count added-records)))
|
||||
:handle-records
|
||||
(ebox-source--table-overlay
|
||||
(ebox-source--index-handle-records index)
|
||||
added-handle-records removed-handles
|
||||
(+ (- (ebox-source-table-count
|
||||
(ebox-source--index-handle-records index))
|
||||
(hash-table-count removed-handles))
|
||||
(hash-table-count added-handle-records)))
|
||||
:order
|
||||
(if final-order
|
||||
(ebox-source--make-order
|
||||
:handles (copy-sequence final-order)
|
||||
:local-handles (copy-sequence added-order) :depth 0)
|
||||
(ebox-source--order-overlay
|
||||
(ebox-source--index-order index)
|
||||
replacement-table removed-handles nil added-order))
|
||||
:subjects (ebox-source--index-subjects index)
|
||||
:node-subjects (ebox-source--index-node-subjects index)
|
||||
:entries (ebox-source--index-entries index)
|
||||
:root-subject (ebox-source--index-root-subject index)
|
||||
:selector-id-table (ebox-source--index-selector-id-table index)
|
||||
:selector-class-table (ebox-source--index-selector-class-table index)
|
||||
:selector-type-table (ebox-source--index-selector-type-table index)
|
||||
:host-ref-table (ebox-source--index-host-ref-table index)
|
||||
:derived-stale-p t)))
|
||||
|
||||
(defun ebox-source-index-handles (index)
|
||||
"Return source handles from immutable INDEX in document order."
|
||||
(unless (ebox-source-index-p index)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-index-p index)))
|
||||
(ebox-source--order-handles (ebox-source--index-order index)))
|
||||
|
||||
(defun ebox-source--index-local-handles (index)
|
||||
"Return handles added by INDEX's newest persistent overlay."
|
||||
(let ((order (ebox-source--index-order index)))
|
||||
(and (ebox-source-order-p order)
|
||||
(copy-sequence (ebox-source-order-local-handles order)))))
|
||||
|
||||
(defun ebox-source-index-handle-member-p (index handle)
|
||||
"Return non-nil when exact opaque HANDLE belongs to candidate INDEX."
|
||||
(unless (ebox-source-index-p index)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-index-p index)))
|
||||
(and (ebox-source-handle-p handle)
|
||||
(ebox-source--table-member-p
|
||||
(ebox-source--index-handle-records index) handle)))
|
||||
|
||||
(defun ebox-source--index-record (index handle)
|
||||
"Return INDEX-owned record for HANDLE, or nil."
|
||||
(unless (ebox-source-index-p index)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-index-p index)))
|
||||
(unless (ebox-source-handle-p handle)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-handle-p handle)))
|
||||
(ebox-source--table-value
|
||||
(ebox-source--index-handle-records index) handle))
|
||||
|
||||
(defun ebox-source-index-key (index handle)
|
||||
"Return HANDLE's detached author key from candidate INDEX."
|
||||
(ebox-source--copy-data (ebox-source--index-key-view index handle)))
|
||||
|
||||
(defun ebox-source--index-key-view (index handle)
|
||||
"Return HANDLE's private immutable author key view in INDEX."
|
||||
(when-let* ((record (ebox-source--index-record index handle)))
|
||||
(ebox-source-record-key record)))
|
||||
|
||||
(defun ebox-source-index-id (index handle)
|
||||
"Return HANDLE's detached selector id from candidate INDEX."
|
||||
(ebox-source--copy-data (ebox-source--index-id-view index handle)))
|
||||
|
||||
(defun ebox-source--index-id-view (index handle)
|
||||
"Return HANDLE's private immutable selector id view in INDEX."
|
||||
(when-let* ((record (ebox-source--index-record index handle)))
|
||||
(ebox-source-record-id record)))
|
||||
|
||||
(defun ebox-source-index-classes (index handle)
|
||||
"Return HANDLE's detached selector classes in candidate INDEX."
|
||||
(ebox-source--copy-data (ebox-source--index-classes-view index handle)))
|
||||
|
||||
(defun ebox-source--index-classes-view (index handle)
|
||||
"Return HANDLE's private immutable selector classes view in INDEX."
|
||||
(when-let* ((record (ebox-source--index-record index handle)))
|
||||
(ebox-source-record-classes record)))
|
||||
|
||||
(defun ebox-source-index-attributes (index handle)
|
||||
"Return HANDLE's detached selector attributes in candidate INDEX."
|
||||
(ebox-source--copy-data (ebox-source--index-attributes-view index handle)))
|
||||
|
||||
(defun ebox-source--index-attributes-view (index handle)
|
||||
"Return HANDLE's private immutable selector attributes view in INDEX."
|
||||
(when-let* ((record (ebox-source--index-record index handle)))
|
||||
(ebox-source-record-attributes record)))
|
||||
|
||||
(defun ebox-source-index-states (index handle)
|
||||
"Return HANDLE's detached selector states in candidate INDEX."
|
||||
(ebox-source--copy-data (ebox-source--index-states-view index handle)))
|
||||
|
||||
(defun ebox-source--index-states-view (index handle)
|
||||
"Return HANDLE's private immutable selector states view in INDEX."
|
||||
(when-let* ((record (ebox-source--index-record index handle)))
|
||||
(ebox-source-record-states record)))
|
||||
|
||||
(defun ebox-source-index-declarations (index handle)
|
||||
"Return HANDLE's detached canonical declarations from candidate INDEX."
|
||||
(ebox-source--copy-data
|
||||
(ebox-source--index-declarations-view index handle)))
|
||||
|
||||
(defun ebox-source--index-declarations-view (index handle)
|
||||
"Return HANDLE's private immutable declaration view in INDEX."
|
||||
(when-let* ((record (ebox-source--index-record index handle)))
|
||||
(ebox-source-record-declarations record)))
|
||||
|
||||
(defun ebox-source-index-provenance (index handle)
|
||||
"Return HANDLE's detached provenance in candidate INDEX."
|
||||
(ebox-source--copy-data (ebox-source--index-provenance-view index handle)))
|
||||
|
||||
(defun ebox-source--index-provenance-view (index handle)
|
||||
"Return HANDLE's private immutable provenance view in INDEX."
|
||||
(when-let* ((record (ebox-source--index-record index handle)))
|
||||
(ebox-source-record-provenance record)))
|
||||
|
||||
(defun ebox-source--copy-record (record)
|
||||
"Return a detached copy of source RECORD."
|
||||
(and record
|
||||
(ebox-source--make-record
|
||||
:key (ebox-source--copy-data (ebox-source-record-key record))
|
||||
:id (ebox-source--copy-data (ebox-source-record-id record))
|
||||
:classes (ebox-source--copy-data
|
||||
(ebox-source-record-classes record))
|
||||
:attributes (ebox-source--copy-data
|
||||
(ebox-source-record-attributes record))
|
||||
:states (ebox-source--copy-data (ebox-source-record-states record))
|
||||
:declarations (ebox-source--copy-data
|
||||
(ebox-source-record-declarations record))
|
||||
:provenance (ebox-source--copy-data
|
||||
(ebox-source-record-provenance record)))))
|
||||
|
||||
(defun ebox-source-index-record (index handle)
|
||||
"Return a detached source record for HANDLE in INDEX, or nil."
|
||||
(ebox-source--copy-record (ebox-source--index-record index handle)))
|
||||
|
||||
(defun ebox-source--index-subject (index handle)
|
||||
"Return INDEX-owned ECSS subject for HANDLE, or nil."
|
||||
(unless (ebox-source-index-p index)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-index-p index)))
|
||||
(ebox-source--table-value
|
||||
(ebox-source--index-subjects index)
|
||||
(ebox-source--handle-id-view handle)))
|
||||
|
||||
(defun ebox-source--index-node-subject (index node)
|
||||
"Return INDEX-owned ECSS subject for canonical NODE, or nil.
|
||||
Node copies retain the same opaque source handle, so subject lookup must not
|
||||
depend on the identity of a particular plist object."
|
||||
(unless (ebox-source-index-p index)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-index-p index)))
|
||||
(when-let* ((handle (and (listp node)
|
||||
(plist-get node :ebox-source-handle))))
|
||||
(ebox-source--table-value
|
||||
(ebox-source--index-node-subjects index) handle)))
|
||||
|
||||
(defun ebox-source--subject-table-get (table handle)
|
||||
"Return exact HANDLE's subject from immutable TABLE."
|
||||
(ebox-source--table-value table handle))
|
||||
|
||||
(defun ebox-source--index-node-subject-table-view (index)
|
||||
"Return INDEX's immutable source-handle to ECSS-subject table."
|
||||
(unless (ebox-source-index-p index)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-index-p index)))
|
||||
(ebox-source--index-node-subjects index))
|
||||
|
||||
(defun ebox-source--index-entries-view (index)
|
||||
"Return INDEX's immutable document-order subject entries."
|
||||
(unless (ebox-source-index-p index)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-index-p index)))
|
||||
(ebox-source--index-entries index))
|
||||
|
||||
(defun ebox-source--index-root-subject-view (index)
|
||||
"Return INDEX's ECSS root subject."
|
||||
(unless (ebox-source-index-p index)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-index-p index)))
|
||||
(ebox-source--index-root-subject index))
|
||||
|
||||
(defun ebox-source--index-host-ref-table-view (index)
|
||||
"Return INDEX's immutable host-reference table."
|
||||
(unless (ebox-source-index-p index)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-index-p index)))
|
||||
(ebox-source--index-host-ref-table index))
|
||||
|
||||
(defun ebox-source--index-selector-table (index kind)
|
||||
"Return INDEX's derived selector table of KIND."
|
||||
(unless (ebox-source-index-p index)
|
||||
(signal 'wrong-type-argument (list 'ebox-source-index-p index)))
|
||||
(pcase kind
|
||||
('id (ebox-source--index-selector-id-table index))
|
||||
('class (ebox-source--index-selector-class-table index))
|
||||
('type (ebox-source--index-selector-type-table index))
|
||||
(_ (error "Unknown Ebox source selector index kind: %S" kind))))
|
||||
|
||||
(provide 'ebox-source)
|
||||
;;; ebox-source.el ends here
|
||||
@ -10,6 +10,7 @@
|
||||
(require 'subr-x)
|
||||
(require 'ecss)
|
||||
(require 'tp-style)
|
||||
(require 'ebox-source)
|
||||
(require 'ebox-layout-config)
|
||||
|
||||
(defvar ebox--longhand)
|
||||
@ -1314,23 +1315,41 @@ SNAPSHOT reuses a previously detached ECSS values snapshot when supplied."
|
||||
"Return non-nil when STYLE PROPERTY has a declaration winner."
|
||||
(ecss-computed-style-specified-p style property))
|
||||
|
||||
(defun ebox-style-node-declarations (node)
|
||||
"Return NODE's immutable canonical ECSS declarations.
|
||||
Callers that need to mutate the declaration spine must copy it first."
|
||||
(let ((declarations (plist-get node :ebox-style-declarations)))
|
||||
(defun ebox-style--node-declarations-from-record (node record)
|
||||
"Return NODE's author declarations from borrowed immutable source RECORD."
|
||||
(let ((declarations
|
||||
(and record (ebox-source-record-declarations record))))
|
||||
(if (memq (plist-get node :ebox-type) '(flex grid))
|
||||
(ebox-style-merge-declarations
|
||||
declarations
|
||||
(plist-get (plist-get node :box) :ebox-style-overrides))
|
||||
declarations)))
|
||||
|
||||
(defun ebox-style-node-specified-value (node name &optional missing)
|
||||
(defun ebox-style-node-declarations (source-index node)
|
||||
"Return NODE's immutable author declarations from candidate SOURCE-INDEX.
|
||||
Private generated layout nodes have no author source and therefore contribute
|
||||
no declarations of their own."
|
||||
(ebox-style--node-declarations-from-record
|
||||
node
|
||||
(when-let* ((handle (plist-get node :ebox-source-handle)))
|
||||
(ebox-source--index-record source-index handle))))
|
||||
|
||||
(defun ebox-style-node-specified-value
|
||||
(node name &optional missing source-index)
|
||||
"Return NODE's canonical specified value for author property NAME.
|
||||
Return MISSING when NAME is valid but not specified. Aliases resolve to the
|
||||
same canonical property id, so callers never inspect backend plist fields."
|
||||
same canonical property id, so callers never inspect backend plist fields.
|
||||
NODE may be a canonical input with one root. A mounted raw node requires its
|
||||
explicit generation SOURCE-INDEX; facts are never recovered from its handle."
|
||||
(when (ebox-canonical-input-p node)
|
||||
(setq source-index (ebox-canonical-input--source-index node)
|
||||
node (ebox-canonical-input--single-root
|
||||
node "ebox-style-node-specified-value")))
|
||||
(unless (ebox-source-index-p source-index)
|
||||
(error "Ebox specified-value lookup requires a source index"))
|
||||
(let ((id (or (ebox-style-schema-id name)
|
||||
(error "Unknown Ebox property: %S" name)))
|
||||
(declarations (ebox-style-node-declarations node)))
|
||||
(declarations (ebox-style-node-declarations source-index node)))
|
||||
(if (plist-member declarations id)
|
||||
(plist-get declarations id)
|
||||
missing)))
|
||||
@ -1583,12 +1602,49 @@ until their private encoding is removed."
|
||||
(setq tail (cddr tail)))))
|
||||
node)
|
||||
|
||||
(defconst ebox-style--item-projection-properties
|
||||
'(:order :flex-grow :flex-shrink :flex-basis :align-self
|
||||
:grid-column :grid-row :grid-column-span :grid-row-span)
|
||||
"Canonical node fields owned by computed parent participation style.")
|
||||
|
||||
(defun ebox-style--transfer-node-property (old new property)
|
||||
"Transfer style-owned PROPERTY presence and value from OLD to NEW."
|
||||
(if (plist-member old property)
|
||||
(plist-put new property (plist-get old property))
|
||||
(ebox-style--delete-node-property new property)))
|
||||
|
||||
(defun ebox-style--computed-projection-transferable-p (old new style)
|
||||
"Return non-nil when OLD's STYLE projection is exact for canonical NEW.
|
||||
Display axes and typed layout config combine style with the fresh node's base
|
||||
structure. They must already agree; otherwise the ordinary projection path
|
||||
recomputes that combination from NEW."
|
||||
(and (memq (plist-get new :ebox-kind) '(text box))
|
||||
(eq (plist-get old :ebox-kind) (plist-get new :ebox-kind))
|
||||
(eq (plist-get old :ebox-type) (plist-get new :ebox-type))
|
||||
(eq style (plist-get old :ebox-computed-style))
|
||||
(equal (plist-get old :display) (plist-get new :display))
|
||||
(equal (plist-get old :ebox-layout-config)
|
||||
(plist-get new :ebox-layout-config))))
|
||||
|
||||
(defun ebox-style--transfer-computed-projection (old new style)
|
||||
"Transfer OLD's materialized STYLE projection onto fresh canonical NEW.
|
||||
OLD and NEW must have the same canonical kind and source-stable computed
|
||||
style. Content, children, source/runtime identity, and scroll state remain
|
||||
owned by NEW; only fields owned by the unchanged computed style are shared."
|
||||
(unless (ebox-style--computed-projection-transferable-p old new style)
|
||||
(error "Ebox computed projection transfer requires source-stable peers"))
|
||||
(plist-put new :ebox-computed-style style)
|
||||
(cl-loop for (property _value) on ebox--longhand by #'cddr
|
||||
unless (memq property '(:content :scroll-offset))
|
||||
do (ebox-style--transfer-node-property old new property))
|
||||
(dolist (property ebox-style--item-projection-properties)
|
||||
(ebox-style--transfer-node-property old new property))
|
||||
new)
|
||||
|
||||
(defun ebox-style--apply-item (node style &optional snapshot)
|
||||
"Apply computed Flex/Grid item fields from STYLE to NODE."
|
||||
(let ((values (ebox-style--context-values style 'item nil snapshot)))
|
||||
(dolist (property '(:order :flex-grow :flex-shrink :flex-basis :align-self
|
||||
:grid-column :grid-row
|
||||
:grid-column-span :grid-row-span))
|
||||
(dolist (property ebox-style--item-projection-properties)
|
||||
(ebox-style--delete-node-property node property))
|
||||
(cl-loop for (property value) on values by #'cddr
|
||||
do (plist-put node property value)))
|
||||
|
||||
238
ebox-surface.el
238
ebox-surface.el
@ -527,8 +527,8 @@ continues to own its inline text directly."
|
||||
(or (eq (plist-get node :ebox-kind) 'text)
|
||||
(null (plist-get node :ebox-kind)))))
|
||||
|
||||
(defun ebox-surface--inline-inheritance-required-p (root)
|
||||
"Return non-nil when inline styles in ROOT require an ECSS cascade.
|
||||
(defun ebox-surface--inline-inheritance-required-p (root source-index)
|
||||
"Return non-nil when ROOT in SOURCE-INDEX requires an ECSS cascade.
|
||||
An empty stylesheet still needs a cascade when an inherited declaration can
|
||||
reach a descendant that does not declare the same property. Other inline
|
||||
declarations are already projected into the candidate engine fields at node
|
||||
@ -539,7 +539,8 @@ construction time and can use the static projection path."
|
||||
(let* ((entry (pop pending))
|
||||
(node (car entry))
|
||||
(inherited (cdr entry))
|
||||
(declarations (ebox-style-node-declarations node)))
|
||||
(declarations
|
||||
(ebox-style-node-declarations source-index node)))
|
||||
(when (cl-some (lambda (property)
|
||||
(and (ebox-surface--inherited-style-consumer-p node)
|
||||
(memq property inherited)
|
||||
@ -1189,6 +1190,11 @@ PREVIOUS-STATE's node-object table."
|
||||
(node-root
|
||||
(tp-object-ensure context surface-root
|
||||
ebox-surface--nodes-key 'ebox/nodes))
|
||||
(source-index
|
||||
(or (plist-get state-overrides :source-index)
|
||||
(and (eq root (plist-get previous-state :root-node))
|
||||
(plist-get previous-state :source-index))
|
||||
(ebox-tree-source-index root)))
|
||||
(range-splice-p
|
||||
(and (eq projection-kind 'mixed-owner-reflow)
|
||||
(ebox-surface--mixed-range-splice-p state-overrides)))
|
||||
@ -1223,16 +1229,29 @@ PREVIOUS-STATE's node-object table."
|
||||
stylesheet-signature)))
|
||||
(bindings-by-subject (make-hash-table :test 'eq))
|
||||
(states-by-subject (make-hash-table :test 'eq))
|
||||
(subjects (when (and style-required-p
|
||||
(not (and (eq projection-kind 'native-frame)
|
||||
(plist-get state-overrides
|
||||
:native-topology-stable-p)))
|
||||
(not (and (memq projection-kind
|
||||
'(owner-scoped scroll-patch
|
||||
formatting-context-reflow
|
||||
mixed-owner-reflow))
|
||||
(not range-splice-p))))
|
||||
(ebox-tree-subject-index root)))
|
||||
(subjects-required-p
|
||||
(and style-required-p
|
||||
(not (and (eq projection-kind 'native-frame)
|
||||
(plist-get state-overrides
|
||||
:native-topology-stable-p)))
|
||||
(not (and (memq projection-kind
|
||||
'(owner-scoped scroll-patch
|
||||
formatting-context-reflow
|
||||
mixed-owner-reflow))
|
||||
(not range-splice-p)))))
|
||||
(_materialized-source-index
|
||||
(when (and subjects-required-p
|
||||
(ebox-source--index-derived-stale-p source-index))
|
||||
;; A local candidate patches exact handle→subject ancestry in
|
||||
;; O(changed) while marking document-order selector views stale.
|
||||
;; Style projection consumes that patched table directly; only a
|
||||
;; fact-only input with no subjects needs a full tree derivation.
|
||||
(unless (ebox-source--index-node-subjects source-index)
|
||||
(setq source-index
|
||||
(ebox-tree-source-index
|
||||
root t ebox--render-root-parent-kind source-index)))))
|
||||
(subjects (when subjects-required-p
|
||||
(ebox-tree-subject-index root source-index)))
|
||||
(selector-tree-snapshot
|
||||
(if (and (memq projection-kind
|
||||
'(owner-scoped scroll-patch
|
||||
@ -1258,14 +1277,16 @@ PREVIOUS-STATE's node-object table."
|
||||
(cond
|
||||
((eq projection-kind 'native-frame)
|
||||
(ebox-surface--project-node-delta
|
||||
context root previous-state state-overrides node-root objects-by-node
|
||||
context root source-index previous-state state-overrides
|
||||
node-root objects-by-node
|
||||
(and subjects (plist-get subjects :node-subject-table))
|
||||
bindings-by-subject states-by-subject style-state-table
|
||||
stylesheet-signature selector-tree-token
|
||||
stylesheet-subject-local-p))
|
||||
(range-splice-p
|
||||
(ebox-surface--project-node-delta
|
||||
context root previous-state state-overrides node-root objects-by-node
|
||||
context root source-index previous-state state-overrides
|
||||
node-root objects-by-node
|
||||
(and subjects (plist-get subjects :node-subject-table))
|
||||
bindings-by-subject states-by-subject style-state-table
|
||||
stylesheet-signature selector-tree-token
|
||||
@ -1278,7 +1299,7 @@ PREVIOUS-STATE's node-object table."
|
||||
(t
|
||||
(tp-object-retain context node-root)
|
||||
(ebox-surface--ensure-node-tree
|
||||
context node-root root objects-by-node
|
||||
context node-root root source-index objects-by-node
|
||||
(and subjects (plist-get subjects :node-subject-table))
|
||||
bindings-by-subject states-by-subject style-state-table
|
||||
stylesheet-signature
|
||||
@ -1288,6 +1309,7 @@ PREVIOUS-STATE's node-object table."
|
||||
(ebox-tree-clear-author-style-pending root))
|
||||
(list :surface-root surface-root
|
||||
:node-root node-root
|
||||
:source-index source-index
|
||||
:objects-by-node objects-by-node
|
||||
:style-binding-states style-state-table
|
||||
:stylesheet-signature stylesheet-signature
|
||||
@ -1309,7 +1331,8 @@ PREVIOUS-STATE's node-object table."
|
||||
objects-by-node)
|
||||
|
||||
(defun ebox-surface--project-node-delta
|
||||
(context root previous-state state-overrides node-root objects-by-node
|
||||
(context root source-index previous-state state-overrides
|
||||
node-root objects-by-node
|
||||
subject-table bindings-by-subject states-by-subject
|
||||
style-state-table stylesheet-signature selector-tree-token
|
||||
stylesheet-subject-local-p)
|
||||
@ -1333,7 +1356,9 @@ PREVIOUS-STATE's node-object table."
|
||||
(maphash
|
||||
(lambda (node-id node)
|
||||
(when subject-table
|
||||
(when-let* ((subject (gethash node subject-table))
|
||||
(when-let* ((subject
|
||||
(ebox-source--subject-table-get
|
||||
subject-table (ebox-tree-node-source-handle node)))
|
||||
(object (gethash node-id old-objects))
|
||||
(style-state (gethash object style-state-table))
|
||||
(binding (plist-get style-state :binding)))
|
||||
@ -1365,7 +1390,8 @@ PREVIOUS-STATE's node-object table."
|
||||
(error "Ebox native projection has an unordered node delta"))
|
||||
(let ((object
|
||||
(tp-object-ensure-at context parent
|
||||
(ebox-surface--node-key node)
|
||||
(ebox-surface--node-key
|
||||
source-index node)
|
||||
(ebox-surface--node-kind node)
|
||||
position)))
|
||||
(tp-object-retain context object)
|
||||
@ -1373,7 +1399,7 @@ PREVIOUS-STATE's node-object table."
|
||||
(tp-object-reuse context object)
|
||||
(when subject-table
|
||||
(ebox-surface--apply-node-style
|
||||
object node subject-table bindings-by-subject
|
||||
source-index object node subject-table bindings-by-subject
|
||||
states-by-subject style-state-table stylesheet-signature
|
||||
selector-tree-token stylesheet-subject-local-p)))
|
||||
(plist-put node :surface-object object)
|
||||
@ -1389,7 +1415,7 @@ PREVIOUS-STATE's node-object table."
|
||||
objects-by-node))
|
||||
|
||||
(defun ebox-surface--complete-projection
|
||||
(context root style-required-p projection)
|
||||
(context root source-index style-required-p projection)
|
||||
"Complete a retained PROJECTION for ROOT after a local proof miss.
|
||||
The initial span attempt already owns the surface and node-root identities in
|
||||
CONTEXT, so full fallback must populate those same objects instead of creating
|
||||
@ -1411,10 +1437,10 @@ the projection roots a second time."
|
||||
(bindings-by-subject (make-hash-table :test 'eq))
|
||||
(states-by-subject (make-hash-table :test 'eq))
|
||||
(subjects (when style-required-p
|
||||
(ebox-tree-subject-index root))))
|
||||
(ebox-tree-subject-index root source-index))))
|
||||
(tp-object-retain context node-root)
|
||||
(ebox-surface--ensure-node-tree
|
||||
context node-root root objects-by-node
|
||||
context node-root root source-index objects-by-node
|
||||
(and subjects (plist-get subjects :node-subject-table))
|
||||
bindings-by-subject states-by-subject style-state-table
|
||||
stylesheet-signature
|
||||
@ -1884,9 +1910,7 @@ identities while always discarding TP handles and render-cache attachments.
|
||||
VALIDATED-P means the caller owns a current topology certificate for SOURCE."
|
||||
(unless (and (listp source) (not (stringp source)))
|
||||
(error "Ebox surface source must be an Ebox node"))
|
||||
(unless validated-p
|
||||
(ebox-tree-validate-declarative-root
|
||||
source t ebox--render-root-parent-kind))
|
||||
(ignore validated-p)
|
||||
(let ((candidate (ebox-tree-copy-node-structure source)))
|
||||
(if preserve-identities-p
|
||||
(ebox-surface--clear-runtime-attachments candidate)
|
||||
@ -1908,10 +1932,13 @@ published generation so a failed transaction has no cache state to restore."
|
||||
(setq copy (plist-put copy key (copy-hash-table value)))))))
|
||||
copy))
|
||||
|
||||
(defun ebox-surface--reconcile-candidate (root previous-state)
|
||||
"Reconcile styled ROOT with PREVIOUS-STATE runtime identities."
|
||||
(defun ebox-surface--reconcile-candidate
|
||||
(root source-index previous-state)
|
||||
"Reconcile ROOT identities using SOURCE-INDEX and PREVIOUS-STATE."
|
||||
(when-let* ((previous-root (plist-get previous-state :root-node)))
|
||||
(ebox-tree-reconcile-runtime previous-root root))
|
||||
(ebox-tree-reconcile-runtime
|
||||
previous-root (plist-get previous-state :source-index)
|
||||
root source-index))
|
||||
root)
|
||||
|
||||
(defun ebox-surface--source-region-id-set (source)
|
||||
@ -1933,11 +1960,11 @@ published generation so a failed transaction has no cache state to restore."
|
||||
(not (ebox--runtime-region-id-conflict
|
||||
(ebox-surface--source-region-id-set source) buffer))))
|
||||
|
||||
(defun ebox-surface--node-key (node &optional positional-p)
|
||||
"Return NODE's namespaced sibling key, or nil for positional identity."
|
||||
(defun ebox-surface--node-key (source-index node &optional positional-p)
|
||||
"Return NODE's SOURCE-INDEX sibling key, or nil when positional."
|
||||
(when-let* ((key (and (not positional-p)
|
||||
(not (plist-get node :ebox-sequence-location))
|
||||
(plist-get node :key))))
|
||||
(ebox-tree-node-author-key source-index node))))
|
||||
(list 'ebox/key key)))
|
||||
|
||||
(defun ebox-surface--node-kind (node)
|
||||
@ -1945,14 +1972,16 @@ published generation so a failed transaction has no cache state to restore."
|
||||
(list 'ebox/node (plist-get node :ebox-type)))
|
||||
|
||||
(defun ebox-surface--node-style-binding
|
||||
(object node subject bindings-by-subject states-by-subject style-state-table
|
||||
(source-index object node subject bindings-by-subject states-by-subject
|
||||
style-state-table
|
||||
stylesheet-signature selector-tree-token
|
||||
stylesheet-subject-local-p)
|
||||
"Install and return NODE style binding on OBJECT for SUBJECT."
|
||||
(let* ((parent (ecss-subject-parent subject))
|
||||
(parent-binding (and parent (gethash parent bindings-by-subject)))
|
||||
(parent-style-state (and parent (gethash parent states-by-subject)))
|
||||
(declarations (ebox-style-node-declarations node))
|
||||
(declarations
|
||||
(ebox-tree-node-source-declarations source-index node))
|
||||
(old-state (gethash object style-state-table))
|
||||
(old-binding (and old-state (plist-get old-state :binding)))
|
||||
(selector-context-stable-p
|
||||
@ -2113,9 +2142,10 @@ published generation so a failed transaction has no cache state to restore."
|
||||
(puthash key style ebox-surface--inline-style-value-cache)
|
||||
style)))))
|
||||
|
||||
(defun ebox-surface--closed-canonical-node-p (node)
|
||||
"Return non-nil when NODE needs only closed inherited style projection."
|
||||
(let ((declarations (ebox-style-node-declarations node)))
|
||||
(defun ebox-surface--closed-canonical-node-p (source-index node)
|
||||
"Return non-nil when NODE in SOURCE-INDEX needs closed inheritance."
|
||||
(let ((declarations
|
||||
(ebox-tree-node-source-declarations source-index node)))
|
||||
(and (eq (plist-get node :ebox-type) 'box)
|
||||
(memq (plist-get node :ebox-kind) '(box text))
|
||||
(not (ebox-style-cascade-active-p))
|
||||
@ -2132,7 +2162,7 @@ the unpublished node and parent tables. Unchanged declaration/subject chains
|
||||
reuse their exact prior style; changed nodes compute once against the prepared
|
||||
candidate parent style. Stylesheet rules deliberately disable this local
|
||||
path because selector scope may extend outside NODE-IDS. Return the prepared
|
||||
node-to-subject table when the requested local preparation was completed."
|
||||
source-handle-to-subject table when the requested local preparation completed."
|
||||
(let ((stylesheet-signature (ebox-surface--stylesheet-signature)))
|
||||
(when (and node-ids
|
||||
(ebox-surface--stylesheet-subject-local-p
|
||||
@ -2140,10 +2170,12 @@ node-to-subject table when the requested local preparation was completed."
|
||||
(let ((old-nodes (plist-get old-state :node-table))
|
||||
(new-nodes (plist-get candidate-index :node-table))
|
||||
(parents (plist-get candidate-index :parent-table))
|
||||
(old-source-index (plist-get old-state :source-index))
|
||||
(new-source-index (plist-get candidate-index :source-index))
|
||||
(resolved (make-hash-table :test 'equal))
|
||||
(visiting (make-hash-table :test 'equal))
|
||||
(subjects-by-id (make-hash-table :test 'equal))
|
||||
(subjects-by-node (make-hash-table :test 'eq))
|
||||
(subjects-by-handle (make-hash-table :test 'eq))
|
||||
(ebox-surface--inline-style-value-cache
|
||||
(make-hash-table :test 'equal)))
|
||||
(cl-labels
|
||||
@ -2176,19 +2208,25 @@ node-to-subject table when the requested local preparation was completed."
|
||||
(and old-node
|
||||
(plist-get old-node :ebox-computed-style)))
|
||||
(declarations
|
||||
(and node (ebox-style-node-declarations node)))
|
||||
(and node
|
||||
(ebox-tree-node-source-declarations
|
||||
new-source-index node)))
|
||||
(old-declarations
|
||||
(and old-node
|
||||
(ebox-style-node-declarations old-node)))
|
||||
(ebox-tree-node-source-declarations
|
||||
old-source-index old-node)))
|
||||
(subject
|
||||
(and node
|
||||
(or (gethash node-id subjects-by-id)
|
||||
(let ((created
|
||||
(ebox-tree-node-subject node)))
|
||||
(ebox-tree-node-subject
|
||||
new-source-index node)))
|
||||
(setf (ecss-subject-parent created)
|
||||
parent-subject)
|
||||
(puthash node-id created subjects-by-id)
|
||||
(puthash node created subjects-by-node)
|
||||
(puthash
|
||||
(ebox-tree-node-source-handle node)
|
||||
created subjects-by-handle)
|
||||
created))))
|
||||
(old-subject
|
||||
(and old-node
|
||||
@ -2203,7 +2241,8 @@ node-to-subject table when the requested local preparation was completed."
|
||||
old-state
|
||||
:style-binding-states))))
|
||||
(plist-get state :subject))
|
||||
(ebox-tree-node-subject old-node))))
|
||||
(ebox-tree-node-subject
|
||||
old-source-index old-node))))
|
||||
(source-stable-p
|
||||
(and old-node
|
||||
(equal parent-id old-parent-id)
|
||||
@ -2222,9 +2261,14 @@ node-to-subject table when the requested local preparation was completed."
|
||||
parent-style)))))
|
||||
(when (and node style (not (eq node old-node)))
|
||||
(cond
|
||||
(source-stable-p)
|
||||
((and source-stable-p
|
||||
(ebox-style--computed-projection-transferable-p
|
||||
old-node node style))
|
||||
(ebox-style--transfer-computed-projection
|
||||
old-node node style))
|
||||
((and (null (plist-get node :ebox-computed-style))
|
||||
(ebox-surface--closed-canonical-node-p node))
|
||||
(ebox-surface--closed-canonical-node-p
|
||||
new-source-index node))
|
||||
(ebox-style-apply-closed-inheritance node style))
|
||||
(t
|
||||
(ebox-style-apply-computed node style)))
|
||||
@ -2236,11 +2280,12 @@ node-to-subject table when the requested local preparation was completed."
|
||||
(dolist (node-id node-ids)
|
||||
(when (gethash node-id new-nodes)
|
||||
(resolve node-id))))
|
||||
subjects-by-node))))
|
||||
subjects-by-handle))))
|
||||
|
||||
(defun ebox-surface--node-style-binding-required-p (node)
|
||||
"Return non-nil when selector-free NODE needs its own style binding."
|
||||
(let ((declarations (ebox-style-node-declarations node)))
|
||||
(defun ebox-surface--node-style-binding-required-p (source-index node)
|
||||
"Return non-nil when NODE in SOURCE-INDEX needs its own style binding."
|
||||
(let ((declarations
|
||||
(ebox-tree-node-source-declarations source-index node)))
|
||||
(or (ebox-surface--style-consuming-node-p node)
|
||||
(cl-some (lambda (property)
|
||||
(plist-member declarations property))
|
||||
@ -2250,14 +2295,17 @@ node-to-subject table when the requested local preparation was completed."
|
||||
(string-prefix-p "--" (symbol-name property)))))))
|
||||
|
||||
(defun ebox-surface--apply-node-style
|
||||
(object node subject-table bindings-by-subject states-by-subject
|
||||
(source-index object node subject-table bindings-by-subject states-by-subject
|
||||
style-state-table
|
||||
stylesheet-signature selector-tree-token
|
||||
stylesheet-subject-local-p)
|
||||
"Compute and apply NODE style using retained OBJECT and SUBJECT-TABLE."
|
||||
(when-let* ((subject (gethash node subject-table)))
|
||||
(when-let* ((subject
|
||||
(ebox-source--subject-table-get
|
||||
subject-table (ebox-tree-node-source-handle node))))
|
||||
(if (and (hash-table-p ebox-surface--inline-style-value-cache)
|
||||
(not (ebox-surface--node-style-binding-required-p node)))
|
||||
(not (ebox-surface--node-style-binding-required-p
|
||||
source-index node)))
|
||||
(when-let* ((parent (ecss-subject-parent subject))
|
||||
(binding (gethash parent bindings-by-subject)))
|
||||
(puthash subject binding bindings-by-subject)
|
||||
@ -2269,13 +2317,15 @@ node-to-subject table when the requested local preparation was completed."
|
||||
(let ((style
|
||||
(tp-binding-read
|
||||
(ebox-surface--node-style-binding
|
||||
object node subject bindings-by-subject states-by-subject
|
||||
source-index object node subject
|
||||
bindings-by-subject states-by-subject
|
||||
style-state-table
|
||||
stylesheet-signature selector-tree-token
|
||||
stylesheet-subject-local-p))))
|
||||
(unless (equal style (plist-get node :ebox-computed-style))
|
||||
(if (and (null (plist-get node :ebox-computed-style))
|
||||
(ebox-surface--closed-canonical-node-p node))
|
||||
(ebox-surface--closed-canonical-node-p
|
||||
source-index node))
|
||||
(ebox-style-apply-closed-inheritance node style)
|
||||
(ebox-style-apply-computed node style)))))))
|
||||
|
||||
@ -2309,6 +2359,7 @@ dependencies, or any malformed index conservatively preserves root planning."
|
||||
(let ((objects (plist-get state :surface-node-object-table))
|
||||
(style-states (plist-get state :style-binding-states))
|
||||
(candidate-nodes (plist-get candidate-state :node-table))
|
||||
(candidate-source-index (plist-get candidate-state :source-index))
|
||||
(stylesheet-signature
|
||||
(let* ((cache ebox-incremental--allocated-slot-proof-cache)
|
||||
(missing (make-symbol "ebox-stylesheet-proof-missing"))
|
||||
@ -2337,7 +2388,8 @@ dependencies, or any malformed index conservatively preserves root planning."
|
||||
(plist-get style-state :subject)))
|
||||
(candidate-declarations
|
||||
(and candidate-node
|
||||
(ebox-style-node-declarations candidate-node)))
|
||||
(ebox-tree-node-source-declarations
|
||||
candidate-source-index candidate-node)))
|
||||
(locally-computed-p
|
||||
(and candidate-node
|
||||
(plist-get candidate-node
|
||||
@ -2355,9 +2407,11 @@ dependencies, or any malformed index conservatively preserves root planning."
|
||||
(ebox-style--paint-declarations-equivalent-p
|
||||
(plist-get style-state :declarations)
|
||||
candidate-declarations))
|
||||
(equal (ebox-tree-node-id candidate-node)
|
||||
(equal (ebox-tree-node-id
|
||||
candidate-source-index candidate-node)
|
||||
(ecss-subject-id subject))
|
||||
(equal (ebox-tree-node-classes candidate-node)
|
||||
(equal (ebox-tree-node-classes
|
||||
candidate-source-index candidate-node)
|
||||
(ecss-subject-classes subject))
|
||||
(equal (ebox-tree-metadata-string
|
||||
(ebox-tree-node-selector-type candidate-node))
|
||||
@ -2368,9 +2422,11 @@ dependencies, or any malformed index conservatively preserves root planning."
|
||||
(cons
|
||||
(substring (symbol-name (car attribute)) 1)
|
||||
(cdr attribute)))
|
||||
(ebox-tree-node-attributes candidate-node))
|
||||
(ebox-tree-node-attributes
|
||||
candidate-source-index candidate-node))
|
||||
(ecss-subject-attributes subject))
|
||||
(equal (ebox-tree-node-state candidate-node)
|
||||
(equal (ebox-tree-node-state
|
||||
candidate-source-index candidate-node)
|
||||
(ecss-subject-states subject))))
|
||||
(equal stylesheet-signature
|
||||
(plist-get style-state :stylesheet-signature))
|
||||
@ -2378,7 +2434,7 @@ dependencies, or any malformed index conservatively preserves root planning."
|
||||
dirty-set))))
|
||||
|
||||
(defun ebox-surface--ensure-node-tree
|
||||
(context parent node table subject-table bindings-by-subject
|
||||
(context parent node source-index table subject-table bindings-by-subject
|
||||
states-by-subject style-state-table stylesheet-signature
|
||||
selector-tree-token stylesheet-subject-local-p
|
||||
&optional parent-kind positional-p)
|
||||
@ -2386,14 +2442,16 @@ dependencies, or any malformed index conservatively preserves root planning."
|
||||
(ebox--ensure-node-id node)
|
||||
(let ((object
|
||||
(tp-object-ensure context parent
|
||||
(ebox-surface--node-key node positional-p)
|
||||
(ebox-surface--node-key
|
||||
source-index node positional-p)
|
||||
(ebox-surface--node-kind node))))
|
||||
(tp-object-retain context object)
|
||||
(plist-put node :surface-object object)
|
||||
(puthash node object table)
|
||||
(when subject-table
|
||||
(ebox-surface--apply-node-style
|
||||
object node subject-table bindings-by-subject states-by-subject
|
||||
source-index object node subject-table
|
||||
bindings-by-subject states-by-subject
|
||||
style-state-table
|
||||
stylesheet-signature selector-tree-token
|
||||
stylesheet-subject-local-p))
|
||||
@ -2402,7 +2460,8 @@ dependencies, or any malformed index conservatively preserves root planning."
|
||||
node
|
||||
(lambda (child)
|
||||
(ebox-surface--ensure-node-tree
|
||||
context object child table subject-table bindings-by-subject
|
||||
context object child source-index table subject-table
|
||||
bindings-by-subject
|
||||
states-by-subject style-state-table stylesheet-signature
|
||||
selector-tree-token stylesheet-subject-local-p
|
||||
child-kind (and (plist-get node :ebox-child-sequence) t)))))
|
||||
@ -3610,16 +3669,27 @@ box with wrapper chrome falls back to the retained wrapper renderer."
|
||||
(hash-table-p (plist-get state :region-node-table))
|
||||
(hash-table-p (plist-get state :region-box-count-table))
|
||||
(hash-table-p (plist-get state :region-box-table))
|
||||
(hash-table-p (plist-get state :host-ref-table))
|
||||
(hash-table-p
|
||||
(and (plist-get state :source-index)
|
||||
(ebox-source--index-host-ref-table-view
|
||||
(plist-get state :source-index))))
|
||||
(hash-table-p (plist-get state :range-ref-table))
|
||||
(vectorp (plist-get state :native-node-postorder)))))
|
||||
|
||||
(defun ebox-surface--finish-runtime-state (state)
|
||||
"Install or retain the prepared runtime indexes in candidate STATE."
|
||||
(unless (ebox-surface--runtime-index-ready-p state)
|
||||
(ebox--render-state-install-index
|
||||
state (ebox--runtime-index (plist-get state :root-node) t))
|
||||
(plist-put state :runtime-index-prepared-p t))
|
||||
(setq state
|
||||
(ebox--render-state-install-index
|
||||
state
|
||||
(ebox--runtime-index
|
||||
(plist-get state :root-node) t (plist-get state :source-index))))
|
||||
(setq state (plist-put state :runtime-index-prepared-p t)))
|
||||
(setq state
|
||||
(plist-put state :selector-index-stale-p
|
||||
(and (plist-get state :source-index)
|
||||
(ebox-source--index-derived-stale-p
|
||||
(plist-get state :source-index)))))
|
||||
state)
|
||||
|
||||
(defun ebox-surface--role-ids-from-properties (properties)
|
||||
@ -4872,9 +4942,28 @@ published runtime and must be consumed without clearing those shared nodes."
|
||||
(not source-path-copied-p))
|
||||
(ebox-surface--isolated-viewport-overrides state-overrides)
|
||||
state-overrides))
|
||||
(source-base-index
|
||||
(plist-get state-overrides :source-base-index))
|
||||
(source-index
|
||||
(or (plist-get state-overrides :source-index)
|
||||
(and source-base-index
|
||||
(ebox-tree-source-index
|
||||
root t ebox--render-root-parent-kind
|
||||
source-base-index))
|
||||
(and (eq root (plist-get previous-state :root-node))
|
||||
(plist-get previous-state :source-index))
|
||||
(ebox-tree-source-index
|
||||
root t ebox--render-root-parent-kind)))
|
||||
(_source-index
|
||||
(progn
|
||||
(setq state-overrides (copy-sequence state-overrides))
|
||||
(cl-remf state-overrides :source-base-index)
|
||||
(setq state-overrides
|
||||
(plist-put state-overrides :source-index source-index))))
|
||||
(inline-inheritance-required-p
|
||||
(unless (or scroll-fast-p retained-runtime-source-p)
|
||||
(ebox-surface--inline-inheritance-required-p root)))
|
||||
(ebox-surface--inline-inheritance-required-p
|
||||
root source-index)))
|
||||
(author-style-pending-p
|
||||
(> (ebox-tree-author-style-count root) 0))
|
||||
(cascade-required-p
|
||||
@ -4893,9 +4982,13 @@ published runtime and must be consumed without clearing those shared nodes."
|
||||
(ebox-surface--projection-start
|
||||
context root style-required-p projection-kind previous-state
|
||||
state-overrides)))
|
||||
(setq source-index (plist-get projection :source-index)
|
||||
state-overrides
|
||||
(plist-put state-overrides :source-index source-index))
|
||||
(unless (or source-isolated-p source-path-copied-p
|
||||
retained-runtime-source-p)
|
||||
(ebox-surface--reconcile-candidate root previous-state))
|
||||
(ebox-surface--reconcile-candidate
|
||||
root source-index previous-state))
|
||||
(let* ((context-axis-facts
|
||||
(and
|
||||
signals
|
||||
@ -4963,6 +5056,7 @@ published runtime and must be consumed without clearing those shared nodes."
|
||||
root previous-state state-overrides projection-kind
|
||||
stylesheet-active-p cascade-required-p axes viewport-width
|
||||
viewport-height display-signature scroll-offsets)))
|
||||
(plist-put state :source-index source-index)
|
||||
(when reuse-context-axes-p
|
||||
(plist-put state :viewport-dependent-node-ids
|
||||
(plist-get context-axis-facts
|
||||
@ -5040,13 +5134,15 @@ published runtime and must be consumed without clearing those shared nodes."
|
||||
span-output
|
||||
(setq projection
|
||||
(ebox-surface--complete-projection
|
||||
context root style-required-p projection))
|
||||
context root source-index
|
||||
style-required-p projection))
|
||||
(setq state
|
||||
(ebox-surface--projection-state
|
||||
root previous-state state-overrides nil
|
||||
stylesheet-active-p cascade-required-p axes
|
||||
viewport-width viewport-height display-signature
|
||||
scroll-offsets))
|
||||
(plist-put state :source-index source-index)
|
||||
(plist-put state :style-binding-states
|
||||
(plist-get projection :style-binding-states))
|
||||
(plist-put state :selector-tree-snapshot
|
||||
|
||||
778
ebox-tree.el
778
ebox-tree.el
@ -11,8 +11,12 @@
|
||||
|
||||
(require 'subr-x)
|
||||
(require 'ecss-selector)
|
||||
(require 'ebox-source)
|
||||
(require 'ebox-style)
|
||||
(require 'ebox-child-range)
|
||||
|
||||
(defvar ebox-tree--incoming-source-indexes nil
|
||||
"Transaction-local handle to incoming source-index map for one delta.")
|
||||
(require 'ebox-layout-config)
|
||||
|
||||
(declare-function ebox-get "ebox" (box property))
|
||||
@ -93,10 +97,24 @@ their private props encoding. Callers receive a detached value."
|
||||
((stringp value) value)
|
||||
(t (format "%s" value))))
|
||||
|
||||
(defun ebox-tree-node-id (node)
|
||||
"Return NODE's selector id string from `:id', or nil."
|
||||
(and (listp node)
|
||||
(ebox-tree-metadata-string (plist-get node :id))))
|
||||
(defun ebox-tree-node-source-handle (node)
|
||||
"Return canonical NODE's opaque source handle, or nil."
|
||||
(and (listp node) (plist-get node :ebox-source-handle)))
|
||||
|
||||
(defun ebox-tree-node-source-identity (node)
|
||||
"Return canonical NODE's opaque source identity, or nil."
|
||||
(when-let* ((handle (ebox-tree-node-source-handle node)))
|
||||
(ebox-source--handle-id-view handle)))
|
||||
|
||||
(defun ebox-tree-node-id (source-index node)
|
||||
"Return NODE's selector id string from SOURCE-INDEX, or nil."
|
||||
(when-let* ((handle (ebox-tree-node-source-handle node)))
|
||||
(ebox-source--index-id-view source-index handle)))
|
||||
|
||||
(defun ebox-tree-node-source-declarations (source-index node)
|
||||
"Return NODE's canonical declarations from SOURCE-INDEX, or nil."
|
||||
(when-let* ((handle (ebox-tree-node-source-handle node)))
|
||||
(ebox-source--index-declarations-view source-index handle)))
|
||||
|
||||
(defun ebox-tree--metadata-tokens (value)
|
||||
"Return VALUE as a list of normalized selector metadata tokens."
|
||||
@ -106,20 +124,25 @@ their private props encoding. Callers receive a detached value."
|
||||
(delq nil (mapcar #'ebox-tree-metadata-string value)))
|
||||
(t (list (ebox-tree-metadata-string value)))))
|
||||
|
||||
(defun ebox-tree-node-classes (node)
|
||||
"Return NODE's selector class strings from `:class'."
|
||||
(and (listp node)
|
||||
(ebox-tree--metadata-tokens (plist-get node :class))))
|
||||
(defun ebox-tree-node-classes (source-index node)
|
||||
"Return NODE's selector class strings from SOURCE-INDEX."
|
||||
(when-let* ((handle (ebox-tree-node-source-handle node)))
|
||||
(ebox-source--index-classes-view source-index handle)))
|
||||
|
||||
(defun ebox-tree-node-state (node)
|
||||
"Return NODE's selector state tokens from `:selector-state'."
|
||||
(and (listp node)
|
||||
(ebox-tree--metadata-tokens (plist-get node :selector-state))))
|
||||
(defun ebox-tree-node-state (source-index node)
|
||||
"Return NODE's selector state tokens from SOURCE-INDEX."
|
||||
(when-let* ((handle (ebox-tree-node-source-handle node)))
|
||||
(ebox-source--index-states-view source-index handle)))
|
||||
|
||||
(defun ebox-tree-node-key (node)
|
||||
"Return NODE's selector key string from `:key', or nil."
|
||||
(and (listp node)
|
||||
(ebox-tree-metadata-string (plist-get node :key))))
|
||||
(defun ebox-tree-node-author-key (source-index node)
|
||||
"Return NODE's raw author key from SOURCE-INDEX."
|
||||
(when-let* ((handle (ebox-tree-node-source-handle node)))
|
||||
(ebox-source--index-key-view source-index handle)))
|
||||
|
||||
(defun ebox-tree-node-key (source-index node)
|
||||
"Return NODE's selector key string from SOURCE-INDEX, or nil."
|
||||
(ebox-tree-metadata-string
|
||||
(ebox-tree-node-author-key source-index node)))
|
||||
|
||||
(defun ebox-tree--selector-attribute-key (key)
|
||||
"Return canonical keyword selector attribute KEY."
|
||||
@ -138,19 +161,18 @@ their private props encoding. Callers receive a detached value."
|
||||
((numberp value) (number-to-string value))
|
||||
(t (copy-tree value))))
|
||||
|
||||
(defun ebox-tree-node-attributes (node)
|
||||
"Return NODE's explicit selector attributes plus built-in id and key."
|
||||
(defun ebox-tree-node-attributes (source-index node)
|
||||
"Return NODE's selector attributes from SOURCE-INDEX plus id and key."
|
||||
(when (listp node)
|
||||
(let ((attributes
|
||||
(delq nil
|
||||
(list (when-let* ((id (ebox-tree-node-id node)))
|
||||
(list (when-let* ((id (ebox-tree-node-id source-index node)))
|
||||
(cons :id id))
|
||||
(when-let* ((key (ebox-tree-node-key node)))
|
||||
(when-let* ((key (ebox-tree-node-key source-index node)))
|
||||
(cons :key key))))))
|
||||
(dolist (attribute (plist-get node :selector-attributes))
|
||||
(unless (consp attribute)
|
||||
(error "Ebox selector attributes must be an alist: %S"
|
||||
(plist-get node :selector-attributes)))
|
||||
(dolist (attribute
|
||||
(when-let* ((handle (ebox-tree-node-source-handle node)))
|
||||
(ebox-source--index-attributes-view source-index handle)))
|
||||
(push (cons (ebox-tree--selector-attribute-key (car attribute))
|
||||
(ebox-tree--selector-attribute-value (cdr attribute)))
|
||||
attributes))
|
||||
@ -345,7 +367,8 @@ NODE must already be a shallow copy. Return the possibly updated plist."
|
||||
(plist-put node property replacement)))
|
||||
node))
|
||||
|
||||
(defun ebox-tree-copy-with-direct-child-replacements (node replacements)
|
||||
(defun ebox-tree-copy-with-direct-child-replacements
|
||||
(node replacements &optional key-function)
|
||||
"Return a shallow copy of NODE with direct child REPLACEMENTS applied.
|
||||
|
||||
REPLACEMENTS is an alist of (OLD-CHILD . NEW-CHILD) pairs. OLD-CHILD is
|
||||
@ -391,25 +414,7 @@ still copied while every nested value remains shared."
|
||||
(dolist (replacement replacements)
|
||||
(let* ((old (car replacement)) (new (cdr replacement))
|
||||
(location (and old (plist-get old :ebox-sequence-location)))
|
||||
(key (and old (plist-get old :key)))
|
||||
(key-location
|
||||
(and (null location) key
|
||||
(ebox-child-range--hash-lookup
|
||||
(ebox-child-range--sequence-key-root updated) key
|
||||
(funcall
|
||||
(ebox-child-range--sequence-hash-function updated)
|
||||
key)))))
|
||||
(when key-location
|
||||
(let* ((segment (ebox-child-range--segment-at
|
||||
updated (car key-location)))
|
||||
(candidate (aref
|
||||
(ebox-child-range--segment-payload segment)
|
||||
(cdr key-location))))
|
||||
(when (eq candidate old)
|
||||
(setq location
|
||||
(list :parent-node-id (plist-get node :node-id)
|
||||
:segment-index (car key-location)
|
||||
:offset (cdr key-location))))))
|
||||
old-key)
|
||||
(unless location
|
||||
(unless identity-locations
|
||||
(setq identity-locations (make-hash-table :test #'eq))
|
||||
@ -429,6 +434,11 @@ still copied while every nested value remains shared."
|
||||
(when (and location
|
||||
(equal (plist-get location :parent-node-id)
|
||||
(plist-get node :node-id)))
|
||||
(let* ((segment
|
||||
(ebox-child-range--segment-at
|
||||
updated (plist-get location :segment-index)))
|
||||
(keys (ebox-child-range--segment-keys segment)))
|
||||
(setq old-key (aref keys (plist-get location :offset))))
|
||||
(cl-incf style-delta
|
||||
(- (ebox-tree-author-style-count new)
|
||||
(ebox-tree-author-style-count old)))
|
||||
@ -436,22 +446,20 @@ still copied while every nested value remains shared."
|
||||
(setq updated
|
||||
(ebox-child-range--replace-item-at
|
||||
updated (plist-get location :segment-index)
|
||||
(plist-get location :offset) old new)
|
||||
(plist-get location :offset) old new
|
||||
(if key-function
|
||||
(funcall key-function new)
|
||||
(if (or (null old-key)
|
||||
(equal
|
||||
(ebox-tree-node-source-identity old)
|
||||
(ebox-tree-node-source-identity new)))
|
||||
old-key
|
||||
(error
|
||||
"Ebox Range replacement requires source key facts"))))
|
||||
changed t)
|
||||
(setq fallback t)))))
|
||||
(when fallback
|
||||
(let (segments)
|
||||
(dotimes (index (ebox-child-range--sequence-count sequence))
|
||||
(let* ((segment (ebox-child-range--segment-at sequence index))
|
||||
(items (append
|
||||
(ebox-child-range--segment-payload segment) nil)))
|
||||
(push (cons (ebox-child-range--segment-ref segment)
|
||||
(ebox-tree--replace-direct-child-list
|
||||
items replacements))
|
||||
segments)))
|
||||
(setq updated (ebox-child-range--build
|
||||
(nreverse segments) nil t)
|
||||
changed t)))
|
||||
(error "Ebox Range item removal requires a Range transaction"))
|
||||
(when changed
|
||||
(setq copy
|
||||
(if (plist-member copy :children)
|
||||
@ -469,7 +477,9 @@ still copied while every nested value remains shared."
|
||||
The returned tree owns distinct node plists and child lists. Values such as
|
||||
keymaps, callbacks, Host references, and surface-property payloads retain their
|
||||
original object identity."
|
||||
(cl-labels
|
||||
(let ((seen (make-hash-table :test #'eq))
|
||||
(active (make-hash-table :test #'eq)))
|
||||
(cl-labels
|
||||
((without (plist key)
|
||||
(cl-loop for (property value) on plist by #'cddr
|
||||
unless (eq property key) append (list property value)))
|
||||
@ -477,6 +487,13 @@ original object identity."
|
||||
(node)
|
||||
(if (or (not (listp node)) (stringp node))
|
||||
node
|
||||
(when (gethash node active)
|
||||
(error "Ebox declarative tree contains a cycle"))
|
||||
(when (gethash node seen)
|
||||
(error "Ebox declarative tree reuses one node object"))
|
||||
(puthash node t seen)
|
||||
(puthash node t active)
|
||||
(prog1
|
||||
(let* ((type (plist-get node :ebox-type))
|
||||
(material-p (or (memq type '(concat stack flex grid))
|
||||
(plist-get node :ebox-child-sequence)))
|
||||
@ -485,12 +502,16 @@ original object identity."
|
||||
(plist-get node :ebox-child-sequence)))
|
||||
(has-range (cl-some #'ebox-child-range--descriptor-p raw)))
|
||||
(if (or has-range sequence)
|
||||
(let ((copy (copy-sequence node)) segments)
|
||||
(let ((copy (copy-sequence node)) segments segment-keys)
|
||||
(when has-range
|
||||
(error "Ebox declarative Range must be normalized before copy"))
|
||||
(if sequence
|
||||
(dotimes (index
|
||||
(ebox-child-range--sequence-count sequence))
|
||||
(let ((segment
|
||||
(ebox-child-range--segment-at sequence index)))
|
||||
(push (ebox-child-range--segment-keys segment)
|
||||
segment-keys)
|
||||
(push
|
||||
(cons (ebox-child-range--segment-ref segment)
|
||||
(mapcar #'copy-node
|
||||
@ -507,7 +528,8 @@ original object identity."
|
||||
(ebox-child-range--descriptor-items entry)))
|
||||
segments)
|
||||
(push (cons nil (list (copy-node entry))) segments))))
|
||||
(setq segments (nreverse segments))
|
||||
(setq segments (nreverse segments)
|
||||
segment-keys (nreverse segment-keys))
|
||||
(pcase type
|
||||
((or 'flex 'grid)
|
||||
(when-let* ((box (plist-get node :box)))
|
||||
@ -518,7 +540,8 @@ original object identity."
|
||||
(setq copy (without (without copy :top) :bottom))))
|
||||
(ebox-tree--put-child-sequence
|
||||
copy
|
||||
(ebox-child-range--build segments nil t)
|
||||
(ebox-child-range--build
|
||||
segments nil nil t segment-keys)
|
||||
(cl-mapcan (lambda (segment)
|
||||
(copy-sequence (cdr segment)))
|
||||
segments)))
|
||||
@ -528,8 +551,9 @@ original object identity."
|
||||
(mapcar (lambda (child)
|
||||
(cons child (copy-node child)))
|
||||
(ebox-tree--children-raw node)))))
|
||||
copy))))))
|
||||
(copy-node root)))
|
||||
copy)))
|
||||
(remhash node active)))))
|
||||
(copy-node root))))
|
||||
|
||||
(defun ebox-tree--runtime-identity-node-shell
|
||||
(node node-id-set region-id-set)
|
||||
@ -542,8 +566,8 @@ Record its identities in NODE-ID-SET and REGION-ID-SET."
|
||||
(error "Ebox runtime identity snapshot found a node without identity"))
|
||||
(puthash node-id t node-id-set)
|
||||
(setq shell (list :ebox-type type :node-id node-id))
|
||||
(when (plist-member node :key)
|
||||
(setq shell (plist-put shell :key (plist-get node :key))))
|
||||
(when-let* ((handle (ebox-tree-node-source-handle node)))
|
||||
(setq shell (plist-put shell :ebox-source-handle handle)))
|
||||
(when (eq type 'box)
|
||||
(let ((region-id (plist-get node :region-id)))
|
||||
(unless region-id
|
||||
@ -617,33 +641,12 @@ Renderable child nodes are assigned stable runtime ids before returning."
|
||||
(ebox--ensure-node-id child)))
|
||||
children))
|
||||
|
||||
(defun ebox-tree-validate-host-refs (root)
|
||||
"Reject duplicate non-nil `:host-ref' values below ROOT and return ROOT.
|
||||
Host references are root-global opaque metadata and are compared with `equal'."
|
||||
(let ((host-refs (make-hash-table :test 'equal))
|
||||
(seen (make-hash-table :test 'eq))
|
||||
(active (make-hash-table :test 'eq)))
|
||||
(cl-labels
|
||||
((visit (node)
|
||||
(when (and (listp node) (not (stringp node)))
|
||||
;; Let the complete declarative validator report cycles. This
|
||||
;; focused preflight is also used by initial buffer mounting,
|
||||
;; where it must remain finite for malformed graphs.
|
||||
(unless (gethash node active)
|
||||
(when-let* ((host-ref (plist-get node :host-ref)))
|
||||
(let ((count (hash-table-count host-refs)))
|
||||
(puthash host-ref t host-refs)
|
||||
(when (= count (hash-table-count host-refs))
|
||||
(error
|
||||
"Ebox declarative tree uses duplicate host ref %S"
|
||||
host-ref))))
|
||||
(unless (gethash node seen)
|
||||
(puthash node t seen)
|
||||
(puthash node t active)
|
||||
(ebox-tree-for-each-direct-child node #'visit)
|
||||
(remhash node active))))))
|
||||
(visit root))
|
||||
root))
|
||||
(defun ebox-tree-validate-host-refs (root &optional source-index)
|
||||
"Validate ROOT's unique opaque source identities and return ROOT.
|
||||
SOURCE-INDEX owns Host lookup identity. Building it rejects duplicate source
|
||||
handles, reused node objects, and cycles without consulting node metadata."
|
||||
(or source-index (ebox-tree-source-index root))
|
||||
root)
|
||||
|
||||
(defconst ebox-tree--common-participation-keys
|
||||
'(:order :align-self)
|
||||
@ -668,27 +671,31 @@ Host references are root-global opaque metadata and are compared with `equal'."
|
||||
(and style
|
||||
(ebox-style--specified-property-p style (ebox-style-schema-id key))))
|
||||
|
||||
(defun ebox-tree--declarations-specify-participation-p (node key)
|
||||
"Return non-nil when NODE author declarations specify participation KEY."
|
||||
(defun ebox-tree--declarations-specify-participation-p
|
||||
(source-index node key)
|
||||
"Return non-nil when NODE in SOURCE-INDEX specifies participation KEY."
|
||||
(and (listp node)
|
||||
(plist-member (ebox-style-node-declarations node)
|
||||
(plist-member (ebox-style-node-declarations source-index node)
|
||||
(ebox-style-schema-id key))))
|
||||
|
||||
(defun ebox-tree--author-specifies-participation-p (node key)
|
||||
"Return non-nil when NODE's author source specifies participation KEY."
|
||||
(or (ebox-tree--declarations-specify-participation-p node key)
|
||||
(defun ebox-tree--author-specifies-participation-p
|
||||
(source-index node key)
|
||||
"Return non-nil when NODE's SOURCE-INDEX specifies participation KEY."
|
||||
(or (ebox-tree--declarations-specify-participation-p
|
||||
source-index node key)
|
||||
(and (null (plist-get node :ebox-computed-style))
|
||||
(plist-member node key))))
|
||||
|
||||
(defun ebox-tree--node-participation-keys (node source)
|
||||
"Return NODE participation keys from AUTHOR or COMPUTED SOURCE."
|
||||
(defun ebox-tree--node-participation-keys (source-index node source)
|
||||
"Return NODE participation keys from SOURCE-INDEX or COMPUTED SOURCE."
|
||||
(let ((style (plist-get node :ebox-computed-style))
|
||||
keys)
|
||||
(dolist (key ebox-tree--participation-keys)
|
||||
(when
|
||||
(pcase source
|
||||
('author
|
||||
(ebox-tree--author-specifies-participation-p node key))
|
||||
(ebox-tree--author-specifies-participation-p
|
||||
source-index node key))
|
||||
('computed
|
||||
(and (plist-member node key)
|
||||
(ebox-tree--style-specifies-participation-p style key)))
|
||||
@ -705,9 +712,11 @@ Host references are root-global opaque metadata and are compared with `equal'."
|
||||
(kind kind))))
|
||||
|
||||
(defun ebox-tree--validate-node-parent-participation
|
||||
(node parent-kind source)
|
||||
(node parent-kind source &optional source-index)
|
||||
"Validate NODE participation under PARENT-KIND from AUTHOR or COMPUTED SOURCE."
|
||||
(let ((keys (ebox-tree--node-participation-keys node source)))
|
||||
(let ((keys
|
||||
(ebox-tree--node-participation-keys
|
||||
source-index node source)))
|
||||
(when keys
|
||||
(when (eq (plist-get node :ebox-kind) 'text)
|
||||
(error "Ebox Text cannot carry participation properties: %S" keys))
|
||||
@ -734,7 +743,7 @@ Host references are root-global opaque metadata and are compared with `equal'."
|
||||
(and parent (ebox-tree--child-layout-kind parent nil))))
|
||||
|
||||
(defun ebox-tree-validate-indexed-participation
|
||||
(node-table parent-table node-ids &optional source)
|
||||
(node-table parent-table node-ids &optional source source-index)
|
||||
"Validate NODE-IDS in indexed final tree using AUTHOR or COMPUTED SOURCE."
|
||||
(setq source (or source 'author))
|
||||
(dolist (node-id (delete-dups (copy-sequence node-ids)))
|
||||
@ -742,92 +751,20 @@ Host references are root-global opaque metadata and are compared with `equal'."
|
||||
(ebox-tree--validate-node-parent-participation
|
||||
node
|
||||
(ebox-tree--indexed-parent-context node-id node-table parent-table)
|
||||
source)))
|
||||
source source-index)))
|
||||
node-table)
|
||||
|
||||
(defun ebox-tree-validate-declarative-root
|
||||
(root &optional validate-participation-p root-parent-kind)
|
||||
(root &optional validate-participation-p root-parent-kind source-index)
|
||||
"Validate ROOT for one declarative runtime commit.
|
||||
|
||||
The runtime tree must be a proper tree: a node object cannot appear in two
|
||||
locations, cycles are rejected, explicit sibling keys must be unique, and
|
||||
non-nil host references must be unique across the root. Keys are sibling-local
|
||||
and all explicit identities are compared with `equal'. Return ROOT on success."
|
||||
(when (ebox-child-range--descriptor-p root)
|
||||
(error "Ebox child Range descriptor cannot be the root"))
|
||||
(ebox-tree-validate-host-refs root)
|
||||
(let ((seen (make-hash-table :test 'eq))
|
||||
(active (make-hash-table :test 'eq))
|
||||
(range-refs (make-hash-table :test 'equal)))
|
||||
(cl-labels
|
||||
((visit (node parent-kind)
|
||||
(when (and (listp node) (not (stringp node)))
|
||||
(when (gethash node active)
|
||||
(error "Ebox declarative tree contains a cycle"))
|
||||
(when (gethash node seen)
|
||||
(error "Ebox declarative tree reuses one node object"))
|
||||
(puthash node t seen)
|
||||
(puthash node t active)
|
||||
(when validate-participation-p
|
||||
(ebox-tree--validate-node-parent-participation
|
||||
node parent-kind 'author))
|
||||
(let* ((keys (make-hash-table :test 'equal))
|
||||
(type (plist-get node :ebox-type))
|
||||
(raw (and (memq type '(box concat stack flex grid))
|
||||
(plist-get node :children)))
|
||||
children)
|
||||
(when (and raw (not (proper-list-p raw)))
|
||||
(error "Ebox material children must be a proper list"))
|
||||
(when-let* ((sequence (plist-get node :ebox-child-sequence)))
|
||||
(dotimes (index
|
||||
(ebox-child-range--sequence-count sequence))
|
||||
(when-let* ((ref
|
||||
(ebox-child-range--segment-ref
|
||||
(ebox-child-range--segment-at sequence index))))
|
||||
(when (gethash ref range-refs)
|
||||
(error "Ebox child Range ref is not unique: %S" ref))
|
||||
(puthash ref t range-refs))))
|
||||
(if (plist-get node :ebox-child-sequence)
|
||||
(ebox-tree-for-each-direct-child
|
||||
node (lambda (child) (push child children)))
|
||||
(dolist (entry (ebox-tree--children-raw node))
|
||||
(if (ebox-child-range--descriptor-p entry)
|
||||
(progn
|
||||
(unless (and (proper-list-p raw) (memq entry raw))
|
||||
(error "Ebox child Range descriptor is invalid in a scalar slot"))
|
||||
(let ((ref (ebox-child-range--descriptor-ref entry))
|
||||
(items (ebox-child-range--descriptor-items entry)))
|
||||
(unless (and ref (proper-list-p items))
|
||||
(error "Ebox child Range descriptor is invalid"))
|
||||
(when (gethash ref range-refs)
|
||||
(error "Ebox child Range ref is not unique: %S" ref))
|
||||
(puthash ref t range-refs)
|
||||
(dolist (item items)
|
||||
(when (ebox-child-range--descriptor-p item)
|
||||
(error "Nested child Range descriptors are reserved"))
|
||||
(unless (and (listp item) (not (stringp item)))
|
||||
(error "Ebox child Range item must be a node"))
|
||||
(push item children))))
|
||||
(push entry children))))
|
||||
(dolist (child (nreverse children))
|
||||
(when (and (eq (plist-get node :ebox-kind) 'box)
|
||||
(not (memq (plist-get child :ebox-kind)
|
||||
'(text box))))
|
||||
(error
|
||||
"Canonical Ebox Box child must be Text or Box: %S"
|
||||
child))
|
||||
(when-let* ((key (and (listp child)
|
||||
(not (stringp child))
|
||||
(plist-get child :key))))
|
||||
(when (gethash key keys)
|
||||
(error "Ebox declarative siblings use duplicate key %S"
|
||||
key))
|
||||
(puthash key t keys))
|
||||
(visit child
|
||||
(ebox-tree--child-layout-kind node parent-kind)))
|
||||
(remhash node active)))))
|
||||
(visit root root-parent-kind))
|
||||
root))
|
||||
(ebox-tree-source-index
|
||||
root validate-participation-p root-parent-kind source-index)
|
||||
root)
|
||||
|
||||
(defun ebox-tree-clear-runtime-identities (root)
|
||||
"Clear Ebox-owned runtime identity fields below ROOT and return ROOT."
|
||||
@ -861,6 +798,7 @@ and all explicit identities are compared with `equal'. Return ROOT on success."
|
||||
(append ebox-tree--runtime-source-keys
|
||||
ebox-tree--child-source-keys
|
||||
'(:ebox-style-overrides
|
||||
:ebox-source-handle
|
||||
:ebox-grid-config
|
||||
:ebox-computed-style :ebox-style-wrapper
|
||||
:ebox-style-generated-wrapper
|
||||
@ -873,7 +811,7 @@ Computed styles, projection counters, duplicate Text storage, and legacy wrapper
|
||||
overrides are derived side state.")
|
||||
|
||||
(defconst ebox-tree-metadata-source-keys
|
||||
'(:key :id :class :host-ref)
|
||||
'(:key :id :class :selector-attributes :selector-state)
|
||||
"Declarative metadata keys that do not directly change rendered text.")
|
||||
|
||||
(defun ebox-tree--plist-keys (plist)
|
||||
@ -1025,13 +963,14 @@ candidate runtime index agree."
|
||||
(plist-put new :node-id (ebox--next-runtime-node-id)))
|
||||
new)
|
||||
|
||||
(defun ebox-tree-reconcile-runtime (old new)
|
||||
"Transfer stable keyed and positional runtime identity from OLD to NEW."
|
||||
(defun ebox-tree-reconcile-runtime
|
||||
(old old-source-index new new-source-index)
|
||||
"Transfer runtime identity from OLD to NEW using their source indexes."
|
||||
(cl-labels
|
||||
((node-key
|
||||
(node)
|
||||
(when (and (listp node) (plist-member node :key))
|
||||
(plist-get node :key)))
|
||||
(source-index node)
|
||||
(and node
|
||||
(ebox-tree-node-author-key source-index node)))
|
||||
(same-type-p
|
||||
(old-node new-node)
|
||||
(and (listp old-node)
|
||||
@ -1039,18 +978,18 @@ candidate runtime index agree."
|
||||
(eq (plist-get old-node :ebox-type)
|
||||
(plist-get new-node :ebox-type))))
|
||||
(keyed-children
|
||||
(children)
|
||||
(source-index children)
|
||||
(let ((table (make-hash-table :test 'equal)))
|
||||
(dolist (child children table)
|
||||
(when-let* ((key (node-key child)))
|
||||
(when-let* ((key (node-key source-index child)))
|
||||
(puthash key child table)))))
|
||||
(match-child
|
||||
(old-children old-keyed new-child index)
|
||||
(if-let* ((key (node-key new-child)))
|
||||
(if-let* ((key (node-key new-source-index new-child)))
|
||||
(let ((old-child (gethash key old-keyed)))
|
||||
(and (same-type-p old-child new-child) old-child))
|
||||
(let ((old-child (nth index old-children)))
|
||||
(and (not (node-key old-child))
|
||||
(and (not (node-key old-source-index old-child))
|
||||
(same-type-p old-child new-child)
|
||||
old-child))))
|
||||
(reconcile
|
||||
@ -1060,7 +999,8 @@ candidate runtime index agree."
|
||||
(let* ((old-children
|
||||
(and old-node (ebox-tree--children-raw old-node)))
|
||||
(new-children (ebox-tree--children-raw new-node))
|
||||
(old-keyed (keyed-children old-children)))
|
||||
(old-keyed
|
||||
(keyed-children old-source-index old-children)))
|
||||
(cl-loop for new-child in new-children
|
||||
for index from 0
|
||||
do (reconcile
|
||||
@ -1104,81 +1044,409 @@ candidate runtime index agree."
|
||||
('grid (ebox-tree-layout-children node))
|
||||
(_ (ebox-tree-children node))))
|
||||
|
||||
(defun ebox-tree-selector-index (root)
|
||||
"Return id, class, and type indexes over ROOT's logical selector tree."
|
||||
(let ((id-table (make-hash-table :test #'equal))
|
||||
(class-table (make-hash-table :test #'equal))
|
||||
(type-table (make-hash-table :test #'eq)))
|
||||
(cl-labels
|
||||
((prepend (table key entry)
|
||||
(puthash key (cons entry (gethash key table)) table))
|
||||
(visit (node path)
|
||||
(when (and (listp node) (not (stringp node)))
|
||||
(let* ((current-path (append path (list node)))
|
||||
(entry (cons node current-path)))
|
||||
(when-let* ((id (ebox-tree-node-id node)))
|
||||
(prepend id-table id entry))
|
||||
(dolist (class (ebox-tree-node-classes node))
|
||||
(prepend class-table class entry))
|
||||
(when-let* ((type (ebox-tree-node-selector-type node)))
|
||||
(prepend type-table type entry))
|
||||
(dolist (child (ebox-tree-semantic-children node))
|
||||
(visit child current-path))))))
|
||||
(visit root nil))
|
||||
(dolist (table (list id-table class-table type-table))
|
||||
(maphash (lambda (key entries)
|
||||
(puthash key (nreverse entries) table))
|
||||
table))
|
||||
(list :selector-id-table id-table
|
||||
:selector-class-table class-table
|
||||
:selector-type-table type-table)))
|
||||
|
||||
(defun ebox-tree-node-subject (node)
|
||||
"Return a detached ECSS selector subject representing NODE."
|
||||
(defun ebox-tree--subject-from-record (node record)
|
||||
"Return one ECSS selector subject for NODE and source RECORD."
|
||||
(ecss-subject-create
|
||||
:type (ebox-tree-metadata-string (ebox-tree-node-selector-type node))
|
||||
:id (ebox-tree-node-id node)
|
||||
:classes (ebox-tree-node-classes node)
|
||||
:id (ebox-source-record-id record)
|
||||
:classes (ebox-source-record-classes record)
|
||||
:attributes
|
||||
(mapcar (lambda (attribute)
|
||||
(cons (substring (symbol-name (car attribute)) 1)
|
||||
(cdr attribute)))
|
||||
(ebox-tree-node-attributes node))
|
||||
:states (ebox-tree-node-state node)))
|
||||
(let ((attributes
|
||||
(delq
|
||||
nil
|
||||
(list
|
||||
(when-let* ((id (ebox-source-record-id record)))
|
||||
(cons :id id))
|
||||
(when-let* ((key
|
||||
(ebox-tree-metadata-string
|
||||
(ebox-source-record-key record))))
|
||||
(cons :key key))))))
|
||||
(dolist (attribute (ebox-source-record-attributes record))
|
||||
(push (cons
|
||||
(ebox-tree--selector-attribute-key (car attribute))
|
||||
(ebox-tree--selector-attribute-value (cdr attribute)))
|
||||
attributes))
|
||||
(nreverse attributes)))
|
||||
:states (ebox-source-record-states record)))
|
||||
|
||||
(defun ebox-tree-subject-index (root)
|
||||
"Return ECSS subjects and semantic node paths below ROOT in document order."
|
||||
(let ((node-subject-table (make-hash-table :test #'eq))
|
||||
(defun ebox-tree-node-subject (source-index node)
|
||||
"Return SOURCE-INDEX's retained ECSS selector subject for NODE."
|
||||
(or (ebox-source--index-node-subject source-index node)
|
||||
(error "Ebox selector node is absent from its source index")))
|
||||
|
||||
(defun ebox-tree-source-builder-snapshot (builder roots)
|
||||
"Return a sealed source-index slice for canonical ROOTS in open BUILDER."
|
||||
(unless (proper-list-p roots)
|
||||
(signal 'wrong-type-argument (list 'proper-list-p roots)))
|
||||
(let ((slice (ebox-source-builder-create))
|
||||
(seen (make-hash-table :test #'eq)))
|
||||
(cl-labels
|
||||
((visit (node)
|
||||
(when (and (listp node) (not (stringp node)))
|
||||
(when-let* ((handle (ebox-tree-node-source-handle node)))
|
||||
(unless (gethash handle seen)
|
||||
(puthash handle t seen)
|
||||
(ebox-source--builder-add-record
|
||||
slice handle
|
||||
(or (ebox-source--builder-record builder handle)
|
||||
(error "Ebox source builder does not own canonical node")))))
|
||||
(ebox-tree-for-each-direct-child node #'visit))))
|
||||
(dolist (root roots) (visit root)))
|
||||
(ebox-source-builder-finish slice)))
|
||||
|
||||
(defun ebox-tree-source-index
|
||||
(root &optional validate-participation-p root-parent-kind base-source-index
|
||||
allow-unreferenced-sources-p)
|
||||
"Build one immutable source and selector index for canonical ROOT.
|
||||
When VALIDATE-PARTICIPATION-P is non-nil, validate ROOT-PARENT-KIND during the
|
||||
same traversal. Topology, Range addresses, source identities, and sibling
|
||||
keys are always validated. BASE-SOURCE-INDEX, when non-nil, is the fact-only
|
||||
index transported with the canonical input; records are never recovered from
|
||||
opaque handles on that path."
|
||||
(when (ebox-child-range--descriptor-p root)
|
||||
(error "Ebox child Range descriptor cannot be the root"))
|
||||
(let ((subjects (make-hash-table :test #'equal))
|
||||
(node-subjects (make-hash-table :test #'eq))
|
||||
(id-table (make-hash-table :test #'equal))
|
||||
(class-table (make-hash-table :test #'equal))
|
||||
(type-table (make-hash-table :test #'eq))
|
||||
(seen (make-hash-table :test #'eq))
|
||||
(active (make-hash-table :test #'eq))
|
||||
(range-refs (make-hash-table :test #'equal))
|
||||
(used-handles (make-hash-table :test #'eq))
|
||||
entries)
|
||||
(cl-labels
|
||||
((visit (node path)
|
||||
((prepend (table key entry)
|
||||
(puthash key (cons entry (gethash key table)) table))
|
||||
(record-for (node)
|
||||
(when-let* ((handle (ebox-tree-node-source-handle node)))
|
||||
(or (and base-source-index
|
||||
(ebox-source--index-record base-source-index handle))
|
||||
(error "Ebox canonical source is absent from its input index"))))
|
||||
(record-key (node)
|
||||
(when-let* ((record (record-for node)))
|
||||
(ebox-source-record-key record)))
|
||||
(record-participation-keys (node record)
|
||||
(let ((declarations (ebox-source-record-declarations record)) keys)
|
||||
(dolist (key ebox-tree--participation-keys)
|
||||
(when (or (plist-member declarations (ebox-style-schema-id key))
|
||||
(and (null (plist-get node :ebox-computed-style))
|
||||
(plist-member node key)))
|
||||
(push key keys)))
|
||||
(nreverse keys)))
|
||||
(validate-participation (node record parent-kind)
|
||||
(when validate-participation-p
|
||||
(let ((keys (record-participation-keys node record)))
|
||||
(when keys
|
||||
(when (eq (plist-get node :ebox-kind) 'text)
|
||||
(error "Ebox Text cannot carry participation properties: %S"
|
||||
keys))
|
||||
(pcase parent-kind
|
||||
('flex
|
||||
(when (cl-intersection
|
||||
keys ebox-tree--grid-participation-keys :test #'eq)
|
||||
(error
|
||||
"Grid participation requires a direct Grid parent: %S"
|
||||
keys)))
|
||||
('grid
|
||||
(when (cl-intersection
|
||||
keys ebox-tree--flex-participation-keys :test #'eq)
|
||||
(error
|
||||
"Flex participation requires a direct Flex parent: %S"
|
||||
keys)))
|
||||
(_
|
||||
(error
|
||||
"Ebox participation requires a Flex or Grid parent: %S"
|
||||
keys)))))))
|
||||
(material-children (node)
|
||||
(let* ((type (plist-get node :ebox-type))
|
||||
(raw (and (memq type '(box concat stack flex grid))
|
||||
(plist-get node :children)))
|
||||
children)
|
||||
(when (and raw (not (proper-list-p raw)))
|
||||
(error "Ebox material children must be a proper list"))
|
||||
(if-let* ((sequence (plist-get node :ebox-child-sequence)))
|
||||
(progn
|
||||
(dotimes (index (ebox-child-range--sequence-count sequence))
|
||||
(when-let* ((ref
|
||||
(ebox-child-range--segment-ref
|
||||
(ebox-child-range--segment-at
|
||||
sequence index))))
|
||||
(when (gethash ref range-refs)
|
||||
(error "Ebox child Range ref is not unique: %S" ref))
|
||||
(puthash ref t range-refs)))
|
||||
(ebox-tree-for-each-direct-child
|
||||
node (lambda (child) (push child children))))
|
||||
(dolist (entry (ebox-tree--children-raw node))
|
||||
(if (ebox-child-range--descriptor-p entry)
|
||||
(progn
|
||||
(unless (and (proper-list-p raw) (memq entry raw))
|
||||
(error
|
||||
"Ebox child Range descriptor is invalid in a scalar slot"))
|
||||
(let ((ref (ebox-child-range--descriptor-ref entry))
|
||||
(items (ebox-child-range--descriptor-items entry)))
|
||||
(unless (and ref (proper-list-p items))
|
||||
(error "Ebox child Range descriptor is invalid"))
|
||||
(when (gethash ref range-refs)
|
||||
(error "Ebox child Range ref is not unique: %S" ref))
|
||||
(puthash ref t range-refs)
|
||||
(dolist (item items)
|
||||
(when (ebox-child-range--descriptor-p item)
|
||||
(error "Nested child Range descriptors are reserved"))
|
||||
(unless (and (listp item) (not (stringp item)))
|
||||
(error "Ebox child Range item must be a node"))
|
||||
(push item children))))
|
||||
(push entry children))))
|
||||
(nreverse children)))
|
||||
(visit-children (node path parent-kind)
|
||||
(let ((keys (make-hash-table :test #'equal)) result)
|
||||
(dolist (child (material-children node))
|
||||
(when (and (eq (plist-get node :ebox-kind) 'box)
|
||||
(not (memq (plist-get child :ebox-kind) '(text box))))
|
||||
(error "Canonical Ebox Box child must be Text or Box: %S"
|
||||
child))
|
||||
(when-let* ((key (record-key child)))
|
||||
(when (gethash key keys)
|
||||
(error "Ebox declarative siblings use duplicate key %S" key))
|
||||
(puthash key t keys))
|
||||
(setq result
|
||||
(nconc
|
||||
result
|
||||
(visit child path
|
||||
(ebox-tree--child-layout-kind node parent-kind)))))
|
||||
result))
|
||||
(visit (node path parent-kind)
|
||||
(when (and (listp node) (not (stringp node)))
|
||||
(let* ((subject (ebox-tree-node-subject node))
|
||||
(when (gethash node active)
|
||||
(error "Ebox declarative tree contains a cycle"))
|
||||
(when (gethash node seen)
|
||||
(error "Ebox declarative tree reuses one node object"))
|
||||
(puthash node t seen)
|
||||
(puthash node t active)
|
||||
(let* ((handle (plist-get node :ebox-source-handle))
|
||||
(current-path (append path (list node))))
|
||||
(puthash node subject node-subject-table)
|
||||
(push (list :node node :path current-path :subject subject)
|
||||
entries)
|
||||
(ecss-subject-set-children
|
||||
subject
|
||||
(delq nil
|
||||
(mapcar (lambda (child) (visit child current-path))
|
||||
(ebox-tree-semantic-children node))))
|
||||
subject))))
|
||||
(let ((root-subject (visit root nil)))
|
||||
(list :root-subject root-subject
|
||||
:entries (nreverse entries)
|
||||
:node-subject-table node-subject-table)))))
|
||||
(if (null handle)
|
||||
(prog1
|
||||
(visit-children node current-path parent-kind)
|
||||
(remhash node active))
|
||||
(let* ((record (record-for node))
|
||||
(identity (ebox-source--handle-id-view handle))
|
||||
(subject
|
||||
(ebox-tree--subject-from-record node record))
|
||||
(entry (cons node current-path)))
|
||||
(puthash handle t used-handles)
|
||||
(validate-participation node record parent-kind)
|
||||
(when (gethash identity subjects)
|
||||
(error "Ebox source identity is not unique: %S" identity))
|
||||
(puthash identity subject subjects)
|
||||
(puthash handle subject node-subjects)
|
||||
(push (list :node node :path current-path :subject subject
|
||||
:source-handle handle)
|
||||
entries)
|
||||
(when-let* ((id (ebox-source-record-id record)))
|
||||
(prepend id-table id entry))
|
||||
(dolist (class (ebox-source-record-classes record))
|
||||
(prepend class-table class entry))
|
||||
(when-let* ((type (ebox-tree-node-selector-type node)))
|
||||
(prepend type-table type entry))
|
||||
(ecss-subject-set-children
|
||||
subject (visit-children node current-path parent-kind))
|
||||
(remhash node active)
|
||||
(list subject)))))))
|
||||
(let* ((root-subjects (visit root nil root-parent-kind))
|
||||
(root-subject (car root-subjects)))
|
||||
(dolist (table (list id-table class-table type-table))
|
||||
(maphash (lambda (key values)
|
||||
(puthash key (nreverse values) table))
|
||||
table))
|
||||
(when (and base-source-index
|
||||
(not allow-unreferenced-sources-p)
|
||||
(/= (hash-table-count used-handles)
|
||||
(ebox-source-table-count
|
||||
(ebox-source--index-handle-records
|
||||
base-source-index))))
|
||||
(error "Ebox canonical input contains unreferenced source facts"))
|
||||
(ebox-source--with-derived
|
||||
(or base-source-index
|
||||
(ebox-source-builder-finish (ebox-source-builder-create)))
|
||||
:subjects subjects :node-subjects node-subjects
|
||||
:entries (nreverse entries)
|
||||
:root-subject root-subject
|
||||
:selector-id-table id-table
|
||||
:selector-class-table class-table
|
||||
:selector-type-table type-table)))))
|
||||
|
||||
(defun ebox-tree-subject-for-path (path)
|
||||
"Return an ECSS subject chain for the root-to-node Ebox PATH."
|
||||
(let (parent subject)
|
||||
(dolist (node path)
|
||||
(when (and (listp node) (not (stringp node)))
|
||||
(setq subject (ebox-tree-node-subject node))
|
||||
(when parent
|
||||
(ecss-subject-set-children parent (list subject)))
|
||||
(setq parent subject)))
|
||||
subject))
|
||||
(defun ebox-tree-source-handle-order (root)
|
||||
"Return canonical source handles below ROOT in document preorder."
|
||||
(let (handles)
|
||||
(cl-labels
|
||||
((visit
|
||||
(node)
|
||||
(when-let* ((handle (ebox-tree-node-source-handle node)))
|
||||
(push handle handles))
|
||||
(ebox-tree-for-each-direct-child node #'visit)))
|
||||
(visit root))
|
||||
(nreverse handles)))
|
||||
|
||||
(defun ebox-tree-source-fact-index-delta
|
||||
(old-index touched removed incoming-source-indexes &optional final-order)
|
||||
"Return fact-only OLD-INDEX after local TOUCHED and REMOVED changes."
|
||||
(let ((removed-handles nil)
|
||||
(added-handles nil)
|
||||
(replacement-pairs nil)
|
||||
(removed-seen (make-hash-table :test #'eq))
|
||||
(added-seen (make-hash-table :test #'eq)))
|
||||
(cl-labels
|
||||
((remove-handle
|
||||
(node)
|
||||
(when-let* ((handle (ebox-tree-node-source-handle node)))
|
||||
(unless (gethash handle removed-seen)
|
||||
(puthash handle t removed-seen)
|
||||
(push handle removed-handles))))
|
||||
(add-handle
|
||||
(node)
|
||||
(when-let* ((handle (ebox-tree-node-source-handle node)))
|
||||
(unless (gethash handle added-seen)
|
||||
(puthash handle t added-seen)
|
||||
(push handle added-handles)))))
|
||||
(dolist (node removed) (remove-handle node))
|
||||
(dolist (entry touched)
|
||||
(let* ((old (car entry))
|
||||
(new (nth 1 entry))
|
||||
(old-handle (and old (ebox-tree-node-source-handle old)))
|
||||
(new-handle (ebox-tree-node-source-handle new)))
|
||||
(unless (eq old-handle new-handle)
|
||||
(when old (remove-handle old))
|
||||
(add-handle new)
|
||||
(when (and old-handle new-handle)
|
||||
(push (cons old-handle new-handle) replacement-pairs)))))
|
||||
(ebox-source--index-replace-handles
|
||||
old-index (nreverse removed-handles)
|
||||
(nreverse added-handles) incoming-source-indexes
|
||||
(nreverse replacement-pairs) final-order))))
|
||||
|
||||
(defun ebox-tree-source-index-delta
|
||||
(old-index touched removed node-table parent-table root)
|
||||
"Return a candidate source index from OLD-INDEX and local tree changes.
|
||||
TOUCHED entries are `(OLD NEW PARENT-ID CHILDREN-CHANGED)'. REMOVED contains
|
||||
retired nodes. NODE-TABLE and PARENT-TABLE describe the final candidate.
|
||||
Records update in O(changed); selector/document-order views remain explicitly
|
||||
stale until a query or full projection materializes them."
|
||||
(let* ((structural-p
|
||||
(or removed
|
||||
(cl-some (lambda (entry)
|
||||
(or (null (car entry)) (nth 3 entry)))
|
||||
touched)))
|
||||
(final-order
|
||||
(and structural-p (ebox-tree-source-handle-order root)))
|
||||
(index
|
||||
(ebox-tree-source-fact-index-delta
|
||||
old-index touched removed ebox-tree--incoming-source-indexes
|
||||
final-order))
|
||||
(base-subjects
|
||||
(or (ebox-source--index-subjects old-index)
|
||||
(ebox-source--flat-table
|
||||
(make-hash-table :test #'equal) #'equal)))
|
||||
(base-node-subjects
|
||||
(or (ebox-source--index-node-subjects old-index)
|
||||
(ebox-source--flat-table
|
||||
(make-hash-table :test #'eq) #'eq)))
|
||||
(removed-identities (make-hash-table :test #'equal))
|
||||
(removed-handles (make-hash-table :test #'eq))
|
||||
(added-subjects (make-hash-table :test #'equal))
|
||||
(added-node-subjects (make-hash-table :test #'eq)))
|
||||
(cl-labels
|
||||
((remove-node
|
||||
(node)
|
||||
(when-let* ((handle (ebox-tree-node-source-handle node)))
|
||||
(puthash handle t removed-handles)
|
||||
(puthash (ebox-source--handle-id-view handle) t
|
||||
removed-identities)))
|
||||
(node-subject
|
||||
(handle)
|
||||
(or (gethash handle added-node-subjects)
|
||||
(and (not (gethash handle removed-handles))
|
||||
(ebox-source--table-value
|
||||
base-node-subjects handle)))))
|
||||
(dolist (node removed) (remove-node node))
|
||||
(dolist (entry touched)
|
||||
(when-let* ((old (car entry))) (remove-node old)))
|
||||
;; TOUCHED is preorder, so a copied parent subject is installed before
|
||||
;; its child and local inheritance can bind without a tree scan.
|
||||
(dolist (entry touched)
|
||||
(let* ((node (nth 1 entry))
|
||||
(handle (ebox-tree-node-source-handle node)))
|
||||
(when handle
|
||||
(let* ((record (or (ebox-source--index-record index handle)
|
||||
(error "Ebox candidate source is absent")))
|
||||
(subject (ebox-tree--subject-from-record node record))
|
||||
(parent-id (nth 2 entry))
|
||||
parent-subject)
|
||||
(while (and parent-id (null parent-subject))
|
||||
(when-let* ((parent (gethash parent-id node-table)))
|
||||
(when-let* ((parent-handle
|
||||
(ebox-tree-node-source-handle parent)))
|
||||
(setq parent-subject (node-subject parent-handle))))
|
||||
(unless parent-subject
|
||||
(setq parent-id (gethash parent-id parent-table))))
|
||||
(setf (ecss-subject-parent subject) parent-subject)
|
||||
(puthash (ebox-source--handle-id-view handle)
|
||||
subject added-subjects)
|
||||
(puthash handle subject added-node-subjects)))))
|
||||
(let* ((subjects
|
||||
(ebox-source--table-overlay
|
||||
base-subjects added-subjects removed-identities
|
||||
(+ (- (ebox-source--table-size base-subjects)
|
||||
(hash-table-count removed-identities))
|
||||
(hash-table-count added-subjects))))
|
||||
(node-subjects
|
||||
(ebox-source--table-overlay
|
||||
base-node-subjects added-node-subjects removed-handles
|
||||
(+ (- (ebox-source--table-size base-node-subjects)
|
||||
(hash-table-count removed-handles))
|
||||
(hash-table-count added-node-subjects)))))
|
||||
(ebox-source--with-derived
|
||||
index
|
||||
:subjects subjects :node-subjects node-subjects
|
||||
:entries (ebox-source--index-entries old-index)
|
||||
:root-subject
|
||||
(when-let* ((root-handle (ebox-tree-node-source-handle root)))
|
||||
(ebox-source--table-value node-subjects root-handle))
|
||||
:selector-id-table (ebox-source--index-selector-id-table old-index)
|
||||
:selector-class-table
|
||||
(ebox-source--index-selector-class-table old-index)
|
||||
:selector-type-table
|
||||
(ebox-source--index-selector-type-table old-index)
|
||||
:derived-stale-p t)))))
|
||||
|
||||
(defun ebox-tree-selector-index (root)
|
||||
"Return derived selector views over ROOT's unique source index."
|
||||
(let ((index (ebox-tree-source-index root)))
|
||||
(list :source-index index
|
||||
:selector-id-table
|
||||
(ebox-source--index-selector-table index 'id)
|
||||
:selector-class-table
|
||||
(ebox-source--index-selector-table index 'class)
|
||||
:selector-type-table
|
||||
(ebox-source--index-selector-table index 'type))))
|
||||
|
||||
(defun ebox-tree-subject-index (root &optional source-index)
|
||||
"Return ECSS subject views over ROOT's SOURCE-INDEX.
|
||||
Build a temporary index only for an unmounted tree query."
|
||||
(let ((index (or source-index (ebox-tree-source-index root))))
|
||||
(list :source-index index
|
||||
:root-subject (ebox-source--index-root-subject-view index)
|
||||
:entries (ebox-source--index-entries-view index)
|
||||
:node-subject-table
|
||||
(ebox-source--index-node-subject-table-view index))))
|
||||
|
||||
(defun ebox-tree-subject-for-path (source-index path)
|
||||
"Return SOURCE-INDEX's retained subject for the final node on PATH."
|
||||
(when-let* ((node (car (last path))))
|
||||
(ebox-tree-node-subject source-index node)))
|
||||
|
||||
(defun ebox-tree-flex-participation-props (node)
|
||||
"Return raw flex item participation props attached to NODE."
|
||||
|
||||
296
ebox.el
296
ebox.el
@ -23,7 +23,7 @@
|
||||
"Directory containing the active Ebox Lisp sources.")
|
||||
|
||||
(defconst ebox--compile-sources
|
||||
'("ebox-cache.el" "ebox-style.el" "ebox-layout-config.el"
|
||||
'("ebox-cache.el" "ebox-source.el" "ebox-style.el" "ebox-layout-config.el"
|
||||
"ebox-node-factory.el"
|
||||
"ebox-child-range.el" "ebox-tree.el" "ebox-measure.el"
|
||||
"ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el"
|
||||
@ -41,6 +41,7 @@
|
||||
(require 'seq)
|
||||
(require 'subr-x)
|
||||
(require 'ebox-cache)
|
||||
(require 'ebox-source)
|
||||
(require 'ebox-style)
|
||||
(require 'ebox-layout-config)
|
||||
(require 'ebox-node-factory)
|
||||
@ -413,20 +414,9 @@ caller, such as the Playground preview, renders against a concrete window.")
|
||||
(plist-get box property))
|
||||
|
||||
(defun ebox-put (box property value)
|
||||
"Set PROPERTY to VALUE in BOX and return the modified box.
|
||||
Registered style properties also update BOX's canonical ECSS declarations so a
|
||||
later cascade computes from the public mutation instead of stale source data."
|
||||
(when-let* ((declaration
|
||||
(ebox-style-compile-declarations (list property value))))
|
||||
(plist-put
|
||||
box :ebox-style-declarations
|
||||
(ebox-style-merge-declarations
|
||||
(plist-get box :ebox-style-declarations) declaration))
|
||||
(when (plist-get box :ebox-style-wrapper)
|
||||
(plist-put
|
||||
box :ebox-style-overrides
|
||||
(ebox-style-merge-declarations
|
||||
(plist-get box :ebox-style-overrides) declaration))))
|
||||
"Set private runtime PROPERTY to VALUE in BOX and return BOX.
|
||||
Author declarations are immutable source-index facts and cannot be mutated
|
||||
through this low-level plist helper."
|
||||
(plist-put box property value))
|
||||
|
||||
;;;###autoload
|
||||
@ -677,10 +667,14 @@ ALIGN can be `top', `center', or `bottom'."
|
||||
;;;###autoload
|
||||
(defun ebox-region-ids (node)
|
||||
"Return NODE's region ids in document order.
|
||||
NODE can be an unrendered layout tree or an already-rendered string.
|
||||
NODE may be a canonical Ebox input, an internal layout tree, or a rendered
|
||||
string. Canonical inputs must contain exactly one root.
|
||||
For layout trees, missing ids are assigned to box plists and remain stable
|
||||
for later renders and dynamic updates."
|
||||
(cond
|
||||
((ebox-canonical-input-p node)
|
||||
(ebox-region-ids
|
||||
(ebox-canonical-input--single-root node "ebox-region-ids")))
|
||||
((stringp node)
|
||||
(ebox--string-region-ids node))
|
||||
((not (listp node)) nil)
|
||||
@ -730,56 +724,6 @@ for later renders and dynamic updates."
|
||||
(ebox--runtime-node-ids node)
|
||||
node)
|
||||
|
||||
(defun ebox--runtime-node-key (node)
|
||||
"Return NODE's explicit runtime key, or nil."
|
||||
(when (and (listp node)
|
||||
(plist-member node :key))
|
||||
(plist-get node :key)))
|
||||
|
||||
(defun ebox--runtime-same-type-p (old new)
|
||||
"Return non-nil when OLD and NEW have the same runtime node type."
|
||||
(and (listp old)
|
||||
(listp new)
|
||||
(eq (plist-get old :ebox-type)
|
||||
(plist-get new :ebox-type))))
|
||||
|
||||
(defun ebox--runtime-node-id-by-key (node key)
|
||||
"Return the first runtime node id under NODE whose :key equals KEY."
|
||||
(when (and key
|
||||
(listp node)
|
||||
(not (stringp node)))
|
||||
(or (when (equal (ebox--runtime-node-key node) key)
|
||||
(ebox--ensure-node-id node))
|
||||
(cl-some (lambda (child)
|
||||
(ebox--runtime-node-id-by-key child key))
|
||||
(ebox--node-children node)))))
|
||||
|
||||
(defun ebox--keyed-runtime-children (children)
|
||||
"Return a KEY -> child hash table for explicitly keyed CHILDREN."
|
||||
(let ((table (make-hash-table :test 'equal)))
|
||||
(dolist (child children)
|
||||
(when-let* ((key (ebox--runtime-node-key child)))
|
||||
(puthash key child table)))
|
||||
table))
|
||||
|
||||
(defun ebox--runtime-positional-match (old-child new-child)
|
||||
"Return OLD-CHILD when NEW-CHILD can reuse its positional identity."
|
||||
(when (and (not (ebox--runtime-node-key old-child))
|
||||
(not (ebox--runtime-node-key new-child))
|
||||
(ebox--runtime-same-type-p old-child new-child))
|
||||
old-child))
|
||||
|
||||
(defun ebox--runtime-keyed-match (old-keyed new-child)
|
||||
"Return the keyed old child that should match NEW-CHILD."
|
||||
(when-let* ((key (ebox--runtime-node-key new-child)))
|
||||
(let ((old-child (gethash key old-keyed)))
|
||||
(when (ebox--runtime-same-type-p old-child new-child)
|
||||
old-child))))
|
||||
|
||||
(defun ebox--assign-runtime-node-id-from-match (old new)
|
||||
"Assign NEW retained runtime identity from OLD when possible."
|
||||
(ebox-tree-transfer-runtime-identity old new))
|
||||
|
||||
(defun ebox--reconcile-runtime-tree (old new)
|
||||
"Transfer stable runtime identities from OLD to matching nodes in NEW.
|
||||
|
||||
@ -788,12 +732,14 @@ matches an old sibling with the same key and node type; unkeyed children may
|
||||
reuse same-position ids when their node type is unchanged. Retained boxes
|
||||
also keep their region ids so buffer properties remain aligned with runtime
|
||||
indexes. This prepares component identity without exposing a component API."
|
||||
(ebox-tree-reconcile-runtime old new))
|
||||
(ebox-tree-reconcile-runtime
|
||||
old (ebox-tree-source-index old)
|
||||
new (ebox-tree-source-index new)))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-build (dsl)
|
||||
"Build an Ebox node from an ETML-style list DSL."
|
||||
(ebox-dsl-build dsl))
|
||||
"Compile an Ebox author form into one opaque canonical input."
|
||||
(ebox-dsl-build-input dsl))
|
||||
|
||||
;;; ============================================================
|
||||
;;; Property Rules: 配置驱动的 Shorthand/Longhand 映射
|
||||
@ -3691,6 +3637,14 @@ FALLBACK is the Emacs scroll command to call when ebox cannot scroll."
|
||||
(max ebox-scroll-step
|
||||
(- (window-body-height nil) next-screen-context-lines))))
|
||||
|
||||
(defun ebox--scroll-page-fallback-argument (requested residual arg)
|
||||
"Return the native page argument for REQUESTED, RESIDUAL, and prefix ARG.
|
||||
Nil preserves Emacs' native whole-page boundary behavior when no Ebox owner
|
||||
consumed an unprefixed request. A prefix or partial residual remains an exact
|
||||
line count so scroll distance is neither duplicated nor dropped."
|
||||
(and (or arg (/= (abs residual) (abs requested)))
|
||||
(abs residual)))
|
||||
|
||||
(defun ebox--wheel-region-ids (event)
|
||||
"Return ebox scroll candidate region ids under mouse wheel EVENT."
|
||||
(when-let* ((start (ignore-errors (event-start event)))
|
||||
@ -3814,7 +3768,8 @@ With prefix ARG, scroll by that many content lines."
|
||||
(ebox--scroll-by
|
||||
delta
|
||||
(lambda (n)
|
||||
(scroll-up-command n)))))
|
||||
(scroll-up-command
|
||||
(ebox--scroll-page-fallback-argument delta n arg))))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-scroll-page-up (&optional arg)
|
||||
@ -3825,7 +3780,8 @@ With prefix ARG, scroll by that many content lines."
|
||||
(ebox--scroll-by
|
||||
delta
|
||||
(lambda (n)
|
||||
(scroll-down-command n)))))
|
||||
(scroll-down-command
|
||||
(ebox--scroll-page-fallback-argument delta n arg))))))
|
||||
|
||||
(defun ebox--region-id-at-pos (pos)
|
||||
"Return the owning ebox region-id at POS, if any."
|
||||
@ -3911,8 +3867,9 @@ Defaults to the current buffer."
|
||||
(when-let* ((text (ebox--region-update-direct-text-node box)))
|
||||
(ebox-text-node-value text)))
|
||||
|
||||
(defun ebox--region-update-analysis (box expanded declarations)
|
||||
"Return change analysis for BOX, EXPANDED fields, and DECLARATIONS."
|
||||
(defun ebox--region-update-analysis
|
||||
(source-index box expanded declarations)
|
||||
"Return change analysis for BOX in SOURCE-INDEX and new DECLARATIONS."
|
||||
(let (content-seen
|
||||
content-value
|
||||
style-changed-p
|
||||
@ -3936,7 +3893,7 @@ Defaults to the current buffer."
|
||||
(user-error "Ebox content update requires one direct Text child"))
|
||||
(list :changed-p
|
||||
(or (not (equal declarations
|
||||
(plist-get box :ebox-style-declarations)))
|
||||
(ebox-style-node-declarations source-index box)))
|
||||
style-changed-p
|
||||
(and content-seen
|
||||
(not (equal (ebox--region-update-content-value box)
|
||||
@ -3972,17 +3929,24 @@ cannot be changed through the unpublished batch root."
|
||||
(ebox-tree-copy-node-structure (plist-get state :root-node)))))
|
||||
|
||||
(defun ebox--surface-region-apply-props
|
||||
(root region-id expanded declarations)
|
||||
(root source-index region-id expanded declarations)
|
||||
"Apply DECLARATIONS then EXPANDED fields to REGION-ID in ROOT."
|
||||
(let ((box (ebox--root-region-box root region-id)) changed-keys)
|
||||
(let ((box (ebox--root-region-box root region-id))
|
||||
(candidate-source-index source-index)
|
||||
changed-keys)
|
||||
(unless box
|
||||
(user-error "Ebox region handle no longer resolves to a box"))
|
||||
(let ((declarations-changed
|
||||
(not (ebox-style-declarations-equal-p
|
||||
declarations
|
||||
(plist-get box :ebox-style-declarations)))))
|
||||
(ebox-style-node-declarations source-index box)))))
|
||||
(when declarations-changed
|
||||
(plist-put box :ebox-style-declarations declarations))
|
||||
(let* ((old-handle (ebox-tree-node-source-handle box))
|
||||
(binding
|
||||
(ebox-source-index-rebind
|
||||
source-index old-handle :declarations declarations)))
|
||||
(setq candidate-source-index (car binding))
|
||||
(plist-put box :ebox-source-handle (cdr binding))))
|
||||
(cl-loop for (key value) on expanded by #'cddr
|
||||
for target-key = (ebox--region-update-normalize-property key)
|
||||
when (or (eq target-key :content)
|
||||
@ -4001,7 +3965,8 @@ cannot be changed through the unpublished batch root."
|
||||
"Ebox content update requires one direct Text child"))
|
||||
(plist-put box target-key value))
|
||||
and do (push target-key changed-keys)))
|
||||
(nreverse changed-keys)))
|
||||
(list :changed-keys (nreverse changed-keys)
|
||||
:source-index candidate-source-index)))
|
||||
|
||||
(defun ebox--surface-region-no-op-report (state region-id handle)
|
||||
"Return and store a no-op report for STATE, REGION-ID, and HANDLE."
|
||||
@ -4223,6 +4188,9 @@ REGION-ID and CHANGED-KEYS describe one update; CHANGES describes a batch."
|
||||
(defun ebox--surface-region-update-target (buffer region-id handle props)
|
||||
"Apply PROPS to BUFFER REGION-ID through TP for optional HANDLE."
|
||||
(let* ((state (ebox--buffer-render-state buffer))
|
||||
(source-index
|
||||
(or (ebox-incremental-surface-batch-source-index buffer)
|
||||
(plist-get state :source-index)))
|
||||
(published-root (plist-get state :root-node))
|
||||
(candidate-box
|
||||
(or (ebox--root-region-box published-root region-id)
|
||||
@ -4230,13 +4198,16 @@ REGION-ID and CHANGED-KEYS describe one update; CHANGES describes a batch."
|
||||
(expanded (ebox--expand-plist props))
|
||||
(declarations
|
||||
(ebox-style-merge-declarations
|
||||
(plist-get candidate-box :ebox-style-declarations)
|
||||
(ebox-style-node-declarations
|
||||
source-index candidate-box)
|
||||
(ebox-style-compile-declarations props)))
|
||||
(old-style-values
|
||||
(and (plist-member expanded :overflow)
|
||||
(list :overflow (ebox-get candidate-box :overflow))))
|
||||
(analysis
|
||||
(ebox--region-update-analysis candidate-box expanded declarations)))
|
||||
(ebox--region-update-analysis
|
||||
source-index
|
||||
candidate-box expanded declarations)))
|
||||
(dolist (key (plist-get analysis :unknown-keys))
|
||||
(message "ebox-region-update: unknown key %S (ignored)" key))
|
||||
(if (not (plist-get analysis :changed-p))
|
||||
@ -4261,28 +4232,33 @@ REGION-ID and CHANGED-KEYS describe one update; CHANGES describes a batch."
|
||||
(candidate-root
|
||||
(ebox--surface-region-candidate-root
|
||||
buffer state region-id content-only-p))
|
||||
(changed-keys
|
||||
(applied
|
||||
(ebox--surface-region-apply-props
|
||||
candidate-root region-id expanded declarations))
|
||||
candidate-root source-index
|
||||
region-id expanded declarations))
|
||||
(changed-keys (plist-get applied :changed-keys))
|
||||
(candidate-source-index (plist-get applied :source-index))
|
||||
(dirty-kind
|
||||
(ebox--region-update-dirty-kind changed-keys))
|
||||
(constraint-change
|
||||
(ebox--region-constraint-change
|
||||
buffer region-id dirty-kind changed-keys)))
|
||||
(if (ebox-incremental-record-surface-region-change
|
||||
buffer candidate-root region-id handle dirty-kind
|
||||
changed-keys old-style-values
|
||||
(if (ebox-incremental-record-surface-region-input-change
|
||||
buffer candidate-root candidate-source-index
|
||||
region-id handle dirty-kind changed-keys old-style-values
|
||||
#'ebox--publish-surface-region-batch)
|
||||
(progn
|
||||
(ebox--cancel-buffer-runtime-prewarm buffer)
|
||||
nil)
|
||||
(ebox--publish-scoped-region-candidate
|
||||
buffer candidate-root region-id changed-keys
|
||||
(append (if handle
|
||||
(list :region-handle handle)
|
||||
(list :region-id region-id))
|
||||
(ebox--constraint-change-report-props
|
||||
constraint-change)))))))))
|
||||
(let ((ebox-incremental--source-base-index
|
||||
candidate-source-index))
|
||||
(ebox--publish-scoped-region-candidate
|
||||
buffer candidate-root region-id changed-keys
|
||||
(append (if handle
|
||||
(list :region-handle handle)
|
||||
(list :region-id region-id))
|
||||
(ebox--constraint-change-report-props
|
||||
constraint-change))))))))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-region-update (handle &rest props)
|
||||
@ -4394,16 +4370,18 @@ Examples:
|
||||
(cons present observer)))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-render-to-buffer (buffer-or-name node &optional options)
|
||||
"Render layout NODE into BUFFER-OR-NAME and return the buffer.
|
||||
NODE is a typed TextNode/BoxNode or a pre-rendered string.
|
||||
(defun ebox-render-to-buffer (buffer-or-name input &optional options)
|
||||
"Render canonical INPUT into BUFFER-OR-NAME and return the buffer.
|
||||
INPUT atomically carries one typed Text/Box root and its source generation.
|
||||
OPTIONS accepts only `:observer'. Its function receives the buffer and flat
|
||||
TP/Ebox reports after accepted initial publication and later commits."
|
||||
(declare (indent 1))
|
||||
(unless (stringp node)
|
||||
;; Validate opaque host identities before preparing a target generation.
|
||||
(ebox-tree-validate-host-refs node))
|
||||
(let* ((observer-option (ebox--render-observer-option options))
|
||||
(unless (ebox-canonical-input-p input)
|
||||
(signal 'wrong-type-argument (list 'ebox-canonical-input-p input)))
|
||||
(let* ((node (ebox-canonical-input--single-root
|
||||
input "ebox-render-to-buffer"))
|
||||
(source-base-index (ebox-canonical-input--source-index input))
|
||||
(observer-option (ebox--render-observer-option options))
|
||||
(buffer (get-buffer-create buffer-or-name))
|
||||
(old-observer (ebox-surface-buffer-observer buffer))
|
||||
(old-bridge (with-current-buffer buffer ebox-surface--tp-observer))
|
||||
@ -4417,7 +4395,10 @@ TP/Ebox reports after accepted initial publication and later commits."
|
||||
buffer 'mount
|
||||
(lambda ()
|
||||
(ebox--with-render-gc
|
||||
(ebox-surface-mount-buffer buffer node)
|
||||
(ebox-surface-mount-buffer
|
||||
buffer node nil nil nil
|
||||
(and source-base-index
|
||||
(list :source-base-index source-base-index)))
|
||||
(with-current-buffer buffer
|
||||
(goto-char (point-min))
|
||||
(add-hook 'kill-buffer-hook
|
||||
@ -4522,8 +4503,6 @@ ITEMS are declarative nodes. Child Range descriptors cannot be nested."
|
||||
(dolist (child (ebox-tree--children-raw node))
|
||||
(reject-range child)))))
|
||||
(reject-range item)))
|
||||
(ebox-tree-validate-declarative-root
|
||||
(list :ebox-type 'stack :display '(block column) :children items))
|
||||
(ebox-child-range--descriptor-create
|
||||
range-ref
|
||||
(mapcar (lambda (item)
|
||||
@ -4533,59 +4512,95 @@ ITEMS are declarative nodes. Child Range descriptors cannot be nested."
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-candidate-replace
|
||||
(candidate node-id next-subtree
|
||||
(candidate node-id input
|
||||
&optional old-semantic-key new-semantic-key)
|
||||
"Replace NODE-ID in CANDIDATE with declarative NEXT-SUBTREE.
|
||||
"Replace NODE-ID in CANDIDATE with canonical INPUT.
|
||||
OLD-SEMANTIC-KEY and NEW-SEMANTIC-KEY may identify a stable anchor's detached
|
||||
semantic variants so a later return can reuse bounded runtime identity.
|
||||
Return CANDIDATE for convenient transaction construction."
|
||||
(ebox-incremental-candidate-replace
|
||||
candidate node-id next-subtree old-semantic-key new-semantic-key))
|
||||
(unless (ebox-canonical-input-p input)
|
||||
(signal 'wrong-type-argument (list 'ebox-canonical-input-p input)))
|
||||
(let ((ebox-incremental--source-base-index
|
||||
(ebox-canonical-input--source-index input)))
|
||||
(ebox-incremental-candidate-replace
|
||||
candidate node-id
|
||||
(ebox-canonical-input--single-root input "ebox-candidate-replace")
|
||||
old-semantic-key new-semantic-key)))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-candidate-replace-host-ref
|
||||
(candidate host-ref next-subtree
|
||||
(candidate host-ref input
|
||||
&optional old-semantic-key new-semantic-key)
|
||||
"Replace HOST-REF in CANDIDATE with declarative NEXT-SUBTREE.
|
||||
"Replace HOST-REF in CANDIDATE with canonical INPUT.
|
||||
HOST-REF is resolved against the exact published runtime captured by
|
||||
`ebox-candidate-begin'. OLD-SEMANTIC-KEY and NEW-SEMANTIC-KEY have the same
|
||||
optional detached-identity meaning as in `ebox-candidate-replace'.
|
||||
Return CANDIDATE."
|
||||
(ebox-incremental-candidate-replace-host-ref
|
||||
candidate host-ref next-subtree old-semantic-key new-semantic-key))
|
||||
(unless (ebox-canonical-input-p input)
|
||||
(signal 'wrong-type-argument (list 'ebox-canonical-input-p input)))
|
||||
(let ((ebox-incremental--source-base-index
|
||||
(ebox-canonical-input--source-index input)))
|
||||
(ebox-incremental-candidate-replace-host-ref
|
||||
candidate host-ref
|
||||
(ebox-canonical-input--single-root
|
||||
input "ebox-candidate-replace-host-ref")
|
||||
old-semantic-key new-semantic-key)))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-candidate-patch-host-paint
|
||||
(candidate host-ref previous-subtree next-subtree)
|
||||
"Patch HOST-REF paint from declarative OLD/NEW subtrees in CANDIDATE.
|
||||
(candidate host-ref previous-input next-input)
|
||||
"Patch HOST-REF paint from canonical PREVIOUS-INPUT/NEXT-INPUT in CANDIDATE.
|
||||
Only node-local paint fields may differ. The Host's children and runtime
|
||||
identities are retained instead of being copied as a replacement subtree.
|
||||
Return CANDIDATE when accepted, or nil when the delta needs ordinary subtree
|
||||
replacement."
|
||||
(ebox-incremental-candidate-patch-host-paint
|
||||
candidate host-ref previous-subtree next-subtree))
|
||||
(dolist (input (list previous-input next-input))
|
||||
(unless (ebox-canonical-input-p input)
|
||||
(signal 'wrong-type-argument (list 'ebox-canonical-input-p input))))
|
||||
(let ((ebox-incremental--previous-source-base-index
|
||||
(ebox-canonical-input--source-index previous-input))
|
||||
(ebox-incremental--source-base-index
|
||||
(ebox-canonical-input--source-index next-input)))
|
||||
(ebox-incremental-candidate-patch-host-paint
|
||||
candidate host-ref
|
||||
(ebox-canonical-input--single-root
|
||||
previous-input "ebox-candidate-patch-host-paint previous input")
|
||||
(ebox-canonical-input--single-root
|
||||
next-input "ebox-candidate-patch-host-paint next input"))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-candidate-replace-range-ref
|
||||
(candidate range-ref next-items &optional reuse-map)
|
||||
"Replace RANGE-REF, retaining slots named by proven REUSE-MAP."
|
||||
(ebox-incremental-candidate-replace-range-ref
|
||||
candidate range-ref next-items reuse-map))
|
||||
(candidate range-ref input &optional reuse-map)
|
||||
"Replace RANGE-REF from canonical INPUT, retaining proven REUSE-MAP slots."
|
||||
(unless (ebox-canonical-input-p input)
|
||||
(signal 'wrong-type-argument (list 'ebox-canonical-input-p input)))
|
||||
(let ((ebox-incremental--source-base-index
|
||||
(ebox-canonical-input--source-index input)))
|
||||
(ebox-incremental-candidate-replace-range-ref
|
||||
candidate range-ref
|
||||
(copy-sequence (ebox-canonical-input--nodes input))
|
||||
reuse-map)))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-candidate-replace-root (candidate next-root)
|
||||
"Replace CANDIDATE's exact implicit root with declarative NEXT-ROOT.
|
||||
(defun ebox-candidate-replace-root (candidate input)
|
||||
"Replace CANDIDATE's exact implicit root with canonical INPUT.
|
||||
The root address is private, base-bound, and cannot collide with host refs.
|
||||
Repeated calls are last-wins and absorb all descendant candidate operations."
|
||||
(ebox-incremental-candidate-replace-root candidate next-root))
|
||||
(unless (ebox-canonical-input-p input)
|
||||
(signal 'wrong-type-argument (list 'ebox-canonical-input-p input)))
|
||||
(let ((ebox-incremental--source-base-index
|
||||
(ebox-canonical-input--source-index input)))
|
||||
(ebox-incremental-candidate-replace-root
|
||||
candidate
|
||||
(ebox-canonical-input--single-root input "ebox-candidate-replace-root"))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-commit
|
||||
(buffer-or-name next-root &optional framework-publish framework-rollback)
|
||||
"Atomically commit declarative NEXT-ROOT into BUFFER-OR-NAME.
|
||||
|
||||
NEXT-ROOT may be a newly built Ebox node tree or a one-shot logical candidate
|
||||
from `ebox-candidate-begin'. A complete root is copied into a buffer-owned
|
||||
NEXT-ROOT must be a canonical Ebox input or a one-shot logical candidate from
|
||||
`ebox-candidate-begin'. A complete root is copied into a buffer-owned
|
||||
runtime. A logical candidate path-copies replaced ancestor chains, shares
|
||||
untouched subtrees, and isolates retained scroll nodes before proof rendering.
|
||||
Both forms reconcile Ebox-owned identity,
|
||||
@ -4617,10 +4632,29 @@ Return the successful publication report stored by `ebox-buffer-update-report'."
|
||||
(error
|
||||
"Ebox declarative commit requires an existing live buffer: %S"
|
||||
buffer-or-name))
|
||||
(let* ((commit-input
|
||||
(if (ebox-candidate-p next-root)
|
||||
(ebox-incremental-consume-candidate buffer next-root)
|
||||
(ebox-incremental-prepare-root-commit buffer next-root)))
|
||||
(unless (or (ebox-canonical-input-p next-root)
|
||||
(ebox-candidate-p next-root))
|
||||
(signal 'wrong-type-argument
|
||||
(list '(or ebox-canonical-input-p ebox-candidate-p)
|
||||
next-root)))
|
||||
(let* ((canonical-input
|
||||
(and (ebox-canonical-input-p next-root) next-root))
|
||||
(source-base-index
|
||||
(and canonical-input
|
||||
(ebox-canonical-input--source-index
|
||||
canonical-input)))
|
||||
(next-root
|
||||
(if canonical-input
|
||||
(ebox-canonical-input--single-root
|
||||
canonical-input "ebox-commit")
|
||||
next-root))
|
||||
(commit-input
|
||||
(let ((ebox-incremental--source-base-index
|
||||
source-base-index))
|
||||
(if (ebox-candidate-p next-root)
|
||||
(ebox-incremental-consume-candidate buffer next-root)
|
||||
(ebox-incremental-prepare-root-commit
|
||||
buffer next-root))))
|
||||
(source (plist-get commit-input :root))
|
||||
(callback
|
||||
(or framework-publish
|
||||
@ -4707,11 +4741,11 @@ through dirty-set and patch-set execution before falling back to root rerender."
|
||||
(plist-get state :viewport-height))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ebox-display-buffer (buffer-or-name node)
|
||||
"Render NODE to BUFFER-OR-NAME through TP and display the buffer."
|
||||
(defun ebox-display-buffer (buffer-or-name input)
|
||||
"Render canonical INPUT to BUFFER-OR-NAME through TP and display it."
|
||||
(declare (indent 1))
|
||||
(delete-other-windows)
|
||||
(switch-to-buffer (ebox-render-to-buffer buffer-or-name node)))
|
||||
(switch-to-buffer (ebox-render-to-buffer buffer-or-name input)))
|
||||
|
||||
(defconst ebox-public-api
|
||||
'(ebox-buffer-mode
|
||||
|
||||
@ -274,10 +274,19 @@
|
||||
|
||||
(defun ebox-performance-evaluator--runtime-output (buffer)
|
||||
"Return a fresh propertized render of BUFFER's current runtime root."
|
||||
(let ((ebox--scroll-global-state (make-hash-table :test #'equal))
|
||||
(ebox--region-box-table (make-hash-table :test #'equal)))
|
||||
(let* ((state (ebox--buffer-render-state buffer))
|
||||
(ebox--scroll-global-state (make-hash-table :test #'equal))
|
||||
(ebox--region-box-table (make-hash-table :test #'equal)))
|
||||
(ebox--with-buffer-render-context buffer
|
||||
(ebox-render (ebox--buffer-root-node buffer)))))
|
||||
(ebox--render-node
|
||||
(ebox--buffer-root-node buffer) (plist-get state :source-index)))))
|
||||
|
||||
(defun ebox-performance-evaluator--surface-producer (input)
|
||||
"Return a one-shot TP producer for canonical INPUT."
|
||||
(ebox-surface-producer
|
||||
(ebox-canonical-input--single-root input "Ebox performance evaluator")
|
||||
nil t
|
||||
(list :source-base-index (ebox-canonical-input--source-index input))))
|
||||
|
||||
(defun ebox-performance-evaluator--static-fixture
|
||||
(&optional styled target-content)
|
||||
@ -334,8 +343,8 @@
|
||||
(ebox-performance-evaluator--reset-runtime-state)
|
||||
(let ((expected
|
||||
(tp-surface-materialize-string
|
||||
(ebox-surface-producer
|
||||
(ebox-performance-evaluator--static-fixture) nil t))))
|
||||
(ebox-performance-evaluator--surface-producer
|
||||
(ebox-performance-evaluator--static-fixture)))))
|
||||
(ebox-performance-evaluator--reset-runtime-state)
|
||||
(let* ((measurement
|
||||
(ebox-performance-evaluator--measure
|
||||
@ -367,8 +376,8 @@
|
||||
(let ((ebox-performance-evaluator--active nil)
|
||||
(expected
|
||||
(tp-surface-materialize-string
|
||||
(ebox-surface-producer
|
||||
(ebox-performance-evaluator--static-fixture t) nil t))))
|
||||
(ebox-performance-evaluator--surface-producer
|
||||
(ebox-performance-evaluator--static-fixture t)))))
|
||||
(ebox-performance-evaluator--reset-runtime-state)
|
||||
(let* ((measurement
|
||||
(ebox-performance-evaluator--measure
|
||||
|
||||
@ -101,7 +101,9 @@
|
||||
(defun ebox-visual-check-report ()
|
||||
"Return visual verification report based on rendered text properties."
|
||||
(let* ((layout (ebox-visual-check-layout))
|
||||
(ids-before (ebox-region-ids layout))
|
||||
(ids-before
|
||||
(ebox-region-ids
|
||||
(ebox-canonical-input--single-root layout "Ebox visual check")))
|
||||
(buffer (generate-new-buffer " *ebox-visual-check-report*")))
|
||||
(unwind-protect
|
||||
(progn
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -132,13 +132,18 @@
|
||||
(require 'ebox)
|
||||
(let ((text-forms '("short" (text "explicit"))))
|
||||
(dolist (form text-forms)
|
||||
(should (ebox-text-node-p (ebox-build form)))))
|
||||
(should
|
||||
(ebox-text-node-p
|
||||
(ebox-canonical-input--single-root
|
||||
(ebox-build form) "Ebox documented Text form")))))
|
||||
(dolist (entry '((box "normal")
|
||||
(row (box "row child"))
|
||||
(column (box "column child"))
|
||||
(flex (box "flex child"))
|
||||
(grid (box "grid child"))))
|
||||
(let* ((node (ebox-build entry))
|
||||
(let* ((node
|
||||
(ebox-canonical-input--single-root
|
||||
(ebox-build entry) "Ebox documented Box form"))
|
||||
(layout (ebox-box-node-layout node)))
|
||||
(should (ebox-box-node-p node))
|
||||
(should (eq (ebox-layout-config-kind layout)
|
||||
|
||||
@ -3,21 +3,109 @@
|
||||
(require 'ert)
|
||||
|
||||
(load-file (expand-file-name "../ebox.el" (file-name-directory (or load-file-name buffer-file-name))))
|
||||
(load-file (expand-file-name "ebox-fixtures.el"
|
||||
(file-name-directory (or load-file-name
|
||||
buffer-file-name))))
|
||||
|
||||
(defun ebox-dsl-test--plain (node)
|
||||
"Render NODE and strip text properties."
|
||||
(substring-no-properties (ebox-render node)))
|
||||
(defun ebox-dsl-test--constructor-input (tag plist)
|
||||
"Return `(BUILDER . PLIST)' for typed constructor TAG and PLIST."
|
||||
(let* ((style-keys
|
||||
(cl-loop for (key _value) on plist by #'cddr
|
||||
when (and (not (memq key
|
||||
'(:value :layout :children :outer
|
||||
:source-handle :owned-facts)))
|
||||
(ebox-style--property key))
|
||||
collect key))
|
||||
(style-props
|
||||
(cl-loop for (key value) on plist by #'cddr
|
||||
when (memq key style-keys) append (list key value)))
|
||||
(declarations
|
||||
(and style-props (ebox-style-compile-form tag style-props)))
|
||||
(plist
|
||||
(cl-loop for (key value) on plist by #'cddr
|
||||
unless (memq key style-keys) append (list key value)))
|
||||
(values
|
||||
(cl-loop for (key value) on plist by #'cddr
|
||||
when (eq key :source-handle)
|
||||
collect value))
|
||||
(builder (ebox-source-builder-create)))
|
||||
(when (plist-member plist :children)
|
||||
(setq plist
|
||||
(plist-put
|
||||
(copy-sequence plist) :children
|
||||
(mapcar (lambda (child)
|
||||
(ebox-test--normalize-child builder child))
|
||||
(plist-get plist :children)))))
|
||||
(when (<= (length values) 1)
|
||||
(let* ((provided (car values))
|
||||
(identity
|
||||
(if (ebox-source-handle-p provided)
|
||||
(ebox-source-handle-id provided)
|
||||
provided))
|
||||
(handle
|
||||
(ebox-source-builder-bind
|
||||
builder :identity identity :declarations declarations
|
||||
:provenance '(:adapter ebox-dsl-test))))
|
||||
(setq plist (plist-put (copy-sequence plist) :source-handle handle))))
|
||||
(setq plist
|
||||
(append plist
|
||||
(list :owned-facts
|
||||
(ebox-canonical-facts-from-declarations
|
||||
tag declarations))))
|
||||
(cons builder plist)))
|
||||
|
||||
(defun ebox-dsl-test--line-widths (node)
|
||||
"Return rendered pixel widths for NODE line by line."
|
||||
(mapcar #'ebox--string-pixel-width (ebox-string-lines (ebox-render node))))
|
||||
(defun ebox-dsl-test--text-create (&rest plist)
|
||||
"Create a canonical Text input with an explicit source boundary."
|
||||
(let* ((input (ebox-dsl-test--constructor-input 'text plist))
|
||||
(builder (car input))
|
||||
(node (apply #'ebox-text-create (cdr input)))
|
||||
(index (ebox-source-builder-finish builder)))
|
||||
(ebox-canonical-input-create (list node) index)))
|
||||
|
||||
(defun ebox-dsl-test--display-line-widths (node)
|
||||
"Return rendered display pixel widths for NODE line by line."
|
||||
(mapcar #'ebox--string-pixel-width (ebox-string-lines (ebox-render node))))
|
||||
(defun ebox-dsl-test--box-create (&rest plist)
|
||||
"Create a canonical Box input with an explicit source boundary."
|
||||
(let* ((layout (plist-get plist :layout))
|
||||
(tag (if (and (ebox-layout-config-p layout)
|
||||
(not (eq (ebox-layout-config-kind layout) 'normal)))
|
||||
(ebox-layout-config-kind layout)
|
||||
'box))
|
||||
(input (ebox-dsl-test--constructor-input tag plist))
|
||||
(builder (car input))
|
||||
(node
|
||||
(let ((ebox-canonical--source-builder builder))
|
||||
(apply #'ebox-box-create (cdr input))))
|
||||
(index (ebox-source-builder-finish builder)))
|
||||
(ebox-canonical-input-create (list node) index)))
|
||||
|
||||
(defun ebox-dsl-test--revise-source (input &rest facts)
|
||||
"Return INPUT with its root source immutably rebound by FACTS."
|
||||
(let* ((node (ebox-test-root input))
|
||||
(handle (ebox-node-source-handle node))
|
||||
(index (ebox-test-source-index input))
|
||||
(binding (apply #'ebox-source-index-rebind index handle facts)))
|
||||
(plist-put node :ebox-source-handle (cdr binding))
|
||||
(ebox-canonical-input-create (list node) (car binding))))
|
||||
|
||||
(defun ebox-dsl-test--plain (input)
|
||||
"Render canonical INPUT and strip text properties."
|
||||
(substring-no-properties (ebox-render input)))
|
||||
|
||||
(defun ebox-dsl-test--root (input)
|
||||
"Return INPUT's canonical root node."
|
||||
(ebox-canonical-input--single-root input "Ebox DSL test"))
|
||||
|
||||
(defun ebox-dsl-test--line-widths (input)
|
||||
"Return rendered pixel widths for canonical INPUT line by line."
|
||||
(mapcar #'ebox--string-pixel-width (ebox-string-lines (ebox-render input))))
|
||||
|
||||
(defun ebox-dsl-test--display-line-widths (input)
|
||||
"Return rendered display pixel widths for canonical INPUT line by line."
|
||||
(mapcar #'ebox--string-pixel-width (ebox-string-lines (ebox-render input))))
|
||||
|
||||
(defun ebox-dsl-test--tree-count (node predicate)
|
||||
"Count nodes below NODE for which PREDICATE returns non-nil."
|
||||
(when (ebox-canonical-input-p node)
|
||||
(setq node (ebox-dsl-test--root node)))
|
||||
(if (or (stringp node) (not (listp node)))
|
||||
0
|
||||
(+ (if (funcall predicate node) 1 0)
|
||||
@ -52,7 +140,8 @@
|
||||
(ert-deftest ebox-typed-form-projects-outer-and-layout-to-display ()
|
||||
"A typed Box form should project its two axes to one display pair."
|
||||
(let ((node (ebox-build '(flex :outer inline "A"))))
|
||||
(should (equal (ebox--computed-display node) '(inline flex)))))
|
||||
(should (equal (ebox--computed-display (ebox-dsl-test--root node))
|
||||
'(inline flex)))))
|
||||
|
||||
(ert-deftest ebox-style-unrelated-facts-preserve-typed-layout-config ()
|
||||
"Unrelated computed facts must not replace direct typed layout values."
|
||||
@ -62,7 +151,8 @@
|
||||
(grid :grid-template-columns ((40) (60))
|
||||
:column-gap (4) "A"))))
|
||||
(let* ((kind (car case))
|
||||
(node (ebox-build (cadr case)))
|
||||
(input (ebox-build (cadr case)))
|
||||
(node (ebox-dsl-test--root input))
|
||||
(before (copy-tree (plist-get node :ebox-layout-config)))
|
||||
(style
|
||||
(ebox-style-compute-subject
|
||||
@ -191,32 +281,47 @@
|
||||
|
||||
(ert-deftest ebox-canonical-text-constructor-is-renderable-and-typed ()
|
||||
"A canonical TextNode should retain its kind, source, and string value."
|
||||
(let ((node (ebox-text-create :value "Hello" :source-handle 'source-text)))
|
||||
(let* ((input (ebox-dsl-test--text-create
|
||||
:value "Hello" :source-handle 'source-text))
|
||||
(node (ebox-dsl-test--root input)))
|
||||
(should (ebox-text-node-p node))
|
||||
(should-not (ebox-box-node-p node))
|
||||
(should (eq (ebox-node-source-handle node) 'source-text))
|
||||
(should (eq (ebox-source-handle-id (ebox-node-source-handle node))
|
||||
'source-text))
|
||||
(should (string= (ebox-text-node-value node) "Hello"))
|
||||
(should (string= (ebox-dsl-test--plain node) "Hello"))
|
||||
(should (string= (ebox-dsl-test--plain input) "Hello"))
|
||||
(should-not (ebox-tree-node-visible-overflow-p node))))
|
||||
|
||||
(ert-deftest ebox-canonical-text-constructor-rejects-non-text-shapes ()
|
||||
"A canonical TextNode should have exactly one string payload."
|
||||
(should-error (ebox-text-create :value 7) :type 'error)
|
||||
(should-error (ebox-text-create :value "A" :content "B") :type 'error)
|
||||
(should-error (ebox-text-create :value "A" :children nil) :type 'error)
|
||||
(should-error (ebox-text-create :value "A" :color "red") :type 'error)
|
||||
(should-error (ebox-dsl-test--text-create :value 7) :type 'error)
|
||||
(should-error (ebox-dsl-test--text-create :value "A" :content "B") :type 'error)
|
||||
(should-error (ebox-dsl-test--text-create :value "A" :children nil) :type 'error)
|
||||
(let* ((builder (ebox-source-builder-create))
|
||||
(handle (ebox-source-builder-bind builder :declarations nil)))
|
||||
(should-error
|
||||
(ebox-text-create
|
||||
:value "A" :source-handle handle
|
||||
:owned-facts (ebox-canonical-facts-from-declarations 'text nil)
|
||||
:color "red")
|
||||
:type 'error))
|
||||
(should-error (ebox-text-create :value "A") :type 'error)
|
||||
(should-error
|
||||
(ebox-text-create :value "A" :source-handle 'raw-source)
|
||||
:type 'error)
|
||||
(should-error (ebox-build '(text :wrap-mode none "A")) :type 'error)
|
||||
(should-error
|
||||
(ebox-text-create :value "A" :value "B")
|
||||
(ebox-dsl-test--text-create :value "A" :value "B")
|
||||
:type 'error)
|
||||
(should-error
|
||||
(ebox-text-create :value "A" :source-handle 'a :source-handle 'b)
|
||||
(ebox-dsl-test--text-create :value "A" :source-handle 'a :source-handle 'b)
|
||||
:type 'error))
|
||||
|
||||
(ert-deftest ebox-canonical-box-uses-registered-runtime-defaults ()
|
||||
"A Box should materialize the definite defaults consumed by layout."
|
||||
(let ((box (ebox-box-create
|
||||
:layout (ebox-normal-layout-create) :children nil)))
|
||||
(let* ((input (ebox-dsl-test--box-create
|
||||
:layout (ebox-normal-layout-create) :children nil))
|
||||
(box (ebox-dsl-test--root input)))
|
||||
(should (eq (ebox-get box :overflow) 'scroll))
|
||||
(dolist (property '(:padding-left-pixel :padding-right-pixel
|
||||
:padding-top-height :padding-bottom-height
|
||||
@ -227,7 +332,8 @@
|
||||
|
||||
(ert-deftest ebox-render-cache-signature-ignores-derived-layout-proofs ()
|
||||
"A render must not invalidate its own canonical node cache key."
|
||||
(let* ((node (ebox-build '(box :width '(80) "Stable")))
|
||||
(let* ((node (ebox-dsl-test--root
|
||||
(ebox-build '(box :width '(80) "Stable"))))
|
||||
(before
|
||||
(let ((ebox--render-cache-signature-cache
|
||||
(make-hash-table :test 'eq)))
|
||||
@ -256,7 +362,10 @@
|
||||
(let* ((root (plist-get (ebox--buffer-render-state buffer)
|
||||
:root-node))
|
||||
(text (car (ebox-box-node-children root))))
|
||||
(should (eq (ebox-style-node-specified-value root :wrap-mode)
|
||||
(should (eq (ebox-style-node-specified-value
|
||||
root :wrap-mode nil
|
||||
(plist-get (ebox--buffer-render-state buffer)
|
||||
:source-index))
|
||||
'none))
|
||||
(should (eq (plist-get root :wrap-mode) 'none))
|
||||
(should (eq (plist-get text :wrap-mode) 'none))))
|
||||
@ -270,14 +379,14 @@
|
||||
(should-error (ebox-build '(box :wrap-mode arbitrary "A")) :type 'error)
|
||||
(let* ((width (ebox--string-pixel-width "A"))
|
||||
(char-box
|
||||
(ebox-box-create
|
||||
(ebox-dsl-test--box-create
|
||||
:layout (ebox-normal-layout-create)
|
||||
:children (list (ebox-text-create :value "AB"))
|
||||
:children (list (ebox-dsl-test--text-create :value "AB"))
|
||||
:width (list width) :wrap-mode 'char))
|
||||
(none-box
|
||||
(ebox-box-create
|
||||
(ebox-dsl-test--box-create
|
||||
:layout (ebox-normal-layout-create)
|
||||
:children (list (ebox-text-create :value "AB"))
|
||||
:children (list (ebox-dsl-test--text-create :value "AB"))
|
||||
:width (list width) :wrap-mode 'none)))
|
||||
(should (= (length (ebox-string-lines (ebox-render char-box))) 2))
|
||||
(should (= (length (ebox-string-lines (ebox-render none-box))) 1)))
|
||||
@ -290,32 +399,42 @@
|
||||
(setq justify-args (list text width))
|
||||
text)))
|
||||
(ebox-render
|
||||
(ebox-box-create
|
||||
(ebox-dsl-test--box-create
|
||||
:layout (ebox-normal-layout-create)
|
||||
:children (list (ebox-text-create :value "paragraph"))
|
||||
:children (list (ebox-dsl-test--text-create :value "paragraph"))
|
||||
:width '(100) :wrap-mode 'kp)))
|
||||
(should (equal justify-args '("paragraph" 100)))))
|
||||
|
||||
(ert-deftest ebox-canonical-normal-box-renders-inline-runs-and-blocks ()
|
||||
"Normal should retain typed children and honor inline/block boundaries."
|
||||
(let* ((text (ebox-text-create :value "A" :source-handle 'source-text))
|
||||
(second-text (ebox-text-create :value "B"))
|
||||
(inline
|
||||
(ebox-box-create
|
||||
(let* ((text-input
|
||||
(ebox-dsl-test--text-create :value "A" :source-handle 'source-text))
|
||||
(text (ebox-dsl-test--root text-input))
|
||||
(second-text-input (ebox-dsl-test--text-create :value "B"))
|
||||
(second-text (ebox-dsl-test--root second-text-input))
|
||||
(inline-input
|
||||
(ebox-dsl-test--box-create
|
||||
:layout (ebox-normal-layout-create) :outer 'inline
|
||||
:width '(100)
|
||||
:children (list (ebox-text-create :value "A B"))))
|
||||
(block
|
||||
(ebox-box-create
|
||||
:children (list (ebox-dsl-test--text-create :value "A B"))))
|
||||
(inline (ebox-dsl-test--root inline-input))
|
||||
(block-input
|
||||
(ebox-dsl-test--box-create
|
||||
:layout (ebox-normal-layout-create)
|
||||
:children (list (ebox-text-create :value "C"))))
|
||||
(tail (ebox-text-create :value "D"))
|
||||
:children (list (ebox-dsl-test--text-create :value "C"))))
|
||||
(block (ebox-dsl-test--root block-input))
|
||||
(tail-input (ebox-dsl-test--text-create :value "D"))
|
||||
(tail (ebox-dsl-test--root tail-input))
|
||||
(layout (ebox-normal-layout-create))
|
||||
(box (ebox-box-create :layout layout
|
||||
:children (list text second-text inline block tail)
|
||||
:source-handle 'source-box
|
||||
:width '(50) :overflow 'visible))
|
||||
(rendered (ebox-render box))
|
||||
(box-input
|
||||
(ebox-dsl-test--box-create
|
||||
:layout layout
|
||||
:children (list text-input second-text-input inline-input
|
||||
block-input tail-input)
|
||||
:source-handle 'source-box
|
||||
:width '(50) :overflow 'visible))
|
||||
(box (ebox-dsl-test--root box-input))
|
||||
(rendered (ebox-render box-input))
|
||||
(rendered-lines (ebox-string-lines rendered))
|
||||
(lines (mapcar (lambda (line)
|
||||
(string-trim-right
|
||||
@ -323,7 +442,8 @@
|
||||
rendered-lines)))
|
||||
(should (ebox-box-node-p box))
|
||||
(should-not (ebox-text-node-p box))
|
||||
(should (eq (ebox-node-source-handle box) 'source-box))
|
||||
(should (eq (ebox-source-handle-id (ebox-node-source-handle box))
|
||||
'source-box))
|
||||
(should (eq (ebox-layout-config-kind
|
||||
(ebox-box-node-layout box))
|
||||
'normal))
|
||||
@ -333,18 +453,27 @@
|
||||
(should (equal lines '("AB" "A B" "C" "D")))
|
||||
(should (>= (ebox--string-pixel-width (nth 1 rendered-lines)) 100))
|
||||
(let ((ebox-style-stylesheet (ecss-stylesheet-create)))
|
||||
(plist-put text :class "fixed-inline-text")
|
||||
(let* ((revised-text
|
||||
(ebox-dsl-test--revise-source
|
||||
text-input :class "fixed-inline-text"))
|
||||
(revised-box
|
||||
(ebox-dsl-test--box-create
|
||||
:layout layout
|
||||
:children (list revised-text second-text-input inline-input
|
||||
block-input tail-input)
|
||||
:source-handle 'source-box
|
||||
:width '(50) :overflow 'visible)))
|
||||
(ebox-style-add-rule ".fixed-inline-text" (list :outer 'block))
|
||||
(should-error (ebox-render box) :type 'error))
|
||||
(should-error (ebox-render revised-box) :type 'error)))
|
||||
(cl-labels
|
||||
((normal-lines
|
||||
(value &optional width)
|
||||
(mapcar #'substring-no-properties
|
||||
(ebox-string-lines
|
||||
(ebox-render
|
||||
(apply #'ebox-box-create
|
||||
(apply #'ebox-dsl-test--box-create
|
||||
:layout (ebox-normal-layout-create)
|
||||
:children (list (ebox-text-create :value value))
|
||||
:children (list (ebox-dsl-test--text-create :value value))
|
||||
(when width
|
||||
(list :width (list width)
|
||||
:overflow 'visible))))))))
|
||||
@ -371,26 +500,33 @@
|
||||
(should (= (length (normal-lines "word" 0)) 1))
|
||||
(should (= (length (normal-lines " " 0)) 1))
|
||||
(let* ((inline-zero
|
||||
(ebox-box-create
|
||||
(ebox-dsl-test--box-create
|
||||
:layout (ebox-normal-layout-create) :outer 'inline
|
||||
:width '(10)
|
||||
:children (list (ebox-text-create :value "I"))))
|
||||
:children (list (ebox-dsl-test--text-create :value "I"))))
|
||||
(zero-parent
|
||||
(ebox-box-create
|
||||
(ebox-dsl-test--box-create
|
||||
:layout (ebox-normal-layout-create) :width '(0)
|
||||
:overflow 'visible :children (list inline-zero))))
|
||||
(should (= (ebox-string-height (ebox-render zero-parent)) 1))))))
|
||||
|
||||
(ert-deftest ebox-canonical-row-box-retains-and-renders-typed-children ()
|
||||
"A Row Box should retain one Box identity around all typed children."
|
||||
(let* ((left (ebox-text-create :value "A" :source-handle 'left))
|
||||
(right (ebox-text-create :value "B\nB\nB" :source-handle 'right))
|
||||
(box (ebox-box-create :layout (ebox-row-layout-create
|
||||
:item-gap 5 :cross-align 'center)
|
||||
:children (list left right)
|
||||
:source-handle 'row)))
|
||||
(let* ((left-input
|
||||
(ebox-dsl-test--text-create :value "A" :source-handle 'left))
|
||||
(right-input
|
||||
(ebox-dsl-test--text-create :value "B\nB\nB" :source-handle 'right))
|
||||
(left (ebox-dsl-test--root left-input))
|
||||
(right (ebox-dsl-test--root right-input))
|
||||
(box-input
|
||||
(ebox-dsl-test--box-create
|
||||
:layout (ebox-row-layout-create :item-gap 5 :cross-align 'center)
|
||||
:children (list left-input right-input)
|
||||
:source-handle 'row))
|
||||
(box (ebox-dsl-test--root box-input)))
|
||||
(should (ebox-box-node-p box))
|
||||
(should (eq (ebox-node-source-handle box) 'row))
|
||||
(should (eq (ebox-source-handle-id (ebox-node-source-handle box))
|
||||
'row))
|
||||
(should (eq (ebox-layout-config-kind (ebox-box-node-layout box)) 'row))
|
||||
(should (equal (ebox-layout-config-props (ebox-box-node-layout box))
|
||||
'(:item-gap 5 :cross-align center)))
|
||||
@ -401,7 +537,7 @@
|
||||
(should (= (ebox-dsl-test--tree-count box #'ebox-node-kind) 3))
|
||||
(should-not (plist-member box :ebox-content-node))
|
||||
(should (equal (ebox--computed-display box) '(block row)))
|
||||
(let* ((rendered (ebox-render box))
|
||||
(let* ((rendered (ebox-render box-input))
|
||||
(lines (ebox-string-lines rendered))
|
||||
(middle (nth 1 lines)))
|
||||
(should-not (string-match-p "A" (substring-no-properties (car lines))))
|
||||
@ -412,11 +548,15 @@
|
||||
|
||||
(ert-deftest ebox-canonical-column-box-retains-and-renders-typed-children ()
|
||||
"A Column Box should retain one Box identity around all typed children."
|
||||
(let* ((top (ebox-text-create :value "A"))
|
||||
(bottom (ebox-text-create :value "BBB"))
|
||||
(box (ebox-box-create :layout (ebox-column-layout-create
|
||||
:item-gap 1 :cross-align 'end)
|
||||
:children (list top bottom))))
|
||||
(let* ((top-input (ebox-dsl-test--text-create :value "A"))
|
||||
(bottom-input (ebox-dsl-test--text-create :value "BBB"))
|
||||
(top (ebox-dsl-test--root top-input))
|
||||
(bottom (ebox-dsl-test--root bottom-input))
|
||||
(box-input
|
||||
(ebox-dsl-test--box-create
|
||||
:layout (ebox-column-layout-create :item-gap 1 :cross-align 'end)
|
||||
:children (list top-input bottom-input)))
|
||||
(box (ebox-dsl-test--root box-input)))
|
||||
(should (ebox-box-node-p box))
|
||||
(should (eq (ebox-layout-config-kind (ebox-box-node-layout box)) 'column))
|
||||
(should (equal (ebox-layout-config-props (ebox-box-node-layout box))
|
||||
@ -428,8 +568,8 @@
|
||||
(should (= (ebox-dsl-test--tree-count box #'ebox-node-kind) 3))
|
||||
(should-not (plist-member box :ebox-content-node))
|
||||
(should (equal (ebox--computed-display box) '(block column)))
|
||||
(should (= (ebox-string-height (ebox-render box)) 3))
|
||||
(let ((lines (ebox-string-lines (ebox-render box))))
|
||||
(should (= (ebox-string-height (ebox-render box-input)) 3))
|
||||
(let ((lines (ebox-string-lines (ebox-render box-input))))
|
||||
(should (string-prefix-p " " (substring-no-properties (car lines))))
|
||||
(should (= (ebox--string-pixel-width (car lines))
|
||||
(ebox--string-pixel-width (nth 2 lines)))))))
|
||||
@ -446,13 +586,14 @@
|
||||
|
||||
(ert-deftest ebox-canonical-flex-box-uses-one-box-identity-and-child-list ()
|
||||
"A FlexConfig should select the Box algorithm without a Flex runtime node."
|
||||
(let* ((left (ebox-text-create :value "A"))
|
||||
(right (ebox-text-create :value "B"))
|
||||
(let* ((left (ebox-dsl-test--text-create :value "A"))
|
||||
(right (ebox-dsl-test--text-create :value "B"))
|
||||
(layout (ebox-flex-layout-create))
|
||||
(box (ebox-box-create :layout layout
|
||||
:children (list left right)
|
||||
:width '(120)))
|
||||
(rendered (ebox-render box)))
|
||||
(input (ebox-dsl-test--box-create :layout layout
|
||||
:children (list left right)
|
||||
:width '(120)))
|
||||
(box (ebox-dsl-test--root input))
|
||||
(rendered (ebox-render input)))
|
||||
(should
|
||||
(equal (ebox-layout-config-props layout)
|
||||
'(:flex-direction row :flex-wrap nowrap
|
||||
@ -491,28 +632,29 @@
|
||||
'(:width 999)))
|
||||
(setf (ebox-layout-config-kind cross-kind-props) 'row)
|
||||
(should-error
|
||||
(ebox-box-create :layout invalid-direction :children nil)
|
||||
(ebox-dsl-test--box-create :layout invalid-direction :children nil)
|
||||
:type 'error)
|
||||
(should-error
|
||||
(ebox-box-create :layout extra-frame-prop :children nil)
|
||||
(ebox-dsl-test--box-create :layout extra-frame-prop :children nil)
|
||||
:type 'error)
|
||||
(should-error
|
||||
(ebox-box-create :layout cross-kind-props :children nil)
|
||||
(ebox-dsl-test--box-create :layout cross-kind-props :children nil)
|
||||
:type 'error)))
|
||||
|
||||
(ert-deftest ebox-canonical-box-detaches-validated-layout-config ()
|
||||
"Caller and accessor mutations should not alter a constructed Box."
|
||||
(let* ((source-layout (ebox-flex-layout-create))
|
||||
(box (ebox-box-create
|
||||
:layout source-layout
|
||||
:children (list (ebox-text-create :value "A")
|
||||
(ebox-text-create :value "B"))))
|
||||
(input (ebox-dsl-test--box-create
|
||||
:layout source-layout
|
||||
:children (list (ebox-dsl-test--text-create :value "A")
|
||||
(ebox-dsl-test--text-create :value "B"))))
|
||||
(box (ebox-dsl-test--root input))
|
||||
(exposed-layout (ebox-box-node-layout box)))
|
||||
(should-not (fboundp 'ebox-layout-config-validator))
|
||||
(setf (ebox-layout-config-props source-layout) '(:flex-direction bogus))
|
||||
(setf (ebox-layout-config-props exposed-layout) '(:width 999))
|
||||
(should (string= (string-trim-right
|
||||
(substring-no-properties (ebox-render box)))
|
||||
(substring-no-properties (ebox-render input)))
|
||||
"AB"))
|
||||
(should (equal (ebox-layout-config-props (ebox-box-node-layout box))
|
||||
ebox--flex-default-config-props))))
|
||||
@ -536,7 +678,8 @@
|
||||
(should (ebox-flex-layout-config-props-p
|
||||
(ebox-layout-config-props layout)))
|
||||
(should (ebox-box-node-p
|
||||
(ebox-box-create :layout layout :children nil)))))
|
||||
(ebox-dsl-test--root
|
||||
(ebox-dsl-test--box-create :layout layout :children nil))))))
|
||||
|
||||
(ert-deftest ebox-flex-layout-create-validates-values-and-rejects-author-sugar ()
|
||||
"FlexConfig should accept its value domain and reject sugar or foreign props."
|
||||
@ -567,7 +710,9 @@
|
||||
"Boundary validation should preserve physical pixels under non-unit fonts."
|
||||
(cl-letf (((symbol-function 'ebox--space-pixel-width) (lambda () 8)))
|
||||
(let* ((layout (ebox-flex-layout-create :column-gap '(8)))
|
||||
(box (ebox-box-create :layout layout :children nil)))
|
||||
(box (ebox-dsl-test--root
|
||||
(ebox-dsl-test--box-create
|
||||
:layout layout :children nil))))
|
||||
(should (= (plist-get (ebox-layout-config-props layout) :column-gap) 8))
|
||||
(should (= (plist-get
|
||||
(ebox-layout-config-props (ebox-box-node-layout box))
|
||||
@ -577,12 +722,13 @@
|
||||
(ert-deftest ebox-canonical-grid-box-uses-one-box-identity-and-child-list ()
|
||||
"A GridConfig should select the Box algorithm without a Grid runtime node."
|
||||
(let* ((layout (ebox-grid-layout-create))
|
||||
(box (ebox-box-create
|
||||
:layout layout
|
||||
:children (list (ebox-text-create :value "A")
|
||||
(ebox-text-create :value "B"))
|
||||
:width '(120)))
|
||||
(rendered (ebox-render box)))
|
||||
(input (ebox-dsl-test--box-create
|
||||
:layout layout
|
||||
:children (list (ebox-dsl-test--text-create :value "A")
|
||||
(ebox-dsl-test--text-create :value "B"))
|
||||
:width '(120)))
|
||||
(box (ebox-dsl-test--root input))
|
||||
(rendered (ebox-render input)))
|
||||
(should (equal (ebox-layout-config-props layout)
|
||||
ebox--grid-default-config-props))
|
||||
(should-not (plist-member (ebox-layout-config-props layout) :width))
|
||||
@ -610,14 +756,14 @@
|
||||
(ert-deftest ebox-canonical-grid-consumes-direct-child-placement ()
|
||||
"Grid placement should stay on child Boxes without grid-item wrappers."
|
||||
(let* ((later
|
||||
(ebox-box-create :layout (ebox-normal-layout-create)
|
||||
:children (list (ebox-text-create :value "A"))
|
||||
(ebox-dsl-test--box-create :layout (ebox-normal-layout-create)
|
||||
:children (list (ebox-dsl-test--text-create :value "A"))
|
||||
:grid-row 2))
|
||||
(earlier
|
||||
(ebox-box-create :layout (ebox-normal-layout-create)
|
||||
:children (list (ebox-text-create :value "B"))
|
||||
(ebox-dsl-test--box-create :layout (ebox-normal-layout-create)
|
||||
:children (list (ebox-dsl-test--text-create :value "B"))
|
||||
:grid-row 1))
|
||||
(box (ebox-box-create :layout (ebox-grid-layout-create)
|
||||
(box (ebox-dsl-test--box-create :layout (ebox-grid-layout-create)
|
||||
:children (list later earlier)))
|
||||
(rendered (substring-no-properties (ebox-render box))))
|
||||
(should (string=
|
||||
@ -630,12 +776,12 @@
|
||||
"Participation should be legal only under its matching direct parent layout."
|
||||
(cl-labels
|
||||
((child (&rest props)
|
||||
(apply #'ebox-box-create
|
||||
(apply #'ebox-dsl-test--box-create
|
||||
:layout (ebox-normal-layout-create)
|
||||
:children (list (ebox-text-create :value "A"))
|
||||
:children (list (ebox-dsl-test--text-create :value "A"))
|
||||
props))
|
||||
(parent (layout child)
|
||||
(ebox-box-create :layout layout :children (list child))))
|
||||
(ebox-dsl-test--box-create :layout layout :children (list child))))
|
||||
(should (stringp
|
||||
(ebox-render
|
||||
(parent (ebox-flex-layout-create) (child :flex-grow 1)))))
|
||||
@ -656,8 +802,8 @@
|
||||
(ebox-render
|
||||
(parent (ebox-grid-layout-create) (child :flex-grow 1)))
|
||||
:type 'error)
|
||||
(let ((text (ebox-text-create :value "A")))
|
||||
(plist-put text :flex-grow 1)
|
||||
(let ((text (ebox-dsl-test--text-create :value "A")))
|
||||
(plist-put (ebox-dsl-test--root text) :flex-grow 1)
|
||||
(should-error
|
||||
(ebox-render (parent (ebox-flex-layout-create) text)) :type 'error))
|
||||
;; The final candidate is authoritative after ECSS projection too.
|
||||
@ -670,7 +816,9 @@
|
||||
;; Detached Flex geometry retains its final parent during computed style.
|
||||
(let ((ebox-style-stylesheet (ecss-stylesheet-create))
|
||||
(detached (child)))
|
||||
(plist-put detached :class "detached-flex-item")
|
||||
(setq detached
|
||||
(ebox-dsl-test--revise-source
|
||||
detached :class "detached-flex-item"))
|
||||
(ebox-style-add-rule ".detached-flex-item" (list :flex-grow 1))
|
||||
(let ((ebox--render-root-parent-kind 'flex))
|
||||
(should (stringp (ebox-render detached))))
|
||||
@ -678,7 +826,9 @@
|
||||
(should-error (ebox-render detached) :type 'error))
|
||||
(let ((ebox-style-stylesheet (ecss-stylesheet-create))
|
||||
(detached (child)))
|
||||
(plist-put detached :class "detached-flex-item")
|
||||
(setq detached
|
||||
(ebox-dsl-test--revise-source
|
||||
detached :class "detached-flex-item"))
|
||||
(ebox-style-add-rule ".detached-flex-item" (list :grid-row 1))
|
||||
(let ((ebox--render-root-parent-kind 'flex))
|
||||
(should-error (ebox-render detached) :type 'error)))
|
||||
@ -688,9 +838,9 @@
|
||||
(ebox-render
|
||||
(parent
|
||||
(ebox-grid-layout-create)
|
||||
(ebox-box-create
|
||||
(ebox-dsl-test--box-create
|
||||
:layout (ebox-flex-layout-create)
|
||||
:children (list (ebox-text-create :value "A"))
|
||||
:children (list (ebox-dsl-test--text-create :value "A"))
|
||||
:grid-row 1)))))))
|
||||
|
||||
(ert-deftest ebox-canonical-grid-revalidates-the-typed-config-boundary ()
|
||||
@ -702,10 +852,10 @@
|
||||
'(:width 999)))
|
||||
(setf (ebox-layout-config-kind cross-kind-props) 'column)
|
||||
(should-error
|
||||
(ebox-box-create :layout extra-frame-prop :children nil)
|
||||
(ebox-dsl-test--box-create :layout extra-frame-prop :children nil)
|
||||
:type 'error)
|
||||
(should-error
|
||||
(ebox-box-create :layout cross-kind-props :children nil)
|
||||
(ebox-dsl-test--box-create :layout cross-kind-props :children nil)
|
||||
:type 'error)))
|
||||
|
||||
(ert-deftest ebox-grid-layout-create-normalizes-the-complete-config-schema ()
|
||||
@ -733,7 +883,8 @@
|
||||
(should (= (plist-get props :column-gap) 8))
|
||||
(should (ebox-grid-layout-config-props-p props))
|
||||
(should (ebox-box-node-p
|
||||
(ebox-box-create :layout layout :children nil)))))
|
||||
(ebox-dsl-test--root
|
||||
(ebox-dsl-test--box-create :layout layout :children nil))))))
|
||||
|
||||
(ert-deftest ebox-grid-layout-create-validates-values-and-rejects-author-sugar ()
|
||||
"GridConfig should accept its value domain and reject sugar or foreign props."
|
||||
@ -771,7 +922,8 @@
|
||||
(let* ((layout
|
||||
(ebox-grid-layout-create
|
||||
:grid-template-columns '((20)) :column-gap '(8)))
|
||||
(box (ebox-box-create :layout layout :children nil))
|
||||
(box (ebox-dsl-test--root
|
||||
(ebox-dsl-test--box-create :layout layout :children nil)))
|
||||
(props (ebox-layout-config-props (ebox-box-node-layout box))))
|
||||
(should (= (plist-get props :column-gap) 8))
|
||||
(should (= (plist-get (car (plist-get props :grid-template-columns))
|
||||
@ -797,21 +949,21 @@
|
||||
:max (:kind fixed :size 2))
|
||||
:max (:kind auto :factor 0)))))
|
||||
(should-error
|
||||
(ebox-box-create :layout fr-minimum :children nil) :type 'error)
|
||||
(ebox-dsl-test--box-create :layout fr-minimum :children nil) :type 'error)
|
||||
(should-error
|
||||
(ebox-box-create :layout nested-minimum :children nil) :type 'error)))
|
||||
(ebox-dsl-test--box-create :layout nested-minimum :children nil) :type 'error)))
|
||||
|
||||
(ert-deftest ebox-canonical-flex-consumes-direct-child-participation ()
|
||||
"Flex participation should stay on child Boxes without item wrappers."
|
||||
(let* ((left-text (ebox-text-create :value "A"))
|
||||
(right-text (ebox-text-create :value "B"))
|
||||
(left (ebox-box-create :layout (ebox-normal-layout-create)
|
||||
(let* ((left-text (ebox-dsl-test--text-create :value "A"))
|
||||
(right-text (ebox-dsl-test--text-create :value "B"))
|
||||
(left (ebox-dsl-test--box-create :layout (ebox-normal-layout-create)
|
||||
:children (list left-text)
|
||||
:order 2))
|
||||
(right (ebox-box-create :layout (ebox-normal-layout-create)
|
||||
(right (ebox-dsl-test--box-create :layout (ebox-normal-layout-create)
|
||||
:children (list right-text)
|
||||
:order 1))
|
||||
(box (ebox-box-create :layout (ebox-flex-layout-create)
|
||||
(box (ebox-dsl-test--box-create :layout (ebox-flex-layout-create)
|
||||
:children (list left right)))
|
||||
(rendered (substring-no-properties (ebox-render box))))
|
||||
(should (string= (string-trim-right rendered) "BA"))
|
||||
@ -824,11 +976,16 @@
|
||||
|
||||
(ert-deftest ebox-canonical-row-and-column-never-collapse-single-child-boxes ()
|
||||
"A one-child Row or Column should preserve its canonical Box boundary."
|
||||
(let* ((child (ebox-text-create :value "A"))
|
||||
(row (ebox-box-create :layout (ebox-row-layout-create)
|
||||
:children (list child)))
|
||||
(column (ebox-box-create :layout (ebox-column-layout-create)
|
||||
:children (list child))))
|
||||
(let* ((child-input (ebox-dsl-test--text-create :value "A"))
|
||||
(child (ebox-dsl-test--root child-input))
|
||||
(row-input
|
||||
(ebox-dsl-test--box-create :layout (ebox-row-layout-create)
|
||||
:children (list child-input)))
|
||||
(column-input
|
||||
(ebox-dsl-test--box-create :layout (ebox-column-layout-create)
|
||||
:children (list child-input)))
|
||||
(row (ebox-dsl-test--root row-input))
|
||||
(column (ebox-dsl-test--root column-input)))
|
||||
(should (ebox-box-node-p row))
|
||||
(should (ebox-box-node-p column))
|
||||
(should-not (eq row child))
|
||||
@ -837,8 +994,8 @@
|
||||
(should (equal (ebox-tree-node-children column) (list child)))
|
||||
(should (= (ebox-dsl-test--tree-count row (lambda (_node) t)) 2))
|
||||
(should (= (ebox-dsl-test--tree-count column (lambda (_node) t)) 2))
|
||||
(should (string= (ebox-dsl-test--plain row) "A"))
|
||||
(should (string= (ebox-dsl-test--plain column) "A"))))
|
||||
(should (string= (ebox-dsl-test--plain row-input) "A"))
|
||||
(should (string= (ebox-dsl-test--plain column-input) "A"))))
|
||||
|
||||
(ert-deftest ebox-canonical-empty-layout-boxes-have-no-runtime-child ()
|
||||
"Empty layout Boxes should not synthesize layout identities."
|
||||
@ -846,19 +1003,25 @@
|
||||
(ebox-column-layout-create)
|
||||
(ebox-flex-layout-create)
|
||||
(ebox-grid-layout-create)))
|
||||
(let ((box (ebox-box-create :layout layout :children nil)))
|
||||
(let* ((input (ebox-dsl-test--box-create
|
||||
:layout layout :children nil))
|
||||
(box (ebox-dsl-test--root input)))
|
||||
(should (ebox-box-node-p box))
|
||||
(should-not (ebox-tree-node-children box))
|
||||
(should (= (ebox-dsl-test--tree-count box (lambda (_node) t)) 1))
|
||||
(should (= (ebox-dsl-test--tree-count box #'ebox-node-kind) 1))
|
||||
(should (string= (ebox-dsl-test--plain box) "")))))
|
||||
(should (string= (ebox-dsl-test--plain input) "")))))
|
||||
|
||||
(ert-deftest ebox-canonical-box-copy-keeps-one-authoritative-child-list ()
|
||||
"A copied Box should expose exactly its copied runtime children."
|
||||
(let* ((left (ebox-text-create :value "A"))
|
||||
(right (ebox-text-create :value "B"))
|
||||
(source (ebox-box-create :layout (ebox-row-layout-create)
|
||||
:children (list left right)))
|
||||
(let* ((left-input (ebox-dsl-test--text-create :value "A"))
|
||||
(right-input (ebox-dsl-test--text-create :value "B"))
|
||||
(left (ebox-dsl-test--root left-input))
|
||||
(right (ebox-dsl-test--root right-input))
|
||||
(source-input
|
||||
(ebox-dsl-test--box-create :layout (ebox-row-layout-create)
|
||||
:children (list left-input right-input)))
|
||||
(source (ebox-dsl-test--root source-input))
|
||||
(copy (ebox-tree-copy-node-structure source))
|
||||
(copied-children (ebox-tree-node-children copy)))
|
||||
(should (eq copied-children (ebox-box-node-children copy)))
|
||||
@ -870,29 +1033,35 @@
|
||||
|
||||
(ert-deftest ebox-canonical-box-rejects-paint-only-properties ()
|
||||
"Canonical Box geometry should not duplicate paint-only state."
|
||||
(should-error
|
||||
(ebox-box-create :layout (ebox-normal-layout-create)
|
||||
:bgcolor "red")
|
||||
:type 'error))
|
||||
(let* ((builder (ebox-source-builder-create))
|
||||
(handle (ebox-source-builder-bind builder :declarations nil)))
|
||||
(should-error
|
||||
(ebox-box-create
|
||||
:layout (ebox-normal-layout-create) :children nil
|
||||
:source-handle handle
|
||||
:owned-facts (ebox-canonical-facts-from-declarations 'box nil)
|
||||
:bgcolor "red")
|
||||
:type 'error)))
|
||||
|
||||
(ert-deftest ebox-build-projects-source-style-without-polluting-canonical-geometry ()
|
||||
"Track the G4 boundary: author paint must leave canonical Box geometry."
|
||||
:expected-result :failed
|
||||
(let ((node (ebox-build '(box :background-color "red" "Paint"))))
|
||||
(let ((node (ebox-dsl-test--root
|
||||
(ebox-build '(box :background-color "red" "Paint")))))
|
||||
(should-not (ebox-get node :bgcolor))))
|
||||
|
||||
(ert-deftest ebox-canonical-box-requires-unique-reserved-fields ()
|
||||
"Typed Box reserved fields should never be missing or duplicated."
|
||||
(let ((normal (ebox-normal-layout-create)))
|
||||
(should-error (ebox-box-create :children nil) :type 'error)
|
||||
(should-error (ebox-dsl-test--box-create :children nil) :type 'error)
|
||||
(should-error
|
||||
(ebox-box-create :layout normal :layout normal :children nil)
|
||||
(ebox-dsl-test--box-create :layout normal :layout normal :children nil)
|
||||
:type 'error)
|
||||
(should-error
|
||||
(ebox-box-create :layout normal :children nil :children nil)
|
||||
(ebox-dsl-test--box-create :layout normal :children nil :children nil)
|
||||
:type 'error)
|
||||
(should-error
|
||||
(ebox-box-create :layout normal :outer nil :children nil)
|
||||
(ebox-dsl-test--box-create :layout normal :outer nil :children nil)
|
||||
:type 'error)))
|
||||
|
||||
(ert-deftest ebox-build-normalizes-the-complete-author-grammar ()
|
||||
@ -908,24 +1077,36 @@
|
||||
(let* ((form (nth 0 case))
|
||||
(kind (nth 1 case))
|
||||
(layout-kind (nth 2 case))
|
||||
(node (ebox-build form)))
|
||||
(node (ebox-dsl-test--root (ebox-build form))))
|
||||
(should (eq (ebox-node-kind node) kind))
|
||||
(should (symbolp (ebox-node-source-handle node)))
|
||||
(should-not (intern-soft
|
||||
(symbol-name (ebox-node-source-handle node))))
|
||||
(should (ebox-source-handle-p (ebox-node-source-handle node)))
|
||||
(should (symbolp
|
||||
(ebox-source-handle-id (ebox-node-source-handle node))))
|
||||
(should-not
|
||||
(eq (ebox-source-handle-id (ebox-node-source-handle node))
|
||||
(intern-soft
|
||||
(symbol-name
|
||||
(ebox-source-handle-id
|
||||
(ebox-node-source-handle node))))))
|
||||
(when layout-kind
|
||||
(should
|
||||
(eq (ebox-layout-config-kind (ebox-box-node-layout node))
|
||||
layout-kind)))))
|
||||
(let ((inline-row (ebox-build '(row :outer inline "A" "B"))))
|
||||
(should (equal (ebox--computed-display inline-row) '(inline row)))))
|
||||
(should (equal (ebox--computed-display (ebox-dsl-test--root inline-row))
|
||||
'(inline row)))))
|
||||
|
||||
(ert-deftest ebox-build-gives-equal-forms-distinct-source-identities ()
|
||||
"Structurally equal author forms must not share opaque source identity."
|
||||
(let* ((node (ebox-build '(row (box "same") (box "same"))))
|
||||
(let* ((node (ebox-dsl-test--root
|
||||
(ebox-build '(row (box "same") (box "same")))))
|
||||
(children (ebox-box-node-children node)))
|
||||
(should-not (equal (plist-get (car children) :host-ref)
|
||||
(plist-get (cadr children) :host-ref)))))
|
||||
(should-not
|
||||
(equal
|
||||
(ebox-source-handle-id
|
||||
(ebox-node-source-handle (car children)))
|
||||
(ebox-source-handle-id
|
||||
(ebox-node-source-handle (cadr children)))))))
|
||||
|
||||
(ert-deftest ebox-build-validates-form-owned-config-and-direct-participation ()
|
||||
"Layout config belongs to its form; item roles belong to direct child Boxes."
|
||||
@ -934,12 +1115,14 @@
|
||||
'(flex :width '(300) :gap '(1 (5))
|
||||
(box :flex 1 "A")
|
||||
"B")))
|
||||
(flex-child (car (ebox-box-node-children flex)))
|
||||
(flex-child (car (ebox-box-node-children
|
||||
(ebox-dsl-test--root flex))))
|
||||
(grid
|
||||
(ebox-build
|
||||
'(grid :grid-template-columns '((20) (20))
|
||||
(box :grid-column '(1 :span 2) "Header"))))
|
||||
(grid-child (car (ebox-box-node-children grid))))
|
||||
(grid-child (car (ebox-box-node-children
|
||||
(ebox-dsl-test--root grid)))))
|
||||
(should (= (plist-get flex-child :flex-grow) 1))
|
||||
(should (equal (plist-get grid-child :grid-column) '(1 :span 2)))
|
||||
(should (string-match-p "A.*B"
|
||||
@ -977,7 +1160,7 @@
|
||||
(unknown "A")))
|
||||
(should-error (ebox-build form) :type 'error))
|
||||
(should-error
|
||||
(ebox-build (ebox-test-box :content "raw runtime node"))
|
||||
(ebox-build (ebox-test-box (ebox-test-text "raw runtime node")))
|
||||
:type 'error))
|
||||
|
||||
(provide 'ebox-dsl-tests)
|
||||
|
||||
@ -11,21 +11,59 @@
|
||||
(require 'ebox)
|
||||
|
||||
(defconst ebox-test--source-fields
|
||||
'(:source-handle :key :class :id)
|
||||
"Canonical source fields accepted by typed fixture nodes.")
|
||||
'(:source-identity :key :class :id
|
||||
:selector-state :selector-attributes)
|
||||
"Author source fields owned by opaque fixture source handles.")
|
||||
|
||||
(defconst ebox-test--direct-fields
|
||||
'(:region-id :selector-state :selector-attributes :surface-properties
|
||||
:scroll-offset :ebox-scroll-offset-controlled-p)
|
||||
'(:region-id :surface-properties :scroll-offset
|
||||
:ebox-scroll-offset-controlled-p)
|
||||
"Private runtime fields used only by focused backend tests.")
|
||||
|
||||
(defconst ebox-test--non-style-fields
|
||||
(append '(:source-handle :host-ref :key :class :id
|
||||
:outer :content :ebox-content-node
|
||||
:item-gap :cross-align)
|
||||
(append ebox-test--source-fields
|
||||
'(:outer :item-gap :cross-align)
|
||||
ebox-test--direct-fields)
|
||||
"Fixture fields that do not enter the canonical Ebox style schema.")
|
||||
|
||||
(defun ebox-test-root (input)
|
||||
"Return the single canonical root owned by fixture INPUT."
|
||||
(ebox-canonical-input--single-root input "Ebox test fixture"))
|
||||
|
||||
(defun ebox-test-forest (input)
|
||||
"Return fixture INPUT's ordered canonical forest."
|
||||
(unless (ebox-canonical-input-p input)
|
||||
(signal 'wrong-type-argument (list 'ebox-canonical-input-p input)))
|
||||
(copy-sequence (ebox-canonical-input--nodes input)))
|
||||
|
||||
(defun ebox-test-source-index (input)
|
||||
"Return the immutable source generation owned by fixture INPUT."
|
||||
(unless (ebox-canonical-input-p input)
|
||||
(signal 'wrong-type-argument (list 'ebox-canonical-input-p input)))
|
||||
(ebox-canonical-input--source-index input))
|
||||
|
||||
(defun ebox-test-child-range (range-ref &rest inputs)
|
||||
"Return a test Range descriptor over canonical INPUTS.
|
||||
The descriptor is consumed only by a fixture parent, which imports every input
|
||||
source generation before lowering the descriptor to raw canonical nodes."
|
||||
(unless range-ref
|
||||
(error "Ebox test Range ref must be non-nil"))
|
||||
(dolist (input inputs)
|
||||
(unless (ebox-canonical-input-p input)
|
||||
(signal 'wrong-type-argument (list 'ebox-canonical-input-p input))))
|
||||
(ebox-child-range--descriptor-create range-ref inputs))
|
||||
|
||||
(defun ebox-test-forest-input (&rest inputs)
|
||||
"Combine canonical INPUTS into one ordered forest input."
|
||||
(let ((builder (ebox-source-builder-create))
|
||||
nodes)
|
||||
(dolist (input inputs)
|
||||
(unless (ebox-canonical-input-p input)
|
||||
(signal 'wrong-type-argument (list 'ebox-canonical-input-p input)))
|
||||
(ebox-source-builder-import builder (ebox-test-source-index input))
|
||||
(setq nodes (nconc nodes (ebox-test-forest input))))
|
||||
(ebox-canonical-input-create nodes (ebox-source-builder-finish builder))))
|
||||
|
||||
(defun ebox-test--keep (plist keys)
|
||||
"Return PLIST entries whose keys occur in KEYS."
|
||||
(cl-loop for (key value) on plist by #'cddr
|
||||
@ -49,31 +87,24 @@
|
||||
(cl-loop for (key value) on plist by #'cddr
|
||||
unless (memq key keys) append (list key value)))
|
||||
|
||||
(defun ebox-test--unquote (value)
|
||||
"Return VALUE without one inert source-level quote wrapper."
|
||||
(if (and (consp value)
|
||||
(eq (car value) 'quote)
|
||||
(null (cddr value)))
|
||||
(cadr value)
|
||||
value))
|
||||
|
||||
(defun ebox-test--normalize-properties (props)
|
||||
"Return fixture PROPS with source literals normalized.
|
||||
The typed author boundary spells explicit no-wrap as `none'; old backend
|
||||
fixtures used nil for the same engine condition."
|
||||
(cl-loop for (key value) on props by #'cddr
|
||||
append (list key
|
||||
(if (and (eq key :wrap-mode) (null value))
|
||||
'none
|
||||
(ebox-test--unquote value)))))
|
||||
|
||||
(defun ebox-test--source-properties (props)
|
||||
"Return typed source metadata projected from fixture PROPS."
|
||||
(let ((source (ebox-test--keep props '(:key :class :id))))
|
||||
(when-let* ((handle (or (plist-get props :source-handle)
|
||||
(plist-get props :host-ref))))
|
||||
(setq source (append source (list :source-handle handle))))
|
||||
source))
|
||||
(defun ebox-test--source-handle (builder props declarations)
|
||||
"Return one opaque fixture source for PROPS and DECLARATIONS.
|
||||
The optional `:source-identity' fixture property explicitly names its stable
|
||||
author identity; opaque source handles remain internal to SourceBuilder."
|
||||
(let* ((identity (plist-get props :source-identity))
|
||||
(arguments
|
||||
(list :key (plist-get props :key)
|
||||
:id (plist-get props :id)
|
||||
:class (plist-get props :class)
|
||||
:selector-state (plist-get props :selector-state)
|
||||
:selector-attributes (plist-get props :selector-attributes)
|
||||
:declarations declarations
|
||||
:provenance '(:adapter ebox-test))))
|
||||
(apply #'ebox-source-builder-bind
|
||||
builder
|
||||
(append
|
||||
(and identity (list :identity identity))
|
||||
arguments))))
|
||||
|
||||
(defun ebox-test--declarations (tag props)
|
||||
"Return canonical TAG declarations from evaluated fixture PROPS."
|
||||
@ -92,43 +123,61 @@ fixtures used nil for the same engine condition."
|
||||
(memq (plist-get property :name)
|
||||
(ebox-layout-config-property-names tag)))))))
|
||||
|
||||
(defun ebox-test--text (value &optional props)
|
||||
"Return one canonical Text fixture for VALUE and author PROPS."
|
||||
(let ((declarations (ebox-test--declarations 'text props)))
|
||||
(apply #'ebox-text-create
|
||||
(append (list :value value :declarations declarations)
|
||||
(ebox-test--source-properties props)))))
|
||||
(defun ebox-test-text (value &rest props)
|
||||
"Return one canonical Text input for VALUE and author PROPS."
|
||||
(let* ((builder (ebox-source-builder-create))
|
||||
(declarations (ebox-test--declarations 'text props))
|
||||
(node
|
||||
(ebox-text-create
|
||||
:value value
|
||||
:owned-facts
|
||||
(ebox-canonical-facts-from-declarations 'text declarations)
|
||||
:source-handle
|
||||
(ebox-test--source-handle builder props declarations)))
|
||||
(index (ebox-source-builder-finish builder)))
|
||||
(ebox-canonical-input-create (list node) index)))
|
||||
|
||||
(defun ebox-test--normalize-child (builder child)
|
||||
"Import fixture CHILD into BUILDER and return its canonical node value."
|
||||
(cond
|
||||
((ebox-canonical-input-p child)
|
||||
(ebox-source-builder-import builder (ebox-test-source-index child))
|
||||
(ebox-test-root child))
|
||||
((ebox-child-range--descriptor-p child)
|
||||
(ebox-child-range--descriptor-create
|
||||
(ebox-child-range--descriptor-ref child)
|
||||
(mapcar (lambda (item)
|
||||
(ebox-test--normalize-child builder item))
|
||||
(ebox-child-range--descriptor-items child))))
|
||||
(t
|
||||
(error "Fixture child must be a canonical input or Range: %S" child))))
|
||||
|
||||
(defun ebox-test--box-node (tag props children)
|
||||
"Return canonical Box form TAG from PROPS and CHILDREN."
|
||||
(let* ((props (ebox-test--normalize-properties props))
|
||||
"Return canonical Box input TAG from PROPS and CHILDREN."
|
||||
(let* ((builder (ebox-source-builder-create))
|
||||
(declarations (ebox-test--declarations tag props))
|
||||
(nodes (mapcar (lambda (child)
|
||||
(ebox-test--normalize-child builder child))
|
||||
children))
|
||||
(node
|
||||
(apply #'ebox-box-create
|
||||
(append
|
||||
(list :layout (ebox-test--layout tag props declarations)
|
||||
:outer (or (plist-get props :outer) 'block)
|
||||
:children children
|
||||
:declarations declarations)
|
||||
(ebox-test--source-properties props)))))
|
||||
(let ((ebox-canonical--source-builder builder))
|
||||
(ebox-box-create
|
||||
:layout (ebox-test--layout tag props declarations)
|
||||
:outer (or (plist-get props :outer) 'block)
|
||||
:children nodes
|
||||
:owned-facts
|
||||
(ebox-canonical-facts-from-declarations tag declarations)
|
||||
:source-handle
|
||||
(ebox-test--source-handle builder props declarations))))
|
||||
(index (ebox-source-builder-finish builder)))
|
||||
(dolist (key ebox-test--direct-fields)
|
||||
(when (plist-member props key)
|
||||
(plist-put node key (copy-tree (plist-get props key)))))
|
||||
node))
|
||||
(ebox-canonical-input-create (list node) index)))
|
||||
|
||||
(defun ebox-test-box (&rest props)
|
||||
"Return one typed Normal Box fixture from evaluated PROPS."
|
||||
(let* ((content-present-p (plist-member props :content))
|
||||
(content (plist-get props :content))
|
||||
(child (plist-get props :ebox-content-node))
|
||||
(children
|
||||
(cond
|
||||
(child (list child))
|
||||
(content-present-p
|
||||
(if (stringp content)
|
||||
(list (ebox-test--text content))
|
||||
(error "Fixture :content must be a string: %S" content)))
|
||||
(t nil))))
|
||||
(defun ebox-test-box (&rest items)
|
||||
"Return one typed Normal Box fixture from evaluated ITEMS."
|
||||
(pcase-let ((`(,props . ,children) (ebox-test--split-items items)))
|
||||
(ebox-test--box-node 'box props children)))
|
||||
|
||||
(defun ebox-test-row (&rest items)
|
||||
@ -141,18 +190,6 @@ fixtures used nil for the same engine condition."
|
||||
(pcase-let ((`(,props . ,children) (ebox-test--split-items items)))
|
||||
(ebox-test--box-node 'column props (delq nil children))))
|
||||
|
||||
(defun ebox-test-concat (left right)
|
||||
"Return a two-child typed Row fixture."
|
||||
(ebox-test-row left right))
|
||||
|
||||
(defun ebox-test-stack (top bottom)
|
||||
"Return a two-child typed Column fixture."
|
||||
(ebox-test-column top bottom))
|
||||
|
||||
(defun ebox-test-spacer (&rest props)
|
||||
"Return an empty typed Normal Box fixture."
|
||||
(apply #'ebox-test-box props))
|
||||
|
||||
(defun ebox-test-flex (&rest items)
|
||||
"Return one typed Flex fixture from evaluated ITEMS."
|
||||
(pcase-let ((`(,props . ,children) (ebox-test--split-items items)))
|
||||
@ -168,85 +205,34 @@ fixtures used nil for the same engine condition."
|
||||
(list 'fr factor))
|
||||
|
||||
(defun ebox-test-flex-item (node &rest props)
|
||||
"Return NODE with direct Flex participation and Box properties from PROPS."
|
||||
(setq props (ebox-test--normalize-properties props))
|
||||
(unless (ebox-box-node-p node)
|
||||
"Return INPUT with direct Flex participation and Box properties from PROPS."
|
||||
(unless (ebox-canonical-input-p node)
|
||||
(signal 'wrong-type-argument (list 'ebox-canonical-input-p node)))
|
||||
(unless (ebox-box-node-p (ebox-test-root node))
|
||||
(setq node (ebox-test--box-node 'box nil (list node))))
|
||||
(let* ((layout (ebox-box-node-layout node))
|
||||
(let* ((source-index (ebox-test-source-index node))
|
||||
(node (ebox-test-root node))
|
||||
(source (or (ebox-node-source-handle node)
|
||||
(error "Fixture Box lacks an opaque source handle: %S"
|
||||
node)))
|
||||
(layout (ebox-box-node-layout node))
|
||||
(children (ebox-box-node-children node))
|
||||
(base (ebox-style-node-declarations node))
|
||||
(base (ebox-style-node-declarations source-index node))
|
||||
(extra (ebox-test--declarations 'box props))
|
||||
(declarations (ebox-style-merge-declarations base extra))
|
||||
(binding
|
||||
(ebox-source-index-rebind
|
||||
source-index source :declarations declarations))
|
||||
(copy
|
||||
(apply #'ebox-box-create
|
||||
(append
|
||||
(list :layout layout :children children
|
||||
:outer (ebox-tree-display-outer node)
|
||||
:declarations
|
||||
(ebox-style-merge-declarations base extra))
|
||||
(ebox-test--source-properties node)))))
|
||||
copy))
|
||||
|
||||
(defun ebox-test--build-item (items)
|
||||
"Return one typed direct-child Box from old backend fixture ITEMS.
|
||||
`item' never reaches production input; this test helper only projects its
|
||||
participation properties onto the material child being exercised."
|
||||
(pcase-let* ((`(,props . ,children) (ebox-test--split-items items))
|
||||
(props (ebox-test--normalize-properties props)))
|
||||
(unless (= (length children) 1)
|
||||
(error "Ebox test item requires exactly one child: %S" items))
|
||||
(apply #'ebox-test-flex-item
|
||||
(ebox-test-build (car children)) props)))
|
||||
|
||||
(defun ebox-test--build-box-form (tag items)
|
||||
"Return one typed TAG Box fixture from old backend fixture ITEMS."
|
||||
(pcase-let* ((`(,props . ,child-forms) (ebox-test--split-items items))
|
||||
(props (ebox-test--normalize-properties props))
|
||||
(content-present-p (plist-member props :content))
|
||||
(content (plist-get props :content))
|
||||
(content-node (plist-get props :ebox-content-node))
|
||||
(props (ebox-test--without
|
||||
props '(:content :ebox-content-node)))
|
||||
(children (mapcar #'ebox-test-build child-forms)))
|
||||
(when content-node
|
||||
(setq children (append children (list content-node))))
|
||||
(when content-present-p
|
||||
(unless (stringp content)
|
||||
(error "Ebox test :content must be a string: %S" content))
|
||||
(setq children (append children (list (ebox-test--text content)))))
|
||||
(ebox-test--box-node tag props children)))
|
||||
|
||||
(defun ebox-test-build (form)
|
||||
"Build canonical typed nodes from one backend test FORM.
|
||||
|
||||
This is deliberately not an author DSL. It isolates retained-layout and
|
||||
publication tests from source parsing while their compact historical fixture
|
||||
data is migrated. Production `ebox-build' remains strict and never accepts
|
||||
`:content', `item', `concat', `stack', or `spacer'."
|
||||
(cond
|
||||
((or (ebox-text-node-p form) (ebox-box-node-p form)) form)
|
||||
((stringp form) (ebox-test--text form))
|
||||
((not (and (consp form) (symbolp (car form))))
|
||||
(error "Invalid Ebox typed test form: %S" form))
|
||||
((eq (car form) 'text)
|
||||
(pcase-let* ((`(,props . ,payloads)
|
||||
(ebox-test--split-items (cdr form)))
|
||||
(props (ebox-test--normalize-properties props)))
|
||||
(unless (and (= (length payloads) 1)
|
||||
(stringp (car payloads)))
|
||||
(error "Ebox test text requires exactly one string payload: %S" form))
|
||||
(ebox-test--text (car payloads) props)))
|
||||
((memq (car form) '(box row column flex grid))
|
||||
(ebox-test--build-box-form (car form) (cdr form)))
|
||||
((eq (car form) 'item)
|
||||
(ebox-test--build-item (cdr form)))
|
||||
((eq (car form) 'concat)
|
||||
(ebox-test--build-box-form 'row (cdr form)))
|
||||
((eq (car form) 'stack)
|
||||
(ebox-test--build-box-form 'column (cdr form)))
|
||||
((eq (car form) 'spacer)
|
||||
(ebox-test--build-box-form 'box (cdr form)))
|
||||
(t
|
||||
(error "Unknown Ebox typed test form: %S" (car form)))))
|
||||
(ebox-box-create
|
||||
:layout layout
|
||||
:children children
|
||||
:outer (ebox-tree-display-outer node)
|
||||
:owned-facts
|
||||
(ebox-canonical-facts-from-declarations
|
||||
'box declarations)
|
||||
:source-handle (cdr binding))))
|
||||
(ebox-canonical-input-create (list copy) (car binding))))
|
||||
|
||||
(provide 'ebox-fixtures)
|
||||
|
||||
|
||||
@ -61,15 +61,16 @@
|
||||
(setq pos end)))
|
||||
(max max-width current-width)))
|
||||
|
||||
(defun ebox-flex-test--content-widths (node)
|
||||
"Return max content widths for NODE's direct material children."
|
||||
(defun ebox-flex-test--content-widths (input)
|
||||
"Return max content widths for INPUT's direct material children."
|
||||
(let ((node (ebox-test-root input)))
|
||||
(let* ((ids (mapcar (lambda (child)
|
||||
(car (ebox-region-ids child)))
|
||||
(ebox-box-node-children node)))
|
||||
(rendered (ebox-flex-test--render node)))
|
||||
(rendered (ebox-flex-test--render input)))
|
||||
(mapcar (lambda (id)
|
||||
(ebox-flex-test--region-owner-width rendered id))
|
||||
ids)))
|
||||
ids))))
|
||||
|
||||
(defun ebox-flex-test--face-has-key-value-p (face key value)
|
||||
"Return non-nil when FACE contains KEY with VALUE."
|
||||
@ -147,8 +148,8 @@
|
||||
(layout
|
||||
(ebox-test-flex
|
||||
(ebox-test-box :id "rule" :class 'grow
|
||||
:content "Rule" :width '(40))
|
||||
(ebox-test-box :id "inline" :content "Inline" :width '(40)
|
||||
(ebox-test-text "Rule") :width '(40))
|
||||
(ebox-test-box :id "inline" (ebox-test-text "Inline") :width '(40)
|
||||
:flex-grow 2)))
|
||||
(buffer (generate-new-buffer " *ebox-flex-style-reset*")))
|
||||
(unwind-protect
|
||||
@ -175,8 +176,8 @@
|
||||
"Flex item participation props may live directly on child boxes."
|
||||
(let* ((layout (ebox-test-flex
|
||||
:width 300
|
||||
(ebox-test-box :content "A" :flex-grow 1 :flex-basis '(80))
|
||||
(ebox-test-box :content "B" :flex-grow 1 :flex-basis '(80))))
|
||||
(ebox-test-box (ebox-test-text "A") :flex-grow 1 :flex-basis '(80))
|
||||
(ebox-test-box (ebox-test-text "B") :flex-grow 1 :flex-basis '(80))))
|
||||
(rendered (let ((ebox-viewport-width 300))
|
||||
(ebox-render layout)))
|
||||
(widths (mapcar #'ebox--string-pixel-width
|
||||
@ -190,10 +191,10 @@
|
||||
(let* ((layout
|
||||
(ebox-test-flex :width '(300)
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "A" :width '(80))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(80))
|
||||
:flex-grow 1 :flex-basis '(80))
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "B" :width '(80))
|
||||
(ebox-test-box (ebox-test-text "B") :width '(80))
|
||||
:flex-grow 2 :flex-basis '(80))))
|
||||
(widths (ebox-flex-test--content-widths layout)))
|
||||
(should (= (length widths) 2))
|
||||
@ -205,7 +206,7 @@
|
||||
"A row flex target should include a border-box item's inline margins."
|
||||
(let ((layout
|
||||
(ebox-test-flex :width '(200) :align-items 'flex-start
|
||||
(ebox-test-box :content "A"
|
||||
(ebox-test-box (ebox-test-text "A")
|
||||
:width '(40)
|
||||
:box-sizing 'border-box
|
||||
:margin-left '(20)
|
||||
@ -219,7 +220,7 @@
|
||||
(ebox-test-flex :width '(80) :height 6
|
||||
:flex-direction 'column
|
||||
:align-items 'flex-start
|
||||
(ebox-test-box :content "A"
|
||||
(ebox-test-box (ebox-test-text "A")
|
||||
:width '(40)
|
||||
:height 1
|
||||
:box-sizing 'border-box
|
||||
@ -232,7 +233,7 @@
|
||||
"Content-box flex targets should subtract padding, borders, and margins."
|
||||
(let ((layout
|
||||
(ebox-test-flex :width '(200) :align-items 'flex-start
|
||||
(ebox-test-box :content "A"
|
||||
(ebox-test-box (ebox-test-text "A")
|
||||
:width '(40)
|
||||
:box-sizing 'content-box
|
||||
:padding-left '(10)
|
||||
@ -249,10 +250,10 @@
|
||||
(let* ((layout
|
||||
(ebox-test-flex :width '(150)
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "A" :width '(100))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(100))
|
||||
:flex-shrink 1 :flex-basis '(100))
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "B" :width '(100))
|
||||
(ebox-test-box (ebox-test-text "B") :width '(100))
|
||||
:flex-shrink 3 :flex-basis '(100))))
|
||||
(widths (ebox-flex-test--content-widths layout)))
|
||||
(should (= (length widths) 2))
|
||||
@ -267,10 +268,10 @@
|
||||
(layout
|
||||
(ebox-test-flex :width '(300)
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content content :width '(160) :wrap-mode 'word)
|
||||
(ebox-test-box (ebox-test-text content) :width '(160) :wrap-mode 'word)
|
||||
:flex-basis 'content)
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content content :width '(160) :wrap-mode 'word)
|
||||
(ebox-test-box (ebox-test-text content) :width '(160) :wrap-mode 'word)
|
||||
:flex-basis 'min-content)))
|
||||
(widths (ebox-flex-test--content-widths layout)))
|
||||
(should (equal widths (list max-content min-content)))))
|
||||
@ -280,10 +281,10 @@
|
||||
(let* ((layout
|
||||
(ebox-test-flex :width '(200)
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "A" :width '(60) :max-width '(80))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(60) :max-width '(80))
|
||||
:flex-grow 1 :flex-basis '(60))
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "B" :width '(60))
|
||||
(ebox-test-box (ebox-test-text "B") :width '(60))
|
||||
:flex-grow 1 :flex-basis '(60))))
|
||||
(widths (ebox-flex-test--content-widths layout)))
|
||||
(should (equal widths '(80 120)))
|
||||
@ -294,10 +295,10 @@
|
||||
(let* ((layout
|
||||
(ebox-test-flex :width '(200)
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "A" :width '(50) :min-width '(80))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(50) :min-width '(80))
|
||||
:flex-grow 1 :flex-basis '(50))
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "B" :width '(50))
|
||||
(ebox-test-box (ebox-test-text "B") :width '(50))
|
||||
:flex-grow 1 :flex-basis '(50))))
|
||||
(widths (ebox-flex-test--content-widths layout)))
|
||||
(should (equal widths '(100 100)))
|
||||
@ -308,10 +309,10 @@
|
||||
(let* ((layout
|
||||
(ebox-test-flex :width '(300)
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "A" :width '(50))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(50))
|
||||
:flex-grow 0.25)
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "B" :width '(50))
|
||||
(ebox-test-box (ebox-test-text "B") :width '(50))
|
||||
:flex-grow 0.25)))
|
||||
(widths (ebox-flex-test--content-widths layout)))
|
||||
(should (equal widths '(100 100)))
|
||||
@ -322,10 +323,10 @@
|
||||
(let* ((layout
|
||||
(ebox-test-flex :width '(150)
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "A" :width '(100) :min-width '(80))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(100) :min-width '(80))
|
||||
:flex-shrink 1 :flex-basis '(100))
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "B" :width '(100))
|
||||
(ebox-test-box (ebox-test-text "B") :width '(100))
|
||||
:flex-shrink 1 :flex-basis '(100))))
|
||||
(widths (ebox-flex-test--content-widths layout)))
|
||||
(should (equal widths '(80 70)))
|
||||
@ -336,11 +337,11 @@
|
||||
(let* ((layout
|
||||
(ebox-test-flex :width '(150)
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "A" :width '(100) :max-width '(80)
|
||||
(ebox-test-box (ebox-test-text "A") :width '(100) :max-width '(80)
|
||||
:wrap-mode 'word)
|
||||
:flex-shrink 1 :flex-basis '(100))
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "B" :width '(100) :wrap-mode 'word)
|
||||
(ebox-test-box (ebox-test-text "B") :width '(100) :wrap-mode 'word)
|
||||
:flex-shrink 1 :flex-basis '(100))))
|
||||
(widths (ebox-flex-test--content-widths layout)))
|
||||
(should (equal widths '(75 75)))
|
||||
@ -351,10 +352,10 @@
|
||||
(let* ((layout
|
||||
(ebox-test-flex :width '(150)
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "A" :width '(100))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(100))
|
||||
:flex-shrink 0.25)
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "B" :width '(100))
|
||||
(ebox-test-box (ebox-test-text "B") :width '(100))
|
||||
:flex-shrink 0.25)))
|
||||
(widths (ebox-flex-test--content-widths layout)))
|
||||
(should (equal widths '(87 88)))
|
||||
@ -365,24 +366,24 @@
|
||||
(should-error
|
||||
(ebox-flex-test--render
|
||||
(ebox-test-flex :width '(-1)
|
||||
(ebox-test-box :content "A" :width '(20))))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(20))))
|
||||
:type 'error)
|
||||
(should-error
|
||||
(ebox-flex-test--render
|
||||
(ebox-test-flex
|
||||
(ebox-test-flex-item (ebox-test-box :content "A" :width '(20))
|
||||
(ebox-test-flex-item (ebox-test-box (ebox-test-text "A") :width '(20))
|
||||
:flex-grow -1)))
|
||||
:type 'error)
|
||||
(should-error
|
||||
(ebox-flex-test--render
|
||||
(ebox-test-flex
|
||||
(ebox-test-flex-item (ebox-test-box :content "A" :width '(20))
|
||||
(ebox-test-flex-item (ebox-test-box (ebox-test-text "A") :width '(20))
|
||||
:flex-shrink -1)))
|
||||
:type 'error)
|
||||
(should-error
|
||||
(ebox-flex-test--render
|
||||
(ebox-test-flex
|
||||
(ebox-test-flex-item (ebox-test-box :content "A" :width '(20))
|
||||
(ebox-test-flex-item (ebox-test-box (ebox-test-text "A") :width '(20))
|
||||
:flex-basis '(-1))))
|
||||
:type 'error))
|
||||
|
||||
@ -390,9 +391,9 @@
|
||||
"Row wrap should create new flex lines using column gaps."
|
||||
(let* ((layout
|
||||
(ebox-test-flex :width '(210) :flex-wrap 'wrap :column-gap '(10)
|
||||
(ebox-test-box :content "A" :width '(100))
|
||||
(ebox-test-box :content "B" :width '(100))
|
||||
(ebox-test-box :content "C" :width '(100))))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(100))
|
||||
(ebox-test-box (ebox-test-text "B") :width '(100))
|
||||
(ebox-test-box (ebox-test-text "C") :width '(100))))
|
||||
(lines (ebox-flex-test--plain-lines layout)))
|
||||
(should (= (length lines) 2))
|
||||
(should (string-match-p "A.*B" (car lines)))
|
||||
@ -506,10 +507,10 @@
|
||||
(let* ((layout
|
||||
(ebox-test-flex :width '(210) :flex-wrap 'wrap :column-gap '(10)
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "A" :width '(200) :max-width '(100))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(200) :max-width '(100))
|
||||
:flex-basis '(200))
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "B" :width '(200) :max-width '(100))
|
||||
(ebox-test-box (ebox-test-text "B") :width '(200) :max-width '(100))
|
||||
:flex-basis '(200))))
|
||||
(lines (ebox-flex-test--plain-lines layout))
|
||||
(widths (ebox-flex-test--content-widths layout)))
|
||||
@ -522,9 +523,9 @@
|
||||
(let* ((ebox-viewport-width 240)
|
||||
(layout
|
||||
(ebox-test-flex :flex-wrap 'wrap :column-gap '(10)
|
||||
(ebox-test-box :content "A" :width '(100))
|
||||
(ebox-test-box :content "B" :width '(100))
|
||||
(ebox-test-box :content "C" :width '(100)))))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(100))
|
||||
(ebox-test-box (ebox-test-text "B") :width '(100))
|
||||
(ebox-test-box (ebox-test-text "C") :width '(100)))))
|
||||
(should (equal (ebox-flex-test--line-widths layout) '(240 240)))))
|
||||
|
||||
(ert-deftest ebox-flex-stretch-width-is-independent-of-container-wrapper ()
|
||||
@ -532,10 +533,10 @@
|
||||
(let* ((ebox-viewport-width 200)
|
||||
(plain
|
||||
(ebox-test-flex :width 'stretch
|
||||
(ebox-test-box :content "A" :width '(40))))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(40))))
|
||||
(padded
|
||||
(ebox-test-flex :width 'stretch :padding 0
|
||||
(ebox-test-box :content "A" :width '(40)))))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(40)))))
|
||||
(should (equal (mapcar #'ebox-flex-test--line-widths
|
||||
(list plain padded))
|
||||
'((200) (200))))))
|
||||
@ -548,10 +549,10 @@
|
||||
(lambda (width)
|
||||
(let ((plain
|
||||
(ebox-test-flex :width width
|
||||
(ebox-test-box :content "A" :width '(40))))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(40))))
|
||||
(padded
|
||||
(ebox-test-flex :width width :padding 0
|
||||
(ebox-test-box :content "A" :width '(40)))))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(40)))))
|
||||
(list width plain padded)))
|
||||
'(stretch auto)))
|
||||
(results
|
||||
@ -573,8 +574,8 @@
|
||||
:border-left '(10)
|
||||
:border-right '(10)
|
||||
:justify-content 'space-between
|
||||
(ebox-test-box :content "A" :width '(40))
|
||||
(ebox-test-box :content "B" :width '(40))))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(40))
|
||||
(ebox-test-box (ebox-test-text "B") :width '(40))))
|
||||
(rendered (ebox-flex-test--render layout))
|
||||
(display-widths
|
||||
(cl-loop for index below (length rendered)
|
||||
@ -594,8 +595,8 @@
|
||||
:border-left '(10)
|
||||
:border-right '(10)
|
||||
:flex-wrap 'wrap
|
||||
(ebox-test-flex-item (ebox-test-box :content "A" :width '(180)))
|
||||
(ebox-test-flex-item (ebox-test-box :content "B" :width '(180))))))
|
||||
(ebox-test-flex-item (ebox-test-box (ebox-test-text "A") :width '(180)))
|
||||
(ebox-test-flex-item (ebox-test-box (ebox-test-text "B") :width '(180))))))
|
||||
(should (cl-every (lambda (width) (<= width ebox-viewport-width))
|
||||
(ebox-flex-test--line-widths layout)))))
|
||||
|
||||
@ -603,10 +604,10 @@
|
||||
"Auto height should preserve natural child height across wrapper branches."
|
||||
(let* ((plain
|
||||
(ebox-test-flex :width '(80) :height 'auto
|
||||
(ebox-test-box :content "A\nB" :width '(40))))
|
||||
(ebox-test-box (ebox-test-text "A\nB") :width '(40))))
|
||||
(padded
|
||||
(ebox-test-flex :width '(80) :height 'auto :padding 0
|
||||
(ebox-test-box :content "A\nB" :width '(40))))
|
||||
(ebox-test-box (ebox-test-text "A\nB") :width '(40))))
|
||||
(results
|
||||
(mapcar
|
||||
(lambda (layout)
|
||||
@ -618,45 +619,50 @@
|
||||
|
||||
(ert-deftest ebox-flex-reuses-measured-non-box-child-render ()
|
||||
"Flex should not fully rerender a measured non-box child during one render."
|
||||
(let* ((child (plist-put
|
||||
(ebox-test-column
|
||||
(ebox-test-box :content "A" :width '(100))
|
||||
(ebox-test-box :content "B" :width '(100)))
|
||||
:key 'measured-child))
|
||||
(let* ((child (ebox-test-column
|
||||
:key 'measured-child
|
||||
(ebox-test-box (ebox-test-text "A") :width '(100))
|
||||
(ebox-test-box (ebox-test-text "B") :width '(100))))
|
||||
(layout (ebox-test-flex :width '(240) :flex-wrap 'wrap
|
||||
child
|
||||
(ebox-test-column
|
||||
(ebox-test-box :content "C" :width '(100))
|
||||
(ebox-test-box :content "D" :width '(100)))))
|
||||
(ebox-test-box (ebox-test-text "C") :width '(100))
|
||||
(ebox-test-box (ebox-test-text "D") :width '(100)))))
|
||||
(target-handle
|
||||
(ebox-node-source-handle (ebox-test-root child)))
|
||||
(calls 0)
|
||||
(original-render (symbol-function 'ebox-render)))
|
||||
(cl-letf (((symbol-function 'ebox-render)
|
||||
(lambda (node)
|
||||
(when (eq (plist-get node :key) 'measured-child)
|
||||
(original-render
|
||||
(symbol-function 'ebox--flex-render-source-for-measurement)))
|
||||
(cl-letf (((symbol-function 'ebox--flex-render-source-for-measurement)
|
||||
(lambda (node viewport)
|
||||
(when (eq (ebox-node-source-handle node) target-handle)
|
||||
(cl-incf calls))
|
||||
(funcall original-render node))))
|
||||
(funcall original-render node viewport))))
|
||||
(ebox-render layout))
|
||||
(should (= calls 1))))
|
||||
|
||||
(ert-deftest ebox-flex-reuses-natural-non-box-render-before-cross-padding ()
|
||||
"Final cross padding should reuse a naturally sized non-box render."
|
||||
(let* ((source (plist-put
|
||||
(ebox-test-column
|
||||
(ebox-test-box :content "A" :width '(80))
|
||||
(ebox-test-box :content "A2" :width '(80)))
|
||||
:key 'natural-child))
|
||||
(let* ((source (ebox-test-column
|
||||
:key 'natural-child
|
||||
(ebox-test-box (ebox-test-text "A") :width '(80))
|
||||
(ebox-test-box (ebox-test-text "A2") :width '(80))))
|
||||
(item (ebox-test-flex-item
|
||||
source :flex-grow 1 :flex-basis '(80)))
|
||||
(layout
|
||||
(ebox-test-flex :width '(220)
|
||||
(ebox-test-flex-item source :flex-grow 1 :flex-basis '(80))
|
||||
(ebox-test-box :content "B" :width '(80) :height 4)))
|
||||
item
|
||||
(ebox-test-box (ebox-test-text "B") :width '(80) :height 4)))
|
||||
(target-handle
|
||||
(ebox-node-source-handle (ebox-test-root item)))
|
||||
(calls 0)
|
||||
(original (symbol-function 'ebox-render))
|
||||
(original (symbol-function 'ebox--render-node))
|
||||
rendered)
|
||||
(cl-letf (((symbol-function 'ebox-render)
|
||||
(lambda (node)
|
||||
(when (eq (plist-get node :key) 'natural-child)
|
||||
(cl-letf (((symbol-function 'ebox--render-node)
|
||||
(lambda (node &optional source-index)
|
||||
(when (eq (ebox-node-source-handle node) target-handle)
|
||||
(cl-incf calls))
|
||||
(funcall original node))))
|
||||
(funcall original node source-index))))
|
||||
(setq rendered (ebox-render layout)))
|
||||
(should (= (ebox-string-height rendered) 4))
|
||||
(should (equal (mapcar #'ebox--string-pixel-width
|
||||
@ -666,9 +672,9 @@
|
||||
|
||||
(ert-deftest ebox-flex-natural-reuse-preserves-stretched-visible-overflow-ownership ()
|
||||
"Stretch sizing should absorb visible overflow into the final box region."
|
||||
(let* ((box (ebox-test-box :content "A\nB" :width '(80) :height 1
|
||||
(let* ((box (ebox-test-box (ebox-test-text "A\nB") :width '(80) :height 1
|
||||
:overflow 'visible :flex-grow 1))
|
||||
(region-id (car (ebox-region-ids box)))
|
||||
(region-id (car (ebox-region-ids (ebox-test-root box))))
|
||||
(rendered (ebox-render (ebox-test-flex :width '(120) box)))
|
||||
(lines (ebox-string-lines rendered)))
|
||||
(should (= (length lines) 2))
|
||||
@ -705,16 +711,16 @@
|
||||
(ert-deftest ebox-flex-natural-row-padding-keeps-overflow-line-local ()
|
||||
"Visible overflow should widen only the line that actually overflows."
|
||||
(let* ((viewport-child
|
||||
(ebox-test-box :content "A" :width '(viewport)
|
||||
(ebox-test-box (ebox-test-text "A") :width '(viewport)
|
||||
:box-sizing 'content-box
|
||||
:padding-left '(10) :padding-right '(10)))
|
||||
(source
|
||||
(ebox-test-column viewport-child
|
||||
(ebox-test-box :content "A2" :width '(20))))
|
||||
(ebox-test-box (ebox-test-text "A2") :width '(20))))
|
||||
(layout
|
||||
(ebox-test-flex :width '(240)
|
||||
(ebox-test-flex-item source :flex-basis '(20))
|
||||
(ebox-test-box :content "B" :width '(20) :height 4)))
|
||||
(ebox-test-box (ebox-test-text "B") :width '(20) :height 4)))
|
||||
(widths
|
||||
(mapcar #'ebox--string-pixel-width
|
||||
(ebox-string-lines (ebox-render layout)))))
|
||||
@ -727,17 +733,19 @@
|
||||
(let* ((layout
|
||||
(ebox-test-flex :width '(120)
|
||||
(ebox-test-row
|
||||
(ebox-test-box :content "Left" :border t)
|
||||
(ebox-test-box :content "Right" :border t))))
|
||||
(ebox-test-box (ebox-test-text "Left") :border t)
|
||||
(ebox-test-box (ebox-test-text "Right") :border t))))
|
||||
(widths (mapcar #'ebox--string-pixel-width
|
||||
(ebox-string-lines (ebox-render layout)))))
|
||||
(should (cl-every (lambda (width) (<= width 120)) widths))))
|
||||
|
||||
(ert-deftest ebox-flex-sized-persistent-hit-populates-pass-local-cache ()
|
||||
"Persistent sized renders should become reusable in the current pass."
|
||||
(let* ((source (ebox-test-box :content "A" :width '(80)))
|
||||
(let* ((source-input (ebox-test-box (ebox-test-text "A") :width '(80)))
|
||||
(source (ebox-test-root source-input))
|
||||
(ebox--render-cache-table (make-hash-table :test 'equal))
|
||||
(ebox--render-cache-signature-cache (make-hash-table :test 'eq))
|
||||
(ebox--surface-materialization-active t)
|
||||
(first (car (ebox--flex-collect-items (list source) 'row 220)))
|
||||
(main (plist-get first :target))
|
||||
(cache-key (list 'row main nil 'flex-start)))
|
||||
@ -760,11 +768,14 @@
|
||||
|
||||
(ert-deftest ebox-flex-measurement-miss-probes-cache-once ()
|
||||
"A flex measurement miss should compute one exact cache probe."
|
||||
(let* ((source (ebox-test-box :content "Measured once" :width '(80)))
|
||||
(let* ((source-input
|
||||
(ebox-test-box (ebox-test-text "Measured once") :width '(80)))
|
||||
(source (ebox-test-root source-input))
|
||||
(props '(:flex-basis auto))
|
||||
(ebox--render-cache-table (make-hash-table :test 'equal))
|
||||
(ebox--render-cache-signature-cache (make-hash-table :test 'eq))
|
||||
(ebox--viewport-dependent-subtree-cache (make-hash-table :test 'eq))
|
||||
(ebox--surface-materialization-active t)
|
||||
(signature-calls 0)
|
||||
(lookup-calls 0)
|
||||
(original-signature
|
||||
@ -786,14 +797,16 @@
|
||||
|
||||
(ert-deftest ebox-flex-reuses-direct-column-natural-measurement ()
|
||||
"A direct Column's nil-viewport natural size should survive parent resize."
|
||||
(let* ((source
|
||||
(let* ((source-input
|
||||
(ebox-test-column
|
||||
(ebox-test-box :content "A" :width '(80))
|
||||
(ebox-test-box :content "B" :width '(80))))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(80))
|
||||
(ebox-test-box (ebox-test-text "B") :width '(80))))
|
||||
(source (ebox-test-root source-input))
|
||||
(props '(:flex-basis auto))
|
||||
(ebox--render-cache-table (make-hash-table :test 'equal))
|
||||
(ebox--render-cache-signature-cache (make-hash-table :test 'eq))
|
||||
(ebox--viewport-dependent-subtree-cache (make-hash-table :test 'eq))
|
||||
(ebox--surface-materialization-active t)
|
||||
(renders 0)
|
||||
(original
|
||||
(symbol-function 'ebox--flex-render-source-for-measurement)))
|
||||
@ -809,7 +822,7 @@
|
||||
"Known flex metrics should not render a wrapped fixed-basis box twice."
|
||||
(let* ((source
|
||||
(ebox-test-box
|
||||
:content "A responsive wrapped child"
|
||||
(ebox-test-text "A responsive wrapped child")
|
||||
:width '(viewport) :min-width 0 :height 4 :wrap-mode 'word))
|
||||
(item
|
||||
(ebox-test-flex-item
|
||||
@ -833,27 +846,30 @@
|
||||
|
||||
(ert-deftest ebox-flex-fixed-basis-nowrap-box-keeps-intrinsic-measurement ()
|
||||
"No-wrap auto minimums should retain their intrinsic render measurement."
|
||||
(let* ((source
|
||||
(ebox-test-box :content "UNBREAKABLE" :min-width 0 :height 1
|
||||
:wrap-mode nil))
|
||||
(let* ((source-input
|
||||
(ebox-test-box (ebox-test-text "UNBREAKABLE") :min-width 0 :height 1
|
||||
:wrap-mode 'none))
|
||||
(source (ebox-test-root source-input))
|
||||
(item-input (ebox-test-flex-item source-input :flex-basis '(40)))
|
||||
(props
|
||||
(ebox--flex-item-props
|
||||
(ebox-test-flex-item source :flex-basis '(40)))))
|
||||
(ebox--flex-item-props (ebox-test-root item-input))))
|
||||
(should-not (ebox--flex-unrendered-measurement source props 'row))))
|
||||
|
||||
(ert-deftest ebox-flex-explicit-zero-min-width-allows-no-wrap-shrink ()
|
||||
"An explicit zero minimum lets a no-wrap card shrink in its Flex line."
|
||||
(let* ((source
|
||||
(ebox-test-box :content "A long no-wrap card title"
|
||||
:min-width 0 :wrap-mode nil))
|
||||
(rendered (ebox-render source)))
|
||||
(let* ((source-input
|
||||
(ebox-test-box (ebox-test-text "A long no-wrap card title")
|
||||
:min-width 0 :wrap-mode 'none))
|
||||
(source (ebox-test-root source-input))
|
||||
(rendered (ebox-render source-input)))
|
||||
(should (zerop (ebox--flex-box-min-main source rendered 'row)))))
|
||||
|
||||
(ert-deftest ebox-flex-bounded-auto-basis-keeps-intrinsic-measurement ()
|
||||
"Clipping must not replace an auto-basis item's intrinsic contribution."
|
||||
(let* ((source
|
||||
(ebox-test-box :content "UNBREAKABLE" :width '(120) :height 2
|
||||
:min-width 0 :wrap-mode nil :overflow 'hidden))
|
||||
(let* ((source-input
|
||||
(ebox-test-box (ebox-test-text "UNBREAKABLE") :width '(120) :height 2
|
||||
:min-width 0 :wrap-mode 'none :overflow 'hidden))
|
||||
(source (ebox-test-root source-input))
|
||||
(props (ebox--flex-item-props source)))
|
||||
(should (eq (plist-get props :flex-basis) 'auto))
|
||||
(should-not (ebox--flex-unrendered-measurement source props 'row))))
|
||||
@ -861,17 +877,19 @@
|
||||
(ert-deftest ebox-flex-column-natural-cache-respects-final-cross-viewport ()
|
||||
"Column flex must render composite items in the final cross viewport."
|
||||
(let* ((viewport-child
|
||||
(ebox-test-box :content "VP" :width '(viewport)))
|
||||
(source
|
||||
(ebox-test-box (ebox-test-text "VP") :width '(viewport)))
|
||||
(source-input
|
||||
(ebox-test-column
|
||||
viewport-child
|
||||
(ebox-test-box :content "F" :width '(100))))
|
||||
(region-id (car (ebox-region-ids viewport-child)))
|
||||
(ebox-test-box (ebox-test-text "F") :width '(100))))
|
||||
(source (ebox-test-root source-input))
|
||||
(region-id
|
||||
(car (ebox-region-ids (ebox-test-root viewport-child))))
|
||||
(layout
|
||||
(ebox-test-flex :height 4
|
||||
:flex-direction 'column
|
||||
:align-items 'stretch
|
||||
(ebox-test-flex-item source :flex-grow 1 :flex-basis 2)))
|
||||
(ebox-test-flex-item source-input :flex-grow 1 :flex-basis 2)))
|
||||
(rendered (ebox-render layout)))
|
||||
(should (ebox-box-node-p source))
|
||||
(should (eq (ebox-layout-config-kind (ebox-box-node-layout source))
|
||||
@ -883,8 +901,8 @@
|
||||
"Nowrap containers with fixed cross size should not measure natural cross."
|
||||
(let* ((layout
|
||||
(ebox-test-flex :width '(220) :height 4 :flex-wrap 'nowrap
|
||||
(ebox-test-box :content "A" :width '(80))
|
||||
(ebox-test-box :content "B" :width '(80))))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(80))
|
||||
(ebox-test-box (ebox-test-text "B") :width '(80))))
|
||||
(calls 0)
|
||||
(original (symbol-function 'ebox--flex-line-cross)))
|
||||
(cl-letf (((symbol-function 'ebox--flex-line-cross)
|
||||
@ -898,8 +916,8 @@
|
||||
"Align-items center should move short row items within the line height."
|
||||
(let* ((layout
|
||||
(ebox-test-flex :width '(180) :height 3 :align-items 'center
|
||||
(ebox-test-box :content "A" :width '(80) :height 1)
|
||||
(ebox-test-box :content "B\nB\nB" :width '(80) :height 3)))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(80) :height 1)
|
||||
(ebox-test-box (ebox-test-text "B\nB\nB") :width '(80) :height 3)))
|
||||
(lines (ebox-flex-test--plain-lines layout)))
|
||||
(should (= (length lines) 3))
|
||||
(should (= (cl-position-if (lambda (line)
|
||||
@ -911,12 +929,12 @@
|
||||
"Order and reverse directions should change visual item order."
|
||||
(let* ((ordered
|
||||
(ebox-test-flex
|
||||
(ebox-test-flex-item (ebox-test-box :content "A" :width '(40)) :order 2)
|
||||
(ebox-test-flex-item (ebox-test-box :content "B" :width '(40)) :order 1)))
|
||||
(ebox-test-flex-item (ebox-test-box (ebox-test-text "A") :width '(40)) :order 2)
|
||||
(ebox-test-flex-item (ebox-test-box (ebox-test-text "B") :width '(40)) :order 1)))
|
||||
(column-reverse
|
||||
(ebox-test-flex :flex-direction 'column-reverse
|
||||
(ebox-test-box :content "A" :width '(40))
|
||||
(ebox-test-box :content "B" :width '(40)))))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(40))
|
||||
(ebox-test-box (ebox-test-text "B") :width '(40)))))
|
||||
(should (string-match-p "B.*A"
|
||||
(car (ebox-flex-test--plain-lines ordered))))
|
||||
(should (string-match-p "B"
|
||||
@ -927,9 +945,9 @@
|
||||
(let* ((layout
|
||||
(ebox-test-flex :width '(180) :height 3 :align-items 'flex-start
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "A" :width '(80) :height 1)
|
||||
(ebox-test-box (ebox-test-text "A") :width '(80) :height 1)
|
||||
:align-self 'flex-end)
|
||||
(ebox-test-box :content "B\nB\nB" :width '(80) :height 3)))
|
||||
(ebox-test-box (ebox-test-text "B\nB\nB") :width '(80) :height 3)))
|
||||
(lines (ebox-flex-test--plain-lines layout)))
|
||||
(should (= (length lines) 3))
|
||||
(should (= (cl-position-if (lambda (line)
|
||||
@ -944,7 +962,7 @@
|
||||
:flex-wrap 'wrap
|
||||
:align-content 'flex-start
|
||||
:align-items 'center
|
||||
(ebox-test-box :content "A" :width '(60) :height 1)))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(60) :height 1)))
|
||||
(lines (ebox-flex-test--plain-lines layout)))
|
||||
(should (= (length lines) 4))
|
||||
(should (= (cl-position-if (lambda (line)
|
||||
@ -959,7 +977,7 @@
|
||||
:padding '(1 (8))
|
||||
:border '(1 "#ff0000")
|
||||
:bgcolor "#101010"
|
||||
(ebox-test-box :content "A" :width '(60))))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(60))))
|
||||
(rendered (ebox-flex-test--render layout))
|
||||
(left-face (ebox-flex-test--face-at-property rendered 'ebox-bl))
|
||||
(right-face (ebox-flex-test--face-at-property rendered 'ebox-br))
|
||||
@ -981,13 +999,13 @@
|
||||
|
||||
(ert-deftest ebox-flex-container-box-preserves-blank-child-bottom-border ()
|
||||
"A visual flex container should preserve child bottom borders on blank lines."
|
||||
(let* ((child (ebox-test-box :content "A"
|
||||
(let* ((child (ebox-test-box (ebox-test-text "A")
|
||||
:width '(80)
|
||||
:height 2
|
||||
:padding '(0 (6))
|
||||
:border '(1 "#00aa00")
|
||||
:flex-basis '(80)))
|
||||
(child-id (car (ebox-region-ids child)))
|
||||
(child-id (car (ebox-region-ids (ebox-test-root child))))
|
||||
(layout (ebox-test-flex :width '(140)
|
||||
:padding '(1 (4))
|
||||
:border '(1 "#444444")
|
||||
@ -1010,18 +1028,18 @@
|
||||
(ebox-test-flex :width '(260) :height 3 :border "gray"
|
||||
:flex-flow '(row wrap)
|
||||
:justify-content 'space-between
|
||||
(ebox-test-box :content "sample box two"
|
||||
(ebox-test-box (ebox-test-text "sample box two")
|
||||
:height 3
|
||||
:box-sizing 'border-box
|
||||
:padding '(0 (8))
|
||||
:border t
|
||||
:wrap-mode nil)
|
||||
(ebox-test-box :content "sample box three"
|
||||
:wrap-mode 'none)
|
||||
(ebox-test-box (ebox-test-text "sample box three")
|
||||
:height 3
|
||||
:box-sizing 'border-box
|
||||
:padding '(0 (8))
|
||||
:border t
|
||||
:wrap-mode nil)))
|
||||
:wrap-mode 'none)))
|
||||
(plain (substring-no-properties (ebox-flex-test--render layout))))
|
||||
(should-not (string-match-p "REWRAPPED" plain))
|
||||
(should (string-match-p "sample box two" plain))
|
||||
@ -1032,7 +1050,7 @@
|
||||
(let* ((layout
|
||||
(ebox-test-flex :width '(180)
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "A" :width '(60))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(60))
|
||||
:flex 1
|
||||
:box-sizing 'border-box
|
||||
:padding '(0 (6))
|
||||
@ -1068,8 +1086,8 @@
|
||||
(ebox-test-flex :width '(220)
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-column
|
||||
(ebox-test-box :content "Top" :width '(80))
|
||||
(ebox-test-box :content "Bottom" :width '(80)))
|
||||
(ebox-test-box (ebox-test-text "Top") :width '(80))
|
||||
(ebox-test-box (ebox-test-text "Bottom") :width '(80)))
|
||||
:width '(180)
|
||||
:box-sizing 'border-box
|
||||
:border t)))
|
||||
@ -1083,40 +1101,43 @@
|
||||
(dolist (props '((:width (120))
|
||||
(:height 2)
|
||||
(:box-sizing border-box)))
|
||||
(let* ((left (ebox-test-box :content "A" :width '(40)))
|
||||
(right (ebox-test-box :content "B" :width '(40)))
|
||||
(let* ((left (ebox-test-box (ebox-test-text "A") :width '(40)))
|
||||
(right (ebox-test-box (ebox-test-text "B") :width '(40)))
|
||||
(layout
|
||||
(apply #'ebox-test-flex
|
||||
(append props (list left right)))))
|
||||
(should (equal (ebox-box-node-children layout) (list left right)))
|
||||
(should (equal (ebox-box-node-children (ebox-test-root layout))
|
||||
(mapcar #'ebox-test-root (list left right))))
|
||||
(should (string-match-p "A.*B"
|
||||
(substring-no-properties
|
||||
(ebox-flex-test--render layout)))))))
|
||||
|
||||
(ert-deftest ebox-flex-box-sizing-alone-keeps-child-region-ids ()
|
||||
"Box-sizing by itself should not wrap a flex container."
|
||||
(let* ((left (ebox-test-box :content "A" :width '(40)))
|
||||
(right (ebox-test-box :content "B" :width '(40)))
|
||||
(let* ((left (ebox-test-box (ebox-test-text "A") :width '(40)))
|
||||
(right (ebox-test-box (ebox-test-text "B") :width '(40)))
|
||||
(layout
|
||||
(ebox-test-flex :width '(120) :box-sizing 'border-box left right)))
|
||||
(should (equal (ebox-box-node-children layout) (list left right)))
|
||||
(should (equal (ebox-box-node-children (ebox-test-root layout))
|
||||
(mapcar #'ebox-test-root (list left right))))
|
||||
(should (equal (ebox-flex-test--line-widths layout) '(120)))))
|
||||
|
||||
(ert-deftest ebox-flex-direct-child-combines-frame-and-participation ()
|
||||
"One child Box should own both frame and Flex participation facts."
|
||||
(let* ((left (ebox-test-box :content "A" :width '(60)
|
||||
(let* ((left (ebox-test-box (ebox-test-text "A") :width '(60)
|
||||
:flex-grow 1 :flex-basis '(60)
|
||||
:box-sizing 'border-box :border t))
|
||||
(right (ebox-test-box :content "B" :width '(40)))
|
||||
(right (ebox-test-box (ebox-test-text "B") :width '(40)))
|
||||
(layout (ebox-test-flex :width '(160) left right))
|
||||
(left-id (car (ebox-region-ids left)))
|
||||
(right-id (car (ebox-region-ids right)))
|
||||
(left-id (car (ebox-region-ids (ebox-test-root left))))
|
||||
(right-id (car (ebox-region-ids (ebox-test-root right))))
|
||||
(rendered (ebox-flex-test--render layout))
|
||||
(left-width
|
||||
(ebox-flex-test--region-owner-width rendered left-id))
|
||||
(right-width
|
||||
(ebox-flex-test--region-owner-width rendered right-id)))
|
||||
(should (equal (ebox-box-node-children layout) (list left right)))
|
||||
(should (equal (ebox-box-node-children (ebox-test-root layout))
|
||||
(mapcar #'ebox-test-root (list left right))))
|
||||
(should (> left-width 60))
|
||||
(should (> left-width right-width))))
|
||||
|
||||
@ -1127,12 +1148,12 @@
|
||||
:flex-flow '(row wrap)
|
||||
:column-gap '(10)
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :content "wide no-wrap label"
|
||||
(ebox-test-box (ebox-test-text "wide no-wrap label")
|
||||
:width '(150)
|
||||
:box-sizing 'border-box
|
||||
:wrap-mode nil)
|
||||
:wrap-mode 'none)
|
||||
:flex 1)
|
||||
(ebox-test-box :content "B" :width '(120))))
|
||||
(ebox-test-box (ebox-test-text "B") :width '(120))))
|
||||
(lines (ebox-flex-test--plain-lines layout)))
|
||||
(should (= (length lines) 2))
|
||||
(should (cl-every (lambda (width) (<= width 160))
|
||||
@ -1146,36 +1167,36 @@
|
||||
(should (stringp
|
||||
(ebox-render
|
||||
(ebox-test-flex :width '(220) :height 4 :flex-direction direction
|
||||
(ebox-test-box :content "A" :width '(60) :height 1)
|
||||
(ebox-test-box :content "B" :width '(60) :height 1))))))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(60) :height 1)
|
||||
(ebox-test-box (ebox-test-text "B") :width '(60) :height 1))))))
|
||||
(dolist (wrap '(nowrap wrap wrap-reverse))
|
||||
(should (stringp
|
||||
(ebox-render
|
||||
(ebox-test-flex :width '(130) :height 4 :flex-wrap wrap
|
||||
(ebox-test-box :content "A" :width '(60) :height 1)
|
||||
(ebox-test-box :content "B" :width '(60) :height 1)
|
||||
(ebox-test-box :content "C" :width '(60) :height 1))))))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(60) :height 1)
|
||||
(ebox-test-box (ebox-test-text "B") :width '(60) :height 1)
|
||||
(ebox-test-box (ebox-test-text "C") :width '(60) :height 1))))))
|
||||
(dolist (justify '(flex-start flex-end center space-between space-around space-evenly start end left right normal))
|
||||
(should (stringp
|
||||
(ebox-render
|
||||
(ebox-test-flex :width '(220) :justify-content justify
|
||||
(ebox-test-box :content "A" :width '(60))
|
||||
(ebox-test-box :content "B" :width '(60)))))))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(60))
|
||||
(ebox-test-box (ebox-test-text "B") :width '(60)))))))
|
||||
(dolist (align '(stretch flex-start flex-end center baseline normal start end self-start self-end))
|
||||
(should (stringp
|
||||
(ebox-render
|
||||
(ebox-test-flex :width '(220) :height 4
|
||||
:align-items align
|
||||
(ebox-test-box :content "A" :width '(60) :height 1)
|
||||
(ebox-test-box :content "B" :width '(60) :height 1))))))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(60) :height 1)
|
||||
(ebox-test-box (ebox-test-text "B") :width '(60) :height 1))))))
|
||||
(dolist (align '(stretch flex-start flex-end center space-between space-around space-evenly baseline normal start end))
|
||||
(should (stringp
|
||||
(ebox-render
|
||||
(ebox-test-flex :width '(130) :height 5 :flex-wrap 'wrap
|
||||
:align-content align
|
||||
(ebox-test-box :content "A" :width '(60) :height 1)
|
||||
(ebox-test-box :content "B" :width '(60) :height 1)
|
||||
(ebox-test-box :content "C" :width '(60) :height 1)))))))
|
||||
(ebox-test-box (ebox-test-text "A") :width '(60) :height 1)
|
||||
(ebox-test-box (ebox-test-text "B") :width '(60) :height 1)
|
||||
(ebox-test-box (ebox-test-text "C") :width '(60) :height 1)))))))
|
||||
|
||||
(provide 'ebox-flex-tests)
|
||||
;;; ebox-flex-tests.el ends here
|
||||
|
||||
@ -19,10 +19,10 @@
|
||||
:grid-template-columns '((20) (20))
|
||||
:grid-template-rows '(1 1)
|
||||
:gap '(0 (1))
|
||||
(ebox-test-box :content "A")
|
||||
(ebox-test-box :content "B")
|
||||
(ebox-test-box :content "C")
|
||||
(ebox-test-box :content "D")))))
|
||||
(ebox-test-box (ebox-test-text "A"))
|
||||
(ebox-test-box (ebox-test-text "B"))
|
||||
(ebox-test-box (ebox-test-text "C"))
|
||||
(ebox-test-box (ebox-test-text "D"))))))
|
||||
(should (string-match-p "A.*B" plain))
|
||||
(should (string-match-p "C.*D" plain))))
|
||||
|
||||
@ -31,8 +31,8 @@
|
||||
(let* ((node (ebox-test-grid
|
||||
:width '(120)
|
||||
:grid-template-columns '(1fr 2fr)
|
||||
(ebox-test-box :content "A")
|
||||
(ebox-test-box :content "B")))
|
||||
(ebox-test-box (ebox-test-text "A"))
|
||||
(ebox-test-box (ebox-test-text "B"))))
|
||||
(lines (ebox-string-lines (ebox-render node))))
|
||||
(should (= (length lines) 1))
|
||||
(should (= (ebox--string-pixel-width (car lines)) 120))))
|
||||
@ -41,8 +41,8 @@
|
||||
"Grid render and resize paths should not reparse canonical track config."
|
||||
(let* ((node (ebox-test-grid :width '(120)
|
||||
:grid-template-columns '(1fr 2fr)
|
||||
(ebox-test-box :content "A")
|
||||
(ebox-test-box :content "B")))
|
||||
(ebox-test-box (ebox-test-text "A"))
|
||||
(ebox-test-box (ebox-test-text "B"))))
|
||||
(calls 0)
|
||||
(original (symbol-function 'ebox-grid--normalize-config-props)))
|
||||
(cl-letf (((symbol-function 'ebox-grid--normalize-config-props)
|
||||
@ -60,15 +60,15 @@
|
||||
:width (list width)
|
||||
:grid-template-columns
|
||||
'((minmax (0) (fr 1)) (minmax (0) (fr 2)))
|
||||
(ebox-test-box :content "A" :width 'stretch)
|
||||
(ebox-test-box :content "B" :width 'stretch)))
|
||||
(ebox-test-box (ebox-test-text "A") :width 'stretch)
|
||||
(ebox-test-box (ebox-test-text "B") :width 'stretch)))
|
||||
(lines (ebox-string-lines (ebox-render node))))
|
||||
(should (= (ebox--string-max-pixel-width
|
||||
(car lines)) width)))))
|
||||
|
||||
(ert-deftest ebox-grid-stretch-children-fill-fractional-tracks ()
|
||||
"Stretch-width children must reflow to their assigned fractional tracks."
|
||||
(let* ((left (ebox-test-box :content "left" :width 'stretch))
|
||||
(let* ((left (ebox-test-box (ebox-test-text "left") :width 'stretch))
|
||||
(calls 0)
|
||||
(entry-left (list :node left :column 1 :column-span 1))
|
||||
(rendered (make-hash-table :test #'eq)))
|
||||
@ -90,8 +90,8 @@
|
||||
:grid-template-columns '((50) (50))
|
||||
:gap '(0 (10))
|
||||
:border "#334155"
|
||||
(ebox-test-box :content "A\nA-long" :padding '(0 (4)))
|
||||
(ebox-test-box :content "B\nB-long" :padding '(0 (4)))))
|
||||
(ebox-test-box (ebox-test-text "A\nA-long") :padding '(0 (4)))
|
||||
(ebox-test-box (ebox-test-text "B\nB-long") :padding '(0 (4)))))
|
||||
(lines (ebox-string-lines (ebox-render node)))
|
||||
(widths (mapcar #'ebox--string-pixel-width lines)))
|
||||
(should (equal widths (make-list (length widths) 120)))))
|
||||
@ -102,8 +102,8 @@
|
||||
:width '(120)
|
||||
:grid-template-columns '((120))
|
||||
(ebox-test-row
|
||||
(ebox-test-box :content "Left" :border t)
|
||||
(ebox-test-box :content "Right" :border t))))
|
||||
(ebox-test-box (ebox-test-text "Left") :border t)
|
||||
(ebox-test-box (ebox-test-text "Right") :border t))))
|
||||
(widths (mapcar #'ebox--string-pixel-width
|
||||
(ebox-string-lines (ebox-render node)))))
|
||||
(should (equal widths '(120)))))
|
||||
@ -115,7 +115,7 @@
|
||||
:grid-template-columns '((38))
|
||||
:border "#334155"
|
||||
(ebox-test-box
|
||||
:content "WIDE CONTENT WIDE CONTENT WIDE CONTENT WIDE CONTENT"
|
||||
(ebox-test-text "WIDE CONTENT WIDE CONTENT WIDE CONTENT WIDE CONTENT")
|
||||
:wrap-mode 'word)))
|
||||
(lines (ebox-string-lines (ebox-render node)))
|
||||
(widths (mapcar #'ebox--string-pixel-width lines)))
|
||||
@ -132,10 +132,10 @@
|
||||
(ebox-test-column
|
||||
:width '(20)
|
||||
(ebox-test-box
|
||||
:content "fractional symbols\n1fr distributes by weight"
|
||||
(ebox-test-text "fractional symbols\n1fr distributes by weight")
|
||||
:padding '(0 (2)) :border "#B85C3C"
|
||||
:wrap-mode 'word)
|
||||
(ebox-test-box :content "body" :height 1))))
|
||||
(ebox-test-box (ebox-test-text "body") :height 1))))
|
||||
(lines (ebox-string-lines (ebox-render node)))
|
||||
(plain (mapcar #'substring-no-properties lines)))
|
||||
(should (> (length lines) 3))
|
||||
@ -153,24 +153,24 @@
|
||||
:gap '(1 (4))
|
||||
:justify-content 'space-between
|
||||
:border "#334155"
|
||||
(ebox-test-box :content "FIXED\n30 PX")
|
||||
(ebox-test-box :content "AUTO\nINTRINSIC")
|
||||
(ebox-test-box :content "1FR\nREMAINDER")))
|
||||
(ebox-test-box (ebox-test-text "FIXED\n30 PX"))
|
||||
(ebox-test-box (ebox-test-text "AUTO\nINTRINSIC"))
|
||||
(ebox-test-box (ebox-test-text "1FR\nREMAINDER"))))
|
||||
(lines (ebox-string-lines (ebox-render node)))
|
||||
(widths (mapcar #'ebox--string-pixel-width lines)))
|
||||
(should (equal widths (make-list (length widths) 120)))))
|
||||
|
||||
(ert-deftest ebox-grid-keeps-padded-items-inside-column-width ()
|
||||
"Stack padding should use the widest Grid line instead of its first line."
|
||||
(let* ((header (ebox-test-box :content "Header" :width '(720)))
|
||||
(let* ((header (ebox-test-box (ebox-test-text "Header") :width '(720)))
|
||||
(grid (ebox-test-grid
|
||||
:width '(718)
|
||||
:grid-template-columns '((220) 1fr)
|
||||
:gap '(1 (12))
|
||||
:border "#C97252"
|
||||
(ebox-test-box :content "Fixed" :width '(196)
|
||||
(ebox-test-box (ebox-test-text "Fixed") :width '(196)
|
||||
:padding '(1 (12)))
|
||||
(ebox-test-box :content "Fractional" :width '(460)
|
||||
(ebox-test-box (ebox-test-text "Fractional") :width '(460)
|
||||
:padding '(1 (12)))))
|
||||
(lines (ebox-string-lines (ebox-render (ebox-test-column header grid))))
|
||||
(widths (mapcar #'ebox--string-pixel-width lines)))
|
||||
@ -183,9 +183,9 @@
|
||||
(ebox-test-grid
|
||||
:grid-template-columns '((20) (20))
|
||||
:grid-template-rows '(1 1)
|
||||
(ebox-test-box :content "Header" :grid-column '(1 :span 2))
|
||||
(ebox-test-box :content "Left" :grid-row 2 :grid-column 1)
|
||||
(ebox-test-box :content "Right" :grid-row 2 :grid-column 2)))))
|
||||
(ebox-test-box (ebox-test-text "Header") :grid-column '(1 :span 2))
|
||||
(ebox-test-box (ebox-test-text "Left") :grid-row 2 :grid-column 1)
|
||||
(ebox-test-box (ebox-test-text "Right") :grid-row 2 :grid-column 2)))))
|
||||
(should (string-match-p "Header" plain))
|
||||
(should (string-match-p "Left.*Right" plain))))
|
||||
|
||||
@ -195,8 +195,8 @@
|
||||
:grid-template-columns '((20))
|
||||
:grid-auto-columns '((30))
|
||||
:grid-auto-flow 'column
|
||||
(ebox-test-box :content "A")
|
||||
(ebox-test-box :content "B")))
|
||||
(ebox-test-box (ebox-test-text "A"))
|
||||
(ebox-test-box (ebox-test-text "B"))))
|
||||
(line (car (ebox-string-lines (ebox-render node)))))
|
||||
(should (= (ebox--string-pixel-width line) 50))))
|
||||
|
||||
@ -206,12 +206,12 @@
|
||||
(ebox-grid-test--plain
|
||||
(ebox-test-grid
|
||||
:grid-template-columns '((20) (20) (20))
|
||||
(ebox-test-box :content "A")
|
||||
(ebox-test-box :content "B")
|
||||
(ebox-test-box :content "C")
|
||||
(ebox-test-box :content "D")
|
||||
(ebox-test-box :content "E")
|
||||
(ebox-test-box :content "F")))))
|
||||
(ebox-test-box (ebox-test-text "A"))
|
||||
(ebox-test-box (ebox-test-text "B"))
|
||||
(ebox-test-box (ebox-test-text "C"))
|
||||
(ebox-test-box (ebox-test-text "D"))
|
||||
(ebox-test-box (ebox-test-text "E"))
|
||||
(ebox-test-box (ebox-test-text "F"))))))
|
||||
(dolist (letter '("A" "B" "C" "D" "E" "F"))
|
||||
(should (string-match-p letter plain)))))
|
||||
|
||||
@ -221,8 +221,8 @@
|
||||
:width '(100)
|
||||
:justify-content 'center
|
||||
:grid-template-columns '((minmax (20) (30)) (repeat 1 (20)))
|
||||
(ebox-test-box :content "A")
|
||||
(ebox-test-box :content "B")))
|
||||
(ebox-test-box (ebox-test-text "A"))
|
||||
(ebox-test-box (ebox-test-text "B"))))
|
||||
(line (car (ebox-string-lines (ebox-render node)))))
|
||||
(should (= (ebox--string-pixel-width line) 100))
|
||||
(should (= (length (seq-filter (lambda (char) (= char ?\s)) line))
|
||||
@ -232,15 +232,15 @@
|
||||
"Invalid flow and non-positive spans should be rejected."
|
||||
(should-error
|
||||
(ebox-render
|
||||
(ebox-test-grid :grid-auto-flow 'diagonal (ebox-test-box :content "A"))))
|
||||
(ebox-test-grid :grid-auto-flow 'diagonal (ebox-test-box (ebox-test-text "A")))))
|
||||
(should-error
|
||||
(ebox-render
|
||||
(ebox-test-grid
|
||||
(ebox-test-box :content "A" :grid-column-span 0))))
|
||||
(ebox-test-box (ebox-test-text "A") :grid-column-span 0))))
|
||||
(should-error
|
||||
(ebox-render
|
||||
(ebox-test-grid
|
||||
(ebox-test-box :content "A" :grid-column 0)))))
|
||||
(ebox-test-box (ebox-test-text "A") :grid-column 0)))))
|
||||
|
||||
(ert-deftest ebox-grid-rejects-invalid-ecss-placement-before-layout ()
|
||||
"ECSS rules should reject invalid Grid placement before layout runs."
|
||||
@ -256,8 +256,8 @@
|
||||
|
||||
(ert-deftest ebox-grid-region-update-reflows-grid-child ()
|
||||
"Replacing one typed Grid child should reflow and preserve its sibling."
|
||||
(let* ((left (ebox-test-box :content "A" :host-ref 'left :width 20))
|
||||
(right (ebox-test-box :content "B" :id "right" :width 20))
|
||||
(let* ((left (ebox-test-box (ebox-test-text "A") :source-identity 'left :width 20))
|
||||
(right (ebox-test-box (ebox-test-text "B") :id "right" :width 20))
|
||||
(node (ebox-test-grid :grid-template-columns '((20) (20)) left right))
|
||||
(buffer (ebox-render-to-buffer
|
||||
(generate-new-buffer-name " *ebox-grid-update*") node)))
|
||||
@ -269,7 +269,7 @@
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'left
|
||||
(ebox-test-box :content "Updated" :host-ref 'left :width 20))
|
||||
(ebox-test-box (ebox-test-text "Updated") :source-identity 'left :width 20))
|
||||
(ebox-commit buffer candidate))
|
||||
(let ((updated (with-current-buffer buffer (buffer-string))))
|
||||
(should (string-match-p "Updated" updated))
|
||||
@ -279,25 +279,28 @@
|
||||
|
||||
(ert-deftest ebox-build-compiles-grid-layout ()
|
||||
"The public Ebox DSL should compile a grid node."
|
||||
(let ((node
|
||||
(ebox-build
|
||||
'(grid :grid-template-columns ((20) (20))
|
||||
(box "A")
|
||||
(box "B")))))
|
||||
(let* ((input
|
||||
(ebox-build
|
||||
'(grid :grid-template-columns ((20) (20))
|
||||
(box "A")
|
||||
(box "B"))))
|
||||
(node (ebox-test-root input)))
|
||||
(should (ebox-box-node-p node))
|
||||
(should (eq (ebox-layout-config-kind (ebox-box-node-layout node)) 'grid))
|
||||
(should (string-match-p "A" (ebox-grid-test--plain node)))
|
||||
(should (string-match-p "B" (ebox-grid-test--plain node)))))
|
||||
(should (string-match-p "A" (ebox-grid-test--plain input)))
|
||||
(should (string-match-p "B" (ebox-grid-test--plain input)))))
|
||||
|
||||
(ert-deftest ebox-build-compiles-direct-grid-placement ()
|
||||
"The Ebox DSL should preserve direct child Box placement metadata."
|
||||
(let ((node
|
||||
(ebox-build
|
||||
'(grid :grid-template-columns ((20) (20))
|
||||
(box :grid-column (1 :span 2) "Header")))))
|
||||
(should (string-match-p "Header" (ebox-grid-test--plain node)))
|
||||
(let* ((input
|
||||
(ebox-build
|
||||
'(grid :grid-template-columns ((20) (20))
|
||||
(box :grid-column (1 :span 2) "Header"))))
|
||||
(node (ebox-test-root input)))
|
||||
(should (string-match-p "Header" (ebox-grid-test--plain input)))
|
||||
(should (equal (ebox-style-node-specified-value
|
||||
(car (ebox-box-node-children node)) :grid-column)
|
||||
(car (ebox-box-node-children node)) :grid-column nil
|
||||
(ebox-test-source-index input))
|
||||
'(1 :span 2)))))
|
||||
|
||||
(provide 'ebox-grid-tests)
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
(ert-deftest ebox-facade-loads-internal-model-modules ()
|
||||
"The public ebox facade should load all internal foundation modules."
|
||||
(require 'ebox)
|
||||
(dolist (feature '(ebox-cache ebox-style ebox-tree ebox-measure
|
||||
(dolist (feature '(ebox-cache ebox-source ebox-style ebox-tree ebox-measure
|
||||
ebox-layout-config
|
||||
ebox-node-factory
|
||||
ebox-fragment ebox-layout ebox-flex ebox-grid
|
||||
@ -77,8 +77,16 @@
|
||||
"--eval"
|
||||
(concat
|
||||
"(progn (require 'ebox-canonical) "
|
||||
"(unless (ebox-text-node-p (ebox-text-create :value \"A\")) "
|
||||
" (error \"Typed Text autoload failed\")))")))))))
|
||||
"(let* ((builder (ebox-source-builder-create)) "
|
||||
" (handle (ebox-source-builder-bind "
|
||||
" builder :declarations nil)) "
|
||||
" (node (ebox-text-create "
|
||||
" :value \"A\" :source-handle handle "
|
||||
" :owned-facts "
|
||||
" (ebox-canonical-facts-from-declarations "
|
||||
" 'text nil)))) "
|
||||
" (unless (ebox-text-node-p node) "
|
||||
" (error \"Typed Text autoload failed\"))))")))))))
|
||||
|
||||
(ert-deftest ebox-facade-defines-native-workflow-without-loading-it ()
|
||||
"The facade should define native commands without loading the subsystem."
|
||||
@ -121,7 +129,8 @@
|
||||
(should (commandp 'ebox-byte-compile))
|
||||
(should
|
||||
(equal ebox--compile-sources
|
||||
'("ebox-cache.el" "ebox-style.el" "ebox-layout-config.el"
|
||||
'("ebox-cache.el" "ebox-source.el" "ebox-style.el"
|
||||
"ebox-layout-config.el"
|
||||
"ebox-node-factory.el"
|
||||
"ebox-child-range.el"
|
||||
"ebox-tree.el" "ebox-measure.el"
|
||||
|
||||
@ -91,19 +91,25 @@
|
||||
(ert-deftest ebox-selector-queries-adjacent-and-general-siblings ()
|
||||
"Selector query should use TP sibling semantics on logical children."
|
||||
(ebox-selector-test--reset-runtime-state)
|
||||
(let ((node (ebox-build
|
||||
'(column
|
||||
(box :id first "A")
|
||||
(box :id second :class later "B")
|
||||
(box :id third :class later "C")))))
|
||||
(let* ((node (ebox-build
|
||||
'(column
|
||||
(box :id first "A")
|
||||
(box :id second :class later "B")
|
||||
(box :id third :class later "C"))))
|
||||
(root (ebox-canonical-input--single-root node "selector test"))
|
||||
(source-index
|
||||
(ebox-tree-source-index
|
||||
root nil nil (ebox-canonical-input--source-index node))))
|
||||
(should (equal
|
||||
(mapcar (lambda (match)
|
||||
(ebox-tree-node-id (plist-get match :node)))
|
||||
(ebox-tree-node-id
|
||||
source-index (plist-get match :node)))
|
||||
(ebox-selector-query-all node "#first + #second"))
|
||||
'("second")))
|
||||
(should (equal
|
||||
(mapcar (lambda (match)
|
||||
(ebox-tree-node-id (plist-get match :node)))
|
||||
(ebox-tree-node-id
|
||||
source-index (plist-get match :node)))
|
||||
(ebox-selector-query-all node "#first ~ .later"))
|
||||
'("second" "third")))))
|
||||
|
||||
@ -126,11 +132,18 @@
|
||||
(ert-deftest ebox-selector-subject-exposes-only-explicit-metadata ()
|
||||
"Selector adaptation should not serialize Ebox runtime containers."
|
||||
(let* ((opaque (vector (make-hash-table :test #'eq)))
|
||||
(node (list :ebox-type 'box :id 'card :class '(panel active)
|
||||
:selector-state '(focused) :state opaque
|
||||
:content "visible" :width 12 :render-cache opaque
|
||||
:selector-attributes '((role . button) (rank . 3))))
|
||||
(subject (ebox-tree-node-subject node))
|
||||
(input (ebox-test-box
|
||||
:id 'card :class '(panel active)
|
||||
:selector-state '(focused)
|
||||
(ebox-test-text "visible") :width 12
|
||||
:selector-attributes '((role . button) (rank . 3))))
|
||||
(node (ebox-test-root input))
|
||||
(_state (plist-put node :state opaque))
|
||||
(_cache (plist-put node :render-cache opaque))
|
||||
(source-index
|
||||
(ebox-tree-source-index
|
||||
node nil nil (ebox-test-source-index input)))
|
||||
(subject (ebox-tree-node-subject source-index node))
|
||||
(attributes (ecss-subject-attributes subject)))
|
||||
(should (equal (ecss-subject-classes subject) '("panel" "active")))
|
||||
(should (equal (ecss-subject-states subject) '("focused")))
|
||||
@ -145,7 +158,7 @@
|
||||
(ert-deftest ebox-selector-queries-explicit-attributes ()
|
||||
"Attribute queries should read only explicit selector metadata."
|
||||
(let ((node (ebox-test-box
|
||||
:content "Run"
|
||||
(ebox-test-text "Run")
|
||||
:selector-attributes '((role . button)))))
|
||||
(should (= (length (ebox-selector-query-all node "[role=button]")) 1))
|
||||
(should-not (ebox-selector-query-all node "[content=Run]"))))
|
||||
|
||||
403
tests/ebox-source-tests.el
Normal file
403
tests/ebox-source-tests.el
Normal file
@ -0,0 +1,403 @@
|
||||
;;; ebox-source-tests.el --- Source ownership contracts -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
(require 'ebox)
|
||||
|
||||
(ert-deftest ebox-source-index-is-the-sole-source-fact-owner ()
|
||||
"Source construction and public reads cannot mutate an immutable index."
|
||||
(let* ((class (list (copy-sequence "row")))
|
||||
(declaration-value (vector (copy-sequence "red")))
|
||||
(declarations (list 'ebox/color declaration-value))
|
||||
(provenance-table (make-hash-table :test #'eq))
|
||||
(_ (puthash 'origin (copy-sequence "fixture") provenance-table))
|
||||
(builder (ebox-source-builder-create))
|
||||
(first
|
||||
(ebox-source-builder-bind
|
||||
builder
|
||||
:key 'row-1 :id "first" :class class
|
||||
:selector-attributes '((role . button))
|
||||
:selector-state '(focused)
|
||||
:declarations declarations
|
||||
:provenance (list :table provenance-table)))
|
||||
(second
|
||||
(ebox-source-builder-bind
|
||||
builder
|
||||
:key 'row-1 :id "first" :class class
|
||||
:declarations declarations))
|
||||
(builder-records (ebox-source--builder-records builder))
|
||||
(builder-handles (ebox-source--builder-handle-records builder))
|
||||
(index (ebox-source-builder-finish builder)))
|
||||
(should-not (eq first second))
|
||||
(should-not (eq (ebox-source-handle-id first)
|
||||
(ebox-source-handle-id second)))
|
||||
(setcar class "damaged")
|
||||
(aset (aref declaration-value 0) 0 ?X)
|
||||
(puthash 'origin "damaged" provenance-table)
|
||||
(clrhash builder-records)
|
||||
(clrhash builder-handles)
|
||||
(should (ebox-source-index-handle-member-p index first))
|
||||
(should (ebox-source-index-record index first))
|
||||
(should-not (ebox-source--builder-records builder))
|
||||
(should-not (ebox-source--builder-handle-records builder))
|
||||
(let ((record (ebox-source-index-record index first)))
|
||||
(should (equal '("row") (ebox-source-record-classes record)))
|
||||
(should (equal ["red"]
|
||||
(plist-get (ebox-source-record-declarations record)
|
||||
'ebox/color)))
|
||||
(should (equal "fixture"
|
||||
(gethash
|
||||
'origin
|
||||
(plist-get (ebox-source-record-provenance record)
|
||||
:table))))
|
||||
(setf (ebox-source-record-id record) "damaged")
|
||||
(setcar (ebox-source-record-classes record) "damaged")
|
||||
(aset (plist-get (ebox-source-record-declarations record) 'ebox/color)
|
||||
0 "damaged"))
|
||||
(let ((fresh (ebox-source-index-record index first)))
|
||||
(should (equal "first" (ebox-source-record-id fresh)))
|
||||
(should (equal '("row") (ebox-source-record-classes fresh)))
|
||||
(should (equal ["red"]
|
||||
(plist-get (ebox-source-record-declarations fresh)
|
||||
'ebox/color))))
|
||||
(let ((handles (ebox-source-index-handles index)))
|
||||
(setcar handles second)
|
||||
(should (equal (list first second)
|
||||
(ebox-source-index-handles index))))
|
||||
(let ((duplicate-builder (ebox-source-builder-create)))
|
||||
(ebox-source-builder-bind duplicate-builder :identity 'same)
|
||||
(should-error
|
||||
(ebox-source-builder-bind duplicate-builder :identity 'same)
|
||||
:type 'error))
|
||||
(should-error (ebox-source-builder-bind builder :identity 'sealed)
|
||||
:type 'error)
|
||||
(let* ((private (ebox-node-factory--create-from-properties
|
||||
:bgcolor "red"))
|
||||
(empty-builder (ebox-source-builder-create))
|
||||
(empty-index (ebox-source-builder-finish empty-builder)))
|
||||
(should-not (plist-member private :ebox-style-declarations))
|
||||
(should-not (ebox-style-node-declarations empty-index private)))))
|
||||
|
||||
(ert-deftest ebox-source-rebind-keeps-order-and-bounded-persistence ()
|
||||
"Long-lived local updates must preserve order without growing recursion."
|
||||
(let* ((builder (ebox-source-builder-create))
|
||||
(first (ebox-source-builder-bind builder :identity 'a))
|
||||
(middle (ebox-source-builder-bind builder :identity 'b))
|
||||
(last (ebox-source-builder-bind builder :identity 'c))
|
||||
(index (ebox-source-builder-finish builder))
|
||||
durations)
|
||||
(let ((gc-cons-threshold most-positive-fixnum)
|
||||
(gc-cons-percentage 1.0))
|
||||
(dotimes (revision 2000)
|
||||
(let ((started (float-time)))
|
||||
(pcase-let ((`(,next-index . ,next-handle)
|
||||
(ebox-source-index-rebind
|
||||
index middle
|
||||
:declarations
|
||||
(list 'ebox/color (number-to-string revision)))))
|
||||
(setq index next-index
|
||||
middle next-handle))
|
||||
(push (* 1000.0 (- (float-time) started)) durations))))
|
||||
(let ((handles (ebox-source-index-handles index)))
|
||||
(should (= (length handles) 3))
|
||||
(should (eq first (car handles)))
|
||||
(should (eq middle (cadr handles)))
|
||||
(should (eq last (caddr handles)))
|
||||
(should (equal '(a b c) (mapcar #'ebox-source-handle-id handles))))
|
||||
(should (< (ebox-source-table-depth
|
||||
(ebox-source--index-records index))
|
||||
ebox-source--persistent-max-depth))
|
||||
(should (< (ebox-source-table-depth
|
||||
(ebox-source--index-handle-records index))
|
||||
ebox-source--persistent-max-depth))
|
||||
(should (< (ebox-source-order-depth (ebox-source--index-order index))
|
||||
ebox-source--persistent-max-depth))
|
||||
(let* ((ordered (sort durations #'<))
|
||||
(p95 (nth (1- (ceiling (* 0.95 (length ordered)))) ordered))
|
||||
(maximum (car (last ordered))))
|
||||
(should (< p95 50.0))
|
||||
(should (< maximum 50.0)))))
|
||||
|
||||
(ert-deftest ebox-source-persistent-lookups-compress-inherited-paths ()
|
||||
"An immutable overlay should not repeatedly traverse its retained history."
|
||||
(let* ((base-values (make-hash-table :test #'equal))
|
||||
(_ (puthash 'retained 'value base-values))
|
||||
(table (ebox-source--flat-table base-values #'equal)))
|
||||
(dotimes (revision 12)
|
||||
(let ((added (make-hash-table :test #'equal))
|
||||
(removed (make-hash-table :test #'equal)))
|
||||
(puthash (list 'revision revision) revision added)
|
||||
(setq table
|
||||
(ebox-source--table-overlay
|
||||
table added removed (1+ (ebox-source-table-count table))))))
|
||||
(should (eq 'value
|
||||
(ebox-source--table-get table 'retained 'missing)))
|
||||
(should (eq 'value
|
||||
(gethash 'retained (ebox-source-table-memo table))))
|
||||
(should (eq 'missing
|
||||
(ebox-source--table-get table 'absent 'missing)))
|
||||
(should (eq ebox-source--memo-missing
|
||||
(gethash 'absent (ebox-source-table-memo table))))))
|
||||
|
||||
(ert-deftest ebox-source-delta-resolves-each-node-record-once ()
|
||||
"Node-local source diff should materialize one record view per generation."
|
||||
(let* ((old-input (ebox-build '(box :id "before" "Same")))
|
||||
(new-input (ebox-build '(box :id "after" "Same")))
|
||||
(old-node
|
||||
(ebox-canonical-input--single-root old-input "source delta test"))
|
||||
(new-node
|
||||
(ebox-canonical-input--single-root new-input "source delta test"))
|
||||
(old-index (ebox-canonical-input--source-index old-input))
|
||||
(new-index (ebox-canonical-input--source-index new-input))
|
||||
(original (symbol-function 'ebox-source--index-record))
|
||||
(lookups 0)
|
||||
changed)
|
||||
(cl-letf (((symbol-function 'ebox-source--index-record)
|
||||
(lambda (index handle)
|
||||
(cl-incf lookups)
|
||||
(funcall original index handle))))
|
||||
(setq changed
|
||||
(ebox-incremental--candidate-local-changed-keys
|
||||
old-node old-index new-node new-index)))
|
||||
(should (= lookups 2))
|
||||
(should (equal changed '(:id)))))
|
||||
|
||||
(ert-deftest ebox-build-produces-handle-only-canonical-nodes ()
|
||||
"The DSL should place author source facts behind opaque handles only."
|
||||
(let* ((input
|
||||
(ebox-build
|
||||
'(column :id "root" :class "shelf"
|
||||
(box :key row-1 :class "row" "One")
|
||||
(box :key row-2 :class "row" "Two"))))
|
||||
(root (ebox-canonical-input--single-root input "source test"))
|
||||
(base-index (ebox-canonical-input--source-index input))
|
||||
nodes handles)
|
||||
(cl-labels
|
||||
((visit (node)
|
||||
(push node nodes)
|
||||
(push (ebox-node-source-handle node) handles)
|
||||
(when (ebox-box-node-p node)
|
||||
(mapc #'visit (ebox-box-node-children node)))))
|
||||
(visit root))
|
||||
(setq nodes (nreverse nodes)
|
||||
handles (nreverse handles))
|
||||
(dolist (node nodes)
|
||||
(should (ebox-source-handle-p (ebox-node-source-handle node)))
|
||||
(should-not (fboundp 'ebox-source--handle-record))
|
||||
(dolist (field '(:key :id :class :ebox-style-declarations))
|
||||
(should-not (plist-member node field))))
|
||||
(let* ((index (ebox-tree-source-index root nil nil base-index))
|
||||
(root-record (ebox-source-index-record index (car handles)))
|
||||
(first-box (nth 1 nodes))
|
||||
(first-record
|
||||
(ebox-source-index-record
|
||||
index (ebox-node-source-handle first-box))))
|
||||
(should (equal "root" (ebox-source-record-id root-record)))
|
||||
(should (equal '("shelf")
|
||||
(ebox-source-record-classes root-record)))
|
||||
(should (eq 'row-1 (ebox-source-record-key first-record)))
|
||||
(should (equal '("row")
|
||||
(ebox-source-record-classes first-record))))
|
||||
(let* ((source-index (ebox-tree-source-index root nil nil base-index))
|
||||
(copy (copy-sequence root)))
|
||||
(should (eq (ebox-tree-node-subject source-index root)
|
||||
(ebox-tree-node-subject source-index copy))))
|
||||
(should-error (ebox-render root) :type 'wrong-type-argument)
|
||||
(should (string-match-p "One" (ebox-render input)))
|
||||
(should (string-match-p "Two" (ebox-render input)))))
|
||||
|
||||
(ert-deftest ebox-canonical-input-owns-root-ref-and-full-equivalence ()
|
||||
"Root addressing and no-op comparison must include immutable source facts."
|
||||
(cl-labels
|
||||
((input
|
||||
(color &optional identity)
|
||||
(let* ((builder (ebox-source-builder-create))
|
||||
(declarations
|
||||
(ebox-style-compile-form 'text (list :color color)))
|
||||
(handle
|
||||
(ebox-source-builder-bind
|
||||
builder :identity (or identity '(host root))
|
||||
:declarations declarations
|
||||
:provenance '(:adapter source-test)))
|
||||
(node
|
||||
(ebox-text-create
|
||||
:value "Same"
|
||||
:owned-facts
|
||||
(ebox-canonical-facts-from-declarations 'text declarations)
|
||||
:source-handle handle)))
|
||||
(ebox-canonical-input-create
|
||||
(list node) (ebox-source-builder-finish builder)))))
|
||||
(let* ((first (input "red"))
|
||||
(same (input "red"))
|
||||
(changed (input "blue"))
|
||||
(root-ref (ebox-canonical-input-root-host-ref first)))
|
||||
(should (equal '(host root) root-ref))
|
||||
(setcar root-ref 'damaged)
|
||||
(should (equal '(host root)
|
||||
(ebox-canonical-input-root-host-ref first)))
|
||||
(should (ebox-canonical-input-equal-p first same))
|
||||
(should-not (ebox-canonical-input-equal-p first changed))
|
||||
(let ((foreign-builder (ebox-source-builder-create)))
|
||||
(should-error
|
||||
(ebox-canonical-input-create
|
||||
(copy-sequence (ebox-canonical-input--nodes first))
|
||||
(ebox-source-builder-finish foreign-builder))
|
||||
:type 'error))
|
||||
(let* ((left (input "red" '(host left)))
|
||||
(right (input "red" '(host right)))
|
||||
(reverse-builder (ebox-source-builder-create)))
|
||||
(ebox-source-builder-import
|
||||
reverse-builder (ebox-canonical-input--source-index right))
|
||||
(ebox-source-builder-import
|
||||
reverse-builder (ebox-canonical-input--source-index left))
|
||||
(let* ((combined
|
||||
(ebox-canonical-input-create
|
||||
(append (copy-sequence (ebox-canonical-input--nodes left))
|
||||
(copy-sequence (ebox-canonical-input--nodes right)))
|
||||
(ebox-source-builder-finish reverse-builder)))
|
||||
(ordered
|
||||
(ebox-source-index-handles
|
||||
(ebox-canonical-input--source-index combined))))
|
||||
(should
|
||||
(equal '((host left) (host right))
|
||||
(mapcar #'ebox-source-handle-id ordered))))))))
|
||||
|
||||
(ert-deftest ebox-canonical-input-selectively-imports-retained-roots ()
|
||||
"Retained roots should adopt only their exact immutable source subtrees."
|
||||
(let* ((builder (ebox-source-builder-create))
|
||||
(left-box-handle
|
||||
(ebox-source-builder-bind builder :identity '(box left)))
|
||||
(left-text-handle
|
||||
(ebox-source-builder-bind builder :identity '(text left)))
|
||||
(right-box-handle
|
||||
(ebox-source-builder-bind builder :identity '(box right)))
|
||||
(right-text-handle
|
||||
(ebox-source-builder-bind builder :identity '(text right)))
|
||||
(text-facts (ebox-canonical-facts-from-declarations 'text nil))
|
||||
(box-facts (ebox-canonical-facts-from-declarations 'box nil))
|
||||
(left-text
|
||||
(ebox-text-create
|
||||
:value "Left" :source-handle left-text-handle
|
||||
:owned-facts text-facts))
|
||||
(right-text
|
||||
(ebox-text-create
|
||||
:value "Right" :source-handle right-text-handle
|
||||
:owned-facts text-facts))
|
||||
(left
|
||||
(ebox-box-create
|
||||
:layout (ebox-normal-layout-create) :children (list left-text)
|
||||
:source-handle left-box-handle :owned-facts box-facts))
|
||||
(right
|
||||
(ebox-box-create
|
||||
:layout (ebox-normal-layout-create) :children (list right-text)
|
||||
:source-handle right-box-handle :owned-facts box-facts))
|
||||
(input
|
||||
(ebox-canonical-input-create
|
||||
(list left right) (ebox-source-builder-finish builder)))
|
||||
(selected-builder (ebox-source-builder-create)))
|
||||
(should (equal (list left right) (ebox-canonical-input-roots input)))
|
||||
(should
|
||||
(equal (list right)
|
||||
(ebox-canonical-input-import-roots
|
||||
input (list right) selected-builder)))
|
||||
(let* ((selected
|
||||
(ebox-canonical-input-create
|
||||
(list right)
|
||||
(ebox-source-builder-finish selected-builder)))
|
||||
(index (ebox-canonical-input--source-index selected)))
|
||||
(should-not
|
||||
(ebox-source-index-handle-member-p index left-box-handle))
|
||||
(should-not
|
||||
(ebox-source-index-handle-member-p index left-text-handle))
|
||||
(should (ebox-source-index-handle-member-p index right-box-handle))
|
||||
(should (ebox-source-index-handle-member-p index right-text-handle))
|
||||
(should
|
||||
(equal '((box right) (text right))
|
||||
(mapcar
|
||||
#'ebox-source-handle-id
|
||||
(ebox-source-index-handles index)))))
|
||||
(should-error
|
||||
(ebox-canonical-input-import-roots
|
||||
input (list right right) (ebox-source-builder-create))
|
||||
:type 'error)))
|
||||
|
||||
(ert-deftest ebox-selector-and-reconcile-read-only-source-index ()
|
||||
"Selectors and keyed reconciliation should consume generation source indexes."
|
||||
(let ((buffer (generate-new-buffer " *ebox-source-index-cutover*")))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-build
|
||||
'(column :id root :class list
|
||||
(box :key a :id first :class row "A")
|
||||
(box :key b :id second :class row "B"))))
|
||||
(let* ((old-state (ebox--buffer-render-state buffer))
|
||||
(old-index (plist-get old-state :source-index))
|
||||
(old-first (car (ebox-selector-query-buffer buffer "#first")))
|
||||
(old-id (plist-get old-first :node-id))
|
||||
(old-handle
|
||||
(ebox-node-source-handle (plist-get old-first :node))))
|
||||
(ebox-commit
|
||||
buffer
|
||||
(ebox-build
|
||||
'(column :id root :class list
|
||||
(box :key b :id second :class row "B")
|
||||
(box :key a :id first :class (row selected) "A2"))))
|
||||
(let* ((state (ebox--buffer-render-state buffer))
|
||||
(index (plist-get state :source-index))
|
||||
(first (car (ebox-selector-query-buffer buffer "#first")))
|
||||
(selected
|
||||
(car (ebox-selector-query-buffer buffer ".selected"))))
|
||||
(should-not (eq index old-index))
|
||||
(should (= old-id (plist-get first :node-id)))
|
||||
(should (= old-id (plist-get selected :node-id)))
|
||||
(should
|
||||
(equal
|
||||
'(b a)
|
||||
(delq
|
||||
nil
|
||||
(mapcar
|
||||
(lambda (handle)
|
||||
(ebox-source-record-key
|
||||
(ebox-source-index-record index handle)))
|
||||
(ebox-source-index-handles index)))))
|
||||
(should (equal '("row")
|
||||
(ebox-source-record-classes
|
||||
(ebox-source-index-record
|
||||
old-index old-handle))))
|
||||
(maphash
|
||||
(lambda (_node-id node)
|
||||
(when (ebox-node-kind node)
|
||||
(should (ebox-source-index-record
|
||||
index (ebox-node-source-handle node)))
|
||||
(dolist (field
|
||||
'(:key :id :class :host-ref
|
||||
:ebox-style-declarations))
|
||||
(should-not (plist-member node field)))))
|
||||
(plist-get state :node-table))
|
||||
(let ((records (ebox-source--index-records index))
|
||||
(subjects (ebox-source--index-subjects index))
|
||||
(node-subjects (ebox-source--index-node-subjects index)))
|
||||
(ebox-selector-update-buffer
|
||||
buffer "#first" :color "#123456")
|
||||
(let* ((next-index
|
||||
(plist-get (ebox--buffer-render-state buffer)
|
||||
:source-index))
|
||||
(next-records
|
||||
(ebox-source--index-records next-index))
|
||||
(next-subjects
|
||||
(ebox-source--index-subjects next-index))
|
||||
(next-node-subjects
|
||||
(ebox-source--index-node-subjects next-index)))
|
||||
(should (eq records (ebox-source-table-base next-records)))
|
||||
(should (eq subjects next-subjects))
|
||||
(should
|
||||
(eq node-subjects
|
||||
(ebox-source-table-base next-node-subjects))))))))
|
||||
(when (buffer-live-p buffer)
|
||||
(kill-buffer buffer)))))
|
||||
|
||||
(provide 'ebox-source-tests)
|
||||
;;; ebox-source-tests.el ends here
|
||||
@ -24,6 +24,17 @@
|
||||
ebox--scroll-global-state))
|
||||
(clrhash table)))
|
||||
|
||||
(defun ebox-surface-test--producer (input &optional previous-state)
|
||||
"Return an internal surface producer for canonical INPUT."
|
||||
(ebox-surface-producer
|
||||
(ebox-test-root input) previous-state nil
|
||||
(list :source-base-index (ebox-test-source-index input))))
|
||||
|
||||
(defun ebox-surface-test--render-runtime (state)
|
||||
"Render retained runtime STATE through Ebox's private node boundary."
|
||||
(ebox--render-node
|
||||
(plist-get state :root-node) (plist-get state :source-index)))
|
||||
|
||||
(defun ebox-surface-test--interactive-content ()
|
||||
"Return fresh interactive propertized content for projection tests."
|
||||
(let ((map (make-sparse-keymap)))
|
||||
@ -327,7 +338,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(list
|
||||
(cons 'box
|
||||
(lambda ()
|
||||
(ebox-test-box :key 'box :content (ebox-surface-test--interactive-content)
|
||||
(ebox-test-box :key 'box (ebox-test-text (ebox-surface-test--interactive-content))
|
||||
:width '(120) :padding '(1 (4))
|
||||
:border "#334155" :bgcolor "#E2E8F0"
|
||||
:color "#0F172A")))
|
||||
@ -335,22 +346,22 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(lambda ()
|
||||
(ebox-test-column
|
||||
(ebox-test-row
|
||||
(ebox-test-box :key 'left :content "Left" :width '(70)
|
||||
(ebox-test-box :key 'left (ebox-test-text "Left") :width '(70)
|
||||
:bgcolor "#DBEAFE" :color "#172554")
|
||||
(ebox-test-box :key 'right :content "Right\nDetail" :width '(90)
|
||||
(ebox-test-box :key 'right (ebox-test-text "Right\nDetail") :width '(90)
|
||||
:bgcolor "#DCFCE7" :color "#14532D"))
|
||||
(ebox-test-box :key 'footer :content "Footer" :width '(160)
|
||||
(ebox-test-box :key 'footer (ebox-test-text "Footer") :width '(160)
|
||||
:bgcolor "#F1F5F9" :color "#0F172A"))))
|
||||
(cons 'flex
|
||||
(lambda ()
|
||||
(ebox-test-flex
|
||||
:width '(210) :flex-wrap 'wrap :column-gap '(10)
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :key 'grow :content "Grow" :width '(80)
|
||||
(ebox-test-box :key 'grow (ebox-test-text "Grow") :width '(80)
|
||||
:bgcolor "#EDE9FE" :color "#2E1065")
|
||||
:flex-grow 1 :flex-basis '(80))
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box :key 'fixed :content "Fixed" :width '(120)
|
||||
(ebox-test-box :key 'fixed (ebox-test-text "Fixed") :width '(120)
|
||||
:bgcolor "#FFEDD5" :color "#7C2D12")))))
|
||||
(cons 'grid
|
||||
(lambda ()
|
||||
@ -358,13 +369,13 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
:width '(220) :grid-template-columns '((70) 1fr)
|
||||
:grid-template-rows '(2) :gap '(1 (8))
|
||||
:border "#475569"
|
||||
(ebox-test-box :key 'grid-left :content "A\nAA"
|
||||
(ebox-test-box :key 'grid-left (ebox-test-text "A\nAA")
|
||||
:bgcolor "#E0F2FE" :color "#0C4A6E")
|
||||
(ebox-test-box :key 'grid-right :content "B\nBB"
|
||||
(ebox-test-box :key 'grid-right (ebox-test-text "B\nBB")
|
||||
:bgcolor "#FCE7F3" :color "#831843"))))
|
||||
(cons 'overflow-scroll
|
||||
(lambda ()
|
||||
(ebox-test-box :key 'scroll :content "zero\none\ntwo\nthree"
|
||||
(ebox-test-box :key 'scroll (ebox-test-text "zero\none\ntwo\nthree")
|
||||
:width '(100) :height 2 :overflow 'scroll
|
||||
:bgcolor "#1E293B" :color "#F8FAFC")))))
|
||||
|
||||
@ -373,11 +384,11 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(ebox-surface-test--reset-render-state)
|
||||
(let ((ebox-viewport-width 240)
|
||||
(ebox-viewport-height 12)
|
||||
(node (funcall builder)))
|
||||
(input (funcall builder)))
|
||||
(if projector-p
|
||||
(tp-surface-materialize-string
|
||||
(ebox-surface-producer node))
|
||||
(ebox-render node))))
|
||||
(ebox-surface-test--producer input))
|
||||
(ebox-render input))))
|
||||
|
||||
(defun ebox-surface-test--project-row-column ()
|
||||
"Project the row/column fixture through the pure TP materializer."
|
||||
@ -588,12 +599,12 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
"Serialize and optionally render the Ebox default foreground reset."
|
||||
(let* ((ebox-viewport-width 80)
|
||||
(ebox-viewport-height 20)
|
||||
(node
|
||||
(input
|
||||
(ebox-test-box
|
||||
:width 20
|
||||
:ebox-content-node
|
||||
(ebox-test--text
|
||||
"native" '(:color ebox/default-foreground))))
|
||||
(ebox-test-text
|
||||
"native" :color 'ebox/default-foreground)))
|
||||
(node (ebox-test-root input))
|
||||
(package (ebox-native-reflow--compile-layout-package node))
|
||||
(document (plist-get package :document))
|
||||
(styles (plist-get document :styles))
|
||||
@ -623,15 +634,16 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
:fragment-span-template)))
|
||||
(should (equal-including-properties
|
||||
(plist-get frame :rendered)
|
||||
(ebox-render node)))))))
|
||||
(ebox-render input)))))))
|
||||
|
||||
(ert-deftest ebox-native-canonical-text-matches-elisp-render ()
|
||||
"Canonical Text IR must preserve Text output and paint properties exactly."
|
||||
(let* ((ebox-viewport-width 80)
|
||||
(ebox-viewport-height 20)
|
||||
(text (ebox-text-create :value "native" :font-weight 'bold))
|
||||
(node (ebox-box-create :layout (ebox-normal-layout-create)
|
||||
:children (list text)))
|
||||
(text-input (ebox-test-text "native" :font-weight 'bold))
|
||||
(text (ebox-test-root text-input))
|
||||
(input (ebox-test-box text-input))
|
||||
(node (ebox-test-root input))
|
||||
(package (ebox-native-reflow--compile-layout-package node))
|
||||
(root (plist-get (plist-get package :document) :root)))
|
||||
(should (eq (plist-get (plist-get package :document) :version) 2))
|
||||
@ -665,7 +677,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(let* ((ebox-viewport-width 80)
|
||||
(ebox-viewport-height 20)
|
||||
(content (propertize "native" 'face 'italic 'help-echo "source"))
|
||||
(node (ebox-test-box :content content :width 20
|
||||
(node (ebox-test-box (ebox-test-text content) :width 20
|
||||
:color "#f0f0f0" :bgcolor "#101010"))
|
||||
(package (ebox-native-reflow--compile-layout-package node))
|
||||
(normal
|
||||
@ -809,28 +821,34 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(lambda ()
|
||||
(ebox-test-box
|
||||
:font-height 1.25
|
||||
:ebox-content-node
|
||||
(ebox-test-box :content "Inherited")))
|
||||
(ebox-test-box (ebox-test-text "Inherited"))))
|
||||
t))
|
||||
(should (> calls 0))))
|
||||
|
||||
(ert-deftest ebox-surface-inline-inheritance-crosses-transparent-layout ()
|
||||
"Transparent layout nodes do not consume inherited paint themselves."
|
||||
(let ((closed
|
||||
(let* ((closed-input
|
||||
(ebox-test-box
|
||||
:color "red"
|
||||
:ebox-content-node
|
||||
(ebox-test-column
|
||||
(ebox-test-box
|
||||
:ebox-content-node
|
||||
(ebox-test--text "child" '(:color "blue"))))))
|
||||
(open
|
||||
(ebox-test-text "child" :color "blue")))))
|
||||
(closed (ebox-test-root closed-input))
|
||||
(open-input
|
||||
(ebox-test-box
|
||||
:color "red"
|
||||
:ebox-content-node
|
||||
(ebox-test-column (ebox-test-box :content "child")))))
|
||||
(should-not (ebox-surface--inline-inheritance-required-p closed))
|
||||
(should (ebox-surface--inline-inheritance-required-p open))))
|
||||
(ebox-test-column (ebox-test-box (ebox-test-text "child")))))
|
||||
(open (ebox-test-root open-input)))
|
||||
(should-not
|
||||
(ebox-surface--inline-inheritance-required-p
|
||||
closed
|
||||
(ebox-tree-source-index
|
||||
closed nil nil (ebox-test-source-index closed-input))))
|
||||
(should
|
||||
(ebox-surface--inline-inheritance-required-p
|
||||
open
|
||||
(ebox-tree-source-index
|
||||
open nil nil (ebox-test-source-index open-input))))))
|
||||
|
||||
(ert-deftest ebox-box-content-cache-reuses-fixed-viewport-subtree ()
|
||||
"A fixed box content viewport should reuse its exact composite layout."
|
||||
@ -845,13 +863,16 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(ebox--scroll-global-state (make-hash-table :test 'equal))
|
||||
(ebox-viewport-width 240)
|
||||
(ebox-viewport-height 8)
|
||||
(child
|
||||
(child-input
|
||||
(ebox-test-column
|
||||
(ebox-test-box :key 'responsive-child
|
||||
:content "fixed local viewport"
|
||||
(ebox-test-text "fixed local viewport")
|
||||
:width '(viewport)
|
||||
:height 1)))
|
||||
(wrapper (ebox-test-box :width '(160)))
|
||||
(child (ebox-test-root child-input))
|
||||
(wrapper-input (ebox-test-box :width '(160)))
|
||||
(wrapper (ebox-test-root wrapper-input))
|
||||
(ebox--surface-materialization-active t)
|
||||
(renders 0)
|
||||
(original (symbol-function
|
||||
'ebox--render-cache-render-with-scroll-actions)))
|
||||
@ -986,7 +1007,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(setq surface
|
||||
(tp-surface-mount
|
||||
buffer
|
||||
(ebox-surface-producer
|
||||
(ebox-surface-test--producer
|
||||
(funcall (cdr (assq 'box (ebox-surface-test--fixtures)))))
|
||||
'(:capability content)))
|
||||
(let* ((state (tp-surface-client-state surface))
|
||||
@ -1006,8 +1027,9 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
"One source description should mount into two independent TP surfaces."
|
||||
(ebox-surface-test--reset-render-state)
|
||||
(let* ((source
|
||||
(ebox-test-box :key 'shared :content "Shared" :width '(100)
|
||||
(ebox-test-box :key 'shared (ebox-test-text "Shared") :width '(100)
|
||||
:bgcolor "#E2E8F0" :color "#0F172A"))
|
||||
(source-node (ebox-test-root source))
|
||||
(first-buffer (generate-new-buffer " *ebox-surface-first*"))
|
||||
(second-buffer (generate-new-buffer " *ebox-surface-second*"))
|
||||
first second)
|
||||
@ -1015,11 +1037,11 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(progn
|
||||
(setq first
|
||||
(tp-surface-mount
|
||||
first-buffer (ebox-surface-producer source)
|
||||
first-buffer (ebox-surface-test--producer source)
|
||||
'(:capability content)))
|
||||
(setq second
|
||||
(tp-surface-mount
|
||||
second-buffer (ebox-surface-producer source)
|
||||
second-buffer (ebox-surface-test--producer source)
|
||||
'(:capability content)))
|
||||
(let* ((first-state (tp-surface-client-state first))
|
||||
(second-state (tp-surface-client-state second))
|
||||
@ -1028,9 +1050,9 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(should-not (eq first-root second-root))
|
||||
(should-not (eq (plist-get first-root :surface-object)
|
||||
(plist-get second-root :surface-object)))
|
||||
(should-not (plist-member source :node-id))
|
||||
(should-not (plist-member source :region-id))
|
||||
(should-not (plist-member source :surface-object))))
|
||||
(should-not (plist-member source-node :node-id))
|
||||
(should-not (plist-member source-node :region-id))
|
||||
(should-not (plist-member source-node :surface-object))))
|
||||
(dolist (surface (list first second))
|
||||
(when (and surface (tp-surface-live-p surface))
|
||||
(tp-surface-unmount surface)))
|
||||
@ -1046,22 +1068,22 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(unwind-protect
|
||||
(let* ((first-source
|
||||
(ebox-test-row
|
||||
(ebox-test-box :key 'left :content "Left" :width '(60))
|
||||
(ebox-test-box :key 'right :content "Right" :width '(60))))
|
||||
(ebox-test-box :key 'left (ebox-test-text "Left") :width '(60))
|
||||
(ebox-test-box :key 'right (ebox-test-text "Right") :width '(60))))
|
||||
(_mount
|
||||
(setq surface
|
||||
(tp-surface-mount
|
||||
buffer (ebox-surface-producer first-source)
|
||||
buffer (ebox-surface-test--producer first-source)
|
||||
'(:capability content))))
|
||||
(first-state (tp-surface-client-state surface))
|
||||
(left (ebox-surface-test--object-by-key first-state 'left))
|
||||
(right (ebox-surface-test--object-by-key first-state 'right))
|
||||
(next-source
|
||||
(ebox-test-row
|
||||
(ebox-test-box :key 'right :content "Right!" :width '(60))
|
||||
(ebox-test-box :key 'left :content "Left!" :width '(60)))))
|
||||
(ebox-test-box :key 'right (ebox-test-text "Right!") :width '(60))
|
||||
(ebox-test-box :key 'left (ebox-test-text "Left!") :width '(60)))))
|
||||
(tp-surface-update
|
||||
surface (ebox-surface-producer next-source first-state))
|
||||
surface (ebox-surface-test--producer next-source first-state))
|
||||
(let ((next-state (tp-surface-client-state surface)))
|
||||
(should (eq left
|
||||
(ebox-surface-test--object-by-key next-state 'left)))
|
||||
@ -1087,9 +1109,9 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-column
|
||||
(ebox-test-box :key 'target :id 'target :content "Before"
|
||||
(ebox-test-box :key 'target :id 'target (ebox-test-text "Before")
|
||||
:width '(100))
|
||||
(ebox-test-box :key 'sibling :content "Sibling" :width '(100))))
|
||||
(ebox-test-box :key 'sibling (ebox-test-text "Sibling") :width '(100))))
|
||||
(let* ((state (ebox--buffer-render-state buffer))
|
||||
(contents
|
||||
(with-current-buffer buffer
|
||||
@ -1131,20 +1153,21 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(funcall original producer))))
|
||||
(should (stringp
|
||||
(ebox-render
|
||||
(ebox-test-box :content "Materialized" :width '(100))))))
|
||||
(ebox-test-box (ebox-test-text "Materialized") :width '(100))))))
|
||||
(should (= calls 0))))
|
||||
|
||||
(ert-deftest ebox-render-is-repeatable-without-consuming-runtime-identities ()
|
||||
"Ephemeral rendering should be exact and leave live identity counters alone."
|
||||
(ebox-surface-test--reset-render-state)
|
||||
(let* ((source (ebox-test-box :content "Repeatable" :width '(100)))
|
||||
(let* ((source (ebox-test-box (ebox-test-text "Repeatable") :width '(100)))
|
||||
(source-node (ebox-test-root source))
|
||||
(first (ebox-render source))
|
||||
(second (ebox-render source)))
|
||||
(should (equal-including-properties first second))
|
||||
(should (= ebox--region-id-counter 0))
|
||||
(should (= ebox--runtime-node-id-counter 0))
|
||||
(should-not (plist-member source :region-id))
|
||||
(should-not (plist-member source :node-id))))
|
||||
(should-not (plist-member source-node :region-id))
|
||||
(should-not (plist-member source-node :node-id))))
|
||||
|
||||
(ert-deftest ebox-render-to-buffer-mounts-one-tp-surface ()
|
||||
"The public buffer renderer should expose TP's committed client state."
|
||||
@ -1155,7 +1178,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :content "Mounted" :width '(100)))
|
||||
(ebox-test-box (ebox-test-text "Mounted") :width '(100)))
|
||||
(setq surface
|
||||
(with-current-buffer buffer ebox-surface--buffer-surface))
|
||||
(should (tp-surface-live-p surface))
|
||||
@ -1176,7 +1199,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(ebox-runtime-idle-reflow-cache-prewarm nil))
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :content "Viewport" :width '(viewport)))
|
||||
(ebox-test-box (ebox-test-text "Viewport") :width '(viewport)))
|
||||
(let* ((surface
|
||||
(with-current-buffer buffer ebox-surface--buffer-surface))
|
||||
(signals
|
||||
@ -1231,7 +1254,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(ebox-test-flex-item
|
||||
(ebox-test-box
|
||||
:key (list 'viewport-reflow-item index)
|
||||
:content (format "item-%02d" index)
|
||||
(ebox-test-text (format "item-%02d" index))
|
||||
:width '(70)
|
||||
:padding '(0 1)
|
||||
:color "#172554"
|
||||
@ -1255,7 +1278,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(expected
|
||||
(let ((ebox-viewport-width 320)
|
||||
(ebox-viewport-height 6))
|
||||
(ebox-render (plist-get state :root-node))))
|
||||
(ebox-surface-test--render-runtime state)))
|
||||
(actual
|
||||
(with-current-buffer buffer
|
||||
(buffer-substring (point-min) (point-max))))
|
||||
@ -1288,7 +1311,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :key 'native-viewport-root
|
||||
:content "Viewport"
|
||||
(ebox-test-text "Viewport")
|
||||
:width '(viewport)))
|
||||
(let* ((state (ebox--buffer-render-state buffer))
|
||||
(session (list 'native-session))
|
||||
@ -1365,7 +1388,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :key 'native-viewport-rollback
|
||||
:content "Viewport"
|
||||
(ebox-test-text "Viewport")
|
||||
:width '(viewport)))
|
||||
(let* ((surface
|
||||
(with-current-buffer buffer ebox-surface--buffer-surface))
|
||||
@ -1441,28 +1464,24 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(ebox-runtime-idle-prewarm nil)
|
||||
(ebox-runtime-idle-reflow-cache-prewarm nil))
|
||||
(setq stable-source
|
||||
(let ((node
|
||||
(ebox-test-column
|
||||
(ebox-test-box :key 'stable-leaf
|
||||
:content "zero\none\ntwo"
|
||||
:width '(100)
|
||||
:height 1
|
||||
:overflow 'hidden
|
||||
:surface-properties
|
||||
'(help-echo "stable")))))
|
||||
(plist-put node :key 'stable-source)
|
||||
node))
|
||||
(ebox-test-column
|
||||
:key 'stable-source
|
||||
(ebox-test-box :key 'stable-leaf
|
||||
(ebox-test-text "zero\none\ntwo")
|
||||
:width '(100)
|
||||
:height 1
|
||||
:overflow 'hidden
|
||||
:surface-properties
|
||||
'(help-echo "stable"))))
|
||||
(setq growing-source
|
||||
(let ((node
|
||||
(ebox-test-column
|
||||
(ebox-test-box :key 'growing-leaf
|
||||
:content "growing"
|
||||
:width '(viewport)
|
||||
:height 1
|
||||
:color "#0F172A"
|
||||
:bgcolor "#DBEAFE"))))
|
||||
(plist-put node :key 'growing-source)
|
||||
node))
|
||||
(ebox-test-column
|
||||
:key 'growing-source
|
||||
(ebox-test-box :key 'growing-leaf
|
||||
(ebox-test-text "growing")
|
||||
:width '(viewport)
|
||||
:height 1
|
||||
:color "#0F172A"
|
||||
:bgcolor "#DBEAFE")))
|
||||
(cl-letf (((symbol-function 'ebox-native-commit-render)
|
||||
(lambda (&rest _) nil)))
|
||||
(let* ((layout
|
||||
@ -1474,12 +1493,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(ebox-test-flex-item stable-source
|
||||
:flex-grow 0 :flex-basis '(120))
|
||||
(ebox-test-flex-item growing-source
|
||||
:flex-grow 1 :flex-basis '(80))))
|
||||
(items (plist-get layout :children)))
|
||||
(setq stable-source
|
||||
(plist-get (car items) :node)
|
||||
growing-source
|
||||
(plist-get (cadr items) :node))
|
||||
:flex-grow 1 :flex-basis '(80)))))
|
||||
(ebox-render-to-buffer buffer layout))
|
||||
(let* ((old-state (ebox--buffer-render-state buffer))
|
||||
(old-root-object
|
||||
@ -1501,7 +1515,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
expected
|
||||
(let ((ebox-viewport-width 360)
|
||||
(ebox-viewport-height 6))
|
||||
(ebox-render (plist-get state :root-node)))
|
||||
(ebox-surface-test--render-runtime state))
|
||||
actual
|
||||
(with-current-buffer buffer
|
||||
(buffer-substring (point-min) (point-max))))
|
||||
@ -1552,8 +1566,9 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
|
||||
(ert-deftest ebox-flex-fragment-key-normalizes-only-height-independent-contexts ()
|
||||
"Retained Flex keys ignore height only after an explicit dependency proof."
|
||||
(let ((ebox--layout-fragments-table (make-hash-table :test 'equal))
|
||||
(node (ebox-test-box :content "stable" :width '(80))))
|
||||
(let* ((ebox--layout-fragments-table (make-hash-table :test 'equal))
|
||||
(input (ebox-test-box (ebox-test-text "stable") :width '(80)))
|
||||
(node (ebox-test-root input)))
|
||||
(cl-labels
|
||||
((key-at (height dependency)
|
||||
(let ((ebox-viewport-height height))
|
||||
@ -1567,7 +1582,8 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(ert-deftest ebox-flex-fragment-key-includes-rendered-region-ownership ()
|
||||
"Retained Flex output must not replay stale region text properties."
|
||||
(let* ((ebox--layout-fragments-table (make-hash-table :test 'equal))
|
||||
(source (ebox-test-box :content "stable" :width '(80)))
|
||||
(source-input (ebox-test-box (ebox-test-text "stable") :width '(80)))
|
||||
(source (ebox-test-root source-input))
|
||||
(first-key
|
||||
(ebox-fragment-flex-allocation-key
|
||||
source 'row 80 1 'stretch 80
|
||||
@ -1613,7 +1629,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :class 'viewport-cascade
|
||||
:content "Cascade"
|
||||
(ebox-test-text "Cascade")
|
||||
:width '(viewport)))
|
||||
(cl-letf (((symbol-function 'ebox-surface--ensure-node-tree)
|
||||
(lambda (&rest args)
|
||||
@ -1657,8 +1673,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(ebox-test-box
|
||||
:font-height 1.25
|
||||
:width '(viewport)
|
||||
:ebox-content-node
|
||||
(ebox-test-box :content "Inherited")))
|
||||
(ebox-test-box (ebox-test-text "Inherited"))))
|
||||
(cl-letf (((symbol-function 'ebox-surface--ensure-node-tree)
|
||||
(lambda (&rest args)
|
||||
(cl-incf ensured-node-count)
|
||||
@ -1669,7 +1684,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(expected
|
||||
(let ((ebox-viewport-width 240)
|
||||
(ebox-viewport-height 6))
|
||||
(ebox-render (plist-get state :root-node))))
|
||||
(ebox-surface-test--render-runtime state)))
|
||||
(actual
|
||||
(with-current-buffer buffer
|
||||
(buffer-substring (point-min) (point-max)))))
|
||||
@ -1688,17 +1703,16 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(list
|
||||
(cons 'nested-scroll
|
||||
(lambda ()
|
||||
(ebox-test-box :content "outer"
|
||||
(ebox-test-box (ebox-test-text "outer")
|
||||
:width '(viewport)
|
||||
:height 2
|
||||
:overflow 'scroll
|
||||
:ebox-content-node
|
||||
(ebox-test-box :content "zero\none\ntwo"
|
||||
(ebox-test-box (ebox-test-text "zero\none\ntwo")
|
||||
:height 2
|
||||
:overflow 'scroll))))
|
||||
(cons 'visible-overflow
|
||||
(lambda ()
|
||||
(ebox-test-box :content "one\ntwo"
|
||||
(ebox-test-box (ebox-test-text "one\ntwo")
|
||||
:width '(viewport)
|
||||
:height 1
|
||||
:overflow 'visible)))))
|
||||
@ -1738,7 +1752,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :key 'root-scroll
|
||||
:content "zero\none\ntwo\nthree"
|
||||
(ebox-test-text "zero\none\ntwo\nthree")
|
||||
:width '(viewport)
|
||||
:height '(viewport-height)
|
||||
:overflow 'scroll))
|
||||
@ -1752,7 +1766,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(expected
|
||||
(let ((ebox-viewport-width 240)
|
||||
(ebox-viewport-height 2))
|
||||
(ebox-render (plist-get state :root-node))))
|
||||
(ebox-surface-test--render-runtime state)))
|
||||
(actual
|
||||
(with-current-buffer buffer
|
||||
(buffer-substring (point-min) (point-max)))))
|
||||
@ -1783,7 +1797,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(ebox-runtime-idle-reflow-cache-prewarm nil))
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :key name :content "One\nTwo"
|
||||
(ebox-test-box :key name (ebox-test-text "One\nTwo")
|
||||
:width (if (eq axes 'height)
|
||||
'(120)
|
||||
'(viewport))
|
||||
@ -1820,7 +1834,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :key 'rollback
|
||||
:content "zero\none\ntwo\nthree"
|
||||
(ebox-test-text "zero\none\ntwo\nthree")
|
||||
:width '(viewport)
|
||||
:height '(viewport-height)
|
||||
:overflow 'scroll))
|
||||
@ -1895,7 +1909,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(ebox-viewport-height 2))
|
||||
(ebox-render-to-buffer
|
||||
responsive
|
||||
(ebox-test-box :content "zero\none\ntwo\nthree"
|
||||
(ebox-test-box (ebox-test-text "zero\none\ntwo\nthree")
|
||||
:width '(viewport)
|
||||
:height '(viewport-height)
|
||||
:overflow 'scroll))
|
||||
@ -1914,7 +1928,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(should (= (tp-signal-subscriber-count signal) 1)))
|
||||
(ebox-render-to-buffer
|
||||
static
|
||||
(ebox-test-box :content "Static" :width '(100)))
|
||||
(ebox-test-box (ebox-test-text "Static") :width '(100)))
|
||||
(setq static-signals
|
||||
(with-current-buffer static ebox-surface--context-signals))
|
||||
(should (= (tp-signal-subscriber-count
|
||||
@ -1952,7 +1966,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(ebox-runtime-idle-reflow-cache-prewarm nil))
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :content "Static" :width '(100)))
|
||||
(ebox-test-box (ebox-test-text "Static") :width '(100)))
|
||||
(let* ((surface
|
||||
(with-current-buffer buffer ebox-surface--buffer-surface))
|
||||
(signals
|
||||
@ -1992,7 +2006,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(ebox-runtime-idle-reflow-cache-prewarm nil))
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :content "Theme" :width '(100)))
|
||||
(ebox-test-box (ebox-test-text "Theme") :width '(100)))
|
||||
(let* ((surface
|
||||
(with-current-buffer buffer ebox-surface--buffer-surface))
|
||||
(signals
|
||||
@ -2018,6 +2032,79 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(when (buffer-live-p buffer)
|
||||
(kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-inline-style-reuse-projects-onto-fresh-text-node ()
|
||||
"A reused computed style must still materialize a fresh Text projection."
|
||||
(let* ((builder (ebox-source-builder-create))
|
||||
(parent-declarations
|
||||
'(ebox/color foreground-slot
|
||||
ebox/background-color background-slot))
|
||||
(parent-handle
|
||||
(ebox-source-builder-bind
|
||||
builder :identity 'parent :declarations parent-declarations))
|
||||
(child-handle
|
||||
(ebox-source-builder-bind builder :identity 'child))
|
||||
(child-facts (ebox-canonical-facts-from-declarations 'text nil))
|
||||
(old-child
|
||||
(ebox-text-create
|
||||
:value "Old" :source-handle child-handle
|
||||
:owned-facts child-facts))
|
||||
(new-child
|
||||
(ebox-text-create
|
||||
:value "New" :source-handle child-handle
|
||||
:owned-facts child-facts))
|
||||
(parent
|
||||
(ebox-box-create
|
||||
:layout (ebox-normal-layout-create)
|
||||
:children (list old-child)
|
||||
:source-handle parent-handle
|
||||
:owned-facts
|
||||
(ebox-canonical-facts-from-declarations
|
||||
'box parent-declarations)))
|
||||
(input
|
||||
(ebox-canonical-input-create
|
||||
(list parent) (ebox-source-builder-finish builder)))
|
||||
(source-index
|
||||
(ebox-tree-source-index
|
||||
parent nil nil (ebox-canonical-input--source-index input)))
|
||||
(parent-subject (ebox-tree-node-subject source-index parent))
|
||||
(child-subject (ebox-tree-node-subject source-index old-child))
|
||||
(parent-style
|
||||
(ebox-style-compute-subject
|
||||
parent-subject parent-declarations))
|
||||
(child-style
|
||||
(progn
|
||||
(setf (ecss-subject-parent child-subject) parent-subject)
|
||||
(ebox-style-compute-subject child-subject nil parent-style)))
|
||||
(old-nodes (make-hash-table :test #'eql))
|
||||
(new-nodes (make-hash-table :test #'eql))
|
||||
(parents (make-hash-table :test #'eql))
|
||||
(old-state
|
||||
(list :node-table old-nodes :parent-table parents
|
||||
:source-index source-index))
|
||||
(candidate
|
||||
(list :node-table new-nodes :parent-table parents
|
||||
:source-index source-index)))
|
||||
(ebox-style-apply-computed parent parent-style)
|
||||
(ebox-style-apply-computed old-child child-style)
|
||||
(plist-put parent :node-id 1)
|
||||
(plist-put old-child :node-id 2)
|
||||
(plist-put new-child :node-id 2)
|
||||
(puthash 1 parent old-nodes)
|
||||
(puthash 2 old-child old-nodes)
|
||||
(puthash 1 parent new-nodes)
|
||||
(puthash 2 new-child new-nodes)
|
||||
(puthash 2 1 parents)
|
||||
(should-not (plist-get new-child :color))
|
||||
(ebox-surface-prepare-inline-candidate-styles
|
||||
old-state candidate '(1 2))
|
||||
(should (eq child-style (plist-get new-child :ebox-computed-style)))
|
||||
(should (eq 'foreground-slot (plist-get new-child :color)))
|
||||
(should (equal "New" (ebox-text-node-value new-child)))
|
||||
(should (eq child-handle (ebox-node-source-handle new-child)))
|
||||
(dolist (property ebox-tree--participation-keys)
|
||||
(should-not (plist-member new-child property)))
|
||||
(should (eq 'background-slot (plist-get parent :bgcolor)))))
|
||||
|
||||
(ert-deftest ebox-style-theme-delta-reuses-cascade-facts ()
|
||||
"Copy a paint-only Theme delta without rerunning ECSS cascade computation."
|
||||
(let* ((subject (ecss-subject-create :type "box"))
|
||||
@ -2097,7 +2184,8 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
|
||||
(ert-deftest ebox-tree-source-signature-ignores-derived-width-proof ()
|
||||
"A layout-derived exact-width flag must not dirty declarative content."
|
||||
(let* ((old (ebox-test-box :content "Stable" :width '(100)))
|
||||
(let* ((input (ebox-test-box (ebox-test-text "Stable") :width '(100)))
|
||||
(old (ebox-test-root input))
|
||||
(new (copy-tree old)))
|
||||
(plist-put old :ebox-content-width-exact-p nil)
|
||||
(plist-put new :ebox-content-width-exact-p t)
|
||||
@ -2216,7 +2304,7 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
"The public mount path should never transfer ownership of its source tree."
|
||||
(ebox-surface-test--reset-render-state)
|
||||
(let* ((source
|
||||
(ebox-test-box :key 'shared :content "Shared" :width '(100)))
|
||||
(ebox-test-box :key 'shared (ebox-test-text "Shared") :width '(100)))
|
||||
(first (generate-new-buffer " *ebox-public-first*"))
|
||||
(second (generate-new-buffer " *ebox-public-second*")))
|
||||
(unwind-protect
|
||||
@ -2234,9 +2322,10 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
(should-not
|
||||
(eq (plist-get (ebox--buffer-root-node first) :surface-object)
|
||||
(plist-get (ebox--buffer-root-node second) :surface-object)))
|
||||
(should-not (plist-member source :node-id))
|
||||
(should-not (plist-member source :region-id))
|
||||
(should-not (plist-member source :surface-object)))
|
||||
(let ((source-node (ebox-test-root source)))
|
||||
(should-not (plist-member source-node :node-id))
|
||||
(should-not (plist-member source-node :region-id))
|
||||
(should-not (plist-member source-node :surface-object))))
|
||||
(dolist (buffer (list first second))
|
||||
(when (buffer-live-p buffer)
|
||||
(kill-buffer buffer))))))
|
||||
@ -2246,7 +2335,7 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
(ebox-surface-test--reset-render-state)
|
||||
(let* ((source (generate-new-buffer " *ebox-context-source*"))
|
||||
(target (generate-new-buffer " *ebox-context-target*"))
|
||||
(node (ebox-test-box :content "MMMM" :width '(100)))
|
||||
(node (ebox-test-box (ebox-test-text "MMMM") :width '(100)))
|
||||
(seen-buffers nil)
|
||||
(original (symbol-function 'ebox--render-layout)))
|
||||
(unwind-protect
|
||||
@ -2276,7 +2365,7 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :key 'root :content "Before" :width '(100)))
|
||||
(ebox-test-box :key 'root (ebox-test-text "Before") :width '(100)))
|
||||
(let* ((surface (with-current-buffer
|
||||
buffer ebox-surface--buffer-surface))
|
||||
(revision (tp-surface-revision surface))
|
||||
@ -2284,7 +2373,7 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
(setq report
|
||||
(ebox-commit
|
||||
buffer
|
||||
(ebox-test-box :key 'root :content "After"
|
||||
(ebox-test-box :key 'root (ebox-test-text "After")
|
||||
:width '(100))))
|
||||
(should (= (tp-surface-revision surface) (1+ revision)))
|
||||
(should (plist-get report :runtime-published))
|
||||
@ -2302,7 +2391,7 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :key 'root :content "Stable" :width '(100)))
|
||||
(ebox-test-box :key 'root (ebox-test-text "Stable") :width '(100)))
|
||||
(let* ((surface (with-current-buffer
|
||||
buffer ebox-surface--buffer-surface))
|
||||
(state (tp-surface-client-state surface))
|
||||
@ -2313,7 +2402,7 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
(should-error
|
||||
(ebox-commit
|
||||
buffer
|
||||
(ebox-test-box :key 'root :content "Rejected" :width '(100))
|
||||
(ebox-test-box :key 'root (ebox-test-text "Rejected") :width '(100))
|
||||
(lambda (_report) (error "Reject publication"))))
|
||||
(should (= (tp-surface-revision surface) revision))
|
||||
(should (eq (tp-surface-client-state surface) state))
|
||||
@ -2334,7 +2423,7 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
(ebox-viewport-height 4))
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :key 'root :content "Stable"
|
||||
(ebox-test-box :key 'root (ebox-test-text "Stable")
|
||||
:width '(viewport)))
|
||||
(let* ((surface
|
||||
(with-current-buffer buffer ebox-surface--buffer-surface))
|
||||
@ -2351,7 +2440,7 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
(should-error
|
||||
(ebox-surface-mount-buffer
|
||||
buffer
|
||||
(ebox-test-box :key 'root :content "Rejected"
|
||||
(ebox-test-box :key 'root (ebox-test-text "Rejected")
|
||||
:width '(viewport))
|
||||
(ebox--update-report nil 'root-rerender)
|
||||
(lambda (_report) (error "Reject context publication"))
|
||||
@ -2375,12 +2464,12 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :key 'root :content "Stable" :width '(100)))
|
||||
(ebox-test-box :key 'root (ebox-test-text "Stable") :width '(100)))
|
||||
(should (gethash buffer ebox--buffer-render-state-table))
|
||||
(should-error
|
||||
(ebox-commit
|
||||
buffer
|
||||
(ebox-test-box :key 'root :content "Rejected" :width '(100))
|
||||
(ebox-test-box :key 'root (ebox-test-text "Rejected") :width '(100))
|
||||
(lambda (_report)
|
||||
(kill-buffer buffer)
|
||||
(error "Reject publication after teardown"))))
|
||||
@ -2397,13 +2486,13 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :key 'root :host-ref 'root
|
||||
:content "Before" :width '(100)))
|
||||
(ebox-test-box :key 'root :source-identity 'root
|
||||
(ebox-test-text "Before") :width '(100)))
|
||||
(let ((candidate (ebox-candidate-begin buffer)))
|
||||
(ebox-candidate-replace-host-ref
|
||||
candidate 'root
|
||||
(ebox-test-box :key 'root :host-ref 'root
|
||||
:content "After" :width '(100)))
|
||||
(ebox-test-box :key 'root :source-identity 'root
|
||||
(ebox-test-text "After") :width '(100)))
|
||||
(let ((report (ebox-commit buffer candidate)))
|
||||
(should (equal report (ebox-buffer-update-report buffer)))
|
||||
(should (eq (plist-get report :constraint-source)
|
||||
@ -2441,7 +2530,7 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
"One logical id should resolve to distinct handles on independent surfaces."
|
||||
(ebox-surface-test--reset-render-state)
|
||||
(let* ((source
|
||||
(ebox-test-box :id "status" :content "Ready" :width '(100)))
|
||||
(ebox-test-box :id "status" (ebox-test-text "Ready") :width '(100)))
|
||||
(first (generate-new-buffer " *ebox-handle-first*"))
|
||||
(second (generate-new-buffer " *ebox-handle-second*")))
|
||||
(unwind-protect
|
||||
@ -2473,8 +2562,8 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-column
|
||||
(ebox-test-box :id "left" :content "Left" :width '(100))
|
||||
(ebox-test-box :id "right" :content "Right" :width '(100))))
|
||||
(ebox-test-box :id "left" (ebox-test-text "Left") :width '(100))
|
||||
(ebox-test-box :id "right" (ebox-test-text "Right") :width '(100))))
|
||||
(let* ((surface (with-current-buffer
|
||||
buffer ebox-surface--buffer-surface))
|
||||
(revision (tp-surface-revision surface))
|
||||
@ -2501,7 +2590,7 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :id "target" :content "before target after"
|
||||
(ebox-test-box :id "target" (ebox-test-text "before target after")
|
||||
:width '(200)))
|
||||
(with-current-buffer buffer (goto-char 4))
|
||||
(let ((point-before (with-current-buffer buffer (point))))
|
||||
@ -2520,7 +2609,7 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :id "target" :content "Before" :width '(100)))
|
||||
(ebox-test-box :id "target" (ebox-test-text "Before") :width '(100)))
|
||||
(let* ((match (car (ebox-selector-query-buffer buffer "#target")))
|
||||
(region-id (plist-get match :region-id)))
|
||||
(should-error (ebox-region-update region-id :content "Wrong")
|
||||
@ -2541,7 +2630,7 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :id "action" :content "Closed" :width '(100)))
|
||||
(ebox-test-box :id "action" (ebox-test-text "Closed") :width '(100)))
|
||||
(let* ((match (car (ebox-selector-query-buffer buffer "#action")))
|
||||
(handle (plist-get match :region-handle)))
|
||||
(should (ebox-region-handle-p handle))
|
||||
@ -2561,12 +2650,12 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :key 'old :id "old"
|
||||
:content "Old" :width '(100)))
|
||||
(ebox-test-text "Old") :width '(100)))
|
||||
(let ((handle (ebox-region-resolve buffer "old")))
|
||||
(ebox-commit
|
||||
buffer
|
||||
(ebox-test-box :key 'new :id "new"
|
||||
:content "New" :width '(100)))
|
||||
(ebox-test-text "New") :width '(100)))
|
||||
(should-error (ebox-region-update handle :content "Invalid")
|
||||
:type 'user-error)))
|
||||
(when (buffer-live-p buffer)
|
||||
@ -2581,7 +2670,7 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
(ebox-viewport-height 2))
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :id "scroll" :content "zero\none\ntwo\nthree"
|
||||
(ebox-test-box :id "scroll" (ebox-test-text "zero\none\ntwo\nthree")
|
||||
:width '(120) :height 2 :overflow 'scroll))
|
||||
(let* ((surface (with-current-buffer
|
||||
buffer ebox-surface--buffer-surface))
|
||||
@ -2627,8 +2716,7 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
(full-renders 0))
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :id "scroll" :content
|
||||
(mapconcat #'number-to-string (number-sequence 0 31) "\n")
|
||||
(ebox-test-box :id "scroll" (ebox-test-text (mapconcat #'number-to-string (number-sequence 0 31) "\n"))
|
||||
:width '(120) :height 2 :overflow 'scroll))
|
||||
(let* ((surface (with-current-buffer
|
||||
buffer ebox-surface--buffer-surface))
|
||||
@ -2705,7 +2793,7 @@ changes. Geometry and non-inherited computed values must remain identical."
|
||||
(ebox-viewport-height 2))
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :id "scroll" :content "zero\none\ntwo\nthree"
|
||||
(ebox-test-box :id "scroll" (ebox-test-text "zero\none\ntwo\nthree")
|
||||
:width '(120) :height 2 :overflow 'scroll))
|
||||
(let* ((surface (with-current-buffer
|
||||
buffer ebox-surface--buffer-surface))
|
||||
@ -2746,13 +2834,13 @@ the staged rendered window must not rerun the Flex wrapper layout."
|
||||
(cl-loop for index below 80
|
||||
collect
|
||||
(ebox-test-box :key (intern (format "flex-cell-%03d" index))
|
||||
:content (format "Cell %03d" index)
|
||||
(ebox-test-text (format "Cell %03d" index))
|
||||
:width '(80) :height 1)))
|
||||
(flex (apply #'ebox-test-flex :flex-flow '(row wrap)
|
||||
:width '(180) :column-gap '(8) :row-gap 1
|
||||
children))
|
||||
(root (ebox-test-box :key 'scroll-root :width '(180) :height 6
|
||||
:overflow 'scroll :ebox-content-node flex))
|
||||
:overflow 'scroll flex))
|
||||
(buffer (ebox-render-to-buffer
|
||||
(generate-new-buffer-name " *ebox-incomplete-flex-scroll*")
|
||||
root))
|
||||
@ -2791,7 +2879,7 @@ the staged rendered window must not rerun the Flex wrapper layout."
|
||||
(ebox-viewport-height 2))
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :id "scroll" :content "zero\none\ntwo\nthree"
|
||||
(ebox-test-box :id "scroll" (ebox-test-text "zero\none\ntwo\nthree")
|
||||
:width '(120) :height 2 :overflow 'scroll))
|
||||
(let* ((surface (with-current-buffer
|
||||
buffer ebox-surface--buffer-surface))
|
||||
@ -2837,7 +2925,7 @@ participant and rollback contract without creating a real frame."
|
||||
(window-point 1))
|
||||
(ebox-render-to-buffer
|
||||
buffer
|
||||
(ebox-test-box :id "native-scroll" :content "zero\none\ntwo\nthree"
|
||||
(ebox-test-box :id "native-scroll" (ebox-test-text "zero\none\ntwo\nthree")
|
||||
:width '(120) :height 2 :overflow 'scroll))
|
||||
(let* ((state (ebox--buffer-render-state buffer))
|
||||
(region-id (car (plist-get state :scroll-region-ids)))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user