ebox/docs/user/ebox-user-guide.zh.md

26 KiB
Raw Blame History

Ebox 用户指南

English

Ebox 是底层 Text/Box 布局与 buffer 渲染包。本指南只使用一套公共 author 语法, 并把框架集成接口单独说明。完整函数清单见公共 API 参考

1. 加载 Ebox

(require 'ebox)

加载 Ebox 不会创建 buffer、在当前 buffer 启用 mode也不会构建 Rust。

2. 只学习一个 author 模型

Ebox 文档由 Text 和 Box 节点组成。author 语法只有七个入口:

入口 含义
"text" 一个 Text 节点的简写。
(text ... "text") 带显式文本 property 的 Text。
(box ... CHILD...) 普通视觉 Box。
(row ... CHILD...) 使用简单横向子布局的 Box。
(column ... CHILD...) 使用简单纵向子布局的 Box。
(flex ... CHILD...) 使用 Flex 子布局的 Box。
(grid ... CHILD...) 使用 Grid 子布局的 Box。

子节点始终直接嵌套。布局 form 只是选择了子布局算法的 Box不是另一种视觉对象。

(defvar ebox-guide-input
  (ebox-build
   '(column :padding ((lh 1) (ch 2))
            :border ((px 1) solid "#8A93A6")
            (text :color "#263244" "Research notes")
            (row :item-gap (ch 1)
                 (box :id "status" :background-color "#F4F6FB" "Inbox")
                 (box :background-color "#EEF2FF" "Archive")))))

ebox-build 返回不透明的 CanonicalEboxInput,把 canonical forest 与对应的 source generation 保持为一个整体。普通 author 代码只需把这个值原样传给渲染和发布函数, 不要取出或重新组装其中的内部 node。

需要空矩形区域时,使用只有几何 property、没有子节点的 box,不需要额外节点类型。

优先利用默认值和继承

默认只写改变预期效果的属性:省略冗余默认值,把共同的文字样式放到已有的共同父节点, 子节点只写差异。参考示例正在讲解的属性仍然显式写出,即使它恰好采用默认值。

eBox 自动继承 :color:font-family:font-size:font-weight:font-style:text-align:wrap-mode:visibility。几何属性和背景色不继承; 子节点未绘制背景时,可以透出父节点的背景。例如:

(ebox-build
 '(column :width (ch 40) :color "#1F2328" :bgcolor "#FAF7F0"
          (box "Shared text color")
          (box :color "#B45309" "Local accent")
          (box :height (lh 1))))

继承不会扩大属性的声明范围。例如 :text-align 只能声明在 box 上,不能因为它可以继承, 就把它移到 rowcolumnflexgrid 上。

在这个普通 Column 中,分隔框会填满可用宽度,并通过 :height (lh 1) 明确保留一行, 无需重复设置宽度和背景。空 Box 的 auto 内容高度为零;需要一行留白时显式设置 :height (lh 1)。padding 和 border 仍可增加各自的外部占用。 这取决于布局上下文Row 中 auto 宽度的子项按固有宽度排布Flex/Grid 也有各自的子项尺寸规则。 不要把所有 stretch 都替换成 auto,也不要删除正在覆盖继承样式、样式表或其他简写的值。 没有其他声明设置对应边距时,可以用 :padding-inline:padding-block 省去零值轴。

3. 用布局名称直接表达意图

普通内容用 box;直接的一维组合用 rowcolumn;需要分配剩余空间或换行时 用 flex;需要二维轨道或显式放置时用 grid

Row 与 column

rowcolumn 接受 :item-gap:cross-align。它们的子节点仍然只是 Text 或 Box。

(ebox-build
 '(row :item-gap (ch 2) :cross-align center
       (box :width (ch 12) "Left")
       (box :width (ch 12) "Right")))

Flex

Flex 容器 property 属于 flex。participation property 直接属于子 box,因为它 描述的是父子关系。

(ebox-build
 '(flex :width (px 480)
        :flex-flow (row wrap)
        :gap ((lh 1) (px 12))
        (box :flex (1 1 auto) "Primary")
        (box :flex-grow 2 "Secondary")))

Grid

Grid 放置从 1 开始。轨道可以是固定值、auto、分数、minmaxrepeat。 placement property 同样直接属于子 box

