Prepare standalone distribution and harden clipping and interaction

This commit is contained in:
Kinneyzhang 2026-09-10 00:22:34 +08:00
parent 79f5bc23d1
commit a8417d56c6
46 changed files with 3206 additions and 147 deletions

View File

@ -7,24 +7,34 @@ on:
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
emacs: ['29.1', '30.2']
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Check out TP runtime dependency
run: git clone --depth 1 https://github.com/Kinneyzhang/tp.git ../tp
- name: Install supported Emacs version
uses: purcell/setup-emacs@34c6ded44899fd1bf74d2889558befd1750e61a7
with:
version: ${{ matrix.emacs }}
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y emacs-nox python3
- name: Fetch locked TP and ECSS dependencies
run: make release-deps RELEASE_DEPS_DIR="$RUNNER_TEMP/ebox-deps"
- name: Run local verification suite
run: make ci EMACS=emacs PYTHON=python3
run: make ci EMACS=emacs PYTHON=python3 TP_DIR="$RUNNER_TEMP/ebox-deps/tp" ECSS_DIR="$RUNNER_TEMP/ebox-deps/ecss"
- name: Verify source package installation without optional providers
run: make release-check EMACS=emacs RELEASE_DIR="$RUNNER_TEMP/ebox-release" RELEASE_OPTIONS="--dependencies $RUNNER_TEMP/ebox-deps"
- name: Verify optional Knuth-Plass installation
run: make release-check EMACS=emacs RELEASE_DIR="$RUNNER_TEMP/ebox-release-kp" RELEASE_OPTIONS=--with-ekp
- name: Run package-lint
run: make package-lint-install EMACS=emacs
run: make package-lint-install EMACS=emacs TP_DIR="$RUNNER_TEMP/ebox-deps/tp" ECSS_DIR="$RUNNER_TEMP/ebox-deps/ecss"
native-build:
strategy:
@ -62,6 +72,27 @@ jobs:
*) exit 1 ;;
esac
- name: Install Emacs for native integration
uses: jcs090218/setup-emacs@54b61ac9c1d7011a54d2b6d66dedbcddf20e2fb9
with:
version: '30.2'
- name: Install source packages and execute the native module
shell: bash
run: |
python scripts/ebox-release.py build --directory "$RUNNER_TEMP/ebox-native-release"
case "$RUNNER_OS" in
Linux) module="$PWD/native/target/release/libebox_native_reflow.so" ;;
macOS) module="$PWD/native/target/release/libebox_native_reflow.dylib" ;;
Windows) module="$PWD/native/target/release/ebox_native_reflow.dll" ;;
*) exit 1 ;;
esac
python scripts/ebox-release.py verify --directory "$RUNNER_TEMP/ebox-native-release" --emacs emacs --native-module "$module"
- name: Verify installed source can build its native module
if: runner.os == 'Linux'
run: make release-native-check EMACS=emacs RELEASE_DIR="$RUNNER_TEMP/ebox-native-release"
native-msrv:
strategy:
fail-fast: false

View File

@ -12,3 +12,5 @@ The old `emacs-box` checkout is a historical full-stack source tree. It is inten
For legacy public CSS size migrations, use `python3 scripts/migrate-css-sizes.py PATH...` first, then the same invocation with `--write` for reviewed public DSL/style callers. The tool preserves comments, strings, and source formatting, and reports dynamic/ambiguous values for manual review. Do not apply it to measured engine structs merely because they use the same property names. See its `--help` for input, exit-status, and cleanup contracts; use `python3 -m unittest discover -s scripts -p 'test_migrate_css_sizes.py'` to verify the tool. Extend this entry point instead of creating ad hoc migration probes.
For mixed test/implementation files, add `--callers ebox-build,ebox-create` (or the actual public API names) to restrict migration to those call subforms. ETAF `(styles ...)` blocks are recognized as literal data; native Emacs `(space ...)` and image display specs retain their own size grammar.
For standalone packaging or clean installation verification, use `make release-check RELEASE_DIR=/tmp/ebox-release EMACS=/path/to/emacs`; the output directory must not exist. The shared `scripts/ebox-release.py` entry point fetches exact public commits from `release-dependencies.json`, creates deterministic package.el source archives, and installs them in a temporary profile without sibling load paths. Add `RELEASE_OPTIONS=--with-ekp` for the optional KP provider. Use `make release-deps RELEASE_DEPS_DIR=/tmp/ebox-deps` for CI checkouts, or `--dependencies /absolute/deps` to reuse explicit clean pinned checkouts during a build. Use `make native-integration RELEASE_DIR=/tmp/ebox-release NATIVE_MODULE=/absolute/module EMACS=/path/to/emacs` to require actual module loading and native rendering, or `make release-native-check RELEASE_DIR=/tmp/ebox-release EMACS=/path/to/emacs` to build the installed package's bundled native source first. Temporary resources are removed on completion, failure, interruption, or subprocess timeout; output archives and requested dependency checkouts are retained. Extend this entry point instead of adding per-release install scripts. Unit coverage is `make release-tool-tests`; never publish, tag, or replace an existing output directory implicitly.

View File

@ -6,7 +6,22 @@ All notable changes to Ebox are documented here.
### Added
- Direct dependencies on the independent ECSS 0.1.0 cascade engine and TP 1.0.1 retained/reactive text runtime.
- Explicit native node capabilities `:help-echo`, `:pointer`, `:hover-style`,
and `:keymap`, including callback adapters `ebox-help-create` and
`ebox-keymap-create`, nested overrides, explicit-nil blocking, and retained
updates. These do not require ETAF or an arbitrary text-property escape hatch.
- Optional `ebox-next-interaction` and `ebox-previous-interaction` navigation
commands for rendered keymap owners; no default navigation bindings or
application state system are installed.
- Explicit geometry units `px`, `%`, `vw`, `vh`, `ch`, and `lh`, composable
`calc`/`min`/`max`/`clamp` functions, and construction-time axis validation.
- A repository-level GPL-3.0-or-later license and consistent Ebox/Rust package
declarations, with third-party notices retained.
- Locked dependency inputs and a reusable source-archive build/install verifier
(`make release-check`), including optional EKP and explicit native-module
integration. CI now prepares ECSS as well as TP, tests the minimum supported
Emacs version, and configures native loading/execution on all three platforms.
- Direct dependencies on the independent ECSS 0.1.0 cascade engine and TP 2.0.0 retained/reactive text runtime.
- ECSS-backed property schemas, selectors, stylesheets, inheritance, cascade layers, custom properties, computed values, and provenance for Box, Flex, and Grid nodes.
- Surface-scoped region handles through `ebox-region-resolve`, retained TP publication through `ebox-render-to-buffer` and `ebox-display-buffer`, and atomic handle/selector/viewport/scroll updates.
- Typography longhands and inherited foreground projection that keep text readable when child boxes override background colors.
@ -15,12 +30,34 @@ All notable changes to Ebox are documented here.
### Changed
- `ebox-display-buffer` renders before calling native `display-buffer`, accepts
its optional action argument, and returns the rendered buffer. Ebox no longer
deletes other windows or selects the result as part of ordinary display.
- `:overflow hidden` now also enforces horizontal content width, including
composed child output, while retaining the owner's padding and border.
Clipping preserves supported text clusters and visible properties, removes
clipped child interactions, and fills remaining pixels without an ellipsis.
This adapts the GPL-compatible s-pixel approach without a new dependency;
partial-glyph masking and stacking are not introduced.
- `:wrap-mode kp` explicitly requires the optional EKP package. Missing or
incompatible EKP now reports an installation/update error instead of silently
selecting word wrapping. Other wrapping modes do not require EKP.
- The minimum TP dependency is now 2.0.0 with the final v2 participant protocol.
Native keymap ownership, callback identity, and shared hover regions require
its property policies; the former TP 1.0.1 transition baseline is insufficient.
- Incremental updates reuse proven unchanged style, identity, ownership, and
viewport facts. Content and paint changes keep the smallest safe publication
scope; geometry or dependency changes still promote to a larger owner when
required. TP publishes the changed text interval and preserves rollback.
- Hover paint covers the declaring owner's text and padding as one native
hover region per rendered line, while preserving physical side borders.
Nested explicit hover declarations remain separate regions.
- Package metadata now distinguishes builds with the public unmount/revision
boundary and SPI v2 compatibility surface from older 2.x development
snapshots.
- TP 1.0.1 is the minimum consumer-first dependency. Ebox accepts its structured
transition protocol and TP 2.0.0's final v2-only protocol, while always using
the public structured participant API.
- Early 3.0 development used TP 1.0.1's structured transition protocol. The
release candidate now requires TP 2.0.0 or newer and always
uses the public structured participant API.
- SPI initial publication owns widening and an outer editor change group. A
failed stage or nonlocal rollback exit restores text, markers, overlays,
point, mark, narrowing, undo history, and modified state before it escapes.
@ -44,10 +81,31 @@ All notable changes to Ebox are documented here.
### Migration
- Install ECSS 0.1.0 and TP 1.0.1 or newer, in either order, before Ebox 3.0.0.
- Replace legacy geometry literals with explicit units: for example,
`:width 80` becomes `:width (ch 80)`, `:width (240)` becomes
`:width (px 240)`, and `:height 3` becomes `:height (lh 3)`. Use
`python3 scripts/migrate-css-sizes.py PATH...` to preview supported literal
rewrites; review reported dynamic or ambiguous values before adding `--write`.
Vertical geometry rejects `px`, and `fit-content` is now a bare keyword.
- Callers requiring selected or exclusive display must choose that window
policy explicitly; ordinary `ebox-display-buffer` follows native display
rules. Rendering without display remains `ebox-render-to-buffer`.
- Install EKP 1.0.0 or newer only when using `:wrap-mode kp`; its optional native
accelerator is independent from Ebox's optional Rust reflow module.
- Install ECSS 0.1.0 and TP 2.0.0 or newer, in either order, before Ebox 3.0.0.
- Remove any customization of `ebox-transaction-participant-route`; Ebox 3.0.0
requires TP's structured participant capability and fails closed when it is
missing or malformed.
- Replace numeric region-id mutation with `ebox-region-resolve` followed by `ebox-region-update` on the returned handle.
- Replace `ebox-pop-to-buffer` or `ebox-switch-to-buffer` with `ebox-display-buffer`, or call `ebox-render-to-buffer` when display is controlled by the caller.
- Ebox does not scan previously rendered text to reconstruct runtime identity. Remount the declarative source through Ebox 3.0.
### Release status and supported boundaries
- `3.0.0` remains unreleased. These changes prepare source distribution; they
do not announce an uploaded package archive or published release tag.
- Each mounted buffer owns one viewport layout. Use separate buffers for views
requiring independent widths. Vertical geometry remains whole-line based.
- Common UI text clusters are preserved, but complete Unicode grapheme
segmentation, browser-level bidi, stacking, and absolute positioning are
outside the current contract.

View File

@ -6,7 +6,18 @@ Ebox 的重要变化记录在这里。
### 新增
- 直接依赖独立的 ECSS 0.1.0 cascade engine 与 TP 1.0.1 retained/reactive text runtime。
- 显式原生节点能力 `:help-echo`、`:pointer`、`:hover-style` 与 `:keymap`,包含回调
适配器 `ebox-help-create`、`ebox-keymap-create`、嵌套覆盖、显式 nil 阻断和 retained
更新。不需要 ETAF 或任意文本属性逃生入口。
- 可选 `ebox-next-interaction`、`ebox-previous-interaction` 导航命令,在渲染后的
keymap owner 之间移动;不安装默认导航绑定或应用状态系统。
- 显式几何单位 `px`、`%`、`vw`、`vh`、`ch`、`lh`,可组合的
`calc`/`min`/`max`/`clamp` 函数,以及构造时的属性方向验证。
- 仓库级 GPL-3.0-or-later 许可证与统一的 Ebox/Rust 包声明,保留第三方声明。
- 固定依赖输入与可复用的源码归档构建/安装验证器(`make release-check`),包含
可选 EKP 和显式 native 模块集成。CI 补齐 ECSS 与 TP、测试最低支持 Emacs 版本,
并配置了三个平台的 native 加载与执行。
- 直接依赖独立的 ECSS 0.1.0 cascade engine 与 TP 2.0.0 retained/reactive text runtime。
- Box、Flex 与 Grid 节点统一使用 ECSS property schema、selector、stylesheet、inheritance、cascade layer、custom property、computed value 与 provenance。
- 通过 `ebox-region-resolve` 提供 surface-scoped region handle`ebox-render-to-buffer` 与 `ebox-display-buffer` 统一使用 retained TP publicationhandle、selector、viewport 与 scroll 更新加入同一原子事务。
- 增加 typography longhand 与继承 foreground 的投影;子盒子覆盖背景色时仍能保持文字可读。
@ -15,10 +26,24 @@ Ebox 的重要变化记录在这里。
### 变更
- `ebox-display-buffer` 先完成渲染,再调用原生 `display-buffer`,接受可选 action
返回渲染结果 buffer。普通显示不再删除其他窗口或选中结果窗口。
- `:overflow hidden` 现在也限制横向内容宽度,包括组合后的子节点输出,并保留自身
padding 与边框。裁剪保留受支持的文本簇和可见属性、移除被裁掉的子节点交互,用空白
补齐剩余像素,不加省略号。复用 GPL 兼容的 s-pixel 思路,不增加依赖、半字形遮罩或层叠。
- `:wrap-mode kp` 明确要求可选 EKP 包。缺失或不兼容时提示安装/更新,不再静默改用
单词换行。其他换行模式不需要 EKP。
- TP 最低依赖提升到 2.0.0 与最终 v2 participant 协议。原生 keymap 所有权、回调身份
和共享 hover 范围需要其属性策略,原来的 TP 1.0.1 过渡基线不满足这些要求。
- 增量更新复用已证明不变的样式、身份、所有权与视口事实。内容和绘制变化保留最小
安全发布范围,几何或依赖变化仍在必要时提升 owner。TP 发布实际变化的文本区间,
并保留失败回滚。
- 每个渲染行内hover 将声明 owner 的文字与 padding 作为一个原生悬停区域,并保留
物理左右边框;子节点显式 hover 声明仍形成独立区域。
- 包 metadata 现在会区分具备 public unmount/revision 边界与 SPI v2
compatibility surface 的构建,旧的 2.x development snapshot 不会再被误接受。
- TP 1.0.1 是 consumer-first 迁移的最低依赖。Ebox 接受其 structured 过渡协议与
TP 2.0.0 的最终 v2-only 协议,但始终只调用公开 structured participant API。
- 早期 3.0 开发使用 TP 1.0.1 的 structured 过渡协议;当前候选版本要求 TP 2.0.0
或更新版本,并始终只调用公开 structured participant API。
- SPI initial publication 自己负责 widening 与外层 editor change group。stage
失败或 rollback 非局部退出向外传播前,会恢复 text、marker、overlay、point、
mark、narrowing、undo history 与 modified state。
@ -41,10 +66,26 @@ Ebox 的重要变化记录在这里。
### 迁移
- ECSS 0.1.0 与 TP 1.0.1 或更高版本互相独立,安装顺序任意;两者都安装后再安装
- 将旧几何字面量改为显式单位:例如 `:width 80` 改为 `:width (ch 80)`
`:width (240)` 改为 `:width (px 240)``:height 3` 改为 `:height (lh 3)`
先用 `python3 scripts/migrate-css-sizes.py PATH...` 预览支持的字面量迁移,检查
动态或歧义报告后再加 `--write`。纵向几何不接受 `px``fit-content` 只使用裸关键词。
- 需要选中或独占显示的调用方应显式选择相应窗口策略;普通 `ebox-display-buffer`
遵循原生显示规则。只渲染不显示时仍使用 `ebox-render-to-buffer`
- 仅在使用 `:wrap-mode kp` 时安装 EKP 1.0.0 或更高版本;其可选 native 加速器与
Ebox 的可选 Rust reflow 模块独立。
- ECSS 0.1.0 与 TP 2.0.0 或更高版本互相独立,安装顺序任意;两者都安装后再安装
Ebox 3.0.0。
- 删除 `ebox-transaction-participant-route` 的自定义设置Ebox 3.0.0 要求 TP
提供 structured participant capability缺失或格式错误时会 fail closed。
- 数字 region id 更新改为先调用 `ebox-region-resolve`,再把返回的 handle 交给 `ebox-region-update`
- `ebox-pop-to-buffer``ebox-switch-to-buffer` 改为 `ebox-display-buffer`;如果显示行为由调用方控制,则使用 `ebox-render-to-buffer`
- Ebox 不扫描历史渲染文本来重建 runtime identity请通过 Ebox 3.0 重新 mount 声明式 source。
### 发布状态与支持边界
- `3.0.0` 仍未发布;这些修改准备源码分发,不代表已上传包归档或发布版本 tag。
- 每个已挂载 buffer 拥有一套视口布局,需要独立宽度的视图使用不同 buffer。纵向
几何仍以完整行为单位。
- 保留常见 UI 文本簇,但完整 Unicode 字素分段、浏览器级双向布局、层叠和绝对定位
不属于当前契约。

View File

