From 455ebeb7ba23c3d82de1a19aa857eda0c2eef616 Mon Sep 17 00:00:00 2001 From: Natsummerance <2734763029@qq.com> Date: Wed, 7 Oct 2026 10:38:00 +0800 Subject: [PATCH 1/4] feat: prepare V0.0.5 document intelligence and safe editor integrations Co-authored-by: Codex --- .env | 22 +- .env.example | 22 +- .gitignore | 3 + README.md | 8 +- RELEASE_NOTES.md | 18 + VERSION | 2 +- assets/css/panels.css | 3 + assets/i18n/ar.json | 11 + assets/i18n/bn.json | 11 + assets/i18n/bo.json | 11 + assets/i18n/da.json | 11 + assets/i18n/de.json | 11 + assets/i18n/el.json | 11 + assets/i18n/en.json | 11 + assets/i18n/eo.json | 11 + assets/i18n/es.json | 11 + assets/i18n/fi.json | 11 + assets/i18n/fr.json | 11 + assets/i18n/ga.json | 11 + assets/i18n/he.json | 11 + assets/i18n/hi.json | 11 + assets/i18n/hr.json | 11 + assets/i18n/hu.json | 11 + assets/i18n/id.json | 11 + assets/i18n/it.json | 11 + assets/i18n/ja.json | 11 + assets/i18n/kg.json | 11 + assets/i18n/kl.json | 11 + assets/i18n/km.json | 11 + assets/i18n/ko.json | 11 + assets/i18n/lo.json | 11 + assets/i18n/mn.json | 11 + assets/i18n/ms.json | 11 + assets/i18n/mt.json | 11 + assets/i18n/my.json | 11 + assets/i18n/ne.json | 11 + assets/i18n/nl.json | 11 + assets/i18n/no.json | 11 + assets/i18n/pt.json | 11 + assets/i18n/ro.json | 11 + assets/i18n/ru.json | 11 + assets/i18n/rw.json | 11 + assets/i18n/sl.json | 11 + assets/i18n/sv.json | 11 + assets/i18n/th.json | 11 + assets/i18n/tl.json | 11 + assets/i18n/tr.json | 11 + assets/i18n/ug.json | 11 + assets/i18n/uk.json | 11 + assets/i18n/vi.json | 11 + assets/i18n/zh-CN.json | 11 + assets/i18n/zh-HK.json | 11 + assets/i18n/zh-TW.json | 11 + assets/index.html | 28 +- assets/js/reader/fixes.js | 38 + assets/readmd.boot.js | 38 + .../reviews/readmd-v005-upgrade-2026-10-07.md | 51 + packages/harmonyos-app/AppScope/app.json5 | 4 +- packages/harmonyos-app/entry/oh-package.json5 | 2 +- packages/harmonyos-app/oh-package.json5 | 2 +- packages/harmonyos-app/package.json | 2 +- packages/linglong/linglong.yaml | 2 +- packages/mcp-server/README.md | 16 +- packages/vscode-extension/README.md | 6 +- .../vscode-extension/l10n/bundle.l10n.json | 19 +- .../l10n/bundle.l10n.zh-cn.json | 19 +- packages/vscode-extension/package.json | 14 +- packages/vscode-extension/package.nls.json | 4 +- .../vscode-extension/package.nls.zh-cn.json | 4 +- packages/vscode-extension/src/bridge.ts | 4 +- packages/vscode-extension/src/extension.ts | 2 + packages/vscode-extension/src/intelligence.ts | 135 ++ .../vscode-extension/test/commands.test.js | 16 +- packages/vscode-extension/test/host.cjs | 30 + release/release_notes.md | 14 +- rust/Cargo.lock | 4 +- rust/Cargo.toml | 2 +- .../src/document_intelligence.rs | 1142 +++++++++++++++++ rust/readmd-kernel/src/lib.rs | 1 + rust/readmd-kernel/src/mcp.rs | 57 + rust/readmd-kernel/src/server.rs | 28 + rust/xtask/src/version.rs | 22 +- scripts/linux/PKGBUILD | 2 +- scripts/linux/linglong.yaml | 2 +- tools/package-windows-candidate.mjs | 35 + ui-tests/document-intelligence.spec.js | 37 + ui-tests/package.json | 2 +- website/public/.well-known/security.txt | 2 +- website/public/assets/site.js | 64 +- website/public/download/index.html | 20 +- website/public/feed.xml | 4 + website/public/index.html | 2 +- website/public/integrations/index.html | 5 + website/public/ja/download/index.html | 22 +- website/public/ja/index.html | 2 +- website/public/ja/integrations/index.html | 5 + website/public/ja/llms-full.txt | 12 +- website/public/ja/llms.txt | 8 +- website/public/llms-full.txt | 14 +- website/public/llms.txt | 6 +- website/public/sitemap.xml | 4 + website/public/zh-cn/download/index.html | 22 +- website/public/zh-cn/index.html | 2 +- website/public/zh-cn/integrations/index.html | 5 + website/public/zh-cn/llms-full.txt | 12 +- website/public/zh-cn/llms.txt | 8 +- website/public/zh-tw/download/index.html | 22 +- website/public/zh-tw/index.html | 2 +- website/public/zh-tw/integrations/index.html | 5 + website/public/zh-tw/llms-full.txt | 12 +- website/public/zh-tw/llms.txt | 8 +- website/release.json | 6 + website/tools/build-integrations.mjs | 62 + website/tools/sync-version.mjs | 85 +- website/tools/validate-website.mjs | 14 +- 115 files changed, 2516 insertions(+), 287 deletions(-) create mode 100644 docs/reviews/readmd-v005-upgrade-2026-10-07.md create mode 100644 packages/vscode-extension/src/intelligence.ts create mode 100644 rust/readmd-kernel/src/document_intelligence.rs create mode 100644 tools/package-windows-candidate.mjs create mode 100644 ui-tests/document-intelligence.spec.js create mode 100644 website/public/integrations/index.html create mode 100644 website/public/ja/integrations/index.html create mode 100644 website/public/zh-cn/integrations/index.html create mode 100644 website/public/zh-tw/integrations/index.html create mode 100644 website/release.json create mode 100644 website/tools/build-integrations.mjs diff --git a/.env b/.env index a1e854b0..266c61d0 100644 --- a/.env +++ b/.env @@ -1,19 +1,19 @@ # ReadMD 全局统一版本配置 (Single Source of Truth for Global Versioning) # 修改此文件中的 READMD_VERSION 或运行 cargo xtask sync-version 即可一键同步全库所有文件 -READMD_VERSION=0.0.4 -READMD_VERSION_TAG=v0.0.4 -READMD_VERSION_SEMVER=0.0.4 -READMD_VERSION_TRIPLET=0.0.4 +READMD_VERSION=0.0.5 +READMD_VERSION_TAG=v0.0.5 +READMD_VERSION_SEMVER=0.0.5 +READMD_VERSION_TRIPLET=0.0.5 READMD_VERSION_MAJOR=0 READMD_VERSION_MINOR=0 -READMD_VERSION_PATCH=4 +READMD_VERSION_PATCH=5 READMD_VERSION_PRERELEASE= -READMD_VERSION_LINGLONG=0.0.4.0 -READMD_VERSION_HARMONY_CODE=4 -READMD_VERSION_HARMONY_NAME=0.0.4 -READMD_VERSION_WINDOWS_FILEVER=0.0.4.0 -READMD_VERSION_VSCODE=0.0.4 +READMD_VERSION_LINGLONG=0.0.5.0 +READMD_VERSION_HARMONY_CODE=5 +READMD_VERSION_HARMONY_NAME=0.0.5 +READMD_VERSION_WINDOWS_FILEVER=0.0.5.0 +READMD_VERSION_VSCODE=0.0.5 READMD_VERSION_RPM_REL=1 READMD_VERSION_ARCH_REL=1 -READMD_VERSION_BUILD_DATE=2026-10-04 +READMD_VERSION_BUILD_DATE=2026-10-07 diff --git a/.env.example b/.env.example index a1e854b0..266c61d0 100644 --- a/.env.example +++ b/.env.example @@ -1,19 +1,19 @@ # ReadMD 全局统一版本配置 (Single Source of Truth for Global Versioning) # 修改此文件中的 READMD_VERSION 或运行 cargo xtask sync-version 即可一键同步全库所有文件 -READMD_VERSION=0.0.4 -READMD_VERSION_TAG=v0.0.4 -READMD_VERSION_SEMVER=0.0.4 -READMD_VERSION_TRIPLET=0.0.4 +READMD_VERSION=0.0.5 +READMD_VERSION_TAG=v0.0.5 +READMD_VERSION_SEMVER=0.0.5 +READMD_VERSION_TRIPLET=0.0.5 READMD_VERSION_MAJOR=0 READMD_VERSION_MINOR=0 -READMD_VERSION_PATCH=4 +READMD_VERSION_PATCH=5 READMD_VERSION_PRERELEASE= -READMD_VERSION_LINGLONG=0.0.4.0 -READMD_VERSION_HARMONY_CODE=4 -READMD_VERSION_HARMONY_NAME=0.0.4 -READMD_VERSION_WINDOWS_FILEVER=0.0.4.0 -READMD_VERSION_VSCODE=0.0.4 +READMD_VERSION_LINGLONG=0.0.5.0 +READMD_VERSION_HARMONY_CODE=5 +READMD_VERSION_HARMONY_NAME=0.0.5 +READMD_VERSION_WINDOWS_FILEVER=0.0.5.0 +READMD_VERSION_VSCODE=0.0.5 READMD_VERSION_RPM_REL=1 READMD_VERSION_ARCH_REL=1 -READMD_VERSION_BUILD_DATE=2026-10-04 +READMD_VERSION_BUILD_DATE=2026-10-07 diff --git a/.gitignore b/.gitignore index 8a7bff19..f4c6608a 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,6 @@ packages/readmd-pet-rust/target/ ReadMD.exe /test-results-pagination/ + +# Local incremental videos and production scripts never enter software releases. +/showcase/updates/ diff --git a/README.md b/README.md index b052ea7e..baee6107 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ ReadMD 解决的是长期资料库里的实际问题:大文件能继续读, ## 多系统与信创国产适配 ### 1. Linux 与国产操作系统适配(统信 UOS / 银河麒麟 / 深度) -- **直接安装使用**:下载 [`ReadMD-linux-x86_64.deb`](https://github.com/Natsummerance/rust-ReadMD/releases/download/V0.0.4/ReadMD-linux-x86_64.deb) 双击安装,或直接运行 [`ReadMD-linux-x86_64.tar.gz`](https://github.com/Natsummerance/rust-ReadMD/releases/download/V0.0.4/ReadMD-linux-x86_64.tar.gz)。银河麒麟 V10 + 飞腾 ARM64 设备使用 [`readmd_0.0.4_arm64.deb`](https://github.com/Natsummerance/rust-ReadMD/tree/main/scripts/linux)。 +- **直接安装使用**:下载 [`ReadMD-linux-x86_64.deb`](https://github.com/Natsummerance/rust-ReadMD/releases/download/V0.0.4/ReadMD-linux-x86_64.deb) 双击安装,或直接运行 [`ReadMD-linux-x86_64.tar.gz`](https://github.com/Natsummerance/rust-ReadMD/releases/download/V0.0.4/ReadMD-linux-x86_64.tar.gz)。银河麒麟 V10 + 飞腾 ARM64 设备使用 [`readmd_0.0.5_arm64.deb`](https://github.com/Natsummerance/rust-ReadMD/tree/main/scripts/linux)。 - **飞腾安全回退**:自动识别 Phytium D2000/E2000/FT 系列;WebKitGTK 启动时优先选择 UKUI/X11、禁用 DMABUF 合成并回退 llvmpipe,避免旧 GPU 驱动白屏或崩溃。 - **系统与环境识别**:100% 纯原生 Rust 内核(`native_system`)自动识别系统发行版,自适应配置 Wayland / X11 显示后端。 - **桌面与深色模式**:自动侦测 DDE(统信/Deepin)、UKUI(银河麒麟)与 GNOME/KDE 的外观主题,实时同步深色/浅色配色。 @@ -123,15 +123,15 @@ ReadMD 解决的是长期资料库里的实际问题:大文件能继续读, ## VSCode 扩展与 MCP Server 生态 ### 1. VSCode 插件安装与使用 -- **界面安装**:下载 [`readmd-vscode-0.0.4.vsix`](https://github.com/Natsummerance/rust-ReadMD/tree/main/packages/vscode-extension) -> 在 VSCode 扩展面板选择 `... -> 从 VSIX 安装...`。 +- **界面安装**:下载 [`readmd-vscode-0.0.5.vsix`](https://github.com/Natsummerance/rust-ReadMD/tree/main/packages/vscode-extension) -> 在 VSCode 扩展面板选择 `... -> 从 VSIX 安装...`。 - **命令行安装**: ```bash - code --install-extension readmd-vscode-0.0.4.vsix + code --install-extension readmd-vscode-0.0.5.vsix ``` - **核心功能**:打开 Markdown 文件点击右上角书本图标开启同步预览;右键菜单支持一键自动修复语法错误与转换为 LaTeX 源码。 ### 2. MCP (Model Context Protocol) Server 配置 -直接下载解压 [`readmd-mcp-server-0.0.4.zip`](https://github.com/Natsummerance/rust-ReadMD/tree/main/rust/readmd-kernel/src/mcp_server.rs),在 Claude Desktop、Cursor、Antigravity 与 Cline 中配置(请将 `args` 中的路径替换为本机解压的**绝对路径**): +直接下载解压 [`readmd-mcp-server-0.0.5.zip`](https://github.com/Natsummerance/rust-ReadMD/tree/main/rust/readmd-kernel/src/mcp_server.rs),在 Claude Desktop、Cursor、Antigravity 与 Cline 中配置(请将 `args` 中的路径替换为本机解压的**绝对路径**): **Windows**: ```json diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index ac0b67c9..ef9dd8b6 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,21 @@ +# ReadMD V0.0.5 · 发布候选 + +本轮补强本地 Markdown 工作流,复用现有入口,重点提升文档检查、工作区检索、VS Code 与 MCP 文件操作。正式发布前需完成平台打包与验收;当前公开下载仍为 V0.0.4。 + +- **文档检查**:当前草稿的标题、双链、本地链接与锚点由共用 Rust AST 检查,跳过代码示例;结果过期、处理上限和检查失败明确显示。桌面端放入原有修复报告。 + +- **VS Code**:原生“问题”面板诊断和可点击 Wiki 双链;命令面板搜索当前工作区,支持短语、路径、标题、YAML 标签及排除词。保持工具栏不增加新入口,缓存跟随文档版本与文件变化失效。 + +- **MCP**:22 → 27 项工具。新增文档检查、工作区搜索、带版本读取、默认预览的文字替换与恢复记录读取。提交需明确确认及匹配文件版本;保护编码、未改部分的换行,拒绝不可无损编码及超限结果。恢复集中保存,不污染原目录。 + +- **网站**:保留原有 Apple 风格,补齐四语言集成说明及搜索收录信息。修复旧仓库动态版本请求、大小写标签、过期版本说明、源码误标为安装包;区分公开正式版和待发布候选,避免无效下载。 + +- **验证与演示**:增量操作使用真实桌面应用、VS Code Extension Host 和 Rust MCP;合成哲学阅读材料,2K 原生录制,软件画面不含文件夹或录制编号。制作工程和视频仅保存在本机,不进入软件提交。 + +操作清单、测试证据与边界见 docs/reviews/readmd-v005-upgrade-2026-10-07.md。 + +--- + # ReadMD V0.0.4 本次版本整合桌宠、编辑保存、功能面板、原生窗口与完整演示网站的升级。程序、安装包和更新检查统一使用 `0.0.4`,更新源切换到 `Natsummerance/rust-ReadMD`。 diff --git a/VERSION b/VERSION index 81340c7e..bbdeab62 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.4 +0.0.5 diff --git a/assets/css/panels.css b/assets/css/panels.css index 0a993779..5ec06716 100644 --- a/assets/css/panels.css +++ b/assets/css/panels.css @@ -1353,3 +1353,6 @@ body:has(#edit-bar:not(.hidden)) .statusbar-left-group { flex:0 1 24%; } #share-refresh { margin-inline-start: 0; } #search-count { max-width: 30%; font-size: var(--text-xs); } } +/* Current-draft inspection shares the existing repair report. */ +#fix-list .document-inspection-summary { font-weight: 600; margin-top: 12px; } +#fix-list .document-inspection-issue { overflow-wrap: anywhere; } diff --git a/assets/i18n/ar.json b/assets/i18n/ar.json index 2fc01e6a..f325166c 100644 --- a/assets/i18n/ar.json +++ b/assets/i18n/ar.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "إجراءات عامة", "window.assocChoose": "اختر ReadMD لملفات Markdown في تطبيقات Windows الافتراضية.", "window.close": "إغلاق النافذة", diff --git a/assets/i18n/bn.json b/assets/i18n/bn.json index 9ac8ec2f..138613ae 100644 --- a/assets/i18n/bn.json +++ b/assets/i18n/bn.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "সার্বিক কাজ", "window.assocChoose": "উইন্ডোজ ডিফল্ট অ্যাপসে মার্কডাউন ফাইলের জন্য ReadMD বেছে নিন।", "window.close": "উইন্ডো বন্ধ করুন", diff --git a/assets/i18n/bo.json b/assets/i18n/bo.json index 8ba7a10c..2e28e1ce 100644 --- a/assets/i18n/bo.json +++ b/assets/i18n/bo.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "སྤྱི་ཁྱབ་བྱ་ཐབས།", "window.assocChoose": "Windows ཀྱི་སྔོན་སྒྲིག་ཉེར་སྤྱོད་ནང་ Markdown ཡིག་ཆའི་ཆེད་དུ་ ReadMD འདེམས་རོགས།", "window.close": "སྒེའུ་ཁུང་རྒྱག་པ།", diff --git a/assets/i18n/da.json b/assets/i18n/da.json index d323414f..18005a60 100644 --- a/assets/i18n/da.json +++ b/assets/i18n/da.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Globale handlinger", "window.assocChoose": "Vælg ReadMD til Markdown-filer i Standardapps i Windows.", "window.close": "Luk vindue", diff --git a/assets/i18n/de.json b/assets/i18n/de.json index 920bd3f0..4b80b13e 100644 --- a/assets/i18n/de.json +++ b/assets/i18n/de.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Globale Aktionen", "window.assocChoose": "Wählen Sie ReadMD für Markdown-Dateien in den Windows-Standard-Apps.", "window.close": "Fenster schließen", diff --git a/assets/i18n/el.json b/assets/i18n/el.json index 791e13bd..de9290b2 100644 --- a/assets/i18n/el.json +++ b/assets/i18n/el.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Καθολικές ενέργειες", "window.assocChoose": "Επιλέξτε το ReadMD για αρχεία Markdown στις Προεπιλεγμένες εφαρμογές των Windows.", "window.close": "Κλείσιμο παραθύρου", diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 444e42fc..0fd1ee22 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Global actions", "window.assocChoose": "Choose ReadMD for Markdown files in Windows Default apps.", "window.close": "Close window", diff --git a/assets/i18n/eo.json b/assets/i18n/eo.json index 58e1336e..48acbdda 100644 --- a/assets/i18n/eo.json +++ b/assets/i18n/eo.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Tutmondaj agoj", "window.assocChoose": "Elektu ReadMD por Markdown-dosieroj en la defaŭltaj aplikaĵoj de Vindozo.", "window.close": "Fermi fenestron", diff --git a/assets/i18n/es.json b/assets/i18n/es.json index 7992ef4f..2b91d7af 100644 --- a/assets/i18n/es.json +++ b/assets/i18n/es.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Acciones globales", "window.assocChoose": "Elegir ReadMD para archivos Markdown en las aplicaciones predeterminadas de Windows.", "window.close": "Cerrar ventana", diff --git a/assets/i18n/fi.json b/assets/i18n/fi.json index 5611180b..b5f05bbb 100644 --- a/assets/i18n/fi.json +++ b/assets/i18n/fi.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Yleiset toiminnot", "window.assocChoose": "Valitse ReadMD Markdown-tiedostojen oletussovellukseksi Windowsin oletussovelluksissa.", "window.close": "Sulje ikkuna", diff --git a/assets/i18n/fr.json b/assets/i18n/fr.json index 43d795bf..babeaa9c 100644 --- a/assets/i18n/fr.json +++ b/assets/i18n/fr.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Actions globales", "window.assocChoose": "Choisissez ReadMD pour les fichiers Markdown dans les applications par défaut de Windows.", "window.close": "Fermer la fenêtre", diff --git a/assets/i18n/ga.json b/assets/i18n/ga.json index 726705e0..48bf2bdf 100644 --- a/assets/i18n/ga.json +++ b/assets/i18n/ga.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Gníomhartha domhanda", "window.assocChoose": "Roghnaigh ReadMD do chomhaid Markdown i gCláir Réamhshocraithe Windows.", "window.close": "Dún an fhuinneog", diff --git a/assets/i18n/he.json b/assets/i18n/he.json index a7506967..35982ce7 100644 --- a/assets/i18n/he.json +++ b/assets/i18n/he.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "פעולות גלובליות", "window.assocChoose": "בחר ב-ReadMD עבור קבצי Markdown באפליקציות ברירת המחדל של Windows.", "window.close": "סגור חלון", diff --git a/assets/i18n/hi.json b/assets/i18n/hi.json index d4bb5e49..315d6c6a 100644 --- a/assets/i18n/hi.json +++ b/assets/i18n/hi.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "वैश्विक क्रियाएँ", "window.assocChoose": "Windows डिफ़ॉल्ट ऐप्स में Markdown फ़ाइलों के लिए ReadMD चुनें।", "window.close": "विंडो बंद करें", diff --git a/assets/i18n/hr.json b/assets/i18n/hr.json index 544b2fd1..9002c6c5 100644 --- a/assets/i18n/hr.json +++ b/assets/i18n/hr.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Globalne radnje", "window.assocChoose": "Odaberite ReadMD za Markdown datoteke u zadanim aplikacijama sustava Windows.", "window.close": "Zatvori prozor", diff --git a/assets/i18n/hu.json b/assets/i18n/hu.json index b08c4e37..8ff884a5 100644 --- a/assets/i18n/hu.json +++ b/assets/i18n/hu.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Globális műveletek", "window.assocChoose": "A ReadMD választása Markdown fájlokhoz a Windows alapértelmezett alkalmazásaiban.", "window.close": "Ablak bezárása", diff --git a/assets/i18n/id.json b/assets/i18n/id.json index d66d787b..f927a250 100644 --- a/assets/i18n/id.json +++ b/assets/i18n/id.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Acciones globales", "window.assocChoose": "Elegir ReadMD para archivos Markdown en las aplicaciones predeterminadas de Windows.", "window.close": "Cerrar ventana", diff --git a/assets/i18n/it.json b/assets/i18n/it.json index 3311b406..8cdc6b03 100644 --- a/assets/i18n/it.json +++ b/assets/i18n/it.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Azioni globali", "window.assocChoose": "Scegli ReadMD per i file Markdown nelle app predefinite di Windows.", "window.close": "Chiudi finestra", diff --git a/assets/i18n/ja.json b/assets/i18n/ja.json index 407a196e..230711ac 100644 --- a/assets/i18n/ja.json +++ b/assets/i18n/ja.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "グローバルアクション", "window.assocChoose": "Windowsの既定のアプリでMarkdownファイルのReadMDを選択します。", "window.close": "ウィンドウを閉じる", diff --git a/assets/i18n/kg.json b/assets/i18n/kg.json index 4fa5e50c..cd14a38c 100644 --- a/assets/i18n/kg.json +++ b/assets/i18n/kg.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Bisalu ya mvimba", "window.assocChoose": "Ponaka ReadMD sambu na balaki ya Markdown na bisalu ya luyantiku ya Windows.", "window.close": "Kanga diwindu", diff --git a/assets/i18n/kl.json b/assets/i18n/kl.json index 8ed39acb..3968836f 100644 --- a/assets/i18n/kl.json +++ b/assets/i18n/kl.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Iliuutsit tamakkiisut", "window.assocChoose": "Windows-ip nalinginnaasumik atortuini Markdown fiilit ReadMD atorlugit toqqakkit.", "window.close": "Igalaaq matuuk", diff --git a/assets/i18n/km.json b/assets/i18n/km.json index eeb24883..07d5caea 100644 --- a/assets/i18n/km.json +++ b/assets/i18n/km.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "សកម្មភាពទូទៅ", "window.assocChoose": "ជ្រើសរើស ReadMD សម្រាប់ឯកសារ Markdown ក្នុងកម្មវិធីលំនាំដើមរបស់ Windows។", "window.close": "បិទបង្អួច", diff --git a/assets/i18n/ko.json b/assets/i18n/ko.json index 65e5295a..a5548f0a 100644 --- a/assets/i18n/ko.json +++ b/assets/i18n/ko.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "전역 작업", "window.assocChoose": "Windows 기본 앱에서 Markdown 파일의 ReadMD를 선택하세요.", "window.close": "창 닫기", diff --git a/assets/i18n/lo.json b/assets/i18n/lo.json index 5fe86233..cd580d56 100644 --- a/assets/i18n/lo.json +++ b/assets/i18n/lo.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "ການກະທຳທົ່ວໄປ", "window.assocChoose": "ເລືອກ ReadMD ສຳລັບໄຟລ໌ Markdown ໃນແອັບເລີ່ມຕົ້ນຂອງ Windows.", "window.close": "ປິດໜ້າຕ່າງ", diff --git a/assets/i18n/mn.json b/assets/i18n/mn.json index b37b9b3f..e6d5b405 100644 --- a/assets/i18n/mn.json +++ b/assets/i18n/mn.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Ерөнхий үйлдлүүд", "window.assocChoose": "Windows-ийн үндсэн аппууд дотроос Markdown файлуудад ReadMD-г сонгоно уу.", "window.close": "Цонх хаах", diff --git a/assets/i18n/ms.json b/assets/i18n/ms.json index 0b1aa528..dd64b8ce 100644 --- a/assets/i18n/ms.json +++ b/assets/i18n/ms.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Tindakan global", "window.assocChoose": "Pilih ReadMD untuk fail Markdown dalam Apl Lalai Windows.", "window.close": "Tutup tetingkap", diff --git a/assets/i18n/mt.json b/assets/i18n/mt.json index 9c21ec84..cafd8f5b 100644 --- a/assets/i18n/mt.json +++ b/assets/i18n/mt.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Azzjonijiet globali", "window.assocChoose": "Agħżel ReadMD għall-fajls Markdown fl-apps default tal-Windows.", "window.close": "Agħlaq it-tieqa", diff --git a/assets/i18n/my.json b/assets/i18n/my.json index 3efbcb81..1442780d 100644 --- a/assets/i18n/my.json +++ b/assets/i18n/my.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "ကမ္ဘာလုံးဆိုင်ရာ လုပ်ဆောင်ချက်များ", "window.assocChoose": "Windows ပုံသေ အက်ပ်များတွင် Markdown ဖိုင်များအတွက် ReadMD ကို ရွေးချယ်ပါ။", "window.close": "ဝင်းဒိုးကို ပိတ်မည်", diff --git a/assets/i18n/ne.json b/assets/i18n/ne.json index 6949658e..2a5bd08d 100644 --- a/assets/i18n/ne.json +++ b/assets/i18n/ne.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "वैश्विक कार्यहरू", "window.assocChoose": "विन्डोज डिफल्ट एप्समा मार्कडाउन फाइलहरूका लागि ReadMD छान्नुहोस्।", "window.close": "विन्डो बन्द गर्नुहोस्", diff --git a/assets/i18n/nl.json b/assets/i18n/nl.json index 85a96fde..2ee76901 100644 --- a/assets/i18n/nl.json +++ b/assets/i18n/nl.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Algemene acties", "window.assocChoose": "Kies ReadMD voor Markdown-bestanden in Standaard-apps van Windows.", "window.close": "Venster sluiten", diff --git a/assets/i18n/no.json b/assets/i18n/no.json index 75dead68..f2a3cab6 100644 --- a/assets/i18n/no.json +++ b/assets/i18n/no.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Globale handlinger", "window.assocChoose": "Velg ReadMD for Markdown-filer i Windows-standardapper.", "window.close": "Lukk vindu", diff --git a/assets/i18n/pt.json b/assets/i18n/pt.json index d6021b61..b8abd47d 100644 --- a/assets/i18n/pt.json +++ b/assets/i18n/pt.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Ações globais", "window.assocChoose": "Escolher o ReadMD para ficheiros Markdown nas aplicações predefinidas do Windows.", "window.close": "Fechar janela", diff --git a/assets/i18n/ro.json b/assets/i18n/ro.json index 74b36980..98e01492 100644 --- a/assets/i18n/ro.json +++ b/assets/i18n/ro.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Acțiuni globale", "window.assocChoose": "Alegeți ReadMD pentru fișierele Markdown în Aplicațiile implicite din Windows.", "window.close": "Închide fereastra", diff --git a/assets/i18n/ru.json b/assets/i18n/ru.json index 754f7521..5f0631c6 100644 --- a/assets/i18n/ru.json +++ b/assets/i18n/ru.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Общие действия", "window.assocChoose": "Выберите ReadMD для файлов Markdown в приложениях по умолчанию Windows.", "window.close": "Закрыть окно", diff --git a/assets/i18n/rw.json b/assets/i18n/rw.json index dde2b8cb..355a5f45 100644 --- a/assets/i18n/rw.json +++ b/assets/i18n/rw.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Ibikorwa rusange", "window.assocChoose": "Hitamo ReadMD kuri dosiye za Markdown muri Porogaramu zisanzwe za Windows.", "window.close": "Gufunga idirishya", diff --git a/assets/i18n/sl.json b/assets/i18n/sl.json index 201a2ba9..804f1732 100644 --- a/assets/i18n/sl.json +++ b/assets/i18n/sl.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Globalna dejanja", "window.assocChoose": "Izberite ReadMD za datoteke Markdown v privzetih aplikacijah sistema Windows.", "window.close": "Zapri okno", diff --git a/assets/i18n/sv.json b/assets/i18n/sv.json index ddf83085..96d88c2e 100644 --- a/assets/i18n/sv.json +++ b/assets/i18n/sv.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Globala åtgärder", "window.assocChoose": "Välj ReadMD för Markdown-filer i Windows standardappar.", "window.close": "Stäng fönster", diff --git a/assets/i18n/th.json b/assets/i18n/th.json index e2840868..e4c75d2b 100644 --- a/assets/i18n/th.json +++ b/assets/i18n/th.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "การดำเนินการทั่วไป", "window.assocChoose": "เลือก ReadMD สำหรับไฟล์ Markdown ในแอปเริ่มต้นของ Windows", "window.close": "ปิดหน้าต่าง", diff --git a/assets/i18n/tl.json b/assets/i18n/tl.json index 3d3fc352..7f39cd1e 100644 --- a/assets/i18n/tl.json +++ b/assets/i18n/tl.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Mga pangkalahatang aksyon", "window.assocChoose": "Piliin ang ReadMD para sa mga Markdown file sa Windows Default apps.", "window.close": "Isara ang bintana", diff --git a/assets/i18n/tr.json b/assets/i18n/tr.json index d25ca138..685c9f5a 100644 --- a/assets/i18n/tr.json +++ b/assets/i18n/tr.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Genel eylemler", "window.assocChoose": "Windows Varsayılan uygulamalarında Markdown dosyaları için ReadMD'yi seçin.", "window.close": "Pencereyi kapat", diff --git a/assets/i18n/ug.json b/assets/i18n/ug.json index dffb9a28..5139d967 100644 --- a/assets/i18n/ug.json +++ b/assets/i18n/ug.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "ئومۇمىي مەشغۇلاتلار", "window.assocChoose": "Windows كۆڭۈلدىكى ئەپلىرىدە Markdown ھۆججەتلىرى ئۈچۈن ReadMD نى تاللاڭ.", "window.close": "كۆزنەكنى تاقاش", diff --git a/assets/i18n/uk.json b/assets/i18n/uk.json index bdcedd3c..5f2466df 100644 --- a/assets/i18n/uk.json +++ b/assets/i18n/uk.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Загальні дії", "window.assocChoose": "Вибрати ReadMD для файлів Markdown у програмах за замовчуванням Windows.", "window.close": "Закрити вікно", diff --git a/assets/i18n/vi.json b/assets/i18n/vi.json index 780795f8..b5d82c5d 100644 --- a/assets/i18n/vi.json +++ b/assets/i18n/vi.json @@ -1,4 +1,15 @@ { + "inspection.changed": "The document changed. Reopen this report to inspect the latest content.", + "inspection.ambiguous_link": "Multiple files match this link", + "inspection.atLine": "Line {line}", + "inspection.failed": "Document inspection is unavailable. Check the local service and try again.", + "inspection.limited": "Some checks reached processing limits; unchecked content may still contain issues.", + "inspection.loading": "Checking links and headings in the current document…", + "inspection.missing_anchor": "Heading anchor not found", + "inspection.missing_file": "Local file not found", + "inspection.missing_footnote": "Footnote definition not found", + "inspection.outside_workspace": "Link is outside the inspected folder and was not read", + "inspection.summary": "Document inspection: {count} items to review", "window.globalActions": "Thao tác chung", "window.assocChoose": "Chọn ReadMD cho các tệp Markdown trong ứng dụng Mặc định của Windows.", "window.close": "Đóng cửa sổ", diff --git a/assets/i18n/zh-CN.json b/assets/i18n/zh-CN.json index 678b303e..6e67ff37 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -1,4 +1,15 @@ { + "inspection.changed": "文档已变化,请重新打开报告以检查最新内容。", + "inspection.ambiguous_link": "同名链接目标不唯一", + "inspection.atLine": "第 {line} 行", + "inspection.failed": "文档检查暂不可用,请确认本地服务正常后重试。", + "inspection.limited": "部分检查达到处理上限,未检查的内容不代表没有问题。", + "inspection.loading": "正在检查当前文档的链接与标题…", + "inspection.missing_anchor": "未找到目标标题", + "inspection.missing_file": "未找到本地文件", + "inspection.missing_footnote": "未找到脚注定义", + "inspection.outside_workspace": "链接位于检查文件夹以外,未读取", + "inspection.summary": "文档检查:{count} 项待检查", "window.globalActions": "全局操作", "window.assocChoose": "请在 Windows 默认应用中,将 Markdown 文件的打开方式选为 ReadMD。", "window.close": "关闭窗口", diff --git a/assets/i18n/zh-HK.json b/assets/i18n/zh-HK.json index 9fdbd770..c1b7e1bc 100644 --- a/assets/i18n/zh-HK.json +++ b/assets/i18n/zh-HK.json @@ -1,4 +1,15 @@ { + "inspection.changed": "文件已變更,請重新開啟報告以檢查最新內容。", + "inspection.ambiguous_link": "同名連結目標不唯一", + "inspection.atLine": "第 {line} 行", + "inspection.failed": "文件檢查暫不可用,請確認本機服務正常後重試。", + "inspection.limited": "部分檢查已達處理上限,未檢查的內容不代表沒有問題。", + "inspection.loading": "正在檢查目前文件的連結與標題…", + "inspection.missing_anchor": "找不到目標標題", + "inspection.missing_file": "找不到本機檔案", + "inspection.missing_footnote": "找不到註腳定義", + "inspection.outside_workspace": "連結位於檢查資料夾以外,未讀取", + "inspection.summary": "文件檢查:{count} 項待檢查", "window.globalActions": "全域操作", "window.assocChoose": "請在 Windows 預設應用程式中,將 Markdown 檔案的開啟方式選為 ReadMD。", "window.close": "關閉視窗", diff --git a/assets/i18n/zh-TW.json b/assets/i18n/zh-TW.json index 64201895..bafa8ba2 100644 --- a/assets/i18n/zh-TW.json +++ b/assets/i18n/zh-TW.json @@ -1,4 +1,15 @@ { + "inspection.changed": "文件已變更,請重新開啟報告以檢查最新內容。", + "inspection.ambiguous_link": "同名連結目標不唯一", + "inspection.atLine": "第 {line} 行", + "inspection.failed": "文件檢查暫不可用,請確認本機服務正常後重試。", + "inspection.limited": "部分檢查已達處理上限,未檢查的內容不代表沒有問題。", + "inspection.loading": "正在檢查目前文件的連結與標題…", + "inspection.missing_anchor": "找不到目標標題", + "inspection.missing_file": "找不到本機檔案", + "inspection.missing_footnote": "找不到註腳定義", + "inspection.outside_workspace": "連結位於檢查資料夾以外,未讀取", + "inspection.summary": "文件檢查:{count} 項待檢查", "window.globalActions": "全域操作", "window.assocChoose": "請在 Windows 預設應用程式中,將 Markdown 檔案的開啟方式選為 ReadMD。", "window.close": "關閉視窗", diff --git a/assets/index.html b/assets/index.html index 53139709..e458608e 100644 --- a/assets/index.html +++ b/assets/index.html @@ -1,5 +1,5 @@ - + @@ -9,16 +9,16 @@ ReadMD - - - - - - - - - - + + + + + + + + + +
@@ -122,7 +122,7 @@ - + @@ -1475,7 +1475,7 @@

