diff --git a/.agents/notes/implemented/feature/2026-11-01-markdown-offline-chemistry-and-subscript.md b/.agents/notes/implemented/feature/2026-11-01-markdown-offline-chemistry-and-subscript.md
new file mode 100644
index 000000000..96de4d430
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-11-01-markdown-offline-chemistry-and-subscript.md
@@ -0,0 +1,69 @@
+# Agent 笔记:Markdown 离线化学公式与下标
+
+状态:已实现
+
+## 先说结论
+
+macOS 已有数学公式和 Mermaid 图表的离线引擎,不需要再造解析器或接在线服务。
+本次补上 KaTeX 的化学扩展 mhchem,使 `\ce{...}` 反应式和 `\pu{...}` 物理单位可用,
+并启用 Comrak 现成的下标扩展。macOS PlantUML 已由独立改动接入,见
+[离线 PlantUML 预览](2026-11-01-macos-offline-plantuml.md);Windows 方言对齐等仍未完成,
+不能因这些 macOS 增强而把 Issue #706 当作整体完成。
+
+## 问题
+
+KaTeX 是把 LaTeX 数学语法转换为网页公式的成熟库,但基础库不包含 mhchem,
+后者是负责解析化学反应和单位的可选扩展。只打包基础库时,数学公式能显示,
+`\ce{H2O}` 却报未定义命令。共享 Markdown 解析器 Comrak 已支持上下标,
+此前只启用了上标,`H~2~O` 不能正确输出下标。
+
+## 决策
+
+- 从同版本 KaTeX 0.16.0 npm 分发包提取 mhchem,先验证整个归档的 SHA-512 完整性,
+ 再固定脚本 SHA-256,保留源文件中被压缩分发去掉的版权和适配声明。
+- 模板按 KaTeX、mhchem、预览脚本顺序加载。现有公式渲染器继续使用 `trust: false`,
+ 不接受公式引入脚本、网页或网络资源;运行时不下载任何依赖。
+- 开启 Comrak 的 `subscript`。单波浪线表示下标,双波浪线仍表示删除线,
+ 代码中的字面语法保持原文。正确例子是 `H~2~O`,不要用正则替换全文中的 `~`,
+ 否则代码、删除线和公式会被误改。
+- 共享输入加真实 WebKit 浏览器引擎回归,同时检查数学、化学、Mermaid 生成的 SVG、
+ 脚注及回链,不能仅断言代码块带有语言类名就声称图表可用。
+
+## 考虑过的备选方案
+
+- 重新实现化学语法:已有与当前 KaTeX 完全匹配的成熟上游,不采用。
+- 运行时从 CDN 加载:离线失败且扩大网络和供应链边界,不采用。
+- 顺手升级 KaTeX、Mermaid 或把 Windows 解析器整体迁移:增加无关兼容性风险,
+ 本批只填已证实缺口,不把两个平台现状混为一致。
+- 直接接入在线 PlantUML 服务器:会把用户文档发送到第三方,也不能离线,不采用。
+ PlantUML 的独立上游、分发和执行隔离由上述独立决策验证,不复用公式渲染的执行上下文。
+
+## 后果
+
+数学公式、化学反应、单位和上下标可以在无网络预览中共存,新增脚本与已有 KaTeX 版本绑定。
+代价是需要随 KaTeX 更新一起核对扩展、许可、来源哈希及真实引擎回归。
+GitHub 特有媒体及 Windows 方言对齐不是本批已交付能力;macOS PlantUML 的能力和限制以独立笔记为准。
+
+这些模板和第三方脚本是 Git 输入,构建复制后再签名,运行时只读加载。
+渲染状态仅在内存和非持久 WebKit 中存活,不新增缓存、下载或安装目录写入;
+资源复用清单显式排除整个 Markdown 预览资源,不跨工作树复制生成 bundle。
+这不改变代码签名或 Sparkle 增量更新所依赖的发布字节。
+
+## 验证
+
+- `cargo test --manifest-path rust/Cargo.toml -p lithe-core project::markdown::tests`。
+- `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter MarkdownPreviewRichContentTests`:
+ 深浅主题使用生产 Rust bridge 和打包模板,检查公式与可访问数学标记 MathML、图表、脚注和代码,
+ 并比较运行前后全部预览文件 SHA-256,检查没有远程资源请求。
+- `node --test scripts/resources/test-reuse-worktree-resources.mjs`:拒绝跨工作树复制预览资源。
+- `./scripts/macos/verify/verify-runtime-bundle-immutability.sh`。
+
+原生组件测试不代替完整应用操作验收,Windows 运行时未在本批验证。
+
+## 适用范围
+
+- `rust/lithe-core/src/project/markdown.rs`
+- `macos/Sources/Lithe/Resources/MarkdownPreview/`
+- `macos/Tests/LitheTests/MarkdownPreviewRichContentTests.swift`
+- `shared/fixtures/editor/markdown-rich-content-v1.json`
+- `scripts/resources/worktree-resources.json`
diff --git a/docs/ci-builds.md b/docs/ci-builds.md
index 8ccddbf0b..fca45f68b 100644
--- a/docs/ci-builds.md
+++ b/docs/ci-builds.md
@@ -604,6 +604,13 @@ Inter 4.1 的 18 个静态 OTF(内部版本 4.001)及许可、JetBrains Mono
以下目录不应直接复制或跨工作树共享:
+- Markdown 离线预览的模板、脚本、样式、字体和许可来自 Git 跟踪的
+ `macos/Sources/Lithe/Resources/MarkdownPreview/`。KaTeX 0.16.0 的 mhchem 扩展与现有
+ KaTeX 版本匹配,npm 归档完整性及文件 SHA-256 固定在 `THIRD_PARTY_NOTICES.md`。
+ 输入与平台架构、工具链无关;构建时从当前 checkout 复制进 bundle 后签名,运行时仅只读加载,
+ 不下载、不写回、不产生持久化渲染缓存,不影响签名和 Sparkle delta。生成 bundle 没有可靠的
+ 跨 worktree identity stamp,`excludedResources.markdown-preview-assets` 走资源脚本的
+ 通用拒绝路由,任何复制阶段都不得从其他工作树或安装 app 复用这些资源。
- macOS Markdown PlantUML:Git 中的 `MarkdownPreview/vendor/plantuml/`、
`plantuml.html` 与 `plantuml-renderer.js` 随当前 checkout 打包,来源为固定的
MIT `@plantuml/core` 1.2026.9,归档 SHA-512、引擎 SHA-256 和上游 revision 见
diff --git a/macos/Sources/Lithe/Resources/MarkdownPreview/THIRD_PARTY_NOTICES.md b/macos/Sources/Lithe/Resources/MarkdownPreview/THIRD_PARTY_NOTICES.md
index c117280d3..7a6c801a4 100644
--- a/macos/Sources/Lithe/Resources/MarkdownPreview/THIRD_PARTY_NOTICES.md
+++ b/macos/Sources/Lithe/Resources/MarkdownPreview/THIRD_PARTY_NOTICES.md
@@ -4,6 +4,7 @@ All preview dependencies are bundled with Lithe and loaded from the application
- Highlight.js 11.5.1 — BSD-3-Clause — `vendor/highlight-LICENSE`
- KaTeX 0.16.0 and its fonts — MIT — `vendor/katex-LICENSE`
+- KaTeX 0.16.0 mhchem extension (mhchem 3.3.0 adaptation) — MIT / upstream Apache-2.0 notice — `vendor/mhchem-NOTICE`
- Mermaid 11.14.0 — MIT — `vendor/mermaid-LICENSE`
- D3 6.7.0 — ISC — `vendor/d3-LICENSE`
- markmap-lib 0.14.1 — MIT — `vendor/markmap-lib-LICENSE`
@@ -12,6 +13,8 @@ All preview dependencies are bundled with Lithe and loaded from the application
The Highlight.js browser bundle is the distribution already vendored by MiaoYan; its version banner and upstream license are preserved. The remaining files come from the matching npm distributions.
+`vendor/mhchem.min.js` comes from `https://registry.npmjs.org/katex/-/katex-0.16.0.tgz` (`package/dist/contrib/mhchem.min.js`), matching the bundled KaTeX runtime. The archive is verified with npm's SHA-512 integrity `sha512-wPRB4iUPysfH97wTgG5/tRLYxmKVq6Q4jRAWRVOUxXB1dsiv4cvcNjqabHkrOvJHM1Bpk3WrgmllSO1vIvP24w==`; the extracted file's SHA-256 is `5c0a121a8b490afc85860a522347aeb34fb508c6b23044e5d29f6b2194227b51`. The source copyright and adaptation notice are preserved separately because the minified distribution omits that header. This is a checked-in build input, never downloaded or modified at runtime.
+
PlantUML uses the official JavaScript build's built-in Smetana layout engine. Graphviz/Viz.js, the URL encoder, optional standard libraries, themes, emoji and external include loaders are not bundled. Rendering is local in a nonpersistent, restricted WebView; the result is displayed as an SVG image, never injected into the document DOM.
`vendor/plantuml/plantuml.js` comes unchanged from `https://registry.npmjs.org/@plantuml/core/-/core-1.2026.9.tgz` (`package/plantuml.js`, upstream Git revision `18d0aae311a4f1eb3723bb4b0ad939185e22f166`). npm archive integrity: `sha512-Qy46og9hEb+wOFtjUfniWkl/BDwHpDuebQX5rF+35s9PS2G4cKZf1iWw4E/ELLQ0pTkESPDoQTbKWcc3ZyVcVA==`. Script SHA-256: `21c049ce47d087eff61ae0836f9c62a2b0c0d2528a5f16c2247057544228fae7`. The npm release's MIT license and upstream notices are preserved in `vendor/plantuml/LICENSE` (line endings normalized to LF only). Versions before 1.2026.6 use different licensing and must not be substituted. These are Git-tracked build inputs, not runtime downloads or writable bundle state.
diff --git a/macos/Sources/Lithe/Resources/MarkdownPreview/index.html b/macos/Sources/Lithe/Resources/MarkdownPreview/index.html
index 19e8eb08a..1d34c85e4 100644
--- a/macos/Sources/Lithe/Resources/MarkdownPreview/index.html
+++ b/macos/Sources/Lithe/Resources/MarkdownPreview/index.html
@@ -10,6 +10,7 @@
+
diff --git a/macos/Sources/Lithe/Resources/MarkdownPreview/vendor/mhchem-NOTICE b/macos/Sources/Lithe/Resources/MarkdownPreview/vendor/mhchem-NOTICE
new file mode 100644
index 000000000..96a2706ac
--- /dev/null
+++ b/macos/Sources/Lithe/Resources/MarkdownPreview/vendor/mhchem-NOTICE
@@ -0,0 +1,279 @@
+KaTeX mhchem extension, distributed with KaTeX 0.16.0
+
+Upstream source notice (preserved from contrib/mhchem/mhchem.js):
+
+/* eslint-disable */
+/* -*- Mode: Javascript; indent-tabs-mode:nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+
+/*************************************************************
+ *
+ * KaTeX mhchem.js
+ *
+ * This file implements a KaTeX version of mhchem version 3.3.0.
+ * It is adapted from MathJax/extensions/TeX/mhchem.js
+ * It differs from the MathJax version as follows:
+ * 1. The interface is changed so that it can be called from KaTeX, not MathJax.
+ * 2. \rlap and \llap are replaced with \mathrlap and \mathllap.
+ * 3. Four lines of code are edited in order to use \raisebox instead of \raise.
+ * 4. The reaction arrow code is simplified. All reaction arrows are rendered
+ * using KaTeX extensible arrows instead of building non-extensible arrows.
+ * 5. \tripledash vertical alignment is slightly adjusted.
+ *
+ * This code, as other KaTeX code, is released under the MIT license.
+ *
+ * /*************************************************************
+ *
+ * MathJax/extensions/TeX/mhchem.js
+ *
+ * Implements the \ce command for handling chemical formulas
+ * from the mhchem LaTeX package.
+ *
+ * ---------------------------------------------------------------------
+ *
+ * Copyright (c) 2011-2015 The MathJax Consortium
+ * Copyright (c) 2015-2018 Martin Hensel
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+KaTeX license:
+
+The MIT License (MIT)
+
+Copyright (c) 2013-2020 Khan Academy and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+Apache License 2.0 text for the upstream MathJax adaptation:
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or
+ Object form, that is based on (or derived from) the Work and
+ for which the editorial revisions, annotations, elaborations,
+ or other modifications represent, as a whole, an original work
+ of authorship. For the purposes of this License, Derivative Works
+ shall not include works that remain separable from, or merely link
+ (or bind by name) to the interfaces of, the Work and Derivative Works
+ thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of
+ its distribution, then any Derivative Works that You
+ distribute must include a readable copy of the attribution
+ notices contained within such NOTICE file, excluding those
+ notices that do not pertain to any part of the Derivative
+ Works, in at least one of the following places: within a
+ NOTICE text file distributed as part of the Derivative Works;
+ within the Source form or documentation, if provided along with
+ the Derivative Works; or, within a display generated by the
+ Derivative Works, if and wherever such third-party notices
+ normally appear. The contents of the NOTICE file are for
+ informational purposes only and do not modify the License.
+ You may add Your own attribution notices within Derivative
+ Works that You distribute, alongside or as an addendum to
+ the NOTICE text from the Work, provided that such additional
+ attribution notices cannot be construed as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing
+ the origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as
+ a result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[ ]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/macos/Sources/Lithe/Resources/MarkdownPreview/vendor/mhchem.min.js b/macos/Sources/Lithe/Resources/MarkdownPreview/vendor/mhchem.min.js
new file mode 100644
index 000000000..84e1198de
--- /dev/null
+++ b/macos/Sources/Lithe/Resources/MarkdownPreview/vendor/mhchem.min.js
@@ -0,0 +1 @@
+!function(t,e){if("object"==typeof exports&&"object"==typeof module)module.exports=e(require("katex"));else if("function"==typeof define&&define.amd)define(["katex"],e);else{var n="object"==typeof exports?e(require("katex")):e(t.katex);for(var o in n)("object"==typeof exports?exports:t)[o]=n[o]}}("undefined"!=typeof self?self:this,(function(t){return function(){"use strict";var e={771:function(e){e.exports=t}},n={};function o(t){var a=n[t];if(void 0!==a)return a.exports;var r=n[t]={exports:{}};return e[t](r,r.exports,o),r.exports}o.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return o.d(e,{a:e}),e},o.d=function(t,e){for(var n in e)o.o(e,n)&&!o.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},o.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)};var a={};return function(){var t=o(771),e=o.n(t);e().__defineMacro("\\ce",(function(t){return n(t.consumeArgs(1)[0],"ce")})),e().__defineMacro("\\pu",(function(t){return n(t.consumeArgs(1)[0],"pu")})),e().__defineMacro("\\tripledash","{\\vphantom{-}\\raisebox{2.56mu}{$\\mkern2mu\\tiny\\text{-}\\mkern1mu\\text{-}\\mkern1mu\\text{-}\\mkern2mu$}}");var n=function(t,e){for(var n="",o=t.length&&t[t.length-1].loc.start,i=t.length-1;i>=0;i--)t[i].loc.start>o&&(n+=" ",o=t[i].loc.start),n+=t[i].text,o+=t[i].text.length;return r.go(a.go(n,e))},a={go:function(t,e){if(!t)return[];void 0===e&&(e="ce");var n,o="0",r={};r.parenthesisLevel=0,t=(t=(t=t.replace(/\n/g," ")).replace(/[\u2212\u2013\u2014\u2010]/g,"-")).replace(/[\u2026]/g,"...");for(var i=10,c=[];;){n!==t?(i=10,n=t):i--;var u=a.stateMachines[e],p=u.transitions[o]||u.transitions["*"];t:for(var s=0;s
0))return c;if(d.revisit||(t=_.remainder),!d.toContinue)break t}}if(i<=0)throw["MhchemBugU","mhchem bug U. Please report."]}},concatArray:function(t,e){if(e)if(Array.isArray(e))for(var n=0;n":/^[=<>]/,"#":/^[#\u2261]/,"+":/^\+/,"-$":/^-(?=[\s_},;\]/]|$|\([a-z]+\))/,"-9":/^-(?=[0-9])/,"- orbital overlap":/^-(?=(?:[spd]|sp)(?:$|[\s,;\)\]\}]))/,"-":/^-/,"pm-operator":/^(?:\\pm|\$\\pm\$|\+-|\+\/-)/,operator:/^(?:\+|(?:[\-=<>]|<<|>>|\\approx|\$\\approx\$)(?=\s|$|-?[0-9]))/,arrowUpDown:/^(?:v|\(v\)|\^|\(\^\))(?=$|[\s,;\)\]\}])/,"\\bond{(...)}":function(t){return a.patterns.findObserveGroups(t,"\\bond{","","","}")},"->":/^(?:<->|<-->|->|<-|<=>>|<<=>|<=>|[\u2192\u27F6\u21CC])/,CMT:/^[CMT](?=\[)/,"[(...)]":function(t){return a.patterns.findObserveGroups(t,"[","","","]")},"1st-level escape":/^(&|\\\\|\\hline)\s*/,"\\,":/^(?:\\[,\ ;:])/,"\\x{}{}":function(t){return a.patterns.findObserveGroups(t,"",/^\\[a-zA-Z]+\{/,"}","","","{","}","",!0)},"\\x{}":function(t){return a.patterns.findObserveGroups(t,"",/^\\[a-zA-Z]+\{/,"}","")},"\\ca":/^\\ca(?:\s+|(?![a-zA-Z]))/,"\\x":/^(?:\\[a-zA-Z]+\s*|\\[_&{}%])/,orbital:/^(?:[0-9]{1,2}[spdfgh]|[0-9]{0,2}sp)(?=$|[^a-zA-Z])/,others:/^[\/~|]/,"\\frac{(...)}":function(t){return a.patterns.findObserveGroups(t,"\\frac{","","","}","{","","","}")},"\\overset{(...)}":function(t){return a.patterns.findObserveGroups(t,"\\overset{","","","}","{","","","}")},"\\underset{(...)}":function(t){return a.patterns.findObserveGroups(t,"\\underset{","","","}","{","","","}")},"\\underbrace{(...)}":function(t){return a.patterns.findObserveGroups(t,"\\underbrace{","","","}_","{","","","}")},"\\color{(...)}0":function(t){return a.patterns.findObserveGroups(t,"\\color{","","","}")},"\\color{(...)}{(...)}1":function(t){return a.patterns.findObserveGroups(t,"\\color{","","","}","{","","","}")},"\\color(...){(...)}2":function(t){return a.patterns.findObserveGroups(t,"\\color","\\","",/^(?=\{)/,"{","","","}")},"\\ce{(...)}":function(t){return a.patterns.findObserveGroups(t,"\\ce{","","","}")},oxidation$:/^(?:[+-][IVX]+|\\pm\s*0|\$\\pm\$\s*0)$/,"d-oxidation$":/^(?:[+-]?\s?[IVX]+|\\pm\s*0|\$\\pm\$\s*0)$/,"roman numeral":/^[IVX]+/,"1/2$":/^[+\-]?(?:[0-9]+|\$[a-z]\$|[a-z])\/[0-9]+(?:\$[a-z]\$|[a-z])?$/,amount:function(t){var e;if(e=t.match(/^(?:(?:(?:\([+\-]?[0-9]+\/[0-9]+\)|[+\-]?(?:[0-9]+|\$[a-z]\$|[a-z])\/[0-9]+|[+\-]?[0-9]+[.,][0-9]+|[+\-]?\.[0-9]+|[+\-]?[0-9]+)(?:[a-z](?=\s*[A-Z]))?)|[+\-]?[a-z](?=\s*[A-Z])|\+(?!\s))/))return{match_:e[0],remainder:t.substr(e[0].length)};var n=a.patterns.findObserveGroups(t,"","$","$","");return n&&(e=n.match_.match(/^\$(?:\(?[+\-]?(?:[0-9]*[a-z]?[+\-])?[0-9]*[a-z](?:[+\-][0-9]*[a-z]?)?\)?|\+|-)\$$/))?{match_:e[0],remainder:t.substr(e[0].length)}:null},amount2:function(t){return this.amount(t)},"(KV letters),":/^(?:[A-Z][a-z]{0,2}|i)(?=,)/,formula$:function(t){if(t.match(/^\([a-z]+\)$/))return null;var e=t.match(/^(?:[a-z]|(?:[0-9\ \+\-\,\.\(\)]+[a-z])+[0-9\ \+\-\,\.\(\)]*|(?:[a-z][0-9\ \+\-\,\.\(\)]+)+[a-z]?)$/);return e?{match_:e[0],remainder:t.substr(e[0].length)}:null},uprightEntities:/^(?:pH|pOH|pC|pK|iPr|iBu)(?=$|[^a-zA-Z])/,"/":/^\s*(\/)\s*/,"//":/^\s*(\/\/)\s*/,"*":/^\s*[*.]\s*/},findObserveGroups:function(t,e,n,o,a,r,i,c,u,p){var s=function(t,e){if("string"==typeof e)return 0!==t.indexOf(e)?null:e;var n=t.match(e);return n?n[0]:null},_=s(t,e);if(null===_)return null;if(t=t.substr(_.length),null===(_=s(t,n)))return null;var d=function(t,e,n){for(var o=0;e":{"0|1|2|3":{action_:"r=",nextState:"r"},"a|as":{action_:["output","r="],nextState:"r"},"*":{action_:["output","r="],nextState:"r"}},"+":{o:{action_:"d= kv",nextState:"d"},"d|D":{action_:"d=",nextState:"d"},q:{action_:"d=",nextState:"qd"},"qd|qD":{action_:"d=",nextState:"qd"},dq:{action_:["output","d="],nextState:"d"},3:{action_:["sb=false","output","operator"],nextState:"0"}},amount:{"0|2":{action_:"a=",nextState:"a"}},"pm-operator":{"0|1|2|a|as":{action_:["sb=false","output",{type_:"operator",option:"\\pm"}],nextState:"0"}},operator:{"0|1|2|a|as":{action_:["sb=false","output","operator"],nextState:"0"}},"-$":{"o|q":{action_:["charge or bond","output"],nextState:"qd"},d:{action_:"d=",nextState:"d"},D:{action_:["output",{type_:"bond",option:"-"}],nextState:"3"},q:{action_:"d=",nextState:"qd"},qd:{action_:"d=",nextState:"qd"},"qD|dq":{action_:["output",{type_:"bond",option:"-"}],nextState:"3"}},"-9":{"3|o":{action_:["output",{type_:"insert",option:"hyphen"}],nextState:"3"}},"- orbital overlap":{o:{action_:["output",{type_:"insert",option:"hyphen"}],nextState:"2"},d:{action_:["output",{type_:"insert",option:"hyphen"}],nextState:"2"}},"-":{"0|1|2":{action_:[{type_:"output",option:1},"beginsWithBond=true",{type_:"bond",option:"-"}],nextState:"3"},3:{action_:{type_:"bond",option:"-"}},a:{action_:["output",{type_:"insert",option:"hyphen"}],nextState:"2"},as:{action_:[{type_:"output",option:2},{type_:"bond",option:"-"}],nextState:"3"},b:{action_:"b="},o:{action_:{type_:"- after o/d",option:!1},nextState:"2"},q:{action_:{type_:"- after o/d",option:!1},nextState:"2"},"d|qd|dq":{action_:{type_:"- after o/d",option:!0},nextState:"2"},"D|qD|p":{action_:["output",{type_:"bond",option:"-"}],nextState:"3"}},amount2:{"1|3":{action_:"a=",nextState:"a"}},letters:{"0|1|2|3|a|as|b|p|bp|o":{action_:"o=",nextState:"o"},"q|dq":{action_:["output","o="],nextState:"o"},"d|D|qd|qD":{action_:"o after d",nextState:"o"}},digits:{o:{action_:"q=",nextState:"q"},"d|D":{action_:"q=",nextState:"dq"},q:{action_:["output","o="],nextState:"o"},a:{action_:"o=",nextState:"o"}},"space A":{"b|p|bp":{}},space:{a:{nextState:"as"},0:{action_:"sb=false"},"1|2":{action_:"sb=true"},"r|rt|rd|rdt|rdq":{action_:"output",nextState:"0"},"*":{action_:["output","sb=true"],nextState:"1"}},"1st-level escape":{"1|2":{action_:["output",{type_:"insert+p1",option:"1st-level escape"}]},"*":{action_:["output",{type_:"insert+p1",option:"1st-level escape"}],nextState:"0"}},"[(...)]":{"r|rt":{action_:"rd=",nextState:"rd"},"rd|rdt":{action_:"rq=",nextState:"rdq"}},"...":{"o|d|D|dq|qd|qD":{action_:["output",{type_:"bond",option:"..."}],nextState:"3"},"*":{action_:[{type_:"output",option:1},{type_:"insert",option:"ellipsis"}],nextState:"1"}},". |* ":{"*":{action_:["output",{type_:"insert",option:"addition compound"}],nextState:"1"}},"state of aggregation $":{"*":{action_:["output","state of aggregation"],nextState:"1"}},"{[(":{"a|as|o":{action_:["o=","output","parenthesisLevel++"],nextState:"2"},"0|1|2|3":{action_:["o=","output","parenthesisLevel++"],nextState:"2"},"*":{action_:["output","o=","output","parenthesisLevel++"],nextState:"2"}},")]}":{"0|1|2|3|b|p|bp|o":{action_:["o=","parenthesisLevel--"],nextState:"o"},"a|as|d|D|q|qd|qD|dq":{action_:["output","o=","parenthesisLevel--"],nextState:"o"}},", ":{"*":{action_:["output","comma"],nextState:"0"}},"^_":{"*":{}},"^{(...)}|^($...$)":{"0|1|2|as":{action_:"b=",nextState:"b"},p:{action_:"b=",nextState:"bp"},"3|o":{action_:"d= kv",nextState:"D"},q:{action_:"d=",nextState:"qD"},"d|D|qd|qD|dq":{action_:["output","d="],nextState:"D"}},"^a|^\\x{}{}|^\\x{}|^\\x|'":{"0|1|2|as":{action_:"b=",nextState:"b"},p:{action_:"b=",nextState:"bp"},"3|o":{action_:"d= kv",nextState:"d"},q:{action_:"d=",nextState:"qd"},"d|qd|D|qD":{action_:"d="},dq:{action_:["output","d="],nextState:"d"}},"_{(state of aggregation)}$":{"d|D|q|qd|qD|dq":{action_:["output","q="],nextState:"q"}},"_{(...)}|_($...$)|_9|_\\x{}{}|_\\x{}|_\\x":{"0|1|2|as":{action_:"p=",nextState:"p"},b:{action_:"p=",nextState:"bp"},"3|o":{action_:"q=",nextState:"q"},"d|D":{action_:"q=",nextState:"dq"},"q|qd|qD|dq":{action_:["output","q="],nextState:"q"}},"=<>":{"0|1|2|3|a|as|o|q|d|D|qd|qD|dq":{action_:[{type_:"output",option:2},"bond"],nextState:"3"}},"#":{"0|1|2|3|a|as|o":{action_:[{type_:"output",option:2},{type_:"bond",option:"#"}],nextState:"3"}},"{}":{"*":{action_:{type_:"output",option:1},nextState:"1"}},"{...}":{"0|1|2|3|a|as|b|p|bp":{action_:"o=",nextState:"o"},"o|d|D|q|qd|qD|dq":{action_:["output","o="],nextState:"o"}},"$...$":{a:{action_:"a="},"0|1|2|3|as|b|p|bp|o":{action_:"o=",nextState:"o"},"as|o":{action_:"o="},"q|d|D|qd|qD|dq":{action_:["output","o="],nextState:"o"}},"\\bond{(...)}":{"*":{action_:[{type_:"output",option:2},"bond"],nextState:"3"}},"\\frac{(...)}":{"*":{action_:[{type_:"output",option:1},"frac-output"],nextState:"3"}},"\\overset{(...)}":{"*":{action_:[{type_:"output",option:2},"overset-output"],nextState:"3"}},"\\underset{(...)}":{"*":{action_:[{type_:"output",option:2},"underset-output"],nextState:"3"}},"\\underbrace{(...)}":{"*":{action_:[{type_:"output",option:2},"underbrace-output"],nextState:"3"}},"\\color{(...)}{(...)}1|\\color(...){(...)}2":{"*":{action_:[{type_:"output",option:2},"color-output"],nextState:"3"}},"\\color{(...)}0":{"*":{action_:[{type_:"output",option:2},"color0-output"]}},"\\ce{(...)}":{"*":{action_:[{type_:"output",option:2},"ce"],nextState:"3"}},"\\,":{"*":{action_:[{type_:"output",option:1},"copy"],nextState:"1"}},"\\x{}{}|\\x{}|\\x":{"0|1|2|3|a|as|b|p|bp|o|c0":{action_:["o=","output"],nextState:"3"},"*":{action_:["output","o=","output"],nextState:"3"}},others:{"*":{action_:[{type_:"output",option:1},"copy"],nextState:"3"}},else2:{a:{action_:"a to o",nextState:"o",revisit:!0},as:{action_:["output","sb=true"],nextState:"1",revisit:!0},"r|rt|rd|rdt|rdq":{action_:["output"],nextState:"0",revisit:!0},"*":{action_:["output","copy"],nextState:"3"}}}),actions:{"o after d":function(t,e){var n;if((t.d||"").match(/^[0-9]+$/)){var o=t.d;t.d=void 0,n=this.output(t),t.b=o}else n=this.output(t);return a.actions["o="](t,e),n},"d= kv":function(t,e){t.d=e,t.dType="kv"},"charge or bond":function(t,e){if(t.beginsWithBond){var n=[];return a.concatArray(n,this.output(t)),a.concatArray(n,a.actions.bond(t,e,"-")),n}t.d=e},"- after o/d":function(t,e,n){var o=a.patterns.match_("orbital",t.o||""),r=a.patterns.match_("one lowercase greek letter $",t.o||""),i=a.patterns.match_("one lowercase latin letter $",t.o||""),c=a.patterns.match_("$one lowercase latin letter$ $",t.o||""),u="-"===e&&(o&&""===o.remainder||r||i||c);!u||t.a||t.b||t.p||t.d||t.q||o||!i||(t.o="$"+t.o+"$");var p=[];return u?(a.concatArray(p,this.output(t)),p.push({type_:"hyphen"})):(o=a.patterns.match_("digits",t.d||""),n&&o&&""===o.remainder?(a.concatArray(p,a.actions["d="](t,e)),a.concatArray(p,this.output(t))):(a.concatArray(p,this.output(t)),a.concatArray(p,a.actions.bond(t,e,"-")))),p},"a to o":function(t){t.o=t.a,t.a=void 0},"sb=true":function(t){t.sb=!0},"sb=false":function(t){t.sb=!1},"beginsWithBond=true":function(t){t.beginsWithBond=!0},"beginsWithBond=false":function(t){t.beginsWithBond=!1},"parenthesisLevel++":function(t){t.parenthesisLevel++},"parenthesisLevel--":function(t){t.parenthesisLevel--},"state of aggregation":function(t,e){return{type_:"state of aggregation",p1:a.go(e,"o")}},comma:function(t,e){var n=e.replace(/\s*$/,"");return n!==e&&0===t.parenthesisLevel?{type_:"comma enumeration L",p1:n}:{type_:"comma enumeration M",p1:n}},output:function(t,e,n){var o,r,i;t.r?(r="M"===t.rdt?a.go(t.rd,"tex-math"):"T"===t.rdt?[{type_:"text",p1:t.rd||""}]:a.go(t.rd),i="M"===t.rqt?a.go(t.rq,"tex-math"):"T"===t.rqt?[{type_:"text",p1:t.rq||""}]:a.go(t.rq),o={type_:"arrow",r:t.r,rd:r,rq:i}):(o=[],(t.a||t.b||t.p||t.o||t.q||t.d||n)&&(t.sb&&o.push({type_:"entitySkip"}),t.o||t.q||t.d||t.b||t.p||2===n?t.o||t.q||t.d||!t.b&&!t.p?t.o&&"kv"===t.dType&&a.patterns.match_("d-oxidation$",t.d||"")?t.dType="oxidation":t.o&&"kv"===t.dType&&!t.q&&(t.dType=void 0):(t.o=t.a,t.d=t.b,t.q=t.p,t.a=t.b=t.p=void 0):(t.o=t.a,t.a=void 0),o.push({type_:"chemfive",a:a.go(t.a,"a"),b:a.go(t.b,"bd"),p:a.go(t.p,"pq"),o:a.go(t.o,"o"),q:a.go(t.q,"pq"),d:a.go(t.d,"oxidation"===t.dType?"oxidation":"bd"),dType:t.dType})));for(var c in t)"parenthesisLevel"!==c&&"beginsWithBond"!==c&&delete t[c];return o},"oxidation-output":function(t,e){var n=["{"];return a.concatArray(n,a.go(e,"oxidation")),n.push("}"),n},"frac-output":function(t,e){return{type_:"frac-ce",p1:a.go(e[0]),p2:a.go(e[1])}},"overset-output":function(t,e){return{type_:"overset",p1:a.go(e[0]),p2:a.go(e[1])}},"underset-output":function(t,e){return{type_:"underset",p1:a.go(e[0]),p2:a.go(e[1])}},"underbrace-output":function(t,e){return{type_:"underbrace",p1:a.go(e[0]),p2:a.go(e[1])}},"color-output":function(t,e){return{type_:"color",color1:e[0],color2:a.go(e[1])}},"r=":function(t,e){t.r=e},"rdt=":function(t,e){t.rdt=e},"rd=":function(t,e){t.rd=e},"rqt=":function(t,e){t.rqt=e},"rq=":function(t,e){t.rq=e},operator:function(t,e,n){return{type_:"operator",kind_:n||e}}}},a:{transitions:a.createTransitions({empty:{"*":{}},"1/2$":{0:{action_:"1/2"}},else:{0:{nextState:"1",revisit:!0}},"$(...)$":{"*":{action_:"tex-math tight",nextState:"1"}},",":{"*":{action_:{type_:"insert",option:"commaDecimal"}}},else2:{"*":{action_:"copy"}}}),actions:{}},o:{transitions:a.createTransitions({empty:{"*":{}},"1/2$":{0:{action_:"1/2"}},else:{0:{nextState:"1",revisit:!0}},letters:{"*":{action_:"rm"}},"\\ca":{"*":{action_:{type_:"insert",option:"circa"}}},"\\x{}{}|\\x{}|\\x":{"*":{action_:"copy"}},"${(...)}$|$(...)$":{"*":{action_:"tex-math"}},"{(...)}":{"*":{action_:"{text}"}},else2:{"*":{action_:"copy"}}}),actions:{}},text:{transitions:a.createTransitions({empty:{"*":{action_:"output"}},"{...}":{"*":{action_:"text="}},"${(...)}$|$(...)$":{"*":{action_:"tex-math"}},"\\greek":{"*":{action_:["output","rm"]}},"\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:["output","copy"]}},else:{"*":{action_:"text="}}}),actions:{output:function(t){if(t.text_){var e={type_:"text",p1:t.text_};for(var n in t)delete t[n];return e}}}},pq:{transitions:a.createTransitions({empty:{"*":{}},"state of aggregation $":{"*":{action_:"state of aggregation"}},i$:{0:{nextState:"!f",revisit:!0}},"(KV letters),":{0:{action_:"rm",nextState:"0"}},formula$:{0:{nextState:"f",revisit:!0}},"1/2$":{0:{action_:"1/2"}},else:{0:{nextState:"!f",revisit:!0}},"${(...)}$|$(...)$":{"*":{action_:"tex-math"}},"{(...)}":{"*":{action_:"text"}},"a-z":{f:{action_:"tex-math"}},letters:{"*":{action_:"rm"}},"-9.,9":{"*":{action_:"9,9"}},",":{"*":{action_:{type_:"insert+p1",option:"comma enumeration S"}}},"\\color{(...)}{(...)}1|\\color(...){(...)}2":{"*":{action_:"color-output"}},"\\color{(...)}0":{"*":{action_:"color0-output"}},"\\ce{(...)}":{"*":{action_:"ce"}},"\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:"copy"}},else2:{"*":{action_:"copy"}}}),actions:{"state of aggregation":function(t,e){return{type_:"state of aggregation subscript",p1:a.go(e,"o")}},"color-output":function(t,e){return{type_:"color",color1:e[0],color2:a.go(e[1],"pq")}}}},bd:{transitions:a.createTransitions({empty:{"*":{}},x$:{0:{nextState:"!f",revisit:!0}},formula$:{0:{nextState:"f",revisit:!0}},else:{0:{nextState:"!f",revisit:!0}},"-9.,9 no missing 0":{"*":{action_:"9,9"}},".":{"*":{action_:{type_:"insert",option:"electron dot"}}},"a-z":{f:{action_:"tex-math"}},x:{"*":{action_:{type_:"insert",option:"KV x"}}},letters:{"*":{action_:"rm"}},"'":{"*":{action_:{type_:"insert",option:"prime"}}},"${(...)}$|$(...)$":{"*":{action_:"tex-math"}},"{(...)}":{"*":{action_:"text"}},"\\color{(...)}{(...)}1|\\color(...){(...)}2":{"*":{action_:"color-output"}},"\\color{(...)}0":{"*":{action_:"color0-output"}},"\\ce{(...)}":{"*":{action_:"ce"}},"\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:"copy"}},else2:{"*":{action_:"copy"}}}),actions:{"color-output":function(t,e){return{type_:"color",color1:e[0],color2:a.go(e[1],"bd")}}}},oxidation:{transitions:a.createTransitions({empty:{"*":{}},"roman numeral":{"*":{action_:"roman-numeral"}},"${(...)}$|$(...)$":{"*":{action_:"tex-math"}},else:{"*":{action_:"copy"}}}),actions:{"roman-numeral":function(t,e){return{type_:"roman numeral",p1:e||""}}}},"tex-math":{transitions:a.createTransitions({empty:{"*":{action_:"output"}},"\\ce{(...)}":{"*":{action_:["output","ce"]}},"{...}|\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:"o="}},else:{"*":{action_:"o="}}}),actions:{output:function(t){if(t.o){var e={type_:"tex-math",p1:t.o};for(var n in t)delete t[n];return e}}}},"tex-math tight":{transitions:a.createTransitions({empty:{"*":{action_:"output"}},"\\ce{(...)}":{"*":{action_:["output","ce"]}},"{...}|\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:"o="}},"-|+":{"*":{action_:"tight operator"}},else:{"*":{action_:"o="}}}),actions:{"tight operator":function(t,e){t.o=(t.o||"")+"{"+e+"}"},output:function(t){if(t.o){var e={type_:"tex-math",p1:t.o};for(var n in t)delete t[n];return e}}}},"9,9":{transitions:a.createTransitions({empty:{"*":{}},",":{"*":{action_:"comma"}},else:{"*":{action_:"copy"}}}),actions:{comma:function(){return{type_:"commaDecimal"}}}},pu:{transitions:a.createTransitions({empty:{"*":{action_:"output"}},space$:{"*":{action_:["output","space"]}},"{[(|)]}":{"0|a":{action_:"copy"}},"(-)(9)^(-9)":{0:{action_:"number^",nextState:"a"}},"(-)(9.,9)(e)(99)":{0:{action_:"enumber",nextState:"a"}},space:{"0|a":{}},"pm-operator":{"0|a":{action_:{type_:"operator",option:"\\pm"},nextState:"0"}},operator:{"0|a":{action_:"copy",nextState:"0"}},"//":{d:{action_:"o=",nextState:"/"}},"/":{d:{action_:"o=",nextState:"/"}},"{...}|else":{"0|d":{action_:"d=",nextState:"d"},a:{action_:["space","d="],nextState:"d"},"/|q":{action_:"q=",nextState:"q"}}}),actions:{enumber:function(t,e){var n=[];return"+-"===e[0]||"+/-"===e[0]?n.push("\\pm "):e[0]&&n.push(e[0]),e[1]&&(a.concatArray(n,a.go(e[1],"pu-9,9")),e[2]&&(e[2].match(/[,.]/)?a.concatArray(n,a.go(e[2],"pu-9,9")):n.push(e[2])),e[3]=e[4]||e[3],e[3]&&(e[3]=e[3].trim(),"e"===e[3]||"*"===e[3].substr(0,1)?n.push({type_:"cdot"}):n.push({type_:"times"}))),e[3]&&n.push("10^{"+e[5]+"}"),n},"number^":function(t,e){var n=[];return"+-"===e[0]||"+/-"===e[0]?n.push("\\pm "):e[0]&&n.push(e[0]),a.concatArray(n,a.go(e[1],"pu-9,9")),n.push("^{"+e[2]+"}"),n},operator:function(t,e,n){return{type_:"operator",kind_:n||e}},space:function(){return{type_:"pu-space-1"}},output:function(t){var e,n=a.patterns.match_("{(...)}",t.d||"");n&&""===n.remainder&&(t.d=n.match_);var o=a.patterns.match_("{(...)}",t.q||"");if(o&&""===o.remainder&&(t.q=o.match_),t.d&&(t.d=t.d.replace(/\u00B0C|\^oC|\^{o}C/g,"{}^{\\circ}C"),t.d=t.d.replace(/\u00B0F|\^oF|\^{o}F/g,"{}^{\\circ}F")),t.q){t.q=t.q.replace(/\u00B0C|\^oC|\^{o}C/g,"{}^{\\circ}C"),t.q=t.q.replace(/\u00B0F|\^oF|\^{o}F/g,"{}^{\\circ}F");var r={d:a.go(t.d,"pu"),q:a.go(t.q,"pu")};"//"===t.o?e={type_:"pu-frac",p1:r.d,p2:r.q}:(e=r.d,r.d.length>1||r.q.length>1?e.push({type_:" / "}):e.push({type_:"/"}),a.concatArray(e,r.q))}else e=a.go(t.d,"pu-2");for(var i in t)delete t[i];return e}}},"pu-2":{transitions:a.createTransitions({empty:{"*":{action_:"output"}},"*":{"*":{action_:["output","cdot"],nextState:"0"}},"\\x":{"*":{action_:"rm="}},space:{"*":{action_:["output","space"],nextState:"0"}},"^{(...)}|^(-1)":{1:{action_:"^(-1)"}},"-9.,9":{0:{action_:"rm=",nextState:"0"},1:{action_:"^(-1)",nextState:"0"}},"{...}|else":{"*":{action_:"rm=",nextState:"1"}}}),actions:{cdot:function(){return{type_:"tight cdot"}},"^(-1)":function(t,e){t.rm+="^{"+e+"}"},space:function(){return{type_:"pu-space-2"}},output:function(t){var e=[];if(t.rm){var n=a.patterns.match_("{(...)}",t.rm||"");e=n&&""===n.remainder?a.go(n.match_,"pu"):{type_:"rm",p1:t.rm}}for(var o in t)delete t[o];return e}}},"pu-9,9":{transitions:a.createTransitions({empty:{0:{action_:"output-0"},o:{action_:"output-o"}},",":{0:{action_:["output-0","comma"],nextState:"o"}},".":{0:{action_:["output-0","copy"],nextState:"o"}},else:{"*":{action_:"text="}}}),actions:{comma:function(){return{type_:"commaDecimal"}},"output-0":function(t){var e=[];if(t.text_=t.text_||"",t.text_.length>4){var n=t.text_.length%3;0===n&&(n=3);for(var o=t.text_.length-3;o>0;o-=3)e.push(t.text_.substr(o,3)),e.push({type_:"1000 separator"});e.push(t.text_.substr(0,n)),e.reverse()}else e.push(t.text_);for(var a in t)delete t[a];return e},"output-o":function(t){var e=[];if(t.text_=t.text_||"",t.text_.length>4){for(var n=t.text_.length-3,o=0;o":case"\u2192":case"\u27f6":return"rightarrow";case"<-":return"leftarrow";case"<->":return"leftrightarrow";case"<--\x3e":return"rightleftarrows";case"<=>":case"\u21cc":return"rightleftharpoons";case"<=>>":return"rightequilibrium";case"<<=>":return"leftequilibrium";default:throw["MhchemBugT","mhchem bug T. Please report."]}},_getBond:function(t){switch(t){case"-":case"1":return"{-}";case"=":case"2":return"{=}";case"#":case"3":return"{\\equiv}";case"~":return"{\\tripledash}";case"~-":return"{\\mathrlap{\\raisebox{-.1em}{$-$}}\\raisebox{.1em}{$\\tripledash$}}";case"~=":case"~--":return"{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$\\tripledash$}}-}";case"-~-":return"{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$-$}}\\tripledash}";case"...":return"{{\\cdot}{\\cdot}{\\cdot}}";case"....":return"{{\\cdot}{\\cdot}{\\cdot}{\\cdot}}";case"->":return"{\\rightarrow}";case"<-":return"{\\leftarrow}";case"<":return"{<}";case">":return"{>}";default:throw["MhchemBugT","mhchem bug T. Please report."]}},_getOperator:function(t){switch(t){case"+":return" {}+{} ";case"-":return" {}-{} ";case"=":return" {}={} ";case"<":return" {}<{} ";case">":return" {}>{} ";case"<<":return" {}\\ll{} ";case">>":return" {}\\gg{} ";case"\\pm":return" {}\\pm{} ";case"\\approx":case"$\\approx$":return" {}\\approx{} ";case"v":case"(v)":return" \\downarrow{} ";case"^":case"(^)":return" \\uparrow{} ";default:throw["MhchemBugT","mhchem bug T. Please report."]}}}}(),a=a.default}()}));
\ No newline at end of file
diff --git a/macos/Tests/LitheTests/MarkdownPreviewRichContentTests.swift b/macos/Tests/LitheTests/MarkdownPreviewRichContentTests.swift
new file mode 100644
index 000000000..f049db501
--- /dev/null
+++ b/macos/Tests/LitheTests/MarkdownPreviewRichContentTests.swift
@@ -0,0 +1,112 @@
+import AppKit
+import CryptoKit
+import Foundation
+import Testing
+import WebKit
+@testable import Lithe
+
+@MainActor
+struct MarkdownPreviewRichContentTests {
+ @Test(arguments: ["light", "dark"])
+ func formulasChemistryMermaidAndFootnotesRenderOffline(appearance: String) async throws {
+ let root = URL(fileURLWithPath: #filePath)
+ .deletingLastPathComponent().deletingLastPathComponent()
+ .deletingLastPathComponent().deletingLastPathComponent()
+ let source = try JSONDecoder().decode(
+ Fixture.self,
+ from: Data(contentsOf: root.appendingPathComponent("shared/fixtures/editor/markdown-rich-content-v1.json"))
+ ).source
+ let html = try RustCoreBridge().markdownRender(source).get().html
+ let directory = try #require(MarkdownPreviewResources.directoryURL)
+ let before = try manifest(directory)
+ let coordinator = MarkdownPreviewWebView.Coordinator(scrollPosition: nil)
+ let configuration = WKWebViewConfiguration()
+ configuration.websiteDataStore = .nonPersistent()
+ let webView = WKWebView(frame: .zero, configuration: configuration)
+ webView.navigationDelegate = coordinator
+ webView.uiDelegate = WebMediaCaptureDenier.shared
+ let window = NSWindow(
+ contentRect: NSRect(x: 0, y: 0, width: 900, height: 700),
+ styleMask: [.titled], backing: .buffered, defer: false
+ )
+ window.isReleasedWhenClosed = false
+ window.contentView = webView
+ window.orderFront(nil)
+ defer {
+ webView.stopLoading()
+ webView.navigationDelegate = nil
+ webView.uiDelegate = nil
+ window.contentView = nil
+ window.close()
+ }
+
+ coordinator.loadTemplate(in: webView)
+ try #require(await waitForChanges(webViewChanges(webView), timeout: .seconds(5)) {
+ !webView.isLoading && webView.title == "Markdown Preview"
+ }, "The production preview template did not load")
+
+ let completed = TestGate()
+ var result: Result?
+ webView.callAsyncJavaScript(
+ """
+ await window.LithePreview.update({ html, appearance });
+ const content = document.getElementById('content');
+ const math = content.querySelectorAll('[data-math-style]');
+ return {
+ formulas: math.length,
+ renderedFormulas: content.querySelectorAll('[data-math-style] .katex').length,
+ formulaErrors: content.querySelectorAll('.katex-error').length,
+ mathml: content.querySelectorAll('math').length,
+ mermaid: !!content.querySelector('.mermaid-shell svg'),
+ diagramErrors: content.querySelectorAll('.diagram-error').length,
+ subscript: Array.from(content.querySelectorAll('p > sub')).some(sub =>
+ sub.textContent === '2' && sub.previousSibling?.textContent.endsWith('H') &&
+ sub.nextSibling?.textContent.startsWith('O')),
+ superscript: Array.from(content.querySelectorAll('p > sup')).some(sup =>
+ sup.textContent === '2' && sup.previousSibling?.textContent.endsWith('x')),
+ strikethrough: !!content.querySelector('del'),
+ code: Array.from(content.querySelectorAll('code')).some(code => code.textContent === 'H~2~O x^2^'),
+ footnote: !!content.querySelector('a[data-footnote-ref][href="#fn-note"]') &&
+ !!content.querySelector('#fn-note a[data-footnote-backref]'),
+ remoteScripts: Array.from(document.scripts).some(script => /^https?:/.test(script.src)),
+ remoteResources: performance.getEntriesByType('resource').some(entry => /^https?:/.test(entry.name)),
+ appearance: document.documentElement.dataset.theme
+ };
+ """,
+ arguments: ["html": html, "appearance": appearance], in: nil, in: .page
+ ) {
+ result = $0
+ completed.open()
+ }
+ try #require(await completed.waitUntilOpen(timeout: .seconds(8)), "Rich Markdown rendering timed out")
+ let response = try #require(result)
+ let values = try #require(try response.get() as? [String: Any])
+ #expect(values["formulas"] as? Int == 3)
+ #expect(values["renderedFormulas"] as? Int == 3)
+ #expect(values["mathml"] as? Int == 3)
+ #expect(values["formulaErrors"] as? Int == 0)
+ #expect(values["diagramErrors"] as? Int == 0)
+ for key in ["mermaid", "subscript", "superscript", "strikethrough", "code", "footnote"] {
+ #expect(values[key] as? Bool == true, "Expected \(key): \(values)")
+ }
+ #expect(values["remoteScripts"] as? Bool == false)
+ #expect(values["remoteResources"] as? Bool == false)
+ #expect(values["appearance"] as? String == appearance)
+ #expect(try manifest(directory) == before, "Rendering must not mutate packaged preview resources")
+ }
+
+ private func manifest(_ directory: URL) throws -> [String: String] {
+ let files = try #require(FileManager.default.enumerator(at: directory, includingPropertiesForKeys: [.isRegularFileKey]))
+ var result: [String: String] = [:]
+ for case let file as URL in files {
+ guard try file.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile == true else { continue }
+ result[String(file.path.dropFirst(directory.path.count))] = SHA256.hash(data: try Data(contentsOf: file))
+ .map { String(format: "%02x", $0) }.joined()
+ }
+ return result
+ }
+
+ private struct Fixture: Decodable {
+ let source: String
+ }
+}
diff --git a/rust/lithe-core/src/project/markdown.rs b/rust/lithe-core/src/project/markdown.rs
index 80f7c97a5..e1d451838 100644
--- a/rust/lithe-core/src/project/markdown.rs
+++ b/rust/lithe-core/src/project/markdown.rs
@@ -36,6 +36,7 @@ pub fn render(request: MarkdownRenderRequest) -> MarkdownRenderResponse {
options.extension.autolink = true;
options.extension.tasklist = true;
options.extension.superscript = true;
+ options.extension.subscript = true;
options.extension.footnotes = true;
options.extension.inline_footnotes = true;
options.extension.description_lists = true;
@@ -170,6 +171,26 @@ mod tests {
assert!(output.contains("Safe"));
}
+ #[test]
+ fn preserves_rich_content_for_offline_preview() {
+ let fixture: serde_json::Value = serde_json::from_str(include_str!(
+ "../../../../shared/fixtures/editor/markdown-rich-content-v1.json"
+ ))
+ .expect("rich Markdown fixture should decode");
+ let output = html(fixture["source"].as_str().expect("fixture source"));
+
+ assert!(output.contains("H2O"));
+ assert!(output.contains("x2"));
+ assert!(output.contains(">removed"));
+ assert!(output.contains(">H~2~O x^2^"));
+ assert_eq!(output.matches("data-math-style=\"inline\"").count(), 2);
+ assert_eq!(output.matches("data-math-style=\"display\"").count(), 1);
+ assert!(output.contains("\\ce{2 H2 + O2 -> 2 H2O}"));
+ assert!(output.contains("\\pu{123 kJ}"));
+ assert!(output.contains("language-mermaid"));
+ assert!(output.contains("class=\"footnotes\""));
+ }
+
#[test]
fn preserves_html_alignment_and_inline_badges_without_allowing_styles() {
let fixture: serde_json::Value = serde_json::from_str(include_str!(
diff --git a/scripts/resources/reuse-worktree-resources.mjs b/scripts/resources/reuse-worktree-resources.mjs
index e60fd04d8..b75534116 100644
--- a/scripts/resources/reuse-worktree-resources.mjs
+++ b/scripts/resources/reuse-worktree-resources.mjs
@@ -89,8 +89,9 @@ async function readRegistry() {
// not reusable build inputs, even without plugin mirror staging.
// Plugin host secrets and workspace state also take this rejection route,
// never a download/cache validator: their identity belongs to the installation.
- // PlantUML is a pinned Git input; neither another bundle nor live render state
- // is a reusable resource. Its isolated WebView never writes packaged inputs.
+ // Offline Markdown libraries, including PlantUML, are pinned Git inputs.
+ // Rebuild the target checkout's template/vendor resources before signing.
+ // Generated bundles and live render state are never reusable resources.
const excludedResources = registry.excludedResources ?? [];
if (!Array.isArray(excludedResources)) throw new Error("Invalid excluded worktree resources");
for (const resource of excludedResources) {
diff --git a/scripts/resources/test-reuse-worktree-resources.mjs b/scripts/resources/test-reuse-worktree-resources.mjs
index afe474469..09edb7b71 100644
--- a/scripts/resources/test-reuse-worktree-resources.mjs
+++ b/scripts/resources/test-reuse-worktree-resources.mjs
@@ -65,6 +65,17 @@ function reuse(extraArguments = []) {
}
try {
+ await test("offline Markdown preview assets come from Git, never another bundle", { timeout: 15000 }, async () => {
+ const registry = JSON.parse(await fs.readFile(path.join(scriptDirectory, "worktree-resources.json"), "utf8"));
+ const resource = registry.excludedResources.find(item => item.id === "markdown-preview-assets");
+ assert.equal(resource.reusable, false);
+ assert.ok(resource.locations.includes("macos/Sources/Lithe/Resources/MarkdownPreview/"));
+ assert.match(resource.identity, /SHA-256/);
+ const refused = reuse(["--resource", resource.id]);
+ assert.notEqual(refused.status, 0);
+ assert.match(diagnostics(refused), /markdown-preview-assets is isolated.*cannot be reused/);
+ });
+
await test("offline PlantUML assets and live renderer state cannot cross worktrees", { timeout: 15000 }, async () => {
const registry = JSON.parse(await fs.readFile(path.join(scriptDirectory, "worktree-resources.json"), "utf8"));
const resource = registry.excludedResources.find(item => item.id === "markdown-plantuml-assets");
diff --git a/scripts/resources/worktree-resources.json b/scripts/resources/worktree-resources.json
index f61deaae4..192d6fe19 100644
--- a/scripts/resources/worktree-resources.json
+++ b/scripts/resources/worktree-resources.json
@@ -33,6 +33,16 @@
}
],
"excludedResources": [
+ {
+ "id": "markdown-preview-assets",
+ "reusable": false,
+ "locations": [
+ "macos/Sources/Lithe/Resources/MarkdownPreview/",
+ "/Contents/Resources/Lithe_Lithe.bundle/MarkdownPreview/"
+ ],
+ "identity": "Git-tracked preview template and offline vendor scripts, styles, fonts and notices. KaTeX/mhchem versions, source archive integrity and extracted mhchem SHA-256 are fixed in THIRD_PARTY_NOTICES.md. Independent of platform architecture/toolchain; generated bundle copies have no reusable identity stamp.",
+ "reason": "Check out source via Git and copy this checkout's resources at build time before signing. Never copy another worktree's generated bundle or download/patch preview libraries at runtime. All rendering is in memory and the packaged files stay read-only, preserving code signatures and Sparkle delta baselines. Every reuse copy stage rejects this resource."
+ },
{
"id": "markdown-plantuml-assets",
"reusable": false,
diff --git a/shared/fixtures/editor/markdown-rich-content-v1.json b/shared/fixtures/editor/markdown-rich-content-v1.json
new file mode 100644
index 000000000..d1f897a4a
--- /dev/null
+++ b/shared/fixtures/editor/markdown-rich-content-v1.json
@@ -0,0 +1,3 @@
+{
+ "source": "# Rich Markdown\n\nInline math $x^2 + y^2 = z^2$ and units $\\pu{123 kJ}$.\n\n$$\n\\ce{2 H2 + O2 -> 2 H2O}\n$$\n\nWater H~2~O, power x^2^, ~~removed~~ and `H~2~O x^2^`.\n\nReference[^note].\n\n[^note]: A real footnote.\n\n```mermaid\nflowchart LR\n A[Source] --> B[Preview]\n```\n"
+}
diff --git a/shared/platform-feature-matrix/features/markdown-rendering.json b/shared/platform-feature-matrix/features/markdown-rendering.json
index f25c1e803..47ace76cc 100644
--- a/shared/platform-feature-matrix/features/markdown-rendering.json
+++ b/shared/platform-feature-matrix/features/markdown-rendering.json
@@ -2,12 +2,15 @@
"id": "markdown-rendering",
"area": "编辑器",
"group": "Markdown",
- "capability": "Markdown 预览与富文本渲染;macOS 内嵌 HTML 徽章保持行内排版和安全对齐,PlantUML 代码块可离线显示时序图与类图",
+ "capability": "Markdown 预览与富文本渲染;macOS 离线数学公式、mhchem 化学方程式与物理单位、Mermaid、脚注及上下标;内嵌 HTML 的徽章图片保持行内排版,并保留容器、段落与标题的安全对齐属性;PlantUML 代码块可离线显示时序图与类图",
"macos": {
"evidence": [
"macos/Sources/Lithe/Views/Editor/MarkdownPreviewView.swift",
"macos/Sources/Lithe/Core/Ports/MarkdownRendering.swift",
+ "macos/Sources/Lithe/Resources/MarkdownPreview/index.html",
+ "macos/Tests/LitheTests/MarkdownPreviewRichContentTests.swift",
"rust/lithe-core/src/project/markdown.rs",
+ "shared/fixtures/editor/markdown-rich-content-v1.json",
"macos/Sources/Lithe/Resources/MarkdownPreview/preview.css",
"macos/Tests/LitheTests/MarkdownPreviewHTMLTests.swift",
"shared/fixtures/editor/markdown-html-v1.json",
@@ -26,6 +29,6 @@
"verificationStatus": "pending"
},
"owner": "Editor",
- "verification": "使用代码高亮、表格、Mermaid、链接和相对路径 fixture 对比渲染结果。打开含上千个代码块的大型 Markdown 预览,渲染不卡住,编辑时预览不会先闪成未高亮的代码;代码里的 `$&`、`$1` 按原文显示。macOS 使用 markdown-html-v1.json 及仓库 README 验证:宽预览中同一段内多个徽章同行居中,窄分栏按可用宽度自然折行;div/p/h1 的 align 生效,显式 br 和普通段落保留,正文行内图片不拆行,相对图片与点击放大仍可用。深浅主题均检查,脚本、事件处理器、危险 URL 和任意 style 仍被剥离。 macOS PlantUML:在深浅主题打开时序图和类图 fenced block,离线生成可放大的 SVG 图片,不能在文档 DOM 执行 SVG;多个代码块顺序执行,切换文档拒绝旧结果;本地 include 被拒绝且保留源码与本地化提示,超大输入立即失败,关闭和取消释放渲染器;运行前后比较预览资源 SHA-256。",
- "notes": "Windows 预览中屏幕外的段落、标题、列表和代码块使用 content-visibility: auto 跳过排版(表格除外,避免溢出被裁切);8.6 万行文档打开预览的最长主线程任务由约 10 秒降到约 1.3 秒,滚动最长任务由约 3.8 秒降到约 0.23 秒。2026-11-01 #1623:macOS 图片不再全局 display:block,Core 保留 div/p/h1-h6 的 align,但不开放 style;新增共享输入和真实 WKWebView 回归。Windows 仍使用独立的简化解析器,未在本次修改或验证其 HTML 行为,整体运行验收状态保持 pending。 2026-11-01 PlantUML:仅 macOS 接入官方 MIT @plantuml/core 1.2026.9,使用内置 Smetana;独立非持久 WebView、固定资源白名单、256KiB 源码/4MiB SVG/4096px/八秒预算,输出为被动 SVG 图片。无在线服务器、Java、Graphviz、WASM 或跨文件 include;可选主题和扩展库不打包,Windows 未修改。"
+ "verification": "使用代码高亮、表格、Mermaid、链接和相对路径 fixture 对比渲染结果。打开含上千个代码块的大型 Markdown 预览,渲染不卡住,编辑时预览不会先闪成未高亮的代码;代码里的 `$&`、`$1` 按原文显示。macOS 在深浅主题运行 markdown-rich-content-v1.json:行内数学、块级化学反应、物理单位均生成可访问 MathML,没有 katex-error;Mermaid 生成真实 SVG,脚注链接及回链存在;H~2~O 与 x^2^ 正确,双波浪线仍是删除线,代码内语法保持原文。运行前后逐文件比较预览资源哈希,确认无网络脚本和远程资源请求、bundle 不变。macOS 使用 markdown-html-v1.json 及仓库 README 验证:宽预览中同一段内多个徽章同行居中,窄分栏按可用宽度自然折行;div/p/h1 的 align 生效,显式 br 和普通段落保留,正文行内图片不拆行,相对图片与点击放大仍可用。深浅主题均检查,脚本、事件处理器、危险 URL 和任意 style 仍被剥离。 macOS PlantUML:在深浅主题打开时序图和类图 fenced block,离线生成可放大的 SVG 图片,不能在文档 DOM 执行 SVG;多个代码块顺序执行,切换文档拒绝旧结果;本地 include 被拒绝且保留源码与本地化提示,超大输入立即失败,关闭和取消释放渲染器;运行前后比较预览资源 SHA-256。",
+ "notes": "Windows 预览中屏幕外的段落、标题、列表和代码块使用 content-visibility: auto 跳过排版(表格除外,避免溢出被裁切);8.6 万行文档打开预览的最长主线程任务由约 10 秒降到约 1.3 秒,滚动最长任务由约 3.8 秒降到约 0.23 秒。2026-11-01 #1623:macOS 图片不再全局 display:block,Core 保留 div/p/h1-h6 的 align,但不开放 style;新增共享输入和真实 WKWebView 回归。2026-11-01 #706 增强第一批:macOS 加载与 KaTeX 0.16.0 匹配的离线 mhchem 3.3.0 扩展,Core 开启 Comrak 下标,并增加真实 WKWebView 的数学、化学、Mermaid、脚注和上下标回归。Windows 仍使用独立简化解析器,未在本批迁移,也未修改或验证其 HTML 行为;不能宣称 #706 已整体完成。完整产品和 Windows 真机验收保持 pending。 2026-11-01 PlantUML:仅 macOS 接入官方 MIT @plantuml/core 1.2026.9,使用内置 Smetana;独立非持久 WebView、固定资源白名单、256KiB 源码/4MiB SVG/4096px/八秒预算,输出为被动 SVG 图片。无在线服务器、Java、Graphviz、WASM 或跨文件 include;可选主题和扩展库不打包,Windows 未修改。"
}