@ -30,9 +30,42 @@ The owners are `ebox-child-range.el` for immutable child-sequence and key indexe
Use the functions and properties in `docs/user/ebox-api-reference.en.md`; the facade inventory is `ebox-public-api`. Do not call `ebox--*` names from an application or a sibling package. Higher-level Component, reactive, behavior, data, and control concepts belong to ETAF.
ECSS and TP are required package dependencies, even when an author does not
write selectors or stylesheets. EKP is a separate optional typesetting
dependency: selecting `:wrap-mode kp` requires its public paragraph layout API.
An unavailable or incompatible EKP is an error, not permission to substitute a
different wrapping algorithm. Its accelerator is independent of Ebox's Rust
reflow module.
`ebox-display-buffer` renders before delegating window placement to native
`display-buffer`. It accepts the caller's display action, returns the buffer,
and does not choose an exclusive window layout or select the result. A buffer
owns one mounted layout and viewport; independently sized views use distinct
buffers rather than competing layouts over the same text.
## Layout scope
Ebox supports explicit `px`, `%`, `vw`, `vh`, `ch`, and `lh` dimensions, padding, margins, borders, colors, overflow, wrapping, row/column/flex formatting, and Grid tracks including fixed, fractional, implicit, gap, placement, span, and alignment behavior. The public typography subset is `:font-family`, CSS-reference-pixel `:font-size`, `:font-weight`, and `:font-style`; `:font-slant` is only an exact parse-time alias of `:font-style`. Emacs faces and text properties belong to the final adapter and are not Ebox author properties. CSS compatibility is intentionally partial: absolute positioning, z-index, shadows, border radius, full browser typography, and browser-level bidi are outside this package.
Ebox supports explicit `px`, `%`, `vw`, `vh`, `ch`, and `lh` dimensions, padding, margins, borders, colors, overflow, wrapping, row/column/flex formatting, and Grid tracks including fixed, fractional, implicit, gap, placement, span, and alignment behavior. The public typography subset is `:font-family`, CSS-reference-pixel `:font-size`, `:font-weight`, and `:font-style`; `:font-slant` is only an exact parse-time alias of `:font-style`. Arbitrary Emacs faces and text-property plists belong to the final adapter and are not Ebox author inputs. Four explicit native capabilities are supported separately: `:help-echo`, `:pointer`, `:hover-style`, and `:keymap`. CSS compatibility is intentionally partial: absolute positioning, z-index, shadows, border radius, full browser typography, and browser-level bidi are outside this package.
The text-cluster model covers common combining marks, variation selectors,
emoji modifiers, ZWJ sequences, and regional-indicator pairs. It is deliberately
smaller than complete Unicode grapheme segmentation; neither character count
nor this limited segmentation promises arbitrary font-shaping equivalence.
Optional interaction navigation moves point between currently rendered keymap
owners. It reads the committed surface, respects narrowing and invisibility,
and installs no key bindings, focus registry, or application state. Ownership
of actions and state remains with the caller.
Horizontal `:overflow hidden` is enforced on the composed content before the
owner's padding and border are added. It selects a measured prefix of complete
supported text clusters and fills the remaining pixels with space. Fixed
display spaces may shrink; glyphs and images are not partially raster-cropped.
Clipped child properties do not survive on neutral filler. The algorithm adapts
the prefix-and-filler approach from
[s-pixel](https://github.com/Kinneyzhang/s-pixel/tree/a2a0d6ae6b3bd71c1a70084d0ea37f56463e5fe0)
under GPL-3.0-or-later, with Ebox's cluster/property model and no `s` or
`s-pixel` dependency. It does not add stacking or overlap composition.
## Size composition

View File

@ -30,9 +30,34 @@ Ebox 是底层空间渲染引擎。它负责把声明式节点树转换为经过
使用 `docs/user/ebox-api-reference.zh.md` 中的函数与 property门面清单是 `ebox-public-api`。应用和同级包不得调用 `ebox--*` 私有名称。更高层的 Component、响应式、behavior、data 和 control 概念属于 ETAF。
ECSS 与 TP 是必需的包依赖即使作者没有写选择器或样式表也需要。EKP 是独立的可选
排版依赖:选择 `:wrap-mode kp` 时需要它的公开段落排版 API。EKP 不可用或不兼容时
会报错,不会擅自替换换行算法。它的加速器与 Ebox Rust reflow 模块相互独立。
`ebox-display-buffer` 先渲染,再把窗口放置交给原生 `display-buffer`。它接受调用方的
display action、返回 buffer不选择独占窗口布局也不选中结果窗口。一个 buffer
拥有一套挂载布局与视口;需要独立尺寸的视图应使用不同 buffer不在同一段文本上
维护互相竞争的布局。
## 布局范围
Ebox 支持显式 `px`、`%`、`vw`、`vh`、`ch`、`lh` 尺寸、padding、margin、border、颜色、overflow、换行、row/column/flex formatting以及固定、分数、隐式、gap、放置、span 和对齐等 Grid 能力。公共 typography 子集是 `:font-family`、以 CSS reference pixel 表示的 `:font-size`、`:font-weight` 和 `:font-style``:font-slant` 只是 `:font-style` 的精确 parse-time alias。Emacs face 与文本属性只属于最终 adapter不是 Ebox 作者属性。CSS 兼容性有意是部分实现绝对定位、z-index、阴影、border radius、完整浏览器 typography 和浏览器级 bidi 不属于本包。
Ebox 支持显式 `px`、`%`、`vw`、`vh`、`ch`、`lh` 尺寸、padding、margin、border、颜色、overflow、换行、row/column/flex formatting以及固定、分数、隐式、gap、放置、span 和对齐等 Grid 能力。公共 typography 子集是 `:font-family`、以 CSS reference pixel 表示的 `:font-size`、`:font-weight` 和 `:font-style``:font-slant` 只是 `:font-style` 的精确 parse-time alias。任意 Emacs face 和文本属性 plist 属于最终 adapter不是 Ebox 作者输入;四种显式原生能力 `:help-echo`、`:pointer`、`:hover-style`、`:keymap` 单独受支持。CSS 兼容性有意是部分实现绝对定位、z-index、阴影、border radius、完整浏览器 typography 和浏览器级 bidi 不属于本包。
文本簇模型覆盖常见组合音标、变体选择符、Emoji 修饰符、ZWJ 序列和区域指示符配对。
它的范围小于完整 Unicode 字素分段;字符数量或这个有限的分段模型,都不能保证任意
字体塑造结果相同。
可选交互导航在当前渲染的 keymap owner 之间移动 point读取已提交 surface尊重
narrowing 与不可见性不安装按键绑定、焦点注册表或应用状态。action 与状态仍属于
调用方。
横向 `:overflow hidden` 在内容组合后、自身 padding 和 border 添加前执行。它选取
测量后能容纳的完整受支持文本簇前缀,再用空白补齐剩余像素。固定 display space 可以
缩短,字形与图像不做局部位图裁剪;裁掉的子节点属性不会残留在中性填充上。
算法按 GPL-3.0-or-later 复用了
[s-pixel](https://github.com/Kinneyzhang/s-pixel/tree/a2a0d6ae6b3bd71c1a70084d0ea37f56463e5fe0)
的前缀加空白补齐思路,适配 Ebox 的文本簇与属性模型,不增加 `s``s-pixel` 依赖,
也不引入层叠或重叠组合。
## 尺寸组合

674
LICENSE Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@ -1,8 +1,13 @@
EMACS ?= emacs
CARGO ?= cargo
PYTHON ?= python3
TP_DIR ?= ../tp
ECSS_DIR ?= ../ecss
LOAD_EXTRA ?=
RELEASE_DIR ?= /tmp/ebox-release
RELEASE_DEPS_DIR ?= /tmp/ebox-release-deps
RELEASE_OPTIONS ?=
NATIVE_MODULE ?=
EMACS_BATCH = $(EMACS) -Q --batch -L . -L $(ECSS_DIR) -L $(TP_DIR) $(LOAD_EXTRA) --eval '(setq load-prefer-newer t)'
EMACS_TEST = $(EMACS_BATCH) -L tests -l tests/ebox-fixtures.el
@ -14,6 +19,7 @@ NATIVE_RELEASE_DIR = native/target/$(NATIVE_TARGET)/release
.PHONY: node-memo-generation-tests
.PHONY: interaction-tests
.PHONY: display-tests release-deps release-build release-check release-tool-tests native-integration release-native-check
all: check
@ -22,6 +28,29 @@ interaction-tests:
test: interaction-tests
display-tests:
$(EMACS_TEST) -l tests/ebox-display-tests.el -f ert-run-tests-batch-and-exit
test: display-tests release-tool-tests
release-deps:
$(PYTHON) scripts/ebox-release.py deps --directory "$(RELEASE_DEPS_DIR)" $(RELEASE_OPTIONS)
release-build:
$(PYTHON) scripts/ebox-release.py build --directory "$(RELEASE_DIR)" $(RELEASE_OPTIONS)
release-check: release-build
$(PYTHON) scripts/ebox-release.py verify --directory "$(RELEASE_DIR)" --emacs "$(EMACS)"
native-integration:
$(PYTHON) scripts/ebox-release.py verify --directory "$(RELEASE_DIR)" --emacs "$(EMACS)" --native-module "$(NATIVE_MODULE)"
release-native-check:
$(PYTHON) scripts/ebox-release.py verify --directory "$(RELEASE_DIR)" --emacs "$(EMACS)" --build-native
release-tool-tests:
$(PYTHON) -m unittest discover -s scripts -p 'test_ebox_release.py'
check: ci
ci: checkdoc load compile test visual-check diff-check

114
README.md
View File

@ -7,10 +7,20 @@ application also needs Components, reactive state, behaviors, or lifecycle.
## Install
Ebox requires Emacs 29.1 or newer, ECSS 0.1.0 or newer, and TP 1.0.1 or newer.
A package manager should
install the declared dependencies. For sibling source checkouts, add the three
directories to `load-path`, then load Ebox:
The source repository is [geekinney/ebox](https://gitea.gklazycat.heiyu.space/geekinney/ebox).
The current `3.0.0` changelog is unreleased; a source checkout is not a published
package archive or stable release tag.
| Dependency | Required for |
| --- | --- |
| Emacs 29.1 or newer | All Ebox use. |
| ECSS 0.1.0 or newer | Style computation and selector semantics; required even without a stylesheet. |
| TP 2.0.0 or newer | Retained publication, native property policies, and transactions; required when loading Ebox. |
| [EKP 1.0.0 or newer](https://github.com/Kinneyzhang/emacs-kp) | Optional; required only when using `:wrap-mode kp`. |
ECSS and TP are declared package dependencies. EKP is deliberately optional:
ordinary `word`, `char`, and `none` wrapping does not need it. For sibling source
checkouts, add the required directories to `load-path`, then load Ebox:
```elisp
(add-to-list 'load-path "/path/to/ecss")
@ -21,6 +31,54 @@ directories to `load-path`, then load Ebox:
Loading Ebox does not create a buffer or build native code.
For KnuthPlass paragraph layout, also add the EKP source directory to
`load-path`, or install its package, before rendering a Box with `:wrap-mode kp`:
```elisp
(add-to-list 'load-path "/path/to/ekp")
(ebox-render
(ebox-build '(box :width (ch 40) :wrap-mode kp :overflow hidden
"A paragraph laid out with KnuthPlass line breaking.")))
```
Ebox loads EKP when that wrapping mode is used. A missing or incompatible EKP
signals an installation or update error; install EKP or explicitly choose
another wrapping mode. Ebox does not silently render using `word`.
EKP's optional accelerator is separate
from Ebox's optional Rust module.
### Build and verify an installable archive
From this checkout, run:
```sh
make release-check EMACS=emacs RELEASE_DIR=/tmp/ebox-release-3.0.0
```
This fetches the exact dependency commits in
[`release-dependencies.json`](release-dependencies.json), builds deterministic
`package.el` source archives, and installs them into a temporary clean profile
to exercise rendering, updates, and selectors. It preserves the archive
directory and `manifest.json`, including checksums and source revision/dirty
status; it does not publish or change your Emacs profile. Choose a new output
directory on each run: existing directories are not overwritten.
To include and test optional KP layout, use a different output directory and
add `RELEASE_OPTIONS=--with-ekp`. Once an archive has passed verification, it can
be used as a local package archive:
```elisp
(require 'package)
(add-to-list 'package-archives '("ebox-local" . "/tmp/ebox-release-3.0.0/"))
(package-refresh-contents)
(package-install 'ebox)
;; Optional, only if the archive was built with --with-ekp:
;; (package-install 'ekp)
```
Tool inputs, cleanup, offline reuse, and native verification are documented in
the [maintainer guide](docs/maintainer/ebox-current-implementation-reference.en.md#source-package-distribution).
## First render
The ordinary author model has seven entries: a string, `text`, `box`, `row`,
@ -93,8 +151,10 @@ ordinary zero-argument callbacks to activation
keys or custom keys; it supplies the clicked window's buffer for mouse commands.
Use `ebox-region-update` with a semantic ID in the current mounted buffer or
an explicit region handle to replace or clear these capabilities.
Native keymaps support independent Ebox interactions; application state,
focus navigation, and command behavior remain the author's responsibility.
Native keymaps support independent Ebox interactions. Optional
`ebox-next-interaction` and `ebox-previous-interaction` commands move point
between rendered keymap owners without adding default key bindings.
Application state and command behavior remain the author's responsibility.
See the [interaction guide](docs/user/ebox-user-guide.en.md#native-node-interaction)
and the Playground's [interaction lab](../ebox-playground/README.md#native-interaction-lab).
@ -102,6 +162,8 @@ and the Playground's [interaction lab](../ebox-playground/README.md#native-inter
- `ebox-render` returns propertized text without publishing a live buffer.
- `ebox-render-to-buffer` mounts a retained surface.
- `ebox-display-buffer` renders successfully first, then displays the buffer
using Emacs display rules and an optional action; it returns the buffer.
- `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.
@ -120,11 +182,11 @@ external capabilities are not frozen. Querying during a TP transaction or
after unmounting signals an error. Compare both mount ID and revision when
identifying a committed generation; remounting can restart revision numbers.
Ebox registers rollback-capable state only through TP's public structured
participant API. TP 1.0.1 supports the consumer-first migration protocol;
TP 2.0.0 publishes the final v2-only protocol. Missing or malformed structured
participant capability stops Ebox loading instead of selecting a compatibility
writer.
Ebox registers rollback-capable state only through TP 2.0.0's public structured
participant API and final v2 protocol. Older transition builds do not provide
the native property ownership and hover identity guarantees required by this
release. Missing or malformed structured participant capability stops Ebox
loading instead of selecting a compatibility writer.
## Optional native module
@ -136,6 +198,31 @@ exact Elisp fallback. Ebox never builds it while loading.
(ebox-native-build)
```
## Display and text boundaries
Each mounted buffer has one viewport layout. Showing the same buffer in two
windows of different widths does not create two independent layouts. Use
separate mounted buffers for independent widths; the same canonical input may
be mounted more than once.
Vertical geometry is materialized in whole lines. Text handling preserves
common combining-mark, variation-selector, emoji-modifier, ZWJ, and flag
sequences; it does not implement complete Unicode grapheme segmentation or a
browser's typography and bidirectional layout. Font availability and native
pointer/help display depend on Emacs and the window system. Terminal output
cannot reproduce every GUI pixel or pointer effect.
`:overflow hidden` limits content to the box's measured width and whole-line
height, preserving its padding and borders. Horizontal clipping keeps whole
supported text clusters and fills unused pixels with space; it does not render
partial glyphs or insert an ellipsis. `scroll` remains vertical scrolling.
The CI configuration covers Emacs 29.1 and 30.2 on Linux, and source-package
installation plus native-module loading/execution on Linux, macOS, and Windows
with Emacs 30.2. Those jobs define the verification matrix; they are not a claim
that every platform's GUI rendering has been visually verified. Consult the
workflow results for the particular revision being installed.
## Verification
```sh
@ -151,3 +238,8 @@ make native-rust-tests
See the [user guide](docs/user/ebox-user-guide.en.md), the [public API
reference](docs/user/ebox-api-reference.en.md), and the sibling
[ebox-playground](../ebox-playground/README.md) examples.
## License
Ebox-owned code is distributed under GPL-3.0-or-later; see [LICENSE](LICENSE).
Third-party files retain their own license notices.

View File

@ -6,9 +6,18 @@ Component、响应式状态、behavior 或生命周期时,使用同级 ETAF
## 安装
Ebox 需要 Emacs 29.1 或更高版本、ECSS 0.1.0 或更高版本,以及 TP 1.0.1 或
更高版本。包管理器应自动安装声明的依赖。使用同级源码 checkout 时,把三个目录
加入 `load-path` 后加载 Ebox
源码仓库为 [geekinney/ebox](https://gitea.gklazycat.heiyu.space/geekinney/ebox)。
当前 `3.0.0` 更新日志仍标记为未发布;源码 checkout 不代表已经发布的包归档或稳定版本 tag。
| 依赖 | 使用条件 |
| --- | --- |
| Emacs 29.1 或更高版本 | 所有 Ebox 使用场景。 |
| ECSS 0.1.0 或更高版本 | 样式计算和选择器语义;即使不使用样式表也需要。 |
| TP 2.0.0 或更高版本 | retained 发布、原生属性策略与事务;加载 Ebox 时需要。 |
| [EKP 1.0.0 或更高版本](https://github.com/Kinneyzhang/emacs-kp) | 可选,仅使用 `:wrap-mode kp` 时需要。 |
ECSS 与 TP 是包声明的必需依赖。EKP 保持可选,普通 `word`、`char`、`none` 换行不需要它。
使用同级源码 checkout 时,把必需的目录加入 `load-path` 后加载 Ebox
```elisp
(add-to-list 'load-path "/path/to/ecss")
@ -19,6 +28,48 @@ Ebox 需要 Emacs 29.1 或更高版本、ECSS 0.1.0 或更高版本,以及 TP
加载 Ebox 不会创建 buffer也不会构建 native 模块。
使用 KnuthPlass 段落排版时,先额外将 EKP 源码目录加入 `load-path`,或安装它的包,
然后在 Box 上设置 `:wrap-mode kp`
```elisp
(add-to-list 'load-path "/path/to/ekp")
(ebox-render
(ebox-build '(box :width (ch 40) :wrap-mode kp :overflow hidden
"A paragraph laid out with KnuthPlass line breaking.")))
```
Ebox 在使用该换行模式时加载 EKP。EKP 缺失或不兼容时会提示安装或更新;请安装 EKP
或显式选择其他换行模式。Ebox 不会静默改用 `word`。EKP 自己的可选加速器与
Ebox 的可选 Rust 模块是两回事。
### 构建并验证可安装归档
在本仓库中运行:
```sh
make release-check EMACS=emacs RELEASE_DIR=/tmp/ebox-release-3.0.0
```
该入口按 [`release-dependencies.json`](release-dependencies.json) 获取精确的依赖提交,
构建可重复的 `package.el` 源码归档,在临时干净配置中安装并验证渲染、更新和选择器。
归档目录与 `manifest.json` 会保留,其中记录校验和、源码提交及工作区是否有修改;
它不会发布归档或修改你的 Emacs 配置。每次使用新的输出目录,不覆盖已有目录。
需要同时包含并测试可选 KP 排版时,换一个输出目录并加上 `RELEASE_OPTIONS=--with-ekp`
归档验证通过后,可以作为本地包归档安装:
```elisp
(require 'package)
(add-to-list 'package-archives '("ebox-local" . "/tmp/ebox-release-3.0.0/"))
(package-refresh-contents)
(package-install 'ebox)
;; 可选,仅当归档构建时使用了 --with-ekp
;; (package-install 'ekp)
```
工具输入、清理、离线复用与 native 验证见
[维护者指南](docs/maintainer/ebox-current-implementation-reference.zh.md#源码包分发)。
## 第一次渲染
普通用户只需理解七个 author 入口:字符串、`text`、`box`、`row`、`column`、
@ -80,7 +131,9 @@ padding 和 border不包含该 Box 的 margin 和结构性换行符。悬停
使用 `ebox-keymap-create` 将普通零参数回调绑定到激活键或自定义按键;鼠标命令会
自动在被点击窗口的 buffer 中运行。`ebox-region-update` 可以使用当前已挂载 buffer
中的语义 ID或显式 region handle替换或清除节点能力。原生 keymap 可用于
独立的 Ebox 交互;应用状态、焦点导航和命令行为由作者管理。参见
独立的 Ebox 交互;可选的 `ebox-next-interaction`、`ebox-previous-interaction` 命令
在渲染后的 keymap owner 之间移动 point不添加默认按键绑定。应用状态与命令行为
由作者管理。参见
[交互指南](docs/user/ebox-user-guide.zh.md#native-node-interaction)和 Playground 的
[交互实验室](../ebox-playground/README.zh-CN.md#native-interaction-lab)。
@ -88,6 +141,7 @@ padding 和 border不包含该 Box 的 margin 和结构性换行符。悬停
- `ebox-render` 返回带属性文本,不发布 live buffer
- `ebox-render-to-buffer` 挂载 retained surface
- `ebox-display-buffer` 先完成渲染,再按 Emacs 显示规则与可选 action 显示结果,返回 buffer
- `ebox-commit` 原子发布重新构建的 canonical input
- `ebox-buffer-update-report` 返回最近一次成功更新报告;
- `ebox-rerender-buffer-with-context` 应用显式 viewport 变化。
@ -103,8 +157,8 @@ Ebox 会在分配 runtime identity 前复制 canonical input因此同一个 b
卸载后查询会报错。识别提交版本时同时比较 mount ID 与 revision避免重新挂载后
revision 从头计数导致混淆。
Ebox 只通过 TP 的公开 structured participant API 注册可回滚状态。TP 1.0.1
支持 consumer-first 迁移协议TP 2.0.0 发布最终的 v2-only 协议。structured
Ebox 只通过 TP 2.0.0 的公开 structured participant API 与最终 v2 协议注册可回滚状态。
旧的过渡版本不具备本版本要求的原生属性所有权与 hover identity 保证。structured
participant capability 缺失或格式错误时Ebox 会停止加载,不会选择兼容 writer。
## 可选 native 模块
@ -117,6 +171,25 @@ fallback。Ebox 加载时不会自动构建它。
(ebox-native-build)
```
## 显示与文本边界
每个已挂载 buffer 只有一套视口布局。同一个 buffer 同时出现在不同宽度的两个窗口中,
不会产生两套独立布局。需要各自宽度时,分别挂载到不同 buffer同一份 canonical input
可以挂载多次。
纵向几何按完整行生成。文本处理保留常见组合音标、变体选择符、Emoji 修饰符、ZWJ 与
旗帜序列,但不实现完整 Unicode 字素分段,也不承诺浏览器级字体排版和双向布局。
可用字体、原生指针和帮助显示由 Emacs 与窗口系统决定;终端输出无法复现全部 GUI
像素或指针效果。
`:overflow hidden` 将内容限制在盒子测量后的宽度与完整行高度内,保留自身 padding 和
边框。横向裁剪保留完整的受支持文本簇,用空白补齐剩余像素,不显示半个字形,也不
添加省略号。`scroll` 仍是纵向滚动。
CI 配置覆盖 Linux 上的 Emacs 29.1、30.2,以及 Linux、macOS、Windows 上使用
Emacs 30.2 的源码包安装和 native 模块加载、执行。这些任务定义验证矩阵,不代表
所有平台的 GUI 渲染都已做视觉实测;请查看待安装提交对应的 workflow 结果。
## 验证
```sh
@ -132,3 +205,8 @@ make native-rust-tests
继续阅读[用户指南](docs/user/ebox-user-guide.zh.md)、[公共 API
参考](docs/user/ebox-api-reference.zh.md),以及同级
[ebox-playground](../ebox-playground/README.md) 示例。
## 许可证
Ebox 自有代码按 GPL-3.0-or-later 分发,详见 [LICENSE](LICENSE)。第三方文件保留各自的
许可证声明。

View File

@ -47,6 +47,16 @@ The active contract also covers `Makefile`, `.github/workflows/ci.yml`, `tests/e
Runtime index behavior is covered by `tests/ebox-runtime-index-tests.el`.
`tests/ebox-display-tests.el` covers render-before-display ordering, optional
native display actions, and preservation of unrelated windows and selection.
`make display-tests` runs it without operating a user's GUI instance.
The source distribution boundary includes `LICENSE`, `release-dependencies.json`,
`scripts/ebox-release.py`, `scripts/ebox-release-smoke.el`, and
`scripts/test_ebox_release.py`. The first tool owns locked dependency retrieval,
deterministic archives, and isolated installation; the Lisp smoke checks run
inside that installation rather than loading sibling checkouts.
Native node capabilities are covered by `tests/ebox-interaction-tests.el` through
`make interaction-tests`. The interactive reference and its command regressions
live in the sibling Playground package.
@ -113,7 +123,7 @@ Caller-owned Source Tree
- `ebox-incremental.el` likewise contains no `ebox-surface` symbol or publication call. `ebox-surface.el` installs one immutable read-only context port for live client state, region mounts, cascade proof, and inline-style preparation; batch observation remains in the higher `ebox.el` orchestrator.
- `ebox-patch-plan.el` consumes only tentative operation plists plus the immutable generation parent table and returns a deterministic plan artifact. The incremental adapter owns all live-fact preparation. E3 defaults to the pure route while retaining explicit `legacy` and equality-checking `shadow` routes; none of the three routes publishes.
- `ebox-style--property-definitions` is also the single used-value projection truth. Non-default engine lowering lives in each property's `:engine-projection`; ECSS metadata exposes the derived `:engine-targets`, and only non-public engine fields may use the small internal exception table.
- `ebox-framework-spi-capabilities` returns a fresh immutable SPI v2 provider record declaring paired initial/update stage+rollback, combined participant ordering, same-object reports, initial observation replay, and the actual accepted TP protocol. `ebox-framework-spi-required-tp-version` exposes the minimum TP 1.0.1 dependency, while `ebox-framework-spi-supported-tp-protocols` lists the consumer-first and final v2 protocol identities. Initial TP/Ebox timing is measured during publication, stored as defensive snapshots in the completed report, and replayed without mutation authority; no synthetic zero-duration timing is emitted. Ebox provides no selected port or consumer bootstrap.
- `ebox-framework-spi-capabilities` returns a fresh immutable SPI v2 provider record declaring paired initial/update stage+rollback, combined participant ordering, same-object reports, initial observation replay, and the actual accepted TP protocol. `ebox-framework-spi-required-tp-version` exposes the minimum TP 2.0.0 dependency, while `ebox-framework-spi-supported-tp-protocols` lists accepted structured protocol identities; protocol membership alone does not satisfy the minimum package version. Initial TP/Ebox timing is measured during publication, stored as defensive snapshots in the completed report, and replayed without mutation authority; no synthetic zero-duration timing is emitted. Ebox provides no selected port or consumer bootstrap.
- Ebox registers every rollback-capable runtime mirror through TP's public
`tp-transaction-participate-v2` API. The loaded manifest must advertise that
callable API with an accepted structured protocol; missing or malformed
@ -162,6 +172,12 @@ make check
make size-tests
make size-migration-tests
make interaction-tests
make display-tests
make release-tool-tests
make release-deps
make release-build
make release-check
make native-integration
make core-tests
make runtime-index-tests
make child-range-tests
@ -198,6 +214,70 @@ Run focused tests first, then `make check` after changes to shared rendering, Gr
`make size-tests` includes `make size-migration-tests`, so the migration tool's
Python regression suite also runs through `make check`.
## Source package distribution
Use `scripts/ebox-release.py` through the Make targets above rather than writing
another installation probe. `release-dependencies.json` pins public HTTPS URLs
and full commit IDs for mandatory TP/ECSS and optional EKP. It is a reproducible
build input, not a replacement for each package's minimum version contract.
EKP is included only with `--with-ekp`; ordinary archives do not make it an
Ebox package dependency.
Recommended complete verification:
```sh
make release-check EMACS=emacs RELEASE_DIR=/tmp/ebox-release-3.0.0
make release-check EMACS=emacs RELEASE_DIR=/tmp/ebox-release-3.0.0-kp RELEASE_OPTIONS=--with-ekp
```
Both commands fetch locked dependencies and build from the current Ebox source
files, excluding compiled output and temporary files. The output contains
deterministic tar archives, `archive-contents`, and a manifest with SHA-256
checksums, dependency commits, and Ebox source revision/dirty status. Modified
source is allowed for development verification and is recorded as such; a
clean release checkout should be used for a formal release artifact. Neither
command uploads files or creates a tag.
`release-check` builds a new directory and then verifies it. To verify an
existing archive without rebuilding or fetching dependencies:
```sh
python3 scripts/ebox-release.py verify --directory /tmp/ebox-release-3.0.0 --emacs emacs
```
`verify` checks archive checksums, uses `emacs -Q --batch`, and lets `package.el`
resolve dependencies from that local archive into a disposable package profile.
It checks autoloads, required package provenance, standalone loading,
render/update/selector behavior, and the real EKP provider when included.
No sibling `load-path` or user's installed packages supply missing dependencies.
The ordinary archive check does not build or require the Rust module.
To reuse already fetched dependencies, run `make release-deps` with a fresh
`RELEASE_DEPS_DIR`, then pass `RELEASE_OPTIONS="--dependencies /path/to/deps"`
to `release-check` or `release-build`. Those checkouts must be clean at the
locked commits. Include `--with-ekp` consistently when fetching and building
the KP variant. `release-build` stops after producing archives.
`make native-integration RELEASE_DIR=/path/to/archive EMACS=emacs
NATIVE_MODULE=/absolute/path/to/module` verifies an existing archive and requires
that exact module to load and produce a native frame. It does not build the
module. Supply the artifact for the host OS and architecture.
Existing output directories are rejected rather than overwritten. Temporary
checkouts and package profiles are removed on success, failure, interruption,
or subprocess timeout; completed archive/dependency outputs remain at the
explicit paths. Nonzero exit means verification is unsuccessful. Use
`make release-tool-tests` for tool regressions. This entry point supersedes
ad hoc clean-install probes.
CI runs the Elisp suite and clean installation on Linux with Emacs 29.1 and
30.2, including optional EKP installation. The native jobs build and test on
Linux, macOS, and Windows, then load and execute the built module through a
clean package installation on Emacs 30.2. Rust MSRV build/test jobs remain
separate. Configuration is not evidence of a successful remote run: inspect
the results for the specific revision, and do not infer GUI visual parity
from batch or Rust tests.
## Migrate literal author sizes
For old Ebox author literals, use the supported source-aware migration tool

View File

@ -47,6 +47,13 @@ active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-cor
Runtime index 行为由 `tests/ebox-runtime-index-tests.el` 覆盖。
`tests/ebox-display-tests.el` 覆盖先渲染后显示、可选原生 display action以及无关窗口
和选中状态的保持。`make display-tests` 不操作用户的 GUI 实例。
源码分发边界包括 `LICENSE`、`release-dependencies.json`、`scripts/ebox-release.py`、
`scripts/ebox-release-smoke.el``scripts/test_ebox_release.py`。第一个工具负责固定
依赖获取、可重复归档及隔离安装Lisp smoke 在安装结果内部运行,不加载同级 checkout。
原生节点能力由 `tests/ebox-interaction-tests.el` 通过 `make interaction-tests` 覆盖。
可交互参考示例及其命令回归测试位于同级 Playground 包。
@ -111,7 +118,7 @@ CSS 尺寸由 `tests/ebox-size-tests.el`、`tests/ebox-size-style-tests.el`、
- `ebox-incremental.el` 同样不再包含任何 `ebox-surface` symbol 或 publication call。`ebox-surface.el` 只安装一个不可变只读 context port提供 live client state、region mount、cascade proof 与 inline-style preparationbatch observation 留在更高层的 `ebox.el` orchestrator。
- `ebox-patch-plan.el` 只消费 tentative operation plist 与不可变 generation parent table并返回 deterministic plan artifact所有 live fact 准备仍由 incremental adapter 拥有。E3 默认使用 pure route同时保留显式 `legacy` 与执行等价检查的 `shadow` route三条 route 都不能发布。
- `ebox-style--property-definitions` 同时也是 used-value projection 的唯一真相源。非默认 engine lowering 写在每个 property 的 `:engine-projection`ECSS metadata 暴露派生的 `:engine-targets`,只有非公共 engine 字段可以进入小型 internal exception table。
- `ebox-framework-spi-capabilities` 返回 fresh immutable SPI v2 provider record声明 paired initial/update stage+rollback、combined participant order、same-object report、initial observation replay以及实际接受的 TP protocol。`ebox-framework-spi-required-tp-version` 公开最低 TP 1.0.1 依赖,`ebox-framework-spi-supported-tp-protocols` 列出 consumer-first 与最终 v2 protocol identity。initial TP/Ebox timing 在 publication 期间真实测量,以 defensive snapshot 写入 completed report 后再 replay不生成虚假的零耗时数据也不携带 mutate authority。Ebox 不创建 selected port 或 consumer bootstrap。
- `ebox-framework-spi-capabilities` 返回 fresh immutable SPI v2 provider record声明 paired initial/update stage+rollback、combined participant order、same-object report、initial observation replay以及实际接受的 TP protocol。`ebox-framework-spi-required-tp-version` 公开最低 TP 2.0.0 依赖,`ebox-framework-spi-supported-tp-protocols` 列出接受的 structured protocol identity只满足协议 membership 不等于满足最低包版本。initial TP/Ebox timing 在 publication 期间真实测量,以 defensive snapshot 写入 completed report 后再 replay不生成虚假的零耗时数据也不携带 mutate authority。Ebox 不创建 selected port 或 consumer bootstrap。
- Ebox 始终通过 TP 的公开 `tp-transaction-participate-v2` API 注册可 rollback 的
runtime mirror。加载的 manifest 必须用 accepted structured protocol 声明该可调用
APIcapability 缺失或格式错误会在加载时 fail closed。
@ -153,6 +160,12 @@ make check
make size-tests
make size-migration-tests
make interaction-tests
make display-tests
make release-tool-tests
make release-deps
make release-build
make release-check
make native-integration
make core-tests
make runtime-index-tests
make child-range-tests
@ -189,6 +202,54 @@ make diff-check
`make size-tests` 包含 `make size-migration-tests`,因此迁移工具的 Python 回归
测试也会通过 `make check` 执行。
## 源码包分发
通过上述 Make 入口使用 `scripts/ebox-release.py`,不另写安装探针。
`release-dependencies.json` 为必需的 TP、ECSS 与可选 EKP 固定公开 HTTPS URL 和完整
提交 ID。它是可重复构建的输入不替代各包声明的最低版本契约。只有指定 `--with-ekp`
才包含 EKP普通归档不会把它变成 Ebox 的必需包依赖。
推荐完整验证:
```sh
make release-check EMACS=emacs RELEASE_DIR=/tmp/ebox-release-3.0.0
make release-check EMACS=emacs RELEASE_DIR=/tmp/ebox-release-3.0.0-kp RELEASE_OPTIONS=--with-ekp
```
两条命令都获取固定依赖,从当前 Ebox 源码文件构建,排除编译产物和临时文件。输出包括
可重复的 tar 归档、`archive-contents`,以及记录 SHA-256 校验和、依赖提交、Ebox
源码提交和工作区修改状态的 manifest。开发验证允许修改过的源码并明确记录正式
发布产物应使用干净的 release checkout。两条命令都不会上传文件或创建 tag。
`release-check` 先构建新目录再验证;检查已有归档而不重建或获取依赖时,使用:
```sh
python3 scripts/ebox-release.py verify --directory /tmp/ebox-release-3.0.0 --emacs emacs
```
`verify` 检查归档校验和,用 `emacs -Q --batch` 启动,通过 `package.el` 从本地归档解析
依赖并安装到临时包目录,检查 autoload、必需包来源、独立加载、渲染/更新/选择器行为,
以及归档包含 EKP 时的真实 provider。缺少依赖不能从同级 `load-path` 或用户已安装包
中补齐。普通归档验证不构建或要求 Rust 模块。
复用已获取依赖时,先给 `make release-deps` 指定新的 `RELEASE_DEPS_DIR`,再给
`release-check``release-build` 传入 `RELEASE_OPTIONS="--dependencies /path/to/deps"`
依赖 checkout 必须位于固定提交且没有修改。KP 变体在获取和构建时均加上 `--with-ekp`
`release-build` 只生成归档。
`make native-integration RELEASE_DIR=/path/to/archive EMACS=emacs
NATIVE_MODULE=/absolute/path/to/module` 验证已有归档,并要求指定模块实际加载、生成
native frame。它不构建模块请提供当前系统与架构对应的产物。
已有输出目录会被拒绝,不会覆盖。临时 checkout 与包目录在成功、失败、中断或子进程
超时后清理,已完成的归档/依赖输出保留在明确指定的路径。非零退出表示验证未成功。
工具回归使用 `make release-tool-tests`。这个入口替代临时的干净安装探针。
CI 在 Linux 上以 Emacs 29.1 和 30.2 运行 Elisp 套件与干净安装,包括可选 EKP 安装。
native 任务在 Linux、macOS、Windows 构建和测试,再通过 Emacs 30.2 的干净包安装
加载并执行刚构建的模块Rust MSRV 构建/测试单独运行。配置不代表远程任务已经成功:
需要查看具体提交的结果,不能从 batch 或 Rust 测试推断 GUI 视觉效果一致。
## 迁移作者尺寸字面量
旧 Ebox 作者字面量使用已有的源码感知迁移工具,不另写一套搜索替换脚本。

View File

@ -6,7 +6,11 @@ This reference describes the current public boundary of the standalone Ebox
package. It separates the ordinary author path from the evaluated typed API
used by framework integrations. Names beginning with `ebox--` are private.
Ebox requires Emacs 29.1 or newer, ECSS, and TP:
Ebox requires Emacs 29.1 or newer, ECSS 0.1.0 or newer, and TP 2.0.0 or newer.
EKP 1.0.0 or newer is an optional dependency required for `:wrap-mode kp`;
`word`, `char`, and `none` do not load it. Missing or incompatible EKP signals
an actionable error instead of silently changing the wrapping algorithm.
See [installation](../../README.md#install) for source loading and dependency roles.
```elisp
(require 'ebox)
@ -71,6 +75,14 @@ another public node or wrapper.
| Text layout | `:text-align`, `:wrap-mode` (`word`, `char`, `kp`, `none`) |
| Overflow | `:overflow` (`visible`, `hidden`, `scroll`) |
`hidden` enforces the content width after composing children, preserving whole
supported text clusters and excluding the container's own padding and border
from the clipping area. Clipped-away child properties are removed with their
text. Partial display spaces can be shortened; oversized glyphs/images are
omitted, not pixel-masked. Vertical clipping uses whole lines. `visible` allows
overflow, and `scroll` provides vertical scrolling. No ellipsis or horizontal
scrolling is implied.
Geometry values use explicit `(unit number)` lists: `(px 240)`, `(% 50)`,
`(vw 100)`, `(vh 100)`, `(ch 80)`, and `(lh 3)`. Each property accepts only
the units in the [shared unit-constraint table](ebox-user-guide.en.md#size-unit-constraints);
@ -169,7 +181,19 @@ current buffer, using native dispatch at point.
Raw native keymaps remain supported. Their mouse commands must obtain the
target window from the event and operate in that window's buffer themselves.
These capabilities add no Ebox focus navigation or application state system.
`ebox-next-interaction` and `ebox-previous-interaction` are optional navigation
commands, each accepting an optional integer count (default one). Negative
counts reverse direction and zero leaves point unchanged. Each declaring node
with a command-bearing keymap contributes one stop at its first accessible
rendered character, including inherited coverage; explicit child maps form
separate stops. From inside an owner, navigation skips that owner. It respects
narrowing and invisibility, ignores ordinary buffer/overlay/global maps, and
uses the current committed output after updates or scrolling. A missing target
signals `user-error` without moving point; commands do not wrap or bind keys.
Menu enable forms and filters are not evaluated, so a declared command can
remain a stop when its application dynamically disables a menu item.
Navigation moves point only; Ebox provides no separate application state or
focus-management system.
See the [interaction guide](ebox-user-guide.en.md#native-node-interaction) for
a runnable example.
@ -271,13 +295,22 @@ though the facade inventory below focuses on application entry points.
| `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-unmount-buffer` | Release a mounted Ebox/TP surface, observers, retained indexes, and runtime authority from a live buffer. |
| `ebox-display-buffer` | Accept a single-root `CanonicalEboxInput`, render through the retained path, delete other windows, and switch to the result. |
| `ebox-display-buffer` | `(BUFFER-OR-NAME INPUT &optional ACTION)` renders a single-root `CanonicalEboxInput`, then calls native `display-buffer` with `ACTION` and returns the buffer. Ebox does not delete other windows or select 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. |
| `ebox-viewport-window-width` | Return Ebox's display-safe pixel width for a window. |
`ebox-display-buffer` respects `display-buffer-alist` and native action
precedence. Rendering failure occurs before any display call. Window placement
belongs to the selected Emacs display action; Ebox does not roll back a
successfully published buffer if a caller-supplied display action fails.
Each mounted buffer has one viewport layout, shared by all windows showing it.
Independent window widths require separate mounted buffers. Explicit viewport
updates change that buffer's shared layout.
`ebox-commit` accepts optional framework publish and rollback callbacks. They
join the existing atomic publication; failures restore the previous buffer,
surface, runtime state, and successful report.
@ -398,7 +431,8 @@ names every entry:
`ebox-normal-layout-create`, `ebox-row-layout-create`,
`ebox-column-layout-create`, `ebox-flex-layout-create`,
`ebox-grid-layout-create`, `ebox-box-create`.
- Interaction: `ebox-help-create`, `ebox-keymap-create`.
- Interaction: `ebox-help-create`, `ebox-keymap-create`,
`ebox-next-interaction`, `ebox-previous-interaction`.
- Render/publication: `ebox-render`, `ebox-render-to-buffer`,
`ebox-unmount-buffer`, `ebox-display-buffer`, `ebox-commit`,
`ebox-buffer-set-observer`,

View File

@ -5,7 +5,10 @@
本文描述独立 Ebox 包当前的公共边界,并把普通 author 路径与框架集成使用的
evaluated typed API 分开。以 `ebox--` 开头的名称都是私有实现。
Ebox 需要 Emacs 29.1 或更高版本、ECSS 和 TP
Ebox 需要 Emacs 29.1 或更高版本、ECSS 0.1.0 或更高版本,以及 TP 2.0.0 或更高版本。
EKP 1.0.0 或更高版本是 `:wrap-mode kp` 所需的可选依赖;`word`、`char`、`none`
不加载它。EKP 缺失或不兼容时会给出可操作的错误,不会静默改变换行算法。
源码加载方式和各项依赖职责见[安装说明](../../README.zh-CN.md#安装)。
```elisp
(require 'ebox)
@ -67,6 +70,11 @@ participation property 就是普通子 Box property不会创建另一种公
| 文本布局 | `:text-align`、`:wrap-mode``word`、`char`、`kp`、`none` |
| Overflow | `:overflow``visible`、`hidden`、`scroll` |
`hidden` 在组合子节点后约束内容宽度,保留完整的受支持文本簇;容器自身的 padding 与
border 不在裁剪区域内。被裁掉的子文本同时移除其属性。固定 display space 可以缩短,
放不下的字形/图像则整体省去,不做像素遮罩。纵向按完整行裁剪。`visible` 允许溢出,
`scroll` 提供纵向滚动;这些设置不隐含省略号或横向滚动。
几何值统一使用 `(单位 数值)``(px 240)`、`(% 50)`、`(vw 100)`、`(vh 100)`、
`(ch 80)`、`(lh 3)`。每个属性只接受[统一单位约束表](ebox-user-guide.zh.md#size-unit-constraints)
规定的单位,不能跨几何轴使用。不接受 CSS 字符串、裸长度、单元素像素列表及旧的
@ -146,7 +154,16 @@ Emacs 设置及窗口系统;单独设置指针形状不会绑定命令。
键盘回调在当前 buffer 中运行,由 point 所在位置进行原生分派。
仍支持直接传入原生 keymap其鼠标命令需要自行从事件取得目标窗口并在该窗口的
buffer 中操作。这些能力不引入 Ebox 焦点导航或应用状态系统。
buffer 中操作。
`ebox-next-interaction``ebox-previous-interaction` 是可选导航命令,均接受可选整数
count默认为一负数反向零不移动。每个声明了含命令 keymap 的节点在其首个可访问
渲染字符处形成一个停靠点,包括继承覆盖;显式子 map 单独形成停靠点。从 owner 内部
移动时跳过该 owner。导航尊重 narrowing 与不可见文本,忽略普通 buffer、overlay 和
全局 map在更新或滚动后使用当前已提交输出。目标不存在时抛出 `user-error`point
不动;命令不循环跳转,也不绑定按键。不会求值菜单 enable 表达式或 filter因此即使
应用动态禁用了菜单项,已声明命令仍可能保留停靠点。导航只移动 pointEbox 不提供
独立的应用状态或焦点管理系统。
可运行示例见[交互指南](ebox-user-guide.zh.md#native-node-interaction)。
## 2. Evaluated typed construction
@ -241,13 +258,20 @@ typed 模块还公开 `ebox-node-kind`、`ebox-text-node-p`、`ebox-box-node-p`
| `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-unmount-buffer` | 从 live buffer 释放 mounted Ebox/TP surface、observer、retained indexes 与 runtime authority。 |
| `ebox-display-buffer` | 接收 single-root `CanonicalEboxInput`,走 retained 路径渲染,删除其他 window 后切换到结果。 |
| `ebox-display-buffer` | `(BUFFER-OR-NAME INPUT &optional ACTION)` 先渲染 single-root `CanonicalEboxInput`,再以 `ACTION` 调用原生 `display-buffer`,返回 buffer。Ebox 不删除其他窗口,也不选中结果窗口。 |
| `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。 |
| `ebox-viewport-window-width` | 返回 Ebox 对 window 使用的 display-safe 像素宽度。 |
`ebox-display-buffer` 遵循 `display-buffer-alist` 与原生 action 优先级。渲染失败时
不会调用显示函数。窗口放置由选用的 Emacs display action 决定;调用方提供的 display
action 报错时Ebox 不会撤销已经成功发布的 buffer。
每个已挂载 buffer 只有一套视口布局,所有显示它的窗口共享该布局。不同窗口需要独立
宽度时,使用不同的挂载 buffer。显式视口更新修改的是该 buffer 共享的布局。
`ebox-commit` 接受可选 framework publish/rollback callback。它们加入现有原子发布
失败会恢复旧 buffer、surface、runtime state 与最近一次成功报告。
@ -355,7 +379,8 @@ 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-help-create`、`ebox-keymap-create`
- 交互:`ebox-help-create`、`ebox-keymap-create`、
`ebox-next-interaction`、`ebox-previous-interaction`
- 渲染/发布:`ebox-render`、`ebox-render-to-buffer`、`ebox-unmount-buffer`、
`ebox-display-buffer`、`ebox-commit`、`ebox-buffer-set-observer`、`ebox-buffer-update-report`、
`ebox-rerender-buffer-with-context`、`ebox-viewport-window-width`、

View File

@ -237,9 +237,21 @@ changes are resolved again. Floating-point intermediate results are preserved:
`33vw` of an 853-pixel viewport is 281.49 pixels before display quantization.
The buffer backend materializes block geometry in whole lines; fractional
line targets are quantized at that boundary. Vertical `px` lengths are not
part of the author contract. `:overflow hidden` currently clips vertical lines; horizontal
overflow is not pixel-clipped by the buffer backend, so content can extend
beyond the computed width.
part of the author contract. `:overflow hidden` clips excess vertical lines and
limits each content line to the measured content width. It retains the fitting
prefix without splitting supported text clusters; a glyph or image that cannot
fit is omitted as a whole. Fixed display spaces can be shortened to the exact
remaining pixel width. Any remaining gap is filled with box content space, so
padding and borders retain their positions. This is pixel-width truncation,
not partial-glyph/image masking or automatic ellipsis. Omitted child text does
not leave its help, hover, pointer, or keymap active in the filler.
For example, `(box :width (ch 3) :wrap-mode none :overflow hidden "ABCDEFGHIJ")`
keeps only the prefix that fits three zero-glyph advances; with `visible`,
overflow may extend outside the computed width. Set `hidden` on the container
whose content boundary should be enforced. It applies to that container's
composed child output as well as its direct text. `scroll` provides vertical
scrolling and does not add horizontal scrolling.
Bare geometry numbers, one-element pixel lists, `viewport`, `viewport-height`,
`contain`, and parameterized `fit-content` are rejected. Even zero needs an
@ -268,6 +280,36 @@ Text Box geometry.
Use `:outer inline` or `:outer block` to state how a Box participates in its
parent. The child-layout algorithm still comes from the form name.
### Wrapping and the optional EKP dependency
`:wrap-mode word` wraps ordinary words and permits character breaks for CJK;
`char` breaks at the common text-cluster boundaries supported by Ebox; `none`
keeps explicit newlines without adding soft line breaks. `kp` delegates
paragraph layout to the independent [EKP package](https://github.com/Kinneyzhang/emacs-kp)
using the KnuthPlass algorithm. The mode belongs to a Box and is inherited by
its text content.
Install EKP 1.0.0 or newer, or add its source directory to `load-path`, before
using `:wrap-mode kp`. It is an optional dependency loaded on demand; ordinary
Ebox layouts require only ECSS and TP. Choosing `kp` without a compatible EKP
signals an installation or update error; install EKP or explicitly select
another mode. Ebox never silently switches to word wrapping. The EKP accelerator and the Ebox Rust
reflow module are separate optional modules; enabling one does not install the
other.
```elisp
(add-to-list 'load-path "/path/to/ekp")
(ebox-render
(ebox-build '(box :width (ch 40) :wrap-mode kp :overflow hidden
"KnuthPlass considers the paragraph when choosing line breaks.")))
```
Common combining marks, variation selectors, emoji modifiers, ZWJ sequences,
and regional-indicator pairs are kept together by Ebox's text-cluster handling.
This is not complete Unicode grapheme segmentation or browser-level
bidirectional layout. Font shaping and glyph availability remain dependent on
Emacs, the selected fonts, and the window system.
<a id="native-node-interaction"></a>
### Native node interaction
@ -325,10 +367,25 @@ vectors paired with zero-argument callbacks. The helper uses the event window's
buffer for mouse callbacks, so a callback can directly update a semantic ID
with `(ebox-region-update "action" :help-echo "Activated")`.
Move point into the surface to use keyboard bindings. Raw native keymaps are
also accepted; their mouse commands must handle the event's target buffer
themselves. Ebox uses native command dispatch; it does not create application
state or focus navigation. A hand pointer alone does not bind a click command.
Move point into the surface to use keyboard bindings. Optional commands
`ebox-next-interaction` and `ebox-previous-interaction` move between declaring
nodes that currently render a usable keymap. Each owner contributes one stop,
even if its map covers padding or several lines; explicit child maps are
separate stops. Empty maps, explicit `nil`, and invisible or clipped-away
content do not add stops. Navigation follows the current committed buffer, so
updates do not require a separate refresh. At a boundary it reports a user error
without wrapping or moving point. These commands add no bindings automatically.
For example, opt in for buffers using `ebox-buffer-mode`:
```elisp
(keymap-set ebox-scroll-map "TAB" #'ebox-next-interaction)
(keymap-set ebox-scroll-map "<backtab>" #'ebox-previous-interaction)
```
Raw native keymaps are also accepted; their mouse commands must handle the
event's target buffer themselves. Ebox uses native command dispatch and does
not create an application state or focus-management system. A hand pointer
alone does not bind a click command.
The Playground's [interaction lab](../../../ebox-playground/README.md#native-interaction-lab)
keeps its business functions and state in `interaction-reference.el`, with its
@ -348,6 +405,21 @@ Both functions, and `ebox-display-buffer`, accept the opaque value returned by
(ebox-render-to-buffer "*Ebox Guide*" ebox-guide-input)
```
To display the result as well, use `ebox-display-buffer`. It finishes rendering
before asking Emacs to place the buffer, respects `display-buffer-alist`, and
accepts the same optional action argument as native `display-buffer`:
```elisp
(ebox-display-buffer "*Ebox Guide*" ebox-guide-input
'(display-buffer-pop-up-window))
```
It returns the buffer and does not select the result or delete other windows.
A rendering error leaves the window layout untouched. A custom display action
can still have its own window effects; if it fails after publication, the
successfully rendered buffer remains available. Use `ebox-render-to-buffer`
when the caller owns display placement entirely.
Build a fresh canonical input and use `ebox-commit` for an atomic update:
```elisp
@ -420,8 +492,12 @@ not select an unrelated scroll box elsewhere on the page.
"line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9"))
```
Visible mounted buffers follow their display window. Integrations may apply an
explicit viewport with:
Visible mounted buffers follow one chosen display window. Each mounted buffer
owns one viewport layout: showing the same buffer in two windows with different
widths does not provide two independent layouts. Mount the same canonical input
in separate buffers when each view needs its own width. Integrations may apply
an explicit viewport, measured in pixels horizontally and lines vertically,
with:
```elisp
(ebox-rerender-buffer-with-context

View File

@ -207,8 +207,16 @@ Box 高度为零;`(box :height (lh 1))` 明确保留一行空白。
中间结果保留小数,例如 853 像素视口的 `33vw` 在显示量化前是 281.49 像素。
当前 buffer 后端按完整行生成 block 几何,小数行目标在这个边界量化。作者接口不
接受纵向 `px` 长度。
`:overflow hidden` 当前只裁剪纵向行buffer 后端尚未实现横向像素裁剪,因此内容
可能超出已经计算确定的容器宽度。
`:overflow hidden` 裁掉超出的纵向行,并将每行内容限制在测量后的内容宽度内。
保留放得下的前缀,不拆开受支持的文本簇;放不下的字形或图像整体省去。固定 display
space 可以缩短到精确的剩余像素宽度,剩余空隙用盒子内容空白补齐,使 padding 和边框
保持位置。这是按像素宽度截断,不是显示半个字形/图像,也不自动添加省略号。
被省去的子文本不会在填充空白上留下自己的帮助、hover、指针或 keymap。
例如 `(box :width (ch 3) :wrap-mode none :overflow hidden "ABCDEFGHIJ")` 只保留
三个 `0` 字形前进宽度内放得下的前缀;使用 `visible` 时,溢出仍可超出计算宽度。
在哪个容器上设置 `hidden`,就约束哪个容器的内容边界,直接文本和组合后的子节点输出
都受约束。`scroll` 提供纵向滚动,不增加横向滚动。
不再接受几何裸数字、单元素像素列表、`viewport`、`viewport-height`、`contain`
以及带参数的 `fit-content`。零长度也需要对应轴允许的单位,例如横向 `(px 0)`
@ -234,6 +242,29 @@ Text 的样式声明只接受字体、前景/背景和文本装饰 property
`:outer inline``:outer block` 表达 Box 如何参与父布局。子布局算法仍由 form
名称决定。
### 换行与可选 EKP 依赖
`:wrap-mode word` 按普通单词换行,并允许 CJK 字符换行;`char` 按 Ebox 支持的常见
文本簇边界换行;`none` 保留显式换行符,不添加软换行。`kp` 将段落交给独立的
[EKP 包](https://github.com/Kinneyzhang/emacs-kp),使用 KnuthPlass 算法排版。
换行模式属于 Box并由其中的文本继承。
使用 `:wrap-mode kp` 前,安装 EKP 1.0.0 或更高版本,或将其源码目录加入 `load-path`
它是按需加载的可选依赖,普通 Ebox 布局只需要 ECSS 和 TP。选择 `kp` 而没有兼容的
EKP 时,会提示安装或更新;请安装 EKP或显式选择其他换行模式不会静默改用单词换行。
EKP 加速器与 Ebox Rust reflow 模块是两个独立的可选模块,启用一个不会安装另一个。
```elisp
(add-to-list 'load-path "/path/to/ekp")
(ebox-render
(ebox-build '(box :width (ch 40) :wrap-mode kp :overflow hidden
"KnuthPlass considers the paragraph when choosing line breaks.")))
```
Ebox 的文本簇处理会保留常见组合音标、变体选择符、Emoji 修饰符、ZWJ 序列和区域指示符
配对,但不实现完整 Unicode 字素分段或浏览器级双向布局。字形塑造和字形是否可用,
仍取决于 Emacs、所选字体和窗口系统。
<a id="native-node-interaction"></a>
### 原生节点交互
@ -284,9 +315,20 @@ margin 及结构性换行符;悬停遵循上面的规则。嵌套的 Text 或
辅助函数让鼠标回调在事件窗口的 buffer 中运行,因此回调可以直接按语义 ID 更新:
`(ebox-region-update "action" :help-echo "Activated")`
使用键盘绑定时,先把 point 移到交互范围内。仍可直接传入原生 keymap其鼠标命令
需要自行处理事件的目标 buffer。Ebox 使用原生命令分派,不创建应用状态或焦点导航。
单独设置手形指针不会绑定点击命令。
使用键盘绑定时,先把 point 移到交互范围内。可选命令 `ebox-next-interaction`
`ebox-previous-interaction` 在当前渲染了可用 keymap 的声明节点之间移动。每个 owner
只有一个停靠点,即使其 map 覆盖 padding 或多行;显式子 map 单独形成停靠点。
空 map、显式 `nil`、不可见或被裁掉的内容不产生停靠点。导航读取当前已提交 buffer
更新后不需要额外刷新。到达边界时报用户错误,不循环跳转,也不移动 point。
这些命令不会自动添加按键绑定。例如,为启用了 `ebox-buffer-mode` 的 buffer 选择启用:
```elisp
(keymap-set ebox-scroll-map "TAB" #'ebox-next-interaction)
(keymap-set ebox-scroll-map "<backtab>" #'ebox-previous-interaction)
```
仍可直接传入原生 keymap其鼠标命令需要自行处理事件的目标 buffer。Ebox 使用原生
命令分派,不创建应用状态或焦点管理系统。单独设置手形指针不会绑定点击命令。
Playground 的[交互实验室](../../../ebox-playground/README.zh-CN.md#native-interaction-lab)
把业务函数与状态放在 `interaction-reference.el`,把布局放在
@ -304,6 +346,18 @@ retained TP surface 并返回 buffer。它们和 `ebox-display-buffer` 都接收
(ebox-render-to-buffer "*Ebox Guide*" ebox-guide-input)
```
需要同时显示结果时,使用 `ebox-display-buffer`。它先完成渲染,再让 Emacs 放置
buffer遵循 `display-buffer-alist`,并接受与原生 `display-buffer` 相同的可选 action
```elisp
(ebox-display-buffer "*Ebox Guide*" ebox-guide-input
'(display-buffer-pop-up-window))
```
它返回 buffer不选中结果窗口也不删除其他窗口。渲染报错不会改变窗口布局。
自定义 display action 仍可能产生自己的窗口效果;如果 action 在发布后报错,
已经成功渲染的 buffer 仍然保留。调用方完全管理显示位置时,使用 `ebox-render-to-buffer`
重新构建 canonical input 后,用 `ebox-commit` 原子更新:
```elisp
@ -367,7 +421,10 @@ Ebox 对 keymap 创建快照;发布新绑定时应替换 `:keymap`,而不是
"line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9"))
```
可见 mounted buffer 会跟随其 display window。集成层也可以显式应用 viewport
可见 mounted buffer 会跟随选定的一个显示窗口。每个已挂载 buffer 只有一套视口布局:
将同一个 buffer 同时放到不同宽度的窗口中,不会产生两套独立布局。每个视图需要
各自宽度时,把同一份 canonical input 分别挂载到不同 buffer。集成层也可以显式应用
viewport横向以像素、纵向以行为单位
```elisp
(ebox-rerender-buffer-with-context

View File

@ -16,6 +16,8 @@
(require 'ebox-layout)
(require 'ebox-flex)
(declare-function ebox--viewport-dependent-subtree-p "ebox-incremental" (node))
(declare-function ebox--viewport-height-dependent-subtree-p "ebox-incremental" (node))
(declare-function ebox--ensure-node-id "ebox" (node))
(declare-function ebox--ensure-region-id "ebox" (box))
(declare-function ebox-box-node-p "ebox-canonical" (node))

View File

@ -49,6 +49,7 @@
(declare-function tp-object-mounts
"tp-surface" (object))
(declare-function ebox-string-lines "ebox" (string))
(declare-function ebox-lines-join "ebox" (lines))
(declare-function ebox-get "ebox" (box key))
(declare-function ebox--ensure-node-id "ebox" (node))
(declare-function ebox--ensure-region-id "ebox" (box))

View File

@ -20,6 +20,10 @@
(declare-function ebox--ensure-region-id "ebox" (box))
(declare-function ebox-runtime-index-get "ebox-runtime-index" (key index &optional default))
(declare-function ebox-fragment-style-source-node "ebox-fragment" (node))
(declare-function ebox-surface-buffer-mounted-p "ebox-surface" (buffer))
(declare-function ebox-surface--buffer-client-state "ebox-surface" (buffer))
(declare-function ebox-surface--paint-node-chain "ebox-surface" (state role-ids))
(defvar ebox-region-types)
(defvar ebox-buffer--decoration-style-map)
(defconst ebox-interaction-properties
@ -103,6 +107,136 @@ This function does not supply focus navigation, state or event propagation."
(push key keys))))
map))
(defun ebox-interaction--navigable-keymap-p (map)
"Return non-nil when MAP contains a command, including inherited bindings.
Inspect prefix and menu maps without invoking menu filters or enable forms."
(when (keymapp map)
(let ((copy (ebox-interaction--keymap-snapshot map))
(seen (make-hash-table :test #'eq)) commands)
(cl-labels
((binding
(value)
(cond
((keymapp value) (visit value) value)
((eq (car-safe value) 'menu-item) (binding (nth 2 value)))
((and (consp value) (stringp (car value))) (binding (cdr value)))
(t (when (and (not (eq value 'undefined)) (commandp value))
(cl-pushnew value commands :test #'eq))
value)))
(visit
(value)
(unless (gethash value seen)
(puthash value t seen)
(let ((parent (keymap-parent value))
(local (copy-sequence value)))
(set-keymap-parent local nil)
(map-keymap (lambda (event definition)
(define-key value (vector event) (binding definition)))
local)
(when parent (visit parent))))))
;; Native lookup composes inherited prefix maps instead of treating
;; a local prefix as shadowing its parent's entire prefix. Remove
;; menu wrappers only in this detached copy before using that lookup.
(visit copy)
(cl-some (lambda (command)
(where-is-internal command (list copy) 'non-ascii nil t))
commands)))))
(defun ebox-interaction--navigation-owner (position state)
"Resolve POSITION's live native keymap to its declaring node in STATE.
Rendered region ownership and the retained parent index identify inheritance;
an explicit nil on a descendant blocks an ancestor's declaration."
(when-let* ((map (get-text-property position 'keymap))
((keymapp map)))
(let ((chain (ebox-surface--paint-node-chain
state (cl-loop for (role . property) in ebox-region-types
for region = (get-text-property position property)
when region collect (cons role region)))))
(catch 'owner
(dolist (node-id chain)
(let* ((node (ebox-runtime-index-get node-id (plist-get state :node-table)))
(source (ebox-fragment-style-source-node node))
(entry (plist-member (ebox-get source :surface-properties) 'keymap)))
(when entry
(throw 'owner
(and (cadr entry)
(ebox-interaction-surface-equal-p
(list 'keymap map) (list 'keymap (cadr entry)))
node-id)))))))))
(defun ebox-interaction--move (count)
"Move by COUNT interaction stops in the accessible live Ebox surface.
Resolve every stop from current rendered properties; keep no navigation state."
(unless (integerp count)
(signal 'wrong-type-argument (list 'integerp count)))
(unless (ebox-surface-buffer-mounted-p (current-buffer))
(user-error "Current buffer has no live Ebox surface"))
(if (zerop count)
(point)
(let* ((state (ebox-surface--buffer-client-state (current-buffer)))
(origin (point))
(current-owner (and (< origin (point-max))
(ebox-interaction--navigation-owner origin state)))
(seen (make-hash-table :test #'equal))
(maps (make-hash-table :test #'eq))
(position (point-min)) stops)
(while (< position (point-max))
(let* ((end (next-char-property-change position (point-max)))
(map (get-text-property position 'keymap))
(owner (and map (ebox-interaction--navigation-owner position state))))
;; Structural line breaks carry no activation target. A property
;; run can begin at one after narrowing or native publication.
(while (and (< position end) (eq (char-after position) ?\n))
(setq position (1+ position)))
(when (and owner (< position end) (not (invisible-p position))
(not (equal (get-text-property position 'display) ""))
(not (gethash owner seen)))
(let ((navigable (gethash map maps 'unknown)))
(when (eq navigable 'unknown)
(setq navigable (ebox-interaction--navigable-keymap-p map))
(puthash map navigable maps))
(when navigable
(puthash owner t seen)
(push (cons position owner) stops))))
(setq position end)))
(when (> count 0) (setq stops (nreverse stops)))
(let* ((eligible
(cl-remove-if-not
(lambda (stop)
(and (not (equal (cdr stop) current-owner))
(if (> count 0) (> (car stop) origin)
(< (car stop) origin))))
stops))
(target (nth (1- (abs count)) eligible)))
(unless target
(user-error "No %s Ebox interaction" (if (> count 0) "next" "previous")))
(goto-char (car target))))))
;;;###autoload
(defun ebox-next-interaction (&optional count)
"Move point to the next rendered Ebox keymap owner; return its position.
With COUNT, move that many stops; a negative count moves backward, zero stays.
Each declaring node is one stop, at its first accessible rendered character,
even across multiple lines or inherited child content. Explicit child maps
are separate stops; nil and maps without commands are skipped. From within
an owner, skip that owner and seek the next stop after point.
Respect narrowing and invisible text. Use the current committed surface, so
updates, removal and scrolling need no navigation refresh. At a boundary,
signal `user-error' without moving; do not wrap or bind keys automatically.
Ordinary buffer, overlay and global keymaps do not create stops. Menu enable
forms and filters are not evaluated; a declared command remains a stop even
when an application dynamically disables its menu item."
(interactive "p")
(ebox-interaction--move (or count 1)))
;;;###autoload
(defun ebox-previous-interaction (&optional count)
"Move point to the previous rendered Ebox keymap owner; return its position.
COUNT defaults to one. Negative COUNT moves forward. See
`ebox-next-interaction' for ownership, narrowing and boundary behavior."
(interactive "p")
(ebox-interaction--move (- (or count 1))))
;;;###autoload
(defun ebox-help-create (function)
"Adapt zero-argument FUNCTION to a native `help-echo' callback.

View File

@ -18,6 +18,7 @@
(require 'ebox-buffer-backend)
(require 'ebox-runtime-index)
(declare-function ekp-pixel-justify "ekp" (string line-pixel))
(declare-function ebox--render-grid "ebox-grid" (node))
(declare-function ebox--render-grid-box-children
"ebox-grid" (box props children))
@ -85,6 +86,9 @@
(defvar ebox--region-id-counter)
(defvar ebox--runtime-node-id-counter)
(defvar ebox--scroll-global-state)
(defvar ebox--scroll-idle-prefetch-timers)
(defvar ebox--smooth-scroll-state-table)
(defvar ebox--region-box-table)
(defvar ebox--render-region-id)
(defvar ebox--defer-scroll-content-index)
@ -699,6 +703,56 @@ pairs. It is intentionally small; full Unicode segmentation is out of scope."
(when (> width max-width) (setq max-width width))))
(max max-width (ebox--space-pixel-width)))))
(defun ebox--clip-line (line max-pixel)
"Clip LINE to MAX-PIXEL without splitting supported grapheme clusters.
Retain visible text properties. A partially visible fixed display space
keeps its properties and uses the remaining width; a glyph that cannot fit
is omitted as a whole. Any remaining width is neutral box content, never
an interactive stand-in for an omitted glyph."
;; The prefix-plus-pixel-filler algorithm follows kinney's GPL-3.0-or-later
;; `s-pixel-floor' and `s-pixel-left' (2026), extended here for graphemes,
;; source properties and clipped display spaces without an extra dependency.
;; https://github.com/Kinneyzhang/s-pixel
;; commit a2a0d6ae6b3bd71c1a70084d0ea37f56463e5fe0.
(if (<= (ebox--string-pixel-width line) max-pixel)
line
(let ((position 0)
(end (length line))
(width 0)
clipped)
(while (and (< position end) (not clipped))
(let* ((display (get-text-property position 'display line))
(space-width (ebox--display-space-width display))
(next (if (and display (not space-width))
;; A replacement string or image is one display unit.
(max (ebox--grapheme-cluster-end line position)
(or (next-single-property-change
position 'display line end) end))
(ebox--grapheme-cluster-end line position)))
;; Measure the complete prefix, preserving the existing font
;; measurement contract when adjacent glyph shaping matters.
(next-width (ebox--substring-pixel-width line 0 next))
(remaining (- max-pixel width)))
(if (<= next-width max-pixel)
(setq position next width next-width)
(setq clipped
(concat
(substring line 0 position)
(when (> remaining 0)
(let ((space (ebox-pixel-space remaining)))
(when space-width
(add-text-properties
0 (length space)
(text-properties-at position line) space)
(put-text-property
0 (length space) 'display
(cons 'space
(plist-put (copy-tree (cdr display))
:width (list remaining)))
space))
space)))))))
(or clipped (substring line 0 position)))))
(defun ebox--wrap-line (line max-pixel wrap-mode)
"Wrap a single LINE to MAX-PIXEL width, preserving text properties.
WRAP-MODE can be `word', `char', or `kp'.
@ -803,10 +857,11 @@ When WRAP-MODE is `kp', use Knuth-Plass algorithm via ekp package."
((memq wrap-mode '(nil none))
text)
((eq wrap-mode 'kp)
(if (and (require 'ekp nil t)
(fboundp 'ekp-pixel-justify))
(ekp-pixel-justify text max-pixel)
(ebox--wrap-text text max-pixel 'word)))
(unless (require 'ekp nil t)
(error "Ebox :wrap-mode kp requires EKP; install the ekp package"))
(unless (fboundp 'ekp-pixel-justify)
(error "Ebox :wrap-mode kp requires EKP with ekp-pixel-justify; update EKP"))
(ekp-pixel-justify text max-pixel))
(t
(let* ((lines (ebox-string-lines text))
(wrapped (mapcan (lambda (l)
@ -1630,16 +1685,15 @@ one carrier line preserves their paint without inventing a content line."
(defun ebox--format-content-string (box content)
"Format BOX CONTENT (wrap and justify) returning a string."
(let ((wrap-mode (ebox-get box :wrap-mode))
(let* ((wrap-mode (ebox-get box :wrap-mode))
(align (ebox-get box :text-align))
(known-width
(gethash content ebox--rendered-uniform-width-table)))
(known-width (gethash content ebox--rendered-uniform-width-table))
(formatted
(if (and (eq align 'left)
(or (and (ebox-style-no-soft-wrap-p wrap-mode)
(or (plist-get box :ebox-content-width-exact-p)
(and (numberp known-width)
(= known-width
(ebox--content-pixel box)))))
(= known-width (ebox--content-pixel box)))))
(and (memq wrap-mode '(word char))
(numberp known-width)
(= known-width (ebox--content-pixel box)))))
@ -1649,13 +1703,21 @@ one carrier line preserves their paint without inventing a content line."
(if (plist-get box :ebox-content-layout-complete-p)
content
(ebox--wrap-text content pixel-width wrap-mode))))
;; kp mode distributes inner gaps, but Ebox still owns the box geometry.
;; Pad ragged lines (notably the last line) back to the content width
;; while preserving the requested alignment for those lines.
;; KP distributes inner gaps; Ebox still owns the Box geometry.
;; Pad ragged lines back to the requested content width.
(ebox--record-rendered-intrinsic-size
(ebox--lines-justify wrapped-content pixel-width align)
(ebox--rendered-intrinsic-size content :width)
(ebox--rendered-intrinsic-size content :height))))))
(if (eq (ebox-get box :overflow) 'hidden)
(let ((pixel-width (ebox--content-pixel box)))
(ebox--record-rendered-intrinsic-size
(ebox-lines-join
(mapcar (lambda (line) (ebox--clip-line line pixel-width))
(ebox-string-lines formatted)))
(and (ebox--rendered-intrinsic-size content :width) pixel-width)
(ebox--rendered-intrinsic-size content :height)))
formatted)))
(defun ebox--format-content (box)
"Format BOX content (wrap and justify) returning a string."

View File

@ -14,6 +14,7 @@
(declare-function ebox--register-render-owned-text-value
"ebox-render-context" (property value))
(declare-function ebox-string-lines "ebox" (string))
(declare-function ebox-lines-join "ebox" (lines))
(declare-function ebox--string-repeat-lines "ebox" (string count))
;; Measurement Cache Model:
@ -75,11 +76,7 @@ of pixel measurements it triggers do not each rebuild the display signature.")
(defun ebox--display-space-width (display)
"Return fixed pixel width for DISPLAY space specs, or nil."
(when (and (consp display) (eq (car display) 'space))
(let ((width (plist-get (cdr display) :width)))
(cond
((numberp width) width)
((and (consp width) (numberp (car width))) (car width))))))
(ebox--fixed-pixel-space-width display))
(defun ebox--fixed-pixel-space-width (display)
"Return DISPLAY's nonnegative fixed pixel width, or nil for other specs.
@ -129,17 +126,17 @@ An already integral STRING is returned unchanged."
(width (ebox--fixed-pixel-space-width
(get-text-property position 'display string))))
(when width
(cl-loop for index from position below next
do (push index positions) (push width widths)))
(push (cons position next) positions)
(push width widths))
(setq position next)))
(cl-mapc
(lambda (index used)
(let ((display (get-text-property index 'display string)))
(lambda (range used)
(let ((display (get-text-property (car range) 'display string)))
(unless (= used (ebox--fixed-pixel-space-width display))
(let ((updated (cons 'space (copy-sequence (cdr display)))))
(setcdr updated (plist-put (cdr updated) :width (list used)))
(put-text-property
index (1+ index) 'display
(car range) (cdr range) 'display
(ebox--register-render-owned-text-value 'display updated)
result)))))
(nreverse positions) (ebox-size-quantize (nreverse widths)))
@ -236,7 +233,7 @@ constant across the selected slice."
;; display-space fractions just as the segmented path does;
;; Emacs's glyph measurement quantizes them before padding.
(width (if space-width
(* space-width (length sample))
space-width
(ebox--scaled-string-pixel-width sample factor)))
(index ebox--string-pixel-width-cache-ring-index)
(evicted (aref ebox--string-pixel-width-cache-ring index)))
@ -268,17 +265,21 @@ keep their absolute pixel width."
(ebox--cached-string-pixel-width line factor cache-key)
(while (< pos end)
(let* ((display (get-text-property pos 'display line))
(space-width (ebox--display-space-width display))
(next
(cl-loop for property in
'(face display)
(if (or (stringp display)
(memq (car-safe display) '(space image)))
;; Emacs replaces an entire EQ display run once. Face
;; boundaries inside it do not create additional glyphs.
(next-single-property-change pos 'display line end)
(cl-loop for property in '(face display)
minimize (or (next-single-property-change
pos property line end)
end)))
(space-width (ebox--display-space-width display)))
end)))))
(setq total
(+ total
(if space-width
(* space-width (- next pos))
space-width
;; The segment ends at every visual property change.
;; Build only its property-free cache key on hits; a
;; propertized substring is materialized solely on miss.

View File

@ -18,6 +18,9 @@
(require 'ebox-runtime-index)
(require 'ebox-size)
(declare-function ebox-string-height "ebox" (string))
(declare-function ebox-surface--retained-property-value-equal-p
"ebox-surface" (left right))
(declare-function ebox-native--module-version "ebox_native_reflow" ())
(declare-function ebox-native--module-layout-ready-p "ebox_native_reflow" ())
(declare-function ebox-native--module-create-session "ebox_native_reflow"
@ -49,6 +52,7 @@
(declare-function ebox-native--module-stats "ebox_native_reflow" (session))
(declare-function ebox-native--module-release "ebox_native_reflow" (session))
(declare-function ebox--grapheme-clusters "ebox-layout" (string))
(declare-function ebox--grapheme-cluster-end "ebox-layout" (string start))
(declare-function ebox--format-content-string "ebox-layout" (box content))
(declare-function ebox-style-no-soft-wrap-p "ebox-style" (value))
(declare-function ebox-style-soft-wrap-p "ebox-style" (value))
@ -101,7 +105,7 @@
(defvar ebox-native-reflow--compile-size-node nil
"Owning node whose font measurements resolve fixed native size inputs.")
(defconst ebox-native-reflow-abi-version "13:7:12"
(defconst ebox-native-reflow-abi-version "14:7:12"
"Version tuple shared by the native module, layout IR, and render tape.")
(defconst ebox-native-reflow--minimum-rust-version "1.82.0"
@ -1681,6 +1685,18 @@ copied separately; no pending frame or registered Rust document is inherited."
(clrhash ebox-native-reflow--source-cluster-cache))
(puthash cache-key measured
ebox-native-reflow--source-cluster-cache))
;; One native atom carries one opaque property template. Splitting a
;; cluster at a property-run boundary would turn combining marks or
;; ZWJ tails into independent atoms that clipping/wrapping could lose.
;; Check the last measured cluster against the complete source, also
;; on cache hits; unsupported mixed-property clusters use Elisp layout.
(when (and (< next length) (> (length measured) 0)
(> (ebox--grapheme-cluster-end
line (- next (length (plist-get
(aref measured (1- (length measured)))
:text))))
next))
(error "Native reflow cannot split a grapheme across text property runs"))
(dolist (cluster (append measured nil))
(push (append (copy-sequence cluster)
(when template-id

View File

@ -18,6 +18,7 @@
(declare-function ebox--ensure-region-id "ebox" (box))
(declare-function ebox--buffer-root-node "ebox-incremental" (buffer))
(declare-function ebox--buffer-render-state "ebox-incremental" (buffer))
(declare-function ebox--buffer-source-index "ebox-incremental" (buffer))
(declare-function ebox--buffer-selector-id-table "ebox-incremental" (buffer))
(declare-function ebox--buffer-selector-class-table
"ebox-incremental" (buffer))

View File

@ -16,7 +16,7 @@
(require 'ebox-surface)
(require 'tp-transaction)
(defconst ebox-framework-spi-required-tp-version "1.0.1"
(defconst ebox-framework-spi-required-tp-version "2.0.0"
"Minimum TP package version required by Ebox framework SPI v2.")
(defconst ebox-framework-spi-supported-tp-protocols

View File

@ -24,6 +24,8 @@
(require 'tp)
(defvar ebox-region-types)
(defvar ebox--region-id-counter)
(defvar ebox--runtime-node-id-counter)
(defvar ebox--region-box-table)
(defvar ebox--scroll-global-state)
(defvar ebox--paint-origin-property)
@ -49,16 +51,24 @@
(defvar ebox-incremental--allocated-slot-proof-cache)
(defvar ebox-incremental--buffer-render-state-override)
(declare-function tp-runtime-manifest "tp" ())
(declare-function ebox--region-ids-visible-in-buffer-p "ebox" (buffer region-ids))
(declare-function ebox--scroll-line-region-ids "ebox" (line))
(declare-function ebox--scroll-build-region-line-span-index "ebox" (lines))
(declare-function tp-transaction-participate-v2 "tp-reactive" (&rest args))
(define-error 'ebox-surface-tp-protocol-error
"Incompatible TP transaction protocol")
"Ebox requires TP 2.0.0 or newer with structured transactions")
(defun ebox-surface--validate-tp-v2-capability (manifest)
"Validate MANIFEST as the required public TP v2 participant capability."
(let ((api (plist-get manifest :structured-participant-api))
(let ((version (plist-get manifest :version))
(api (plist-get manifest :structured-participant-api))
(protocol (plist-get manifest :transaction-protocol)))
(unless (and (memq protocol
(unless (and (stringp version)
(condition-case nil
(not (version< version "2.0.0"))
(error nil))
(memq protocol
'(tp-transaction-protocol-v1+v2
tp-transaction-protocol-v2))
(eq api 'tp-transaction-participate-v2)

26
ebox.el
View File

@ -1,10 +1,16 @@
;;; ebox.el --- Pixel-perfect box model renderer -*- lexical-binding: t -*-
;; SPDX-License-Identifier: GPL-3.0-or-later
;; Author: ebox contributors
;; Version: 3.0.0
;; Package-Requires: ((emacs "29.1") (ecss "0.1.0") (tp "1.0.1"))
;; Package-Requires: ((emacs "29.1") (ecss "0.1.0") (tp "2.0.0"))
;; Keywords: ui, graphics, convenience
;; URL: https://github.com/ginqi7/ebox
;; URL: https://gitea.gklazycat.heiyu.space/geekinney/ebox
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version. See LICENSE for the full text.
;;; Commentary:
@ -4908,11 +4914,17 @@ 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 input)
"Render canonical INPUT to BUFFER-OR-NAME through TP and display it."
(defun ebox-display-buffer (buffer-or-name input &optional action)
"Render canonical INPUT to BUFFER-OR-NAME, display it, and return its buffer.
Render before requesting a window, so invalid input does not disturb the
window layout. ACTION is passed to `display-buffer'; ordinary display rules
apply and this function does not select the displayed window."
(declare (indent 1))
(delete-other-windows)
(switch-to-buffer (ebox-render-to-buffer buffer-or-name input)))
(let ((buffer (ebox-render-to-buffer buffer-or-name input)))
(let ((window (display-buffer buffer action)))
(when (window-live-p window)
(ebox--window-size-change (window-frame window))))
buffer))
(defconst ebox-public-api
'(ebox-buffer-mode
@ -4943,6 +4955,8 @@ through dirty-set and patch-set execution before falling back to root rerender."
ebox-host-ref-position
ebox-help-create
ebox-keymap-create
ebox-next-interaction
ebox-previous-interaction
ebox-native-build
ebox-native-status
ebox-region-ids

View File

@ -3,6 +3,7 @@ name = "ebox-native-reflow"
version = "0.1.0"
edition = "2021"
rust-version = "1.82"
license = "GPL-3.0-or-later"
publish = false
[lib]

View File

@ -164,7 +164,7 @@ ebox_module_version(emacs_env *env, ptrdiff_t nargs, emacs_value *args,
(void) nargs;
(void) args;
(void) data;
static const char version[] = "13:7:12";
static const char version[] = "14:7:12";
return env->make_string(env, version, (ptrdiff_t) (sizeof version - 1));
}

View File

@ -3,6 +3,7 @@ name = "etaf-core"
version = "0.1.0"
edition = "2021"
rust-version = "1.82"
license = "GPL-3.0-or-later"
publish = false
[lib]

View File

@ -2079,6 +2079,35 @@ impl Line {
self
}
fn clipped(&self, width: i64) -> Self {
if self.width <= width {
return line_plan::clone_line(self);
}
let mut kept = Vec::new();
let mut remaining = width.max(0);
for atom in self.atoms.to_vec() {
if atom.width() <= remaining {
remaining -= atom.width();
kept.push(atom);
} else {
if remaining > 0 {
// A display space can shrink. An indivisible glyph is
// omitted and its replacement blank has no hit ownership.
let properties = match atom {
Atom::Space { properties, .. } => properties,
Atom::Text { .. } => AtomProperties::default(),
};
kept.push(Atom::Space {
width: remaining,
properties,
});
}
break;
}
}
Self::from_atoms(&kept)
}
fn blank(width: i64) -> Self {
Self::blank_with_properties(width, AtomProperties::default())
}
@ -7648,6 +7677,13 @@ fn render_node_body(
}
}
}
if *overflow == Overflow::Hidden && formatted.max_width() > content_width {
// Horizontal clipping changes atom positions but not line
// origins. Preserve break properties and let the ordinary
// owner projection rebuild the affected local metadata.
formatted = formatted.map_lines(|_, line| line.clipped(content_width));
change = None;
}
formatted = project_box_lines(
scope,
BoxProjectionSlot::ContentOwn,
@ -8739,6 +8775,118 @@ mod tests {
assert!(character_lines.iter().all(|line| line.width <= 3));
}
#[test]
fn hidden_inline_overflow_clips_before_padding_and_borders() {
for overflow_value in [Overflow::Hidden, Overflow::Visible, Overflow::Scroll] {
let mut root = text_box(
1,
measured_text(vec!["ABCDEFGHIJ"
.chars()
.map(|ch| cluster(&ch.to_string(), 1, Some(0)))
.collect()]),
None,
);
let LayoutNode::Box {
width,
overflow,
padding_left,
padding_right,
border_left,
border_right,
..
} = &mut root
else {
unreachable!()
};
*width = Size::Pixels { value: 7 };
*overflow = overflow_value;
*padding_left = 1;
*padding_right = 1;
*border_left = 1;
*border_right = 1;
let document = LayoutDocument {
version: LAYOUT_VERSION,
space_width: 1,
style_count: 0,
property_template_count: 1,
styles: Vec::new(),
root,
};
let tape = document.layout_tape(test_context(), None).unwrap();
assert_eq!(
tape.lines[0].width,
if overflow_value == Overflow::Hidden {
7
} else {
14
}
);
let flat = flatten_layout_tape(tape, true).unwrap();
let text = flat
.characters
.iter()
.map(|ch| ch.value)
.collect::<String>();
assert!(text.contains(if overflow_value == Overflow::Hidden {
"ABC"
} else {
"ABCDEFGHIJ"
}));
if overflow_value == Overflow::Hidden {
assert!(!text.contains('D'));
}
assert!(flat
.characters
.first()
.unwrap()
.properties
.roles
.iter()
.any(|role| role.role == RegionRole::BorderLeft));
assert!(flat
.characters
.last()
.unwrap()
.properties
.roles
.iter()
.any(|role| role.role == RegionRole::BorderRight));
}
}
#[test]
fn hidden_inline_overflow_keeps_clusters_and_partial_spaces() {
let source =
Line::from_clusters(&[cluster("e\u{301}", 2, Some(0)), cluster("👨‍👩‍👧‍👦", 4, Some(1))]);
let clipped = source.clipped(3);
assert_eq!(clipped.width, 3);
let atoms = clipped.atoms.to_vec();
assert_eq!(atoms.len(), 2);
assert!(matches!(&atoms[0], Atom::Text { text, .. } if text == "e\u{301}"));
assert_eq!(atoms[0].properties().property_template_ids, vec![0]);
assert!(matches!(&atoms[1], Atom::Space { width: 1, .. }));
assert!(atoms[1].properties().property_template_ids.is_empty());
let space = Line::blank_with_properties(
20,
AtomProperties {
property_template_ids: vec![0],
..AtomProperties::default()
},
)
.clipped(3);
assert_eq!(space.width, 3);
assert_eq!(
space
.atoms
.first()
.unwrap()
.properties()
.property_template_ids,
vec![0]
);
assert_eq!(source.clipped(0).width, 0);
}
#[test]
fn source_template_ids_follow_measured_clusters() {
let root = text_box(

19
release-dependencies.json Normal file
View File

@ -0,0 +1,19 @@
{
"schema": 1,
"dependencies": {
"tp": {
"url": "https://gitea.gklazycat.heiyu.space/geekinney/tp.git",
"revision": "0a820bd0cb9cf227f08c3776f9cbe1d4c302d196"
},
"ecss": {
"url": "https://gitea.gklazycat.heiyu.space/geekinney/ecss.git",
"revision": "13559a41f095b4f74acab67e589ad8bd8c21a248"
}
},
"optional": {
"ekp": {
"url": "https://github.com/Kinneyzhang/emacs-kp.git",
"revision": "578540549511e352696c430338afb0c98522cd2f"
}
}
}

View File

@ -0,0 +1,145 @@
;;; ebox-release-smoke.el --- Isolated package installation smoke -*- lexical-binding: t; -*-
;;; Commentary:
;; Invoked by scripts/ebox-release.py verify in a disposable user directory.
;; No source checkout or sibling package may enter this process's load-path.
;;; Code:
(require 'cl-lib)
(require 'json)
(require 'package)
(require 'bytecomp)
(let* ((archive (getenv "EBOX_RELEASE_ARCHIVE"))
(profile (getenv "EBOX_RELEASE_PROFILE"))
(native (getenv "EBOX_RELEASE_NATIVE"))
(build-native (equal (getenv "EBOX_RELEASE_BUILD_NATIVE") "1"))
(manifest (with-temp-buffer
(insert-file-contents (expand-file-name "manifest.json" archive))
(json-parse-buffer :object-type 'alist :array-type 'list)))
(packages (alist-get 'packages manifest)))
(unless (and archive profile)
(error "Run through the release verification entry point"))
(setq user-emacs-directory (file-name-as-directory profile)
package-user-dir (expand-file-name "elpa" profile)
package-archives (list (cons "ebox-release" (file-name-as-directory archive)))
package-check-signature nil
package-directory-list nil
native-comp-jit-compilation nil)
(package-initialize)
(package-refresh-contents)
(let* (compile-issues
(capture-warning
(lambda (message &rest _)
(when (and byte-compile-current-file
(string-match-p "\\`ebox.*\\.el\\'"
(file-name-nondirectory byte-compile-current-file))
;; package.el owns generated descriptor/autoload compilation.
(not (string-match-p "-\\(?:pkg\\|autoloads\\)\\.el\\'"
byte-compile-current-file)))
(push (format "%s: %s" (file-name-nondirectory byte-compile-current-file)
message) compile-issues)))))
(unwind-protect
(progn
(advice-add 'byte-compile-log-warning :before capture-warning)
(package-install 'ebox)
(when (seq-some (lambda (entry) (equal (alist-get 'name entry) "ekp")) packages)
(package-install 'ekp)))
(advice-remove 'byte-compile-log-warning capture-warning))
(when compile-issues
(error "Installed Ebox compilation diagnostics:\n%s"
(mapconcat #'identity (nreverse compile-issues) "\n"))))
(dolist (command '(ebox-build ebox-render ebox-next-interaction ebox-previous-interaction))
(unless (fboundp command)
(error "Installed autoload registry omitted %s" command)))
(require 'ebox)
(dolist (feature '(tp ecss ebox))
(let ((library (locate-library (symbol-name feature))))
(unless (and (featurep feature) library
(file-in-directory-p library package-user-dir))
(error "Package escaped the clean installation: %s" feature))))
(when (featurep 'etaf)
(error "Standalone installation unexpectedly loaded ETAF"))
(when (featurep 'ebox-native-reflow)
(error "Source installation eagerly loaded native subsystem"))
(let* ((input (ebox-build '(box :width (ch 10) :height (lh 1) :overflow scroll
"first\nsecond\nthird")))
(region-counter ebox--region-id-counter)
(node-counter ebox--runtime-node-id-counter)
(scroll-count (hash-table-count ebox--scroll-global-state)))
(dotimes (_ 3) (ebox-render input))
(unless (and (= region-counter ebox--region-id-counter)
(= node-counter ebox--runtime-node-id-counter)
(= scroll-count (hash-table-count ebox--scroll-global-state)))
(error "Installed ephemeral rendering leaked live identity or scroll state")))
(when build-native
(let ((process (ebox-native-build))
(deadline (+ (float-time) 480)))
(unwind-protect
(progn
(while (and (process-live-p process) (< (float-time) deadline))
(accept-process-output process 0.1))
(when (process-live-p process)
(error "Installed native build timed out"))
(unless (and (zerop (process-exit-status process))
(ebox-native-reflow-layout-ready-p))
(error "Installed native build failed: %s"
(with-current-buffer (process-buffer process) (buffer-string))))
(setq native (plist-get (ebox-native-reflow-runtime-report) :loaded-module-path)))
(when (process-live-p process) (delete-process process)))))
(when (and native (not (string-empty-p native)))
(require 'ebox-native-reflow)
(setq ebox-native-reflow-module-path native)
(unless (ebox-native-reflow-layout-ready-p)
(error "Installed Ebox could not load native module: %S"
(ebox-native-reflow-runtime-report))))
(let ((buffer (generate-new-buffer " *ebox-release-smoke*")) stale-handle)
(unwind-protect
(progn
(ebox-render-to-buffer
buffer (ebox-build '(column
(box :id "counter" :class "value" "before")
(box "unchanged"))))
(with-current-buffer buffer
(setq stale-handle (ebox-region-resolve buffer "counter"))
(dotimes (index 30)
(ebox-region-update stale-handle :content (format "after %02d" index)))
(unless (and (string-match-p "after" (buffer-string))
(string-match-p "unchanged" (buffer-string))
(not (string-match-p "before" (buffer-string)))
(= 1 (length (ebox-selector-query-buffer buffer ".value"))))
(error "Installed render/update/selector smoke failed"))))
(kill-buffer buffer))
(condition-case nil
(progn (ebox-region-update stale-handle :content "invalid")
(error "Installed stale handle accepted an update after unmount"))
(user-error nil)))
(if (assoc "ekp" (mapcar (lambda (entry) (cons (alist-get 'name entry) entry)) packages))
(let ((calls 0))
(require 'ekp)
(let ((probe (lambda (&rest _) (cl-incf calls))))
(unwind-protect
(progn
(advice-add 'ekp-pixel-justify :before probe)
(ebox-render (ebox-build '(box :width (ch 12) :wrap-mode kp
"one two three four five six seven")))
(unless (> calls 0)
(error "KP installation did not invoke the real EKP provider")))
(advice-remove 'ekp-pixel-justify probe))))
(when (locate-library "ekp")
(error "Default installation unexpectedly found optional EKP")))
(when (and native (not (string-empty-p native)))
(let* ((input (ebox-build '(box :width (ch 8) "native")))
(node (ebox-canonical-input--single-root input "Release smoke"))
(result (ebox-native-reflow-execute-sync
node '(:key 1 :viewport-width 80 :viewport-height 20
:runtime-revision 0 :context-hash 0 :complete t))))
(unless (and (plist-get result :native-frame)
(string-match-p "native" (plist-get result :rendered)))
(error "Installed native module did not produce a native render frame"))))
(princ (format "Clean installation OK: Emacs %s; render, update, selector; EKP %s; native %s\n"
emacs-version (if (featurep 'ekp) "verified" "absent")
(if (and native (not (string-empty-p native))) "verified" "not requested"))))
;;; ebox-release-smoke.el ends here

239
scripts/ebox-release.py Normal file
View File

@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""Build deterministic package.el archives and verify clean-room installation.
Use `make release-check RELEASE_DIR=/tmp/ebox-release EMACS=/path/to/emacs`.
Only the explicit output directory is retained; subprocesses have timeouts and
temporary checkouts/installations are removed on success, error, or interruption.
No command publishes, changes a source checkout, or installs into the user profile.
"""
import argparse
import hashlib
import io
import json
import os
from pathlib import Path
import re
import signal
import subprocess
import sys
import tarfile
import tempfile
ROOT = Path(__file__).resolve().parents[1]
def run(args, cwd=None, env=None, timeout=180):
"""Run a bounded subprocess; include its output on failure."""
process = subprocess.Popen(args, cwd=cwd, env=env, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, text=True,
start_new_session=(os.name != "nt"))
try:
stdout, stderr = process.communicate(timeout=timeout)
except (subprocess.TimeoutExpired, KeyboardInterrupt):
if os.name == "nt":
subprocess.run(["taskkill", "/PID", str(process.pid), "/T", "/F"],
capture_output=True, timeout=20, check=False)
else:
try:
os.killpg(process.pid, signal.SIGKILL)
except ProcessLookupError:
pass
process.communicate()
raise
if process.returncode:
raise RuntimeError(f"Command failed: {args[0]}\n{stdout}{stderr}")
return stdout
def lock_entries(lock, with_ekp=False):
"""Validate and select required dependencies and the optional KP provider."""
if lock.get("schema") != 1 or set(lock.get("dependencies", {})) != {"tp", "ecss"}:
raise ValueError("Expected schema 1 with pinned tp and ecss dependencies")
entries = dict(lock["dependencies"])
if with_ekp:
entries["ekp"] = lock["optional"]["ekp"]
for name, entry in entries.items():
if not re.fullmatch(r"[0-9a-f]{40}", entry["revision"]):
raise ValueError(f"{name}: revision must be a full commit hash")
if not entry["url"].startswith("https://"):
raise ValueError(f"{name}: public dependency URL must use HTTPS")
return entries
def checkout_dependencies(directory, entries):
"""Fetch exact commits into a new directory; never replace a checkout."""
if directory.exists():
raise ValueError(f"Refusing to replace existing dependency directory: {directory}")
directory.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix=".ebox-deps-", dir=directory.parent) as work:
stage = Path(work) / "deps"
stage.mkdir()
env = dict(os.environ, GIT_TERMINAL_PROMPT="0")
for name, entry in entries.items():
target = stage / name
run(["git", "init", "-q", str(target)])
run(["git", "fetch", "--quiet", "--depth", "1", entry["url"],
entry["revision"]], cwd=target, env=env)
run(["git", "checkout", "--quiet", "--detach", "FETCH_HEAD"], cwd=target)
if run(["git", "rev-parse", "HEAD"], cwd=target).strip() != entry["revision"]:
raise ValueError(f"{name}: fetched revision mismatch")
stage.rename(directory)
def validate_dependencies(directory, entries):
"""Reject stale or modified explicit dependency checkouts."""
for name, entry in entries.items():
target = directory / name
revision = run(["git", "rev-parse", "HEAD"], cwd=target).strip()
dirty = run(["git", "status", "--porcelain", "--untracked-files=all"], cwd=target)
if revision != entry["revision"] or dirty:
raise ValueError(f"{name}: dependency must be clean at {entry['revision']}")
def package_metadata(root, name):
"""Read package.el metadata from the package's main Lisp header."""
source = (root / f"{name}.el").read_text()
version = re.search(r"^;; Version: ([0-9]+(?:\.[0-9]+)*)$", source, re.M)
requirements = re.search(r"^;; Package-Requires: (.+)$", source, re.M)
if not version or not requirements:
raise ValueError(f"{name}: missing supported version or requirements header")
return version[1], requirements[1]
def package_files(root, name):
"""Select source inputs, documentation and licenses, excluding build output."""
result = {}
candidates = run(["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z"], cwd=root)
for filename in sorted(set(candidates.split("\0")) - {""}):
path = root / filename
relative = path.relative_to(root)
parts = relative.parts
if (not path.is_file() or path.is_symlink()
or any(part.startswith(".") or part in ("target", "__pycache__")
for part in parts)
or path.suffix in (".elc", ".eln", ".so", ".dylib", ".dll", ".o", ".a")):
continue
top_file = len(parts) == 1
include = (top_file and (path.suffix == ".el" or
path.name.startswith(("README", "LICENSE", "COPYING", "NOTICE", "CHANGELOG"))))
include = include or (name == "ebox" and parts[0] in ("native", "docs"))
include = include or (name == "ekp" and parts[0] in ("dictionaries", "ekp_c"))
if include and not path.name.endswith("-pkg.el"):
result[relative.as_posix()] = path.read_bytes()
if f"{name}.el" not in result:
raise ValueError(f"{name}: package entry missing")
return result
def write_package(output, root, name):
"""Write a deterministic uncompressed package.el tar archive."""
version, requirements = package_metadata(root, name)
files = package_files(root, name)
files[f"{name}-pkg.el"] = (
f';;; {name}-pkg.el --- Package metadata -*- lexical-binding: t; -*-\n'
f'(define-package "{name}" "{version}" "{name} source package" '
f"'{requirements})\n").encode()
archive = output / f"{name}-{version}.tar"
with tarfile.open(archive, "w", format=tarfile.USTAR_FORMAT) as stream:
for relative, content in sorted(files.items()):
info = tarfile.TarInfo(f"{name}-{version}/{relative}")
info.size, info.mode, info.mtime = len(content), 0o644, 0
info.uid = info.gid = 0
stream.addfile(info, io.BytesIO(content))
return {"name": name, "version": version, "requirements": requirements,
"file": archive.name, "sha256": hashlib.sha256(archive.read_bytes()).hexdigest()}
def build(output, root, dependencies, entries):
"""Atomically publish a local archive directory after all packages build."""
if output.exists():
raise ValueError(f"Refusing to replace existing release directory: {output}")
validate_dependencies(dependencies, entries)
output.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix=".ebox-release-", dir=output.parent) as work:
stage = Path(work) / "archive"
stage.mkdir()
packages = [write_package(stage, dependencies / name, name) for name in entries]
packages.append(write_package(stage, root, "ebox"))
manifest = {"schema": 1, "dependencies": entries, "packages": packages,
"source_revision": run(["git", "rev-parse", "HEAD"], cwd=root).strip(),
"source_dirty": bool(run(["git", "status", "--porcelain"], cwd=root))}
(stage / "manifest.json").write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n")
records = []
for package in packages:
version_list = "(" + package["version"].replace(".", " ") + ")"
requirements = re.sub(r'"([0-9]+(?:\.[0-9]+)*)"',
lambda match: "(" + match[1].replace(".", " ") + ")",
package["requirements"])
records.append(f' ({package["name"]} . [{version_list} '
f'{requirements} "{package["name"]} source package" tar])')
(stage / "archive-contents").write_text("(1\n" + "\n".join(records) + "\n)\n")
stage.rename(output)
return manifest
def verify(directory, emacs, native_module=None, build_native=False):
"""Install source archives in a disposable package-user-dir and exercise Ebox."""
manifest = json.loads((directory / "manifest.json").read_text())
if native_module and not native_module.is_file():
raise ValueError(f"Native module does not exist: {native_module}")
for package in manifest["packages"]:
file = directory / package["file"]
if file.parent != directory or not file.is_file():
raise ValueError("Invalid archive filename")
if hashlib.sha256(file.read_bytes()).hexdigest() != package["sha256"]:
raise ValueError(f"Archive checksum mismatch: {file.name}")
with tempfile.TemporaryDirectory(prefix="ebox-install-") as work:
env = dict(os.environ, EBOX_RELEASE_ARCHIVE=str(directory),
EBOX_RELEASE_PROFILE=work, EBOX_RELEASE_NATIVE=str(native_module or ""),
EBOX_RELEASE_BUILD_NATIVE="1" if build_native else "")
# No repository or sibling load paths, HOME changes, network archive refresh,
# or user package installation. package.el compiles only this temp profile.
result = run([emacs, "-Q", "--batch", "-l", str(ROOT / "scripts/ebox-release-smoke.el")],
cwd=work, env=env, timeout=600)
print(result.strip())
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("command", choices=("deps", "build", "verify"))
parser.add_argument("--directory", type=Path, required=True,
help="New deps/build output directory, or archive to verify")
parser.add_argument("--dependencies", type=Path, help="Explicit pinned dependency checkout directory")
parser.add_argument("--with-ekp", action="store_true", help="Include optional Knuth-Plass provider")
parser.add_argument("--emacs", default="emacs", help="Emacs executable for verify")
parser.add_argument("--native-module", type=Path, help="Require loading and executing this native module")
parser.add_argument("--build-native", action="store_true",
help="Build, install and execute bundled native source in the temporary profile")
args = parser.parse_args()
directory = args.directory.resolve()
entries = lock_entries(json.loads((ROOT / "release-dependencies.json").read_text()), args.with_ekp)
if args.command == "deps":
checkout_dependencies(directory, entries)
print(f"Pinned dependencies ready: {directory}")
elif args.command == "build":
if args.dependencies:
manifest = build(directory, ROOT, args.dependencies.resolve(), entries)
else:
with tempfile.TemporaryDirectory(prefix="ebox-release-deps-") as work:
dependencies = Path(work) / "deps"
checkout_dependencies(dependencies, entries)
manifest = build(directory, ROOT, dependencies, entries)
print(json.dumps({"archive": str(directory), "packages": manifest["packages"]}, indent=2))
else:
if args.native_module and args.build_native:
parser.error("Choose --native-module or --build-native, not both")
verify(directory, args.emacs, args.native_module.resolve() if args.native_module else None,
args.build_native)
if __name__ == "__main__":
try:
main()
except (ValueError, RuntimeError, OSError, subprocess.TimeoutExpired) as error:
print(f"release verification failed: {error}", file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
print("release operation interrupted; temporary resources removed", file=sys.stderr)
sys.exit(130)

View File

@ -0,0 +1,100 @@
"""Regression tests for deterministic packaging and dependency boundaries."""
import importlib.util
import json
from pathlib import Path
import subprocess
import tarfile
import tempfile
import unittest
from unittest import mock
SPEC = importlib.util.spec_from_file_location("ebox_release", Path(__file__).with_name("ebox-release.py"))
RELEASE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(RELEASE)
class ReleaseTests(unittest.TestCase):
def test_lock_requires_exact_revisions_and_optional_provider_is_explicit(self):
lock = json.loads((RELEASE.ROOT / "release-dependencies.json").read_text())
self.assertEqual(set(RELEASE.lock_entries(lock)), {"tp", "ecss"})
self.assertEqual(set(RELEASE.lock_entries(lock, True)), {"tp", "ecss", "ekp"})
lock["dependencies"]["tp"]["revision"] = "main"
with self.assertRaises(ValueError):
RELEASE.lock_entries(lock)
def test_archives_are_reproducible_source_complete_and_exclude_local_outputs(self):
with tempfile.TemporaryDirectory() as work:
root = Path(work) / "source"
root.mkdir()
subprocess.run(["git", "init", "-q", str(root)], check=True)
(root / "ebox.el").write_text(';;; ebox.el --- Test -*- lexical-binding: t -*-\n;; Version: 3.0.0\n;; Package-Requires: ((emacs "29.1"))\n')
(root / "LICENSE").write_text("license")
(root / "native/src").mkdir(parents=True)
(root / "native/src/lib.rs").write_text("native source")
(root / "native/target/release").mkdir(parents=True)
(root / "native/target/release/private.log").write_text("exclude")
(root / "ebox.elc").write_text("stale bytecode")
first, second = Path(work) / "first", Path(work) / "second"
first.mkdir()
second.mkdir()
one = RELEASE.write_package(first, root, "ebox")
(root / "ebox.el").touch()
two = RELEASE.write_package(second, root, "ebox")
self.assertEqual(one["sha256"], two["sha256"])
with tarfile.open(first / one["file"]) as archive:
names = archive.getnames()
self.assertIn("ebox-3.0.0/LICENSE", names)
self.assertIn("ebox-3.0.0/native/src/lib.rs", names)
self.assertIn("ebox-3.0.0/ebox-pkg.el", names)
self.assertFalse(any("target/" in name or name.endswith(".elc") for name in names))
self.assertTrue(all(member.mtime == 0 for member in archive.getmembers()))
def test_existing_output_is_preserved(self):
with tempfile.TemporaryDirectory() as work:
directory = Path(work)
sentinel = directory / "keep"
sentinel.write_text("owned")
with self.assertRaises(ValueError):
RELEASE.checkout_dependencies(directory, {})
with self.assertRaises(ValueError):
RELEASE.build(directory, directory, directory, {})
self.assertEqual(sentinel.read_text(), "owned")
def test_modified_archive_fails_before_emacs_runs(self):
with tempfile.TemporaryDirectory() as work:
directory = Path(work)
(directory / "bad.tar").write_bytes(b"changed")
(directory / "manifest.json").write_text(json.dumps({"packages": [{"file": "bad.tar", "sha256": "wrong"}]}))
with self.assertRaisesRegex(ValueError, "checksum mismatch"):
RELEASE.verify(directory, "must-not-run")
def test_failed_fetch_removes_temporary_checkout_and_never_creates_output(self):
with tempfile.TemporaryDirectory() as work:
parent = Path(work)
output = parent / "deps"
with mock.patch.object(RELEASE, "run", side_effect=RuntimeError("fetch failed")):
with self.assertRaisesRegex(RuntimeError, "fetch failed"):
RELEASE.checkout_dependencies(output, {"tp": {"url": "https://invalid", "revision": "0" * 40}})
self.assertEqual(list(parent.iterdir()), [])
def test_dependency_validation_rejects_modified_checkout(self):
with tempfile.TemporaryDirectory() as work:
root = Path(work)
dependency = root / "tp"
dependency.mkdir()
subprocess.run(["git", "init", "-q", str(dependency)], check=True)
(dependency / "tp.el").write_text("original")
subprocess.run(["git", "add", "tp.el"], cwd=dependency, check=True)
subprocess.run(["git", "-c", "user.name=Release Test", "-c", "user.email=release@example.invalid",
"commit", "-qm", "fixture"], cwd=dependency, check=True)
revision = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=dependency).decode().strip()
entries = {"tp": {"revision": revision}}
RELEASE.validate_dependencies(root, entries)
(dependency / "tp.el").write_text("modified")
with self.assertRaisesRegex(ValueError, "dependency must be clean"):
RELEASE.validate_dependencies(root, entries)
if __name__ == "__main__":
unittest.main()

View File

@ -56,8 +56,15 @@
(ert-deftest ebox-ci-workflow-uses-local-entry-points ()
"GitHub Actions should call the same local checks as maintainers."
(let ((source (ebox-ci-test--read ".github/workflows/ci.yml")))
(should (string-match-p "emacs-nox" source))
(should (string-match-p "Kinneyzhang/tp" source))
(should (string-match-p "setup-emacs@" source))
(should (string-match-p "'29.1'" source))
(should (string-match-p "'30.2'" source))
(should (string-match-p "make release-deps" source))
(should (string-match-p "TP_DIR=" source))
(should (string-match-p "ECSS_DIR=" source))
(should (string-match-p "make release-check" source))
(should (string-match-p "--with-ekp" source))
(should (string-match-p "--native-module" source))
(should (string-match-p "make ci" source))
(should (string-match-p (regexp-quote "cargo +stable test") source))
(should (string-match-p (regexp-quote "cargo +1.82.0 test") source))))

View File

@ -1744,29 +1744,39 @@
(should (= (ebox-commit-test--participant-v2-count) 2)))
(ert-deftest ebox-transaction-participant-accepts-structured-protocols ()
"Both consumer-first and final TP manifests satisfy Ebox's v2 contract."
"Supported structured protocols also require the current TP semantics."
(dolist (protocol '(tp-transaction-protocol-v1+v2
tp-transaction-protocol-v2))
(should
(ebox-surface--validate-tp-v2-capability
(list :transaction-protocol protocol
(list :version "2.0.0" :transaction-protocol protocol
:structured-participant-api
'tp-transaction-participate-v2)))))
(ert-deftest ebox-transaction-participant-rejects-old-provider-semantics ()
"An API symbol alone does not prove native property ownership support."
(dolist (version '(nil "1.0.1" "2.0.0-pre" "invalid" 2))
(should-error
(ebox-surface--validate-tp-v2-capability
(list :version version
:transaction-protocol 'tp-transaction-protocol-v2
:structured-participant-api 'tp-transaction-participate-v2))
:type 'ebox-surface-tp-protocol-error)))
(ert-deftest ebox-transaction-participant-rejects-missing-or-malformed-v2 ()
"Missing or incompatible structured capabilities fail closed."
(should-error
(ebox-surface--validate-tp-v2-capability
'(:transaction-protocol tp-transaction-protocol-v1+v2))
'(:version "2.0.0" :transaction-protocol tp-transaction-protocol-v1+v2))
:type 'ebox-surface-tp-protocol-error)
(should-error
(ebox-surface--validate-tp-v2-capability
'(:transaction-protocol tp-transaction-protocol-v1+v2
'(:version "2.0.0" :transaction-protocol tp-transaction-protocol-v1+v2
:structured-participant-api ignore))
:type 'ebox-surface-tp-protocol-error)
(should-error
(ebox-surface--validate-tp-v2-capability
'(:transaction-protocol incompatible
'(:version "2.0.0" :transaction-protocol incompatible
:structured-participant-api tp-transaction-participate-v2))
:type 'ebox-surface-tp-protocol-error))

View File

@ -4526,12 +4526,19 @@
(ebox--render-source-index (ebox-test-source-index box-input))
(content (ebox-pixel-space 120))
(wrap-count 0)
(original-wrap (symbol-function 'ebox--wrap-text)))
(original-wrap (symbol-function 'ebox--wrap-text))
(original-require (symbol-function 'require)))
(ebox--record-rendered-uniform-width content (nth 2 case))
(cl-letf (((symbol-function 'ebox--wrap-text)
(lambda (&rest arguments)
(cl-incf wrap-count)
(apply original-wrap arguments))))
(apply original-wrap arguments)))
((symbol-function 'require)
(lambda (feature &optional filename noerror)
(if (eq feature 'ekp) t
(funcall original-require feature filename noerror))))
((symbol-function 'ekp-pixel-justify)
(lambda (text _width) text)))
(ebox--format-content-string box content))
(should (> wrap-count 0)))))
@ -7001,6 +7008,117 @@ line compositor's complete property output, including empty lines and chrome."
'(paint-patch)))
(should-not (plist-get report :full-rerender))))))
(ert-deftest ebox-overflow-hidden-clips-inline-content-before-chrome ()
"Hidden overflow clips content on both axes without consuming its chrome."
(ebox-test--reset-runtime-state)
(dolist (chrome '(nil t))
(let* ((input
(ebox-build
`(box :width (ch 3) :height (lh 1) :wrap-mode none
:overflow hidden :box-sizing content-box
,@(when chrome
'(:padding-inline (px 3) :border ((px 1) solid "red")))
(text :help-echo "visible" "ABCDEFGHIJ\nKLMNOP"))))
(rendered (ebox-render input))
(start (string-match "ABC" rendered)))
(should start)
(should-not (string-match-p "[D-Z]" rendered))
(should (= (ebox-string-height rendered) 1))
(should (= (ebox-string-pixel-width rendered)
(+ (* 3 (ebox--size-character-width)) (if chrome 8 0))))
(should (equal (get-text-property start 'help-echo rendered) "visible"))
(when chrome
(should (get-text-property 0 'ebox-bl rendered))
(should (get-text-property (1- (length rendered)) 'ebox-br rendered))))))
(ert-deftest ebox-overflow-hidden-preserves-whole-graphemes ()
"A clipped cluster disappears as a whole and never leaves a partial glyph."
(ebox-test--reset-runtime-state)
(dolist (cluster '("" "" "👨‍👩‍👧‍👦" "🇨🇳"))
(let* ((width (ebox--string-pixel-width cluster))
(input (ebox-build
`(box :width (px ,width) :wrap-mode none :overflow hidden
,(concat cluster cluster))))
(output (ebox-render input)))
(should (equal (substring-no-properties output) cluster))
(let ((narrow (ebox-render
(ebox-build
`(box :width (px ,(max 0 (1- width)))
:wrap-mode none :overflow hidden ,cluster)))))
(should (string-blank-p narrow))
(should (= (ebox-string-pixel-width narrow) (max 0 (1- width))))))))
(ert-deftest ebox-overflow-hidden-clips-pixel-spaces-with-source-properties ()
"A partial display space preserves only the visible space's properties."
(ebox-test--reset-runtime-state)
(let* ((space (propertize (ebox-pixel-space 30) 'help-echo "space"))
(input (ebox-build
`(box :width (px 10) :wrap-mode none :overflow hidden
(text ,(concat space "invisible")))))
(output (ebox-render input)))
(should (= (ebox-string-pixel-width output) 10))
(should (string-blank-p output))
(should (equal (get-text-property 0 'help-echo output) "space"))))
(ert-deftest ebox-overflow-hidden-preserves-grouped-display-space-geometry ()
"One shared replacement display is measured once and keeps its suffix."
(let* ((display '(space :width (15) :height 2 :ascent 80))
(source (concat (propertize "abc" 'display display 'help-echo "glue")
"suffix"))
(output (ebox--clip-line source 20))
(partial (ebox--clip-line source 10)))
(should (= (ebox-string-pixel-width (substring source 0 3)) 15))
(should (= (ebox-string-pixel-width output) 20))
(should (equal (substring-no-properties output) "abcsuffi"))
(should (equal (get-text-property 0 'display output) display))
(should (= (length partial) 1))
(should (= (ebox-string-pixel-width partial) 10))
(should (equal (get-text-property 0 'display partial)
'(space :width (10) :height 2 :ascent 80)))
(should (equal (get-text-property 0 'help-echo partial) "glue"))
(should (equal (get-text-property 1 'display source) display))))
(ert-deftest ebox-overflow-hidden-inline-updates-match-fresh-output ()
"Content growth, shrinking, and overflow reversal use the same clip boundary."
(ebox-test--reset-runtime-state)
(ebox-test--with-rendered-buffer
(ebox-build '(box :id clip :width (ch 3) :overflow hidden :wrap-mode none
(text :id value "ABCDEFGHIJ")))
(dolist (value '("ZYXWVUT" "A" "123456789"))
(ebox-region-update 'value :content value)
(let ((fresh (ebox-render
(ebox-build
`(box :width (ch 3) :overflow hidden :wrap-mode none
,value)))))
(should (equal (buffer-substring-no-properties (point-min) (point-max))
(substring-no-properties fresh)))
(should (= (ebox-string-pixel-width (buffer-string))
(ebox-string-pixel-width fresh)))))
(ebox-region-update 'clip :overflow 'visible)
(should (string-match-p "123456789" (buffer-string)))
(ebox-region-update 'clip :overflow 'hidden)
(should (equal (buffer-substring-no-properties (point-min) (point-max))
"123"))))
(ert-deftest ebox-overflow-hidden-excludes-clipped-child-hit-properties ()
"A clipped child remains in the source without leaving an invisible action."
(ebox-test--reset-runtime-state)
(let ((map (make-sparse-keymap)))
(define-key map (kbd "RET") #'ignore)
(ebox-test--with-rendered-buffer
(ebox-build `(box :id clip :width (ch 3) :wrap-mode none :overflow hidden
(text "ABC")
(text :id clipped :keymap ,map :help-echo "hidden" "HIDE")))
(should (equal (buffer-substring-no-properties (point-min) (point-max)) "ABC"))
(should-not (text-property-not-all (point-min) (point-max) 'keymap nil))
(should-not (text-property-not-all (point-min) (point-max) 'help-echo nil))
(ebox-region-update 'clipped :content "NEW")
(should (equal (buffer-substring-no-properties (point-min) (point-max)) "ABC"))
(ebox-region-update 'clip :width '(ch 6))
(should (equal (buffer-substring-no-properties (point-min) (point-max)) "ABCNEW"))
(should (eq (lookup-key (get-text-property (+ (point-min) 3) 'keymap)
(kbd "RET")) #'ignore)))))
(ert-deftest ebox-overflow-hidden-clips-extra-content-lines ()
(ebox-test--reset-runtime-state)
(let* ((box (ebox-test-box (ebox-test-text "A\nB\nC")
@ -13345,6 +13463,53 @@ explicit visible-window handoff may admit its full-content publication."
'char))
(list china usa)))))
(ert-deftest ebox-kp-wrap-mode-requires-functional-ekp ()
"Explicit KP requests never silently change to word wrapping."
(let ((original-require (symbol-function 'require))
(original-fboundp (symbol-function 'fboundp)))
(dolist (available '(nil t))
(cl-letf (((symbol-function 'require)
(lambda (feature &optional filename noerror)
(if (eq feature 'ekp) available
(funcall original-require feature filename noerror))))
((symbol-function 'fboundp)
(lambda (function)
(and (not (eq function 'ekp-pixel-justify))
(funcall original-fboundp function)))))
(let ((failure (should-error (ebox--wrap-text "abc" 100 'kp))))
(should (string-match-p "EKP" (error-message-string failure))))))))
(ert-deftest ebox-ordinary-wrap-does-not-load-ekp ()
"The optional dependency is required only when KP is requested."
(let ((original-require (symbol-function 'require)))
(cl-letf (((symbol-function 'require)
(lambda (feature &optional filename noerror)
(when (eq feature 'ekp) (ert-fail "Unexpected EKP load"))
(funcall original-require feature filename noerror))))
(dolist (mode '(word char none))
(should (stringp (ebox--wrap-text "alpha beta" 30 mode)))))))
(ert-deftest ebox-kp-missing-dependency-update-preserves-mounted-output ()
"Dependency failure cannot publish a different wrapping algorithm."
(ebox-test--reset-runtime-state)
(ebox-test--with-rendered-buffer
(ebox-build '(box :id paragraph :width (px 100) "alpha beta gamma"))
(let ((before (buffer-string))
(revision (plist-get (ebox-surface-buffer-snapshot (current-buffer))
:revision))
(original-require (symbol-function 'require)))
(cl-letf (((symbol-function 'require)
(lambda (feature &optional filename noerror)
(unless (eq feature 'ekp)
(funcall original-require feature filename noerror)))))
(should-error (ebox-region-update 'paragraph :wrap-mode 'kp)))
(should (equal-including-properties (buffer-string) before))
(should (equal (plist-get (ebox-surface-buffer-snapshot (current-buffer))
:revision)
revision))
(ebox-region-update 'paragraph :content "still usable")
(should (string-match-p "still usable" (buffer-string))))))
(ert-deftest ebox-kp-wrap-mode-invokes-ekp-justification ()
(ebox-test--reset-runtime-state)
(let (require-called justify-args)
@ -13363,6 +13528,30 @@ explicit visible-window handoff may admit its full-content publication."
(should require-called)
(should (equal justify-args (list "abc" 100)))))
(ert-deftest ebox-kp-real-pixel-budget-and-glue-fit-hidden-content ()
"Installed EKP receives content pixels and its real glue survives clipping."
(skip-unless (locate-library "ekp"))
(require 'ekp)
(let* ((width (* 12 (ebox--space-pixel-width)))
(original (symbol-function 'ekp-pixel-justify))
calls output)
(cl-letf (((symbol-function 'ekp-pixel-justify)
(lambda (text pixels)
(push pixels calls)
(funcall original text pixels))))
(setq output
(ebox-render
(ebox-build
`(box :width (px ,width) :box-sizing content-box
:wrap-mode kp :overflow hidden :padding-inline (px 2)
:border "red"
"alpha beta gamma delta epsilon zeta eta theta")))))
(should calls)
(should (cl-every (lambda (pixels) (= pixels width)) calls))
(should (> (ebox-string-height output) 1))
(dolist (line (ebox-string-lines output))
(should (= (ebox-string-pixel-width line) (+ width 6))))))
(ert-deftest ebox-kp-wrap-mode-respects-text-align ()
"KP-wrapped lines should honor the box's horizontal text alignment."
(ebox-test--reset-runtime-state)

View File

@ -475,8 +475,8 @@
:column-gap (% 25) (box "A") (box "B"))))))
(should (= (ebox-string-height vertical) (if (eq overflow 'hidden) 8 10)))
(should (= (ebox--rendered-intrinsic-size horizontal :width) 80))
;; The buffer backend's existing hidden overflow clips vertically only.
(should (= (ebox--string-max-pixel-width horizontal) 100)))))
(should (= (ebox--string-max-pixel-width horizontal)
(if (eq overflow 'hidden) 80 100))))))
(ert-deftest ebox-css-flex-preserves-nested-grid-intrinsic-extents ()
"A Grid child's cyclic gap spill does not enlarge its Flex layout size."

View File

@ -90,9 +90,15 @@
21)))
(ert-deftest ebox-css-layout-pixel-quantization-preserves-private-input ()
"Fractional runs split per glyph; zero widths keep ownership and newlines."
"Distinct fractional display glyphs retain ownership and line boundaries."
(let* ((display '(space :width (0.5) :height 1))
(source (propertize " \n " 'display display 'ebox-content 17))
;; Each replacement needs its own EQ identity. One shared display
;; object over several characters represents only one Emacs glyph.
(source (mapconcat (lambda (text)
(if (equal text "\n") text
(propertize text 'display (copy-tree display)
'ebox-content 17)))
'(" " " " "\n" " " " ") ""))
(before (copy-sequence source))
(result (ebox--quantize-pixel-spaces source)))
(should (equal-including-properties source before))

View File

@ -0,0 +1,73 @@
;;; ebox-display-tests.el --- Display integration tests -*- lexical-binding: t; -*-
;;; Code:
(require 'ert)
(require 'ebox)
(ert-deftest ebox-display-invalid-input-preserves-windows ()
"Invalid input must fail before changing the caller's window layout."
(save-window-excursion
(split-window-right)
(let ((before (current-window-configuration))
(buffer (generate-new-buffer " *ebox-invalid-display*")))
(unwind-protect
(progn
(should-error (ebox-display-buffer buffer 'invalid-input))
(should (compare-window-configurations
before (current-window-configuration))))
(kill-buffer buffer)))))
(ert-deftest ebox-display-honors-native-display-action ()
"The caller chooses the display window without changing selection."
(save-window-excursion
(let* ((selected (selected-window))
(other (split-window-right))
(buffer (generate-new-buffer " *ebox-display-action*"))
(input (ebox-build '(box "Ready"))))
(unwind-protect
(progn
(should
(eq buffer
(ebox-display-buffer
buffer input
(list (lambda (target alist)
(let ((window (alist-get 'window alist)))
(set-window-buffer window target)
window))
(cons 'window other)))))
(should (eq selected (selected-window)))
(should (= 2 (length (window-list))))
(should (eq buffer (window-buffer other)))
(with-current-buffer buffer
(should (string-match-p "Ready" (buffer-string)))
(should ebox-buffer-mode)))
(kill-buffer buffer)))))
(ert-deftest ebox-display-honors-display-buffer-alist ()
"Native display rules also apply when no explicit action is supplied."
(let ((buffer (generate-new-buffer " *ebox-display-rule*")) called)
(unwind-protect
(let ((display-buffer-alist
(list (list (regexp-quote (buffer-name buffer))
(lambda (target _action)
(setq called target)
(selected-window))))))
(should (eq buffer (ebox-display-buffer buffer (ebox-build "Ready"))))
(should (eq buffer called)))
(kill-buffer buffer))))
(ert-deftest ebox-display-delivers-the-target-viewport-intent ()
"A newly displayed buffer uses the ordinary deferred viewport bridge."
(let ((buffer (generate-new-buffer " *ebox-display-viewport*")) frame)
(unwind-protect
(cl-letf (((symbol-function 'display-buffer)
(lambda (_buffer _action) (selected-window)))
((symbol-function 'ebox--window-size-change)
(lambda (target) (setq frame target))))
(ebox-display-buffer buffer (ebox-build "Ready"))
(should (eq frame (selected-frame))))
(kill-buffer buffer))))
(provide 'ebox-display-tests)
;;; ebox-display-tests.el ends here

View File

@ -23,7 +23,7 @@
"Public documents that must describe only the canonical author grammar.")
(defconst ebox-docs-test--active-files
'("Makefile" ".github/workflows/ci.yml"
'("Makefile" ".github/workflows/ci.yml" "LICENSE" "release-dependencies.json"
"ebox.el" "ebox-cache.el" "ebox-runtime-index.el" "ebox-interaction.el"
"ebox-state-contract.el" "ebox-size.el" "ebox-style.el"
"ebox-child-range.el" "ebox-tree.el"
@ -33,7 +33,7 @@
"ebox-surface.el" "ebox-dsl.el" "ebox-spi.el"
"ebox-selector.el" "ebox-native-reflow.el"
"tests/ebox-size-tests.el" "tests/ebox-size-style-tests.el"
"tests/ebox-interaction-tests.el"
"tests/ebox-interaction-tests.el" "tests/ebox-display-tests.el"
"tests/ebox-size-native-tests.el" "tests/ebox-zero-height-tests.el"
"tests/ebox-css-layout-tests.el" "tests/ebox-css-flex-grid-tests.el"
"tests/ebox-css-viewport-tests.el"
@ -54,12 +54,16 @@
"native/src/layout.rs" "native/c/ebox_module.c"
"scripts/ebox-package-lint.el" "scripts/ebox-visual-check.el"
"scripts/ebox-performance-evaluator.el"
"scripts/ebox-release.py" "scripts/ebox-release-smoke.el"
"scripts/test_ebox_release.py"
"scripts/migrate-css-sizes.py" "scripts/test_migrate_css_sizes.py")
"Active Ebox files that the maintainer map must cover.")
(defconst ebox-docs-test--targets
'("check" "ci" "load" "compile" "size-tests" "size-migration-tests"
"core-tests" "runtime-index-tests" "interaction-tests"
"core-tests" "runtime-index-tests" "interaction-tests" "display-tests"
"release-deps" "release-build" "release-check" "release-tool-tests"
"native-integration"
"child-range-tests" "grid-tests"
"ebox-commit-tests" "surface-tests" "visual-check-tests" "package-tests"
"selector-tests" "dsl-tests" "flex-tests" "state-contract-tests"

View File

@ -148,5 +148,44 @@
(when (buffer-live-p buffer)
(kill-buffer buffer)))))
(ert-deftest ebox-measure-display-replacements-own-one-run ()
"A display replacement is one unit even across face and metadata changes."
(dolist (display (list '(space :width (15)) "X"))
(let ((text (propertize "abc" 'display display)))
(should (= (ebox-string-pixel-width text) (string-pixel-width text)))
(put-text-property 1 2 'face 'bold text)
(put-text-property 2 3 'help-echo "last" text)
(should (= (ebox-string-pixel-width text) (string-pixel-width text)))
(should (= (ebox-string-pixel-width (concat text "Z"))
(string-pixel-width (concat text "Z")))))))
(ert-deftest ebox-measure-distinguishes-adjacent-display-identities ()
"Equal but distinct display values are two replacements, including cache hits."
(let* ((one (propertize "ab" 'display (list 'space :width (list 15))))
(two (concat (propertize "a" 'display (list 'space :width (list 15)))
(propertize "b" 'display (list 'space :width (list 15))))))
(dotimes (_ 2)
(should (= (ebox-string-pixel-width one) 15))
(should (= (ebox-string-pixel-width two) 30)))))
(ert-deftest ebox-measure-quantizes-display-runs-without-splitting-them ()
"Fractional replacements round once per EQ run and retain source properties."
(let* ((first (propertize "abc" 'display (list 'space :width (list 3.5))
'help-echo "first"))
(second (propertize "de" 'display (list 'space :width (list 3.5))))
(source (concat first second))
(result (ebox--quantize-pixel-spaces source)))
(should (= (ebox-string-pixel-width source) 7))
(should (= (ebox-string-pixel-width result) 7))
(should (eq (get-text-property 0 'display result)
(get-text-property 2 'display result)))
(should (eq (get-text-property 3 'display result)
(get-text-property 4 'display result)))
(should-not (eq (get-text-property 2 'display result)
(get-text-property 3 'display result)))
(should (equal (get-text-property 2 'help-echo result) "first"))
(should (= (ebox--fixed-pixel-space-width (get-text-property 0 'display source))
3.5))))
(provide 'ebox-font-tests)
;;; ebox-font-tests.el ends here

View File

@ -958,6 +958,238 @@ The helper keeps face objects out of the caller's ERT assertion temporaries."
(should (eq (key-binding (kbd "RET")) #'backward-char))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-interaction-navigation-nested-owners-and-boundaries ()
"Inherited fragments form one stop while explicit nested maps stay distinct."
(with-temp-buffer
(ebox-render-to-buffer
(current-buffer)
(ebox-build '(column
(text "intro")
(box :keymap (keymap (13 . ignore))
(column (text "parent-one")
(text :keymap nil "blocked")
(text :keymap (keymap (13 . forward-char)) "child")
(text "parent-two")))
(text :keymap (keymap (13 . ignore)) "last"))))
(let* ((text (buffer-string))
(parent (1+ (ebox-interaction-test--position text "parent-one")))
(child (1+ (ebox-interaction-test--position text "child")))
(tail (1+ (ebox-interaction-test--position text "parent-two")))
(last (1+ (ebox-interaction-test--position text "last"))))
(goto-char (point-min))
(should (= (ebox-next-interaction) parent))
(should (= (ebox-next-interaction) child))
(should (= (ebox-next-interaction) last))
(should-error (ebox-next-interaction) :type 'user-error)
(should (= (point) last))
(should (= (ebox-previous-interaction 2) parent))
(should-error (ebox-previous-interaction) :type 'user-error)
(should (= (point) parent))
(should (= (ebox-next-interaction 0) parent))
(should (= (ebox-previous-interaction -1) child))
(should (= (ebox-next-interaction -1) parent))
(goto-char tail)
(should (= (ebox-previous-interaction) child))
(goto-char tail)
(should (= (ebox-next-interaction) last))
(goto-char (point-min))
(should-error (ebox-next-interaction 4) :type 'user-error)
(should (= (point) (point-min)))
(should-error (ebox-next-interaction 1.5) :type 'wrong-type-argument))))
(ert-deftest ebox-interaction-navigation-uses-live-mount-and-declared-maps ()
"Buffer keymaps, copied rendering and empty or blocked maps are not stops."
(with-temp-buffer
(insert (ebox-render (ebox-build '(text :keymap (keymap (13 . ignore)) "copy"))))
(should-error (ebox-next-interaction) :type 'user-error))
(with-temp-buffer
(use-local-map (let ((map (make-sparse-keymap)))
(define-key map (kbd "RET") #'ignore) map))
(ebox-render-to-buffer
(current-buffer)
(ebox-build '(column (text "intro")
(text :keymap (keymap) "empty")
(text :keymap (keymap (13 . undefined)) "disabled")
(box :visibility hidden :keymap (keymap (13 . ignore)) "hidden")
(box :height (lh 0) :overflow hidden
:keymap (keymap (13 . ignore)) "clipped")
(text :keymap (keymap (13 . ignore)) "active"))))
(let* ((text (buffer-string))
(active (1+ (ebox-interaction-test--position text "active"))))
(goto-char (point-min))
(should (= (ebox-next-interaction) active))
(should-error (ebox-next-interaction) :type 'user-error)
(should-error (ebox-previous-interaction) :type 'user-error))))
(ert-deftest ebox-interaction-navigation-narrowing-and-invisible-text ()
"Only accessible visible characters contribute a stop, without widening."
(with-temp-buffer
(ebox-render-to-buffer
(current-buffer)
(ebox-build '(column (text :keymap (keymap (13 . ignore)) "first")
(text :keymap (keymap (13 . forward-char)) "second")
(text :keymap (keymap (13 . backward-char)) "third"))))
(let* ((text (buffer-string))
(first (1+ (ebox-interaction-test--position text "first")))
(second (1+ (ebox-interaction-test--position text "second")))
(third (1+ (ebox-interaction-test--position text "third"))))
(save-restriction
(narrow-to-region (+ first 2) third)
(goto-char (point-max))
(should (= (ebox-previous-interaction) second))
(should (= (ebox-previous-interaction) (point-min)))
(should-error (ebox-previous-interaction) :type 'user-error)
(should (= (point-min) (+ first 2))))
(let ((overlay (make-overlay second (+ second 6))))
(unwind-protect
(progn
(overlay-put overlay 'invisible t)
(goto-char first)
(should (= (ebox-next-interaction) third))
(move-overlay overlay second (+ second 2))
(goto-char first)
(should (= (ebox-next-interaction) (+ second 2))))
(delete-overlay overlay))))))
(ert-deftest ebox-interaction-navigation-padding-and-native-overrides ()
"Nested padding resolves to its own box; foreign maps do not create stops."
(with-temp-buffer
(ebox-render-to-buffer
(current-buffer)
(ebox-build '(column (text "intro")
(row :keymap (keymap (13 . ignore))
(box :padding-left (ch 2)
:keymap (keymap (13 . forward-char)) "child")
(text "parent"))
(text "ordinary"))))
(let* ((text (buffer-string))
(child (1+ (ebox-interaction-test--position text "child")))
(parent (1+ (ebox-interaction-test--position text "parent")))
(ordinary (1+ (ebox-interaction-test--position text "ordinary"))))
(goto-char (point-min))
(let ((padding (ebox-next-interaction)))
(should (< padding child))
(should (eq (key-binding (kbd "RET")) #'forward-char))
(should (= (ebox-next-interaction) parent)))
(let ((inhibit-read-only t))
(put-text-property ordinary (+ ordinary 8) 'keymap '(keymap (13 . ignore))))
(should-error (ebox-next-interaction) :type 'user-error)
(let ((overlay (make-overlay ordinary (+ ordinary 8))))
(unwind-protect
(progn
(overlay-put overlay 'keymap '(keymap (13 . forward-char)))
(should-error (ebox-next-interaction) :type 'user-error))
(delete-overlay overlay))))))
(ert-deftest ebox-interaction-navigation-follows-updates-and-scroll ()
"Updated, removed and newly visible actions are read from the publication."
(with-temp-buffer
(ebox-render-to-buffer
(current-buffer)
(ebox-build '(column
(text "intro")
(text :id "first" :keymap (keymap (13 . ignore)) "first")
(box :id "scroll" :height (lh 1) :width (ch 20) :overflow scroll
(column
(text :keymap (keymap (13 . forward-char)) "second")
(text :keymap (keymap (13 . backward-char)) "third"))))))
(goto-char (point-min))
(ebox-next-interaction)
(should (looking-at "first"))
(ebox-region-update (ebox-region-resolve (current-buffer) "first") :keymap nil)
(goto-char (point-min))
(ebox-next-interaction)
(should (looking-at "second"))
(ebox-region-update (ebox-region-resolve (current-buffer) "scroll") :scroll-offset 1)
(goto-char (point-min))
(ebox-next-interaction)
(should (looking-at "third"))
(ebox-region-update (ebox-region-resolve (current-buffer) "first")
:keymap '(keymap (13 . ignore)) :content "restored")
(goto-char (point-min))
(ebox-next-interaction)
(should (looking-at "restored"))
(should-error
(ebox-commit (current-buffer) (ebox-build '(text "rejected"))
(lambda (_report) (error "Reject navigation change"))))
(goto-char (point-min))
(ebox-next-interaction)
(should (looking-at "restored"))
(ebox-commit (current-buffer) (ebox-build '(text "no interactions")))
(should-error (ebox-next-interaction) :type 'user-error)))
(ert-deftest ebox-interaction-navigation-keymap-prefixes-parents-and-menus ()
"Inspect native binding graphs without executing dynamic menu predicates."
(let* ((map (make-sparse-keymap))
(prefix (make-sparse-keymap))
(parent (make-sparse-keymap))
(calls 0))
(define-key prefix "r" map)
(define-key map "p" prefix)
(should-not (ebox-interaction--navigable-keymap-p map))
(define-key parent (kbd "RET") #'ignore)
(set-keymap-parent map parent)
(should (ebox-interaction--navigable-keymap-p map))
(define-key map (kbd "RET") #'undefined)
(should-not (ebox-interaction--navigable-keymap-p map))
(define-key map (kbd "RET") nil)
(should-not (lookup-key map (kbd "RET")))
(should-not (ebox-interaction--navigable-keymap-p map))
(keymap-unset map "RET" t)
(should (ebox-interaction--navigable-keymap-p map))
(set-keymap-parent map nil)
(define-key map [menu-bar item]
(list 'menu-item "Action" #'ignore
:enable '(progn (cl-incf calls) nil)
:filter (lambda (definition) (cl-incf calls) definition)))
(should (ebox-interaction--navigable-keymap-p map))
(should (= calls 0)))
(let ((full (make-keymap)) (parent (make-sparse-keymap)))
(define-key parent (kbd "RET") #'ignore)
(set-keymap-parent full parent)
(set-char-table-range (nth 1 full) t #'undefined)
(should-not (ebox-interaction--navigable-keymap-p full))
(define-key full (kbd "RET") nil)
(should-not (lookup-key full (kbd "RET")))
(should-not (ebox-interaction--navigable-keymap-p full))
(define-key full (kbd "RET") #'ignore)
(should (ebox-interaction--navigable-keymap-p full))))
(ert-deftest ebox-interaction-navigation-inherits-effective-prefix-bindings ()
"Native prefix inheritance remains navigable without losing child blocking."
(let ((map (make-sparse-keymap))
(parent (make-sparse-keymap))
(prefix (make-sparse-keymap))
(calls 0))
(define-key parent "pr" #'ignore)
(define-key map "p" prefix)
(set-keymap-parent map parent)
(should (eq (lookup-key map "pr") #'ignore))
(should (ebox-interaction--navigable-keymap-p map))
(with-temp-buffer
(ebox-render-to-buffer
(current-buffer)
(ebox-build (list 'column '(text "intro") (list 'text :keymap map "action"))))
(goto-char (point-min))
(ebox-next-interaction)
(should (looking-at "action"))
(should (eq (key-binding "pr") #'ignore)))
(define-key prefix "r" #'undefined)
(should (eq (lookup-key map "pr") #'undefined))
(should-not (ebox-interaction--navigable-keymap-p map))
(keymap-unset prefix "r" t)
(define-key parent "pr"
(list 'menu-item "Inherited" #'ignore
:filter (lambda (definition) (cl-incf calls) definition)))
(should (ebox-interaction--navigable-keymap-p map))
(should (= calls 0))
(should (eq (car (lookup-key parent "p")) 'keymap))
;; Inspection must retain the original menu wrapper and callback.
(should (eq (car (cdr (assq ?r (cdr (lookup-key parent "p"))))) 'menu-item))
(define-key map "p" #'undefined)
(should-not (ebox-interaction--navigable-keymap-p map))
(should (= calls 0))))
(provide 'ebox-interaction-tests)
;;; ebox-interaction-tests.el ends here

View File

@ -11,6 +11,7 @@
(defconst ebox-test--autoload-source-files
'("ebox.el"
"ebox-interaction.el"
"ebox-layout-config.el"
"ebox-node-factory.el"
"ebox-measure.el"
@ -46,11 +47,11 @@
(buffer-string)))
(source (ebox-test--production-lisp-text)))
(should (string-match-p "Version: 3.0.0" facade))
(should (string-match-p (regexp-quote "(tp \"1.0.1\")") facade))
(should (string-match-p (regexp-quote "(tp \"2.0.0\")") facade))
(should-not (string-match-p "ebox-transaction-participant-route" source))
(should-not (string-match-p "(tp-transaction-participate\\_>" source))
(should (string-match-p "(tp-transaction-participate-v2\\_>" source))
(should (equal ebox-framework-spi-required-tp-version "1.0.1"))
(should (equal ebox-framework-spi-required-tp-version "2.0.0"))
(should (memq tp-transaction-protocol
ebox-framework-spi-supported-tp-protocols))))
@ -90,9 +91,8 @@
(ert-deftest ebox-canonical-module-loads-typed-port-without-facade ()
"The canonical autoload boundary should not depend on loading `ebox.el'."
(let* ((emacs (expand-file-name invocation-name invocation-directory))
(parent (file-name-directory (directory-file-name ebox-test--root)))
(ecss (expand-file-name "ecss" parent))
(tp (expand-file-name "tp" parent)))
(ecss (file-name-directory (locate-library "ecss")))
(tp (file-name-directory (locate-library "tp"))))
(with-temp-buffer
(should
(zerop
@ -127,9 +127,8 @@
(ert-deftest ebox-native-command-autoloads-may-target-the-facade ()
"Startup registries may safely autoload native commands from `ebox'."
(let* ((emacs (expand-file-name invocation-name invocation-directory))
(parent (file-name-directory (directory-file-name ebox-test--root)))
(ecss (expand-file-name "ecss" parent))
(tp (expand-file-name "tp" parent)))
(ecss (file-name-directory (locate-library "ecss")))
(tp (file-name-directory (locate-library "tp"))))
(with-temp-buffer
(should
(zerop
@ -246,6 +245,8 @@
("ebox-byte-compile" . "defun")
("ebox-native-build" . "defun")
("ebox-native-status" . "defun")
("ebox-next-interaction" . "defun")
("ebox-previous-interaction" . "defun")
("ebox-buffer-mode" . "define-minor-mode")
("ebox-render-to-buffer" . "defun")
("ebox-display-buffer" . "defun")))

View File

@ -25,6 +25,214 @@
(should-not ebox-native-reflow--loaded-module-path))
(delete-file module))))
(ert-deftest ebox-size-native-rejects-pre-inline-clipping-module-abi ()
"An older module cannot silently keep the previous visible inline overflow."
(let ((module (make-temp-file "ebox-old-native-clipping-"))
(ebox-native-reflow--load-attempted-p nil)
(ebox-native-reflow--available-p nil)
(ebox-native-reflow--load-error nil)
(ebox-native-reflow--loaded-module-path nil)
(ebox-native-reflow--loaded-module-hash nil))
(unwind-protect
(cl-letf (((symbol-function 'ebox-native-reflow--candidate-paths)
(lambda () (list module)))
((symbol-function 'module-load) #'ignore)
((symbol-function 'ebox-native--module-version)
(lambda () "13:7:12")))
(should-not (ebox-native-reflow-load))
(should (string-match-p "ABI mismatch" ebox-native-reflow--load-error)))
(delete-file module))))
(ert-deftest ebox-size-native-inline-clipping-matches-elisp ()
"Native clipping retains content properties and Box chrome within its width."
(skip-unless (ebox-native-reflow-layout-ready-p))
(dolist (text '("ABCDEFGHIJ" "界界界" "ééé" "👨‍👩‍👧‍👦👨‍👩‍👧‍👦"))
(dolist (width '(0 3 8 24))
(let* ((input (ebox-build
`(box :width (px ,width) :box-sizing content-box
:padding-inline (px 2) :border "red"
:overflow hidden :wrap-mode none
(text :help-echo "clipped text" ,text))))
(node (ebox-canonical-input--single-root input "Native clipping"))
(normal (let ((ebox--surface-materialization-active t))
(ebox--render-with-cache node)))
(frame (ebox-native-reflow-execute-sync
node '(:key 1 :viewport-width 80 :viewport-height 20
:runtime-revision 0 :context-hash 0 :complete t)))
(actual (plist-get frame :rendered)))
(ert-info ((format "width=%s text=%s" width text))
(should (plist-get frame :native-frame))
(should (= (ebox-string-pixel-width actual) (+ width 6)))
(should (equal-including-properties actual normal)))))))
(ert-deftest ebox-size-native-inline-clipping-retains-update-and-rollback-parity ()
"Retained native publication uses the same clip boundary as a fresh render."
(skip-unless (ebox-native-reflow-layout-ready-p))
(let ((ebox-viewport-width 80)
(ebox-viewport-height 20)
(ebox-runtime-idle-prewarm nil)
(ebox-runtime-idle-reflow-cache-prewarm nil))
(cl-labels ((source (text width overflow)
(ebox-build
`(column :key root :width (px 80)
(box :key clip :width (px ,width)
:overflow ,overflow :wrap-mode none
:border "red" :padding-inline (px 2)
(text :key label :help-echo "value" ,text))
(box :key peer "unchanged"))))
(assert-fresh ()
(let ((state (ebox--buffer-render-state (current-buffer))))
(should (equal-including-properties
(buffer-string)
(ebox--render-node
(plist-get state :root-node)
(plist-get state :source-index)))))))
(with-temp-buffer
(ebox-render-to-buffer (current-buffer) (source "ABCDEFGHIJ" 9 'hidden))
(dolist (case '(("ZYXWVUT" 9 hidden) ("A" 9 hidden)
("123456789" 9 visible) ("123456789" 9 hidden)
("界界界" 11 hidden)))
(let ((report (ebox-commit (current-buffer) (apply #'source case))))
(should (eq (plist-get report :projection-kind) 'native-frame))
(assert-fresh)))
(let ((before (buffer-string))
(revision (ebox-surface-buffer-revision (current-buffer))))
(should-error
(ebox-commit (current-buffer) (source "NEWVALUE" 10 'hidden)
(lambda (_report) (error "Reject clipping update"))))
(should (equal-including-properties before (buffer-string)))
(should (= (ebox-surface-buffer-revision (current-buffer)) revision)))
(ebox-commit (current-buffer) (source "NEWVALUE" 10 'hidden))
(assert-fresh)
(ebox-unmount-buffer (current-buffer))))))
(ert-deftest ebox-size-native-declines-cross-property-graphemes ()
"A single native atom cannot encode different properties inside a cluster."
(dolist (parts '(("e" "́") ("👨" "‍👩‍👧‍👦") ("🇨" "🇳")))
(let* ((base (propertize (car parts) 'help-echo "base"))
(mark (propertize (cadr parts) 'help-echo "mark"))
(source (concat base mark)))
;; Prime the property-run cache before checking the cross-run boundary.
(should (ebox-native-reflow--compile-text base))
(dotimes (_ 2)
(let ((failure (should-error (ebox-native-reflow--compile-text source))))
(should (string-match-p "grapheme" (error-message-string failure)))))
(should (ebox-native-reflow--compile-text
(concat (propertize (concat (car parts) (cadr parts))
'help-echo "complete")
(propertize "x" 'help-echo "next")))))))
(ert-deftest ebox-size-native-cross-property-grapheme-fallback-and-rollback ()
"Initial render and native-to-Elisp updates preserve complete styled clusters."
(skip-unless (ebox-native-reflow-layout-ready-p))
(let ((ebox-viewport-width 80)
(ebox-viewport-height 20)
(ebox-runtime-idle-prewarm nil)
(ebox-runtime-idle-reflow-cache-prewarm nil))
(dolist (parts '(("e" "́") ("👨" "‍👩‍👧‍👦") ("🇨" "🇳")))
(let* ((plain (concat (car parts) (cadr parts)))
(styled (concat (propertize (car parts) 'help-echo "base")
(propertize (cadr parts) 'help-echo "mark")))
(width (ebox--string-pixel-width plain)))
(cl-labels ((source (text)
(ebox-build `(box :key root :width (px ,width)
:overflow hidden :wrap-mode none
(text :key label ,(concat text plain)))))
(assert-fresh ()
(let ((state (ebox--buffer-render-state (current-buffer))))
(should (equal (buffer-substring-no-properties
(point-min) (point-max)) plain))
(should (equal-including-properties
(buffer-string)
(ebox--render-node
(plist-get state :root-node)
(plist-get state :source-index)))))))
(with-temp-buffer
(ebox-render-to-buffer (current-buffer) (source styled))
(assert-fresh)
(should (equal (get-text-property (point-min) 'help-echo) "base"))
(should (equal (get-text-property
(+ (point-min) (length (car parts))) 'help-echo)
"mark"))
(ebox-unmount-buffer (current-buffer)))
(with-temp-buffer
(ebox-render-to-buffer (current-buffer) (source plain))
(should (eq (plist-get (ebox--buffer-render-state (current-buffer))
:projection-kind) 'native-frame))
(let ((before (buffer-string))
(revision (ebox-surface-buffer-revision (current-buffer))))
(should-error
(ebox-commit (current-buffer) (source styled)
(lambda (_report) (error "Reject grapheme fallback"))))
(should (equal-including-properties before (buffer-string)))
(should (= (ebox-surface-buffer-revision (current-buffer)) revision)))
(ebox-commit (current-buffer) (source styled))
(assert-fresh)
(should (equal (get-text-property (point-min) 'help-echo) "base"))
(ebox-commit (current-buffer) (source plain))
(assert-fresh)
(ebox-unmount-buffer (current-buffer))))))))
(ert-deftest ebox-size-native-grouped-display-capability-preserves-pixel-runs ()
"Opaque source displays fall back; formatted glue compiles as one atom."
(let* ((display '(space :width (15)))
(plain (propertize " " 'display display))
(formatted (ebox-native-reflow--compile-text plain t))
(clusters (plist-get (aref (plist-get formatted :lines) 0) :clusters)))
(should (= (length clusters) 1))
(should (= (plist-get (aref clusters 0) :width) 15))
(should (eq (plist-get (aref clusters 0) :pixel-space) t))
(dolist (text (list plain
(concat (propertize "a" 'display display 'help-echo "first")
(propertize "bc" 'display display 'help-echo "rest"))))
(let ((failure (should-error (ebox-native-reflow--compile-text text))))
(should (string-match-p "unsafe property display"
(error-message-string failure)))))))
(ert-deftest ebox-size-native-grouped-display-fallback-keeps-suffix-and-rollback ()
"Public native-capable mounts preserve one grouped display and its suffix."
(skip-unless (ebox-native-reflow-layout-ready-p))
(let* ((ebox-viewport-width 80)
(ebox-viewport-height 20)
(ebox-runtime-idle-prewarm nil)
(ebox-runtime-idle-reflow-cache-prewarm nil)
(display '(space :width (15)))
(grouped (concat (propertize "a" 'display display 'help-echo "first")
(propertize "bc" 'display display 'help-echo "rest")
"suffix")))
(cl-labels ((source (text)
(ebox-build `(box :key root :width (px 20) :overflow hidden
:wrap-mode none (text :key label ,text))))
(assert-grouped ()
(should (equal (buffer-substring-no-properties
(point-min) (point-max)) "abcsuffi"))
(should (= (ebox-string-pixel-width (buffer-string)) 20))
(should (= (string-pixel-width (buffer-string)) 20))
(should (eq (get-text-property (point-min) 'display)
(get-text-property (1+ (point-min)) 'display)))
(should (equal (get-text-property (point-min) 'help-echo) "first"))
(should (equal (get-text-property (1+ (point-min)) 'help-echo) "rest"))))
(with-temp-buffer
(ebox-render-to-buffer (current-buffer) (source grouped))
(assert-grouped)
(should-not (eq (plist-get (ebox--buffer-render-state (current-buffer))
:projection-kind) 'native-frame))
(ebox-unmount-buffer (current-buffer)))
(with-temp-buffer
(ebox-render-to-buffer (current-buffer) (source "plain"))
(should (eq (plist-get (ebox--buffer-render-state (current-buffer))
:projection-kind) 'native-frame))
(let ((before (buffer-string))
(revision (ebox-surface-buffer-revision (current-buffer))))
(should-error
(ebox-commit (current-buffer) (source grouped)
(lambda (_report) (error "Reject display fallback"))))
(should (equal-including-properties before (buffer-string)))
(should (= (ebox-surface-buffer-revision (current-buffer)) revision)))
(ebox-commit (current-buffer) (source grouped))
(assert-grouped)
(ebox-unmount-buffer (current-buffer))))))
(ert-deftest ebox-size-native-fixed-units-use-owning-font ()
"Fixed units and math compile using the source node's measured font."
(let ((node (list :font-owner t)))

View File

@ -38,7 +38,7 @@
(should (ebox-framework-spi-validate-provider first))
(should-not (eq first second))
(should (= (ebox-framework-spi-provider-spi-version first) 2))
(should (equal ebox-framework-spi-required-tp-version "1.0.1"))
(should (equal ebox-framework-spi-required-tp-version "2.0.0"))
(should (equal ebox-framework-spi-supported-tp-protocols
'(tp-transaction-protocol-v1+v2
tp-transaction-protocol-v2)))
@ -120,7 +120,7 @@
"etaf-renderer-port" "etaf-spi-bootstrap"))
(should-not (string-match-p (regexp-quote pattern) source)))
(should (string-match-p "Version: 3.0.0" facade))
(should (string-match-p "(tp \"1.0.1\")" facade))
(should (string-match-p "(tp \"2.0.0\")" facade))
(should (functionp #'ebox-render-to-buffer))
(should (functionp #'ebox-commit))
(should (functionp #'ebox-framework-spi-capabilities))))