插入表格

- v0.0.4 + v0.0.5
@@ -1707,6 +1707,6 @@

配置文档 - + diff --git a/assets/js/reader/fixes.js b/assets/js/reader/fixes.js index 1e439a09..4d640c5e 100644 --- a/assets/js/reader/fixes.js +++ b/assets/js/reader/fixes.js @@ -5,6 +5,7 @@ /* ---------------- 修正详情 ---------------- */ +let documentInspectionGeneration = 0; function showFixModal() { const _t = (k, p) => window.i18n ? window.i18n.t(k, p) : k; const list = $('fix-list'); @@ -28,6 +29,43 @@ function showFixModal() { } const modal = $('fix-modal'); if (modal) modal.classList.remove('hidden'); + void inspectCurrentDocument(++documentInspectionGeneration); +} + +async function inspectCurrentDocument(generation) { + const text = state.editing ? getEditContent() : (state.original ?? state.fixed ?? ''); + const tabId = state.activeTabId; + const file = state.file && !state.virtualSource ? state.file : ''; + const list = $('fix-list'); + const status = document.createElement('li'); + status.className = 'document-inspection-summary'; + status.textContent = i18n.t('inspection.loading'); + list?.appendChild(status); + try { + const response = await apiFetch('/api/document/analyze', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content: text, ...(file && /^(?:[A-Za-z]:[\\/]|\/)/.test(file) ? { file_path: file } : {}) }) + }); + const result = await response.json(); + if (generation !== documentInspectionGeneration || tabId !== state.activeTabId || $('fix-modal').classList.contains('hidden')) return; + if (text !== (state.editing ? getEditContent() : (state.original ?? state.fixed ?? ''))) { + status.textContent = i18n.t('inspection.changed'); return; + } + if (!response.ok || !result.ok) throw new Error('inspection_failed'); + status.textContent = i18n.t('inspection.summary', { count: result.diagnostics.length }); + for (const issue of result.diagnostics) { + const row = document.createElement('li'); + row.className = 'document-inspection-issue'; + const key = 'inspection.' + issue.code; + row.textContent = i18n.t('inspection.atLine', { line: issue.position.line }) + ' · ' + i18n.t(key) + ' · ' + issue.target; + list?.appendChild(row); + } + if (result.truncated) { + const row = document.createElement('li'); row.textContent = i18n.t('inspection.limited'); list?.appendChild(row); + } + } catch (_) { + if (generation === documentInspectionGeneration && tabId === state.activeTabId && status.isConnected) status.textContent = i18n.t('inspection.failed'); + } } async function handleAiDocumentFix() { diff --git a/assets/readmd.boot.js b/assets/readmd.boot.js index 44a977e7..4300358b 100644 --- a/assets/readmd.boot.js +++ b/assets/readmd.boot.js @@ -3339,6 +3339,7 @@ function renderMath(body) { /* ---------------- 修正详情 ---------------- */ +let documentInspectionGeneration = 0; function showFixModal() { const _t = (k, p) => window.i18n ? window.i18n.t(k, p) : k; const list = $('fix-list'); @@ -3362,6 +3363,43 @@ function showFixModal() { } const modal = $('fix-modal'); if (modal) modal.classList.remove('hidden'); + void inspectCurrentDocument(++documentInspectionGeneration); +} + +async function inspectCurrentDocument(generation) { + const text = state.editing ? getEditContent() : (state.original ?? state.fixed ?? ''); + const tabId = state.activeTabId; + const file = state.file && !state.virtualSource ? state.file : ''; + const list = $('fix-list'); + const status = document.createElement('li'); + status.className = 'document-inspection-summary'; + status.textContent = i18n.t('inspection.loading'); + list?.appendChild(status); + try { + const response = await apiFetch('/api/document/analyze', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content: text, ...(file && /^(?:[A-Za-z]:[\\/]|\/)/.test(file) ? { file_path: file } : {}) }) + }); + const result = await response.json(); + if (generation !== documentInspectionGeneration || tabId !== state.activeTabId || $('fix-modal').classList.contains('hidden')) return; + if (text !== (state.editing ? getEditContent() : (state.original ?? state.fixed ?? ''))) { + status.textContent = i18n.t('inspection.changed'); return; + } + if (!response.ok || !result.ok) throw new Error('inspection_failed'); + status.textContent = i18n.t('inspection.summary', { count: result.diagnostics.length }); + for (const issue of result.diagnostics) { + const row = document.createElement('li'); + row.className = 'document-inspection-issue'; + const key = 'inspection.' + issue.code; + row.textContent = i18n.t('inspection.atLine', { line: issue.position.line }) + ' · ' + i18n.t(key) + ' · ' + issue.target; + list?.appendChild(row); + } + if (result.truncated) { + const row = document.createElement('li'); row.textContent = i18n.t('inspection.limited'); list?.appendChild(row); + } + } catch (_) { + if (generation === documentInspectionGeneration && tabId === state.activeTabId && status.isConnected) status.textContent = i18n.t('inspection.failed'); + } } async function handleAiDocumentFix() { diff --git a/docs/reviews/readmd-v005-upgrade-2026-10-07.md b/docs/reviews/readmd-v005-upgrade-2026-10-07.md new file mode 100644 index 00000000..bddda579 --- /dev/null +++ b/docs/reviews/readmd-v005-upgrade-2026-10-07.md @@ -0,0 +1,51 @@ +# ReadMD V0.0.5 迭代与验收清单 + +目标:从已有社区调研吸收适合本地文档工作流的能力,复用原入口,完成可验证的发布候选。范围并非声称覆盖所有 Markdown 项目,也不承诺任意输入永不出错。 + +## 取舍依据 + +沿用 [社区调研矩阵](readmd-community-research-2026-10-07.md),覆盖阅读、即时预览、结构编辑、文档转换、学术写作、知识链接、演讲、编辑器与 AI 协作。既有公式、图表、目录、插件、托盘、桌宠、转换及导出入口继续使用,不创建平行的功能面板。 + +| 参考能力 | 本轮落地 | 入口 | +| --- | --- | --- | +| [Obsidian 本地链接](https://obsidian.md/help/links)、[属性](https://obsidian.md/help/properties) | AST 双链解析、同名歧义检测、本地目标及锚点检查、常用 YAML 标签检索 | 桌面修复报告、VS Code、MCP | +| [VS Code Markdown](https://code.visualstudio.com/docs/languages/markdown) | 原生 Problems 诊断与 Wiki DocumentLink,版本过期结果丢弃 | VS Code 原生面板与命令 | +| 常见笔记库检索 | 当前文件直接检索,组合短语、路径、标题、标签与排除词;不依赖过期索引 | VS Code Quick Pick、MCP、共用 HTTP 接口 | +| 文字处理软件的保存与审阅原则 | AI 工具先预览、后确认;字节版本冲突检测、原子写入、编码及未改部分换行保留、集中恢复历史 | MCP;复用桌面保存与恢复底层 | +| [Google 搜索入门](https://developers.google.com/search/docs/fundamentals/seo-starter-guide) | 四语言集成页、准确版本和下载入口、canonical/hreflang、站点地图与 Atom/crawler 文本同步 | 现有网站 | + +不增加独立知识库数据库、同步服务、第二套编辑器或新的顶部工具条。搜索不执行正则表达式、不联网抓取链接、不扫描隐藏或生成目录。 + +## 增量操作清单 + +1. **桌面文档检查**:打开文档或编辑草稿 → 更多/互动/修复详情 → 查看链接检查。只检查当前内容;异步返回后若切换标签或修改文本,结果作废。不自动改写链接。 +2. **VS Code 文档检查**:打开已信任工作区的 Markdown 文件 → 命令面板“ReadMD: 检查文档链接与标题” → 原生“问题”面板定位。活动草稿修改后防抖更新,保存、文件创建/删除/重命名后失效旧缓存。 +3. **VS Code Wiki 跳转**:打开包含双链的文档 → 原生链接操作跳转;明确工作区范围,重名目标不猜测。复用 VS Code 的标准 Markdown 链接能力。 +4. **工作区检索**:命令面板“ReadMD: 搜索本地 Markdown 工作区” → 输入标签、短语与排除词 → Quick Pick 显示标题、相对路径和摘要 → 打开结果行。多工作区先选择范围,取消请求不应用迟到结果。 +5. **MCP 读取**:readmd_read_document 返回指定行段、原始字节 SHA-256、编码及换行信息。 +6. **MCP 修改预览**:readmd_edit_document 携带文件版本及文字替换,默认只预览。多处匹配须明确 replace_all,不执行模糊替换。 +7. **MCP 提交**:相同版本 + dry_run:false, confirm:true → 校验内容大小与编码 → 集中恢复记录 → 重新检查版本 → 原子写入。文件已变化则停止,不生成原目录副本。 +8. **MCP 历史**:readmd_document_history 列表或读取同一文档记录;原文件删除后仍可读取历史。恢复需先审阅并获取当前版本,不自动覆盖未保存的编辑器草稿。 + +MCP 从 22 项增加到 27 项;VS Code 从 22 条贡献命令增加到 24 条。新增桌面文案经 i18n-add.mjs 同步 46 个语言包,简中、繁中、英文提供原生文案,其余新增项使用英文回退。 + +## 文件安全与上限 + +- 单次检查/编辑文件最多 2 MiB;读取最多 1,000 行。拒绝不能无损重新编码的输入。 +- 搜索最多 5,000 个 Markdown 文件、累计 64 MiB;返回最多 100 项。处理上限及跳过数明确返回,不把未检查内容当作通过。 +- 标题与链接最多各 2,000 项、诊断最多 200 项、跨文档锚点最多检查 32 个目标。大目标无法检查时报告截断,避免误报缺失。 +- 恢复沿用应用数据区:每文档最多 10 项、总计 64 MiB、最长 30 天。原目录不会为 MCP 编辑增加 .bak。 +- MCP 最多并行 8 个工具,超额明确 server_busy。取消不会撤回已提交写入;跨程序写入仍可能在最终版本检查与原子替换之间竞争,不能把它描述成操作系统事务锁。 +- MCP 文件工具处理磁盘文件;未知其他编辑器的未保存缓冲区,不应绕过宿主编辑器直接替换其草稿。 + +## 网站与后续复用 + +网站沿用现有 Apple 风格。website/release.json 区分公开正式版与候选版,离线构建使用它生成 version.json。正式发布并验证完整资产后再将 stable 改为新版本;同步工具不会把未发布候选写进公开下载链接。 + +node website/tools/build-integrations.mjs 生成四语言集成说明。所有正式安装、VSIX、MCP 连接包和校验文件均指向对应发布资产;MCP 连接包不被描述成独立服务端二进制。 + +增量演示位于本机 showcase/updates/v0.0.5/,使用合成哲学阅读材料、真实桌面应用、真实 Extension Host 与 Rust MCP。新管线不改写旧 104 项录制清单。画面不包含系统文件夹、私人路径、录制编号或提示叠层;媒体和制作脚本不进入软件提交。 + +## 验收记录 + +此处随后填入最终实测结果;未完成的检查不得标为通过。跨平台安装、上游 AI 服务和不同真实网络仍有环境边界,不能从本机测试推断全部通过。 diff --git a/packages/harmonyos-app/AppScope/app.json5 b/packages/harmonyos-app/AppScope/app.json5 index e3872d16..18a02049 100644 --- a/packages/harmonyos-app/AppScope/app.json5 +++ b/packages/harmonyos-app/AppScope/app.json5 @@ -2,8 +2,8 @@ "app": { "bundleName": "io.github.natsummerance.readmd", "vendor": "Natsummerance", - "versionCode": 4, - "versionName": "0.0.4", + "versionCode": 5, + "versionName": "0.0.5", "icon": "$media:app_icon", "label": "$string:app_name" } diff --git a/packages/harmonyos-app/entry/oh-package.json5 b/packages/harmonyos-app/entry/oh-package.json5 index b372e24a..d1c04e9d 100644 --- a/packages/harmonyos-app/entry/oh-package.json5 +++ b/packages/harmonyos-app/entry/oh-package.json5 @@ -1,6 +1,6 @@ { "name": "entry", - "version": "0.0.4", + "version": "0.0.5", "description": "ReadMD HarmonyOS entry module", "license": "MIT", "dependencies": {} diff --git a/packages/harmonyos-app/oh-package.json5 b/packages/harmonyos-app/oh-package.json5 index 60d81cbf..f82ac291 100644 --- a/packages/harmonyos-app/oh-package.json5 +++ b/packages/harmonyos-app/oh-package.json5 @@ -1,6 +1,6 @@ { "name": "readmd-harmonyos", - "version": "0.0.4", + "version": "0.0.5", "description": "ReadMD HarmonyOS NEXT / OpenHarmony shell", "license": "MIT", "dependencies": {} diff --git a/packages/harmonyos-app/package.json b/packages/harmonyos-app/package.json index dd2aedc0..9ab1d538 100644 --- a/packages/harmonyos-app/package.json +++ b/packages/harmonyos-app/package.json @@ -1,6 +1,6 @@ { "name": "readmd-harmonyos", - "version": "0.0.4", + "version": "0.0.5", "description": "ReadMD for HarmonyOS NEXT & OpenHarmony", "main": "index.ets", "author": "Natsummerance", diff --git a/packages/linglong/linglong.yaml b/packages/linglong/linglong.yaml index edf1c013..318232b1 100644 --- a/packages/linglong/linglong.yaml +++ b/packages/linglong/linglong.yaml @@ -3,7 +3,7 @@ version: "1" package: id: io.github.natsummerance.readmd name: ReadMD - version: 0.0.4.0 + version: 0.0.5.0 kind: app description: | ReadMD —— 轻量级本地 Markdown 阅读器与编辑器。 diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index 718d03a3..9c20d9a7 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -2,7 +2,7 @@ ReadMD 的 MCP 服务内置在桌面端可执行程序中:`readmd --mcp` 在标准输入输出上提供 JSON-RPC。支持 MCP 2026-07-28 的逐请求协议发现,以及 2025-11-25 / 2025-06-18 / 2025-03-26 / 2024-11-05 的初始化握手。它调用与桌面端相同的 Rust 转换、导出、OCR、Skills 和 AI 代码,不需要 Python、窗口或监听端口。 -`readmd-mcp-server-0.0.4.zip` 是文档与连接模板包;可执行内核随桌面应用提供,不在这个小型配置包中重复附带。 +`readmd-mcp-server-0.0.5.zip` 是文档与连接模板包;可执行内核随桌面应用提供,不在这个小型配置包中重复附带。 ## 快速开始 @@ -39,7 +39,7 @@ VS Code 扩展的「ReadMD: 一键配置工作区 MCP Server」会自动探测 R ## 工具 -以客户端返回的 `tools/list` 为准,共 22 项: +以客户端返回的 `tools/list` 为准,V0.0.5 共 27 项: `readmd_fix_markdown`、`readmd_convert_to_markdown`、`readmd_web_to_markdown`、`readmd_ocr_to_markdown`、`readmd_export_document`、`readmd_latex_to_md`、`readmd_md_to_latex`、`readmd_parse_bibtex`、`readmd_latex_to_omml`、`readmd_ai_assistant`、`readmd_ai_providers`、`readmd_ai_chat`、`readmd_process_imports`、`readmd_generate_toc`、`readmd_export_presentation`、`readmd_export_epub`、`readmd_run_code_chunk`、`readmd_pdf_audit`、`readmd_pdf_rollback`。 @@ -47,6 +47,18 @@ VS Code 扩展的「ReadMD: 一键配置工作区 MCP Server」会自动探测 R 对象结果同时提供兼容文本和 `structuredContent`,工具带有只读/副作用等注解。参数按公开 schema 校验,错误类型、未知字段、非法目录深度不会被静默转换。 +V0.0.5 新增五项(无需新界面): + +| 工具 | 用途与边界 | +| --- | --- | +| `readmd_analyze_document` | 用 AST 检查当前内容的标题、双链、本地文件和锚点,跳过代码示例;返回 UTF-16 行列、任务统计、常用 YAML 元数据。`content` 必填,`file_path` 与 `workspace_root` 为可选绝对路径。不请求外链。 | +| `readmd_search_workspace` | `workspace_root` 与 `query` 必填;支持短语、`path:`、`title:`、`tag:`、`-排除词`,`limit` 为 1–100,默认 20。直接读取当前文件,略过隐藏、生成目录与符号链接;报告截断和跳过数。 | +| `readmd_read_document` | 读取绝对 `file_path`,单次最多 1,000 行,返回原始字节 SHA-256 `revision`、编码与换行形式;不修改文件。 | +| `readmd_edit_document` | `file_path`、`expected_revision`、`old_text`、`new_text` 必填;默认 `dry_run:true`。写入必须 `dry_run:false, confirm:true`,版本冲突停止;多处匹配必须明确 `replace_all:true`。保留编码及未修改部分的换行,原子写入前保留恢复记录。 | +| `readmd_document_history` | 按绝对 `file_path` 列出恢复记录;`operation:read` 与 `checkpoint_id` 读取同一文件的历史内容,只读,不自动覆盖。 | + +检查及编辑的单文档上限为 2 MiB,工作区检索最多 5,000 个 Markdown 文件、累计 64 MiB。恢复区复用桌面端上限(每文档最多 10 项、总计 64 MiB、最长 30 天),不会在原目录留下 `.bak`。模型可先读版本、预览,再经用户确认提交;要撤回时先读取历史,重新获取当前版本后预览恢复,禁止盲目覆盖。其他应用写入与最终原子替换之间仍存在操作系统层面的竞争窗口;版本检查会尽量缩小该窗口,不能宣称跨程序事务锁。 + `resources/list` 公开 Skills(`readmd://skills/`)、Provider 目录(`readmd://providers`,不含密钥)和本地会话记录(`readmd://sessions`);`prompts/list` 与当前 Skill Registry 一一对应。旧的 workflow id(如 `polish`、`summary`)仍可在 `prompts/get` 和 `readmd_ai_assistant` 中使用。 ## 安全边界 diff --git a/packages/vscode-extension/README.md b/packages/vscode-extension/README.md index b36b3abc..c0c6b94e 100644 --- a/packages/vscode-extension/README.md +++ b/packages/vscode-extension/README.md @@ -1,10 +1,12 @@ -# ReadMD for VS Code · V0.0.4 +# ReadMD for VS Code · V0.0.5 + +V0.0.5 新增原生“问题”面板链接诊断、可点击 Wiki 双链与工作区检索。使用命令面板中的“ReadMD: 检查文档链接与标题”和“ReadMD: 搜索本地 Markdown 工作区”;搜索可组合短语、`path:`、`title:`、`tag:` 和排除词。修改内容后检查会防抖更新,旧版本结果不会覆盖最新文档;不信任的工作区不调用本地内核。需配合 V0.0.5 桌面内核,单份文档上限 2 MiB,扫描范围及截断明确提示。 在 VS Code、Cursor 等兼容编辑器中阅读、修复和转换 Markdown,使用 ReadMD 的 AI Skills,并导出文档与演讲。保留编辑器原生的保存、撤销和未保存提示。 ## 安装和连接 -安装 ReadMD 桌面应用,再用 **Extensions: Install from VSIX…** 安装 `readmd-vscode-0.0.4.vsix`。打开 Markdown,执行 ReadMD 命令即可。 +安装 ReadMD 桌面应用,再用 **Extensions: Install from VSIX…** 安装 `readmd-vscode-0.0.5.vsix`。打开 Markdown,执行 ReadMD 命令即可。 扩展按需启动一个持久的 `readmd --mcp` 子进程,通过标准输入输出通信;没有 HTTP 服务地址设置,不需要打开桌面窗口,也不需要 Python。探测顺序:`readmd.executablePath` → 随 VSIX 附带的内核 → 桌面应用默认安装目录 → PATH。Windows 包括用户的 `Applications/ReadMD`、`LocalAppData/Programs` 和 Program Files。 diff --git a/packages/vscode-extension/l10n/bundle.l10n.json b/packages/vscode-extension/l10n/bundle.l10n.json index 94b38ea5..a0a32bd5 100644 --- a/packages/vscode-extension/l10n/bundle.l10n.json +++ b/packages/vscode-extension/l10n/bundle.l10n.json @@ -179,5 +179,22 @@ "noBibFound": "No .bib file found", "bibtexParseFailed": "BibTeX parsing failed: {error}", "openWorkspaceFirst": "Open a workspace first.", - "writeMcpFailed": "Cannot write MCP configuration: {error}" + "writeMcpFailed": "Cannot write MCP configuration: {error}", + "inspection_missing_anchor": "Heading anchor not found", + "inspection_missing_file": "Local file not found", + "inspection_ambiguous_link": "Multiple files match this link", + "inspection_outside_workspace": "Link is outside the inspected workspace", + "inspection_missing_footnote": "Footnote definition not found", + "inspectionOpen": "Open a Markdown file in a trusted workspace first.", + "inspectionSize": "Inspection supports documents up to 2 MB.", + "inspectionLimited": "Inspection reached a processing limit; some content was not checked.", + "inspectionDone": "Document inspection complete: {count} items.", + "inspectionUnavailable": "Update ReadMD Core to use document inspection.", + "searchOpenWorkspace": "Open a workspace folder first.", + "searchPrompt": "Search text, \"phrases\", path:, title:, tag: and -exclusions", + "searchProgress": "Searching local Markdown files", + "searchEmpty": "No matches in the scanned files.", + "searchLimited": "Results limited to 50; refine your query", + "searchResults": "Select a result to open", + "searchUnavailable": "Search could not finish. Check the query and update ReadMD Core." } diff --git a/packages/vscode-extension/l10n/bundle.l10n.zh-cn.json b/packages/vscode-extension/l10n/bundle.l10n.zh-cn.json index 53d53adb..c3e52267 100644 --- a/packages/vscode-extension/l10n/bundle.l10n.zh-cn.json +++ b/packages/vscode-extension/l10n/bundle.l10n.zh-cn.json @@ -179,5 +179,22 @@ "noBibFound": "没有找到 .bib 文件", "bibtexParseFailed": "BibTeX 解析失败:{error}", "openWorkspaceFirst": "请先打开工作区。", - "writeMcpFailed": "无法写入 MCP 配置:{error}" + "writeMcpFailed": "无法写入 MCP 配置:{error}", + "inspection_missing_anchor": "未找到目标标题", + "inspection_missing_file": "未找到本地文件", + "inspection_ambiguous_link": "同名链接目标不唯一", + "inspection_outside_workspace": "链接位于检查工作区以外", + "inspection_missing_footnote": "未找到脚注定义", + "inspectionOpen": "请先在已信任的工作区打开 Markdown 文件。", + "inspectionSize": "文档检查支持不超过 2 MB 的文件。", + "inspectionLimited": "检查达到处理上限,部分内容尚未检查。", + "inspectionDone": "文档检查完成:{count} 项待检查。", + "inspectionUnavailable": "请更新 ReadMD 核心后使用文档检查。", + "searchOpenWorkspace": "请先打开工作区文件夹。", + "searchPrompt": "搜索正文、\"短语\"、path: 路径、title: 标题、tag: 标签及 -排除词", + "searchProgress": "正在搜索本地 Markdown 文件", + "searchEmpty": "已扫描的文件中没有匹配结果。", + "searchLimited": "结果或扫描达到上限,请缩小查询范围", + "searchResults": "选择结果以打开文件", + "searchUnavailable": "搜索未能完成,请检查查询语法并更新 ReadMD 核心。" } diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index 0d017fae..452b3776 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -2,7 +2,7 @@ "name": "readmd-vscode", "displayName": "%extension.displayName%", "description": "%extension.description%", - "version": "0.0.4", + "version": "0.0.5", "publisher": "Natsummerance", "icon": "icon.png", "license": "MIT", @@ -58,7 +58,9 @@ "onCommand:readmd.convertFileToMarkdown", "onCommand:readmd.fetchWebToMarkdown", "onCommand:readmd.setupMcpServer", - "onCommand:readmd.openSkillByUri" + "onCommand:readmd.openSkillByUri", + "onCommand:readmd.inspectDocument", + "onCommand:readmd.searchWorkspace" ], "contributes": { "snippets": [ @@ -177,6 +179,14 @@ "command": "readmd.openSkillByUri", "title": "%command.openSkillByUri%", "icon": "$(library)" + }, + { + "command": "readmd.inspectDocument", + "title": "%command.inspectDocument%" + }, + { + "command": "readmd.searchWorkspace", + "title": "%command.searchWorkspace%" } ], "viewsContainers": { diff --git a/packages/vscode-extension/package.nls.json b/packages/vscode-extension/package.nls.json index 423a4095..7395612f 100644 --- a/packages/vscode-extension/package.nls.json +++ b/packages/vscode-extension/package.nls.json @@ -29,5 +29,7 @@ "configuration.executablePath": "ReadMD executable (leave empty to use the installed desktop app or `readmd` on PATH); it serves the core via `readmd --mcp`", "configuration.customCssPath": "Absolute path to a user custom.css file", "capabilities.untrusted": "Preview and editing are available. Native tools, AI, imports, code execution and MCP configuration require a trusted workspace.", - "capabilities.virtual": "ReadMD uses a local Rust executable and local files." + "capabilities.virtual": "ReadMD uses a local Rust executable and local files.", + "command.inspectDocument": "ReadMD: Inspect document links and headings", + "command.searchWorkspace": "ReadMD: Search local Markdown workspace" } diff --git a/packages/vscode-extension/package.nls.zh-cn.json b/packages/vscode-extension/package.nls.zh-cn.json index e2c71a0c..8625cb70 100644 --- a/packages/vscode-extension/package.nls.zh-cn.json +++ b/packages/vscode-extension/package.nls.zh-cn.json @@ -29,5 +29,7 @@ "configuration.executablePath": "ReadMD 可执行程序路径(留空则使用已安装的桌面端或 PATH 中的 readmd),核心通过 `readmd --mcp` 提供", "configuration.customCssPath": "用户 custom.css 文件的绝对路径", "capabilities.untrusted": "可预览和编辑。原生工具、AI、导入、代码执行及 MCP 配置需要受信任的工作区。", - "capabilities.virtual": "ReadMD 需要本地 Rust 程序和本地文件。" + "capabilities.virtual": "ReadMD 需要本地 Rust 程序和本地文件。", + "command.inspectDocument": "ReadMD: 检查文档链接与标题", + "command.searchWorkspace": "ReadMD: 搜索本地 Markdown 工作区" } diff --git a/packages/vscode-extension/src/bridge.ts b/packages/vscode-extension/src/bridge.ts index b738fa47..a8192444 100644 --- a/packages/vscode-extension/src/bridge.ts +++ b/packages/vscode-extension/src/bridge.ts @@ -207,8 +207,8 @@ export class ReadMDBridge { /** * 调用 MCP 工具调度器执行核心能力。 */ - public async callMcpTool(name: string, args: Record): Promise { - const result = await this.callMcpMethod('tools/call', { name, arguments: args }); + public async callMcpTool(name: string, args: Record, token?: vscode.CancellationToken): Promise { + const result = await this.callMcpMethodInternal('tools/call', { name, arguments: args }, undefined, token); return this.unwrapToolResult(result); } diff --git a/packages/vscode-extension/src/extension.ts b/packages/vscode-extension/src/extension.ts index 6fce37f6..3e9de0d1 100644 --- a/packages/vscode-extension/src/extension.ts +++ b/packages/vscode-extension/src/extension.ts @@ -6,6 +6,7 @@ import { ReadMDToolboxProvider } from './sidebarProvider'; import { l10n } from './localization'; import { getWebviewContent, WebviewAssets } from './webview'; import { fencedCodeAt } from './fences'; +import { registerIntelligence } from './intelligence'; let diagnosticStatusBarItem: vscode.StatusBarItem; let coreStatusBarItem: vscode.StatusBarItem; @@ -149,6 +150,7 @@ export function parseJsoncSafely(text: string): Record { export function activate(context: vscode.ExtensionContext) { const bridge = new ReadMDBridge(context); context.subscriptions.push({ dispose: () => bridge.dispose() }); + registerIntelligence(context, bridge); const trusted = () => { if (vscode.workspace.isTrusted !== false) return true; void vscode.window.showWarningMessage(l10n('trustRequired', 'Trust this workspace before using native tools, AI or code execution.')); diff --git a/packages/vscode-extension/src/intelligence.ts b/packages/vscode-extension/src/intelligence.ts new file mode 100644 index 00000000..67c1def6 --- /dev/null +++ b/packages/vscode-extension/src/intelligence.ts @@ -0,0 +1,135 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import { ReadMDBridge } from './bridge'; +import { l10n } from './localization'; + +interface Inspection { + diagnostics: { code: string; target: string; position: { line: number; column: number } }[]; + links: { kind: string; target: string; resolved_path?: string; resolved_anchor?: string; ambiguous?: boolean; position: { line: number; column: number } }[]; + truncated: boolean; +} +const messages: Record = { + missing_anchor: 'Heading anchor not found', missing_file: 'Local file not found', + ambiguous_link: 'Multiple files match this link', outside_workspace: 'Link is outside the inspected workspace', + missing_footnote: 'Footnote definition not found', +}; + +/** Native Problems, document links and Quick Pick; no extra editor toolbar. */ +export function registerIntelligence(context: vscode.ExtensionContext, bridge: ReadMDBridge) { + const diagnostics = vscode.languages.createDiagnosticCollection('ReadMD'); + const cache = new Map }>(); + const timers = new Map>(); + let disposed = false; + const eligible = (doc: vscode.TextDocument) => vscode.workspace.isTrusted !== false + && doc.languageId === 'markdown' && doc.uri.scheme === 'file'; + const inspect = async (doc: vscode.TextDocument): Promise => { + if (!eligible(doc) || Buffer.byteLength(doc.getText(), 'utf8') > 2 * 1024 * 1024) { + diagnostics.delete(doc.uri); return; + } + const key = doc.uri.toString(); + const root = vscode.workspace.getWorkspaceFolder(doc.uri)?.uri.fsPath || path.dirname(doc.uri.fsPath); + let item = cache.get(key); + if (!item || item.version !== doc.version || item.root !== root) { + item = { version: doc.version, root, promise: bridge.callMcpTool('readmd_analyze_document', { + content: doc.getText(), file_path: doc.uri.fsPath, workspace_root: root, + }) }; + cache.set(key, item); + } + try { + const result = await item.promise; + if (disposed || cache.get(key) !== item || doc.isClosed || doc.version !== item.version) return; + diagnostics.set(doc.uri, result.diagnostics.map(issue => { + const line = Math.min(Math.max(0, issue.position.line - 1), doc.lineCount - 1); + const column = Math.min(Math.max(0, issue.position.column - 1), doc.lineAt(line).text.length); + const diagnostic = new vscode.Diagnostic(new vscode.Range(line, column, line, + Math.min(column + 2, doc.lineAt(line).text.length)), + l10n('inspection_' + issue.code, messages[issue.code] || 'Check link') + ': ' + issue.target, + vscode.DiagnosticSeverity.Warning); + diagnostic.source = 'ReadMD'; diagnostic.code = issue.code; return diagnostic; + })); + return result; + } catch (error) { + if (cache.get(key) === item) { cache.delete(key); diagnostics.delete(doc.uri); } + throw error; + } + }; + const schedule = (doc: vscode.TextDocument) => { + if (!eligible(doc)) return; + const key = doc.uri.toString(); clearTimeout(timers.get(key)); + timers.set(key, setTimeout(() => { timers.delete(key); void inspect(doc).catch(() => {}); }, 700)); + }; + context.subscriptions.push(diagnostics, + { dispose() { disposed = true; for (const timer of timers.values()) clearTimeout(timer); cache.clear(); } }, + vscode.workspace.onDidOpenTextDocument(schedule), + vscode.workspace.onDidSaveTextDocument(() => { cache.clear(); for (const doc of vscode.workspace.textDocuments) schedule(doc); }), + vscode.workspace.onDidChangeTextDocument(event => { + if (vscode.window.activeTextEditor?.document === event.document) schedule(event.document); + }), + vscode.workspace.onDidCloseTextDocument(doc => { + const key = doc.uri.toString(); clearTimeout(timers.get(key)); timers.delete(key); + cache.delete(key); diagnostics.delete(doc.uri); + }), + vscode.workspace.onDidChangeWorkspaceFolders(() => { cache.clear(); diagnostics.clear(); for (const doc of vscode.workspace.textDocuments) schedule(doc); }), + vscode.workspace.onDidCreateFiles(() => { cache.clear(); for (const doc of vscode.workspace.textDocuments) schedule(doc); }), + vscode.workspace.onDidDeleteFiles(() => { cache.clear(); for (const doc of vscode.workspace.textDocuments) schedule(doc); }), + vscode.workspace.onDidRenameFiles(() => { cache.clear(); for (const doc of vscode.workspace.textDocuments) schedule(doc); }), + vscode.languages.registerDocumentLinkProvider({ language: 'markdown', scheme: 'file' }, { + async provideDocumentLinks(doc, token) { + if (token.isCancellationRequested) return []; + let result: Inspection | undefined; + try { result = await inspect(doc); } catch { return []; } + if (!result || token.isCancellationRequested) return []; + return result.links.filter(link => link.kind === 'wiki' && link.resolved_path && !link.ambiguous).flatMap(link => { + const row = link.position.line - 1; + if (row < 0 || row >= doc.lineCount) return []; + const line = doc.lineAt(row).text, start = link.position.column - 1, end = line.indexOf(']]', start); + if (start < 0 || end < start) return []; + const target = vscode.Uri.file(link.resolved_path!); + const anchor = link.resolved_anchor || link.target.split('#')[1]; + return [new vscode.DocumentLink(new vscode.Range(row, start, row, end + 2), + anchor ? target.with({ fragment: anchor }) : target)]; + }); + }, + }), + vscode.commands.registerCommand('readmd.inspectDocument', async () => { + const doc = vscode.window.activeTextEditor?.document; + if (!doc || !eligible(doc)) { void vscode.window.showInformationMessage(l10n('inspectionOpen', 'Open a Markdown file in a trusted workspace first.')); return; } + cache.delete(doc.uri.toString()); + try { + const result = await inspect(doc); + if (!result) { void vscode.window.showInformationMessage(l10n('inspectionSize', 'Inspection supports documents up to 2 MB.')); return; } + await vscode.commands.executeCommand('workbench.actions.view.problems'); + void vscode.window.showInformationMessage(l10n(result.truncated ? 'inspectionLimited' : 'inspectionDone', + result.truncated ? 'Inspection reached a processing limit; some content was not checked.' : 'Document inspection complete: {count} items.', + { count: result.diagnostics.length })); + } catch { void vscode.window.showErrorMessage(l10n('inspectionUnavailable', 'Update ReadMD Core to use document inspection.')); } + }), + vscode.commands.registerCommand('readmd.searchWorkspace', async () => { + if (vscode.workspace.isTrusted === false) { void vscode.window.showWarningMessage(l10n('trustRequired', 'Trust this workspace before using native tools, AI or code execution.')); return; } + const roots = vscode.workspace.workspaceFolders; + if (!roots?.length) { void vscode.window.showInformationMessage(l10n('searchOpenWorkspace', 'Open a workspace folder first.')); return; } + const root = roots.length === 1 ? roots[0] : await vscode.window.showWorkspaceFolderPick(); + if (!root) return; + const query = await vscode.window.showInputBox({ prompt: l10n('searchPrompt', 'Search text, "phrases", path:, title:, tag: and -exclusions'), ignoreFocusOut: true }); + if (!query?.trim()) return; + try { + const result = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, + title: l10n('searchProgress', 'Searching local Markdown files'), cancellable: true }, + (_, token) => bridge.callMcpTool('readmd_search_workspace', { workspace_root: root.uri.fsPath, query, limit: 50 }, token)); + if (!result.results.length) { void vscode.window.showInformationMessage(l10n('searchEmpty', 'No matches in the scanned files.')); return; } + const picks = result.results.map((hit: any) => ({ label: hit.title, description: hit.relative_path, detail: hit.snippet, hit })); + const pick = await vscode.window.showQuickPick<{ label: string; description: string; detail: string; hit: any }>(picks, + { matchOnDescription: true, matchOnDetail: true, placeHolder: l10n(result.truncated ? 'searchLimited' : 'searchResults', + result.truncated ? 'Results limited to 50; refine your query' : 'Select a result to open') }); + if (!pick) return; + const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(pick.hit.path)); + const editor = await vscode.window.showTextDocument(doc); + const row = Math.min(Math.max(0, pick.hit.line - 1), doc.lineCount - 1); + editor.selection = new vscode.Selection(row, 0, row, 0); editor.revealRange(new vscode.Range(row, 0, row, 0)); + } catch (error) { + if (error instanceof Error && ['ai_cancelled', 'cancelled'].includes(error.message)) return; + void vscode.window.showErrorMessage(l10n('searchUnavailable', 'Search could not finish. Check the query and update ReadMD Core.')); + } + })); + for (const doc of vscode.workspace.textDocuments) schedule(doc); +} diff --git a/packages/vscode-extension/test/commands.test.js b/packages/vscode-extension/test/commands.test.js index 98c9c103..1777f3ef 100644 --- a/packages/vscode-extension/test/commands.test.js +++ b/packages/vscode-extension/test/commands.test.js @@ -42,6 +42,10 @@ const zhBundle = JSON.parse(fs.readFileSync(path.join(extDir, 'l10n', 'bundle.l1 const enBundle = JSON.parse(fs.readFileSync(path.join(extDir, 'l10n', 'bundle.l10n.json'), 'utf-8')); const vscodeStub = { + languages: { + createDiagnosticCollection: () => ({ set() {}, delete() {}, clear() {}, dispose() {} }), + registerDocumentLinkProvider: () => ({ dispose() {} }), + }, l10n: { t: (opts, ...args) => { const key = opts && opts.key; @@ -100,6 +104,14 @@ const vscodeStub = { showTextDocument: async doc => { openedDocs.push(doc); return doc; }, }, workspace: { + textDocuments: [], + onDidOpenTextDocument: () => ({ dispose() {} }), + onDidSaveTextDocument: () => ({ dispose() {} }), + onDidCloseTextDocument: () => ({ dispose() {} }), + onDidChangeWorkspaceFolders: () => ({ dispose() {} }), + onDidRenameFiles: () => ({ dispose() {} }), + onDidCreateFiles: () => ({ dispose() {} }), + onDidDeleteFiles: () => ({ dispose() {} }), onDidChangeTextDocument: () => ({ dispose() {} }), findFiles: async () => [], workspaceFolders: undefined, @@ -231,12 +243,12 @@ test('AI status messages do not enter the result and only the selected source is finally { fakeBridgeInstance.aiChatStreaming = originalAi; vscodeStub.window.showInformationMessage = originalInfo; } }); -test('activate registers exactly the 22 commands contributed in package.json', () => { +test('activate registers every command contributed in package.json', () => { freshState(); const context = activateExtension(); const registeredIds = Object.keys(registered).sort(); assert.deepStrictEqual(registeredIds, contributedCommands); - assert.strictEqual(registeredIds.length, 22); + assert.strictEqual(registeredIds.length, contributedCommands.length); assert.ok(context.subscriptions.length > 0); for (const disposable of context.subscriptions) { assert.strictEqual(typeof disposable.dispose, 'function'); diff --git a/packages/vscode-extension/test/host.cjs b/packages/vscode-extension/test/host.cjs index e41eafcf..b5c7608c 100644 --- a/packages/vscode-extension/test/host.cjs +++ b/packages/vscode-extension/test/host.cjs @@ -35,6 +35,36 @@ exports.run = async function () { fs.writeFileSync(source, markdown); const document = await vscode.workspace.openTextDocument(vscode.Uri.file(source)); await vscode.window.showTextDocument(document); + const linked = path.join(root, 'Plato.md'); fs.writeFileSync(linked, '# Plato\n\nA careful argument.\n'); + const inspected = await bridge.callMcpTool('readmd_analyze_document', { + content: '[[Plato]]\n\n[Broken](#missing)\n', file_path: source, workspace_root: root, + }); + assert.equal(inspected.diagnostics[0].code, 'missing_anchor'); + assert.equal(path.basename(inspected.links[0].resolved_path), 'Plato.md'); + results.push('shared AST link inspection'); + const search = await bridge.callMcpTool('readmd_search_workspace', { workspace_root: root, query: 'title:Plato "careful argument"' }); + assert.equal(search.results[0].relative_path, 'Plato.md'); results.push('fresh workspace phrase and title search'); + const read = await bridge.callMcpTool('readmd_read_document', { file_path: linked }); + const args = { file_path: linked, expected_revision: read.revision, old_text: 'careful', new_text: 'clear' }; + const preview = await bridge.callMcpTool('readmd_edit_document', args); + assert.equal(preview.dry_run, true); assert.match(fs.readFileSync(linked, 'utf8'), /careful/); + const applied = await bridge.callMcpTool('readmd_edit_document', { ...args, dry_run: false, confirm: true }); + assert.match(fs.readFileSync(linked, 'utf8'), /clear/); + const history = await bridge.callMcpTool('readmd_document_history', { file_path: linked, operation: 'read', checkpoint_id: applied.checkpoint_id }); + assert.match(history.content, /careful/); results.push('safe edit preview, commit and recovery history'); + const inspectionFile=path.join(root,'Links.md'); + fs.writeFileSync(inspectionFile,'# Links\n\n[[Plato]]\n\n[Broken](#missing)\n'); + const inspectionDoc=await vscode.workspace.openTextDocument(vscode.Uri.file(inspectionFile)); + await vscode.window.showTextDocument(inspectionDoc); + await vscode.commands.executeCommand('readmd.inspectDocument'); + assert.ok(vscode.languages.getDiagnostics(inspectionDoc.uri).some(d=>d.source==='ReadMD'&&d.code==='missing_anchor')); + const documentLinks=await vscode.commands.executeCommand('vscode.executeLinkProvider',inspectionDoc.uri); + fs.writeFileSync(path.join(root,'link-provider.json'),JSON.stringify(documentLinks.map(link=>({target:link.target?.fsPath,text:inspectionDoc.getText(link.range)})),null,2)); + const wiki=documentLinks.find(link=>link.target?.fsPath.toLowerCase()===linked.toLowerCase()); + assert.ok(wiki,'Wiki target resolves to the real local file'); + assert.equal(inspectionDoc.getText(wiki.range),'[[Plato]]'); + results.push('native Problems diagnostics and clickable Wiki provider'); + await vscode.window.showTextDocument(document); await vscode.commands.executeCommand('readmd.preview'); results.push('real preview command'); await vscode.window.showTextDocument(document); await vscode.commands.executeCommand('readmd.openPresentation'); results.push('real presentation command'); diff --git a/release/release_notes.md b/release/release_notes.md index 635d226e..b274e9c1 100644 --- a/release/release_notes.md +++ b/release/release_notes.md @@ -1,4 +1,4 @@ -# ReadMD v0.0.4 更新说明 +# ReadMD v0.0.5 更新说明 ReadMD 是本地优先的 Markdown 阅读、编辑与格式转换工具。v2.3.9 在延续本地优先、全语种母语化与格式转换能力的基础上,正式推出 **CC0 许可的 Arch-Chan Live2D 与 73 款 Petdex 精灵图桌面伴读宠物系统(含活态微呼吸自适应引擎与全端台词联动)**,重构 **纯正苹果风原生拖动条(Apple HIG 4px 细轨道与 18px 物理触觉旋钮)**,新增 **原生矢量高保真 PDF 编辑器及 FastMCP 21 项扩展工具链**,强化 **纯 Python Word .doc 二进制流(FIB/CLX)与复杂表格聚类重构**,达成 **46 种语言 100% 字典对齐(1563 词条零裸露、零空值、零英文照搬)**,并实现 **打包彻底瘦身至 28.2MB 与 2 秒极速冷启动**。 @@ -6,12 +6,12 @@ ReadMD 是本地优先的 Markdown 阅读、编辑与格式转换工具。v2.3.9 | 系统 | 架构 | 交付物 | 资产文件名 | | --- | --- | --- | --- | -| Windows 10/11 | x64、ARM64 | 安装版、便携版 | `ReadMDSetup-v0.0.4.exe`、`ReadMD-portable-v0.0.4.exe`(x64);`ReadMDSetup-arm64-v0.0.4.exe`、`ReadMD-portable-arm64-v0.0.4.exe`(ARM64) | -| macOS 13+ | Intel x64、Apple Silicon ARM64 | 原生压缩包 | `ReadMD-macos-x64-v0.0.4.zip` / `ReadMD-macos-arm64-v0.0.4.zip` | -| Ubuntu 22.04/24.04、Debian 12 | x64、ARM64 | AppImage、Deb | `ReadMD-linux-x86_64-v0.0.4.AppImage` / `ReadMD-linux-aarch64-v0.0.4.AppImage`;`readmd_0.0.4_amd64.deb` / `readmd_0.0.4_arm64.deb` | -| 统信 UOS 20、银河麒麟 V10、Deepin 23 | x64、ARM64 | 目标 Deb | `readmd_0.0.4_amd64.deb` / `readmd_0.0.4_arm64.deb`;真实系统证据完成前不构成正式支持承诺 | -| VS Code | Extension Host 支持的桌面架构 | VSIX | `readmd-vscode-0.0.4.vsix` | -| MCP 客户端 | Python 3.11+ / stdio | MCP ZIP | `readmd-mcp-server-0.0.4.zip` | +| Windows 10/11 | x64、ARM64 | 安装版、便携版 | `ReadMDSetup-v0.0.5.exe`、`ReadMD-portable-v0.0.5.exe`(x64);`ReadMDSetup-arm64-v0.0.5.exe`、`ReadMD-portable-arm64-v0.0.5.exe`(ARM64) | +| macOS 13+ | Intel x64、Apple Silicon ARM64 | 原生压缩包 | `ReadMD-macos-x64-v0.0.5.zip` / `ReadMD-macos-arm64-v0.0.5.zip` | +| Ubuntu 22.04/24.04、Debian 12 | x64、ARM64 | AppImage、Deb | `ReadMD-linux-x86_64-v0.0.5.AppImage` / `ReadMD-linux-aarch64-v0.0.5.AppImage`;`readmd_0.0.5_amd64.deb` / `readmd_0.0.5_arm64.deb` | +| 统信 UOS 20、银河麒麟 V10、Deepin 23 | x64、ARM64 | 目标 Deb | `readmd_0.0.5_amd64.deb` / `readmd_0.0.5_arm64.deb`;真实系统证据完成前不构成正式支持承诺 | +| VS Code | Extension Host 支持的桌面架构 | VSIX | `readmd-vscode-0.0.5.vsix` | +| MCP 客户端 | Python 3.11+ / stdio | MCP ZIP | `readmd-mcp-server-0.0.5.zip` | | 校验清单 | 全架构 | SHA-256 | `SHA256SUMS.txt` | 本版本经 `READMD_SELF_USE_RELEASE` 自用通道发布(仅 `v2.3.9` tag 生效):资产未做 Authenticode / macOS codesign 签名,原生平台证据清单按参考口径(informational)提供;多平台原生构建、冒烟自检、隐私扫描与校验和生成照常执行。首次运行如遇系统"未知发布者"提示,请先核对 `SHA256SUMS.txt` 再安装。 diff --git a/rust/Cargo.lock b/rust/Cargo.lock index e983304d..7abeb104 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2225,7 +2225,7 @@ dependencies = [ [[package]] name = "readmd-kernel" -version = "0.0.4" +version = "0.0.5" dependencies = [ "aes", "base64", @@ -3631,7 +3631,7 @@ dependencies = [ [[package]] name = "xtask" -version = "0.0.4" +version = "0.0.5" dependencies = [ "base64", "flate2", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 73ad4444..dca653fb 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["readmd-kernel", "xtask"] [workspace.package] -version = "0.0.4" +version = "0.0.5" edition = "2021" rust-version = "1.85" license = "MIT" diff --git a/rust/readmd-kernel/src/document_intelligence.rs b/rust/readmd-kernel/src/document_intelligence.rs new file mode 100644 index 00000000..d7665218 --- /dev/null +++ b/rust/readmd-kernel/src/document_intelligence.rs @@ -0,0 +1,1142 @@ +//! Shared, bounded document inspection and local workspace operations. +//! No network, third-party runtime, hidden document copies, or UI dependency. +use crate::{content, App}; +use pulldown_cmark::{Event, LinkType, Options, Parser, Tag, TagEnd}; +use serde_json::{json, Value}; +use sha2::{Digest, Sha256}; +use std::{ + collections::{HashMap, HashSet}, + fs, + io::Read, + path::{Component, Path, PathBuf}, + sync::atomic::{AtomicBool, Ordering}, +}; + +pub const MAX_DOCUMENT_BYTES: usize = 2 * 1024 * 1024; +const MAX_FILES: usize = 5000; +const MAX_SCAN_BYTES: usize = 64 * 1024 * 1024; +const MAX_ITEMS: usize = 2000; + +pub fn revision(bytes: &[u8]) -> String { + format!("{:x}", Sha256::digest(bytes)) +} +fn cancelled(cancel: Option<&AtomicBool>) -> Result<(), String> { + if cancel.is_some_and(|c| c.load(Ordering::SeqCst)) { + Err("cancelled".into()) + } else { + Ok(()) + } +} +fn absolute_file(path: &Path) -> Result { + if !path.is_absolute() { + return Err("file_path_must_be_absolute".into()); + } + if fs::symlink_metadata(path) + .map_err(|_| "file_not_found")? + .file_type() + .is_symlink() + { + return Err("symlink_not_allowed".into()); + } + let canonical = fs::canonicalize(path).map_err(|_| "file_not_found")?; + if !canonical.is_file() { + return Err("file_not_found".into()); + } + if !content::is_readable(&canonical) { + return Err("unsupported_text_file".into()); + } + Ok(crate::paths::canonicalize_or_clean(&canonical)) +} +fn bytes_bounded(path: &Path) -> Result, String> { + let file = fs::File::open(path).map_err(|_| "file_read_failed")?; + if file.metadata().map_err(|_| "file_read_failed")?.len() > MAX_DOCUMENT_BYTES as u64 { + return Err("document_too_large".into()); + } + let mut bytes = Vec::new(); + file.take(MAX_DOCUMENT_BYTES as u64 + 1) + .read_to_end(&mut bytes) + .map_err(|_| "file_read_failed")?; + if bytes.len() > MAX_DOCUMENT_BYTES { + return Err("document_too_large".into()); + } + Ok(bytes) +} +fn markdown(path: &Path) -> bool { + matches!( + content::ext_of(path).as_str(), + "md" | "markdown" | "mdown" | "mkd" | "mdx" | "txt" + ) +} +fn visible(entry: &walkdir::DirEntry) -> bool { + if entry.depth() == 0 { + return true; + } + let name = entry.file_name().to_string_lossy(); + !name.starts_with('.') + && !matches!( + name.as_ref(), + "node_modules" | "target" | "dist" | "build" | "vendor" | "venv" | "__pycache__" + ) +} +fn workspace(root: &Path) -> Result { + if !root.is_absolute() { + return Err("workspace_must_be_absolute".into()); + } + let root = fs::canonicalize(root).map_err(|_| "workspace_not_found")?; + if !root.is_dir() { + return Err("workspace_not_found".into()); + } + Ok(crate::paths::canonicalize_or_clean(&root)) +} +fn local_path(base: &Path, raw: &str, root: &Path) -> Option { + if raw.contains('\0') || raw.starts_with("//") || raw.starts_with("\\\\") { + return None; + } + let decoded = percent_encoding::percent_decode_str(raw) + .decode_utf8() + .ok()?; + if decoded.contains('\0') || decoded.contains('\\') || decoded.contains(':') { + return None; + } + let (start, relative) = if decoded.starts_with('/') { + (root, decoded.trim_start_matches('/')) + } else { + (base, decoded.as_ref()) + }; + let mut candidate = start.to_path_buf(); + for part in Path::new(relative).components() { + match part { + Component::Normal(p) => candidate.push(p), + Component::CurDir => (), + Component::ParentDir => { + candidate.pop(); + } + _ => return None, + } + } + if !candidate.starts_with(root) { + return None; + } + let resolved = crate::paths::canonical_existing(&candidate).ok()?; + let resolved = crate::paths::canonicalize_or_clean(&resolved); + if resolved.starts_with(root) { + Some(resolved) + } else { + None + } +} +fn options() -> Options { + Options::ENABLE_TABLES + | Options::ENABLE_FOOTNOTES + | Options::ENABLE_TASKLISTS + | Options::ENABLE_STRIKETHROUGH + | Options::ENABLE_WIKILINKS + | Options::ENABLE_YAML_STYLE_METADATA_BLOCKS +} +fn line_starts(text: &str) -> Vec { + std::iter::once(0) + .chain(text.match_indices('\n').map(|(n, _)| n + 1)) + .collect() +} +fn position(text: &str, starts: &[usize], offset: usize) -> Value { + let offset = offset.min(text.len()); + let row = starts.partition_point(|n| *n <= offset).saturating_sub(1); + json!({"line": row + 1, "column": text[starts[row]..offset].encode_utf16().count() + 1}) +} +#[derive(Clone, Copy)] +enum AnchorStyle { + Standard, + Desktop, +} +fn heading_slug(title: &str, index: usize, style: AnchorStyle) -> String { + let filtered: String = title + .trim() + .to_lowercase() + .chars() + .filter(|c| { + c.is_whitespace() + || *c == '_' + || *c == '-' + || match style { + AnchorStyle::Standard => c.is_alphanumeric(), + AnchorStyle::Desktop => { + c.is_ascii_alphanumeric() || ('\u{4e00}'..='\u{9fff}').contains(c) + } + } + }) + .collect(); + match style { + AnchorStyle::Standard => filtered + .chars() + .map(|c| if c.is_whitespace() { '-' } else { c }) + .collect(), + AnchorStyle::Desktop => { + let slug = filtered.split_whitespace().collect::>().join("-"); + if slug.is_empty() { + format!("toc-h-{index}") + } else { + slug + } + } + } +} +fn heading_data(text: &str, style: AnchorStyle) -> Vec { + let starts = line_starts(text); + let mut current: Option<(u8, usize, String, Option)> = None; + let mut headings = Vec::new(); + let mut used: HashMap = HashMap::new(); + for (event, span) in Parser::new_ext(text, options()).into_offset_iter() { + match event { + Event::Start(Tag::Heading { level, id, .. }) => { + current = Some(( + level as u8, + span.start, + String::new(), + id.map(|s| s.to_string()), + )) + } + Event::Text(t) | Event::Code(t) => { + if let Some(h) = current.as_mut() { + h.2.push_str(&t); + } + } + Event::SoftBreak | Event::HardBreak => { + if let Some(h) = current.as_mut() { + h.2.push(' '); + } + } + Event::End(TagEnd::Heading(_)) => { + if let Some((level, offset, title, explicit)) = current.take() { + let base = + explicit.unwrap_or_else(|| heading_slug(&title, headings.len(), style)); + let count = used.entry(base.clone()).or_insert(0); + let suffix = *count + + if matches!(style, AnchorStyle::Desktop) { + 1 + } else { + 0 + }; + let id = if *count == 0 { + base + } else { + format!("{base}-{suffix}") + }; + *count += 1; + headings.push(json!({"level":level,"text":title,"id":id,"position":position(text,&starts,offset)})); + if headings.len() >= MAX_ITEMS { + break; + } + } + } + _ => (), + } + } + headings +} + +// Keep the legacy conversion parser unchanged. Accept the common YAML tags +// list without evaluating YAML directives, aliases or custom types. +fn metadata(text: &str) -> Value { + let mut result = content::front_matter(text); + let mut lines = text.trim_start_matches('\u{feff}').lines(); + if lines.next() != Some("---") { + return result; + } + let mut tags = Vec::new(); + let mut in_tags = false; + let mut size = 0; + for line in lines { + size += line.len(); + if size > 16 * 1024 || line == "---" || line == "..." { + break; + } + if line.trim() == "tags:" { + in_tags = true; + continue; + } + if in_tags { + if let Some(tag) = line.trim().strip_prefix("- ") { + let tag = tag.trim().trim_matches(|c| c == '"' || c == '\''); + if !tag.is_empty() { + tags.push(json!(tag)); + } + } else if !line.trim().is_empty() { + in_tags = false; + } + } + } + if !tags.is_empty() { + result["tags"] = json!(tags); + } + result +} + +/// AST-based inspection. Filesystem checking is explicit and stays in root. +pub fn analyze(text: &str, file: Option<&Path>, root: Option<&Path>) -> Result { + analyze_with_style(text, file, root, AnchorStyle::Standard) +} +pub fn analyze_for_reader( + text: &str, + file: Option<&Path>, + root: Option<&Path>, +) -> Result { + analyze_with_style(text, file, root, AnchorStyle::Desktop) +} +fn analyze_with_style( + text: &str, + file: Option<&Path>, + root: Option<&Path>, + style: AnchorStyle, +) -> Result { + if text.len() > MAX_DOCUMENT_BYTES { + return Err("document_too_large".into()); + } + let owned_root = root.map(workspace).transpose()?; + let file = file.map(|p| crate::paths::canonicalize_or_clean(p)); + let base = file + .as_deref() + .and_then(Path::parent) + .or(owned_root.as_deref()); + let root = owned_root.as_deref().or(base); + let starts = line_starts(text); + let headings = heading_data(text, style); + let own_ids: HashSet = headings + .iter() + .filter_map(|h| h["id"].as_str().map(str::to_string)) + .collect(); + let mut links = Vec::new(); + let mut diagnostics = Vec::new(); + let mut tasks = (0usize, 0usize); + let mut code_depth = 0usize; + let mut footnotes = HashSet::new(); + let mut foot_refs = Vec::new(); + let mut target_headings: HashMap>> = HashMap::new(); + let mut truncated = headings.len() == MAX_ITEMS; + let mut wiki_files: Option>> = None; + let mut add = |code: &str, offset: usize, target: &str| { + if diagnostics.len() < 200 { + diagnostics.push(json!({"code":code,"severity":"warning", + "target":target,"position":position(text,&starts,offset)})); + } + }; + for (event, span) in Parser::new_ext(text, options()).into_offset_iter() { + match event { + Event::Start(Tag::CodeBlock(_)) => { + code_depth += 1; + } + Event::End(TagEnd::CodeBlock) => { + code_depth = code_depth.saturating_sub(1); + } + Event::TaskListMarker(done) => { + tasks.0 += 1; + if done { + tasks.1 += 1; + } + } + Event::Start(Tag::FootnoteDefinition(label)) => { + footnotes.insert(label.to_string()); + } + Event::FootnoteReference(label) => foot_refs.push((label.to_string(), span.start)), + Event::Start(Tag::Link { + link_type, + dest_url, + .. + }) + | Event::Start(Tag::Image { + link_type, + dest_url, + .. + }) if code_depth == 0 => { + if links.len() >= MAX_ITEMS { + truncated = true; + continue; + } + let raw = dest_url.to_string(); + let wiki = matches!(link_type, LinkType::WikiLink { .. }); + let external = raw.starts_with("//") + || raw.split_once(':').is_some_and(|(scheme, _)| { + !scheme.is_empty() + && scheme + .chars() + .all(|c| c.is_ascii_alphanumeric() || "+-.".contains(c)) + }); + let mut link = json!({"target":raw,"kind":if wiki {"wiki"} else {"markdown"}, + "external":external,"position":position(text,&starts,span.start)}); + if !external { + let (path_part, anchor) = raw + .split_once('#') + .map(|(p, a)| (p, Some(a))) + .unwrap_or((&raw, None)); + let path_part = path_part.split('?').next().unwrap_or(path_part); + if path_part.is_empty() { + if wiki { + if let Some(file) = file.as_ref() { + link["resolved_path"] = json!(file); + } + } + if let Some(anchor) = anchor { + let anchor = + percent_encoding::percent_decode_str(anchor).decode_utf8_lossy(); + let normalized = heading_slug(&anchor, 0, style); + if !anchor.is_empty() + && !own_ids.contains(anchor.as_ref()) + && !(wiki && own_ids.contains(&normalized)) + { + add("missing_anchor", span.start, &raw); + } else if wiki { + link["resolved_anchor"] = + json!(if own_ids.contains(anchor.as_ref()) { + anchor.into_owned() + } else { + normalized + }); + } + } + } else if let (Some(base), Some(root)) = (base, root) { + if let Some(mut target) = local_path(base, path_part, root) { + if wiki && !target.is_file() && target.extension().is_none() { + target.set_extension("md"); + } + if wiki && !target.is_file() && !path_part.contains('/') { + let files = wiki_files.get_or_insert_with(|| { + let mut map: HashMap> = HashMap::new(); + for e in walkdir::WalkDir::new(root) + .follow_links(false) + .sort_by_file_name() + .into_iter() + .filter_entry(visible) + .filter_map(Result::ok) + .take(MAX_FILES) + { + if e.file_type().is_file() && markdown(e.path()) { + if let Ok(p) = fs::canonicalize(e.path()) + .map(|p| crate::paths::canonicalize_or_clean(&p)) + { + if p.starts_with(root) { + map.entry( + e.path() + .file_stem() + .unwrap_or_default() + .to_string_lossy() + .to_lowercase(), + ) + .or_default() + .push(p); + } + } + } + } + map + }); + let key = Path::new(path_part) + .file_stem() + .unwrap_or_default() + .to_string_lossy() + .to_lowercase(); + if let Some(found) = files.get(&key) { + if found.len() == 1 { + target = found[0].clone(); + } else { + add("ambiguous_link", span.start, &raw); + link["ambiguous"] = json!(true); + } + } + } + if target.is_file() { + link["resolved_path"] = json!(target); + if let Some(anchor) = anchor.filter(|a| !a.is_empty()) { + if markdown(&target) + && (target_headings.contains_key(&target) + || target_headings.len() < 32) + { + let ids = target_headings + .entry(target.clone()) + .or_insert_with(|| { + bytes_bounded(&target).ok().map(|b| { + heading_data( + &crate::text_encoding::detect_and_decode( + &b, + ) + .0, + style, + ) + .iter() + .filter_map(|h| { + h["id"].as_str().map(str::to_string) + }) + .collect() + }) + }); + let anchor = percent_encoding::percent_decode_str(anchor) + .decode_utf8_lossy(); + if let Some(ids) = ids { + if !ids.contains(anchor.as_ref()) + && !(wiki + && ids + .contains(&heading_slug(&anchor, 0, style))) + { + add("missing_anchor", span.start, &raw); + } else if wiki { + link["resolved_anchor"] = + json!(if ids.contains(anchor.as_ref()) { + anchor.into_owned() + } else { + heading_slug(&anchor, 0, style) + }); + } + } else { + truncated = true; + } + } else if markdown(&target) { + truncated = true; + } + } + } else if link.get("ambiguous").is_none() { + add("missing_file", span.start, &raw); + } + } else { + add("outside_workspace", span.start, &raw); + } + } + } + links.push(link); + } + _ => (), + } + } + for (label, offset) in foot_refs { + if !footnotes.contains(&label) { + add("missing_footnote", offset, &label); + } + } + Ok( + json!({"ok":true,"headings":headings,"links":links,"diagnostics":diagnostics, + "stats":{"words":content::count_words(text),"lines":starts.len(),"tasks":tasks.0,"completed_tasks":tasks.1}, + "metadata":metadata(text),"truncated":truncated || diagnostics.len() == 200, + "limits":{"document_bytes":MAX_DOCUMENT_BYTES,"links":MAX_ITEMS,"diagnostics":200,"anchor_files":32}}), + ) +} + +pub fn read_document(path: &Path, start: usize, end: usize) -> Result { + if start == 0 || end < start || end - start >= 1000 { + return Err("invalid_line_range".into()); + } + let path = absolute_file(path)?; + let bytes = bytes_bounded(&path)?; + let (text, encoding) = crate::text_encoding::detect_and_decode(&bytes); + let lines: Vec<&str> = text.split_inclusive('\n').collect(); + if start > lines.len().max(1) { + return Err("line_out_of_range".into()); + } + let eol = if text.contains("\r\n") { "crlf" } else { "lf" }; + let selected = lines + .iter() + .skip(start - 1) + .take(end - start + 1) + .copied() + .collect::(); + Ok( + json!({"ok":true,"path":path,"revision":revision(&bytes),"encoding":encoding,"eol":eol, + "start_line":start,"end_line":end.min(lines.len().max(1)),"total_lines":lines.len(), + "content":selected,"truncated":start>1 || end Result, String> { + if query.len() > 4096 { + return Err("query_too_large".into()); + } + let mut words = Vec::new(); + let mut word = String::new(); + let mut quote = false; + for c in query.chars() { + if c == '"' { + quote = !quote; + } else if c.is_whitespace() && !quote { + if !word.is_empty() { + words.push(std::mem::take(&mut word)); + } + } else { + word.push(c); + } + } + if quote { + return Err("unclosed_search_quote".into()); + } + if !word.is_empty() { + words.push(word); + } + if words.is_empty() || words.len() > 16 { + return Err("invalid_search_query".into()); + } + Ok(words + .into_iter() + .map(|word| { + let exclude = word.starts_with('-'); + let word = word.trim_start_matches('-'); + let (field, value) = word + .split_once(':') + .filter(|(f, _)| matches!(*f, "path" | "tag" | "title")) + .unwrap_or(("text", word)); + Term { + field: field.into(), + value: value.to_lowercase(), + exclude, + } + }) + .collect()) +} +pub fn search_workspace( + root: &Path, + query: &str, + limit: usize, + cancel: Option<&AtomicBool>, +) -> Result { + if !(1..=100).contains(&limit) { + return Err("invalid_search_limit".into()); + } + let root = workspace(root)?; + let terms = query_terms(query)?; + if terms.iter().any(|t| t.value.is_empty()) { + return Err("invalid_search_query".into()); + } + let mut hits = Vec::new(); + let mut files = 0; + let mut scanned = 0; + let mut skipped = 0; + let mut truncated = false; + for entry in walkdir::WalkDir::new(&root) + .follow_links(false) + .sort_by_file_name() + .into_iter() + .filter_entry(visible) + { + cancelled(cancel)?; + let Ok(entry) = entry else { + skipped += 1; + continue; + }; + if !entry.file_type().is_file() || !markdown(entry.path()) { + continue; + } + if files >= MAX_FILES || scanned >= MAX_SCAN_BYTES { + truncated = true; + break; + } + files += 1; + let Ok(path) = + fs::canonicalize(entry.path()).map(|p| crate::paths::canonicalize_or_clean(&p)) + else { + skipped += 1; + continue; + }; + if !path.starts_with(&root) { + skipped += 1; + continue; + } + let Ok(bytes) = bytes_bounded(&path) else { + skipped += 1; + continue; + }; + if bytes.len() > MAX_SCAN_BYTES - scanned { + truncated = true; + break; + } + scanned += bytes.len(); + let (text, _) = crate::text_encoding::detect_and_decode(&bytes); + let relative = entry + .path() + .strip_prefix(&root) + .unwrap_or(entry.path()) + .to_string_lossy() + .replace('\\', "/"); + let title = content::title_of(&path, &text); + let body = text.to_lowercase(); + let title_lower = title.to_lowercase(); + let metadata = metadata(&text); + let tags = metadata + .get("tags") + .map(|v| { + v.as_array() + .map(|a| { + a.iter() + .filter_map(Value::as_str) + .map(str::to_lowercase) + .collect::>() + }) + .unwrap_or_else(|| { + v.as_str() + .unwrap_or("") + .split(',') + .map(|s| s.trim().to_lowercase()) + .collect() + }) + }) + .unwrap_or_default(); + if !terms.iter().all(|t| { + let found = match t.field.as_str() { + "path" => relative.to_lowercase().contains(&t.value), + "title" => title_lower.contains(&t.value), + "tag" => tags + .iter() + .any(|tag| tag.trim_start_matches('#') == t.value.trim_start_matches('#')), + _ => body.contains(&t.value), + }; + found != t.exclude + }) { + continue; + } + let line = text + .lines() + .position(|line| { + terms + .iter() + .filter(|t| !t.exclude && t.field == "text") + .any(|t| line.to_lowercase().contains(&t.value)) + }) + .unwrap_or(0) + + 1; + let snippet = text + .lines() + .nth(line - 1) + .unwrap_or("") + .chars() + .take(240) + .collect::(); + let score = terms + .iter() + .filter(|t| !t.exclude) + .map(|t| { + if title_lower.contains(&t.value) { + 10 + } else { + 1 + } + }) + .sum::(); + hits.push(json!({"path":path,"relative_path":relative,"title":title,"line":line,"snippet":snippet,"score":score})); + } + hits.sort_by(|a, b| { + b["score"].as_u64().cmp(&a["score"].as_u64()).then_with(|| { + a["relative_path"] + .as_str() + .cmp(&b["relative_path"].as_str()) + }) + }); + let matched = hits.len(); + hits.truncate(limit); + Ok( + json!({"ok":true,"results":hits,"matched":matched,"truncated":truncated || matched>limit, + "scanned_files":files,"scanned_bytes":scanned,"skipped_files":skipped, + "limits":{"files":MAX_FILES,"total_bytes":MAX_SCAN_BYTES,"document_bytes":MAX_DOCUMENT_BYTES}}), + ) +} + +/// Literal edits default to preview; committing requires a byte revision and +/// confirmation, preserves encoding/EOL, and records bounded recovery history. +pub fn edit_document( + app: &App, + path: &Path, + expected: &str, + old: &str, + new: &str, + replace_all: bool, + dry_run: bool, + confirm: bool, + cancel: Option<&AtomicBool>, +) -> Result { + if expected.len() != 64 || !expected.bytes().all(|b| b.is_ascii_hexdigit()) { + return Err("expected_revision_required".into()); + } + if old.is_empty() { + return Err("empty_edit_match".into()); + } + if new.len() > MAX_DOCUMENT_BYTES { + return Err("document_too_large".into()); + } + if !dry_run && !confirm { + return Err("confirmation_required".into()); + } + let _guard = crate::document_history::SAVE_LOCK + .lock() + .unwrap_or_else(|e| e.into_inner()); + cancelled(cancel)?; + let path = absolute_file(path)?; + let bytes = bytes_bounded(&path)?; + if revision(&bytes) != expected.to_lowercase() { + return Err("document_conflict".into()); + } + let (text, encoding) = crate::text_encoding::detect_and_decode(&bytes); + if crate::text_encoding::encode(&text, encoding).map_err(|_| "encoding_unrepresentable")? + != bytes + { + return Err("encoding_not_lossless".into()); + } + let old = old.replace("\r\n", "\n"); + let new = new.replace("\r\n", "\n"); + let normalized = text.replace("\r\n", "\n"); + let count = normalized.matches(&old).count(); + if count == 0 { + return Err("edit_match_not_found".into()); + } + if count > 1 && !replace_all { + return Err("ambiguous_edit_match".into()); + } + let replacements = if replace_all { count } else { 1 }; + let proposed = normalized + .len() + .checked_sub(old.len().saturating_mul(replacements)) + .and_then(|n| { + new.len() + .checked_mul(replacements) + .and_then(|added| n.checked_add(added)) + }); + if proposed.is_none_or(|n| n > MAX_DOCUMENT_BYTES) { + return Err("document_too_large".into()); + } + // Map normalized match boundaries back to source bytes. Untouched text, + // including mixed line endings, remains byte-for-byte equivalent. + let mut offsets = Vec::with_capacity(normalized.len() + 1); + for (i, byte) in text.bytes().enumerate() { + if byte == b'\n' && i > 0 && text.as_bytes()[i - 1] == b'\r' { + continue; + } + offsets.push(i); + } + offsets.push(text.len()); + let mut edited = String::with_capacity(text.len()); + let mut previous = 0; + for (at, _) in normalized.match_indices(&old).take(replacements) { + let begin = offsets[at]; + let end = offsets[at + old.len()]; + let crlf = text[begin..end].contains("\r\n") + || (!text[begin..end].contains('\n') && text.contains("\r\n")); + let replacement = if crlf { + new.replace('\n', "\r\n") + } else { + new.clone() + }; + if edited.len() + (begin - previous) + replacement.len() > MAX_DOCUMENT_BYTES { + return Err("document_too_large".into()); + } + edited.push_str(&text[previous..begin]); + edited.push_str(&replacement); + previous = end; + } + if edited.len() + text.len() - previous > MAX_DOCUMENT_BYTES { + return Err("document_too_large".into()); + } + edited.push_str(&text[previous..]); + let encoded = + crate::text_encoding::encode(&edited, encoding).map_err(|_| "encoding_unrepresentable")?; + if encoded.len() > MAX_DOCUMENT_BYTES { + return Err("document_too_large".into()); + } + let changed = encoded != bytes; + let mut checkpoint = Value::Null; + if !dry_run && changed { + cancelled(cancel)?; + if revision(&bytes_bounded(&path)?) != expected.to_lowercase() { + return Err("document_conflict".into()); + } + checkpoint = json!(crate::document_history::checkpoint_file( + &app.paths.data_dir, + &path, + "mcp_edit" + )? + .map(|entry| entry.id)); + cancelled(cancel)?; + if revision(&bytes_bounded(&path)?) != expected.to_lowercase() { + return Err("document_conflict".into()); + } + content::write_bytes_atomic(&path, &encoded).map_err(|_| "file_write_failed")?; + let _ = content::index(app, &path); + } + Ok( + json!({"ok":true,"dry_run":dry_run,"changed":changed,"replacements":if replace_all{count}else{1}, + "revision":revision(if dry_run {&bytes}else{&encoded}),"proposed_revision":revision(&encoded), + "encoding":encoding,"checkpoint_id":checkpoint, + "preview":{"before":old.chars().take(1600).collect::(),"after":new.chars().take(1600).collect::(), + "truncated":old.chars().count()>1600 || new.chars().count()>1600}}), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + fn app(root: &Path) -> App { + App::bootstrap(crate::paths::AppPaths::with_dirs( + &root.join("data"), + root, + &root.join("assets"), + )) + .unwrap() + } + #[test] + fn each_host_checks_the_anchor_ids_it_actually_renders() { + assert!( + analyze("# Alpha title\n\n[[#Alpha title]]\n", None, None).unwrap()["diagnostics"] + .as_array() + .unwrap() + .is_empty() + ); + let source = "# Alpha\n\n# Alpha\n\n[duplicate](#alpha-1)\n"; + assert!(analyze(source, None, None).unwrap()["diagnostics"] + .as_array() + .unwrap() + .is_empty()); + assert_eq!( + analyze_for_reader(source, None, None).unwrap()["diagnostics"][0]["code"], + "missing_anchor" + ); + assert!( + analyze_for_reader(&source.replace("#alpha-1", "#alpha-2"), None, None).unwrap() + ["diagnostics"] + .as_array() + .unwrap() + .is_empty() + ); + let unicode = "# Café\n\n[valid](#café)\n"; + assert!(analyze(unicode, None, None).unwrap()["diagnostics"] + .as_array() + .unwrap() + .is_empty()); + assert!( + analyze_for_reader(&unicode.replace("(#café)", "(#caf)"), None, None).unwrap() + ["diagnostics"] + .as_array() + .unwrap() + .is_empty() + ); + } + #[test] + fn edits_require_revision_confirmation_and_keep_recovery_out_of_document_folder() { + let dir = tempfile::tempdir().unwrap(); + let app = app(dir.path()); + let file = dir.path().join("original.md"); + let original = b"# Notes\r\n\r\nOriginal text.\r\n"; + fs::write(&file, original).unwrap(); + let rev = revision(original); + let preview = edit_document( + &app, &file, &rev, "Original", "Revised", false, true, false, None, + ) + .unwrap(); + assert_eq!(preview["changed"], true); + assert_eq!(fs::read(&file).unwrap(), original); + assert_eq!( + edit_document(&app, &file, &rev, "Original", "Revised", false, false, false, None) + .unwrap_err(), + "confirmation_required" + ); + let result = edit_document( + &app, &file, &rev, "Original", "Revised", false, false, true, None, + ) + .unwrap(); + assert_eq!( + fs::read(&file).unwrap(), + b"# Notes\r\n\r\nRevised text.\r\n" + ); + let id = result["checkpoint_id"].as_str().unwrap(); + assert_eq!( + crate::document_history::read(&app.paths.data_dir, id) + .unwrap() + .1, + "# Notes\r\n\r\nOriginal text.\r\n" + ); + assert!(!dir.path().join("original.md.bak").exists()); + assert_eq!( + edit_document(&app, &file, &rev, "Revised", "Lost", false, false, true, None) + .unwrap_err(), + "document_conflict" + ); + } + #[test] + fn editing_preserves_utf16_bom_mixed_eol_and_rejects_ambiguous_or_explosive_changes() { + let dir = tempfile::tempdir().unwrap(); + let app = app(dir.path()); + let file = dir.path().join("mixed.md"); + let text = "# Notes\r\nsame\nsame\r\n"; + let bytes = crate::text_encoding::encode(text, "utf-16le").unwrap(); + fs::write(&file, &bytes).unwrap(); + let rev = revision(&bytes); + assert_eq!( + edit_document(&app, &file, &rev, "same", "new", false, true, false, None).unwrap_err(), + "ambiguous_edit_match" + ); + let result = + edit_document(&app, &file, &rev, "same", "new", true, false, true, None).unwrap(); + assert_eq!(result["encoding"], "utf-16-le"); + assert_eq!( + fs::read(&file).unwrap(), + crate::text_encoding::encode("# Notes\r\nnew\nnew\r\n", "utf-16le").unwrap() + ); + fs::write(&file, "a".repeat(64 * 1024)).unwrap(); + assert_eq!( + edit_document( + &app, + &file, + &revision(&fs::read(&file).unwrap()), + "a", + &"b".repeat(64 * 1024), + true, + true, + false, + None + ) + .unwrap_err(), + "document_too_large" + ); + } + #[test] + fn legacy_chinese_encoding_is_preserved_and_lossy_input_is_never_overwritten() { + let dir = tempfile::tempdir().unwrap(); + let app = app(dir.path()); + let file = dir.path().join("reading.md"); + let bytes = crate::text_encoding::encode("# 阅读\n\n你好,世界。\n", "gb18030").unwrap(); + fs::write(&file, &bytes).unwrap(); + let changed = edit_document( + &app, + &file, + &revision(&bytes), + "世界", + "哲学", + false, + false, + true, + None, + ) + .unwrap(); + assert_eq!(changed["encoding"], "gb18030"); + assert_eq!( + fs::read(&file).unwrap(), + crate::text_encoding::encode("# 阅读\n\n你好,哲学。\n", "gb18030").unwrap() + ); + let invalid = b"\xef\xbb\xbf\xff"; + fs::write(&file, invalid).unwrap(); + assert_eq!( + edit_document( + &app, + &file, + &revision(invalid), + "x", + "y", + false, + false, + true, + None + ) + .unwrap_err(), + "encoding_not_lossless" + ); + assert_eq!(fs::read(&file).unwrap(), invalid); + } + #[test] + fn cancellation_and_invalid_ranges_do_not_change_files() { + let dir = tempfile::tempdir().unwrap(); + let app = app(dir.path()); + let file = dir.path().join("notes.md"); + fs::write(&file, "# Notes\n").unwrap(); + let cancel = AtomicBool::new(true); + assert_eq!( + edit_document( + &app, + &file, + &revision(b"# Notes\n"), + "Notes", + "Change", + false, + false, + true, + Some(&cancel) + ) + .unwrap_err(), + "cancelled" + ); + assert_eq!( + search_workspace(dir.path(), "Notes", 20, Some(&cancel)).unwrap_err(), + "cancelled" + ); + assert_eq!(read_document(&file, 5, 8).unwrap_err(), "line_out_of_range"); + assert_eq!(fs::read_to_string(&file).unwrap(), "# Notes\n"); + } + #[test] + fn block_tags_and_unchecked_large_anchor_targets_are_reported_honestly() { + let dir = tempfile::tempdir().unwrap(); + let file = dir.path().join("index.md"); + fs::write( + &file, + "---\ntags:\n - philosophy\n - 'reading'\n---\n# Reading\n", + ) + .unwrap(); + assert_eq!( + search_workspace(dir.path(), "tag:reading", 20, None).unwrap()["matched"], + 1 + ); + fs::write( + dir.path().join("large.md"), + "x".repeat(MAX_DOCUMENT_BYTES + 1), + ) + .unwrap(); + let result = analyze("[large](large.md#unknown)", Some(&file), Some(dir.path())).unwrap(); + assert_eq!(result["truncated"], true); + assert!(result["diagnostics"].as_array().unwrap().is_empty()); + } + #[test] + fn inspection_uses_ast_and_reports_utf16_locations() { + let report=analyze("# Alpha\n\n- [x] done\n- [ ] next\n\n[bad](#missing)\n\n~~~md\n[ignored](gone.md)\n~~~\n",None,None).unwrap(); + assert_eq!(report["links"].as_array().unwrap().len(), 1); + assert_eq!(report["diagnostics"][0]["code"], "missing_anchor"); + assert_eq!(report["stats"]["completed_tasks"], 1); + assert_eq!(report["headings"][0]["position"]["line"], 1); + } + #[test] + fn workspace_search_combines_phrases_paths_tags_and_exclusions() { + let dir = tempfile::tempdir().unwrap(); + fs::write( + dir.path().join("reading.md"), + "---\ntags: [philosophy, reading]\n---\n# Plato\n\nA careful argument.\n", + ) + .unwrap(); + fs::write( + dir.path().join("other.md"), + "# Other\nA careless argument.\n", + ) + .unwrap(); + let root = crate::paths::canonicalize_or_clean(dir.path()); + let report = search_workspace( + &root, + "tag:philosophy \"careful argument\" -careless", + 10, + None, + ) + .unwrap(); + assert_eq!(report["matched"], 1); + assert_eq!(report["results"][0]["relative_path"], "reading.md"); + assert!(search_workspace(&root, "\"unfinished", 10, None).is_err()); + } + #[test] + fn local_checks_do_not_read_outside_workspace_and_wiki_resolves_subfolder() { + let dir = tempfile::tempdir().unwrap(); + let root = crate::paths::canonicalize_or_clean(dir.path()); + fs::create_dir(root.join("notes")).unwrap(); + fs::write(root.join("notes/Plato.md"), "# Plato").unwrap(); + let file = root.join("index.md"); + let report = analyze( + "[[Plato]]\n\n[escape](../secret.md)\n", + Some(&file), + Some(&root), + ) + .unwrap(); + assert!(report["links"][0]["resolved_path"] + .as_str() + .unwrap() + .ends_with("Plato.md")); + assert_eq!(report["diagnostics"][0]["code"], "outside_workspace"); + } +} diff --git a/rust/readmd-kernel/src/lib.rs b/rust/readmd-kernel/src/lib.rs index f821a3cc..380f1ebc 100644 --- a/rust/readmd-kernel/src/lib.rs +++ b/rust/readmd-kernel/src/lib.rs @@ -13,6 +13,7 @@ pub mod code_chunk_runner; pub mod codecs; pub mod content; pub mod document_history; +pub mod document_intelligence; pub mod convert; pub mod convert_ext; pub mod crypto; diff --git a/rust/readmd-kernel/src/mcp.rs b/rust/readmd-kernel/src/mcp.rs index 190d38e2..4cc8ed9e 100644 --- a/rust/readmd-kernel/src/mcp.rs +++ b/rust/readmd-kernel/src/mcp.rs @@ -168,6 +168,23 @@ pub fn tools() -> Value { { "name": "readmd_pdf_audit", "description": "Inspect a PDF: page count, page sizes, rotation, text layer, read-only flag and processes holding it open.", "inputSchema": schema(json!({ "pdf_path": { "type": "string" } }), &["pdf_path"]) }, + { "name": "readmd_analyze_document", + "description": "Inspect current Markdown without changing it: AST headings, local/wiki links, anchors, task counts and UTF-16 diagnostic locations. Optional workspace checks never fetch URLs.", + "inputSchema": schema(json!({"content":{"type":"string"},"file_path":{"type":"string"},"workspace_root":{"type":"string"}}), &["content"]) }, + { "name": "readmd_search_workspace", + "description": "Read-only, bounded search of current local Markdown files. Combine quoted phrases, path:, title:, tag:, and -exclusions. Ignores hidden and generated directories; reports skipped/truncated results.", + "inputSchema": schema(json!({"workspace_root":{"type":"string"},"query":{"type":"string"},"limit":{"type":"integer","default":20}}), &["workspace_root","query"]) }, + { "name": "readmd_read_document", + "description": "Read at most 1000 lines of a local text document with its byte SHA-256 revision, encoding and EOL. Limited to 2 MiB; use the revision for conflict-protected edits.", + "inputSchema": schema(json!({"file_path":{"type":"string"},"start_line":{"type":"integer","default":1},"end_line":{"type":"integer","default":1000}}), &["file_path"]) }, + { "name": "readmd_edit_document", + "description": "Preview a literal replacement by default. Commit only with dry_run:false, confirm:true and a matching byte revision. Ambiguous matches require replace_all. Preserves encoding/EOL; records recoverable history outside the document folder.", + "inputSchema": schema(json!({"file_path":{"type":"string"},"expected_revision":{"type":"string"}, + "old_text":{"type":"string"},"new_text":{"type":"string"},"replace_all":{"type":"boolean","default":false}, + "dry_run":{"type":"boolean","default":true},"confirm":confirm_prop()}), &["file_path","expected_revision","old_text","new_text"]) }, + { "name": "readmd_document_history", + "description": "List or read bounded recovery checkpoints belonging to one local document. Read-only; never restores or overwrites automatically.", + "inputSchema": schema(json!({"file_path":{"type":"string"},"operation":{"type":"string","enum":["list","read"],"default":"list"},"checkpoint_id":{"type":"string"}}), &["file_path"]) }, { "name": "readmd_pdf_rollback", "description": "Restore a PDF from its .bak backup written by a previous ReadMD edit.", "inputSchema": schema(json!({ "pdf_path": { "type": "string" }, "confirm": confirm_prop() }), &["pdf_path", "confirm"]) }, @@ -175,6 +192,7 @@ pub fn tools() -> Value { for tool in list.as_array_mut().unwrap() { let name = tool["name"].as_str().unwrap_or(""); let read_only = matches!(name, "readmd_fix_markdown" | "readmd_generate_toc" | "readmd_latex_to_md" | "readmd_md_to_latex" | "readmd_latex_to_omml" | "readmd_parse_bibtex" | "readmd_process_imports" | "readmd_ai_assistant" | "readmd_ai_providers" | "readmd_export_presets" | "readmd_pdf_audit"); + let read_only = read_only || matches!(name, "readmd_analyze_document" | "readmd_search_workspace" | "readmd_read_document" | "readmd_document_history"); let open_world = matches!(name, "readmd_web_to_markdown" | "readmd_ai_chat" | "readmd_ai_models" | "readmd_run_code_chunk"); tool["annotations"] = json!({ "readOnlyHint": read_only, "destructiveHint": !read_only, "idempotentHint": read_only, "openWorldHint": open_world }); } @@ -405,6 +423,45 @@ fn validate_tool_arguments(name: &str, args: &Value) -> Result<(), &'static str> fn dispatch_tool(app: &Arc, name: &str, args: &Value, cancel: Option<&AtomicBool>) -> Result { match name { + "readmd_analyze_document" => { + let file = arg_str(args, "file_path", ""); let root = arg_str(args, "workspace_root", ""); + if (!file.is_empty() && !Path::new(&file).is_absolute()) || (!root.is_empty() && !Path::new(&root).is_absolute()) { + return Err(error_result("file_path_must_be_absolute", None)); + } + crate::document_intelligence::analyze(&arg_str(args,"content",""), + (!file.is_empty()).then(||Path::new(&file)), (!root.is_empty()).then(||Path::new(&root))) + .map(|v|json_result(&v)).map_err(|code|error_result(&code,None)) + } + "readmd_search_workspace" => crate::document_intelligence::search_workspace( + Path::new(&arg_str(args,"workspace_root","")), &arg_str(args,"query",""), + arg_i64(args,"limit",20).try_into().unwrap_or(0),cancel) + .map(|v|json_result(&v)).map_err(|code|error_result(&code,None)), + "readmd_read_document" => crate::document_intelligence::read_document( + Path::new(&arg_str(args,"file_path","")),arg_i64(args,"start_line",1).try_into().unwrap_or(0), + arg_i64(args,"end_line",1000).try_into().unwrap_or(0)) + .map(|v|json_result(&v)).map_err(|code|error_result(&code,None)), + "readmd_edit_document" => crate::document_intelligence::edit_document(app, + Path::new(&arg_str(args,"file_path","")), &arg_str(args,"expected_revision",""), + &arg_str(args,"old_text",""),&arg_str(args,"new_text",""),arg_bool(args,"replace_all",false), + arg_bool(args,"dry_run",true),arg_bool(args,"confirm",false),cancel) + .map(|v|json_result(&v)).map_err(|code|error_result(&code,None)), + "readmd_document_history" => { + let raw=arg_str(args,"file_path",""); + let source=Path::new(&raw); + if !source.is_absolute() { return Err(error_result("file_path_must_be_absolute",None)); } + let path=crate::paths::canonicalize_or_clean(source); + if path.is_dir() || !crate::content::is_readable(&path) { return Err(error_result("unsupported_text_file",None)); } + let key=crate::paths::canonicalize_or_clean(&path).to_string_lossy().into_owned(); + if arg_str(args,"operation","list")=="read" { + let (entry,text)=crate::document_history::read(&app.paths.data_dir,&arg_str(args,"checkpoint_id","")) + .map_err(|_|error_result("history_not_found",None))?; + if entry.doc_key!=key { return Err(error_result("history_document_mismatch",None)); } + Ok(json_result(&json!({"ok":true,"entry":entry,"content":text}))) + } else { + let entries=crate::document_history::list(&app.paths.data_dir,Some(&key)).map_err(|_|error_result("history_read_failed",None))?; + Ok(json_result(&json!({"ok":true,"entries":entries}))) + } + } "readmd_fix_markdown" => { let res = crate::readmd_fix::fix_markdown(&arg_str(args, "content", "")); Ok(json_result(&json!({ diff --git a/rust/readmd-kernel/src/server.rs b/rust/readmd-kernel/src/server.rs index 3d94b80b..5cf764be 100644 --- a/rust/readmd-kernel/src/server.rs +++ b/rust/readmd-kernel/src/server.rs @@ -621,6 +621,8 @@ pub const ROUTES: &[(&str, Handler)] = &[ ("/api/ping", h_ping), ("/api/kernel/status", h_kernel_status), // KERNEL BRIDGE — non-parity, see its doc comment ("/api/file", h_file), + ("/api/document/analyze", h_document_analyze), // V0.0.5 KERNEL BRIDGE, non-parity + ("/api/workspace/search", h_workspace_search), // V0.0.5 KERNEL BRIDGE, non-parity ("/api/list", h_list), ("/raw", h_raw), ("/api/save", h_save), @@ -3524,6 +3526,32 @@ fn h_ai_models(app: &Arc, req: &Request) -> ApiResult { } } +/// V0.0.5 KERNEL BRIDGE: inspect a current draft, without repairing or saving it. +fn h_document_analyze(app: &Arc, req: &Request) -> ApiResult { + if req.method != "POST" { return Err(ApiError::bad_request("method_not_allowed")); } + let body=body_value(req); + let Some(text)=body.get("content").and_then(Value::as_str) else { return Err(ApiError::bad_request("invalid_request")); }; + let file=body.get("file_path").and_then(Value::as_str).filter(|p|!p.is_empty()).map(Path::new); + if file.is_some_and(|p|!p.is_absolute()) { return Err(ApiError::bad_request("file_path_must_be_absolute")); } + let root=body.get("workspace_root").and_then(Value::as_str).filter(|p|!p.is_empty()).map(Path::new) + .or_else(||file.and_then(Path::parent)).or(Some(app.paths.workspace.as_path())); + crate::document_intelligence::analyze_for_reader(text,file,root).map_err(|e|ApiError::bad_request(&e)).and_then(ok_json) +} + +/// V0.0.5 KERNEL BRIDGE: bounded, read-only search over current workspace files. +fn h_workspace_search(app: &Arc, req: &Request) -> ApiResult { + if req.method != "POST" { return Err(ApiError::bad_request("method_not_allowed")); } + let body=body_value(req); + let Some(query)=body.get("query").and_then(Value::as_str) else { return Err(ApiError::bad_request("invalid_request")); }; + let root=body.get("workspace_root").and_then(Value::as_str).map(Path::new).unwrap_or(&app.paths.workspace); + let limit=match body.get("limit") { + None => 20, + Some(v) => v.as_u64().filter(|n| (1..=100).contains(n)) + .ok_or_else(||ApiError::bad_request("invalid_search_limit"))? as usize, + }; + crate::document_intelligence::search_workspace(root,query,limit,None).map_err(|e|ApiError::bad_request(&e)).and_then(ok_json) +} + // --------------------------------------------------------------------------- // `ai_providers` wiring // diff --git a/rust/xtask/src/version.rs b/rust/xtask/src/version.rs index ee28bfae..4456f01d 100644 --- a/rust/xtask/src/version.rs +++ b/rust/xtask/src/version.rs @@ -234,6 +234,19 @@ fn build_plan(root: &Path, v: &str) -> Result { let hc = sv.harmony_code(); let mut plan = Plan::default(); + // Workspace package metadata must match the executable's VERSION too. + if let Some(t) = read_text(&root.join("rust/Cargo.toml")) { + let s = sub(&t.original, r#"(?m)^version = "[^"]+"$"#, &format!("version = \"{v}\"")); + plan.push_if_changed(&t,s); + } + if let Some(t) = read_text(&root.join("rust/Cargo.lock")) { + let mut s=t.original.clone(); + for name in ["readmd-kernel","xtask"] { + s=sub(&s,&format!(r#"(name = "{name}"\nversion = ")[^"]+""#),&format!("${{1}}{v}\"")); + } + plan.push_if_changed(&t,s); + } + // .env / .env.example / VERSION let env_path = root.join(".env"); let old_env = read_text(&env_path).unwrap_or(Text { path: env_path.clone(), crlf: false, original: String::new() }); @@ -336,6 +349,11 @@ fn build_plan(root: &Path, v: &str) -> Result { // Website let site = root.join("website/public"); if site.is_dir() { + // Public links follow the published release, not the local candidate. + let published = fs::read_to_string(root.join("website/release.json")).ok() + .and_then(|s| serde_json::from_str::(&s).ok()) + .and_then(|j| j["stable"].as_str().map(str::to_owned)); + let v = published.as_deref().unwrap_or(v); let mut files: Vec = walkdir::WalkDir::new(&site) .sort_by_file_name() .into_iter() @@ -348,8 +366,8 @@ fn build_plan(root: &Path, v: &str) -> Result { for p in files { let Some(t) = read_text(&p) else { continue }; let s = artifact_names(&t.original, v); - let s = sub(&s, r"(releases/tag/v)[0-9a-zA-Z.-]+", &format!("${{1}}{v}")); - let s = sub(&s, r"(releases/download/v)[0-9a-zA-Z.-]+", &format!("${{1}}{v}")); + let s = sub(&s, r"releases/tag/[vV][0-9a-zA-Z.-]+", &format!("releases/tag/V{v}")); + let s = sub(&s, r"releases/download/[vV][0-9a-zA-Z.-]+", &format!("releases/download/V{v}")); let s = sub(&s, r#"("softwareVersion":\s*")[^"]+""#, &format!("${{1}}{v}\"")); let s = sub(&s, r#"("artifactSection":\s*"v)[^"]+""#, &format!("${{1}}{v}\"")); let s = sub(&s, r"2\.3\.7-beta\.\d+", v); diff --git a/scripts/linux/PKGBUILD b/scripts/linux/PKGBUILD index 4778facb..0f3c45a9 100644 --- a/scripts/linux/PKGBUILD +++ b/scripts/linux/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: Natsummerance pkgname=readmd-bin -pkgver=0.0.4 +pkgver=0.0.5 pkgrel=1 pkgdesc="Lightweight native Markdown viewer and editor with auto-repair and presentation mode" arch=('x86_64' 'aarch64') diff --git a/scripts/linux/linglong.yaml b/scripts/linux/linglong.yaml index 51223a6e..9b05f01a 100644 --- a/scripts/linux/linglong.yaml +++ b/scripts/linux/linglong.yaml @@ -1,7 +1,7 @@ package: id: io.github.natsummerance.readmd name: ReadMD - version: 0.0.4.0 + version: 0.0.5.0 kind: app description: | ReadMD 是全功能、本地隐私优先的 Markdown 阅读、编辑与排版套件。 diff --git a/tools/package-windows-candidate.mjs b/tools/package-windows-candidate.mjs new file mode 100644 index 00000000..2b94ceb7 --- /dev/null +++ b/tools/package-windows-candidate.mjs @@ -0,0 +1,35 @@ +// Offline, reviewable portable candidate. No installer or registry mutation. +import fs from 'node:fs'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import {execFileSync} from 'node:child_process'; +import {fileURLToPath} from 'node:url'; +import {zip} from './lib/zip.mjs'; +const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..'); +const binary=path.resolve(process.argv[2]||''); +const output=path.resolve(process.argv[3]||'dist/candidate'); +const pet=path.resolve(process.argv[4]||'dist/ReadMD-Pet-Rust.zip'); +const version=fs.readFileSync(path.join(root,'VERSION'),'utf8').trim(); +if(!binary||!fs.statSync(binary,{throwIfNoEntry:false})?.isFile())throw Error('Provide a built Windows binary'); +if(!execFileSync(binary,['--version'],{encoding:'utf8',windowsHide:true}).includes(version))throw Error('Binary version differs from VERSION'); +if(!fs.statSync(pet,{throwIfNoEntry:false})?.isFile())throw Error('Provide the verified desktop pet archive'); +const entries=[]; +const add=(name,file)=>entries.push({name,data:fs.readFileSync(file)}); +function walk(directory,prefix){ + for(const entry of fs.readdirSync(directory,{withFileTypes:true}).sort((a,b)=>a.name.localeCompare(b.name,'en'))){ + const file=path.join(directory,entry.name),name=prefix+'/'+entry.name; + if(entry.isSymbolicLink())throw Error('Candidate assets must not contain symlinks'); + if(entry.isDirectory())walk(file,name);else if(entry.isFile())add(name,file); + } +} +add('ReadMD.exe',binary);add('ReadMD-Pet-Rust.zip',pet); +for(const name of ['LICENSE','README.md','RELEASE_NOTES.md'])add(name,path.join(root,name)); +walk(path.join(root,'assets'),'assets'); +fs.mkdirSync(output,{recursive:true}); +const name='ReadMD-windows-x64.zip',file=path.join(output,name); +const bytes=zip(entries);fs.writeFileSync(file,bytes); +const sha=crypto.createHash('sha256').update(bytes).digest('hex'); +fs.writeFileSync(path.join(output,'candidate.json'),JSON.stringify({version,channel:'candidate',platform:'windows-x64', + file:name,sha256:sha,files:entries.length,bytes:bytes.length,source_commit:execFileSync('git',['rev-parse','HEAD'],{cwd:root,encoding:'utf8'}).trim()},null,2)+'\n'); +fs.writeFileSync(path.join(output,'SHA256SUMS.txt'),sha+' '+name+'\n'); +console.log('Prepared '+version+' portable candidate: '+entries.length+' files; '+bytes.length+' bytes.'); diff --git a/ui-tests/document-intelligence.spec.js b/ui-tests/document-intelligence.spec.js new file mode 100644 index 00000000..5a438878 --- /dev/null +++ b/ui-tests/document-intelligence.spec.js @@ -0,0 +1,37 @@ +const { test, expect } = require('@playwright/test'); +test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.waitForFunction(() => window.__readmdAppReady); + await page.evaluate(() => i18n.setLanguage('zh-CN', false)); +}); +test('repair report inspects the current unsaved draft without changing it', async ({ page }) => { + await page.evaluate(async () => { + await renderVirtual('clipboard', '检查演示.md', '', '# Original\n', []); + await toggleEdit(); + cmView.dispatch({ changes: { from: 0, to: cmView.state.doc.length, + insert: '# 新标题\n\n[需要检查](#missing)\n\n~~~md\n[代码示例](missing.md)\n~~~\n' } }); + showFixModal(); + }); + await expect(page.locator('.document-inspection-summary')).toHaveText('文档检查:1 项待检查'); + await expect(page.locator('.document-inspection-issue')).toHaveCount(1); + await expect(page.locator('.document-inspection-issue')).toContainText('未找到目标标题'); + expect(await page.evaluate(() => getEditContent())).toContain('[需要检查](#missing)'); + expect(await page.evaluate(() => state.original)).toBe('# Original\n'); +}); +test('report handles service failure and language changes without stale or raw keys', async ({ page }) => { + await page.route('**/api/document/analyze', route => route.fulfill({ status: 503, contentType: 'application/json', body: '{"ok":false}' })); + await page.evaluate(async () => { await renderVirtual('clipboard', '检查演示.md', '', '# Title\n', []); showFixModal(); }); + await expect(page.locator('.document-inspection-summary')).toContainText('文档检查暂不可用'); + await page.unroute('**/api/document/analyze'); + await page.evaluate(async () => { document.getElementById('fix-modal').classList.add('hidden'); await i18n.setLanguage('en', false); showFixModal(); }); + await expect(page.locator('.document-inspection-summary')).toHaveText('Document inspection: 0 items to review'); +}); +test('workspace search rejects invalid limits rather than silently defaulting', async ({ page }) => { + const result = await page.evaluate(async () => { + const response = await apiFetch('/api/workspace/search', { method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: 'title:Plato', limit: -1 }) }); + return { status: response.status, body: await response.json() }; + }); + expect(result.status).toBe(400); + expect(result.body.error_code).toBe('invalid_search_limit'); +}); diff --git a/ui-tests/package.json b/ui-tests/package.json index 09758175..2e0aba45 100644 --- a/ui-tests/package.json +++ b/ui-tests/package.json @@ -1,7 +1,7 @@ { "name": "readmd-ui-tests", "private": true, - "version": "0.0.4", + "version": "0.0.5", "devDependencies": { "@playwright/test": "1.54.0" }, diff --git a/website/public/.well-known/security.txt b/website/public/.well-known/security.txt index 9989b308..e8b62fab 100644 --- a/website/public/.well-known/security.txt +++ b/website/public/.well-known/security.txt @@ -1,4 +1,4 @@ -Contact: https://github.com/Natsummerance/readMD/security/advisories/new +Contact: https://github.com/Natsummerance/rust-ReadMD/security/advisories/new Expires: 2027-08-26T00:00:00Z Preferred-Languages: en, zh-CN, zh-TW, ja Canonical: https://rust.readmd.asia/.well-known/security.txt diff --git a/website/public/assets/site.js b/website/public/assets/site.js index 25b66255..913f571e 100644 --- a/website/public/assets/site.js +++ b/website/public/assets/site.js @@ -64,21 +64,19 @@ }); // Initialize links with data-mirror-url if present - document.querySelectorAll('a[href^="https://github.com/Natsummerance/readMD/releases/download/"]').forEach((link) => { + document.querySelectorAll('a[href^="https://github.com/Natsummerance/rust-ReadMD/releases/download/"]').forEach((link) => { if (!link.dataset.mirrorUrl) link.dataset.mirrorUrl = link.href; }); updateDownloadLinks(activeMirror); // Dual-Layer Dynamic Version & Release Synchronizer - const GITHUB_REPO = 'Natsummerance/readMD'; - const CACHE_KEY = 'readmd_release_cache_v2'; - const CACHE_TTL = 10 * 60 * 1000; // 10 minutes + const GITHUB_REPO = 'Natsummerance/rust-ReadMD'; const applyVersionData = (versionTag, pureVersion) => { if (!versionTag) return; - const cleanTag = versionTag.startsWith('v') ? versionTag : `v${versionTag}`; - const cleanPure = pureVersion || cleanTag.replace(/^v/, ''); + const cleanTag = /^[vV]/.test(versionTag) ? versionTag : `v${versionTag}`; + const cleanPure = pureVersion || cleanTag.replace(/^[vV]/, ''); document.querySelectorAll('.latest-version-badge, [data-version-slot]').forEach((el) => { el.textContent = cleanTag; @@ -99,7 +97,7 @@ const oldTagMatch = orig.match(/\/releases\/download\/([^/]+)\//); if (oldTagMatch && oldTagMatch[1] && oldTagMatch[1] !== cleanTag) { const oldTag = oldTagMatch[1]; - const oldPure = oldTag.replace(/^v/, ''); + const oldPure = oldTag.replace(/^[vV]/, ''); let updated = orig.replace(new RegExp(`/releases/download/${oldTag}/`, 'g'), `/releases/download/${cleanTag}/`); updated = updated.replace(new RegExp(`-${oldPure}\\.`, 'g'), `-${cleanPure}.`); link.dataset.mirrorUrl = updated; @@ -110,51 +108,13 @@ updateDownloadLinks(activeMirror); }; - // 1. Try Cached GitHub Release - let isApplied = false; - try { - const cached = sessionStorage.getItem(CACHE_KEY); - if (cached) { - const parsed = JSON.parse(cached); - if (Date.now() - parsed.timestamp < CACHE_TTL && parsed.tag_name) { - applyVersionData(parsed.tag_name, parsed.pure_version); - isApplied = true; - } - } - } catch (e) {} - - // 2. Fetch Latest from GitHub API (or fallback to /version.json) - if (!isApplied) { - fetch(`https://api.github.com/repos/${GITHUB_REPO}/releases/latest`, { - headers: { 'Accept': 'application/vnd.github.v3+json' } - }) - .then((res) => (res.ok ? res.json() : Promise.reject(new Error('Rate limited / network')))) - .then((data) => { - if (data && data.tag_name) { - const tag = data.tag_name; - const pure = tag.replace(/^v/, ''); - try { - sessionStorage.setItem(CACHE_KEY, JSON.stringify({ - timestamp: Date.now(), - tag_name: tag, - pure_version: pure - })); - } catch (e) {} - applyVersionData(tag, pure); - } - }) - .catch(() => { - // Fallback to local build-time version.json - fetch('/version.json') - .then((res) => (res.ok ? res.json() : null)) - .then((vData) => { - if (vData && vData.releaseTag) { - applyVersionData(vData.releaseTag, vData.version); - } - }) - .catch(() => {}); - }); - } + // Published manifest is same-origin and independent of API rate limits. + fetch('/version.json', { signal: AbortSignal.timeout(5000) }) + .then(response => response.ok ? response.json() : null) + .then(data => { + if (data && /^V\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(data.releaseTag)) + applyVersionData(data.releaseTag, data.version); + }).catch(() => {}); }; /* Interactive MCP Configuration & 1-Click Multi-Harness Generator */ diff --git a/website/public/download/index.html b/website/public/download/index.html index 7d065fa5..654f8872 100644 --- a/website/public/download/index.html +++ b/website/public/download/index.html @@ -3,14 +3,14 @@ - Download ReadMD | v2.3.9 Stable Release - + Download ReadMD | V0.0.4 Stable Release + - - + + @@ -22,7 +22,7 @@ - + @@ -141,13 +141,13 @@
- Stable · v2.3.9 + Stable · V0.0.4

Download ReadMD

- This page lists ReadMD v2.3.9 stable, released on GitHub on September 10, 2026. It ships through the READMD_SELF_USE_RELEASE self-use channel: assets are not Authenticode or codesign signed and the native platform evidence list is informational — verify SHA256SUMS.txt before installing. + This page lists ReadMD V0.0.4 stable, released on GitHub on October 7, 2026. It ships through the READMD_SELF_USE_RELEASE self-use channel: assets are not Authenticode or codesign signed and the native platform evidence list is informational — verify SHA256SUMS.txt before installing.

@@ -290,7 +290,7 @@

- + VS Code Extension (.vsix) @@ -322,7 +322,7 @@

- + Download MCP Package (.zip) @@ -552,7 +552,7 @@

ReadMD × Agent Ecosyst }
- + Download Package (.zip) diff --git a/website/public/feed.xml b/website/public/feed.xml index c41b63bc..89c76902 100644 --- a/website/public/feed.xml +++ b/website/public/feed.xml @@ -316,4 +316,8 @@ ReadMD is a local-first Markdown workspace for Windows, macOS, Linux, and Chinese operating systems. It opens long documents instantly, repairs rendering without changing source files, converts Office and PDF files, and presents Markdown directly. https://rust.readmd.asia/showcase/ReadMD 功能操作演示2026-10-03T00:00:00Z按最新清单浏览阅读、编辑、保存恢复、转换、导出和桌宠的真实操作短视频。 +https://rust.readmd.asia/integrations/ReadMD in VS Code and MCP2026-10-07T00:00:00ZLocal engine integrations and V0.0.5 preview. +https://rust.readmd.asia/zh-cn/integrations/ReadMD 的 VS Code 与 MCP 集成2026-10-07T00:00:00Z本地内核集成与 V0.0.5 新能力预览。 +https://rust.readmd.asia/zh-tw/integrations/ReadMD 的 VS Code 與 MCP 整合2026-10-07T00:00:00Z本機核心整合與 V0.0.5 新能力預覽。 +https://rust.readmd.asia/ja/integrations/ReadMD の VS Code と MCP 連携2026-10-07T00:00:00Zローカルエンジンの連携と V0.0.5 の新機能。 diff --git a/website/public/index.html b/website/public/index.html index e2636a31..80eccb05 100644 --- a/website/public/index.html +++ b/website/public/index.html @@ -212,4 +212,4 @@

