From c5cb153f72a0224c684cc4ad11bd25ed1a452200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 10 Oct 2026 13:02:06 -0400 Subject: [PATCH] Page warnings log their line only, never words or source (M-11) A document can forge both a console message and its source (KaTeX's \errmessage, data: scripts, sourceURL, encoded paths), so the display handler logs a line number and nothing the page wrote. The page's own diagnostics reach the app over elyQuery: a missed reveal sends its line; the visual failure and diagram lines already did. A test calls the real handler and checks the captured log. Round 5 found only the missing test; it is added and mutation-checked, with no sixth review under the five-round cap. M-15 is new: the print page runs a document's scripts. Co-Authored-By: Claude Opus 5.5 --- agents.md | 2 +- apps/ely-mark/src/page.rs | 19 ++++++++- apps/ely-mark/src/view.rs | 21 +++------- apps/ely-mark/web/src/main.js | 5 +-- apps/ely-mark/web/src/render.js | 3 +- crates/ely-web-content/src/handlers.rs | 54 +++++++++++++++++++++++--- docs/BugList.md | 7 ++++ tasks/BUGS.md | 3 +- tasks/reviews/M-11-r1.md | 3 ++ tasks/reviews/M-11-r2.md | 3 ++ tasks/reviews/M-11-r3.md | 3 ++ tasks/reviews/M-11-r4.md | 5 +++ tasks/reviews/M-11-r5.md | 3 ++ 13 files changed, 101 insertions(+), 30 deletions(-) create mode 100644 tasks/reviews/M-11-r1.md create mode 100644 tasks/reviews/M-11-r2.md create mode 100644 tasks/reviews/M-11-r3.md create mode 100644 tasks/reviews/M-11-r4.md create mode 100644 tasks/reviews/M-11-r5.md diff --git a/agents.md b/agents.md index 62879a0..14ea6f4 100644 --- a/agents.md +++ b/agents.md @@ -225,7 +225,7 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-06(用户定):OCR 随包语言数据用 tessdata_fast,英文、简体中文、繁体中文(eng、chi_sim、chi_tra)。 - 2026-10-06(用户定):ElyMail 在八个应用里最后做:任务表中 C12 移到 C15 之后、C16 之前;CEF 内容宿主随 ElyMark 先建(C14.0),邮件编号依次前移。 - 2026-10-06(用户定):`ely-office` 的外壳不再只服务 LOK:`Host` 带关联类型 `Engine`(`ely_office::Engine`:后台载入、视图、撤销历史的标记、写出、关闭,视图的落定、编辑计数、待落地、焦点与事件),`Tab` 与 `Core` 按引擎取文档、视图与 worker 类型。LOK 的实现在 `ely_office::lok`(`Lok`、`start`、只对 LOK 的 `Commands` 即 `run_active`/`run_in`;查找替换、面板、图表仍只限 LOK);MuPDF 的在 `apps/ely-pdf/src/engine.rs`(`Pdf`;worker 首次载入才起,`Mupdf::get`)。`Kind::macros` 为 `None` 表示该格式无宏,不扫文件。ElyPDF 每个标签一个 `PdfView` 实体(页面、图块、文字、选择、找到的词都在里面,自己绘制),`Viewer` 只管标签条、书签栏、查找框与状态栏;打开、密码、关闭询问与保存走外壳。外壳的密码框:对话框开着时后到的加锁文件排队(`Core::locked`),对话框关闭(含取消、提交)后下一个再问;每次询问一个身份(按输入框实体),连续询问的 Esc 各归各的;提交时对话框先关、再载入。ElyDoc 的加锁测试原先清的是应用自己的对话框字段,改为清外壳的。 -- 2026-10-07:本地 Web 内容宿主(C14.0,§17.3)。`crates/ely-web-content`:CEF 154.0.32(Chromium 154.0.8037.58,cef-rs a712ad9,`default-features = false`),`scripts/fetch-cef.sh` 取 minimal 包到 `engines/cef`,`.cargo/config.toml` 的 `CEF_PATH` 让构建用它。进程:应用的 `main` 先调 `ely_web_content::subprocess()`(参数里有 `--type=` 即为 CEF 子进程,载入框架后 `execute_process`,不碰 GPUI);开发构建子进程即应用本身的二进制;macOS 包里是五个辅助应用(`<二进制名> Helper`、`(GPU)`、`(Renderer)`、`(Plugin)`、`(Alerts)`,`Contents/Frameworks`,`LSUIElement`,可执行文件为 `ely-web-helper`),框架在 `Contents/Frameworks`,`packaging/macos/bundle.sh` 对依赖 ely-web-content 的应用自动放入并先签内层;查找次序 `ELY_CEF`、包内、辅助应用所在、构建时的 `engines/cef`。启动:`start` 在 GPUI 里只做准备(载入框架、给 NSApp 的类补上 Chromium 要的 `isHandlingSendEvent`、`setHandlingSendEvent:` 与包住 `sendEvent:`,并加 `CrAppProtocol`、`CrAppControlProtocol`),`initialize` 与建浏览器都在消息泵任务里、GPUI 的更新之外进行(实测:在更新里同步调 CEF,CEF 转动 AppKit 的循环,GPUI 报 RefCell 已借用、整窗不画);浏览器异步建(`on_after_created` 交回)。凡在 GPUI 里对 CEF 的调用(输入、焦点、尺寸、脚本、缩放、重新载入、关闭)都排进主线程的队列,由泵在下一轮、更新之外依次执行(实测:在实体释放时同步关浏览器,CEF 转动 AppKit 的循环去校验菜单,GPUI 的菜单回调遇已借用的应用状态而中止,约三成复现)。泵:外部消息泵,按 `on_schedule_message_pump_work` 的时间,另照 cefclient 每轮后以 1/30 秒兜底(实测:只按请求时第一轮后 CEF 再不请求,页面不载入);空闲时 CPU 约 1.5%。离屏渲染须用 Alloy 风格(`runtime_style`;Chrome 风格不出画面)。同一缓存目录只允许一个实例,第二个启动时报出占用它的进程号。画面:`on_paint` 的 BGRA 直接成 GPUI 图像,上一帧在下一次绘制时从图集释放(`drop_image`);光标随页面。输入:鼠标点击、移动、滚轮(行按 40 点),键照 cefclient 的 macOS 做法:键盘按下带字符与系统键码发 `KEYDOWN` 再发 `CHAR`(退格、回车、方向等用 AppKit 的字符),可打出字的键等 GPUI 的输入法回调后以所打的字发同样两个事件,多字或组字经 `ime_commit_text`/`ime_set_composition`(无替换范围时用 CEF 的“无范围”,两端为最大值;传空指针 CEF 不收);松开带同一字符(不带时 Chromium 当作又按一次,实测退格删两个字);拷贝、剪切、粘贴、全选走框架命令。焦点随 GPUI 的焦点给页面。页面与应用之间:CEF 的消息路由,页面调 `window.elyQuery({request, onSuccess, onFailure})`(`onSuccess` 必须有,缺了路由不发),应用收到 `Event::Query(请求, 回复)`;应用以 `run_js` 调页面。主题:每次载入与主题变化时把调色板、代码色、字体与字号比例、深浅写成 `:root` 的 `--ely-*` 变量与 `color-scheme`。缩放 `set_zoom(倍数)`。页面只许载入所在内容文件夹里的文件:按真实路径判定(`..`、百分号转义、指向外面的符号链接都算外面,文件须存在),导航与每个资源请求(脚本、图片、样式、请求等)都过这道门,资源另许 `data:` 与本页的 `blob:`,其余一律取消;另在每个 CEF 进程以 `--proxy-server=127.0.0.1:9` 与 `--proxy-bypass-list=<-loopback>` 把一切网络(含本机回环与 WebSocket,后者不经资源回调)送往不通的代理;WebRTC 走 UDP 不经代理,故另以 `--force-webrtc-ip-handling-policy=disable_non_proxied_udp` 只许它经代理,并在每个页面上下文建立时、页面脚本之前删去 `RTCPeerConnection` 等构造(渲染进程的 `on_context_created`);页面的地址由真实路径逐字节转义生成(文件名含 `#`、`?` 照样载入);用户点出的链接以 `Event::Leave` 交给应用;页面的警告与错误写入日志。载入失败(主框架,停止不算)、渲染进程退出、CEF 起不来都报 `Event::Failed`,页面区改显示组件库的错误视图与“重新载入”(浏览器在即重载,否则重建;CEF 起不来时只说明),载入成功才报 `Event::Loaded`。浏览器关闭时断开它与表面的互相持有,页面的内存随之释放(释放时写日志)。真窗口(macOS,开发构建与 `dist/ElyMark.app`):页面以应用深色主题画出,点选、键入中英文、回车、退格、方向键、拷贝粘贴、`elyQuery` 往返;`tests/scripts/webview.sh` 以输入法回调组“你好”、键入、读回焦点、主题变量与缩放 1.25,核对页面跳不出所在文件夹、外面的脚本不执行、本机 HTTP 收不到请求、`#` 名的页面载入、载入前被删的页面报失败、开关 20 个页面都释放内存、杀掉渲染进程后页面区不再显示旧画面、经辅助功能按下“Reload”后页面重新画出(截窗口量标记色),WebRTC 不存在且本机 UDP 收不到 STUN,并截窗口量页面画出的标记色(去掉绘制、去掉断开持有、去掉代理或不显示故障时该脚本都失败);需屏幕录制与辅助功能权限,缺即报失败。未验证:Windows 与 Linux;辅助技术(见缺口);树莓派(R-06)。 +- 2026-10-07:本地 Web 内容宿主(C14.0,§17.3)。`crates/ely-web-content`:CEF 154.0.32(Chromium 154.0.8037.58,cef-rs a712ad9,`default-features = false`),`scripts/fetch-cef.sh` 取 minimal 包到 `engines/cef`,`.cargo/config.toml` 的 `CEF_PATH` 让构建用它。进程:应用的 `main` 先调 `ely_web_content::subprocess()`(参数里有 `--type=` 即为 CEF 子进程,载入框架后 `execute_process`,不碰 GPUI);开发构建子进程即应用本身的二进制;macOS 包里是五个辅助应用(`<二进制名> Helper`、`(GPU)`、`(Renderer)`、`(Plugin)`、`(Alerts)`,`Contents/Frameworks`,`LSUIElement`,可执行文件为 `ely-web-helper`),框架在 `Contents/Frameworks`,`packaging/macos/bundle.sh` 对依赖 ely-web-content 的应用自动放入并先签内层;查找次序 `ELY_CEF`、包内、辅助应用所在、构建时的 `engines/cef`。启动:`start` 在 GPUI 里只做准备(载入框架、给 NSApp 的类补上 Chromium 要的 `isHandlingSendEvent`、`setHandlingSendEvent:` 与包住 `sendEvent:`,并加 `CrAppProtocol`、`CrAppControlProtocol`),`initialize` 与建浏览器都在消息泵任务里、GPUI 的更新之外进行(实测:在更新里同步调 CEF,CEF 转动 AppKit 的循环,GPUI 报 RefCell 已借用、整窗不画);浏览器异步建(`on_after_created` 交回)。凡在 GPUI 里对 CEF 的调用(输入、焦点、尺寸、脚本、缩放、重新载入、关闭)都排进主线程的队列,由泵在下一轮、更新之外依次执行(实测:在实体释放时同步关浏览器,CEF 转动 AppKit 的循环去校验菜单,GPUI 的菜单回调遇已借用的应用状态而中止,约三成复现)。泵:外部消息泵,按 `on_schedule_message_pump_work` 的时间,另照 cefclient 每轮后以 1/30 秒兜底(实测:只按请求时第一轮后 CEF 再不请求,页面不载入);空闲时 CPU 约 1.5%。离屏渲染须用 Alloy 风格(`runtime_style`;Chrome 风格不出画面)。同一缓存目录只允许一个实例,第二个启动时报出占用它的进程号。画面:`on_paint` 的 BGRA 直接成 GPUI 图像,上一帧在下一次绘制时从图集释放(`drop_image`);光标随页面。输入:鼠标点击、移动、滚轮(行按 40 点),键照 cefclient 的 macOS 做法:键盘按下带字符与系统键码发 `KEYDOWN` 再发 `CHAR`(退格、回车、方向等用 AppKit 的字符),可打出字的键等 GPUI 的输入法回调后以所打的字发同样两个事件,多字或组字经 `ime_commit_text`/`ime_set_composition`(无替换范围时用 CEF 的“无范围”,两端为最大值;传空指针 CEF 不收);松开带同一字符(不带时 Chromium 当作又按一次,实测退格删两个字);拷贝、剪切、粘贴、全选走框架命令。焦点随 GPUI 的焦点给页面。页面与应用之间:CEF 的消息路由,页面调 `window.elyQuery({request, onSuccess, onFailure})`(`onSuccess` 必须有,缺了路由不发),应用收到 `Event::Query(请求, 回复)`;应用以 `run_js` 调页面。主题:每次载入与主题变化时把调色板、代码色、字体与字号比例、深浅写成 `:root` 的 `--ely-*` 变量与 `color-scheme`。缩放 `set_zoom(倍数)`。页面只许载入所在内容文件夹里的文件:按真实路径判定(`..`、百分号转义、指向外面的符号链接都算外面,文件须存在),导航与每个资源请求(脚本、图片、样式、请求等)都过这道门,资源另许 `data:` 与本页的 `blob:`,其余一律取消;另在每个 CEF 进程以 `--proxy-server=127.0.0.1:9` 与 `--proxy-bypass-list=<-loopback>` 把一切网络(含本机回环与 WebSocket,后者不经资源回调)送往不通的代理;WebRTC 走 UDP 不经代理,故另以 `--force-webrtc-ip-handling-policy=disable_non_proxied_udp` 只许它经代理,并在每个页面上下文建立时、页面脚本之前删去 `RTCPeerConnection` 等构造(渲染进程的 `on_context_created`);页面的地址由真实路径逐字节转义生成(文件名含 `#`、`?` 照样载入);用户点出的链接以 `Event::Leave` 交给应用;页面的警告与错误只记行号(警告文字与来源都可由文档伪造,不进日志);页面自己的诊断经 `elyQuery` 交应用记录。载入失败(主框架,停止不算)、渲染进程退出、CEF 起不来都报 `Event::Failed`,页面区改显示组件库的错误视图与“重新载入”(浏览器在即重载,否则重建;CEF 起不来时只说明),载入成功才报 `Event::Loaded`。浏览器关闭时断开它与表面的互相持有,页面的内存随之释放(释放时写日志)。真窗口(macOS,开发构建与 `dist/ElyMark.app`):页面以应用深色主题画出,点选、键入中英文、回车、退格、方向键、拷贝粘贴、`elyQuery` 往返;`tests/scripts/webview.sh` 以输入法回调组“你好”、键入、读回焦点、主题变量与缩放 1.25,核对页面跳不出所在文件夹、外面的脚本不执行、本机 HTTP 收不到请求、`#` 名的页面载入、载入前被删的页面报失败、开关 20 个页面都释放内存、杀掉渲染进程后页面区不再显示旧画面、经辅助功能按下“Reload”后页面重新画出(截窗口量标记色),WebRTC 不存在且本机 UDP 收不到 STUN,并截窗口量页面画出的标记色(去掉绘制、去掉断开持有、去掉代理或不显示故障时该脚本都失败);需屏幕录制与辅助功能权限,缺即报失败。未验证:Windows 与 Linux;辅助技术(见缺口);树莓派(R-06)。 - 2026-10-07:ElyTree 恢复与清理、外部工具、操作规则(C13.6,TREE-13、14,§13.3,QA-19、20)。仓库页第二行:在终端中打开(macOS `open -a Terminal`,其他平台报未构建)、在访达中显示(`reveal_path`)、清理未跟踪的文件…;页底“引用日志”列出 HEAD 最近 200 次移动(`reflog show -z`,位置、Git 写的原因、时间),选中即中间显示该提交(同提交详情,`Whose::Reflog`),上方“在此新建分支…”(找回删掉的分支)与“把当前分支移到此…”:有已暂存的改动即拒绝(`reset --keep` 会丢掉它们,请先提交或取消暂存);否则在后台算出离开该分支的提交,对话框写明分支、从哪到哪、离开的提交(引用日志仍可找回)与“工作区改动保留”,确认后 `reset --keep`(Git 在改动冲突时拒绝;没有任何按钮做硬重置),执行前在写队列里核对 HEAD 仍在预览时的分支(完整引用名,或仍游离)与提交、仍无暂存,任一变了即什么也不做。引用日志的时间是该次移动的时间(`--date=unix` 下的 `%gd`),不是提交时间;选中按事件本身(提交、原因、时间与它在全部引用日志中自最早起的序号,后者新移动不改,同一秒同原因的两次也分得开),同一提交的多条不会一起选中;之后有新的移动时该事件的位置后移,选中与详情跟着它,事件已不在引用日志里即不显示。清理:对话框先由 `git clean -n -d`(勾“包括被忽略的文件”另加 `-x`)得出 Git 会删除的路径(`core.quotePath=false`,其余 C 引号照 Git 规则还原),其中的文件夹当场展开为其中的文件(`ls-files --others`,同一忽略规则,另一仓库不入),对话框逐个列出文件与文件夹;确认后先逐段查每个所列路径:仍在仓库里、途中没有符号链接、文件仍是文件、文件夹仍是文件夹(任一变了即整体拒绝,什么也不删),再以 `git clean -f [-x] -- 所列文件` 删去(只给文件,Git 不递归;此后被跟踪了的文件 Git 留下,也不穿过链接),最后自深而浅删去所列文件夹中已空的(预览后在其中新出现的文件不动,文件夹随之留下),每次都问。文件标题栏多出“在编辑器中打开”(用户在“仓库›选择编辑器…”选的程序,存在数据目录 `tools.json`,字段类型不对与读不出同样报错且不覆盖;macOS 的 .app 经 `open -a`,在后台等它结束,失败即显示其原话;未选时先问)与 Markdown 的“在 ElyMark 中打开”(`ely_platform::apps::open`:按安装布局找同级的应用,macOS 的 `名称.app/Contents/MacOS`,Windows 的 `Ely Office\名称`,开发构建的同一目录;找不到即报错)。锁(§13.3):任何写操作因 Git 的锁文件失败(`….lock': File exists`,或配置文件的 `could not lock config file …: File exists`)时,对话框说明另一个 Git 正在工作、Ely 从不删除锁文件,“重试”把同一写操作重新排队。QA-19:测试都以独立的 Git 命令核对索引、提交、引用与工作区。QA-20:冲突、钩子失败、凭据被拒(本机 401 服务器,显示 Git 的 “terminal prompts disabled” 原话)、锁、变基中断后中止、取消(停止取回)都有窗口测试,结束时仓库状态以 Git 读回核对。切换到系统 Git 目前经 `ELY_GIT`(设置页随 C18.1);状态栏显示所用 Git 的路径与版本。真窗口(macOS):仓库页工具行、清理预览对话框、文件标题栏的编辑器与 ElyMark 按钮,按下后同级的 ely-mark 以该文件启动。未验证:Windows 与 Linux 的终端;ElyMark 打开文件的内容(C14)。 - 2026-10-07:ElyTree 储藏、远程、工作树、子模块、LFS(C13.5,TREE-10 至 12,§13.3)。左栏第四页“仓库”:上方取回(`fetch --progress --all`)、拉取(`pull --progress`,照用户的 pull 设置)、推送、强制推送…;储藏(`stash list -z`,新的在前;“储藏改动…”问说明与是否包括未跟踪的文件,`stash push`),远程(名称与地址,`remote` 加 `config --get-regexp` 读出取回与推送地址),工作树(`worktree list --porcelain -z`,行名为文件夹名,有失效的可“清理”即 `worktree prune`),子模块(索引里的 gitlink 与文件夹自身 `.git` 的 HEAD:未检出、在记录的提交上、在别的提交上、冲突),LFS 文件(`git lfs ls-files --long --json`:工作树里是内容、对象在本机而工作树是指针、只有指针而对象不在本机);没有子模块或 LFS 文件时不列。各列表分别读出,一个读不出只在该处显示 Git 原话;切到此页时读,此后每次状态重读时重读,只取最新一次。选中一项,中间显示:储藏为提交详情的同一套(`hist::Picked`,历史与储藏共用读取与面板):改动的文件(对第一个父提交)与储藏的未跟踪文件(第三个父提交,各自对空树),选文件看差异,上方应用、弹出、删除…;远程为地址与推送地址,取回、修改…(改名 `remote rename`、改地址 `remote set-url`)、移除…(先问);工作树为路径与分支或提交、锁定与失效原因,打开(作为标签)、移除…(先问,主工作树与当前的不给);子模块为记录的与检出的提交,“检出记录的提交”(`submodule update --init`,经远程运行)与打开。用户输入的名称、地址、路径前一律加 `--`(推送的远程名同样)。表单与选择对话框(新建分支、标签、改名、身份、储藏、远程、工作树、选远程与上游)记下打开时的仓库,提交时写进那个仓库,不随当前标签变;储藏列表按每条三个字段读(说明可为空)。储藏的应用、弹出、删除按 `stash@{n}` 执行,执行前在写队列里核对它仍是所见的那个储藏(另有储藏插入即拒绝,什么也不做);弹出遇冲突时 Git 保留储藏,对话框显示 Git 原话。删除储藏、移除远程与工作树每次都问。分支页的本地分支多出“设置上游…”(列出远程分支,`branch --set-upstream-to=refs/remotes/名`)与“取消上游”。推送:有上游即 `push --progress`;没有上游时后台读出远程,问推到哪个远程(`push --set-upstream -- 远程 refs/heads/分支:refs/heads/分支`),没有远程或 HEAD 游离即说明。强制推送(§13.3):后台读出当前分支、上游的远程与远端分支、上次取回时远端的提交(租约)、此刻 HEAD 的提交(源)与远端有而源没有的提交,对话框写明远程、分支与将被覆盖的提交(或写明没有),确认后推送的就是所示的源提交(`push --force-with-lease=远端分支:租约提交 -- 远程 源提交:远端分支`),确认前本地分支再动也不改变推送内容;远端自上次取回后变了即被拒绝;没有上游或游离即说明不能。远程命令(取回、拉取、推送、子模块更新)是写队列里的一种(`Write::Remote`),经 `running::Running` 运行:Git 在自己的会话里(`setsid`,SSH 找不到终端,不会挂在终端提示上;凭据照用 credential helper 与 ssh-agent),进度逐行显示在中间上方的提示条,可停止(向整个进程组发 SIGTERM,Git 自行清理;已结束的不再发);用户停止时提示“已停止”、清掉其后的排队写,不当作失败;失败时对话框显示 Git 的 stdout 与 stderr(以回车结尾的进度行被下一行改写,只留最后一次;以换行结尾的行全部保留,钩子的每一行都在);拉取遇冲突照合并类命令判定为停下。关窗与退出时停下所有远程命令与克隆。克隆改用同一运行器。真窗口(macOS):仓库页五类列表、储藏详情与文件、工作树详情、分支行。未验证:Windows 与 Linux(停止远程命令在非 Unix 上报未构建);真实网络与 HTTPS、SSH 凭据;LFS 对象的下载(只显示状态)。 - 2026-10-07(用户定):跟随组件库最新的 main。组件库 K01 把 gpui 从 zed 仓库的提交换成 crates.io 的 `gpui-pre` =0.3.8(`gpui-pre-platform` 同版本),K02 让 Tab 绑在 FocusScope 的 ElyFocus 上下文下、更深的上下文先得;本项目工作区的 `gpui`、`gpui_platform` 随之改为这两个包(以 `package =` 保留原名,源码不变),组件库 pin 到 dabc1eb(其上加本项目一个提交:树与树行的无障碍角色)。升级后全工作区编译无改动。 diff --git a/apps/ely-mark/src/page.rs b/apps/ely-mark/src/page.rs index 04edc15..8420213 100644 --- a/apps/ely-mark/src/page.rs +++ b/apps/ely-mark/src/page.rs @@ -5,7 +5,9 @@ use std::path::{Path, PathBuf}; use anyhow::{Result, bail}; use ely_web_content::WebView; use gpui::App; -use serde_json::json; +use serde_json::{Value, json}; + +use crate::view::Heading; /// The app's Resources, else this build's `engines/content`. pub fn content() -> Result { @@ -95,3 +97,18 @@ pub fn folder(view: &WebView, folder: &Path) { pub fn call(view: &WebView, name: &str, argument: &serde_json::Value) { view.run_js(&format!("ely.{name}({argument})")); } + +/// Headings from the page; anything malformed is None. +pub fn headings(items: &Value) -> Option> { + let heading = |item: &Value| { + let level = item["level"] + .as_u64() + .filter(|level| (1..=6).contains(level))?; + let title = item["title"].as_str()?.to_owned(); + Some(Heading { + level: level as usize, + title, + }) + }; + items.as_array()?.iter().map(heading).collect() +} diff --git a/apps/ely-mark/src/view.rs b/apps/ely-mark/src/view.rs index 9188e09..fbb58c0 100644 --- a/apps/ely-mark/src/view.rs +++ b/apps/ely-mark/src/view.rs @@ -353,7 +353,11 @@ impl MarkView { log::error!("mark: the page built no editor ({what})"); cx.emit(Failed(ely_ui::i18n::text(cx, "mark.page.broke", &[("what", what)]).to_string())); } - (Some("outline"), ..) => match headings(&said["items"]) { + (Some("missed"), ..) => match said["line"].as_u64() { + Some(line) => log::warn!("mark: nothing to reveal at line {line}"), + None => log::warn!("mark: no such heading to reveal"), + }, + (Some("outline"), ..) => match crate::page::headings(&said["items"]) { Some(outline) => { self.outline = outline; cx.notify(); @@ -466,21 +470,6 @@ fn found(said: &Value) -> Option { }) } -/// Headings from the page; anything malformed is None. -fn headings(items: &Value) -> Option> { - let heading = |item: &Value| { - let level = item["level"] - .as_u64() - .filter(|level| (1..=6).contains(level))?; - let title = item["title"].as_str()?.to_owned(); - Some(Heading { - level: level as usize, - title, - }) - }; - items.as_array()?.iter().map(heading).collect() -} - impl Render for MarkView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { // Dropped files: images go beside the document. diff --git a/apps/ely-mark/web/src/main.js b/apps/ely-mark/web/src/main.js index f78fa56..4e54f22 100644 --- a/apps/ely-mark/web/src/main.js +++ b/apps/ely-mark/web/src/main.js @@ -89,7 +89,6 @@ async function build(text, mode) { made = await visual(place, body, mode === 'visual', texts, edited, edge, linked) } catch (error) { // A broken editor falls back to source and says so. - console.error(`ely: the visual editor failed: ${error.name}`) entry.broke = error.name } if (made) { @@ -262,8 +261,8 @@ function reveal(place) { } const shown = 'heading' in place ? current.heading(place.heading) : current.line(place.line, place.query) - // Logs keep no search text; a line number at most. - if (shown === false) console.warn('heading' in place ? 'ely: no such heading' : `ely: nothing at line ${place.line}`) + // The app logs a line number, never the query. + if (shown === false) tell({ kind: 'missed', generation, line: place.line ?? null }) } /** The app's find box hears where the search stands. */ diff --git a/apps/ely-mark/web/src/render.js b/apps/ely-mark/web/src/render.js index edc0eae..20d1dd2 100644 --- a/apps/ely-mark/web/src/render.js +++ b/apps/ely-mark/web/src/render.js @@ -27,9 +27,8 @@ async function draw(tree) { try { const svg = await paint(node.value, 'paper') Object.assign(node, { type: 'html', value: `
${svg}
` }) - } catch (error) { + } catch { // Lines only: Mermaid's message quotes the document. - console.warn(`ely: a diagram did not draw: ${error.name}`) failed.push(node.position.start.line) } } diff --git a/crates/ely-web-content/src/handlers.rs b/crates/ely-web-content/src/handlers.rs index 25ba1c1..fb213b9 100644 --- a/crates/ely-web-content/src/handlers.rs +++ b/crates/ely-web-content/src/handlers.rs @@ -264,15 +264,13 @@ wrap_display_handler! { &self, _browser: Option<&mut Browser>, level: LogSeverity, - message: Option<&CefString>, - source: Option<&CefString>, + _message: Option<&CefString>, + _source: Option<&CefString>, line: ::std::os::raw::c_int, ) -> ::std::os::raw::c_int { - // A page's warnings and errors, never its other chatter. + // Words and sources a document may forge stay out. if level.get_raw() >= LogSeverity::WARNING.get_raw() { - let said = message.map(CefString::to_string).unwrap_or_default(); - let at = source.map(CefString::to_string).unwrap_or_default(); - log::warn!("web: page says {said} at {at}:{line}"); + log::warn!("web: the page warned at line {line}"); } 0 } @@ -449,3 +447,47 @@ fn keys() -> Option { fn keys() -> Option { None } + + +#[cfg(test)] +mod tests { + use std::sync::{Arc, Mutex, OnceLock}; + + use cef::{CefString, ImplDisplayHandler, LogSeverity}; + + struct Kept; + + fn kept() -> &'static Mutex> { + static KEPT: OnceLock>> = OnceLock::new(); + KEPT.get_or_init(Mutex::default) + } + + impl log::Log for Kept { + fn enabled(&self, _: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + kept().lock().expect("the kept lock").push(record.args().to_string()); + } + + fn flush(&self) {} + } + + #[test] + fn a_pages_words_and_sources_never_reach_the_log() { + log::set_boxed_logger(Box::new(Kept)).expect("the only logger"); + log::set_max_level(log::LevelFilter::Trace); + let surface = crate::surface::Surface::new(std::env::temp_dir(), 1.0); + let display = super::Display::new(Arc::new(Mutex::new(surface))); + let said = CefString::from("ely: M11_SECRET"); + let at = CefString::from("file:///content/M11_SECRET.js"); + for level in [LogSeverity::WARNING, LogSeverity::ERROR, LogSeverity::INFO] { + display.on_console_message(None, level, Some(&said), Some(&at), 7); + } + let records = kept().lock().expect("the kept lock").clone(); + let warned = records.iter().filter(|said| *said == "web: the page warned at line 7"); + assert_eq!(warned.count(), 2, "warnings and errors stay seen: {records:#?}"); + assert!(!records.iter().any(|said| said.contains("M11_SECRET")), "{records:#?}"); + } +} diff --git a/docs/BugList.md b/docs/BugList.md index 0a58a98..26b6b6f 100644 --- a/docs/BugList.md +++ b/docs/BugList.md @@ -428,6 +428,13 @@ - 位置:`apps/ely-mark/src/export.rs:276-282`(渲染回调里 `inline`、`read_to_string`、`fs::write`);对照 `:172-176` 的 `write_output` 已在后台 - 修法:暂存步骤移到 `background_spawn`。 + +### M-15 打印与导出 PDF 的暂存页执行文档里的脚本 +- 严重度:中 | 置信度:已确认(M-11 第 2、3 轮审查在 Windows 与 WSL 的 CEF 上复现) +- 位置:`apps/ely-mark/web/src/render.js`(`allowDangerousHtml` 原样保留 HTML 块)、`apps/ely-mark/src/export.rs`(`render_pdf`、打印把同一 HTML 暂存后交不显示的 CEF 视图) +- 现象:Markdown 里的 `` and print or export PDF: the real page's HTML renderer preserves this script, the staged page executes it (`export.rs`'s `render_pdf`/`print_page`; the resource fence permits `data:`), and CEF supplies the entire data URL as `source`. The unchanged display handler logs `web: the page warned at data:text/javascript,console.warn('M11_PRIVATE_DOCUMENT_SENTINEL'):1`. Reproduced with the freshly built page and native CEF on both Windows and WSL; the assertion excluding the document marker fails on both. The previous KaTeX prefix leak is fixed, but this source-field leak remains untested by the repository tests. Evidence: `.work/m11-r2/windows-cef-source.log`, `.work/m11-r2/linux-cef-source.log`, and `.work/m11-r2/evidence.log`. Fix direction: omit arbitrary console source strings or reduce them to validated, trusted diagnostic identifiers; add a regression that asserts document text cannot reach logs through either console message or source fields. diff --git a/tasks/reviews/M-11-r3.md b/tasks/reviews/M-11-r3.md new file mode 100644 index 0000000..82d3321 --- /dev/null +++ b/tasks/reviews/M-11-r3.md @@ -0,0 +1,3 @@ +FAIL + +- crates/ely-web-content/src/handlers.rs:453 — `warned()` trusts every `file://` console source as a bundled script and logs its final URL segment, including query/fragment text. JavaScript's `sourceURL` can supply this metadata without loading that file. Extend round 2's data-script Markdown fixture with `console.warn('hidden message');\n//# sourceURL=file:///not-bundled/M11_PRIVATE_SOURCE_SENTINEL.js`: the actual page renderer retains it, and the staged print page's warning becomes `web: the page warned at M11_PRIVATE_SOURCE_SENTINEL.js:1`. Native CEF probes using the unchanged production display handler and resource fence reproduce this on Windows and WSL; both privacy assertions fail. A source ending in `main.js?M11_PRIVATE_QUERY_SENTINEL` also leaks its query. This still violates PRD §18.2 and agents.md rule 4. The previous plain `data:` source and KaTeX message cases are fixed; script execution itself is outside this finding. Fix direction: log only verified bundled script identifiers, exclude URL queries/fragments, and use the fixed inline-script diagnostic for all other sources. Add regressions for forged `file://` sources and URL suffixes; the current two-case test passes despite this leak. Evidence: `.work/m11-r3/windows-cef-export.log`, `.work/m11-r3/linux-cef-export.log`, both `*-privacy-assert.log`, and `evidence.log`. diff --git a/tasks/reviews/M-11-r4.md b/tasks/reviews/M-11-r4.md new file mode 100644 index 0000000..caf7a62 --- /dev/null +++ b/tasks/reviews/M-11-r4.md @@ -0,0 +1,5 @@ +FAIL + +- crates/ely-web-content/src/surface.rs:82 — The logged name still comes from the untrusted URL rather than the verified file. On Windows, with a real `main.js` under the page root, execute `console.warn('hidden');` with `//# sourceURL=file:///.../content/M11_PRIVATE_DOCUMENT_SENTINEL%2F..%2Fmain.js`. Windows canonicalization removes the decoded nonexistent intermediate directory followed by `..`, so `holds()` accepts the real `main.js`; `script()` returns `M11_PRIVATE_DOCUMENT_SENTINEL%2F..%2Fmain.js`. Native CEF preserves that source metadata, and the unchanged display handler logs the document marker verbatim. This violates PRD §18.2 and rule 4; it is a leak introduced by the new name extraction, independent of the pre-existing W-02 test failures. Round 3's missing-file, query, fragment and data-source cases are fixed, but the new repository test passes despite this encoded-path leak. Fix direction: derive the diagnostic name from the canonical file verified by the fence, and add a regression excluding document text carried by encoded path components. Evidence: `.work/m11-r4/windows-probe.log`, `windows-native.log`, and `evidence.log`. + +- crates/ely-web-content/src/surface.rs:81 — `holds()` verifies canonical containment but does not require a file, so `script()` also trusts directories ending in `.js`. Create `content/M11_DIRECTORY_SENTINEL.js` as a directory and use its file URL as a console `sourceURL`: both Windows and WSL native CEF probes log `web: the page warned at M11_DIRECTORY_SENTINEL.js:1`, although no such script file exists. This breaks the required real-file restriction and agents.md's updated claim. The added regression omits directories and therefore passes. Fix direction: require the canonical target to be a regular file under the same root before returning its script name; test a `.js` directory explicitly. Evidence: `.work/m11-r4/windows-probe.log`, `linux-probe.log`, `windows-native.log`, and `linux-native.log`. Probe copies and their dedicated targets were deleted before this verdict. diff --git a/tasks/reviews/M-11-r5.md b/tasks/reviews/M-11-r5.md new file mode 100644 index 0000000..6cc01a5 --- /dev/null +++ b/tasks/reviews/M-11-r5.md @@ -0,0 +1,3 @@ +FAIL + +- crates/ely-web-content/src/handlers.rs:273 — The new console privacy boundary has no repository regression test; the previous `Surface::script` test was removed without replacement. In a review copy, restoring logging of `_message` and `_source` leaves all 32 ElyMark/Web-content Cargo tests passing on WSL, although a warning carrying document text now writes it verbatim to logs, violating PRD §18.2 and rule 4. A probe capturing this actual callback's log records passes with the current implementation and fails with that mutation on both Windows and WSL. Retain such a regression in the repository: private markers in warning/error messages and arbitrary sources must never reach logs, while the line diagnostic remains observable. Evidence: `.work/m11-r5/linux-mutant-repository.log`, `windows-mutant-probe.log`, `linux-mutant-probe.log`, and `evidence.log`. Both round 4 leaks are fixed, confirmed by native CEF probes on both platforms. Probe copies and their dedicated targets were deleted before this verdict.