Prepare standalone distribution and harden clipping and interaction
This commit is contained in:
parent
79f5bc23d1
commit
a8417d56c6
47
.github/workflows/ci.yml
vendored
47
.github/workflows/ci.yml
vendored
@ -7,24 +7,34 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
emacs: ['29.1', '30.2']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository
|
- name: Check out repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Check out TP runtime dependency
|
- name: Install supported Emacs version
|
||||||
run: git clone --depth 1 https://github.com/Kinneyzhang/tp.git ../tp
|
uses: purcell/setup-emacs@34c6ded44899fd1bf74d2889558befd1750e61a7
|
||||||
|
with:
|
||||||
|
version: ${{ matrix.emacs }}
|
||||||
|
|
||||||
- name: Install system dependencies
|
- name: Fetch locked TP and ECSS dependencies
|
||||||
run: |
|
run: make release-deps RELEASE_DEPS_DIR="$RUNNER_TEMP/ebox-deps"
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y emacs-nox python3
|
|
||||||
|
|
||||||
- name: Run local verification suite
|
- 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
|
- 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:
|
native-build:
|
||||||
strategy:
|
strategy:
|
||||||
@ -62,6 +72,27 @@ jobs:
|
|||||||
*) exit 1 ;;
|
*) exit 1 ;;
|
||||||
esac
|
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:
|
native-msrv:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|||||||
@ -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 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 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.
|
||||||
|
|||||||
68
CHANGELOG.md
68
CHANGELOG.md
@ -6,7 +6,22 @@ All notable changes to Ebox are documented here.
|
|||||||
|
|
||||||
### Added
|
### 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.
|
- 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.
|
- 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.
|
- 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
|
### 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
|
- Package metadata now distinguishes builds with the public unmount/revision
|
||||||
boundary and SPI v2 compatibility surface from older 2.x development
|
boundary and SPI v2 compatibility surface from older 2.x development
|
||||||
snapshots.
|
snapshots.
|
||||||
- TP 1.0.1 is the minimum consumer-first dependency. Ebox accepts its structured
|
- Early 3.0 development used TP 1.0.1's structured transition protocol. The
|
||||||
transition protocol and TP 2.0.0's final v2-only protocol, while always using
|
release candidate now requires TP 2.0.0 or newer and always
|
||||||
the public structured participant API.
|
uses the public structured participant API.
|
||||||
- SPI initial publication owns widening and an outer editor change group. A
|
- SPI initial publication owns widening and an outer editor change group. A
|
||||||
failed stage or nonlocal rollback exit restores text, markers, overlays,
|
failed stage or nonlocal rollback exit restores text, markers, overlays,
|
||||||
point, mark, narrowing, undo history, and modified state before it escapes.
|
point, mark, narrowing, undo history, and modified state before it escapes.
|
||||||
@ -44,10 +81,31 @@ All notable changes to Ebox are documented here.
|
|||||||
|
|
||||||
### Migration
|
### 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
|
- Remove any customization of `ebox-transaction-participant-route`; Ebox 3.0.0
|
||||||
requires TP's structured participant capability and fails closed when it is
|
requires TP's structured participant capability and fails closed when it is
|
||||||
missing or malformed.
|
missing or malformed.
|
||||||
- Replace numeric region-id mutation with `ebox-region-resolve` followed by `ebox-region-update` on the returned handle.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@ -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。
|
- 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 publication;handle、selector、viewport 与 scroll 更新加入同一原子事务。
|
- 通过 `ebox-region-resolve` 提供 surface-scoped region handle;`ebox-render-to-buffer` 与 `ebox-display-buffer` 统一使用 retained TP publication;handle、selector、viewport 与 scroll 更新加入同一原子事务。
|
||||||
- 增加 typography longhand 与继承 foreground 的投影;子盒子覆盖背景色时仍能保持文字可读。
|
- 增加 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
|
- 包 metadata 现在会区分具备 public unmount/revision 边界与 SPI v2
|
||||||
compatibility surface 的构建,旧的 2.x development snapshot 不会再被误接受。
|
compatibility surface 的构建,旧的 2.x development snapshot 不会再被误接受。
|
||||||
- TP 1.0.1 是 consumer-first 迁移的最低依赖。Ebox 接受其 structured 过渡协议与
|
- 早期 3.0 开发使用 TP 1.0.1 的 structured 过渡协议;当前候选版本要求 TP 2.0.0
|
||||||
TP 2.0.0 的最终 v2-only 协议,但始终只调用公开 structured participant API。
|
或更新版本,并始终只调用公开 structured participant API。
|
||||||
- SPI initial publication 自己负责 widening 与外层 editor change group。stage
|
- SPI initial publication 自己负责 widening 与外层 editor change group。stage
|
||||||
失败或 rollback 非局部退出向外传播前,会恢复 text、marker、overlay、point、
|
失败或 rollback 非局部退出向外传播前,会恢复 text、marker、overlay、point、
|
||||||
mark、narrowing、undo history 与 modified state。
|
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 3.0.0。
|
||||||
- 删除 `ebox-transaction-participant-route` 的自定义设置;Ebox 3.0.0 要求 TP
|
- 删除 `ebox-transaction-participant-route` 的自定义设置;Ebox 3.0.0 要求 TP
|
||||||
提供 structured participant capability,缺失或格式错误时会 fail closed。
|
提供 structured participant capability,缺失或格式错误时会 fail closed。
|
||||||
- 数字 region id 更新改为先调用 `ebox-region-resolve`,再把返回的 handle 交给 `ebox-region-update`。
|
- 数字 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-pop-to-buffer` 或 `ebox-switch-to-buffer` 改为 `ebox-display-buffer`;如果显示行为由调用方控制,则使用 `ebox-render-to-buffer`。
|
||||||
- Ebox 不扫描历史渲染文本来重建 runtime identity;请通过 Ebox 3.0 重新 mount 声明式 source。
|
- Ebox 不扫描历史渲染文本来重建 runtime identity;请通过 Ebox 3.0 重新 mount 声明式 source。
|
||||||
|
|
||||||
|
### 发布状态与支持边界
|
||||||
|
|
||||||
|
- `3.0.0` 仍未发布;这些修改准备源码分发,不代表已上传包归档或发布版本 tag。
|
||||||
|
- 每个已挂载 buffer 拥有一套视口布局,需要独立宽度的视图使用不同 buffer。纵向
|
||||||
|
几何仍以完整行为单位。
|
||||||
|
- 保留常见 UI 文本簇,但完整 Unicode 字素分段、浏览器级双向布局、层叠和绝对定位
|
||||||
|
不属于当前契约。
|
||||||
|
|||||||
35
DESIGN.md
35
DESIGN.md
@ -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.
|
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
|
## 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
|
## Size composition
|
||||||
|
|
||||||
|
|||||||
27
DESIGN.zh.md
27
DESIGN.zh.md
@ -30,9 +30,34 @@ Ebox 是底层空间渲染引擎。它负责把声明式节点树转换为经过
|
|||||||
|
|
||||||
使用 `docs/user/ebox-api-reference.zh.md` 中的函数与 property;门面清单是 `ebox-public-api`。应用和同级包不得调用 `ebox--*` 私有名称。更高层的 Component、响应式、behavior、data 和 control 概念属于 ETAF。
|
使用 `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
674
LICENSE
Normal 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>.
|
||||||
29
Makefile
29
Makefile
@ -1,8 +1,13 @@
|
|||||||
EMACS ?= emacs
|
EMACS ?= emacs
|
||||||
CARGO ?= cargo
|
CARGO ?= cargo
|
||||||
|
PYTHON ?= python3
|
||||||
TP_DIR ?= ../tp
|
TP_DIR ?= ../tp
|
||||||
ECSS_DIR ?= ../ecss
|
ECSS_DIR ?= ../ecss
|
||||||
LOAD_EXTRA ?=
|
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_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
|
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: node-memo-generation-tests
|
||||||
.PHONY: interaction-tests
|
.PHONY: interaction-tests
|
||||||
|
.PHONY: display-tests release-deps release-build release-check release-tool-tests native-integration release-native-check
|
||||||
|
|
||||||
all: check
|
all: check
|
||||||
|
|
||||||
@ -22,6 +28,29 @@ interaction-tests:
|
|||||||
|
|
||||||
test: 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
|
check: ci
|
||||||
|
|
||||||
ci: checkdoc load compile test visual-check diff-check
|
ci: checkdoc load compile test visual-check diff-check
|
||||||
|
|||||||
114
README.md
114
README.md
@ -7,10 +7,20 @@ application also needs Components, reactive state, behaviors, or lifecycle.
|
|||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Ebox requires Emacs 29.1 or newer, ECSS 0.1.0 or newer, and TP 1.0.1 or newer.
|
The source repository is [geekinney/ebox](https://gitea.gklazycat.heiyu.space/geekinney/ebox).
|
||||||
A package manager should
|
The current `3.0.0` changelog is unreleased; a source checkout is not a published
|
||||||
install the declared dependencies. For sibling source checkouts, add the three
|
package archive or stable release tag.
|
||||||
directories to `load-path`, then load Ebox:
|
|
||||||
|
| 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
|
```elisp
|
||||||
(add-to-list 'load-path "/path/to/ecss")
|
(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.
|
Loading Ebox does not create a buffer or build native code.
|
||||||
|
|
||||||
|
For Knuth–Plass 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 Knuth–Plass 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
|
## First render
|
||||||
|
|
||||||
The ordinary author model has seven entries: a string, `text`, `box`, `row`,
|
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.
|
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
|
Use `ebox-region-update` with a semantic ID in the current mounted buffer or
|
||||||
an explicit region handle to replace or clear these capabilities.
|
an explicit region handle to replace or clear these capabilities.
|
||||||
Native keymaps support independent Ebox interactions; application state,
|
Native keymaps support independent Ebox interactions. Optional
|
||||||
focus navigation, and command behavior remain the author's responsibility.
|
`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)
|
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).
|
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` returns propertized text without publishing a live buffer.
|
||||||
- `ebox-render-to-buffer` mounts a retained surface.
|
- `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-commit` atomically publishes a newly built canonical input.
|
||||||
- `ebox-buffer-update-report` returns the last successful update report.
|
- `ebox-buffer-update-report` returns the last successful update report.
|
||||||
- `ebox-rerender-buffer-with-context` applies an explicit viewport change.
|
- `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
|
after unmounting signals an error. Compare both mount ID and revision when
|
||||||
identifying a committed generation; remounting can restart revision numbers.
|
identifying a committed generation; remounting can restart revision numbers.
|
||||||
|
|
||||||
Ebox registers rollback-capable state only through TP's public structured
|
Ebox registers rollback-capable state only through TP 2.0.0's public structured
|
||||||
participant API. TP 1.0.1 supports the consumer-first migration protocol;
|
participant API and final v2 protocol. Older transition builds do not provide
|
||||||
TP 2.0.0 publishes the final v2-only protocol. Missing or malformed structured
|
the native property ownership and hover identity guarantees required by this
|
||||||
participant capability stops Ebox loading instead of selecting a compatibility
|
release. Missing or malformed structured participant capability stops Ebox
|
||||||
writer.
|
loading instead of selecting a compatibility writer.
|
||||||
|
|
||||||
## Optional native module
|
## Optional native module
|
||||||
|
|
||||||
@ -136,6 +198,31 @@ exact Elisp fallback. Ebox never builds it while loading.
|
|||||||
(ebox-native-build)
|
(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
|
## Verification
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@ -151,3 +238,8 @@ make native-rust-tests
|
|||||||
See the [user guide](docs/user/ebox-user-guide.en.md), the [public API
|
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
|
reference](docs/user/ebox-api-reference.en.md), and the sibling
|
||||||
[ebox-playground](../ebox-playground/README.md) examples.
|
[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.
|
||||||
|
|||||||
@ -6,9 +6,18 @@ Component、响应式状态、behavior 或生命周期时,使用同级 ETAF
|
|||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
Ebox 需要 Emacs 29.1 或更高版本、ECSS 0.1.0 或更高版本,以及 TP 1.0.1 或
|
源码仓库为 [geekinney/ebox](https://gitea.gklazycat.heiyu.space/geekinney/ebox)。
|
||||||
更高版本。包管理器应自动安装声明的依赖。使用同级源码 checkout 时,把三个目录
|
当前 `3.0.0` 更新日志仍标记为未发布;源码 checkout 不代表已经发布的包归档或稳定版本 tag。
|
||||||
加入 `load-path` 后加载 Ebox:
|
|
||||||
|
| 依赖 | 使用条件 |
|
||||||
|
| --- | --- |
|
||||||
|
| 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
|
```elisp
|
||||||
(add-to-list 'load-path "/path/to/ecss")
|
(add-to-list 'load-path "/path/to/ecss")
|
||||||
@ -19,6 +28,48 @@ Ebox 需要 Emacs 29.1 或更高版本、ECSS 0.1.0 或更高版本,以及 TP
|
|||||||
|
|
||||||
加载 Ebox 不会创建 buffer,也不会构建 native 模块。
|
加载 Ebox 不会创建 buffer,也不会构建 native 模块。
|
||||||
|
|
||||||
|
使用 Knuth–Plass 段落排版时,先额外将 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 Knuth–Plass 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`、
|
普通用户只需理解七个 author 入口:字符串、`text`、`box`、`row`、`column`、
|
||||||
@ -80,7 +131,9 @@ padding 和 border,不包含该 Box 的 margin 和结构性换行符。悬停
|
|||||||
使用 `ebox-keymap-create` 将普通零参数回调绑定到激活键或自定义按键;鼠标命令会
|
使用 `ebox-keymap-create` 将普通零参数回调绑定到激活键或自定义按键;鼠标命令会
|
||||||
自动在被点击窗口的 buffer 中运行。`ebox-region-update` 可以使用当前已挂载 buffer
|
自动在被点击窗口的 buffer 中运行。`ebox-region-update` 可以使用当前已挂载 buffer
|
||||||
中的语义 ID,或显式 region handle,替换或清除节点能力。原生 keymap 可用于
|
中的语义 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 的
|
[交互指南](docs/user/ebox-user-guide.zh.md#native-node-interaction)和 Playground 的
|
||||||
[交互实验室](../ebox-playground/README.zh-CN.md#native-interaction-lab)。
|
[交互实验室](../ebox-playground/README.zh-CN.md#native-interaction-lab)。
|
||||||
|
|
||||||
@ -88,6 +141,7 @@ padding 和 border,不包含该 Box 的 margin 和结构性换行符。悬停
|
|||||||
|
|
||||||
- `ebox-render` 返回带属性文本,不发布 live buffer;
|
- `ebox-render` 返回带属性文本,不发布 live buffer;
|
||||||
- `ebox-render-to-buffer` 挂载 retained surface;
|
- `ebox-render-to-buffer` 挂载 retained surface;
|
||||||
|
- `ebox-display-buffer` 先完成渲染,再按 Emacs 显示规则与可选 action 显示结果,返回 buffer;
|
||||||
- `ebox-commit` 原子发布重新构建的 canonical input;
|
- `ebox-commit` 原子发布重新构建的 canonical input;
|
||||||
- `ebox-buffer-update-report` 返回最近一次成功更新报告;
|
- `ebox-buffer-update-report` 返回最近一次成功更新报告;
|
||||||
- `ebox-rerender-buffer-with-context` 应用显式 viewport 变化。
|
- `ebox-rerender-buffer-with-context` 应用显式 viewport 变化。
|
||||||
@ -103,8 +157,8 @@ Ebox 会在分配 runtime identity 前复制 canonical input,因此同一个 b
|
|||||||
卸载后查询会报错。识别提交版本时同时比较 mount ID 与 revision,避免重新挂载后
|
卸载后查询会报错。识别提交版本时同时比较 mount ID 与 revision,避免重新挂载后
|
||||||
revision 从头计数导致混淆。
|
revision 从头计数导致混淆。
|
||||||
|
|
||||||
Ebox 只通过 TP 的公开 structured participant API 注册可回滚状态。TP 1.0.1
|
Ebox 只通过 TP 2.0.0 的公开 structured participant API 与最终 v2 协议注册可回滚状态。
|
||||||
支持 consumer-first 迁移协议;TP 2.0.0 发布最终的 v2-only 协议。structured
|
旧的过渡版本不具备本版本要求的原生属性所有权与 hover identity 保证。structured
|
||||||
participant capability 缺失或格式错误时,Ebox 会停止加载,不会选择兼容 writer。
|
participant capability 缺失或格式错误时,Ebox 会停止加载,不会选择兼容 writer。
|
||||||
|
|
||||||
## 可选 native 模块
|
## 可选 native 模块
|
||||||
@ -117,6 +171,25 @@ fallback。Ebox 加载时不会自动构建它。
|
|||||||
(ebox-native-build)
|
(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
|
```sh
|
||||||
@ -132,3 +205,8 @@ make native-rust-tests
|
|||||||
继续阅读[用户指南](docs/user/ebox-user-guide.zh.md)、[公共 API
|
继续阅读[用户指南](docs/user/ebox-user-guide.zh.md)、[公共 API
|
||||||
参考](docs/user/ebox-api-reference.zh.md),以及同级
|
参考](docs/user/ebox-api-reference.zh.md),以及同级
|
||||||
[ebox-playground](../ebox-playground/README.md) 示例。
|
[ebox-playground](../ebox-playground/README.md) 示例。
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
Ebox 自有代码按 GPL-3.0-or-later 分发,详见 [LICENSE](LICENSE)。第三方文件保留各自的
|
||||||
|
许可证声明。
|
||||||
|
|||||||
@ -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`.
|
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
|
Native node capabilities are covered by `tests/ebox-interaction-tests.el` through
|
||||||
`make interaction-tests`. The interactive reference and its command regressions
|
`make interaction-tests`. The interactive reference and its command regressions
|
||||||
live in the sibling Playground package.
|
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-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-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-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
|
- Ebox registers every rollback-capable runtime mirror through TP's public
|
||||||
`tp-transaction-participate-v2` API. The loaded manifest must advertise that
|
`tp-transaction-participate-v2` API. The loaded manifest must advertise that
|
||||||
callable API with an accepted structured protocol; missing or malformed
|
callable API with an accepted structured protocol; missing or malformed
|
||||||
@ -162,6 +172,12 @@ make check
|
|||||||
make size-tests
|
make size-tests
|
||||||
make size-migration-tests
|
make size-migration-tests
|
||||||
make interaction-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 core-tests
|
||||||
make runtime-index-tests
|
make runtime-index-tests
|
||||||
make child-range-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
|
`make size-tests` includes `make size-migration-tests`, so the migration tool's
|
||||||
Python regression suite also runs through `make check`.
|
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
|
## Migrate literal author sizes
|
||||||
|
|
||||||
For old Ebox author literals, use the supported source-aware migration tool
|
For old Ebox author literals, use the supported source-aware migration tool
|
||||||
|
|||||||
@ -47,6 +47,13 @@ active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-cor
|
|||||||
|
|
||||||
Runtime index 行为由 `tests/ebox-runtime-index-tests.el` 覆盖。
|
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` 覆盖。
|
原生节点能力由 `tests/ebox-interaction-tests.el` 通过 `make interaction-tests` 覆盖。
|
||||||
可交互参考示例及其命令回归测试位于同级 Playground 包。
|
可交互参考示例及其命令回归测试位于同级 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 preparation;batch observation 留在更高层的 `ebox.el` orchestrator。
|
- `ebox-incremental.el` 同样不再包含任何 `ebox-surface` symbol 或 publication call。`ebox-surface.el` 只安装一个不可变只读 context port,提供 live client state、region mount、cascade proof 与 inline-style preparation;batch 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-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-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 的
|
- Ebox 始终通过 TP 的公开 `tp-transaction-participate-v2` API 注册可 rollback 的
|
||||||
runtime mirror。加载的 manifest 必须用 accepted structured protocol 声明该可调用
|
runtime mirror。加载的 manifest 必须用 accepted structured protocol 声明该可调用
|
||||||
API;capability 缺失或格式错误会在加载时 fail closed。
|
API;capability 缺失或格式错误会在加载时 fail closed。
|
||||||
@ -153,6 +160,12 @@ make check
|
|||||||
make size-tests
|
make size-tests
|
||||||
make size-migration-tests
|
make size-migration-tests
|
||||||
make interaction-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 core-tests
|
||||||
make runtime-index-tests
|
make runtime-index-tests
|
||||||
make child-range-tests
|
make child-range-tests
|
||||||
@ -189,6 +202,54 @@ make diff-check
|
|||||||
`make size-tests` 包含 `make size-migration-tests`,因此迁移工具的 Python 回归
|
`make size-tests` 包含 `make size-migration-tests`,因此迁移工具的 Python 回归
|
||||||
测试也会通过 `make check` 执行。
|
测试也会通过 `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 作者字面量使用已有的源码感知迁移工具,不另写一套搜索替换脚本。
|
旧 Ebox 作者字面量使用已有的源码感知迁移工具,不另写一套搜索替换脚本。
|
||||||
|
|||||||
@ -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
|
package. It separates the ordinary author path from the evaluated typed API
|
||||||
used by framework integrations. Names beginning with `ebox--` are private.
|
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
|
```elisp
|
||||||
(require 'ebox)
|
(require 'ebox)
|
||||||
@ -71,6 +75,14 @@ another public node or wrapper.
|
|||||||
| Text layout | `:text-align`, `:wrap-mode` (`word`, `char`, `kp`, `none`) |
|
| Text layout | `:text-align`, `:wrap-mode` (`word`, `char`, `kp`, `none`) |
|
||||||
| Overflow | `:overflow` (`visible`, `hidden`, `scroll`) |
|
| 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)`,
|
Geometry values use explicit `(unit number)` lists: `(px 240)`, `(% 50)`,
|
||||||
`(vw 100)`, `(vh 100)`, `(ch 80)`, and `(lh 3)`. Each property accepts only
|
`(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);
|
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
|
Raw native keymaps remain supported. Their mouse commands must obtain the
|
||||||
target window from the event and operate in that window's buffer themselves.
|
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
|
See the [interaction guide](ebox-user-guide.en.md#native-node-interaction) for
|
||||||
a runnable example.
|
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` | 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-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-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-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-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-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-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-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
|
`ebox-commit` accepts optional framework publish and rollback callbacks. They
|
||||||
join the existing atomic publication; failures restore the previous buffer,
|
join the existing atomic publication; failures restore the previous buffer,
|
||||||
surface, runtime state, and successful report.
|
surface, runtime state, and successful report.
|
||||||
@ -398,7 +431,8 @@ names every entry:
|
|||||||
`ebox-normal-layout-create`, `ebox-row-layout-create`,
|
`ebox-normal-layout-create`, `ebox-row-layout-create`,
|
||||||
`ebox-column-layout-create`, `ebox-flex-layout-create`,
|
`ebox-column-layout-create`, `ebox-flex-layout-create`,
|
||||||
`ebox-grid-layout-create`, `ebox-box-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`,
|
- Render/publication: `ebox-render`, `ebox-render-to-buffer`,
|
||||||
`ebox-unmount-buffer`, `ebox-display-buffer`, `ebox-commit`,
|
`ebox-unmount-buffer`, `ebox-display-buffer`, `ebox-commit`,
|
||||||
`ebox-buffer-set-observer`,
|
`ebox-buffer-set-observer`,
|
||||||
|
|||||||
@ -5,7 +5,10 @@
|
|||||||
本文描述独立 Ebox 包当前的公共边界,并把普通 author 路径与框架集成使用的
|
本文描述独立 Ebox 包当前的公共边界,并把普通 author 路径与框架集成使用的
|
||||||
evaluated typed API 分开。以 `ebox--` 开头的名称都是私有实现。
|
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
|
```elisp
|
||||||
(require 'ebox)
|
(require 'ebox)
|
||||||
@ -67,6 +70,11 @@ participation property 就是普通子 Box property,不会创建另一种公
|
|||||||
| 文本布局 | `:text-align`、`:wrap-mode`(`word`、`char`、`kp`、`none`) |
|
| 文本布局 | `:text-align`、`:wrap-mode`(`word`、`char`、`kp`、`none`) |
|
||||||
| Overflow | `:overflow`(`visible`、`hidden`、`scroll`) |
|
| Overflow | `:overflow`(`visible`、`hidden`、`scroll`) |
|
||||||
|
|
||||||
|
`hidden` 在组合子节点后约束内容宽度,保留完整的受支持文本簇;容器自身的 padding 与
|
||||||
|
border 不在裁剪区域内。被裁掉的子文本同时移除其属性。固定 display space 可以缩短,
|
||||||
|
放不下的字形/图像则整体省去,不做像素遮罩。纵向按完整行裁剪。`visible` 允许溢出,
|
||||||
|
`scroll` 提供纵向滚动;这些设置不隐含省略号或横向滚动。
|
||||||
|
|
||||||
几何值统一使用 `(单位 数值)`:`(px 240)`、`(% 50)`、`(vw 100)`、`(vh 100)`、
|
几何值统一使用 `(单位 数值)`:`(px 240)`、`(% 50)`、`(vw 100)`、`(vh 100)`、
|
||||||
`(ch 80)`、`(lh 3)`。每个属性只接受[统一单位约束表](ebox-user-guide.zh.md#size-unit-constraints)
|
`(ch 80)`、`(lh 3)`。每个属性只接受[统一单位约束表](ebox-user-guide.zh.md#size-unit-constraints)
|
||||||
规定的单位,不能跨几何轴使用。不接受 CSS 字符串、裸长度、单元素像素列表及旧的
|
规定的单位,不能跨几何轴使用。不接受 CSS 字符串、裸长度、单元素像素列表及旧的
|
||||||
@ -146,7 +154,16 @@ Emacs 设置及窗口系统;单独设置指针形状不会绑定命令。
|
|||||||
键盘回调在当前 buffer 中运行,由 point 所在位置进行原生分派。
|
键盘回调在当前 buffer 中运行,由 point 所在位置进行原生分派。
|
||||||
|
|
||||||
仍支持直接传入原生 keymap;其鼠标命令需要自行从事件取得目标窗口,并在该窗口的
|
仍支持直接传入原生 keymap;其鼠标命令需要自行从事件取得目标窗口,并在该窗口的
|
||||||
buffer 中操作。这些能力不引入 Ebox 焦点导航或应用状态系统。
|
buffer 中操作。
|
||||||
|
|
||||||
|
`ebox-next-interaction` 和 `ebox-previous-interaction` 是可选导航命令,均接受可选整数
|
||||||
|
count,默认为一;负数反向,零不移动。每个声明了含命令 keymap 的节点在其首个可访问
|
||||||
|
渲染字符处形成一个停靠点,包括继承覆盖;显式子 map 单独形成停靠点。从 owner 内部
|
||||||
|
移动时跳过该 owner。导航尊重 narrowing 与不可见文本,忽略普通 buffer、overlay 和
|
||||||
|
全局 map,在更新或滚动后使用当前已提交输出。目标不存在时抛出 `user-error`,point
|
||||||
|
不动;命令不循环跳转,也不绑定按键。不会求值菜单 enable 表达式或 filter,因此即使
|
||||||
|
应用动态禁用了菜单项,已声明命令仍可能保留停靠点。导航只移动 point;Ebox 不提供
|
||||||
|
独立的应用状态或焦点管理系统。
|
||||||
可运行示例见[交互指南](ebox-user-guide.zh.md#native-node-interaction)。
|
可运行示例见[交互指南](ebox-user-guide.zh.md#native-node-interaction)。
|
||||||
|
|
||||||
## 2. Evaluated typed construction
|
## 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` | 接收 single-root `CanonicalEboxInput`,返回带属性字符串,不发布 live buffer。 |
|
||||||
| `ebox-render-to-buffer` | 接收 single-root `CanonicalEboxInput`,挂载 retained TP surface,启用 `ebox-buffer-mode` 并返回 buffer;可选 plist 只接受 `:observer FUNCTION`。 |
|
| `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-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-commit` | 原子发布 single-root `CanonicalEboxInput` 或 `ebox-candidate-begin` 返回的 one-shot logical candidate。 |
|
||||||
| `ebox-buffer-set-observer` | 给已经 mounted 的 buffer 设置或移除一个 function observer。 |
|
| `ebox-buffer-set-observer` | 给已经 mounted 的 buffer 设置或移除一个 function observer。 |
|
||||||
| `ebox-buffer-update-report` | 返回最近一次成功更新报告的防御性副本。 |
|
| `ebox-buffer-update-report` | 返回最近一次成功更新报告的防御性副本。 |
|
||||||
| `ebox-rerender-buffer-with-context` | 应用显式 viewport width 与可选 height,同时保留 identity。 |
|
| `ebox-rerender-buffer-with-context` | 应用显式 viewport width 与可选 height,同时保留 identity。 |
|
||||||
| `ebox-viewport-window-width` | 返回 Ebox 对 window 使用的 display-safe 像素宽度。 |
|
| `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。它们加入现有原子发布;
|
`ebox-commit` 接受可选 framework publish/rollback callback。它们加入现有原子发布;
|
||||||
失败会恢复旧 buffer、surface、runtime state 与最近一次成功报告。
|
失败会恢复旧 buffer、surface、runtime state 与最近一次成功报告。
|
||||||
|
|
||||||
@ -355,7 +379,8 @@ native 模块只是加速器,不是正确性依赖。加载包不会构建它
|
|||||||
- 构造:`ebox-build`、`ebox-text-create`、`ebox-normal-layout-create`、
|
- 构造:`ebox-build`、`ebox-text-create`、`ebox-normal-layout-create`、
|
||||||
`ebox-row-layout-create`、`ebox-column-layout-create`、
|
`ebox-row-layout-create`、`ebox-column-layout-create`、
|
||||||
`ebox-flex-layout-create`、`ebox-grid-layout-create`、`ebox-box-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-render`、`ebox-render-to-buffer`、`ebox-unmount-buffer`、
|
||||||
`ebox-display-buffer`、`ebox-commit`、`ebox-buffer-set-observer`、`ebox-buffer-update-report`、
|
`ebox-display-buffer`、`ebox-commit`、`ebox-buffer-set-observer`、`ebox-buffer-update-report`、
|
||||||
`ebox-rerender-buffer-with-context`、`ebox-viewport-window-width`、
|
`ebox-rerender-buffer-with-context`、`ebox-viewport-window-width`、
|
||||||
|
|||||||
@ -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.
|
`33vw` of an 853-pixel viewport is 281.49 pixels before display quantization.
|
||||||
The buffer backend materializes block geometry in whole lines; fractional
|
The buffer backend materializes block geometry in whole lines; fractional
|
||||||
line targets are quantized at that boundary. Vertical `px` lengths are not
|
line targets are quantized at that boundary. Vertical `px` lengths are not
|
||||||
part of the author contract. `:overflow hidden` currently clips vertical lines; horizontal
|
part of the author contract. `:overflow hidden` clips excess vertical lines and
|
||||||
overflow is not pixel-clipped by the buffer backend, so content can extend
|
limits each content line to the measured content width. It retains the fitting
|
||||||
beyond the computed width.
|
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`,
|
Bare geometry numbers, one-element pixel lists, `viewport`, `viewport-height`,
|
||||||
`contain`, and parameterized `fit-content` are rejected. Even zero needs an
|
`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
|
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.
|
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 Knuth–Plass 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
|
||||||
|
"Knuth–Plass 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>
|
<a id="native-node-interaction"></a>
|
||||||
### Native node interaction
|
### 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
|
buffer for mouse callbacks, so a callback can directly update a semantic ID
|
||||||
with `(ebox-region-update "action" :help-echo "Activated")`.
|
with `(ebox-region-update "action" :help-echo "Activated")`.
|
||||||
|
|
||||||
Move point into the surface to use keyboard bindings. Raw native keymaps are
|
Move point into the surface to use keyboard bindings. Optional commands
|
||||||
also accepted; their mouse commands must handle the event's target buffer
|
`ebox-next-interaction` and `ebox-previous-interaction` move between declaring
|
||||||
themselves. Ebox uses native command dispatch; it does not create application
|
nodes that currently render a usable keymap. Each owner contributes one stop,
|
||||||
state or focus navigation. A hand pointer alone does not bind a click command.
|
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)
|
The Playground's [interaction lab](../../../ebox-playground/README.md#native-interaction-lab)
|
||||||
keeps its business functions and state in `interaction-reference.el`, with its
|
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)
|
(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:
|
Build a fresh canonical input and use `ebox-commit` for an atomic update:
|
||||||
|
|
||||||
```elisp
|
```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"))
|
"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
|
Visible mounted buffers follow one chosen display window. Each mounted buffer
|
||||||
explicit viewport with:
|
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
|
```elisp
|
||||||
(ebox-rerender-buffer-with-context
|
(ebox-rerender-buffer-with-context
|
||||||
|
|||||||
@ -207,8 +207,16 @@ Box 高度为零;`(box :height (lh 1))` 明确保留一行空白。
|
|||||||
中间结果保留小数,例如 853 像素视口的 `33vw` 在显示量化前是 281.49 像素。
|
中间结果保留小数,例如 853 像素视口的 `33vw` 在显示量化前是 281.49 像素。
|
||||||
当前 buffer 后端按完整行生成 block 几何,小数行目标在这个边界量化。作者接口不
|
当前 buffer 后端按完整行生成 block 几何,小数行目标在这个边界量化。作者接口不
|
||||||
接受纵向 `px` 长度。
|
接受纵向 `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`,
|
不再接受几何裸数字、单元素像素列表、`viewport`、`viewport-height`、`contain`,
|
||||||
以及带参数的 `fit-content`。零长度也需要对应轴允许的单位,例如横向 `(px 0)`、
|
以及带参数的 `fit-content`。零长度也需要对应轴允许的单位,例如横向 `(px 0)`、
|
||||||
@ -234,6 +242,29 @@ Text 的样式声明只接受字体、前景/背景和文本装饰 property;
|
|||||||
用 `:outer inline` 或 `:outer block` 表达 Box 如何参与父布局。子布局算法仍由 form
|
用 `:outer inline` 或 `:outer block` 表达 Box 如何参与父布局。子布局算法仍由 form
|
||||||
名称决定。
|
名称决定。
|
||||||
|
|
||||||
|
### 换行与可选 EKP 依赖
|
||||||
|
|
||||||
|
`:wrap-mode word` 按普通单词换行,并允许 CJK 字符换行;`char` 按 Ebox 支持的常见
|
||||||
|
文本簇边界换行;`none` 保留显式换行符,不添加软换行。`kp` 将段落交给独立的
|
||||||
|
[EKP 包](https://github.com/Kinneyzhang/emacs-kp),使用 Knuth–Plass 算法排版。
|
||||||
|
换行模式属于 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
|
||||||
|
"Knuth–Plass considers the paragraph when choosing line breaks.")))
|
||||||
|
```
|
||||||
|
|
||||||
|
Ebox 的文本簇处理会保留常见组合音标、变体选择符、Emoji 修饰符、ZWJ 序列和区域指示符
|
||||||
|
配对,但不实现完整 Unicode 字素分段或浏览器级双向布局。字形塑造和字形是否可用,
|
||||||
|
仍取决于 Emacs、所选字体和窗口系统。
|
||||||
|
|
||||||
<a id="native-node-interaction"></a>
|
<a id="native-node-interaction"></a>
|
||||||
### 原生节点交互
|
### 原生节点交互
|
||||||
|
|
||||||
@ -284,9 +315,20 @@ margin 及结构性换行符;悬停遵循上面的规则。嵌套的 Text 或
|
|||||||
辅助函数让鼠标回调在事件窗口的 buffer 中运行,因此回调可以直接按语义 ID 更新:
|
辅助函数让鼠标回调在事件窗口的 buffer 中运行,因此回调可以直接按语义 ID 更新:
|
||||||
`(ebox-region-update "action" :help-echo "Activated")`。
|
`(ebox-region-update "action" :help-echo "Activated")`。
|
||||||
|
|
||||||
使用键盘绑定时,先把 point 移到交互范围内。仍可直接传入原生 keymap,其鼠标命令
|
使用键盘绑定时,先把 point 移到交互范围内。可选命令 `ebox-next-interaction` 与
|
||||||
需要自行处理事件的目标 buffer。Ebox 使用原生命令分派,不创建应用状态或焦点导航。
|
`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)
|
Playground 的[交互实验室](../../../ebox-playground/README.zh-CN.md#native-interaction-lab)
|
||||||
把业务函数与状态放在 `interaction-reference.el`,把布局放在
|
把业务函数与状态放在 `interaction-reference.el`,把布局放在
|
||||||
@ -304,6 +346,18 @@ retained TP surface 并返回 buffer。它们和 `ebox-display-buffer` 都接收
|
|||||||
(ebox-render-to-buffer "*Ebox Guide*" ebox-guide-input)
|
(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` 原子更新:
|
重新构建 canonical input 后,用 `ebox-commit` 原子更新:
|
||||||
|
|
||||||
```elisp
|
```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"))
|
"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
|
```elisp
|
||||||
(ebox-rerender-buffer-with-context
|
(ebox-rerender-buffer-with-context
|
||||||
|
|||||||
@ -16,6 +16,8 @@
|
|||||||
(require 'ebox-layout)
|
(require 'ebox-layout)
|
||||||
(require 'ebox-flex)
|
(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-node-id "ebox" (node))
|
||||||
(declare-function ebox--ensure-region-id "ebox" (box))
|
(declare-function ebox--ensure-region-id "ebox" (box))
|
||||||
(declare-function ebox-box-node-p "ebox-canonical" (node))
|
(declare-function ebox-box-node-p "ebox-canonical" (node))
|
||||||
|
|||||||
@ -49,6 +49,7 @@
|
|||||||
(declare-function tp-object-mounts
|
(declare-function tp-object-mounts
|
||||||
"tp-surface" (object))
|
"tp-surface" (object))
|
||||||
(declare-function ebox-string-lines "ebox" (string))
|
(declare-function ebox-string-lines "ebox" (string))
|
||||||
|
(declare-function ebox-lines-join "ebox" (lines))
|
||||||
(declare-function ebox-get "ebox" (box key))
|
(declare-function ebox-get "ebox" (box key))
|
||||||
(declare-function ebox--ensure-node-id "ebox" (node))
|
(declare-function ebox--ensure-node-id "ebox" (node))
|
||||||
(declare-function ebox--ensure-region-id "ebox" (box))
|
(declare-function ebox--ensure-region-id "ebox" (box))
|
||||||
|
|||||||
@ -20,6 +20,10 @@
|
|||||||
(declare-function ebox--ensure-region-id "ebox" (box))
|
(declare-function ebox--ensure-region-id "ebox" (box))
|
||||||
(declare-function ebox-runtime-index-get "ebox-runtime-index" (key index &optional default))
|
(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-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)
|
(defvar ebox-buffer--decoration-style-map)
|
||||||
|
|
||||||
(defconst ebox-interaction-properties
|
(defconst ebox-interaction-properties
|
||||||
@ -103,6 +107,136 @@ This function does not supply focus navigation, state or event propagation."
|
|||||||
(push key keys))))
|
(push key keys))))
|
||||||
map))
|
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
|
;;;###autoload
|
||||||
(defun ebox-help-create (function)
|
(defun ebox-help-create (function)
|
||||||
"Adapt zero-argument FUNCTION to a native `help-echo' callback.
|
"Adapt zero-argument FUNCTION to a native `help-echo' callback.
|
||||||
|
|||||||
122
ebox-layout.el
122
ebox-layout.el
@ -18,6 +18,7 @@
|
|||||||
(require 'ebox-buffer-backend)
|
(require 'ebox-buffer-backend)
|
||||||
(require 'ebox-runtime-index)
|
(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 "ebox-grid" (node))
|
||||||
(declare-function ebox--render-grid-box-children
|
(declare-function ebox--render-grid-box-children
|
||||||
"ebox-grid" (box props children))
|
"ebox-grid" (box props children))
|
||||||
@ -85,6 +86,9 @@
|
|||||||
|
|
||||||
(defvar ebox--region-id-counter)
|
(defvar ebox--region-id-counter)
|
||||||
(defvar ebox--runtime-node-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--region-box-table)
|
||||||
(defvar ebox--render-region-id)
|
(defvar ebox--render-region-id)
|
||||||
(defvar ebox--defer-scroll-content-index)
|
(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))))
|
(when (> width max-width) (setq max-width width))))
|
||||||
(max max-width (ebox--space-pixel-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)
|
(defun ebox--wrap-line (line max-pixel wrap-mode)
|
||||||
"Wrap a single LINE to MAX-PIXEL width, preserving text properties.
|
"Wrap a single LINE to MAX-PIXEL width, preserving text properties.
|
||||||
WRAP-MODE can be `word', `char', or `kp'.
|
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))
|
((memq wrap-mode '(nil none))
|
||||||
text)
|
text)
|
||||||
((eq wrap-mode 'kp)
|
((eq wrap-mode 'kp)
|
||||||
(if (and (require 'ekp nil t)
|
(unless (require 'ekp nil t)
|
||||||
(fboundp 'ekp-pixel-justify))
|
(error "Ebox :wrap-mode kp requires EKP; install the ekp package"))
|
||||||
(ekp-pixel-justify text max-pixel)
|
(unless (fboundp 'ekp-pixel-justify)
|
||||||
(ebox--wrap-text text max-pixel 'word)))
|
(error "Ebox :wrap-mode kp requires EKP with ekp-pixel-justify; update EKP"))
|
||||||
|
(ekp-pixel-justify text max-pixel))
|
||||||
(t
|
(t
|
||||||
(let* ((lines (ebox-string-lines text))
|
(let* ((lines (ebox-string-lines text))
|
||||||
(wrapped (mapcan (lambda (l)
|
(wrapped (mapcan (lambda (l)
|
||||||
@ -1630,32 +1685,39 @@ one carrier line preserves their paint without inventing a content line."
|
|||||||
|
|
||||||
(defun ebox--format-content-string (box content)
|
(defun ebox--format-content-string (box content)
|
||||||
"Format BOX CONTENT (wrap and justify) returning a string."
|
"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))
|
(align (ebox-get box :text-align))
|
||||||
(known-width
|
(known-width (gethash content ebox--rendered-uniform-width-table))
|
||||||
(gethash content ebox--rendered-uniform-width-table)))
|
(formatted
|
||||||
(if (and (eq align 'left)
|
(if (and (eq align 'left)
|
||||||
(or (and (ebox-style-no-soft-wrap-p wrap-mode)
|
(or (and (ebox-style-no-soft-wrap-p wrap-mode)
|
||||||
(or (plist-get box :ebox-content-width-exact-p)
|
(or (plist-get box :ebox-content-width-exact-p)
|
||||||
(and (numberp known-width)
|
(and (numberp known-width)
|
||||||
(= known-width
|
(= known-width (ebox--content-pixel box)))))
|
||||||
(ebox--content-pixel box)))))
|
(and (memq wrap-mode '(word char))
|
||||||
(and (memq wrap-mode '(word char))
|
(numberp known-width)
|
||||||
(numberp known-width)
|
(= known-width (ebox--content-pixel box)))))
|
||||||
(= known-width (ebox--content-pixel box)))))
|
content
|
||||||
content
|
(let* ((pixel-width (ebox--content-pixel box))
|
||||||
(let* ((pixel-width (ebox--content-pixel box))
|
(wrapped-content
|
||||||
(wrapped-content
|
(if (plist-get box :ebox-content-layout-complete-p)
|
||||||
(if (plist-get box :ebox-content-layout-complete-p)
|
content
|
||||||
content
|
(ebox--wrap-text content pixel-width wrap-mode))))
|
||||||
(ebox--wrap-text content pixel-width wrap-mode))))
|
;; KP distributes inner gaps; Ebox still owns the Box geometry.
|
||||||
;; kp mode distributes inner gaps, but Ebox still owns the box geometry.
|
;; Pad ragged lines back to the requested content width.
|
||||||
;; Pad ragged lines (notably the last line) back to the content width
|
(ebox--record-rendered-intrinsic-size
|
||||||
;; while preserving the requested alignment for those lines.
|
(ebox--lines-justify wrapped-content pixel-width align)
|
||||||
(ebox--record-rendered-intrinsic-size
|
(ebox--rendered-intrinsic-size content :width)
|
||||||
(ebox--lines-justify wrapped-content pixel-width align)
|
(ebox--rendered-intrinsic-size content :height))))))
|
||||||
(ebox--rendered-intrinsic-size content :width)
|
(if (eq (ebox-get box :overflow) 'hidden)
|
||||||
(ebox--rendered-intrinsic-size content :height))))))
|
(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)
|
(defun ebox--format-content (box)
|
||||||
"Format BOX content (wrap and justify) returning a string."
|
"Format BOX content (wrap and justify) returning a string."
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
(declare-function ebox--register-render-owned-text-value
|
(declare-function ebox--register-render-owned-text-value
|
||||||
"ebox-render-context" (property value))
|
"ebox-render-context" (property value))
|
||||||
(declare-function ebox-string-lines "ebox" (string))
|
(declare-function ebox-string-lines "ebox" (string))
|
||||||
|
(declare-function ebox-lines-join "ebox" (lines))
|
||||||
(declare-function ebox--string-repeat-lines "ebox" (string count))
|
(declare-function ebox--string-repeat-lines "ebox" (string count))
|
||||||
|
|
||||||
;; Measurement Cache Model:
|
;; 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)
|
(defun ebox--display-space-width (display)
|
||||||
"Return fixed pixel width for DISPLAY space specs, or nil."
|
"Return fixed pixel width for DISPLAY space specs, or nil."
|
||||||
(when (and (consp display) (eq (car display) 'space))
|
(ebox--fixed-pixel-space-width display))
|
||||||
(let ((width (plist-get (cdr display) :width)))
|
|
||||||
(cond
|
|
||||||
((numberp width) width)
|
|
||||||
((and (consp width) (numberp (car width))) (car width))))))
|
|
||||||
|
|
||||||
(defun 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.
|
"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
|
(width (ebox--fixed-pixel-space-width
|
||||||
(get-text-property position 'display string))))
|
(get-text-property position 'display string))))
|
||||||
(when width
|
(when width
|
||||||
(cl-loop for index from position below next
|
(push (cons position next) positions)
|
||||||
do (push index positions) (push width widths)))
|
(push width widths))
|
||||||
(setq position next)))
|
(setq position next)))
|
||||||
(cl-mapc
|
(cl-mapc
|
||||||
(lambda (index used)
|
(lambda (range used)
|
||||||
(let ((display (get-text-property index 'display string)))
|
(let ((display (get-text-property (car range) 'display string)))
|
||||||
(unless (= used (ebox--fixed-pixel-space-width display))
|
(unless (= used (ebox--fixed-pixel-space-width display))
|
||||||
(let ((updated (cons 'space (copy-sequence (cdr display)))))
|
(let ((updated (cons 'space (copy-sequence (cdr display)))))
|
||||||
(setcdr updated (plist-put (cdr updated) :width (list used)))
|
(setcdr updated (plist-put (cdr updated) :width (list used)))
|
||||||
(put-text-property
|
(put-text-property
|
||||||
index (1+ index) 'display
|
(car range) (cdr range) 'display
|
||||||
(ebox--register-render-owned-text-value 'display updated)
|
(ebox--register-render-owned-text-value 'display updated)
|
||||||
result)))))
|
result)))))
|
||||||
(nreverse positions) (ebox-size-quantize (nreverse widths)))
|
(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;
|
;; display-space fractions just as the segmented path does;
|
||||||
;; Emacs's glyph measurement quantizes them before padding.
|
;; Emacs's glyph measurement quantizes them before padding.
|
||||||
(width (if space-width
|
(width (if space-width
|
||||||
(* space-width (length sample))
|
space-width
|
||||||
(ebox--scaled-string-pixel-width sample factor)))
|
(ebox--scaled-string-pixel-width sample factor)))
|
||||||
(index ebox--string-pixel-width-cache-ring-index)
|
(index ebox--string-pixel-width-cache-ring-index)
|
||||||
(evicted (aref 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)
|
(ebox--cached-string-pixel-width line factor cache-key)
|
||||||
(while (< pos end)
|
(while (< pos end)
|
||||||
(let* ((display (get-text-property pos 'display line))
|
(let* ((display (get-text-property pos 'display line))
|
||||||
|
(space-width (ebox--display-space-width display))
|
||||||
(next
|
(next
|
||||||
(cl-loop for property in
|
(if (or (stringp display)
|
||||||
'(face display)
|
(memq (car-safe display) '(space image)))
|
||||||
minimize (or (next-single-property-change
|
;; Emacs replaces an entire EQ display run once. Face
|
||||||
pos property line end)
|
;; boundaries inside it do not create additional glyphs.
|
||||||
end)))
|
(next-single-property-change pos 'display line end)
|
||||||
(space-width (ebox--display-space-width display)))
|
(cl-loop for property in '(face display)
|
||||||
|
minimize (or (next-single-property-change
|
||||||
|
pos property line end)
|
||||||
|
end)))))
|
||||||
(setq total
|
(setq total
|
||||||
(+ total
|
(+ total
|
||||||
(if space-width
|
(if space-width
|
||||||
(* space-width (- next pos))
|
space-width
|
||||||
;; The segment ends at every visual property change.
|
;; The segment ends at every visual property change.
|
||||||
;; Build only its property-free cache key on hits; a
|
;; Build only its property-free cache key on hits; a
|
||||||
;; propertized substring is materialized solely on miss.
|
;; propertized substring is materialized solely on miss.
|
||||||
|
|||||||
@ -18,6 +18,9 @@
|
|||||||
(require 'ebox-runtime-index)
|
(require 'ebox-runtime-index)
|
||||||
(require 'ebox-size)
|
(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-version "ebox_native_reflow" ())
|
||||||
(declare-function ebox-native--module-layout-ready-p "ebox_native_reflow" ())
|
(declare-function ebox-native--module-layout-ready-p "ebox_native_reflow" ())
|
||||||
(declare-function ebox-native--module-create-session "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-stats "ebox_native_reflow" (session))
|
||||||
(declare-function ebox-native--module-release "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-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--format-content-string "ebox-layout" (box content))
|
||||||
(declare-function ebox-style-no-soft-wrap-p "ebox-style" (value))
|
(declare-function ebox-style-no-soft-wrap-p "ebox-style" (value))
|
||||||
(declare-function ebox-style-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
|
(defvar ebox-native-reflow--compile-size-node nil
|
||||||
"Owning node whose font measurements resolve fixed native size inputs.")
|
"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.")
|
"Version tuple shared by the native module, layout IR, and render tape.")
|
||||||
|
|
||||||
(defconst ebox-native-reflow--minimum-rust-version "1.82.0"
|
(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))
|
(clrhash ebox-native-reflow--source-cluster-cache))
|
||||||
(puthash cache-key measured
|
(puthash cache-key measured
|
||||||
ebox-native-reflow--source-cluster-cache))
|
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))
|
(dolist (cluster (append measured nil))
|
||||||
(push (append (copy-sequence cluster)
|
(push (append (copy-sequence cluster)
|
||||||
(when template-id
|
(when template-id
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
(declare-function ebox--ensure-region-id "ebox" (box))
|
(declare-function ebox--ensure-region-id "ebox" (box))
|
||||||
(declare-function ebox--buffer-root-node "ebox-incremental" (buffer))
|
(declare-function ebox--buffer-root-node "ebox-incremental" (buffer))
|
||||||
(declare-function ebox--buffer-render-state "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-id-table "ebox-incremental" (buffer))
|
||||||
(declare-function ebox--buffer-selector-class-table
|
(declare-function ebox--buffer-selector-class-table
|
||||||
"ebox-incremental" (buffer))
|
"ebox-incremental" (buffer))
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
(require 'ebox-surface)
|
(require 'ebox-surface)
|
||||||
(require 'tp-transaction)
|
(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.")
|
"Minimum TP package version required by Ebox framework SPI v2.")
|
||||||
|
|
||||||
(defconst ebox-framework-spi-supported-tp-protocols
|
(defconst ebox-framework-spi-supported-tp-protocols
|
||||||
|
|||||||
@ -24,6 +24,8 @@
|
|||||||
(require 'tp)
|
(require 'tp)
|
||||||
|
|
||||||
(defvar ebox-region-types)
|
(defvar ebox-region-types)
|
||||||
|
(defvar ebox--region-id-counter)
|
||||||
|
(defvar ebox--runtime-node-id-counter)
|
||||||
(defvar ebox--region-box-table)
|
(defvar ebox--region-box-table)
|
||||||
(defvar ebox--scroll-global-state)
|
(defvar ebox--scroll-global-state)
|
||||||
(defvar ebox--paint-origin-property)
|
(defvar ebox--paint-origin-property)
|
||||||
@ -49,16 +51,24 @@
|
|||||||
(defvar ebox-incremental--allocated-slot-proof-cache)
|
(defvar ebox-incremental--allocated-slot-proof-cache)
|
||||||
(defvar ebox-incremental--buffer-render-state-override)
|
(defvar ebox-incremental--buffer-render-state-override)
|
||||||
(declare-function tp-runtime-manifest "tp" ())
|
(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))
|
(declare-function tp-transaction-participate-v2 "tp-reactive" (&rest args))
|
||||||
|
|
||||||
(define-error 'ebox-surface-tp-protocol-error
|
(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)
|
(defun ebox-surface--validate-tp-v2-capability (manifest)
|
||||||
"Validate MANIFEST as the required public TP v2 participant capability."
|
"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)))
|
(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-v1+v2
|
||||||
tp-transaction-protocol-v2))
|
tp-transaction-protocol-v2))
|
||||||
(eq api 'tp-transaction-participate-v2)
|
(eq api 'tp-transaction-participate-v2)
|
||||||
|
|||||||
26
ebox.el
26
ebox.el
@ -1,10 +1,16 @@
|
|||||||
;;; ebox.el --- Pixel-perfect box model renderer -*- lexical-binding: t -*-
|
;;; ebox.el --- Pixel-perfect box model renderer -*- lexical-binding: t -*-
|
||||||
|
|
||||||
|
;; SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
;; Author: ebox contributors
|
;; Author: ebox contributors
|
||||||
;; Version: 3.0.0
|
;; 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
|
;; 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:
|
;;; Commentary:
|
||||||
|
|
||||||
@ -4908,11 +4914,17 @@ through dirty-set and patch-set execution before falling back to root rerender."
|
|||||||
(plist-get state :viewport-height))))
|
(plist-get state :viewport-height))))
|
||||||
|
|
||||||
;;;###autoload
|
;;;###autoload
|
||||||
(defun ebox-display-buffer (buffer-or-name input)
|
(defun ebox-display-buffer (buffer-or-name input &optional action)
|
||||||
"Render canonical INPUT to BUFFER-OR-NAME through TP and display it."
|
"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))
|
(declare (indent 1))
|
||||||
(delete-other-windows)
|
(let ((buffer (ebox-render-to-buffer buffer-or-name input)))
|
||||||
(switch-to-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
|
(defconst ebox-public-api
|
||||||
'(ebox-buffer-mode
|
'(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-host-ref-position
|
||||||
ebox-help-create
|
ebox-help-create
|
||||||
ebox-keymap-create
|
ebox-keymap-create
|
||||||
|
ebox-next-interaction
|
||||||
|
ebox-previous-interaction
|
||||||
ebox-native-build
|
ebox-native-build
|
||||||
ebox-native-status
|
ebox-native-status
|
||||||
ebox-region-ids
|
ebox-region-ids
|
||||||
|
|||||||
@ -3,6 +3,7 @@ name = "ebox-native-reflow"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.82"
|
rust-version = "1.82"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|||||||
@ -164,7 +164,7 @@ ebox_module_version(emacs_env *env, ptrdiff_t nargs, emacs_value *args,
|
|||||||
(void) nargs;
|
(void) nargs;
|
||||||
(void) args;
|
(void) args;
|
||||||
(void) data;
|
(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));
|
return env->make_string(env, version, (ptrdiff_t) (sizeof version - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ name = "etaf-core"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.82"
|
rust-version = "1.82"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|||||||
@ -2079,6 +2079,35 @@ impl Line {
|
|||||||
self
|
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 {
|
fn blank(width: i64) -> Self {
|
||||||
Self::blank_with_properties(width, AtomProperties::default())
|
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(
|
formatted = project_box_lines(
|
||||||
scope,
|
scope,
|
||||||
BoxProjectionSlot::ContentOwn,
|
BoxProjectionSlot::ContentOwn,
|
||||||
@ -8739,6 +8775,118 @@ mod tests {
|
|||||||
assert!(character_lines.iter().all(|line| line.width <= 3));
|
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]
|
#[test]
|
||||||
fn source_template_ids_follow_measured_clusters() {
|
fn source_template_ids_follow_measured_clusters() {
|
||||||
let root = text_box(
|
let root = text_box(
|
||||||
|
|||||||
19
release-dependencies.json
Normal file
19
release-dependencies.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
145
scripts/ebox-release-smoke.el
Normal file
145
scripts/ebox-release-smoke.el
Normal 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
239
scripts/ebox-release.py
Normal 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)
|
||||||
100
scripts/test_ebox_release.py
Normal file
100
scripts/test_ebox_release.py
Normal 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()
|
||||||
@ -56,8 +56,15 @@
|
|||||||
(ert-deftest ebox-ci-workflow-uses-local-entry-points ()
|
(ert-deftest ebox-ci-workflow-uses-local-entry-points ()
|
||||||
"GitHub Actions should call the same local checks as maintainers."
|
"GitHub Actions should call the same local checks as maintainers."
|
||||||
(let ((source (ebox-ci-test--read ".github/workflows/ci.yml")))
|
(let ((source (ebox-ci-test--read ".github/workflows/ci.yml")))
|
||||||
(should (string-match-p "emacs-nox" source))
|
(should (string-match-p "setup-emacs@" source))
|
||||||
(should (string-match-p "Kinneyzhang/tp" 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 "make ci" source))
|
||||||
(should (string-match-p (regexp-quote "cargo +stable test") source))
|
(should (string-match-p (regexp-quote "cargo +stable test") source))
|
||||||
(should (string-match-p (regexp-quote "cargo +1.82.0 test") source))))
|
(should (string-match-p (regexp-quote "cargo +1.82.0 test") source))))
|
||||||
|
|||||||
@ -1744,29 +1744,39 @@
|
|||||||
(should (= (ebox-commit-test--participant-v2-count) 2)))
|
(should (= (ebox-commit-test--participant-v2-count) 2)))
|
||||||
|
|
||||||
(ert-deftest ebox-transaction-participant-accepts-structured-protocols ()
|
(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
|
(dolist (protocol '(tp-transaction-protocol-v1+v2
|
||||||
tp-transaction-protocol-v2))
|
tp-transaction-protocol-v2))
|
||||||
(should
|
(should
|
||||||
(ebox-surface--validate-tp-v2-capability
|
(ebox-surface--validate-tp-v2-capability
|
||||||
(list :transaction-protocol protocol
|
(list :version "2.0.0" :transaction-protocol protocol
|
||||||
:structured-participant-api
|
:structured-participant-api
|
||||||
'tp-transaction-participate-v2)))))
|
'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 ()
|
(ert-deftest ebox-transaction-participant-rejects-missing-or-malformed-v2 ()
|
||||||
"Missing or incompatible structured capabilities fail closed."
|
"Missing or incompatible structured capabilities fail closed."
|
||||||
(should-error
|
(should-error
|
||||||
(ebox-surface--validate-tp-v2-capability
|
(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)
|
:type 'ebox-surface-tp-protocol-error)
|
||||||
(should-error
|
(should-error
|
||||||
(ebox-surface--validate-tp-v2-capability
|
(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))
|
:structured-participant-api ignore))
|
||||||
:type 'ebox-surface-tp-protocol-error)
|
:type 'ebox-surface-tp-protocol-error)
|
||||||
(should-error
|
(should-error
|
||||||
(ebox-surface--validate-tp-v2-capability
|
(ebox-surface--validate-tp-v2-capability
|
||||||
'(:transaction-protocol incompatible
|
'(:version "2.0.0" :transaction-protocol incompatible
|
||||||
:structured-participant-api tp-transaction-participate-v2))
|
:structured-participant-api tp-transaction-participate-v2))
|
||||||
:type 'ebox-surface-tp-protocol-error))
|
:type 'ebox-surface-tp-protocol-error))
|
||||||
|
|
||||||
|
|||||||
@ -4526,12 +4526,19 @@
|
|||||||
(ebox--render-source-index (ebox-test-source-index box-input))
|
(ebox--render-source-index (ebox-test-source-index box-input))
|
||||||
(content (ebox-pixel-space 120))
|
(content (ebox-pixel-space 120))
|
||||||
(wrap-count 0)
|
(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))
|
(ebox--record-rendered-uniform-width content (nth 2 case))
|
||||||
(cl-letf (((symbol-function 'ebox--wrap-text)
|
(cl-letf (((symbol-function 'ebox--wrap-text)
|
||||||
(lambda (&rest arguments)
|
(lambda (&rest arguments)
|
||||||
(cl-incf wrap-count)
|
(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))
|
(ebox--format-content-string box content))
|
||||||
(should (> wrap-count 0)))))
|
(should (> wrap-count 0)))))
|
||||||
|
|
||||||
@ -7001,6 +7008,117 @@ line compositor's complete property output, including empty lines and chrome."
|
|||||||
'(paint-patch)))
|
'(paint-patch)))
|
||||||
(should-not (plist-get report :full-rerender))))))
|
(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 ()
|
(ert-deftest ebox-overflow-hidden-clips-extra-content-lines ()
|
||||||
(ebox-test--reset-runtime-state)
|
(ebox-test--reset-runtime-state)
|
||||||
(let* ((box (ebox-test-box (ebox-test-text "A\nB\nC")
|
(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))
|
'char))
|
||||||
(list china usa)))))
|
(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 ()
|
(ert-deftest ebox-kp-wrap-mode-invokes-ekp-justification ()
|
||||||
(ebox-test--reset-runtime-state)
|
(ebox-test--reset-runtime-state)
|
||||||
(let (require-called justify-args)
|
(let (require-called justify-args)
|
||||||
@ -13363,6 +13528,30 @@ explicit visible-window handoff may admit its full-content publication."
|
|||||||
(should require-called)
|
(should require-called)
|
||||||
(should (equal justify-args (list "abc" 100)))))
|
(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 ()
|
(ert-deftest ebox-kp-wrap-mode-respects-text-align ()
|
||||||
"KP-wrapped lines should honor the box's horizontal text alignment."
|
"KP-wrapped lines should honor the box's horizontal text alignment."
|
||||||
(ebox-test--reset-runtime-state)
|
(ebox-test--reset-runtime-state)
|
||||||
|
|||||||
@ -475,8 +475,8 @@
|
|||||||
:column-gap (% 25) (box "A") (box "B"))))))
|
:column-gap (% 25) (box "A") (box "B"))))))
|
||||||
(should (= (ebox-string-height vertical) (if (eq overflow 'hidden) 8 10)))
|
(should (= (ebox-string-height vertical) (if (eq overflow 'hidden) 8 10)))
|
||||||
(should (= (ebox--rendered-intrinsic-size horizontal :width) 80))
|
(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)
|
||||||
(should (= (ebox--string-max-pixel-width horizontal) 100)))))
|
(if (eq overflow 'hidden) 80 100))))))
|
||||||
|
|
||||||
(ert-deftest ebox-css-flex-preserves-nested-grid-intrinsic-extents ()
|
(ert-deftest ebox-css-flex-preserves-nested-grid-intrinsic-extents ()
|
||||||
"A Grid child's cyclic gap spill does not enlarge its Flex layout size."
|
"A Grid child's cyclic gap spill does not enlarge its Flex layout size."
|
||||||
|
|||||||
@ -90,9 +90,15 @@
|
|||||||
21)))
|
21)))
|
||||||
|
|
||||||
(ert-deftest ebox-css-layout-pixel-quantization-preserves-private-input ()
|
(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))
|
(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))
|
(before (copy-sequence source))
|
||||||
(result (ebox--quantize-pixel-spaces source)))
|
(result (ebox--quantize-pixel-spaces source)))
|
||||||
(should (equal-including-properties source before))
|
(should (equal-including-properties source before))
|
||||||
|
|||||||
73
tests/ebox-display-tests.el
Normal file
73
tests/ebox-display-tests.el
Normal 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
|
||||||
@ -23,7 +23,7 @@
|
|||||||
"Public documents that must describe only the canonical author grammar.")
|
"Public documents that must describe only the canonical author grammar.")
|
||||||
|
|
||||||
(defconst ebox-docs-test--active-files
|
(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.el" "ebox-cache.el" "ebox-runtime-index.el" "ebox-interaction.el"
|
||||||
"ebox-state-contract.el" "ebox-size.el" "ebox-style.el"
|
"ebox-state-contract.el" "ebox-size.el" "ebox-style.el"
|
||||||
"ebox-child-range.el" "ebox-tree.el"
|
"ebox-child-range.el" "ebox-tree.el"
|
||||||
@ -33,7 +33,7 @@
|
|||||||
"ebox-surface.el" "ebox-dsl.el" "ebox-spi.el"
|
"ebox-surface.el" "ebox-dsl.el" "ebox-spi.el"
|
||||||
"ebox-selector.el" "ebox-native-reflow.el"
|
"ebox-selector.el" "ebox-native-reflow.el"
|
||||||
"tests/ebox-size-tests.el" "tests/ebox-size-style-tests.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-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-layout-tests.el" "tests/ebox-css-flex-grid-tests.el"
|
||||||
"tests/ebox-css-viewport-tests.el"
|
"tests/ebox-css-viewport-tests.el"
|
||||||
@ -54,12 +54,16 @@
|
|||||||
"native/src/layout.rs" "native/c/ebox_module.c"
|
"native/src/layout.rs" "native/c/ebox_module.c"
|
||||||
"scripts/ebox-package-lint.el" "scripts/ebox-visual-check.el"
|
"scripts/ebox-package-lint.el" "scripts/ebox-visual-check.el"
|
||||||
"scripts/ebox-performance-evaluator.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")
|
"scripts/migrate-css-sizes.py" "scripts/test_migrate_css_sizes.py")
|
||||||
"Active Ebox files that the maintainer map must cover.")
|
"Active Ebox files that the maintainer map must cover.")
|
||||||
|
|
||||||
(defconst ebox-docs-test--targets
|
(defconst ebox-docs-test--targets
|
||||||
'("check" "ci" "load" "compile" "size-tests" "size-migration-tests"
|
'("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"
|
"child-range-tests" "grid-tests"
|
||||||
"ebox-commit-tests" "surface-tests" "visual-check-tests" "package-tests"
|
"ebox-commit-tests" "surface-tests" "visual-check-tests" "package-tests"
|
||||||
"selector-tests" "dsl-tests" "flex-tests" "state-contract-tests"
|
"selector-tests" "dsl-tests" "flex-tests" "state-contract-tests"
|
||||||
|
|||||||
@ -148,5 +148,44 @@
|
|||||||
(when (buffer-live-p buffer)
|
(when (buffer-live-p buffer)
|
||||||
(kill-buffer 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)
|
(provide 'ebox-font-tests)
|
||||||
;;; ebox-font-tests.el ends here
|
;;; ebox-font-tests.el ends here
|
||||||
|
|||||||
@ -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))))
|
(should (eq (key-binding (kbd "RET")) #'backward-char))))
|
||||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
(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)
|
(provide 'ebox-interaction-tests)
|
||||||
|
|
||||||
;;; ebox-interaction-tests.el ends here
|
;;; ebox-interaction-tests.el ends here
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
(defconst ebox-test--autoload-source-files
|
(defconst ebox-test--autoload-source-files
|
||||||
'("ebox.el"
|
'("ebox.el"
|
||||||
|
"ebox-interaction.el"
|
||||||
"ebox-layout-config.el"
|
"ebox-layout-config.el"
|
||||||
"ebox-node-factory.el"
|
"ebox-node-factory.el"
|
||||||
"ebox-measure.el"
|
"ebox-measure.el"
|
||||||
@ -46,11 +47,11 @@
|
|||||||
(buffer-string)))
|
(buffer-string)))
|
||||||
(source (ebox-test--production-lisp-text)))
|
(source (ebox-test--production-lisp-text)))
|
||||||
(should (string-match-p "Version: 3.0.0" facade))
|
(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 "ebox-transaction-participant-route" source))
|
||||||
(should-not (string-match-p "(tp-transaction-participate\\_>" source))
|
(should-not (string-match-p "(tp-transaction-participate\\_>" source))
|
||||||
(should (string-match-p "(tp-transaction-participate-v2\\_>" 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
|
(should (memq tp-transaction-protocol
|
||||||
ebox-framework-spi-supported-tp-protocols))))
|
ebox-framework-spi-supported-tp-protocols))))
|
||||||
|
|
||||||
@ -90,9 +91,8 @@
|
|||||||
(ert-deftest ebox-canonical-module-loads-typed-port-without-facade ()
|
(ert-deftest ebox-canonical-module-loads-typed-port-without-facade ()
|
||||||
"The canonical autoload boundary should not depend on loading `ebox.el'."
|
"The canonical autoload boundary should not depend on loading `ebox.el'."
|
||||||
(let* ((emacs (expand-file-name invocation-name invocation-directory))
|
(let* ((emacs (expand-file-name invocation-name invocation-directory))
|
||||||
(parent (file-name-directory (directory-file-name ebox-test--root)))
|
(ecss (file-name-directory (locate-library "ecss")))
|
||||||
(ecss (expand-file-name "ecss" parent))
|
(tp (file-name-directory (locate-library "tp"))))
|
||||||
(tp (expand-file-name "tp" parent)))
|
|
||||||
(with-temp-buffer
|
(with-temp-buffer
|
||||||
(should
|
(should
|
||||||
(zerop
|
(zerop
|
||||||
@ -127,9 +127,8 @@
|
|||||||
(ert-deftest ebox-native-command-autoloads-may-target-the-facade ()
|
(ert-deftest ebox-native-command-autoloads-may-target-the-facade ()
|
||||||
"Startup registries may safely autoload native commands from `ebox'."
|
"Startup registries may safely autoload native commands from `ebox'."
|
||||||
(let* ((emacs (expand-file-name invocation-name invocation-directory))
|
(let* ((emacs (expand-file-name invocation-name invocation-directory))
|
||||||
(parent (file-name-directory (directory-file-name ebox-test--root)))
|
(ecss (file-name-directory (locate-library "ecss")))
|
||||||
(ecss (expand-file-name "ecss" parent))
|
(tp (file-name-directory (locate-library "tp"))))
|
||||||
(tp (expand-file-name "tp" parent)))
|
|
||||||
(with-temp-buffer
|
(with-temp-buffer
|
||||||
(should
|
(should
|
||||||
(zerop
|
(zerop
|
||||||
@ -246,6 +245,8 @@
|
|||||||
("ebox-byte-compile" . "defun")
|
("ebox-byte-compile" . "defun")
|
||||||
("ebox-native-build" . "defun")
|
("ebox-native-build" . "defun")
|
||||||
("ebox-native-status" . "defun")
|
("ebox-native-status" . "defun")
|
||||||
|
("ebox-next-interaction" . "defun")
|
||||||
|
("ebox-previous-interaction" . "defun")
|
||||||
("ebox-buffer-mode" . "define-minor-mode")
|
("ebox-buffer-mode" . "define-minor-mode")
|
||||||
("ebox-render-to-buffer" . "defun")
|
("ebox-render-to-buffer" . "defun")
|
||||||
("ebox-display-buffer" . "defun")))
|
("ebox-display-buffer" . "defun")))
|
||||||
|
|||||||
@ -25,6 +25,214 @@
|
|||||||
(should-not ebox-native-reflow--loaded-module-path))
|
(should-not ebox-native-reflow--loaded-module-path))
|
||||||
(delete-file module))))
|
(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 ()
|
(ert-deftest ebox-size-native-fixed-units-use-owning-font ()
|
||||||
"Fixed units and math compile using the source node's measured font."
|
"Fixed units and math compile using the source node's measured font."
|
||||||
(let ((node (list :font-owner t)))
|
(let ((node (list :font-owner t)))
|
||||||
|
|||||||
@ -38,7 +38,7 @@
|
|||||||
(should (ebox-framework-spi-validate-provider first))
|
(should (ebox-framework-spi-validate-provider first))
|
||||||
(should-not (eq first second))
|
(should-not (eq first second))
|
||||||
(should (= (ebox-framework-spi-provider-spi-version first) 2))
|
(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
|
(should (equal ebox-framework-spi-supported-tp-protocols
|
||||||
'(tp-transaction-protocol-v1+v2
|
'(tp-transaction-protocol-v1+v2
|
||||||
tp-transaction-protocol-v2)))
|
tp-transaction-protocol-v2)))
|
||||||
@ -120,7 +120,7 @@
|
|||||||
"etaf-renderer-port" "etaf-spi-bootstrap"))
|
"etaf-renderer-port" "etaf-spi-bootstrap"))
|
||||||
(should-not (string-match-p (regexp-quote pattern) source)))
|
(should-not (string-match-p (regexp-quote pattern) source)))
|
||||||
(should (string-match-p "Version: 3.0.0" facade))
|
(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-render-to-buffer))
|
||||||
(should (functionp #'ebox-commit))
|
(should (functionp #'ebox-commit))
|
||||||
(should (functionp #'ebox-framework-spi-capabilities))))
|
(should (functionp #'ebox-framework-spi-capabilities))))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user