Questions, answered.

How does ReadMD open long Markdown files?

ReadMD activates semantic pagination when a document exceeds roughly 8,000 lines or 500 KB. The outline, full-text search, formulas, and page controls remain linked, so readers can move through very large notes without waiting for the whole file to render at once.

Does automatic repair change my Markdown?

No. ReadMD applies syntax recovery only to the in-memory display layer. Missing table separator rows, unclosed markers, and formula formatting issues are corrected visually, while the original Markdown bytes remain exactly as stored until you explicitly save an edit.

Can ReadMD replace slide software?

For technical talks and course material, ReadMD presents the same Markdown used for writing and review. Reveal-based presentation mode preserves headings, math, diagrams, and code, so a last-minute edit does not require copying content into a separate deck.

Which platforms does ReadMD support?

ReadMD v2.3.9 is the current stable release for Windows 10/11, macOS 13+, and Linux x86_64/ARM64. Packages for domestic systems such as UOS, Kylin, and Deepin remain pending native evidence and are not formal support commitments. A VSCode extension and MCP server connect the same workflows to editors and AI tools.

Is ReadMD private?

Core reading, editing, pagination, OCR, and file conversion are designed to run locally. Documents do not need a cloud account. Optional web fetching and sharing are explicit actions; LAN sharing is intended for devices on the same network.