(ebox-build
 '(grid :width (px 640)
        :grid-template-columns ((px 200) (fr 1) (fr 1))
        :grid-template-rows ((lh 1) (lh 1))
        :gap ((lh 1) (px 12))
        (box :grid-column (1 :span 3) "Header")
        (box :grid-column 1 :grid-row 2 "Navigation")
        (box :grid-column 2 :grid-row 2 "Main")
        (box :grid-column 3 :grid-row 2 "Aside")))

4. 使用几何与绘制 property

Box 几何统一使用 (单位 数值)。宽高及其最小/最大值、内外边距、gap、边框宽度、 Flex basis 和 Grid 固定轨道都使用这套格式。Ebox 实现 CSS 尺寸语义的一个子集, 输入是 Elisp 数据,不接受 "80ch" 这样的 CSS 字符串。

单位 含义
(px 240) 240 像素。
(% 50) 该属性所参照的包含块尺寸的 50%。
(vw 100) 视口宽度的 100%。
(vh 100) 视口高度的 100%。
(ch 80) 当前有效字体中字符 0 的排版前进宽度的 80 倍。
(lh 3) 三个有效行高。

ch 不表示任意字符或汉字的个数。1vw1vh 分别表示视口对应轴的 1%。 显示中的 Ebox 视口是目标窗口的 可用正文区域,不包含 mode-line 和 header-line因此 Playground 使用预览窗口。

按属性限制单位

下表是所有 Ebox 作者入口共同遵循的单位契约。Ebox 当前支持的书写模式中inline 表示横向block 表示纵向。这是针对 Emacs 收窄后的规则,比 CSS 的通用长度模型严格。

值的用途 允许单位 对应属性
Inline 几何 pxchvw% widthmin-widthmax-width;左右和 inline padding/margincolumn-gapRow 的 item-gapGrid 列轨道。
Block 几何 lhvh% heightmin-heightmax-height;上下和 block padding/marginrow-gapColumn 的 item-gapGrid 行轨道。
Flex 主轴尺寸 所在父 Flex 主轴允许的单位 flex-basis(grow shrink basis) 中的 basisrow 方向用 inline 单位column 方向用 block 单位。
边框绘制厚度 pxchlhvwvh border-width、各侧边框宽度、border 简写中的宽度;不接受百分比。

calcminmaxclamp 的所有分支以及嵌套的 Grid 轨道函数,都递归遵循 对应的单位限制。出现不允许的单位时,ebox-build 初次构造树就报错;即使计算会 抵消它,或最终会选中其他分支,也不例外。例如 :height (px 24):width (lh 3):height (min (lh 2) (px 24)) 都不接受。边框厚度是独立的绘制值,因此 :border ((px 1) solid "red") 可以同时设置四个方向的边框。

样式表和动态更新也遵循同一契约:计算样式确定后、布局开始前,重新检查 Flex 父容器方向与子项 basis。非法更新不会发布到已有 buffer。数字 :flex 1 的隐含 basis 为 (% 0),因此可用于两个主轴;显式 basis 仍按父方向验证。

简写展开到各个方向后逐项验证。单值 paddingmargingap 会同时用于两轴, 因此这种共享长度只能使用 %;通常直接分开两轴,例如 :padding ((lh 1) (ch 2)):gap ((lh 1) (px 12))。 只设置一轴时,可以写 :padding-inline (px 12):padding-block (lh 1)

百分比宽度参照包含块宽度;百分比高度需要确定的包含块高度,否则按该属性 的尺寸不确定规则处理。四个方向的百分比 padding、margin 都参照包含块宽度。 边框宽度不接受百分比。 此子集暂不支持负 margin 和 auto margin当前 buffer 后端没有实现外边距重叠 所需的几何能力。

属性 关键词 默认值
widthheight automin-contentmax-contentfit-contentstretch auto
min-widthmin-height automin-contentmax-contentfit-contentstretch auto
max-widthmax-height nonemin-contentmax-contentfit-contentstretch none