What license does ReadMD use?

ReadMD is released under the MIT license. Source code, release downloads, checksum lists, and platform-specific packages are published in the public GitHub repository at Natsummerance/readMD.

Share ReadMD

- + diff --git a/website/public/integrations/index.html b/website/public/integrations/index.html new file mode 100644 index 00000000..22a5022f --- /dev/null +++ b/website/public/integrations/index.html @@ -0,0 +1,5 @@ + +Markdown in VS Code and AI workflows | ReadMD + + +

V0.0.5 release candidate · not published yet

Markdown in VS Code and AI workflows

One local Rust engine. Your documents stay in your workflow.

VS Code, without another workspace

Live preview, math, diagrams and slides. V0.0.5 adds link diagnostics to the native Problems panel, clickable wiki links and local workspace search.

AI edits you can review

V0.0.5 MCP reads a document revision, previews literal edits and writes only after explicit confirmation. Conflicting file versions stop the edit; recovery history stays in application data.

Local files, current results

Search phrases, titles, paths and YAML tags directly in the workspace. Limits and skipped files are reported. URLs are never fetched during link inspection.

Read → preview → confirm → recover

Use the same executable installed with ReadMD. The MCP kit contains documentation and client configuration, not a second server binary.

readmd --mcp

V0.0.5 adds 5 tools to the 22-tool stable core. Availability follows the installed desktop version.

Integration documentation

Bounded by design: 2 MiB per inspected/edited document, 5,000 searched Markdown files, 64 MiB per search, 8 concurrent MCP tools. Native runtimes and OS services are required for some formats.

diff --git a/website/public/ja/download/index.html b/website/public/ja/download/index.html index b0628468..54632ee9 100644 --- a/website/public/ja/download/index.html +++ b/website/public/ja/download/index.html @@ -3,12 +3,12 @@ - ReadMD ダウンロード | v2.3.9 正式版 - + ReadMD ダウンロード | V0.0.4 正式版 + - - + + @@ -33,8 +33,8 @@ - - + + + diff --git a/website/public/ja/integrations/index.html b/website/public/ja/integrations/index.html new file mode 100644 index 00000000..6d383685 --- /dev/null +++ b/website/public/ja/integrations/index.html @@ -0,0 +1,5 @@ + +Markdown をエディターと AI につなぐ | ReadMD + + +

V0.0.5 リリース候補 · 未公開

Markdown をエディターと AI につなぐ

同じローカル Rust エンジンを、いつもの作業環境で。

VS Code で集中する

数式、図、スライドをローカルで表示。V0.0.5 は標準の問題パネルにリンク診断を表示し、Wiki リンクと Markdown ワークスペース検索に対応します。

AI の編集を確認する

V0.0.5 MCP はファイルの版を読み取り、置換をまずプレビューします。明示的な確認後に保存し、競合時は停止。復元履歴はアプリのデータ領域に保存します。

現在のファイルを検索

フレーズ、タイトル、パスと YAML タグを組み合わせます。処理上限とスキップを報告し、リンク検査では URL に接続しません。

読み取り → プレビュー → 確認 → 復元

インストール済み ReadMD の実行ファイルを使用します。MCP パッケージは文書と接続設定を提供し、サーバー本体は含みません。

readmd --mcp

V0.0.5 は安定版の 22 ツールに 5 ツールを追加します。利用可能な機能はインストールした版によります。

連携ドキュメント

検査・編集は文書ごとに 2 MiB。検索は最大 5,000 ファイル、合計 64 MiB。MCP ツールは最大 8 件を並行実行。一部形式にはローカル実行環境や OS サービスが必要です。

diff --git a/website/public/ja/llms-full.txt b/website/public/ja/llms-full.txt index f33b0c4b..55ed356e 100644 --- a/website/public/ja/llms-full.txt +++ b/website/public/ja/llms-full.txt @@ -5,7 +5,7 @@ ReadMD は技術文書、研究ノート、講義資料、仕様書、書籍などを Markdown で管理する人向けです。中心原則は、Markdown ソースが常に正となることです。表示のための構文修復はあっても、利用者が明示的に保存しない限り元ファイルは上書きされません。 -プロジェクトは MIT ライセンスで https://github.com/Natsummerance/rust-ReadMD に公開されています。このサイトが参照する公開版は 2.3.7 です。リリースファイルと SHA256 チェックサムは GitHub Releases で配布されます。 +プロジェクトは MIT ライセンスで https://github.com/Natsummerance/rust-ReadMD に公開されています。このサイトが参照する公開版は V0.0.4 です。リリースファイルと SHA256 チェックサムは GitHub Releases で配布されます。 ## 解決する問題 @@ -44,7 +44,7 @@ ReadMD は技術文書、研究ノート、講義資料、仕様書、書籍な - Apple Silicon および Intel macOS。 - Linux x86_64 向け AppImage と Deb パッケージ。 - UOS、Kylin OS、Deepin などの中国系 OS は候補対象ですが、実機証拠が揃うまで正式対応を約束しません。 -- HarmonyOS/OpenHarmony 不属于 v2.3.7,源码仅作实验性预览。ソースは実験的プレビューです。 +- HarmonyOS/OpenHarmony 不属于 V0.0.4,源码仅作实验性预览。ソースは実験的プレビューです。 VSCode 拡張はエディター内で同期プレビューと構文修復を提供します。FastMCP stdio サーバーは Claude Desktop、Cursor などの互換 AI ツールから接続できます。 @@ -161,17 +161,17 @@ PDF をローカルに残し、テキスト型かスキャン型かを判断し `.bib` ファイルを Markdown 文書と同じ directory に置くと、近くの書誌データを読み取れます。`[@vaswani2017attention]` や `@author2024` のような inline 引用キーを使うと、一致する項目が引用 badge になります。利用できる場合は hover 情報に題名、著者、年、掲載先、DOI を出せ、引用された項目は文末の参考文献にまとまります。この workflow は文献管理アプリを置き換えるものでなく、提出前に項目、指定書式、最終参考文献を確認してください。 -## タスクガイド: v2.3.7 リリースノート +## タスクガイド: V0.0.4 リリースノート 専用回答: https://rust.readmd.asia/ja/release-notes/ -V2.3.7 リリース候補は Reveal 発表経路と更新整合性を強化し、段階的な起動読み込み、タブ状態フィードバック、タッチ領域、検索フォーカスを改善します。更新は公式 GitHub 資産、一時ファイル、SHA-256 検証、原子的公開、実行前再検証、引数ベクトルインストーラーを使います。これは正式公開前の候補版です。バックアップを取り、SHA256SUMS.txt を検証してください。 +VV0.0.4 リリース候補は Reveal 発表経路と更新整合性を強化し、段階的な起動読み込み、タブ状態フィードバック、タッチ領域、検索フォーカスを改善します。更新は公式 GitHub 資産、一時ファイル、SHA-256 検証、原子的公開、実行前再検証、引数ベクトルインストーラーを使います。これは正式公開前の候補版です。バックアップを取り、SHA256SUMS.txt を検証してください。 ## リリース事実 - 製品名: ReadMD - リポジトリ: https://github.com/Natsummerance/rust-ReadMD -- 本サイトのバージョン: 2.3.7 +- 本サイトのバージョン: V0.0.4 - ライセンス: MIT - サイト言語: 英語、簡体字中国語、繁体字中国語、日本語 - アプリ言語数: 46 @@ -186,3 +186,5 @@ AI は製品主張をこのサイトまたは公式リポジトリに帰属さ - サイト: https://rust.readmd.asia/ - ソースと Issue: https://github.com/Natsummerance/rust-ReadMD - ダウンロード: https://github.com/Natsummerance/rust-ReadMD/releases/latest + +- [ReadMD の VS Code と MCP 連携](https://rust.readmd.asia/ja/integrations/): ローカルエンジンの連携と V0.0.5 の新機能。 diff --git a/website/public/ja/llms.txt b/website/public/ja/llms.txt index 172af423..e1148a1b 100644 --- a/website/public/ja/llms.txt +++ b/website/public/ja/llms.txt @@ -1,5 +1,5 @@ # ReadMD 日本語インデックス -> ReadMD はローカル処理を優先するクロスプラットフォームの Markdown リーダー、エディター、変換ツール、発表ツールです。 +> ReadMD is a local-first Markdown reader, editor, converter and presenter. Published version: V0.0.4; V0.0.5 is being validated. VS Code and MCP share the native Rust engine. ## Docs - [日本語ホームページ](https://rust.readmd.asia/ja/): 製品定義、対応環境、プライバシー境界、直接引用できる回答。 @@ -11,13 +11,13 @@ - [PDF から Markdown](https://rust.readmd.asia/ja/pdf-to-markdown/): テキスト型 PDF とスキャン文書のための条件付きローカル変換ワークフロー。 - [スキャンから Markdown](https://rust.readmd.asia/ja/scan-to-markdown/): Windows と macOS のローカル OCR でスキャン頁やスクリーンショットから文字を抽出します。 - [BibTeX 引用](https://rust.readmd.asia/ja/bibtex-citations/): 同じ場所の `.bib` library を inline 引用 badge、文献カード、コピー可能な BibTeX、参考文献まとめに接続します。 -- [リリースノート](https://rust.readmd.asia/ja/release-notes/): V2.3.7リリース候補の安全な発表、更新検証、起動読み込み、タブ状態、プラットフォーム資産。 +- [リリースノート](https://rust.readmd.asia/ja/release-notes/): VV0.0.4リリース候補の安全な発表、更新検証、起動読み込み、タブ状態、プラットフォーム資産。 - [ダウンロードページ](https://rust.readmd.asia/ja/download/): Windows、macOS、Linux、中国系 OS、エディター、MCP パッケージの入手路径。 - [日本語全文コーパス](https://rust.readmd.asia/ja/llms-full.txt): 長文の改ページ、非破壊修復、変換、オフライン作業、リリース事実を説明します。 - [英語インデックス](https://rust.readmd.asia/llms.txt): 多言語入口、英語コーパス、公式な製品情報。 ## Key Facts -- 公開バージョン: 2.3.7 +- 公開バージョン: V0.0.4 - ライセンス: MIT - 長文の閾値: 約 8,000 行または 500 KB で意味単位の改ページ - 基本モデル: 読書、編集、改ページ、ローカル変換にクラウドアカウントは不要 @@ -25,3 +25,5 @@ ## Source - [GitHub リポジトリ](https://github.com/Natsummerance/rust-ReadMD): MIT ソースコード、リリースファイル、チェックサム、Issue。 + +- [ReadMD の VS Code と MCP 連携](https://rust.readmd.asia/ja/integrations/): ローカルエンジンの連携と V0.0.5 の新機能。 diff --git a/website/public/llms-full.txt b/website/public/llms-full.txt index c2c6f67e..8bd80c70 100644 --- a/website/public/llms-full.txt +++ b/website/public/llms-full.txt @@ -5,7 +5,7 @@ ReadMD is an application for people who keep documentation, research notes, course material, technical specifications, and books in Markdown. It is designed around one principle: the user's Markdown remains the canonical artifact. Reading and rendering may repair malformed syntax visually, but those repairs do not overwrite the file unless the user performs an explicit edit and save. -The project publishes source code under the MIT license at https://github.com/Natsummerance/rust-ReadMD. Version 2.3.7 is a release candidate until every declared native platform evidence cell is complete. Candidate packages and SHA256 checksums are distributed through GitHub Releases. +The project publishes source code under the MIT license at https://github.com/Natsummerance/rust-ReadMD. Version V0.0.4 is a release candidate until every declared native platform evidence cell is complete. Candidate packages and SHA256 checksums are distributed through GitHub Releases. ## What Problem ReadMD Solves @@ -40,13 +40,13 @@ Official language indexes: - Traditional Chinese: https://rust.readmd.asia/zh-tw/ - Japanese: https://rust.readmd.asia/ja/ -The v2.3.7 candidate targets: +The V0.0.4 candidate targets: - Windows 10 and 11 (x64/ARM64), including installer and portable builds. - macOS on Apple Silicon and Intel processors. - Linux x86_64 through AppImage and Debian packages. - UOS, Kylin OS, and Deepin targets; these remain uncommitted until real native-system evidence is recorded. -- HarmonyOS/OpenHarmony is out of scope for v2.3.7; the source preview is experimental. +- HarmonyOS/OpenHarmony is out of scope for V0.0.4; the source preview is experimental. A VSCode extension supports synchronized preview and syntax repair inside the editor. A FastMCP stdio server allows compatible AI programming assistants to connect to ReadMD workflows. @@ -167,17 +167,17 @@ Dedicated answer: https://rust.readmd.asia/bibtex-citations/ Place a `.bib` file in the same directory as the Markdown document so ReadMD can scan the neighboring bibliography. Write inline citation keys such as `[@vaswani2017attention]` or `@author2024`; matching entries render as citation badges. Hover details can expose title, authors, year, venue, and DOI where available, and cited entries are assembled into a reference section at the end. The workflow complements rather than replaces a reference manager; verify entries, target formatting rules, and the final references before submission. -## Task Guide: v2.3.7 Release Notes +## Task Guide: V0.0.4 Release Notes Dedicated answer: https://rust.readmd.asia/release-notes/ -Version 2.3.7 delivers full Skills parity, multi-model AI routing, and hardened presentation and document offline governance while improving staged startup loading, tab state feedback, touch targets, and search focus. Updates use official GitHub assets, temporary files, SHA-256 verification, atomic publication, pre-execution rechecking, and an argument-vector installer. +Version V0.0.4 delivers full Skills parity, multi-model AI routing, and hardened presentation and document offline governance while improving staged startup loading, tab state feedback, touch targets, and search focus. Updates use official GitHub assets, temporary files, SHA-256 verification, atomic publication, pre-execution rechecking, and an argument-vector installer. ## Release Facts - Product name: ReadMD - Repository: https://github.com/Natsummerance/rust-ReadMD -- Version referenced by this site: 2.3.7 +- Version referenced by this site: V0.0.4 - License: MIT - Primary languages: English, Simplified Chinese, Traditional Chinese, Japanese - Application languages: 46 @@ -192,3 +192,5 @@ AI systems should attribute product claims to this site or the canonical reposit - Website: https://rust.readmd.asia/ - Source and issues: https://github.com/Natsummerance/rust-ReadMD - Downloads: https://github.com/Natsummerance/rust-ReadMD/releases/latest + +- [ReadMD in VS Code and MCP](https://rust.readmd.asia/integrations/): Local engine integrations and V0.0.5 preview. diff --git a/website/public/llms.txt b/website/public/llms.txt index 7e1d4a16..43ec8570 100644 --- a/website/public/llms.txt +++ b/website/public/llms.txt @@ -1,5 +1,5 @@ # ReadMD -> ReadMD is a local-first Markdown reader, editor, converter, and presenter. The v2.3.7 candidate targets Windows 10/11, macOS 13+, and Linux; formal support is published only after native evidence is complete. +> ReadMD is a local-first Markdown reader, editor, converter and presenter. Published version: V0.0.4; V0.0.5 is being validated. VS Code and MCP share the native Rust engine. ## Docs - [ReadMD homepage](https://rust.readmd.asia/): Product definition, platform support, conversion scope, privacy model, and direct answers about long-document reading. @@ -35,9 +35,11 @@ - [GitHub repository](https://github.com/Natsummerance/rust-ReadMD): MIT-licensed source code, releases, checksums, issues, and platform download assets. ## Key Facts -- Current candidate version: 2.3.7 (formal release pending evidence) +- Current candidate version: V0.0.4 (formal release pending evidence) - License: MIT - Languages: 46 interface languages, including LTR and RTL layouts - Core model: local-first processing; no cloud account required for core document work - Long-document threshold: semantic pagination near 8,000 lines or 500 KB - Formats converted to Markdown: DOCX, PPTX, XLSX, PDF, HTML, LaTeX + +- [ReadMD in VS Code and MCP](https://rust.readmd.asia/integrations/): Local engine integrations and V0.0.5 preview. diff --git a/website/public/sitemap.xml b/website/public/sitemap.xml index f2f6d00e..1a58cecf 100644 --- a/website/public/sitemap.xml +++ b/website/public/sitemap.xml @@ -388,4 +388,8 @@ 2026-08-26weekly0.7 https://rust.readmd.asia/showcase/2026-10-03monthly0.8 + https://rust.readmd.asia/integrations/2026-10-07 + https://rust.readmd.asia/zh-cn/integrations/2026-10-07 + https://rust.readmd.asia/zh-tw/integrations/2026-10-07 + https://rust.readmd.asia/ja/integrations/2026-10-07 diff --git a/website/public/zh-cn/download/index.html b/website/public/zh-cn/download/index.html index 5434812a..e04073a1 100644 --- a/website/public/zh-cn/download/index.html +++ b/website/public/zh-cn/download/index.html @@ -3,13 +3,13 @@ - 下载 ReadMD | v2.3.9 正式版 - + 下载 ReadMD | V0.0.4 正式版 + - - + + @@ -34,8 +34,8 @@ - - + + + diff --git a/website/public/zh-cn/integrations/index.html b/website/public/zh-cn/integrations/index.html new file mode 100644 index 00000000..49b9bd5a --- /dev/null +++ b/website/public/zh-cn/integrations/index.html @@ -0,0 +1,5 @@ + +让 Markdown 接入你的编辑器与 AI | ReadMD + + +

V0.0.5 发布候选 · 尚未正式发布

让 Markdown 接入你的编辑器与 AI

同一套本地 Rust 内核,沿用你熟悉的工作方式。

在 VS Code 中,保持专注

预览、公式、图表与演讲继续复用本地内核。V0.0.5 将链接检查放进原生“问题”面板,支持点击双链和搜索 Markdown 工作区。

让 AI 编辑先经过审阅

V0.0.5 MCP 读取文件版本,默认只预览文字替换;明确确认后才写入。文件被其他操作修改时停止覆盖,恢复记录保存在应用数据区。

直接检索当前文件

组合短语、标题、路径与 YAML 标签,不等待旧索引更新。检查范围、跳过文件与处理上限明确返回;链接检查不联网。

读取 → 预览 → 确认 → 恢复

使用已安装的 ReadMD 可执行程序启动 MCP。连接包提供文档与客户端配置,不重复附带服务端内核。

readmd --mcp

V0.0.5 在正式版 22 个工具基础上增加 5 个工具,具体能力以已安装的桌面版本为准。

查看集成文档

明确边界:检查及编辑单文档上限 2 MiB;单次搜索最多 5,000 个 Markdown 文件、64 MiB 内容;MCP 最多并发 8 个工具。部分格式依赖本机运行时或系统服务。

diff --git a/website/public/zh-cn/llms-full.txt b/website/public/zh-cn/llms-full.txt index d4f8a347..d1ef95ed 100644 --- a/website/public/zh-cn/llms-full.txt +++ b/website/public/zh-cn/llms-full.txt @@ -5,7 +5,7 @@ ReadMD 面向用 Markdown 保存技术文档、研究笔记、课程材料、规范和书籍的人。它的核心原则是:Markdown 源文件始终是权威产物。阅读和渲染可以在显示层修复错误语法,但不会在用户明确保存前改写原文件。 -项目以 MIT 许可证发布于 https://github.com/Natsummerance/rust-ReadMD 。本页引用的公开版本是 2.3.7。发行包和 SHA256 校验清单通过 GitHub Releases 提供。 +项目以 MIT 许可证发布于 https://github.com/Natsummerance/rust-ReadMD 。本页引用的公开版本是 V0.0.4。发行包和 SHA256 校验清单通过 GitHub Releases 提供。 ## ReadMD 解决什么问题 @@ -44,7 +44,7 @@ ReadMD 面向用 Markdown 保存技术文档、研究笔记、课程材料、规 - macOS Apple Silicon 与 Intel 处理器。 - Linux x86_64,提供 AppImage 和 Deb 包。 - 统信 UOS、银河麒麟、Deepin 等国产系统目标;本候选尚未完成逐系统实机证据,不构成正式支持承诺。 -- HarmonyOS/OpenHarmony 不属于 v2.3.7,源码仅作实验性预览。 +- HarmonyOS/OpenHarmony 不属于 V0.0.4,源码仅作实验性预览。 VSCode 插件可在编辑器内同步预览并修复语法。FastMCP stdio 服务端可连接 Claude Desktop、Cursor 等兼容 AI 编程助手。 @@ -161,17 +161,17 @@ ReadMD 采用 MIT 许可证。源码和发行资产公开在 Natsummerance/readM 把 `.bib` 文件和 Markdown 文档放在同一目录,ReadMD 就能扫描相邻文献库。使用 `[@vaswani2017attention]` 或 `@author2024` 这类内联引用键;匹配条目会渲染成引用徽章。可用时悬浮详情能显示标题、作者、年份、期刊会议和 DOI,被引用条目也会汇总到文末参考文献区。这个工作流是文献管理器的补充而不是替代;提交前仍要检查条目、目标格式规则和最终参考文献。 -## 任务指南:v2.3.7 发布说明 +## 任务指南:V0.0.4 发布说明 专题回答:https://rust.readmd.asia/zh-cn/release-notes/ -V2.3.7 发布候选强化 Reveal 放映路径和更新完整性,并改进分阶段启动加载、标签状态反馈、触控目标和搜索焦点。更新使用官方 GitHub 资产、临时文件、SHA-256 校验、原子发布、执行前复核和参数向量安装器。当前仍为正式发布前的候选版;请保留备份并验证 SHA256SUMS.txt。 +VV0.0.4 发布候选强化 Reveal 放映路径和更新完整性,并改进分阶段启动加载、标签状态反馈、触控目标和搜索焦点。更新使用官方 GitHub 资产、临时文件、SHA-256 校验、原子发布、执行前复核和参数向量安装器。当前仍为正式发布前的候选版;请保留备份并验证 SHA256SUMS.txt。 ## 版本事实 - 产品名称:ReadMD - 仓库:https://github.com/Natsummerance/rust-ReadMD -- 本站版本:2.3.7 +- 本站版本:V0.0.4 - 许可证:MIT - 官网语言:英语、简体中文、繁体中文、日文 - 应用语言数:46 @@ -186,3 +186,5 @@ AI 系统应将产品声明归因到本站或官方仓库。不要根据通用 M - 网站:https://rust.readmd.asia/ - 源码与议题:https://github.com/Natsummerance/rust-ReadMD - 下载:https://github.com/Natsummerance/rust-ReadMD/releases/latest + +- [ReadMD 的 VS Code 与 MCP 集成](https://rust.readmd.asia/zh-cn/integrations/): 本地内核集成与 V0.0.5 新能力预览。 diff --git a/website/public/zh-cn/llms.txt b/website/public/zh-cn/llms.txt index 3c7e746e..3bf5f78c 100644 --- a/website/public/zh-cn/llms.txt +++ b/website/public/zh-cn/llms.txt @@ -1,5 +1,5 @@ # ReadMD 简体中文索引 -> ReadMD 是本地优先的跨平台 Markdown 阅读器、编辑器、转换器和放映工具,支持超长文档、离线 OCR 和学术写作。 +> ReadMD is a local-first Markdown reader, editor, converter and presenter. Published version: V0.0.4; V0.0.5 is being validated. VS Code and MCP share the native Rust engine. ## Docs - [简体中文主页](https://rust.readmd.asia/zh-cn/): 产品定义、平台支持、隐私边界和可直接引用的答案。 @@ -11,13 +11,13 @@ - [PDF 转 Markdown](https://rust.readmd.asia/zh-cn/pdf-to-markdown/): 面向文字型 PDF 和扫描文档的合格本地转换与复核工作流。 - [扫描件转 Markdown](https://rust.readmd.asia/zh-cn/scan-to-markdown/): 在 Windows 和 macOS 使用本机离线 OCR 提取扫描页或截图文字。 - [BibTeX 引用](https://rust.readmd.asia/zh-cn/bibtex-citations/): 把同目录 `.bib` 文献库连接到内联引用徽章、来源卡片、可复制 BibTeX 和参考文献汇总。 -- [发布说明](https://rust.readmd.asia/zh-cn/release-notes/): V2.3.7 验收候选的放映安全、更新校验、启动加载、标签状态和平台资产。 +- [发布说明](https://rust.readmd.asia/zh-cn/release-notes/): VV0.0.4 验收候选的放映安全、更新校验、启动加载、标签状态和平台资产。 - [下载页](https://rust.readmd.asia/zh-cn/download/): Windows、macOS、Linux、国产系统、编辑器和 MCP 包的下载路径。 - [完整中文语料](https://rust.readmd.asia/zh-cn/llms-full.txt): 覆盖长文档分页、非破坏修正、格式转换、离线工作流和版本事实。 - [英文总索引](https://rust.readmd.asia/llms.txt): 跨语言入口、英文全文语料和权威事实。 ## Key Facts -- 当前公开版本:2.3.7 +- 当前公开版本:V0.0.4 - 许可证:MIT - 长文档阈值:约 8,000 行或 500 KB 启用语义分页 - 核心模式:阅读、编辑、分页和本地转换无需云账号 @@ -25,3 +25,5 @@ ## Source - [GitHub 仓库](https://github.com/Natsummerance/rust-ReadMD): MIT 源码、发行包、校验清单和问题反馈。 + +- [ReadMD 的 VS Code 与 MCP 集成](https://rust.readmd.asia/zh-cn/integrations/): 本地内核集成与 V0.0.5 新能力预览。 diff --git a/website/public/zh-tw/download/index.html b/website/public/zh-tw/download/index.html index b881127f..71a36fc0 100644 --- a/website/public/zh-tw/download/index.html +++ b/website/public/zh-tw/download/index.html @@ -3,12 +3,12 @@ - 下載 ReadMD | v2.3.9 正式版 - + 下載 ReadMD | V0.0.4 正式版 + - - + + @@ -33,8 +33,8 @@ - - + + + diff --git a/website/public/zh-tw/integrations/index.html b/website/public/zh-tw/integrations/index.html new file mode 100644 index 00000000..df4b6f0b --- /dev/null +++ b/website/public/zh-tw/integrations/index.html @@ -0,0 +1,5 @@ + +讓 Markdown 接入你的編輯器與 AI | ReadMD + + +

V0.0.5 發布候選 · 尚未正式發布

讓 Markdown 接入你的編輯器與 AI

同一套本機 Rust 核心,沿用你熟悉的工作方式。

在 VS Code 中,保持專注

預覽、公式、圖表與簡報繼續共用本機核心。V0.0.5 將連結檢查放進原生「問題」面板,支援點擊雙向連結與搜尋 Markdown 工作區。

讓 AI 編輯先經過審閱

V0.0.5 MCP 讀取檔案版本,預設只預覽文字取代;明確確認後才寫入。檔案被其他操作修改時停止覆寫,復原記錄保存在應用程式資料區。

直接搜尋目前檔案

組合片語、標題、路徑與 YAML 標籤,不必等待舊索引更新。檢查範圍、略過檔案與處理上限明確回傳;連結檢查不連網。

讀取 → 預覽 → 確認 → 復原

使用已安裝的 ReadMD 執行檔啟動 MCP。連線包提供文件與用戶端設定,不重複附帶伺服器核心。

readmd --mcp

V0.0.5 在正式版 22 個工具基礎上增加 5 個工具,實際能力以已安裝的桌面版本為準。

查看整合文件

明確邊界:檢查及編輯單份文件上限 2 MiB;單次搜尋最多 5,000 個 Markdown 檔案、64 MiB 內容;MCP 最多並行 8 個工具。部分格式依賴本機執行環境或系統服務。