普通块级根容器有可用视口时,auto 宽度填满可用空间,auto 高度由内容决定。 min-contentmax-content 请求内容固有尺寸;fit-content 在这两个固有尺寸之间 适应可用空间;stretch 让 margin box 填满可用空间。none 表示不限制最大尺寸。 自动最小尺寸取决于布局;显式设置横向最小值 (px 0) 或纵向最小值 (lh 0) 可允许 item 缩小到其自动内容最小尺寸以下。没有 padding、border 和显式高度的空 Box 高度为零;(box :height (lh 1)) 明确保留一行空白。

四种尺寸函数可以组合单位,不会在布局阶段执行任意 Lisp

'(column :width (min (% 100) (ch 80))
         :height (calc (- (vh 100) (lh 1)))
         (box :width (max (px 120) (% 25)) "侧栏")
         (box :width (clamp (ch 20) (% 50) (ch 60)) "正文"))

calc 接受一个算术表达式;+- 组合长度,* 将长度乘以标量,/ 将长度 除以非零标量。minmax 从一个或多个允许的长度中选取最小值、最大值;clamp 的三个 参数依次为最小值、首选值、最大值。函数可以嵌套。算术表达式内的裸数字只是标量, 不会隐式变成 chlh

单位和函数一直保留为数据,到布局阶段才解析,因此视口或包含块变化后会重新计算。 中间结果保留小数,例如 853 像素视口的 33vw 在显示量化前是 281.49 像素。 当前 buffer 后端按完整行生成 block 几何,小数行目标在这个边界量化。作者接口不 接受纵向 px 长度。 :overflow hidden 裁掉超出的纵向行,并将每行内容限制在测量后的内容宽度内。 保留放得下的前缀,不拆开受支持的文本簇;放不下的字形或图像整体省去。固定 display space 可以缩短到精确的剩余像素宽度,剩余空隙用盒子内容空白补齐,使 padding 和边框 保持位置。这是按像素宽度截断,不是显示半个字形/图像,也不自动添加省略号。 被省去的子文本不会在填充空白上留下自己的帮助、hover、指针或 keymap。

例如 (box :width (ch 3) :wrap-mode none :overflow hidden "ABCDEFGHIJ") 只保留 三个 0 字形前进宽度内放得下的前缀;使用 visible 时,溢出仍可超出计算宽度。 在哪个容器上设置 hidden,就约束哪个容器的内容边界,直接文本和组合后的子节点输出 都受约束。scroll 提供纵向滚动,不增加横向滚动。

不再接受几何裸数字、单元素像素列表、viewportviewport-heightcontain 以及带参数的 fit-content。零长度也需要对应轴允许的单位,例如横向 (px 0)、 纵向 (lh 0)。Grid 的 (fr n) 仍表示轨道 份额Grid 放置索引、Flex grow/shrink 因子仍是无量纲数字。

Text 的样式声明只接受字体、前景/背景和文本装饰 property此外Text 还接受下文 介绍的四种原生节点能力。padding、margin、border、 尺寸、:outer、overflow、可见性和换行策略只属于 Box。Box 上的字体与颜色可以 作为其 Text 子树的继承来源,但不会让 Text 获得 Box 几何。

(ebox-build
 '(box :width (px 420)
       :padding ((lh 1) (ch 2))
       :margin ((lh 0) (ch 1))
       :border ((px 1) solid "#8A93A6")
       :color "#263244"
       :background-color "#FFFFFF"
       "A readable panel"))

:outer inline:outer block 表达 Box 如何参与父布局。子布局算法仍由 form 名称决定。

换行与可选 EKP 依赖

:wrap-mode word 按普通单词换行,并允许 CJK 字符换行;char 按 Ebox 支持的常见 文本簇边界换行;none 保留显式换行符,不添加软换行。kp 将段落交给独立的 EKP 包,使用 KnuthPlass 算法排版。 换行模式属于 Box并由其中的文本继承。

使用 :wrap-mode kp 前,安装 EKP 1.0.0 或更高版本,或将其源码目录加入 load-path。 它是按需加载的可选依赖,普通 Ebox 布局只需要 ECSS 和 TP。选择 kp 而没有兼容的 EKP 时,会提示安装或更新;请安装 EKP或显式选择其他换行模式不会静默改用单词换行。 EKP 加速器与 Ebox Rust reflow 模块是两个独立的可选模块,启用一个不会安装另一个。