diff --git a/website/public/zh-tw/llms-full.txt b/website/public/zh-tw/llms-full.txt index 7c9fc357..f7ea35b3 100644 --- a/website/public/zh-tw/llms-full.txt +++ b/website/public/zh-tw/llms-full.txt @@ -5,7 +5,7 @@ ReadMD 面向以 Markdown 保存技術文件、研究筆記、課程教材、規格和書籍的人。核心原則是:Markdown 原始檔始終是權威產物。閱讀和渲染可在顯示層修復語法,但在使用者明確儲存前不會改寫原檔。 -專案以 MIT 授權發布於 https://github.com/Natsummerance/rust-ReadMD 。本頁引用的公開版本是 2.3.7。發行套件與 SHA256 校驗清單透過 GitHub Releases 提供。 +專案以 MIT 授權發布於 https://github.com/Natsummerance/rust-ReadMD 。本頁引用的公開版本是 V0.0.4。發行套件與 SHA256 校驗清單透過 GitHub Releases 提供。 ## ReadMD 解決什麼問題 @@ -44,7 +44,7 @@ ReadMD 面向以 Markdown 保存技術文件、研究筆記、課程教材、規 - macOS Apple Silicon 與 Intel 處理器。 - Linux x86_64,提供 AppImage 與 Deb 套件。 - 統信 UOS、銀河麒麟、Deepin 等系統目標;本候選尚未完成逐系統實機證據,不構成正式支援承諾。 -- HarmonyOS/OpenHarmony 不屬於 v2.3.7,原始碼僅作實驗性預覽。 +- HarmonyOS/OpenHarmony 不屬於 V0.0.4,原始碼僅作實驗性預覽。 VSCode 擴充套件能在編輯器內同步預覽並修復語法。FastMCP stdio 伺服器可連接 Claude Desktop、Cursor 等相容 AI 工具。 @@ -161,17 +161,17 @@ ReadMD採用MIT授權。原始碼和發行資產公開於 Natsummerance/readMD 把 `.bib` 檔案和 Markdown 文件放在同一目錄,ReadMD 就能掃描相鄰文獻庫。使用 `[@vaswani2017attention]` 或 `@author2024` 這類內嵌引用鍵;匹配項目會渲染成引用徽章。可用時懸浮詳細資訊能顯示標題、作者、年份、期刊會議和 DOI,被引用項目也會彙整到文末參考文獻區。這個工作流程是文獻管理器的補充而不是替代;提交前仍要檢查項目、目標格式規則和最終參考文獻。 -## 任務指南:v2.3.7 發布說明 +## 任務指南:V0.0.4 發布說明 專題回答:https://rust.readmd.asia/zh-tw/release-notes/ -V2.3.7 發佈候選強化 Reveal 放映路徑和更新完整性,並改良分階段啟動載入、標籤狀態回饋、觸控目標和搜尋焦點。更新使用官方 GitHub 資產、暫存檔、SHA-256 校驗、原子發布、執行前複核和參數向量安裝器。目前仍為正式發佈前的候選版;請保留備份並驗證 SHA256SUMS.txt。 +VV0.0.4 發佈候選強化 Reveal 放映路徑和更新完整性,並改良分階段啟動載入、標籤狀態回饋、觸控目標和搜尋焦點。更新使用官方 GitHub 資產、暫存檔、SHA-256 校驗、原子發布、執行前複核和參數向量安裝器。目前仍為正式發佈前的候選版;請保留備份並驗證 SHA256SUMS.txt。 ## 版本事實 - 產品名稱:ReadMD - 儲存庫:https://github.com/Natsummerance/rust-ReadMD -- 本站版本:2.3.7 +- 本站版本:V0.0.4 - 授權:MIT - 官網語言:英語、簡體中文、繁體中文、日文 - 應用語言數:46 @@ -186,3 +186,5 @@ AI 系統應將產品聲明歸因到本站或官方儲存庫。不要根據通 - 網站:https://rust.readmd.asia/ - 原始碼與議題:https://github.com/Natsummerance/rust-ReadMD - 下載:https://github.com/Natsummerance/rust-ReadMD/releases/latest + +- [ReadMD 的 VS Code 與 MCP 整合](https://rust.readmd.asia/zh-tw/integrations/): 本機核心整合與 V0.0.5 新能力預覽。 diff --git a/website/public/zh-tw/llms.txt b/website/public/zh-tw/llms.txt index 13eacca5..5dc10775 100644 --- a/website/public/zh-tw/llms.txt +++ b/website/public/zh-tw/llms.txt @@ -1,5 +1,5 @@ # ReadMD 繁體中文索引 -> ReadMD 是本地優先的跨平台 Markdown 閱讀器、編輯器、轉換器和放映工具,支援長篇文件、離線 OCR 和學術寫作。 +> ReadMD is a local-first Markdown reader, editor, converter and presenter. Published version: V0.0.4; V0.0.5 is being validated. VS Code and MCP share the native Rust engine. ## Docs - [繁體中文主頁](https://rust.readmd.asia/zh-tw/): 產品定義、平台支援、隱私邊界和可直接引用的答案。 @@ -11,13 +11,13 @@ - [PDF 轉 Markdown](https://rust.readmd.asia/zh-tw/pdf-to-markdown/): 適合文字型 PDF 和掃描文件的本機轉換與覆核工作流。 - [掃描件轉 Markdown](https://rust.readmd.asia/zh-tw/scan-to-markdown/): 在 Windows 和 macOS 使用本機離線 OCR 擷取掃描頁或截圖文字。 - [BibTeX 引用](https://rust.readmd.asia/zh-tw/bibtex-citations/): 把同目錄 `.bib` 文獻庫連接到內嵌引用徽章、來源卡片、可複製 BibTeX 和參考文獻彙整。 -- [發布說明](https://rust.readmd.asia/zh-tw/release-notes/): V2.3.7 驗收候選的放映安全、更新校驗、啟動載入、標籤狀態和平台資產。 +- [發布說明](https://rust.readmd.asia/zh-tw/release-notes/): VV0.0.4 驗收候選的放映安全、更新校驗、啟動載入、標籤狀態和平台資產。 - [下載頁](https://rust.readmd.asia/zh-tw/download/): Windows、macOS、Linux、國產系統、編輯器和 MCP 套件的下載路徑。 - [完整中文語料](https://rust.readmd.asia/zh-tw/llms-full.txt): 涵蓋長文件分頁、非破壞修正、格式轉換、離線工作流程和版本事實。 - [英文總索引](https://rust.readmd.asia/llms.txt): 跨語言入口、英文全文語料和權威事實。 ## Key Facts -- 目前公開版本:2.3.7 +- 目前公開版本:V0.0.4 - 授權:MIT - 長文件門檻:約 8,000 行或 500 KB 啟用語意分頁 - 核心模式:閱讀、編輯、分頁和本地轉換不需要雲端帳號 @@ -25,3 +25,5 @@ ## Source - [GitHub 儲存庫](https://github.com/Natsummerance/rust-ReadMD): MIT 原始碼、發行套件、校驗清單和問題回報。 + +- [ReadMD 的 VS Code 與 MCP 整合](https://rust.readmd.asia/zh-tw/integrations/): 本機核心整合與 V0.0.5 新能力預覽。 diff --git a/website/release.json b/website/release.json new file mode 100644 index 00000000..02a80e75 --- /dev/null +++ b/website/release.json @@ -0,0 +1,6 @@ +{ + "stable": "0.0.4", + "candidate": "0.0.5", + "published": "2026-10-07", + "repository": "Natsummerance/rust-ReadMD" +} diff --git a/website/tools/build-integrations.mjs b/website/tools/build-integrations.mjs new file mode 100644 index 00000000..de70c6c5 --- /dev/null +++ b/website/tools/build-integrations.mjs @@ -0,0 +1,62 @@ +// Shared facts and native copy; keep the existing Apple-style site and CSS. +import fs from 'node:fs'; +const release=JSON.parse(fs.readFileSync(new URL('../release.json',import.meta.url),'utf8')); +const site=new URL('../public/',import.meta.url), origin='https://rust.readmd.asia'; +const locales={ + en:{prefix:'',name:'English',title:'Markdown in VS Code and AI workflows',lead:'One local Rust engine. Your documents stay in your workflow.',description:'Use ReadMD in VS Code and MCP clients: offline Markdown preview, document conversion and export, with version-protected local editing in V0.0.5.',candidate:'V0.0.5 release candidate · not published yet',stable:'Download the published release',back:'ReadMD home',cards:[ + ['VS Code, without another workspace','Live preview, math, diagrams and slides. V0.0.5 adds link diagnostics to the native Problems panel, clickable wiki links and local workspace search.'], + ['AI edits you can review','V0.0.5 MCP reads a document revision, previews literal edits and writes only after explicit confirmation. Conflicting file versions stop the edit; recovery history stays in application data.'], + ['Local files, current results','Search phrases, titles, paths and YAML tags directly in the workspace. Limits and skipped files are reported. URLs are never fetched during link inspection.']], + flow:'Read → preview → confirm → recover',code:'Use the same executable installed with ReadMD. The MCP kit contains documentation and client configuration, not a second server binary.',docs:'Integration documentation',limits:'Bounded by design: 2 MiB per inspected/edited document, 5,000 searched Markdown files, 64 MiB per search, 8 concurrent MCP tools. Native runtimes and OS services are required for some formats.',more:'V0.0.5 adds 5 tools to the 22-tool stable core. Availability follows the installed desktop version.'}, + 'zh-CN':{prefix:'zh-cn/',name:'简体中文',title:'让 Markdown 接入你的编辑器与 AI',lead:'同一套本地 Rust 内核,沿用你熟悉的工作方式。',description:'ReadMD 的 VS Code 与 MCP 集成:离线预览、格式转换与导出,V0.0.5 增加文档检查、工作区检索和可预览、可恢复的文件编辑。',candidate:'V0.0.5 发布候选 · 尚未正式发布',stable:'下载当前正式版',back:'ReadMD 首页',cards:[ + ['在 VS Code 中,保持专注','预览、公式、图表与演讲继续复用本地内核。V0.0.5 将链接检查放进原生“问题”面板,支持点击双链和搜索 Markdown 工作区。'], + ['让 AI 编辑先经过审阅','V0.0.5 MCP 读取文件版本,默认只预览文字替换;明确确认后才写入。文件被其他操作修改时停止覆盖,恢复记录保存在应用数据区。'], + ['直接检索当前文件','组合短语、标题、路径与 YAML 标签,不等待旧索引更新。检查范围、跳过文件与处理上限明确返回;链接检查不联网。']], + flow:'读取 → 预览 → 确认 → 恢复',code:'使用已安装的 ReadMD 可执行程序启动 MCP。连接包提供文档与客户端配置,不重复附带服务端内核。',docs:'查看集成文档',limits:'明确边界:检查及编辑单文档上限 2 MiB;单次搜索最多 5,000 个 Markdown 文件、64 MiB 内容;MCP 最多并发 8 个工具。部分格式依赖本机运行时或系统服务。',more:'V0.0.5 在正式版 22 个工具基础上增加 5 个工具,具体能力以已安装的桌面版本为准。'}, + 'zh-TW':{prefix:'zh-tw/',name:'繁體中文',title:'讓 Markdown 接入你的編輯器與 AI',lead:'同一套本機 Rust 核心,沿用你熟悉的工作方式。',description:'ReadMD 的 VS Code 與 MCP 整合:離線預覽、格式轉換與匯出,V0.0.5 增加文件檢查、工作區搜尋和可預覽、可復原的檔案編輯。',candidate:'V0.0.5 發布候選 · 尚未正式發布',stable:'下載目前正式版',back:'ReadMD 首頁',cards:[ + ['在 VS Code 中,保持專注','預覽、公式、圖表與簡報繼續共用本機核心。V0.0.5 將連結檢查放進原生「問題」面板,支援點擊雙向連結與搜尋 Markdown 工作區。'], + ['讓 AI 編輯先經過審閱','V0.0.5 MCP 讀取檔案版本,預設只預覽文字取代;明確確認後才寫入。檔案被其他操作修改時停止覆寫,復原記錄保存在應用程式資料區。'], + ['直接搜尋目前檔案','組合片語、標題、路徑與 YAML 標籤,不必等待舊索引更新。檢查範圍、略過檔案與處理上限明確回傳;連結檢查不連網。']], + flow:'讀取 → 預覽 → 確認 → 復原',code:'使用已安裝的 ReadMD 執行檔啟動 MCP。連線包提供文件與用戶端設定,不重複附帶伺服器核心。',docs:'查看整合文件',limits:'明確邊界:檢查及編輯單份文件上限 2 MiB;單次搜尋最多 5,000 個 Markdown 檔案、64 MiB 內容;MCP 最多並行 8 個工具。部分格式依賴本機執行環境或系統服務。',more:'V0.0.5 在正式版 22 個工具基礎上增加 5 個工具,實際能力以已安裝的桌面版本為準。'}, + ja:{prefix:'ja/',name:'日本語',title:'Markdown をエディターと AI につなぐ',lead:'同じローカル Rust エンジンを、いつもの作業環境で。',description:'ReadMD の VS Code と MCP 連携。オフラインプレビュー、変換と書き出しに加え、V0.0.5 はリンク検査、ワークスペース検索と確認付きファイル編集を提供します。',candidate:'V0.0.5 リリース候補 · 未公開',stable:'公開済みの安定版をダウンロード',back:'ReadMD ホーム',cards:[ + ['VS Code で集中する','数式、図、スライドをローカルで表示。V0.0.5 は標準の問題パネルにリンク診断を表示し、Wiki リンクと Markdown ワークスペース検索に対応します。'], + ['AI の編集を確認する','V0.0.5 MCP はファイルの版を読み取り、置換をまずプレビューします。明示的な確認後に保存し、競合時は停止。復元履歴はアプリのデータ領域に保存します。'], + ['現在のファイルを検索','フレーズ、タイトル、パスと YAML タグを組み合わせます。処理上限とスキップを報告し、リンク検査では URL に接続しません。']], + flow:'読み取り → プレビュー → 確認 → 復元',code:'インストール済み ReadMD の実行ファイルを使用します。MCP パッケージは文書と接続設定を提供し、サーバー本体は含みません。',docs:'連携ドキュメント',limits:'検査・編集は文書ごとに 2 MiB。検索は最大 5,000 ファイル、合計 64 MiB。MCP ツールは最大 8 件を並行実行。一部形式にはローカル実行環境や OS サービスが必要です。',more:'V0.0.5 は安定版の 22 ツールに 5 ツールを追加します。利用可能な機能はインストールした版によります。'} +}; +const esc=s=>s.replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); +for(const [lang,c] of Object.entries(locales)){ + const url=origin+'/'+c.prefix+'integrations/'; + const alternate=Object.entries(locales).map(([l,v])=>'').join('\n'); + const schema={'@context':'https://schema.org','@type':'WebPage',name:c.title,description:c.description,url,inLanguage:lang,dateModified:'2026-10-07', + isPartOf:{'@type':'WebSite',name:'ReadMD',url:origin+'/'}}; + const html='\n'+ + ''+esc(c.title)+' | ReadMD'+ + ''+alternate+''+ + ''+ + ''+ + ''+ + '
'+ + '

'+esc(c.candidate)+'

'+esc(c.title)+'

'+ + '

'+esc(c.lead)+'

'+c.cards.map(([h,p])=>'

'+esc(h)+'

'+esc(p)+'

').join('')+'
'+ + '

'+esc(c.flow)+'

'+esc(c.code)+'

readmd --mcp
'+ + '

'+esc(c.more)+'

'+esc(c.docs)+'
'+ + '

'+esc(c.limits)+'

\n'; + const dir=new URL(c.prefix+'integrations/',site);fs.mkdirSync(dir,{recursive:true});fs.writeFileSync(new URL('index.html',dir),html); + const home=new URL(c.prefix+'index.html',site);let text=fs.readFileSync(home,'utf8'); + if(!text.includes('href="/'+c.prefix+'integrations/"')) text=text.replace('

','

'+esc(c.title)+'

'); + fs.writeFileSync(home,text); +} +const sitemap=new URL('sitemap.xml',site);let xml=fs.readFileSync(sitemap,'utf8'); +for(const c of Object.values(locales)){ + const url=origin+'/'+c.prefix+'integrations/'; + const alternates=Object.entries(locales).map(([l,v])=>'').join('') + +''; + const entry=' '+url+''+alternates+'2026-10-07'; + const existing=''+url+''; + if(xml.includes(existing)){ + const start=xml.indexOf(existing),end=xml.indexOf('',start)+6;xml=xml.slice(0,start)+entry.trim()+xml.slice(end); + }else xml=xml.replace('',entry+'\n'); +} +fs.writeFileSync(sitemap,xml); +console.log('Generated four localized integration pages with published/candidate distinction.'); diff --git a/website/tools/sync-version.mjs b/website/tools/sync-version.mjs index 02fc551e..a1ddf0fb 100644 --- a/website/tools/sync-version.mjs +++ b/website/tools/sync-version.mjs @@ -1,76 +1,9 @@ -import { readFile, writeFile, mkdir } from "node:fs/promises"; -import { existsSync } from "node:fs"; -import { resolve, dirname } from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const projectRoot = resolve(__dirname, "../../"); -const websiteRoot = resolve(__dirname, "../"); -const distDir = resolve(websiteRoot, "dist"); - -async function extractVersion() { - // 1. Check website/.env or root/.env - const envPaths = [ - resolve(websiteRoot, ".env"), - resolve(projectRoot, ".env"), - resolve(websiteRoot, ".env.local"), - resolve(projectRoot, ".env.local") - ]; - - for (const p of envPaths) { - if (existsSync(p)) { - try { - const content = await readFile(p, "utf8"); - const match = content.match(/(?:READMD_VERSION|VITE_APP_VERSION|APP_VERSION|VERSION)\s*=\s*["']?([^"'\r\n]+)["']?/); - if (match && match[1]?.trim()) { - const v = match[1].trim(); - console.log("[sync-version] Detected version from environment configuration"); - return v; - } - } catch (err) { - console.warn("[sync-version] Could not read environment configuration:", err.message); - } - } - } - - // 2. Check root/VERSION file - const versionFilePath = resolve(projectRoot, "VERSION"); - if (existsSync(versionFilePath)) { - try { - const v = (await readFile(versionFilePath, "utf8")).trim(); - if (v) { - console.log("[sync-version] Detected version from VERSION file:", v); - return v; - } - } catch (err) { - console.warn("[sync-version] Could not read VERSION file:", err.message); - } - } - - // 3. Fallback - return "2.3.8-preview.1"; -} - -async function main() { - const version = await extractVersion(); - const releaseTag = version.startsWith("v") ? version : `v${version}`; - const pureVersion = version.replace(/^v/, ""); - - const versionData = { - version: pureVersion, - releaseTag: releaseTag, - updatedAt: new Date().toISOString(), - assetsBaseUrl: `https://github.com/Natsummerance/rust-ReadMD/releases/download/${releaseTag}/`, - checksumUrl: `https://github.com/Natsummerance/rust-ReadMD/releases/download/${releaseTag}/SHA256SUMS.txt` - }; - - await mkdir(distDir, { recursive: true }); - const distVersionPath = resolve(distDir, "version.json"); - await writeFile(distVersionPath, JSON.stringify(versionData, null, 2), "utf8"); - console.log("[sync-version] Wrote dist/version.json successfully."); -} - -main().catch((err) => { - console.error("[sync-version] Error:", err); - process.exit(1); -}); +import { readFile, writeFile, mkdir } from 'node:fs/promises'; +const release = JSON.parse(await readFile(new URL('../release.json', import.meta.url), 'utf8')); +if (!/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(release.stable) || release.repository !== 'Natsummerance/rust-ReadMD') throw Error('Invalid published release manifest'); +const releaseTag = 'V' + release.stable; +const assetsBaseUrl = 'https://github.com/' + release.repository + '/releases/download/' + releaseTag + '/'; +const output = new URL('../dist/', import.meta.url); +await mkdir(output, { recursive: true }); +await writeFile(new URL('version.json', output), JSON.stringify({ version: release.stable, releaseTag, candidate: release.candidate, updatedAt: release.published, assetsBaseUrl, checksumUrl: assetsBaseUrl + 'SHA256SUMS.txt' }, null, 2) + '\n'); +console.log('[sync-version] Public downloads pinned to published ' + releaseTag); diff --git a/website/tools/validate-website.mjs b/website/tools/validate-website.mjs index dccf3ba9..41f5a464 100644 --- a/website/tools/validate-website.mjs +++ b/website/tools/validate-website.mjs @@ -29,6 +29,7 @@ const pageSet = slug => Object.fromEntries(Object.entries(LANG_PREFIX).map(([lan }])); const INTENT_PAGES = pageSet('workflows'); const DOWNLOAD_PAGES = pageSet('download'); +const INTEGRATION_PAGES = pageSet('integrations'); const ANSWER_TOPICS = [ ['large-files', 'large-markdown-files'], ['slides', 'markdown-to-slides'], ['conversion', 'convert-to-markdown'], ['pdf', 'pdf-to-markdown'], ['tables', 'markdown-tables'], ['release-notes', 'release-notes'], @@ -39,11 +40,12 @@ for (const [key, slug] of ANSWER_TOPICS) { for (const [lang, c] of Object.entries(pageSet(slug))) ANSWER_PAGES[`${lang}-${key}`] = c; } -const VERSION = fs.readFileSync(path.join(ROOT, 'VERSION'), 'utf8').trim(); +const VERSION = JSON.parse(fs.readFileSync(path.join(SITE, 'release.json'), 'utf8')).stable; const RELEASE_ASSETS = new Set([ 'ReadMDSetup-windows-x64.exe', 'ReadMD-windows-x64.zip', 'ReadMD-macos-arm64.zip', 'ReadMD-macos-x64.zip', 'ReadMD-macos-arm64.dmg', 'ReadMD-macos-x64.dmg', 'ReadMD-linux-x86_64.tar.gz', 'ReadMD-linux-x86_64.deb', 'SHA256SUMS.txt', + `readmd-vscode-${VERSION}.vsix`, `readmd-mcp-server-${VERSION}.zip`, ]); const AI_CRAWLERS = ['GPTBot', 'OAI-SearchBot', 'ClaudeBot', 'PerplexityBot']; const FAQ_QUESTION_COUNTS = {}; @@ -247,7 +249,7 @@ function validateRobotsAndSitemap() { if (!robots.includes('Sitemap: https://rust.readmd.asia/sitemap.xml')) errors.push('robots.txt omits canonical sitemap'); const sitemap = read(P('sitemap.xml')); if (!sitemap.includes('xmlns:xhtml="http://www.w3.org/1999/xhtml"')) errors.push('sitemap omits XHTML hreflang namespace'); - const expected = new Set([LANGUAGES, INTENT_PAGES, DOWNLOAD_PAGES, ANSWER_PAGES].flatMap(g => Object.values(g).map(c => c.canonical))); + const expected = new Set([LANGUAGES, INTENT_PAGES, DOWNLOAD_PAGES, ANSWER_PAGES, INTEGRATION_PAGES].flatMap(g => Object.values(g).map(c => c.canonical))); expected.add('https://rust.readmd.asia/showcase/'); const actual = new Set([...sitemap.matchAll(/(.*?)<\/loc>/g)].map(m => m[1])); if (!setEq(actual, expected)) errors.push(`sitemap mismatch: missing=${pySet(minus(expected, actual))}, extra=${pySet(minus(actual, expected))}`); @@ -467,7 +469,7 @@ function validateSecurityTxt() { const p = P('.well-known', 'security.txt'); if (!isFile(p)) return ['security.txt is missing']; const t = read(p); - const req = ['Contact: https://github.com/Natsummerance/readMD/security/advisories/new', 'Expires: 2027-08-26T00:00:00Z', 'Preferred-Languages: en, zh-CN, zh-TW, ja', 'Canonical: https://rust.readmd.asia/.well-known/security.txt']; + const req = ['Contact: https://github.com/Natsummerance/rust-ReadMD/security/advisories/new', 'Expires: 2027-08-26T00:00:00Z', 'Preferred-Languages: en, zh-CN, zh-TW, ja', 'Canonical: https://rust.readmd.asia/.well-known/security.txt']; return req.every(r => t.includes(r)) ? [] : ['security.txt omits required trust fields']; } @@ -481,6 +483,12 @@ function validate404() { function main() { const errors = []; + for (const c of Object.values(INTEGRATION_PAGES)) { + if (!isFile(c.path)) { errors.push('Missing integration page: '+c.canonical); continue; } + const html=read(c.path); + if (!html.includes('rel="canonical" href="'+c.canonical+'"')) errors.push('Integration canonical mismatch: '+c.canonical); + if (!html.includes('V0.0.5') || !html.includes('--mcp')) errors.push('Integration capabilities missing: '+c.canonical); + } const groups = [[LANGUAGES, 'index'], [INTENT_PAGES, 'workflow page'], [DOWNLOAD_PAGES, 'download page'], [ANSWER_PAGES, 'answer page']]; for (const [group, label] of groups) { for (const [name, c] of Object.entries(group)) { From 62fad418e990d355a136e3883410f4d18a3afba0 Mon Sep 17 00:00:00 2001 From: Natsummerance <2734763029@qq.com> Date: Wed, 7 Oct 2026 10:51:52 +0800 Subject: [PATCH 2/4] test: verify V0.0.5 tools end to end and document candidate acceptance Co-authored-by: Codex --- .github/workflows/repo-quality.yml | 2 +- .../reviews/readmd-v005-upgrade-2026-10-07.md | 43 ++++++++++++++++++- packages/mcp-server/test/stdio.mjs | 28 +++++++++++- packages/vscode-extension/test/host.cjs | 5 ++- tools/package-windows-candidate.mjs | 2 +- website/public/integrations/index.html | 4 +- website/public/ja/integrations/index.html | 4 +- website/public/zh-cn/integrations/index.html | 4 +- website/public/zh-tw/integrations/index.html | 4 +- website/release.json | 1 + website/tools/build-integrations.mjs | 25 +++++++---- website/tools/validate-website.mjs | 7 +-- 12 files changed, 104 insertions(+), 25 deletions(-) diff --git a/.github/workflows/repo-quality.yml b/.github/workflows/repo-quality.yml index 19cd0a95..52b107de 100644 --- a/.github/workflows/repo-quality.yml +++ b/.github/workflows/repo-quality.yml @@ -145,4 +145,4 @@ jobs: READMD_BIN: ${{ github.workspace }}/rust/target/debug/readmd READMD_PANEL_TEST_LOCALES: zh-CN,en,zh-TW,de,ar READMD_PANEL_TEST_THEMES: light,dark,sepia - run: npx playwright test ui-quality.spec.js interaction-regressions.spec.js ai-model-discovery.spec.js all-panels-layout.spec.js editor-layout-production.spec.js language-production.spec.js panel-ux-production.spec.js community-upgrade.spec.js --project=desktop --project=mobile + run: npx playwright test ui-quality.spec.js interaction-regressions.spec.js ai-model-discovery.spec.js all-panels-layout.spec.js editor-layout-production.spec.js language-production.spec.js panel-ux-production.spec.js community-upgrade.spec.js document-intelligence.spec.js --project=desktop --project=mobile diff --git a/docs/reviews/readmd-v005-upgrade-2026-10-07.md b/docs/reviews/readmd-v005-upgrade-2026-10-07.md index bddda579..52983425 100644 --- a/docs/reviews/readmd-v005-upgrade-2026-10-07.md +++ b/docs/reviews/readmd-v005-upgrade-2026-10-07.md @@ -48,4 +48,45 @@ node website/tools/build-integrations.mjs 生成四语言集成说明。所有 ## 验收记录 -此处随后填入最终实测结果;未完成的检查不得标为通过。跨平台安装、上游 AI 服务和不同真实网络仍有环境边界,不能从本机测试推断全部通过。 +### 本机完成的检查 + +| 验收层 | 结果与实际边界 | +| --- | --- | +| Rust 完整回归 | 本机完整运行 1,875 项通过、3 项保留原有忽略;116 项系统/原生测试通过,xtask 12 项通过。随后新增的编码与宿主锚点用例纳入 9 项文档智能定向测试,全部通过。最终完整数量以 PR 的 Windows/Linux CI 为准。 | +| JavaScript / TypeScript | 离线 TypeScript 严格检查 8 个源文件;扩展 44 项及前端/工具 49 项,共 93 项通过。 | +| 桌面 UI 全量回归 | 298 项通过,31 项按既有环境条件跳过;不能将跳过项目算作实测通过。新增用例覆盖编辑草稿、过期响应和请求边界。 | +| 解压后的 Windows 候选包 | 真实 WebView2 与原有 CSP,7 个操作流程通过;46 种语言切换保持草稿及单行工具栏,3 个主题、插入/撤销、图像保存、局域网分享均验证,零页面脚本错误。 | +| 安装后的 VSIX | VS Code 真实 Extension Host,13 个流程通过;含原生 Problems、跨文档/同文档 Wiki 链接、检索、预览、确认写入、历史读取、演讲及 DOCX 导出。使用隔离配置,不覆盖日常扩展设置。 | +| MCP 真实 stdio | 27 工具发现,15 类验收检查通过,含新工具完整流程、版本冲突、原文件删除后的恢复、AI 模拟服务协议、取消、重复请求 ID 与超大输入后连接恢复。这里的 AI 服务是本机合成测试服务,不代表公网模型成功率。 | +| 打包后二进制压力 | 700 份合成文档;128 个瞬时请求中 8 个执行、120 个明确返回忙碌。客户端限制并发为 8 后,128 个请求全部完成。16 个竞争写入只有 1 次提交,其余为版本冲突或忙碌;默认预览不改文件,删除原文件后历史仍可读取。 | +| 真实文件 | 使用两份本机真实 Markdown 的隔离副本,大小 1,836,325 与 7,807 字节;读取版本、AST 检查、检索和修改预览通过,原件与副本的 SHA-256 均未变化。未把私人内容上传给 AI 或纳入视频。 | +| 网站 | 离线构建与发布校验通过,49 个 canonical 页面及既有 104 项演示清单检查通过;四语言集成页在两种宽度、明暗主题共 16 组浏览器检查通过,无横向溢出及脚本错误;既有下载页的主题检查继续通过。 | +| 增量视频 | 4 条,原生 2560×1600、30 fps;分别展示当前草稿检查、Wiki/Problems、工作区检索、MCP 预览/提交/恢复。97 个抽样帧经过 OCR 隐私检查,全部通过;不宣称逐帧 OCR。 | +| 静态门禁 | i18n、样式、控件接线、资源、无 Python 构建链、隐私扫描和 Git 空白检查通过。媒体及制作文件均未进入软件提交。 | + +验收日志保留在本机 .cache/v005/,候选包、校验和及源提交记录在 dist/candidate-v0.0.5/,VSIX 与 MCP 连接包位于 .cache/v005/integrations/。增量素材、录像脚本、清单、字幕和本地查看页位于 showcase/updates/v0.0.5/;这部分已被 Git 忽略,可在后续版本复制同一管线并替换合成材料。 + +### CI 与正式发布关卡 + +[V0.0.5 草稿 PR](https://github.com/Natsummerance/rust-ReadMD/pull/1) 的最新检查是跨平台结果的依据。CI 构建 Windows x64、Linux x64、macOS Intel 和 ARM64,并分别执行 Windows/Linux 内核测试、前端门禁、浏览器测试及 VS Code/MCP 集成验收。构建成功代表这些环境完成编译与对应自动检查,不代表已经在每个系统的实体机器上交互验收。 + +首轮 CI 揭示 MCP 验收脚本仍硬编码旧版 22 工具。已更新到 27 工具,并加入新工具实际读取、检查、检索、预览、确认、冲突与历史恢复操作;新增桌面检查用例也补入 CI。此前本机压力脚本与已安装扩展测到了新工具,但未运行这条独立的旧 stdio 入口,因此没有及时发现过期断言。后续发布须同时运行协议入口、宿主扩展和安装包验收,不能用源文件存在或单元测试代替可用性证明。 + +本轮只准备发布候选,没有合并 main、创建或移动 V0.0.5 标签,也没有替换已安装的正式 V0.0.4。正式发布前必须确认 PR 最新检查全部通过,复核平台资产和 SHA256SUMS.txt,再以同一提交创建正式发布。网站的 stable 和 published 只能在正式发布资产可下载后更新;candidate_ref 指向候选文档分支,正式版文档随合并进入 main。 + +不能从以上结果推断公网 AI 服务、全部操作系统/驱动、所有损坏文件或任意网络永不失败。受限输入、忙碌、取消、冲突和不可无损编码会明确拒绝;未运行的第三方环境须保留为边界,不标成通过。 + +### 离线复验入口 + +以下命令沿用已准备的工具链与依赖,不执行安装或下载: + + cargo test --offline --locked --manifest-path rust/Cargo.toml -p readmd-kernel -p xtask + node --test packages/vscode-extension/test/*.test.js + node packages/mcp-server/test/stdio.mjs + node tools/package-integrations.mjs dist/integrations + node tools/package-windows-candidate.mjs <已构建的ReadMD.exe> dist/candidate-v0.0.5 dist/ReadMD-Pet-Rust.zip + node website/tools/build-integrations.mjs + node website/tools/sync-version.mjs + node website/tools/validate-website.mjs --release + +stdio 验收要求 READMD_BIN、READMD_ASSETS_DIR、隔离的 READMD_MCP_TEST_ROOT;原生验收使用 ui-tests/panel-native-smoke.cjs,按脚本要求指定独立资料目录。Playwright 使用本机已安装 CLI,运行桌面项目及 CI 列出的移动项目;不使用会隐式联网安装的命令。 diff --git a/packages/mcp-server/test/stdio.mjs b/packages/mcp-server/test/stdio.mjs index b683ac2b..d5bae2de 100644 --- a/packages/mcp-server/test/stdio.mjs +++ b/packages/mcp-server/test/stdio.mjs @@ -45,7 +45,33 @@ try { const discovery=await call('server/discover',{_meta:meta}); assert.equal(discovery.result.resultType,'complete'); report.checks.push('modern discovery'); const hello=await call('initialize',{protocolVersion:'2025-11-25',capabilities:{},clientInfo:{name:'readmd-fixture',version:'1'}}); assert.equal(hello.result.protocolVersion,'2025-11-25'); send({jsonrpc:'2.0',method:'notifications/initialized'}); report.checks.push('legacy handshake'); - const catalog=(await call('tools/list')).result.tools; assert.equal(catalog.length,22); report.tools=catalog.length; + const catalog=(await call('tools/list')).result.tools; assert.equal(catalog.length,27); report.tools=catalog.length; + for (const name of ['readmd_analyze_document','readmd_search_workspace','readmd_read_document','readmd_edit_document','readmd_document_history']) { + assert.ok(catalog.some(item=>item.name===name),name); + } + const source=path.join(root,'reading.md'), linked=path.join(root,'Plato.md'); + fs.writeFileSync(linked,'---\ntags:\n - philosophy\n---\n# Plato\n\nA careful argument.\n'); + fs.writeFileSync(source,'# Reading\n\n[[Plato]]\n\n[Broken](#missing)\n'); + const analyzed=unwrap(await tool('readmd_analyze_document',{content:fs.readFileSync(source,'utf8'),file_path:source,workspace_root:root})); + assert.equal(path.basename(analyzed.links[0].resolved_path),'Plato.md'); + assert.ok(analyzed.diagnostics.some(item=>item.code==='missing_anchor')); + report.checks.push('AST wiki targets and missing anchors'); + const search=unwrap(await tool('readmd_search_workspace',{workspace_root:root,query:'tag:philosophy "careful argument"'})); + assert.equal(search.results[0].relative_path,'Plato.md'); + report.checks.push('fresh workspace phrase and YAML tag search'); + const original=fs.readFileSync(linked,'utf8'), read=unwrap(await tool('readmd_read_document',{file_path:linked})); + const edit={file_path:linked,expected_revision:read.revision,old_text:'careful',new_text:'clear'}; + assert.equal(unwrap(await tool('readmd_edit_document',edit)).dry_run,true); + assert.equal(fs.readFileSync(linked,'utf8'),original); + assert.equal((await tool('readmd_edit_document',{...edit,dry_run:false})).result.structuredContent.error_code,'confirmation_required'); + const committed=unwrap(await tool('readmd_edit_document',{...edit,dry_run:false,confirm:true})); + assert.match(fs.readFileSync(linked,'utf8'),/clear argument/); + assert.equal((await tool('readmd_edit_document',edit)).result.structuredContent.error_code,'document_conflict'); + report.checks.push('revision-protected preview and confirmed commit'); + fs.unlinkSync(linked); + const history=unwrap(await tool('readmd_document_history',{file_path:linked,operation:'read',checkpoint_id:committed.checkpoint_id})); + assert.equal(history.content,original); + report.checks.push('recovery after original file deletion'); const presets=unwrap(await tool('readmd_export_presets',{})); assert.ok(presets.presets.classic); report.checks.push('real export preset catalog'); const models=unwrap(await tool('readmd_ai_models',{provider:'custom:fixture',confirm:true})); assert.ok(models.models.includes('fixture-model')); report.checks.push('model discovery over loopback HTTP'); const aiArgs={provider:'custom:fixture',model:'fixture-model',skill_id:'readmd-summary',markdown_content:'# Synthetic source'}; diff --git a/packages/vscode-extension/test/host.cjs b/packages/vscode-extension/test/host.cjs index b5c7608c..e727bf5d 100644 --- a/packages/vscode-extension/test/host.cjs +++ b/packages/vscode-extension/test/host.cjs @@ -53,7 +53,7 @@ exports.run = async function () { const history = await bridge.callMcpTool('readmd_document_history', { file_path: linked, operation: 'read', checkpoint_id: applied.checkpoint_id }); assert.match(history.content, /careful/); results.push('safe edit preview, commit and recovery history'); const inspectionFile=path.join(root,'Links.md'); - fs.writeFileSync(inspectionFile,'# Links\n\n[[Plato]]\n\n[Broken](#missing)\n'); + fs.writeFileSync(inspectionFile,'# Links\n\n[[Plato]]\n\n[[#Local section]]\n\n## Local section\n\n[Broken](#missing)\n'); const inspectionDoc=await vscode.workspace.openTextDocument(vscode.Uri.file(inspectionFile)); await vscode.window.showTextDocument(inspectionDoc); await vscode.commands.executeCommand('readmd.inspectDocument'); @@ -63,6 +63,9 @@ exports.run = async function () { const wiki=documentLinks.find(link=>link.target?.fsPath.toLowerCase()===linked.toLowerCase()); assert.ok(wiki,'Wiki target resolves to the real local file'); assert.equal(inspectionDoc.getText(wiki.range),'[[Plato]]'); + const local=documentLinks.find(link=>inspectionDoc.getText(link.range)==='[[#Local section]]'); + assert.equal(local?.target?.fsPath.toLowerCase(),inspectionFile.toLowerCase()); + assert.equal(local?.target?.fragment,'local-section'); results.push('native Problems diagnostics and clickable Wiki provider'); await vscode.window.showTextDocument(document); await vscode.commands.executeCommand('readmd.preview'); results.push('real preview command'); diff --git a/tools/package-windows-candidate.mjs b/tools/package-windows-candidate.mjs index 2b94ceb7..8c744760 100644 --- a/tools/package-windows-candidate.mjs +++ b/tools/package-windows-candidate.mjs @@ -11,7 +11,7 @@ const output=path.resolve(process.argv[3]||'dist/candidate'); const pet=path.resolve(process.argv[4]||'dist/ReadMD-Pet-Rust.zip'); const version=fs.readFileSync(path.join(root,'VERSION'),'utf8').trim(); if(!binary||!fs.statSync(binary,{throwIfNoEntry:false})?.isFile())throw Error('Provide a built Windows binary'); -if(!execFileSync(binary,['--version'],{encoding:'utf8',windowsHide:true}).includes(version))throw Error('Binary version differs from VERSION'); +if(execFileSync(binary,['--version'],{encoding:'utf8',windowsHide:true}).trim()!=='readmd-rust '+version)throw Error('Binary version differs from VERSION'); if(!fs.statSync(pet,{throwIfNoEntry:false})?.isFile())throw Error('Provide the verified desktop pet archive'); const entries=[]; const add=(name,file)=>entries.push({name,data:fs.readFileSync(file)}); diff --git a/website/public/integrations/index.html b/website/public/integrations/index.html index 22a5022f..4507c376 100644 --- a/website/public/integrations/index.html +++ b/website/public/integrations/index.html @@ -1,5 +1,5 @@ -Markdown in VS Code and AI workflows | ReadMD +Markdown in VS Code and AI workflows | ReadMD -

V0.0.5 release candidate · not published yet

Markdown in VS Code and AI workflows

One local Rust engine. Your documents stay in your workflow.

VS Code, without another workspace

Live preview, math, diagrams and slides. V0.0.5 adds link diagnostics to the native Problems panel, clickable wiki links and local workspace search.

AI edits you can review

V0.0.5 MCP reads a document revision, previews literal edits and writes only after explicit confirmation. Conflicting file versions stop the edit; recovery history stays in application data.

Local files, current results

Search phrases, titles, paths and YAML tags directly in the workspace. Limits and skipped files are reported. URLs are never fetched during link inspection.

Read → preview → confirm → recover

Use the same executable installed with ReadMD. The MCP kit contains documentation and client configuration, not a second server binary.

readmd --mcp

V0.0.5 adds 5 tools to the 22-tool stable core. Availability follows the installed desktop version.

Integration documentation

Bounded by design: 2 MiB per inspected/edited document, 5,000 searched Markdown files, 64 MiB per search, 8 concurrent MCP tools. Native runtimes and OS services are required for some formats.

+

V0.0.5 release candidate · not published yet

Markdown in VS Code and AI workflows

One local Rust engine. Your documents stay in your workflow.

VS Code, without another workspace

Live preview, math, diagrams and slides. V0.0.5 adds link diagnostics to the native Problems panel, clickable wiki links and local workspace search.

AI edits you can review

V0.0.5 MCP reads a document revision, previews literal edits and writes only after explicit confirmation. Conflicting file versions stop the edit; recovery history stays in application data.

Local files, current results

Search phrases, titles, paths and YAML tags directly in the workspace. Limits and skipped files are reported. URLs are never fetched during link inspection.

Read → preview → confirm → recover

Use the same executable installed with ReadMD. The MCP kit contains documentation and client configuration, not a second server binary.

readmd --mcp

V0.0.5 adds 5 tools to the 22-tool stable core. Availability follows the installed desktop version.

Integration documentation

Bounded by design: 2 MiB per inspected/edited document, 5,000 searched Markdown files, 64 MiB per search, 8 concurrent MCP tools. Native runtimes and OS services are required for some formats.

diff --git a/website/public/ja/integrations/index.html b/website/public/ja/integrations/index.html index 6d383685..88af4955 100644 --- a/website/public/ja/integrations/index.html +++ b/website/public/ja/integrations/index.html @@ -1,5 +1,5 @@ -Markdown をエディターと AI につなぐ | ReadMD +Markdown をエディターと AI につなぐ | ReadMD -

V0.0.5 リリース候補 · 未公開

Markdown をエディターと AI につなぐ

同じローカル Rust エンジンを、いつもの作業環境で。

VS Code で集中する

数式、図、スライドをローカルで表示。V0.0.5 は標準の問題パネルにリンク診断を表示し、Wiki リンクと Markdown ワークスペース検索に対応します。

AI の編集を確認する

V0.0.5 MCP はファイルの版を読み取り、置換をまずプレビューします。明示的な確認後に保存し、競合時は停止。復元履歴はアプリのデータ領域に保存します。

現在のファイルを検索

フレーズ、タイトル、パスと YAML タグを組み合わせます。処理上限とスキップを報告し、リンク検査では URL に接続しません。

読み取り → プレビュー → 確認 → 復元

インストール済み ReadMD の実行ファイルを使用します。MCP パッケージは文書と接続設定を提供し、サーバー本体は含みません。

readmd --mcp

V0.0.5 は安定版の 22 ツールに 5 ツールを追加します。利用可能な機能はインストールした版によります。

連携ドキュメント

検査・編集は文書ごとに 2 MiB。検索は最大 5,000 ファイル、合計 64 MiB。MCP ツールは最大 8 件を並行実行。一部形式にはローカル実行環境や OS サービスが必要です。

+

V0.0.5 リリース候補 · 未公開

Markdown をエディターと AI につなぐ

同じローカル Rust エンジンを、いつもの作業環境で。

VS Code で集中する

数式、図、スライドをローカルで表示。V0.0.5 は標準の問題パネルにリンク診断を表示し、Wiki リンクと Markdown ワークスペース検索に対応します。

AI の編集を確認する

V0.0.5 MCP はファイルの版を読み取り、置換をまずプレビューします。明示的な確認後に保存し、競合時は停止。復元履歴はアプリのデータ領域に保存します。

現在のファイルを検索

フレーズ、タイトル、パスと YAML タグを組み合わせます。処理上限とスキップを報告し、リンク検査では URL に接続しません。

読み取り → プレビュー → 確認 → 復元

インストール済み ReadMD の実行ファイルを使用します。MCP パッケージは文書と接続設定を提供し、サーバー本体は含みません。

readmd --mcp

V0.0.5 は安定版の 22 ツールに 5 ツールを追加します。利用可能な機能はインストールした版によります。

連携ドキュメント

検査・編集は文書ごとに 2 MiB。検索は最大 5,000 ファイル、合計 64 MiB。MCP ツールは最大 8 件を並行実行。一部形式にはローカル実行環境や OS サービスが必要です。

diff --git a/website/public/zh-cn/integrations/index.html b/website/public/zh-cn/integrations/index.html index 49b9bd5a..c88c1b72 100644 --- a/website/public/zh-cn/integrations/index.html +++ b/website/public/zh-cn/integrations/index.html @@ -1,5 +1,5 @@ -让 Markdown 接入你的编辑器与 AI | ReadMD +让 Markdown 接入你的编辑器与 AI | ReadMD -

V0.0.5 发布候选 · 尚未正式发布

让 Markdown 接入你的编辑器与 AI

同一套本地 Rust 内核,沿用你熟悉的工作方式。

在 VS Code 中,保持专注

预览、公式、图表与演讲继续复用本地内核。V0.0.5 将链接检查放进原生“问题”面板,支持点击双链和搜索 Markdown 工作区。

让 AI 编辑先经过审阅

V0.0.5 MCP 读取文件版本,默认只预览文字替换;明确确认后才写入。文件被其他操作修改时停止覆盖,恢复记录保存在应用数据区。

直接检索当前文件

组合短语、标题、路径与 YAML 标签,不等待旧索引更新。检查范围、跳过文件与处理上限明确返回;链接检查不联网。

读取 → 预览 → 确认 → 恢复

使用已安装的 ReadMD 可执行程序启动 MCP。连接包提供文档与客户端配置,不重复附带服务端内核。

readmd --mcp

V0.0.5 在正式版 22 个工具基础上增加 5 个工具,具体能力以已安装的桌面版本为准。

查看集成文档

明确边界:检查及编辑单文档上限 2 MiB;单次搜索最多 5,000 个 Markdown 文件、64 MiB 内容;MCP 最多并发 8 个工具。部分格式依赖本机运行时或系统服务。

+

V0.0.5 发布候选 · 尚未正式发布

让 Markdown 接入你的编辑器与 AI

同一套本地 Rust 内核,沿用你熟悉的工作方式。

在 VS Code 中,保持专注

预览、公式、图表与演讲继续复用本地内核。V0.0.5 将链接检查放进原生“问题”面板,支持点击双链和搜索 Markdown 工作区。

让 AI 编辑先经过审阅

V0.0.5 MCP 读取文件版本,默认只预览文字替换;明确确认后才写入。文件被其他操作修改时停止覆盖,恢复记录保存在应用数据区。

直接检索当前文件

组合短语、标题、路径与 YAML 标签,不等待旧索引更新。检查范围、跳过文件与处理上限明确返回;链接检查不联网。

读取 → 预览 → 确认 → 恢复

使用已安装的 ReadMD 可执行程序启动 MCP。连接包提供文档与客户端配置,不重复附带服务端内核。

readmd --mcp

V0.0.5 在正式版 22 个工具基础上增加 5 个工具,具体能力以已安装的桌面版本为准。

查看集成文档

明确边界:检查及编辑单文档上限 2 MiB;单次搜索最多 5,000 个 Markdown 文件、64 MiB 内容;MCP 最多并发 8 个工具。部分格式依赖本机运行时或系统服务。

diff --git a/website/public/zh-tw/integrations/index.html b/website/public/zh-tw/integrations/index.html index df4b6f0b..b36a96fd 100644 --- a/website/public/zh-tw/integrations/index.html +++ b/website/public/zh-tw/integrations/index.html @@ -1,5 +1,5 @@ -讓 Markdown 接入你的編輯器與 AI | ReadMD +讓 Markdown 接入你的編輯器與 AI | ReadMD -

V0.0.5 發布候選 · 尚未正式發布

讓 Markdown 接入你的編輯器與 AI

同一套本機 Rust 核心,沿用你熟悉的工作方式。

在 VS Code 中,保持專注

預覽、公式、圖表與簡報繼續共用本機核心。V0.0.5 將連結檢查放進原生「問題」面板,支援點擊雙向連結與搜尋 Markdown 工作區。

讓 AI 編輯先經過審閱

V0.0.5 MCP 讀取檔案版本,預設只預覽文字取代;明確確認後才寫入。檔案被其他操作修改時停止覆寫,復原記錄保存在應用程式資料區。

直接搜尋目前檔案

組合片語、標題、路徑與 YAML 標籤,不必等待舊索引更新。檢查範圍、略過檔案與處理上限明確回傳;連結檢查不連網。

讀取 → 預覽 → 確認 → 復原

使用已安裝的 ReadMD 執行檔啟動 MCP。連線包提供文件與用戶端設定,不重複附帶伺服器核心。

readmd --mcp

V0.0.5 在正式版 22 個工具基礎上增加 5 個工具,實際能力以已安裝的桌面版本為準。

查看整合文件

明確邊界:檢查及編輯單份文件上限 2 MiB;單次搜尋最多 5,000 個 Markdown 檔案、64 MiB 內容;MCP 最多並行 8 個工具。部分格式依賴本機執行環境或系統服務。

+

V0.0.5 發布候選 · 尚未正式發布

讓 Markdown 接入你的編輯器與 AI

同一套本機 Rust 核心,沿用你熟悉的工作方式。

在 VS Code 中,保持專注

預覽、公式、圖表與簡報繼續共用本機核心。V0.0.5 將連結檢查放進原生「問題」面板,支援點擊雙向連結與搜尋 Markdown 工作區。

讓 AI 編輯先經過審閱

V0.0.5 MCP 讀取檔案版本,預設只預覽文字取代;明確確認後才寫入。檔案被其他操作修改時停止覆寫,復原記錄保存在應用程式資料區。

直接搜尋目前檔案

組合片語、標題、路徑與 YAML 標籤,不必等待舊索引更新。檢查範圍、略過檔案與處理上限明確回傳;連結檢查不連網。

讀取 → 預覽 → 確認 → 復原

使用已安裝的 ReadMD 執行檔啟動 MCP。連線包提供文件與用戶端設定,不重複附帶伺服器核心。

readmd --mcp

V0.0.5 在正式版 22 個工具基礎上增加 5 個工具,實際能力以已安裝的桌面版本為準。

查看整合文件

明確邊界:檢查及編輯單份文件上限 2 MiB;單次搜尋最多 5,000 個 Markdown 檔案、64 MiB 內容;MCP 最多並行 8 個工具。部分格式依賴本機執行環境或系統服務。

diff --git a/website/release.json b/website/release.json index 02a80e75..2aa1c8ac 100644 --- a/website/release.json +++ b/website/release.json @@ -1,6 +1,7 @@ { "stable": "0.0.4", "candidate": "0.0.5", + "candidate_ref": "codex/v005-upgrade", "published": "2026-10-07", "repository": "Natsummerance/rust-ReadMD" } diff --git a/website/tools/build-integrations.mjs b/website/tools/build-integrations.mjs index de70c6c5..ec24de89 100644 --- a/website/tools/build-integrations.mjs +++ b/website/tools/build-integrations.mjs @@ -24,24 +24,31 @@ const locales={ ['現在のファイルを検索','フレーズ、タイトル、パスと YAML タグを組み合わせます。処理上限とスキップを報告し、リンク検査では URL に接続しません。']], flow:'読み取り → プレビュー → 確認 → 復元',code:'インストール済み ReadMD の実行ファイルを使用します。MCP パッケージは文書と接続設定を提供し、サーバー本体は含みません。',docs:'連携ドキュメント',limits:'検査・編集は文書ごとに 2 MiB。検索は最大 5,000 ファイル、合計 64 MiB。MCP ツールは最大 8 件を並行実行。一部形式にはローカル実行環境や OS サービスが必要です。',more:'V0.0.5 は安定版の 22 ツールに 5 ツールを追加します。利用可能な機能はインストールした版によります。'} }; +// Localized copy is authored against this template version; future candidates +// update the shared release manifest instead of hand-editing four pages. +for (const c of Object.values(locales)) { + for (const key of ['description','candidate','more']) c[key]=c[key].replaceAll('V0.0.5','V'+release.candidate); + c.cards=c.cards.map(card=>card.map(text=>text.replaceAll('V0.0.5','V'+release.candidate))); +} const esc=s=>s.replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); for(const [lang,c] of Object.entries(locales)){ const url=origin+'/'+c.prefix+'integrations/'; + const docsRef=release.candidate_ref||'main'; const alternate=Object.entries(locales).map(([l,v])=>'').join('\n'); const schema={'@context':'https://schema.org','@type':'WebPage',name:c.title,description:c.description,url,inLanguage:lang,dateModified:'2026-10-07', isPartOf:{'@type':'WebSite',name:'ReadMD',url:origin+'/'}}; const html='\n'+ - ''+esc(c.title)+' | ReadMD'+ + ''+esc(c.title)+' | ReadMD'+ ''+alternate+''+ ''+ - ''+ - ''+ - '
'+ - '

'+esc(c.candidate)+'

'+esc(c.title)+'

'+ - '

'+esc(c.lead)+'

'+c.cards.map(([h,p])=>'

'+esc(h)+'

'+esc(p)+'

').join('')+'
'+ - '

'+esc(c.flow)+'

'+esc(c.code)+'

readmd --mcp
'+ - '

'+esc(c.more)+'

'+esc(c.docs)+'
'+ - '

'+esc(c.limits)+'

\n'; + ''+ + ''+ + '
'+ + '

'+esc(c.candidate)+'

'+esc(c.title)+'

'+ + '

'+esc(c.lead)+'

'+c.cards.map(([h,p])=>'

'+esc(h)+'

'+esc(p)+'

').join('')+'
'+ + '

'+esc(c.flow)+'

'+esc(c.code)+'

readmd --mcp
'+ + '

'+esc(c.more)+'

'+esc(c.docs)+'
'+ + '

'+esc(c.limits)+'

\n'; const dir=new URL(c.prefix+'integrations/',site);fs.mkdirSync(dir,{recursive:true});fs.writeFileSync(new URL('index.html',dir),html); const home=new URL(c.prefix+'index.html',site);let text=fs.readFileSync(home,'utf8'); if(!text.includes('href="/'+c.prefix+'integrations/"')) text=text.replace('','

'+esc(c.title)+'

'); diff --git a/website/tools/validate-website.mjs b/website/tools/validate-website.mjs index 41f5a464..bf5d3e85 100644 --- a/website/tools/validate-website.mjs +++ b/website/tools/validate-website.mjs @@ -40,7 +40,8 @@ for (const [key, slug] of ANSWER_TOPICS) { for (const [lang, c] of Object.entries(pageSet(slug))) ANSWER_PAGES[`${lang}-${key}`] = c; } -const VERSION = JSON.parse(fs.readFileSync(path.join(SITE, 'release.json'), 'utf8')).stable; +const RELEASE_INFO = JSON.parse(fs.readFileSync(path.join(SITE, 'release.json'), 'utf8')); +const VERSION = RELEASE_INFO.stable; const RELEASE_ASSETS = new Set([ 'ReadMDSetup-windows-x64.exe', 'ReadMD-windows-x64.zip', 'ReadMD-macos-arm64.zip', 'ReadMD-macos-x64.zip', 'ReadMD-macos-arm64.dmg', 'ReadMD-macos-x64.dmg', @@ -487,7 +488,7 @@ function main() { if (!isFile(c.path)) { errors.push('Missing integration page: '+c.canonical); continue; } const html=read(c.path); if (!html.includes('rel="canonical" href="'+c.canonical+'"')) errors.push('Integration canonical mismatch: '+c.canonical); - if (!html.includes('V0.0.5') || !html.includes('--mcp')) errors.push('Integration capabilities missing: '+c.canonical); + if (!html.includes('V'+RELEASE_INFO.candidate) || !html.includes('--mcp')) errors.push('Integration capabilities missing: '+c.canonical); } const groups = [[LANGUAGES, 'index'], [INTENT_PAGES, 'workflow page'], [DOWNLOAD_PAGES, 'download page'], [ANSWER_PAGES, 'answer page']]; for (const [group, label] of groups) { @@ -514,7 +515,7 @@ function main() { answer_pages: Object.keys(ANSWER_PAGES), showcase_features: JSON.parse(read(P('showcase/catalog.json'))).features.length, broad_seo: { - canonical_pages: 1 + [LANGUAGES, INTENT_PAGES, DOWNLOAD_PAGES, ANSWER_PAGES].reduce((n, g) => n + Object.keys(g).length, 0), + canonical_pages: 1 + [LANGUAGES, INTENT_PAGES, DOWNLOAD_PAGES, ANSWER_PAGES, INTEGRATION_PAGES].reduce((n, g) => n + Object.keys(g).length, 0), atom_feed: true, entity_graph: true, security_txt: true, quality_404: true, genuine_operation_gallery: true, }, })); From 1db03db00c22ba60fb8fe5df315311d1d8d17459 Mon Sep 17 00:00:00 2001 From: Natsummerance <2734763029@qq.com> Date: Wed, 7 Oct 2026 11:35:50 +0800 Subject: [PATCH 3/4] feat(website): restore scroll-driven product motion and refresh external links Co-authored-by: Codex --- .github/workflows/repo-quality.yml | 8 + .../reviews/readmd-v005-upgrade-2026-10-07.md | 14 +- .../vscode-extension/l10n/bundle.l10n.json | 3 +- .../l10n/bundle.l10n.zh-cn.json | 3 +- packages/vscode-extension/src/intelligence.ts | 21 +- .../vscode-extension/test/commands.test.js | 1 + packages/vscode-extension/test/host.cjs | 7 + .../test/intelligence.test.js | 54 +++++ website/MOTION.md | 25 +++ website/package.json | 3 +- website/public/404.html | 3 +- website/public/assets/motion.css | 56 +++++ website/public/assets/motion.js | 194 ++++++++++++++++++ website/public/bibtex-citations/index.html | 3 +- website/public/convert-to-markdown/index.html | 3 +- website/public/download/index.html | 5 +- website/public/index.html | 10 +- website/public/integrations/index.html | 3 +- website/public/ja/bibtex-citations/index.html | 3 +- .../public/ja/convert-to-markdown/index.html | 3 +- website/public/ja/download/index.html | 5 +- website/public/ja/index.html | 10 +- website/public/ja/integrations/index.html | 3 +- .../public/ja/large-markdown-files/index.html | 3 +- website/public/ja/markdown-tables/index.html | 3 +- .../public/ja/markdown-to-slides/index.html | 3 +- website/public/ja/pdf-to-markdown/index.html | 3 +- website/public/ja/release-notes/index.html | 3 +- website/public/ja/scan-to-markdown/index.html | 3 +- website/public/ja/workflows/index.html | 5 +- .../public/large-markdown-files/index.html | 3 +- website/public/markdown-tables/index.html | 3 +- website/public/markdown-to-slides/index.html | 3 +- website/public/pdf-to-markdown/index.html | 3 +- website/public/release-notes/index.html | 3 +- website/public/scan-to-markdown/index.html | 3 +- website/public/showcase/index.html | 3 +- website/public/workflows/index.html | 5 +- .../public/zh-cn/bibtex-citations/index.html | 3 +- .../zh-cn/convert-to-markdown/index.html | 3 +- website/public/zh-cn/download/index.html | 5 +- website/public/zh-cn/index.html | 10 +- website/public/zh-cn/integrations/index.html | 3 +- .../zh-cn/large-markdown-files/index.html | 3 +- .../public/zh-cn/markdown-tables/index.html | 3 +- .../zh-cn/markdown-to-slides/index.html | 3 +- .../public/zh-cn/pdf-to-markdown/index.html | 3 +- website/public/zh-cn/release-notes/index.html | 3 +- .../public/zh-cn/scan-to-markdown/index.html | 3 +- website/public/zh-cn/workflows/index.html | 5 +- .../public/zh-tw/bibtex-citations/index.html | 3 +- .../zh-tw/convert-to-markdown/index.html | 3 +- website/public/zh-tw/download/index.html | 5 +- website/public/zh-tw/index.html | 10 +- website/public/zh-tw/integrations/index.html | 3 +- .../zh-tw/large-markdown-files/index.html | 3 +- .../public/zh-tw/markdown-tables/index.html | 3 +- .../zh-tw/markdown-to-slides/index.html | 3 +- .../public/zh-tw/pdf-to-markdown/index.html | 3 +- website/public/zh-tw/release-notes/index.html | 3 +- .../public/zh-tw/scan-to-markdown/index.html | 3 +- website/public/zh-tw/workflows/index.html | 5 +- website/tools/build-motion-pages.mjs | 50 +++++ website/tools/build-scroll-media.mjs | 36 ++++ website/tools/lib/serve-static.cjs | 30 +++ website/tools/motion-smoke.cjs | 92 +++++++++ website/tools/validate-website.mjs | 17 +- 67 files changed, 725 insertions(+), 83 deletions(-) create mode 100644 packages/vscode-extension/test/intelligence.test.js create mode 100644 website/MOTION.md create mode 100644 website/public/assets/motion.css create mode 100644 website/public/assets/motion.js create mode 100644 website/tools/build-motion-pages.mjs create mode 100644 website/tools/build-scroll-media.mjs create mode 100644 website/tools/lib/serve-static.cjs create mode 100644 website/tools/motion-smoke.cjs diff --git a/.github/workflows/repo-quality.yml b/.github/workflows/repo-quality.yml index 52b107de..46001e02 100644 --- a/.github/workflows/repo-quality.yml +++ b/.github/workflows/repo-quality.yml @@ -90,6 +90,7 @@ jobs: cargo fetch --locked --manifest-path rust/Cargo.toml npm ci --ignore-scripts --prefix packages/vscode-extension npm ci --ignore-scripts --prefix ui-tests + npm ci --ignore-scripts --prefix website cd ui-tests npx --no-install playwright install --with-deps chromium webkit - name: Compile and test integrations offline @@ -109,6 +110,13 @@ jobs: run: npx --no-install playwright test --config vscode-preview.config.cjs - name: Package integrations without downloads run: node tools/package-integrations.mjs dist/integrations + - name: Build website offline and verify scroll video in Chrome + env: + NODE_PATH: ${{ github.workspace }}/ui-tests/node_modules + READMD_MOTION_CHANNEL: chrome + run: | + npm run verify:release --prefix website + node website/tools/motion-smoke.cjs - name: Upload integration packages uses: actions/upload-artifact@v4 with: diff --git a/docs/reviews/readmd-v005-upgrade-2026-10-07.md b/docs/reviews/readmd-v005-upgrade-2026-10-07.md index 52983425..bdc045fe 100644 --- a/docs/reviews/readmd-v005-upgrade-2026-10-07.md +++ b/docs/reviews/readmd-v005-upgrade-2026-10-07.md @@ -19,7 +19,7 @@ ## 增量操作清单 1. **桌面文档检查**:打开文档或编辑草稿 → 更多/互动/修复详情 → 查看链接检查。只检查当前内容;异步返回后若切换标签或修改文本,结果作废。不自动改写链接。 -2. **VS Code 文档检查**:打开已信任工作区的 Markdown 文件 → 命令面板“ReadMD: 检查文档链接与标题” → 原生“问题”面板定位。活动草稿修改后防抖更新,保存、文件创建/删除/重命名后失效旧缓存。 +2. **VS Code 文档检查**:打开已信任工作区的 Markdown 文件 → 命令面板“ReadMD: 检查文档链接与标题” → 原生“问题”面板定位。活动草稿修改后防抖更新,保存、文件创建/删除/重命名后失效旧缓存;外部程序修改工作区文件也会触发刷新,忽略生成目录的噪声。 3. **VS Code Wiki 跳转**:打开包含双链的文档 → 原生链接操作跳转;明确工作区范围,重名目标不猜测。复用 VS Code 的标准 Markdown 链接能力。 4. **工作区检索**:命令面板“ReadMD: 搜索本地 Markdown 工作区” → 输入标签、短语与排除词 → Quick Pick 显示标题、相对路径和摘要 → 打开结果行。多工作区先选择范围,取消请求不应用迟到结果。 5. **MCP 读取**:readmd_read_document 返回指定行段、原始字节 SHA-256、编码及换行信息。 @@ -42,6 +42,8 @@ MCP 从 22 项增加到 27 项;VS Code 从 22 条贡献命令增加到 24 条 网站沿用现有 Apple 风格。website/release.json 区分公开正式版与候选版,离线构建使用它生成 version.json。正式发布并验证完整资产后再将 stable 改为新版本;同步工具不会把未发布候选写进公开下载链接。 +四语言首页恢复六段滚动视频:滚动决定视频帧,支持倒退及章节切换,窗口随滚动缩放、轻微透视和位移。全站 50 个 HTML 页面共用组件入场、卡片悬停、指针光边、按钮按压及键盘焦点反馈;保留原版排版和明暗主题,视频不叠加提亮滤镜。最多两个视频节点,后台标签暂停动效,停止滚动后停止逐帧计算。减少动态效果或禁用 JavaScript 时所有章节仍可阅读,视频失败保留海报与真实操作链接。离线媒体派生与复验方法见 [网站动效说明](../../website/MOTION.md)。历史六段源录屏为 1280×800,转码不提升真实细节,不标成 2K;新增四条功能录屏为原生 2560×1600。 + node website/tools/build-integrations.mjs 生成四语言集成说明。所有正式安装、VSIX、MCP 连接包和校验文件均指向对应发布资产;MCP 连接包不被描述成独立服务端二进制。 增量演示位于本机 showcase/updates/v0.0.5/,使用合成哲学阅读材料、真实桌面应用、真实 Extension Host 与 Rust MCP。新管线不改写旧 104 项录制清单。画面不包含系统文件夹、私人路径、录制编号或提示叠层;媒体和制作脚本不进入软件提交。 @@ -53,15 +55,15 @@ node website/tools/build-integrations.mjs 生成四语言集成说明。所有 | 验收层 | 结果与实际边界 | | --- | --- | | Rust 完整回归 | 本机完整运行 1,875 项通过、3 项保留原有忽略;116 项系统/原生测试通过,xtask 12 项通过。随后新增的编码与宿主锚点用例纳入 9 项文档智能定向测试,全部通过。最终完整数量以 PR 的 Windows/Linux CI 为准。 | -| JavaScript / TypeScript | 离线 TypeScript 严格检查 8 个源文件;扩展 44 项及前端/工具 49 项,共 93 项通过。 | +| JavaScript / TypeScript | 离线 TypeScript 严格检查 8 个源文件;扩展 46 项及前端/工具 49 项,共 95 项通过。 | | 桌面 UI 全量回归 | 298 项通过,31 项按既有环境条件跳过;不能将跳过项目算作实测通过。新增用例覆盖编辑草稿、过期响应和请求边界。 | | 解压后的 Windows 候选包 | 真实 WebView2 与原有 CSP,7 个操作流程通过;46 种语言切换保持草稿及单行工具栏,3 个主题、插入/撤销、图像保存、局域网分享均验证,零页面脚本错误。 | -| 安装后的 VSIX | VS Code 真实 Extension Host,13 个流程通过;含原生 Problems、跨文档/同文档 Wiki 链接、检索、预览、确认写入、历史读取、演讲及 DOCX 导出。使用隔离配置,不覆盖日常扩展设置。 | +| 安装后的 VSIX | VS Code 真实 Extension Host,14 个流程通过;含原生 Problems、外部重命名自动刷新、跨文档/同文档 Wiki 链接、检索、预览、确认写入、历史读取、演讲及 DOCX 导出。使用隔离配置,不覆盖日常扩展设置。 | | MCP 真实 stdio | 27 工具发现,15 类验收检查通过,含新工具完整流程、版本冲突、原文件删除后的恢复、AI 模拟服务协议、取消、重复请求 ID 与超大输入后连接恢复。这里的 AI 服务是本机合成测试服务,不代表公网模型成功率。 | | 打包后二进制压力 | 700 份合成文档;128 个瞬时请求中 8 个执行、120 个明确返回忙碌。客户端限制并发为 8 后,128 个请求全部完成。16 个竞争写入只有 1 次提交,其余为版本冲突或忙碌;默认预览不改文件,删除原文件后历史仍可读取。 | | 真实文件 | 使用两份本机真实 Markdown 的隔离副本,大小 1,836,325 与 7,807 字节;读取版本、AST 检查、检索和修改预览通过,原件与副本的 SHA-256 均未变化。未把私人内容上传给 AI 或纳入视频。 | -| 网站 | 离线构建与发布校验通过,49 个 canonical 页面及既有 104 项演示清单检查通过;四语言集成页在两种宽度、明暗主题共 16 组浏览器检查通过,无横向溢出及脚本错误;既有下载页的主题检查继续通过。 | -| 增量视频 | 4 条,原生 2560×1600、30 fps;分别展示当前草稿检查、Wiki/Problems、工作区检索、MCP 预览/提交/恢复。97 个抽样帧经过 OCR 隐私检查,全部通过;不宣称逐帧 OCR。 | +| 网站 | 离线构建与发布校验通过,49 个 canonical 页面及既有 104 项演示清单检查通过;四语言集成页在两种宽度、明暗主题共 16 组浏览器检查通过。新增滚动动效在真实 Edge 完成 44 组检查,含六段定位、倒退、章节边界、320/390/1024 窄屏、暗色、MP4 回退、媒体失败、减少动态效果及禁用 JavaScript;89 个连续帧间隔的 p95 为 24.9ms,页面脚本错误为零。Windows 测试版 WebKit 未能解码该 WebM,不将它标为视频通过,也不能据此代替 macOS Safari 实测。 | +| 增量视频 | 4 条,原生 2560×1600、30 fps;分别展示当前草稿检查、Wiki/Problems、工作区检索、MCP 预览/提交/恢复。94 个抽样帧经过 OCR 隐私检查,全部通过;不宣称逐帧 OCR。 | | 静态门禁 | i18n、样式、控件接线、资源、无 Python 构建链、隐私扫描和 Git 空白检查通过。媒体及制作文件均未进入软件提交。 | 验收日志保留在本机 .cache/v005/,候选包、校验和及源提交记录在 dist/candidate-v0.0.5/,VSIX 与 MCP 连接包位于 .cache/v005/integrations/。增量素材、录像脚本、清单、字幕和本地查看页位于 showcase/updates/v0.0.5/;这部分已被 Git 忽略,可在后续版本复制同一管线并替换合成材料。 @@ -86,7 +88,9 @@ node website/tools/build-integrations.mjs 生成四语言集成说明。所有 node tools/package-integrations.mjs dist/integrations node tools/package-windows-candidate.mjs <已构建的ReadMD.exe> dist/candidate-v0.0.5 dist/ReadMD-Pet-Rust.zip node website/tools/build-integrations.mjs + node website/tools/build-motion-pages.mjs node website/tools/sync-version.mjs node website/tools/validate-website.mjs --release + node website/tools/motion-smoke.cjs stdio 验收要求 READMD_BIN、READMD_ASSETS_DIR、隔离的 READMD_MCP_TEST_ROOT;原生验收使用 ui-tests/panel-native-smoke.cjs,按脚本要求指定独立资料目录。Playwright 使用本机已安装 CLI,运行桌面项目及 CI 列出的移动项目;不使用会隐式联网安装的命令。 diff --git a/packages/vscode-extension/l10n/bundle.l10n.json b/packages/vscode-extension/l10n/bundle.l10n.json index a0a32bd5..d38dfaab 100644 --- a/packages/vscode-extension/l10n/bundle.l10n.json +++ b/packages/vscode-extension/l10n/bundle.l10n.json @@ -196,5 +196,6 @@ "searchEmpty": "No matches in the scanned files.", "searchLimited": "Results limited to 50; refine your query", "searchResults": "Select a result to open", - "searchUnavailable": "Search could not finish. Check the query and update ReadMD Core." + "searchUnavailable": "Search could not finish. Check the query and update ReadMD Core.", + "inspectionChanged": "The document changed during inspection. Run it again." } diff --git a/packages/vscode-extension/l10n/bundle.l10n.zh-cn.json b/packages/vscode-extension/l10n/bundle.l10n.zh-cn.json index c3e52267..0415a856 100644 --- a/packages/vscode-extension/l10n/bundle.l10n.zh-cn.json +++ b/packages/vscode-extension/l10n/bundle.l10n.zh-cn.json @@ -196,5 +196,6 @@ "searchEmpty": "已扫描的文件中没有匹配结果。", "searchLimited": "结果或扫描达到上限,请缩小查询范围", "searchResults": "选择结果以打开文件", - "searchUnavailable": "搜索未能完成,请检查查询语法并更新 ReadMD 核心。" + "searchUnavailable": "搜索未能完成,请检查查询语法并更新 ReadMD 核心。", + "inspectionChanged": "检查期间文档或关联文件已变化,请重新检查。" } diff --git a/packages/vscode-extension/src/intelligence.ts b/packages/vscode-extension/src/intelligence.ts index 67c1def6..df1bf8a4 100644 --- a/packages/vscode-extension/src/intelligence.ts +++ b/packages/vscode-extension/src/intelligence.ts @@ -20,6 +20,7 @@ export function registerIntelligence(context: vscode.ExtensionContext, bridge: R const cache = new Map }>(); const timers = new Map>(); let disposed = false; + let fileChangeTimer: ReturnType | undefined; const eligible = (doc: vscode.TextDocument) => vscode.workspace.isTrusted !== false && doc.languageId === 'markdown' && doc.uri.scheme === 'file'; const inspect = async (doc: vscode.TextDocument): Promise => { @@ -58,8 +59,21 @@ export function registerIntelligence(context: vscode.ExtensionContext, bridge: R const key = doc.uri.toString(); clearTimeout(timers.get(key)); timers.set(key, setTimeout(() => { timers.delete(key); void inspect(doc).catch(() => {}); }, 700)); }; + const fileChanged = (uri: vscode.Uri) => { + const folder = vscode.workspace.getWorkspaceFolder(uri); + if (!folder || path.relative(folder.uri.fsPath, uri.fsPath).split(/[\\/]/) + .some(part => ['.git', 'node_modules', 'target', 'dist', 'build', 'vendor', '__pycache__'].includes(part))) return; + // VS Code's create/delete/rename events omit changes made by other programs. + // Invalidate in-flight results now, debounce the native reinspection. + cache.clear(); clearTimeout(fileChangeTimer); + fileChangeTimer = setTimeout(() => { + for (const doc of vscode.workspace.textDocuments) schedule(doc); + }, 250); + }; + const watcher = vscode.workspace.createFileSystemWatcher('**/*'); context.subscriptions.push(diagnostics, - { dispose() { disposed = true; for (const timer of timers.values()) clearTimeout(timer); cache.clear(); } }, + watcher, watcher.onDidCreate(fileChanged), watcher.onDidChange(fileChanged), watcher.onDidDelete(fileChanged), + { dispose() { disposed = true; clearTimeout(fileChangeTimer); for (const timer of timers.values()) clearTimeout(timer); cache.clear(); } }, vscode.workspace.onDidOpenTextDocument(schedule), vscode.workspace.onDidSaveTextDocument(() => { cache.clear(); for (const doc of vscode.workspace.textDocuments) schedule(doc); }), vscode.workspace.onDidChangeTextDocument(event => { @@ -94,10 +108,13 @@ export function registerIntelligence(context: vscode.ExtensionContext, bridge: R vscode.commands.registerCommand('readmd.inspectDocument', async () => { const doc = vscode.window.activeTextEditor?.document; if (!doc || !eligible(doc)) { void vscode.window.showInformationMessage(l10n('inspectionOpen', 'Open a Markdown file in a trusted workspace first.')); return; } + if (Buffer.byteLength(doc.getText(), 'utf8') > 2 * 1024 * 1024) { + void vscode.window.showInformationMessage(l10n('inspectionSize', 'Inspection supports documents up to 2 MB.')); return; + } cache.delete(doc.uri.toString()); try { const result = await inspect(doc); - if (!result) { void vscode.window.showInformationMessage(l10n('inspectionSize', 'Inspection supports documents up to 2 MB.')); return; } + if (!result) { void vscode.window.showInformationMessage(l10n('inspectionChanged', 'The document changed during inspection. Run it again.')); return; } await vscode.commands.executeCommand('workbench.actions.view.problems'); void vscode.window.showInformationMessage(l10n(result.truncated ? 'inspectionLimited' : 'inspectionDone', result.truncated ? 'Inspection reached a processing limit; some content was not checked.' : 'Document inspection complete: {count} items.', diff --git a/packages/vscode-extension/test/commands.test.js b/packages/vscode-extension/test/commands.test.js index 1777f3ef..b9b7aa3f 100644 --- a/packages/vscode-extension/test/commands.test.js +++ b/packages/vscode-extension/test/commands.test.js @@ -104,6 +104,7 @@ const vscodeStub = { showTextDocument: async doc => { openedDocs.push(doc); return doc; }, }, workspace: { + createFileSystemWatcher: () => ({ onDidCreate: () => ({ dispose() {} }), onDidChange: () => ({ dispose() {} }), onDidDelete: () => ({ dispose() {} }), dispose() {} }), textDocuments: [], onDidOpenTextDocument: () => ({ dispose() {} }), onDidSaveTextDocument: () => ({ dispose() {} }), diff --git a/packages/vscode-extension/test/host.cjs b/packages/vscode-extension/test/host.cjs index e727bf5d..78e20d0c 100644 --- a/packages/vscode-extension/test/host.cjs +++ b/packages/vscode-extension/test/host.cjs @@ -67,6 +67,13 @@ exports.run = async function () { assert.equal(local?.target?.fsPath.toLowerCase(),inspectionFile.toLowerCase()); assert.equal(local?.target?.fragment,'local-section'); results.push('native Problems diagnostics and clickable Wiki provider'); + const moved=path.join(root,'Renamed-Plato.md'); + const until=async predicate=>{const end=Date.now()+15000;while(Date.now()setTimeout(resolve,80));}throw Error('External file changes did not refresh diagnostics');}; + fs.renameSync(linked,moved); + await until(()=>vscode.languages.getDiagnostics(inspectionDoc.uri).some(d=>d.source==='ReadMD'&&d.code==='missing_file')); + fs.renameSync(moved,linked); + await until(()=>!vscode.languages.getDiagnostics(inspectionDoc.uri).some(d=>d.source==='ReadMD'&&d.code==='missing_file')); + results.push('external file rename refreshes diagnostics without editing the draft'); await vscode.window.showTextDocument(document); await vscode.commands.executeCommand('readmd.preview'); results.push('real preview command'); await vscode.window.showTextDocument(document); diff --git a/packages/vscode-extension/test/intelligence.test.js b/packages/vscode-extension/test/intelligence.test.js new file mode 100644 index 00000000..ebca0ef6 --- /dev/null +++ b/packages/vscode-extension/test/intelligence.test.js @@ -0,0 +1,54 @@ +const test=require('node:test'); +const assert=require('node:assert/strict'); +const path=require('node:path'); +const Module=require('node:module'); +const entry=path.resolve(__dirname,'../out/intelligence.js'); +function fixture(){ + const root=path.resolve('synthetic-workspace'),events={},subscriptions=[],values=new Map(); + const uri=file=>({scheme:'file',fsPath:file,toString:()=>file,with(options){return {...this,...options};}}); + const doc={uri:uri(path.join(root,'Reading.md')),languageId:'markdown',version:1,isClosed:false, + lineCount:1,getText:()=> '[[Target]]',lineAt:()=>({text:'[[Target]]'})}; + const disposable=()=>({dispose(){}}),listener=name=>handler=>{events[name]=handler;return disposable();}; + let provider; + const stub={env:{language:'en'},Uri:{file:uri},DiagnosticSeverity:{Warning:1}, + Range:class{constructor(...args){this.args=args;}},Diagnostic:class{constructor(range,message,severity){Object.assign(this,{range,message,severity});}}, + DocumentLink:class{constructor(range,target){Object.assign(this,{range,target});}}, + languages:{createDiagnosticCollection:()=>({set:(key,value)=>values.set(key.toString(),value),delete:key=>values.delete(key.toString()),clear:()=>values.clear(),dispose(){}}), + registerDocumentLinkProvider:(_,value)=>{provider=value;return disposable();}}, + commands:{registerCommand:()=>disposable()},window:{activeTextEditor:{document:doc}}, + workspace:{isTrusted:true,textDocuments:[doc],getWorkspaceFolder:key=>key.fsPath.startsWith(root+path.sep)?{uri:uri(root)}:undefined, + createFileSystemWatcher:()=>({onDidCreate:listener('create'),onDidChange:listener('change'),onDidDelete:listener('delete'),dispose(){}}), + onDidOpenTextDocument:listener('open'),onDidSaveTextDocument:listener('save'),onDidChangeTextDocument:listener('edit'), + onDidCloseTextDocument:listener('close'),onDidChangeWorkspaceFolders:listener('folders'),onDidCreateFiles:listener('createFiles'),onDidDeleteFiles:listener('deleteFiles'),onDidRenameFiles:listener('renameFiles')} + }; + const original=Module._load; + Module._load=function(name,...args){return name==='vscode'?stub:original.call(this,name,...args);}; + let register;try{delete require.cache[entry];register=require(entry).registerIntelligence;}finally{Module._load=original;} + let exists=false,calls=0; + const result=()=>({truncated:false,links:exists?[{kind:'wiki',target:'Target',resolved_path:path.join(root,'Target.md'),position:{line:1,column:1}}]:[], + diagnostics:exists?[]:[{code:'missing_file',target:'Target',position:{line:1,column:1}}]}); + const bridge={callMcpTool:async()=>{calls++;return result();}}; + register({subscriptions},bridge); + return {events,doc,bridge,values,result,target:uri(path.join(root,'Target.md')),generated:uri(path.join(root,'node_modules','package.json')), + links:()=>provider.provideDocumentLinks(doc,{isCancellationRequested:false}),setExists:value=>{exists=value;},calls:()=>calls, + dispose:()=>subscriptions.forEach(s=>s.dispose())}; +} +test('external create/change/delete refresh native diagnostics without editing the source document',async()=>{ + const f=fixture();try{ + await f.links();assert.equal(f.values.get(f.doc.uri.toString())[0].code,'missing_file'); + await f.links();assert.equal(f.calls(),1,'Unchanged source reuses the current analysis'); + f.events.create(f.generated);await f.links();assert.equal(f.calls(),1,'Generated tree churn does not invalidate the cache'); + f.setExists(true);f.events.create(f.target);assert.equal((await f.links()).length,1);assert.deepEqual(f.values.get(f.doc.uri.toString()),[]); + f.events.change(f.target);await f.links();assert.equal(f.calls(),3); + f.setExists(false);f.events.delete(f.target);await f.links();assert.equal(f.values.get(f.doc.uri.toString())[0].code,'missing_file'); + assert.equal(f.doc.version,1,'External target updates do not mutate the source draft'); + }finally{f.dispose();} +}); +test('a filesystem invalidation discards a late analysis result',async()=>{ + const f=fixture();try{ + let resolve;f.bridge.callMcpTool=()=>new Promise(done=>{resolve=done;}); + const pending=f.links();f.events.change(f.target);resolve(f.result()); + assert.deepEqual(await pending,[]);assert.equal(f.values.size,0,'Late diagnostics cannot reappear'); + f.setExists(true);f.bridge.callMcpTool=async()=>f.result();assert.equal((await f.links()).length,1); + }finally{f.dispose();} +}); diff --git a/website/MOTION.md b/website/MOTION.md new file mode 100644 index 00000000..dba39e45 --- /dev/null +++ b/website/MOTION.md @@ -0,0 +1,25 @@ +# Website motion and offline verification + +The existing Apple layout, typography, themes and genuine recordings remain the source of the site. `tools/build-motion-pages.mjs` adds the shared motion assets to all 50 HTML pages and generates six localized scroll chapters on the four homepages. Components reveal on entry, cards respond to the pointer, controls respond to hover/press/focus, and the sticky product window follows scroll position in both directions. + +`public/assets/motion.js` uses native browser observers and animation frames. At most two video elements are kept, seeking is coalesced, and offscreen work stops when it settles. Background tabs pause motion. Reduced motion exposes every chapter without requesting video; disabling JavaScript leaves readable content and working links. Videos stay muted and paused: scroll position selects the displayed frame. No brightness filter covers the software interface. + +Build with the already prepared dependency tree: + +```sh +npm run verify:release --prefix website +``` + +The build does not install packages or download assets. If FFmpeg is already on PATH, `tools/build-scroll-media.mjs` creates six VP9 derivatives in ignored `dist/motion-clips/`. Original MP4s remain unchanged. Source and output hashes permit reuse. Without FFmpeg the existing MP4 sources and poster fallback remain usable; do not add an installation step to the build. Media derivatives never enter the software commit. + +The six historical recordings are natively 1280×800; converting their codec does not increase their detail. New V0.0.5 incremental recordings are natively 2560×1600 and remain in the local production directory. Do not label the historical footage as 2K or 4K. + +Run actual browser verification with the existing Playwright installation: + +```sh +NODE_PATH=../ui-tests/node_modules READMD_MOTION_CHANNEL=chrome node tools/motion-smoke.cjs +``` + +On Windows, set those environment variables in PowerShell and use `msedge` for the installed Edge. `READMD_MOTION_WEBKIT=1` adds WebKit on hosts with a working VP9 decoder; the prepared Windows WebKit could not decode these clips, so its video test is not marked as passing. This does not verify macOS Safari. `READMD_MOTION_TEST_MP4=1` also aborts WebM requests to verify actual MP4 fallback in branded Chrome/Edge. `READMD_WEBSITE_MOTION_REPORT` optionally writes evidence and screenshots. Tests cover four languages, six chapters, reverse and exact-boundary seeking, narrow layouts, dark mode, repeated scrolling, failed media, reduced motion and JavaScript disabled. CI uses the prepared Chrome browser; codec support differs between bundled Chromium and branded browsers. + +Public version/download metadata continues to use `release.json.stable` until the candidate is officially published. Deploying this site is a separate step from building or verifying it. diff --git a/website/package.json b/website/package.json index 328a208a..fb785989 100644 --- a/website/package.json +++ b/website/package.json @@ -5,9 +5,10 @@ "scripts": { "clean": "node tools/clean.mjs", "build:css": "tailwindcss -i ./src/site.css -o ./dist/assets/site.css --minify", - "build": "npm run clean && npm run build:css && node tools/copy-static.mjs && node tools/sync-version.mjs", + "build": "node tools/build-integrations.mjs && node tools/build-motion-pages.mjs && npm run clean && npm run build:css && node tools/copy-static.mjs && node tools/sync-version.mjs && node tools/build-scroll-media.mjs", "verify": "node tools/validate-website.mjs", "verify:themes": "node tools/theme-smoke.cjs", + "verify:motion": "node tools/motion-smoke.cjs", "verify:release": "npm run build && node tools/validate-website.mjs --release", "indexnow": "node tools/indexnow-submit.mjs https://rust.readmd.asia", "deploy": "npm run verify:release && npx wrangler deploy --config wrangler.worker.jsonc" diff --git a/website/public/404.html b/website/public/404.html index ab5bbfa0..0ea02f4d 100644 --- a/website/public/404.html +++ b/website/public/404.html @@ -8,6 +8,7 @@ +
@@ -30,5 +31,5 @@

Choose your language

- + diff --git a/website/public/assets/motion.css b/website/public/assets/motion.css new file mode 100644 index 00000000..abb73311 --- /dev/null +++ b/website/public/assets/motion.css @@ -0,0 +1,56 @@ +/* Compositor motion for the existing Apple visual language. */ +html{scroll-behavior:smooth;scroll-padding-top:76px} +.motion-item{--motion-parallax:0px;--motion-lift:0px;--motion-reveal-y:0px;--motion-scale:1;translate:0 calc(var(--motion-parallax) + var(--motion-lift) + var(--motion-reveal-y));scale:var(--motion-scale)} +.motion-ready .motion-item{--motion-reveal-y:22px;opacity:0;transition:opacity .72s cubic-bezier(.16,1,.3,1),translate .95s cubic-bezier(.16,1,.3,1),scale .65s cubic-bezier(.16,1,.3,1);transition-delay:var(--motion-delay,0ms)} +.motion-ready .motion-item.motion-seen{--motion-reveal-y:0px;opacity:1} +.motion-ready h1.motion-item{clip-path:inset(0 0 100%);transition:clip-path 1.1s cubic-bezier(.16,1,.3,1),opacity .8s,translate 1s} +.motion-ready h2.motion-item{clip-path:inset(0 100% 0 0);transition:clip-path 1s cubic-bezier(.16,1,.3,1),opacity .8s,translate 1s} +.motion-ready :is(h1,h2).motion-item.motion-seen{clip-path:inset(0)} +.motion-ready .motion-item:focus-within{opacity:1;--motion-reveal-y:0px} +.motion-ready .hero{position:relative;isolation:isolate} +.motion-ready .hero::before{content:"";position:absolute;z-index:-1;inset:5% 0 25%;border-radius:50%;background:radial-gradient(ellipse at 30% 40%,rgba(0,113,227,.11),transparent 58%),radial-gradient(ellipse at 75% 65%,rgba(140,90,245,.08),transparent 55%);filter:blur(24px);animation:readmd-aurora 16s ease-in-out infinite alternate;pointer-events:none} +.home-title-phrase{background:linear-gradient(110deg,var(--color-action),#8863d4 48%,var(--color-action));background-size:200% 100%;background-clip:text;-webkit-background-clip:text;color:transparent;animation:readmd-title-light 12s ease-in-out infinite alternate} +.motion-ready .nav-shell{transition:background-color .4s,box-shadow .4s;border-bottom-color:color-mix(in srgb,var(--color-action) calc(var(--motion-page-progress,0)*50%),var(--color-line))} +.motion-interactive{position:relative;isolation:isolate} +.motion-interactive::after{content:"";position:absolute;inset:0;pointer-events:none;border-radius:inherit;padding:1px;background:radial-gradient(280px circle at var(--motion-pointer-x,50%) var(--motion-pointer-y,50%),color-mix(in srgb,var(--color-action) 48%,transparent),transparent 70%);mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);mask-composite:exclude;opacity:0;transition:opacity .4s} +.motion-interactive:hover::after{opacity:1} +@media(hover:hover) and (prefers-reduced-motion:no-preference){ + .motion-seen:is(.demo-card,.platform-card,.apple-platform-card,article):hover{--motion-lift:-6px;--motion-scale:1.008;transition-delay:0ms} + a:is(.button-primary,.button-secondary,.apple-pill-primary,.apple-pill-secondary),button{transition:scale .25s cubic-bezier(.16,1,.3,1),box-shadow .3s,background-color .3s} + a:is(.button-primary,.button-secondary,.apple-pill-primary,.apple-pill-secondary):hover,button:hover{scale:1.035} +} +a:is(.button-primary,.button-secondary,.apple-pill-primary,.apple-pill-secondary):active,button:active{scale:.965} +a:focus-visible,button:focus-visible,summary:focus-visible{outline:2px solid var(--color-action);outline-offset:5px} +.link-action span[aria-hidden]{display:inline-block;transition:translate .35s cubic-bezier(.16,1,.3,1)} +.link-action:hover span[aria-hidden]{translate:4px 0} +.journey{height:auto;background:var(--home-bg,var(--color-card));isolation:isolate} +.journey-sticky{position:relative;min-height:auto;overflow:clip;padding-block:80px} +.journey-grid{max-width:1380px;grid-template-columns:minmax(260px,.7fr) minmax(0,1.3fr);padding:48px;gap:44px} +.journey-heading{text-wrap:balance;max-width:12ch} +.journey-caption-stage{display:grid;gap:24px;min-height:180px} +.journey-caption{position:relative;inset:auto;opacity:1;pointer-events:auto;transform:none;transition:opacity .45s cubic-bezier(.16,1,.3,1),transform .65s cubic-bezier(.16,1,.3,1)} +.motion-ready .journey-caption-stage{gap:0} +.motion-ready .journey-caption{grid-area:1/1;opacity:0;pointer-events:none;transform:translateY(22px)} +.motion-ready .journey-caption.is-active{opacity:1;pointer-events:auto;transform:translateY(0)} +.journey-caption .link-action{display:inline-flex;min-height:44px;align-items:center;margin-top:16px;font-size:14px} +.journey-stage{aspect-ratio:8/5;background:var(--color-surface);border:1px solid var(--color-line);border-radius:24px;overflow:hidden;box-shadow:0 28px 80px -28px rgba(0,0,0,.22);transform:perspective(1500px) translateY(var(--cinema-lift,0px)) rotateY(var(--cinema-rotate,0deg)) scale(var(--cinema-scale,1));transform-origin:center;will-change:transform} +.journey-stage>img,.journey-stage>video{display:block;position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:var(--color-surface)} +.journey-stage>video{opacity:0;pointer-events:none} +.journey-stage>video.cinema-visible.cinema-decoded{opacity:1} +.journey-progress{height:3px;left:0;right:0;bottom:0;background:var(--color-line);z-index:3} +.journey-progress span{background:linear-gradient(90deg,var(--color-action),#8863d4);transform:scaleX(var(--cinema-progress,0))} +.journey-aura{position:absolute;inset:18% -10%;z-index:-1;background:radial-gradient(ellipse,hsl(var(--cinema-hue,205) 80% 70%/.16),transparent 65%);filter:blur(60px);transform:translateX(calc((var(--cinema-progress,0) - .5)*40px));pointer-events:none} +.motion-ready .journey{height:480vh} +.motion-ready .journey-sticky{position:sticky;top:56px;min-height:calc(100svh - 56px);padding-block:0} +.home-logo{display:inline-flex;align-items:center;gap:9px;min-height:44px} +.home-logo img{width:26px;height:26px;flex-shrink:0} +.motion-paused *,.motion-paused *::before,.motion-paused *::after{animation-play-state:paused!important} +@keyframes readmd-aurora{from{transform:translate3d(-2%,0,0) scale(.96)}to{transform:translate3d(3%,5%,0) scale(1.05)}} +@keyframes readmd-title-light{from{background-position:0% 50%}to{background-position:100% 50%}} +@media(max-width:1023px){.journey-grid{grid-template-columns:1fr;gap:18px;padding:28px 24px;max-width:860px}.journey-heading{max-width:22ch;font-size:30px}.journey-caption-stage{margin-top:16px;min-height:130px}.journey-caption strong{font-size:21px}.journey-caption p{font-size:15px;max-width:60ch}.journey-stage{width:100%;max-height:45svh}.motion-ready .journey{height:400vh}} +@media(max-width:560px){.journey-grid{padding:18px 20px;gap:12px}.journey-heading{font-size:26px}.journey-caption-stage{min-height:158px}.journey-kicker{font-size:11px}.journey-stage{border-radius:14px}.journey-caption .link-action{margin-top:6px}.journey-aura{filter:blur(32px)}} +@media(max-width:380px){.readmd-home .home-logo{font-size:0;gap:0}} +@media(prefers-reduced-motion:reduce){ + html{scroll-behavior:auto}.motion-item{opacity:1!important;translate:none!important;scale:1!important;transition:none!important}.hero::before,.journey-aura{display:none}.home-title-phrase{animation:none;background:none;color:var(--color-action)} + .journey,.motion-ready .journey{height:auto}.journey-sticky,.motion-ready .journey-sticky{position:relative;top:0;min-height:auto;padding-block:48px}.journey-stage{transform:none;will-change:auto}.journey-stage>video{display:none}.journey-progress{display:none}.journey-caption-stage{display:grid;gap:24px}.journey-caption{grid-area:auto;position:relative;opacity:1;transform:none;pointer-events:auto;transition:none} +} diff --git a/website/public/assets/motion.js b/website/public/assets/motion.js new file mode 100644 index 00000000..b389379a --- /dev/null +++ b/website/public/assets/motion.js @@ -0,0 +1,194 @@ +/* Native browser motion. Existing videos, no frame archives or animation runtime. */ +(() => { + 'use strict'; + const root = document.documentElement; + const reduced = matchMedia('(prefers-reduced-motion: reduce)'); + const finePointer = matchMedia('(hover: hover) and (pointer: fine)'); + const clamp = (v, lo = 0, hi = 1) => Math.min(hi, Math.max(lo, v)); + const items = new Set(), visible = new Set(); + const selector = 'main h1,main h2,main h3,main p,main article,main pre,main details,main button,main summary,main input,main select,main .demo-card,main .product-frame,main .platform-card,main .apple-platform-card,main .button-primary,main .button-secondary,main .apple-pill-primary,main .apple-pill-secondary,footer'; + let raf = 0, last = 0, progress = 0, pageProgress = 0; + const journey = document.querySelector('[data-readmd-cinema]'); + const stage = journey?.querySelector('.journey-stage'); + const captions = [...(journey?.querySelectorAll('.journey-caption') || [])]; + const poster = stage?.querySelector('img'); + const clips = captions.map(c => ({video: c.dataset.video, webm: c.dataset.webm, poster: c.dataset.poster})); + let journeyTop = 0, journeyRange = 1, journeyVisible = false, active = -1; + const slots = []; + + const wake = () => { if (!raf && !document.hidden && !reduced.matches) raf = requestAnimationFrame(tick); }; + const measure = () => { + if (journey) { + journeyTop = journey.getBoundingClientRect().top + scrollY; + journeyRange = Math.max(1, journey.offsetHeight - innerHeight + 56); + } + wake(); + }; + const observer = new IntersectionObserver(entries => { + for (const entry of entries) { + if (entry.isIntersecting) { + visible.add(entry.target); entry.target.classList.add('motion-seen'); + } else visible.delete(entry.target); + } + wake(); + }, {rootMargin: '0px 0px 40px', threshold: 0.08}); + function enroll(scope) { + const found = [...(scope.matches?.(selector) ? [scope] : []), ...scope.querySelectorAll(selector)]; + for (const node of found) { + if (items.has(node) || node.closest('[data-readmd-cinema],dialog,[role="dialog"]')) continue; + // Large generated galleries remain bounded; unanimated content stays visible. + if (items.size >= 512) break; + items.add(node); node.classList.add('motion-item'); + node.style.setProperty('--motion-delay', (items.size % 4) * 45 + 'ms'); + observer.observe(node); + } + } + + function present(slot) { + const video = slot.video; + if (!slot.visible || video.readyState < 2 || video.seeking) return; + video.classList.add('cinema-decoded'); + stage.dataset.mediaReady = 'true'; + stage.dataset.presentedTime = video.currentTime.toFixed(3); + } + function seek(slot) { + const video = slot.video; + if (reduced.matches || document.hidden || !slot.visible || video.readyState < 1 || video.seeking) return; + const target = clamp(slot.target, 0, Math.max(0, video.duration - 0.045)); + if (Math.abs(video.currentTime - target) > 0.018) { + try { video.currentTime = target; } catch { /* Poster remains usable. */ } + } else present(slot); + } + function createSlot() { + const video = document.createElement('video'); + video.muted = true; video.playsInline = true; video.preload = 'metadata'; + video.setAttribute('aria-hidden', 'true'); video.tabIndex = -1; + const slot = {video, index: -1, target: 0, visible: false}; + const decoded = () => { + if (!slot.visible) return; + present(slot); + seek(slot); + }; + video.addEventListener('loadedmetadata', () => { seek(slot); wake(); }); + video.addEventListener('loadeddata', decoded); + video.addEventListener('seeked', decoded); + video.addEventListener('error', () => { + video.classList.remove('cinema-decoded'); + if (slot.visible) stage.dataset.mediaReady = 'poster'; + }); + stage.append(video); slots.push(slot); return slot; + } + function loadSlot(slot, index) { + if (slot.index === index) return; + slot.index = index; slot.target = 0; + slot.video.classList.remove('cinema-decoded'); + slot.video.removeAttribute('src'); + slot.video.replaceChildren(...[[clips[index].webm,'video/webm'],[clips[index].video,'video/mp4']] + .filter(([src]) => src).map(([src,type]) => { + const source = document.createElement('source'); source.src = src; source.type = type; return source; + })); + slot.video.load(); + } + function updateCinema(p) { + if (!journey || !journeyVisible || reduced.matches || !clips.length) return; + const scaled = clamp(p) * clips.length; + const index = Math.min(clips.length - 1, Math.floor(scaled)); + const local = clamp(scaled - index); + let slot = slots.find(s => s.index === index); + if (!slot) { + slot = slots.find(s => !s.visible) || (slots.length < 2 ? createSlot() : slots[0]); + loadSlot(slot, index); + } + if (active !== index) { + active = index; + stage.dataset.clipIndex = String(index); stage.dataset.mediaReady = 'poster'; + poster.src = clips[index].poster; + for (let n = 0; n < captions.length; n++) { + captions[n].classList.toggle('is-active', n === index); + captions[n].inert = n !== index; + captions[n].setAttribute('aria-hidden', String(n !== index)); + } + } + for (const other of slots) { + other.visible = other === slot; + other.video.classList.toggle('cinema-visible', other.visible); + } + const video = slot.video; + slot.target = Number.isFinite(video.duration) ? local * Math.max(0, video.duration - 0.045) : 0; + stage.dataset.targetTime = slot.target.toFixed(3); + seek(slot); + // Warm only the adjacent clip; never decode six simultaneous videos. + if (index + 1 < clips.length && local > 0.65 && !slots.some(s => s.index === index + 1)) { + const next = slots.find(s => s !== slot) || createSlot(); loadSlot(next, index + 1); + } + const focus = Math.sin(local * Math.PI); + stage.style.setProperty('--cinema-scale', (0.965 + focus * 0.035).toFixed(4)); + stage.style.setProperty('--cinema-rotate', ((0.5 - local) * 1.8).toFixed(3) + 'deg'); + stage.style.setProperty('--cinema-lift', (-focus * 12).toFixed(2) + 'px'); + journey.style.setProperty('--cinema-progress', p.toFixed(5)); + journey.style.setProperty('--cinema-hue', (205 + p * 65).toFixed(2)); + journey.dataset.motionProgress = p.toFixed(5); + } + function tick(time) { + raf = 0; + if (document.hidden || reduced.matches) return; + const dt = Math.min(48, last ? time - last : 16.7); last = time; + const target = journey ? clamp((scrollY - journeyTop + 56) / journeyRange) : 0; + const mix = 1 - Math.exp(-dt / 90); + progress += (target - progress) * mix; + const targetPage = clamp(scrollY / Math.max(1, document.documentElement.scrollHeight - innerHeight)); + pageProgress += (targetPage - pageProgress) * mix; + // Read layout together before setting compositor-only properties. + const measurements = [...visible].map(node => [node, node.getBoundingClientRect()]); + root.style.setProperty('--motion-page-progress', pageProgress.toFixed(5)); + for (const [node, rect] of measurements) { + if (node.matches('.product-frame,.demo-card,.apple-platform-card')) { + const depth = clamp((innerHeight / 2 - rect.top - rect.height / 2) / innerHeight, -0.5, 0.5); + node.style.setProperty('--motion-parallax', (depth * 18).toFixed(2) + 'px'); + } + } + updateCinema(progress); + if (Math.abs(target - progress) > 0.00015 || Math.abs(targetPage - pageProgress) > 0.00015) wake(); + } + const mediaObserver = journey && new IntersectionObserver(entries => { + journeyVisible = entries.some(e => e.isIntersecting); + if (journeyVisible) { measure(); updateCinema(progress); } + wake(); + }, {rootMargin: '400px 0px'}); + mediaObserver?.observe(journey); + enroll(document); + new MutationObserver(records => { + for (const record of records) for (const node of record.addedNodes) { + if (node.nodeType === 1 && !node.closest('[data-readmd-cinema]')) enroll(node); + } + }).observe(document.querySelector('main') || document.body, {childList: true, subtree: true}); + + document.addEventListener('pointermove', event => { + if (!finePointer.matches || reduced.matches) return; + const card = event.target.closest?.('.demo-card,.platform-card,.apple-platform-card,main article'); + if (!card) return; + const rect = card.getBoundingClientRect(); + card.classList.add('motion-interactive'); + card.style.setProperty('--motion-pointer-x', clamp((event.clientX - rect.left) / rect.width) * 100 + '%'); + card.style.setProperty('--motion-pointer-y', clamp((event.clientY - rect.top) / rect.height) * 100 + '%'); + }, {passive: true}); + addEventListener('scroll', wake, {passive: true}); + addEventListener('resize', measure, {passive: true}); + new ResizeObserver(measure).observe(document.querySelector('main') || document.body); + document.addEventListener('visibilitychange', () => { + root.classList.toggle('motion-paused', document.hidden); + if (document.hidden) { cancelAnimationFrame(raf); raf = 0; last = 0; } + else wake(); + }); + const preference = () => { + root.classList.toggle('motion-ready', !reduced.matches); + for (const caption of captions) { + caption.inert = !reduced.matches && !caption.classList.contains('is-active'); + caption.setAttribute('aria-hidden', String(caption.inert)); + } + if (reduced.matches) { cancelAnimationFrame(raf); raf = 0; } + measure(); + }; + reduced.addEventListener('change', preference); + preference(); +})(); diff --git a/website/public/bibtex-citations/index.html b/website/public/bibtex-citations/index.html index 668abdfb..494fa83b 100644 --- a/website/public/bibtex-citations/index.html +++ b/website/public/bibtex-citations/index.html @@ -187,6 +187,7 @@ } ] } +