(add-to-list 'load-path "/path/to/ekp")
(ebox-render
 (ebox-build '(box :width (ch 40) :wrap-mode kp :overflow hidden
                   "KnuthPlass considers the paragraph when choosing line breaks.")))

Ebox 的文本簇处理会保留常见组合音标、变体选择符、Emoji 修饰符、ZWJ 序列和区域指示符 配对,但不实现完整 Unicode 字素分段或浏览器级双向布局。字形塑造和字形是否可用, 仍取决于 Emacs、所选字体和窗口系统。

原生节点交互

Text 与所有 Box form 都接受 :help-echo:pointer:hover-style:keymap。它们是 CSS cascade 之外的显式节点能力。例如,下面的 Box 组合了 动态帮助、手形指针、悬停绘制,以及供鼠标和键盘激活共用的回调:

(ebox-render-to-buffer
 "*Ebox Interaction*"
 (ebox-build
  `(box :id "action" :padding ((lh 1) (ch 2))
        :border ((px 1) solid "#5893A3")
        :help-echo ,(ebox-help-create
                     (lambda ()
                       (format-time-string "Help requested at %H:%M:%S")))
        :pointer hand
        :hover-style (:color "#FFFFFF" :background-color "#286477"
                      :text-decoration-line underline)
        :keymap ,(ebox-keymap-create :activate #'describe-mode)
        "Click or press RET / SPC here to describe this buffer's mode.")))

:help-echo 接受字符串、参数为 (WINDOW OBJECT POSITION) 的原生函数或 nil。 推荐通过 ebox-help-create 适配返回字符串或 nil 的零参数业务函数,由 Ebox 提供 悬停 buffer 的上下文。Emacs 在请求帮助时调用函数DSL 构造树时不会调用它。 :pointer 接受 textarrowvdragmodelinehandhdragnhdraghourglassnil。 帮助的显示方式和指针的具体外观由用户的 Emacs 设置及窗口系统决定。

:hover-style 接受 nil,或仅包含 :color:background-color:text-decoration-line:text-decoration-color:text-decoration-style 的 Ebox 绘制 plist。同一渲染行中一个声明节点覆盖的文字与 padding 共享原生 mouse-face,未指定属性采用该节点的基础样式;被该悬停覆盖的不同色子文本也使用 同一悬停基础样式。物理左右边框不参与悬停,水平边框线条保持完整;字体度量与几何 不改变。允许的值见 API 参考。Ebox 将这个 plist 编译为原生 mouse-face,不提供任意原生 property 透传。

Box 的帮助、指针和 keymap 覆盖渲染内容、padding 和 border不包含该 Box 自身的 margin 及结构性换行符;悬停遵循上面的规则。嵌套的 Text 或 Box 可通过显式值替换 每项能力;显式 nil 阻止外层值覆盖, 省略属性则保留外层交互范围的覆盖。

:keymap 接受原生 Emacs keymap 或 nil。推荐使用 ebox-keymap-create :activate 的回调不带参数,处理 RET[return]SPC[mouse-1]:bindings 接受 alist按键描述字符串或事件向量对应零参数回调。 辅助函数让鼠标回调在事件窗口的 buffer 中运行,因此回调可以直接按语义 ID 更新: (ebox-region-update "action" :help-echo "Activated")

使用键盘绑定时,先把 point 移到交互范围内。可选命令 ebox-next-interactionebox-previous-interaction 在当前渲染了可用 keymap 的声明节点之间移动。每个 owner 只有一个停靠点,即使其 map 覆盖 padding 或多行;显式子 map 单独形成停靠点。 空 map、显式 nil、不可见或被裁掉的内容不产生停靠点。导航读取当前已提交 buffer 更新后不需要额外刷新。到达边界时报用户错误,不循环跳转,也不移动 point。 这些命令不会自动添加按键绑定。例如,为启用了 ebox-buffer-mode 的 buffer 选择启用:

(keymap-set ebox-scroll-map "TAB" #'ebox-next-interaction)
(keymap-set ebox-scroll-map "<backtab>" #'ebox-previous-interaction)

仍可直接传入原生 keymap其鼠标命令需要自行处理事件的目标 buffer。Ebox 使用原生 命令分派,不创建应用状态或焦点管理系统。单独设置手形指针不会绑定点击命令。

Playground 的交互实验室 把业务函数与状态放在 interaction-reference.el,把布局放在 interaction-reference.ebox,展示回调、嵌套覆盖,以及通过公共 region 更新替换或 清除四种能力。

5. 渲染与发布

ebox-render 返回带属性文本,不修改 live bufferebox-render-to-buffer 挂载 retained TP surface 并返回 buffer。它们和 ebox-display-buffer 都接收 ebox-build 返回的不透明值。

(ebox-render ebox-guide-input)
(ebox-render-to-buffer "*Ebox Guide*" ebox-guide-input)

需要同时显示结果时,使用 ebox-display-buffer。它先完成渲染,再让 Emacs 放置 buffer遵循 display-buffer-alist,并接受与原生 display-buffer 相同的可选 action

(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 原子更新:

(ebox-commit
 "*Ebox Guide*"
 (ebox-build
  '(column :padding ((lh 1) (ch 2))
           (text "Updated notes")
           (box :key body "The new input is caller-owned."))))

验证、渲染或发布失败时,旧 buffer 与 runtime 状态保持不变。 ebox-buffer-update-report 返回最近一次成功更新报告的防御性副本。

6. 查询和更新 mounted surface

:id:class:key 是 author metadata。Selector 使用 ECSS 语义。 在目标已挂载 buffer 中,可以直接传入语义 :id

(with-current-buffer "*Ebox Guide*"
  (ebox-region-update "status" :color "#166534"))

需要显式指定 buffer 时,把 :id 解析为不透明、surface-scoped 的 handle

(let ((handle (ebox-region-resolve "*Ebox Guide*" "status")))
  (ebox-region-update handle :color "#166534"))

同一 API 可以替换 Text 或 Box region 的原生能力。对上面的交互示例:

(with-current-buffer "*Ebox Interaction*"
  (ebox-region-update "action" :help-echo "Updated help" :pointer 'arrow
                      :hover-style '(:background-color "#F6D6AB"))
  (ebox-region-update "action" :help-echo nil :pointer nil
                      :hover-style nil :keymap nil))

更新参数是正常求值的 Elisp因此符号和字面量 plist 需要 quote它们与已经被 quote 的 DSL 数据不同。省略的更新属性保持不变,显式 nil 清除对应能力并阻止外层值覆盖。 Ebox 对 keymap 创建快照;发布新绑定时应替换 :keymap,而不是修改原 map。 清除节点 keymap 后,普通 buffer 和全局绑定仍然可用。

ebox-selector-query-buffer 按文档顺序返回 mounted buffer 中的匹配; ebox-selector-update-buffer 对所有可编辑匹配应用一次样式更新。数字 region id 只是诊断用渲染 metadata不是稳定更新 handle。

7. Resize 与滚动

内容超过有限高度时,:overflow scroll 会创建内部 scroll window。键盘滚动以 文本光标point为目标鼠标和触摸板滚动以事件中的鼠标位置为目标。剩余滚动距离 沿包含该位置的 scroll owner 从内向外传递,最后交给普通 Emacs 滚动。鼠标位于 其他栏、盒子外或 mode line 时,不会选取页面中无关的滚动盒子。

(ebox-build
 '(box :id log :width (px 420) :height (lh 8) :overflow scroll
       "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9"))

可见 mounted buffer 会跟随选定的一个显示窗口。每个已挂载 buffer 只有一套视口布局: 将同一个 buffer 同时放到不同宽度的窗口中,不会产生两套独立布局。每个视图需要 各自宽度时,把同一份 canonical input 分别挂载到不同 buffer。集成层也可以显式应用 viewport横向以像素、纵向以行为单位

(ebox-rerender-buffer-with-context
 (get-buffer "*Ebox Guide*") 800 30)

8. 独立 .ebox 文件

.ebox 文件包含一个普通 Elisp 表达式,其求值结果就是交给 ebox-build 的布局 数据。静态布局对整个列表加 quote

'(column :padding ((lh 1) (ch 2))
         :cross-align center
         (text :color "#263244" "Title")
         (box :width (px 240) "Body"))

同级 ebox-playground 读取唯一一个表达式,以词法绑定求值,再将得到的数据传给 ebox-build,不再单独求值属性或子节点。这与 .el(ebox-build ...) 的参数 表达式完全一致;在 .ebox 中只需省略外层的 ebox-build 调用。

必要的业务函数与变量放在布局旁边的同名 .el 文件中。预览 notes.ebox 时,如果 存在 notes.elPlayground 会在每次求值布局前加载它;C-c C-cebox-playground-open-file 和文件渲染入口都遵循这个顺序。加载的是指定的 .el 源码,因此保存修改后再次预览即可生效,不使用 require 缓存,也不优先选择旧的 .elc。没有配套文件时,独立 .ebox 仍可使用;配套文件加载出错时,渲染停止, 不会继续求值布局。

例如,由 notes.el 定义数据与操作:

;;; notes.el --- Notes example behavior -*- lexical-binding: t; -*-
(defvar notes-body "A long article.")

(defun notes-help ()
  "Return business help for the article."
  (format "Article: %s" notes-body))

(defun notes-activate ()
  "Mark the article in the current Ebox buffer."
  (ebox-region-update "article" :color "#166534"))

notes.ebox 专注表达布局:

`(column :padding ((lh 1) (ch 2))
         (box :id "article" :width (px 480)
              :help-echo ,(ebox-help-create #'notes-help)
              :pointer hand
              :keymap ,(ebox-keymap-create :activate #'notes-activate)
              ,notes-body))

反引号保留布局数据,, 插入值,,@ 展开子节点列表。不要仅为了隐藏重复 DSL 属性列表而添加辅助函数。每次预览先加载配套文件,再求值一次布局;普通 Elisp 变量定义决定应用状态是初始化、保留还是重置。

旧的属性求值格式需要显式迁移:静态布局在最外层加 quote去掉内部 list 和 symbol 属性的 quote动态布局使用反引号并在需要求值的位置加逗号。例如旧的 :padding '(1 2) 改成引用列表内的 :padding ((lh 1) (ch 2));计算像素宽度时, 在反引号列表内写 :width (px ,(+ 200 40))。 不再自动将裸结构 form 解释为 DSL也不自动识别旧格式。

9. Typed 集成 API

已经完成 author normalization 的框架可以绕过 list DSL这是集成 API不是第二套 author 语法。一个 source builder 拥有完整 source generation。每个 TextNode 或 BoxNode 都取得该 builder 返回的不透明 handle并使用由同一份 normalized declarations 投影出的 node-owned facts。最后封存 builder把 forest 与 source index 作为一个 CanonicalEboxInput 传递。

(let* ((builder (ebox-source-builder-create))
       (root-declarations nil)
       (left-declarations nil)
       (right-declarations nil)
       (root-handle
        (ebox-source-builder-bind
         builder :declarations root-declarations))
       (left-handle
        (ebox-source-builder-bind
         builder :declarations left-declarations))
       (right-handle
        (ebox-source-builder-bind
         builder :declarations right-declarations))
       (left
        (ebox-text-create
         :value "Left"
         :source-handle left-handle
         :owned-facts
         (ebox-canonical-facts-from-declarations
          'text left-declarations)))
       (right
        (ebox-text-create
         :value "Right"
         :source-handle right-handle
         :owned-facts
         (ebox-canonical-facts-from-declarations
          'text right-declarations)))
       (root
        (ebox-box-create
         :layout (ebox-row-layout-create
                  :item-gap '(ch 1) :cross-align 'center)
         :children (list left right)
         :source-handle root-handle
         :owned-facts
         (ebox-canonical-facts-from-declarations
          'row root-declarations))))
  (ebox-canonical-input-create
   (list root)
   (ebox-source-builder-finish builder)))

ebox-source-builder-createebox-source-builder-bindebox-source-builder-finishebox-canonical-facts-from-declarations、typed node/layout constructor 与 ebox-canonical-input-create 只属于集成边界。这里的 :layout:children:source-handle:owned-facts 是 evaluated constructor 字段,不是 author property也没有扩展七入口语法。

10. 可选 native reflow 与验证

Rust 模块是可选加速器Ebox 加载时不会构建它:

(ebox-native-status)
(ebox-native-build)

在仓库根目录运行:

make docs-contract-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
make interaction-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
make dsl-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs