Files
mdpolish/README.md
T

441 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# mdpolish
`mdpolish` 是实验室共用的、项目无关的 Python Markdown 修改库。它提供函数式 `Modifier`、精确文本编辑执行器、
有序 `Pipeline`、正则修改器工厂,以及少量可以用合成样例完整说明的通用修改器。
当前发布版本是 [`v0.7.0`](https://github.com/Bepr4/mdpolish/releases/tag/v0.7.0)。清洗核心只处理内存中的 Markdown
字符串,不读取或写入文件,不提供默认流水线,也不包含任何项目的规则集合、数据清单或实验脚本。安装包另外提供一个
必须显式启动的本地只读评审器,用来展示项目已经保存的正式 `full` review JSON;它不替项目运行清洗或管理产物。
## 当前能力
| 能力 | 作用 | 明确边界 |
| --- | --- | --- |
| `Modifier` | 把不可变元数据与普通提议函数组合起来 | 函数只提议修改,不直接改字符串或文件 |
| 精确编辑执行器 | 校验快照、范围、原文、重复和冲突后原子应用一个批次 | 不判断项目业务语义 |
| `Pipeline` | 按调用方顺序运行修改器,并对最终快照做只读稳定性复查 | 不自动选规则、不重排、不循环执行 |
| `build_review_document()` | 验证并重放已有结果,提供可信阶段、位置、全文和错误/残留证据 | 不重新运行修改器,不猜测损坏或不完整的结果 |
| `render_markdown_report()` | 把评审视图编排成完整 Markdown 源码报告字符串 | 只返回内存字符串,不创建文件或业务页面 |
| `review_document_to_dict()` | 按 schema `1.0` 把评审视图投影成普通 JSON 基本值 | 单向投影,不反序列化或重新应用修改 |
| `render_json_report()` | 复用正式 dict 投影生成确定的内存 JSON 字符串 | 不创建文件;默认摘要不等于公开安全日志 |
| `parse_json_report()` | 解析并校验正式 review JSON`full` 会验证哈希、阶段链和 Change 重放 | 不恢复 `ReviewDocument`,不接收路径 |
| `mdpolish-reviewer` | 在回环地址展示一个明确目录中的 `full` JSON | 只读,不运行 Pipeline,不提供业务审核流程 |
| `mdpolish.text_ranges` | 返回 CR/LF/CRLF 物理行的精确不可变原文范围 | 不解析 Markdown 块,不自动执行或修改文本 |
| `regex_replace()` | 把非空正则匹配转换为精确编辑 | 不提供规则注册表、配置加载或默认模式 |
| `mapped_line_join()` | 用精确、正则或可选本地词典规则合并跨行片段 | 无默认规则;代码、表格、未知结构和歧义失败关闭 |
| HTML 表格修改器 | 处理严格表格子集的实体和单行布局 | 不是完整 HTML parser,也不是 HTML→GFM 转换器 |
一次运行会返回 `success``failed``unstable`
- `success`:所选修改器完成运行,且对最终结果不再提出修改;
- `failed`:修改器、契约或编辑验证发生错误;
- `unstable`:运行没有错误,但最终复查仍发现有效候选修改。
`success` 只代表本次选择的修改器已经稳定,不代表文档不存在其他质量问题。
## 安装
正式版本只通过 GitHub tag 和 GitHub Release 交付,不发布到 PyPI 或其他 Python 包索引。使用项目可以固定
不可移动的 tag
```bash
python -m pip install 'mdpolish @ git+https://github.com/Bepr4/mdpolish.git@v0.7.0'
python -m pip install 'mdpolish[lexical] @ git+https://github.com/Bepr4/mdpolish.git@v0.7.0'
python -m pip install 'mdpolish[frequency] @ git+https://github.com/Bepr4/mdpolish.git@v0.7.0'
```
也可以安装同一 GitHub Release 附带的 wheel
```bash
python -m pip install 'mdpolish[lexical] @ https://github.com/Bepr4/mdpolish/releases/download/v0.7.0/mdpolish-0.7.0-py3-none-any.whl'
```
Release 页面同时提供 wheel 的 SHA-256 校验值。仓库或 Release 如果是私有的,调用方需要自行配置 GitHub 访问权限;
库不会保存凭据。开发环境仍从本地工作树安装:
```bash
python -m venv .venv
.venv/bin/python -m pip install -e '.[dev]'
```
核心和本地评审服务的 Python 运行时只依赖标准库,支持 Python 3.11 及以上版本。React 和 CodeMirror 已编译为 wheel
内的静态资源,使用评审器不需要 Node.js。自动词典规则需要调用方明确安装并选择对应 extra:
```bash
python -m pip install '/path/to/mdpolish[lexical]' # pyspellchecker + Pyphen
python -m pip install '/path/to/mdpolish[frequency]' # wordfreq,体积和传递依赖更大
```
安装 extra 不会自动启用规则,也不会触发在线下载或改变精确/正则规则行为。
## 组装自己的流水线
项目拥有规则、参数和顺序。下面的正则规则与两个通用修改器最终都是 `Modifier`
```python
from mdpolish import Pipeline, RunStatus, regex_replace
from mdpolish.modifiers import html_table_entity_unescape, mapped_line_join
normalize_spaces = regex_replace(
modifier_id="my_project.normalize_spaces",
version="1.0.0",
pattern=r" {2,}",
replacement=" ",
applicability="把正文中连续两个及以上的 ASCII 空格收敛为一个;项目需自行排除不适用区域。",
)
join_fragments = mapped_line_join(
(
("exam-", "ple", "example"),
("rule-", "based", "rule-based"),
)
)
pipeline = Pipeline(
(
normalize_spaces,
join_fragments,
html_table_entity_unescape(),
)
)
result = pipeline.transform("an exam-\nple text")
if result.status is not RunStatus.SUCCESS:
raise RuntimeError(f"cleaning did not succeed: {result.status}")
assert result.output_markdown == "an example text"
```
## 自动英文断词
普通英文断词不需要逐词维护映射。调用方安装 `lexical` extra 后,可以明确选择 `pyspellchecker` 的本地词典,
并用 Pyphen 排除不合法的断词位置:
```python
from mdpolish import Pipeline, RunStatus
from mdpolish.modifiers import mapped_line_join
from mdpolish.modifiers.mapped_line_join import (
LexicalCandidateForm,
LexicalLineJoinRule,
LexiconBackend,
)
join_english_wraps = mapped_line_join(
(
LexicalLineJoinRule(
rule_id="english.dehyphenate",
left_pattern=r"[A-Za-z]+",
right_pattern=r"[a-z]+",
separator="-",
backend=LexiconBackend.SPELLCHECKER,
language="en",
candidate_forms=(
LexicalCandidateForm.JOINED,
LexicalCandidateForm.HYPHENATED,
),
minimum_score=1.0,
minimum_score_margin=0.5,
hyphenation_language="en_US",
case_sensitive=False,
),
)
)
result = Pipeline((join_english_wraps,)).transform("an exam-\nple")
if result.status is not RunStatus.SUCCESS:
raise RuntimeError(f"cleaning did not succeed: {result.status}")
assert result.output_markdown == "an example"
```
`pyspellchecker` 的内置语言词典不支持大小写敏感查询,因此这个 backend 要求调用方明确写出
`case_sensitive=False`;库不会替调用方改变该开关或切换 backend。上面的分数和 margin 只用于演示完整配置,
不是适合所有项目的生产阈值。`JOINED` 与源 `HYPHENATED` 会共同参与候选选择;词典不能确认唯一结果时保持原文。
库不会保存 `output_markdown`。调用项目应先检查状态,再自行决定写入位置:
```python
from pathlib import Path
source_path = Path("input.md")
output_path = Path("output.md")
result = pipeline.transform(source_path.read_text(encoding="utf-8"))
if result.status is RunStatus.SUCCESS and result.output_markdown is not None:
output_path.write_text(result.output_markdown, encoding="utf-8")
```
清洗输入发现、输出命名、覆盖策略和批处理仍属于调用项目。`mdpolish` 可以生成通用的内存评审视图、机器投影、JSON
字符串与 Markdown 报告字符串,但不会自动保存它们,也不知道报告来自哪个文件。唯一通用 CLI 是下面的只读评审器;它只
消费调用方已经保存的正式 JSON,不承担项目文件适配。
## 构建内存评审视图和报告
调用方保留原始 Markdown,并把它与 `TransformResult` 一起传给评审构建函数:
```python
from mdpolish.review import (
build_review_document,
parse_json_report,
render_json_report,
render_markdown_report,
review_document_to_dict,
)
input_markdown = "an exam-\nple text"
result = pipeline.transform(input_markdown)
review = build_review_document(input_markdown, result)
review_summary = review_document_to_dict(review)
report_json = render_json_report(review, detail="changes")
report_markdown = render_markdown_report(review)
parsed_report = parse_json_report(report_json, expected_detail="changes")
assert review.current_markdown == "an example text"
assert review_summary["schema_version"] == "1.0"
assert review_summary["detail"] == "summary"
assert report_json.startswith('{\n "schema_name": "mdpolish.review"')
assert parsed_report["status"] == "success"
assert report_markdown.startswith("# mdpolish review report\n")
```
`build_review_document()` 不调用 `Modifier.propose()`,而是从输入开始重放已经记录的实际修改,逐阶段验证修改器身份、快照
哈希、范围、原文、冲突、排序和阶段结果。无法证明结果一致时会抛出 `ReviewBuildError`,不会生成近似报告。
成功结果的 `stages_complete``True``unstable` 和只有 final review 错误的 `failed` 也已经完成全部 transform 阶段,
但其当前全文仍明确标记为部分输出;preflight 或 transform 失败只包含失败前能够证明完成的阶段。
Markdown reporter 会包含完整输入、当前全文、统一 diff 以及实际修改的 `before` / `after`,因此不是安全日志。它默认只列出
前 20 条残留候选,并且不重复输出残留候选正文;调用项目如果保存报告,仍需负责路径、权限、脱敏和保留周期。每个完整
阶段都会保留前后快照,当前版本没有承诺无限文档长度或修改器数量下的内存上限。
机器投影使用独立于包版本的 schema `1.0`,并提供三个显式 detail
- `summary`:默认值;只含状态、哈希、计数、modifier 身份、阶段摘要和稳定错误代码,不含正文承载字段;
- `changes`:再加入 modifier 参数、适用说明、实际修改片段、错误消息和未应用残留候选;
- `full`:再加入完整输入、当前全文和每个阶段的完整前后全文。
高 detail 可能还原敏感内容。即使 `summary` 不含正文,它仍然携带项目元数据和哈希,不能自动视为匿名或适合公开传播。
dict 和 JSON 都是单向派生视图,不用于恢复 `ReviewDocument` 或重新应用修改。`parse_json_report()` 返回新的普通 dict/list
容器;对 `full` 会验证全文哈希、阶段首尾,并用核心共用的精确编辑原语证明每个 stage after 确实由所列 Change 产生。
完整 schema、坐标、哈希和兼容口径见
[`review-projection-schema-v1.md`](research-wiki/reference/review-projection-schema-v1.md)。
## 使用本地评审页面
项目先把每份评审结果显式保存为直属的 `*.review.json`。页面需要完整输入、当前文本和所有 Modifier 阶段,因此必须使用
`detail="full"`
```python
from pathlib import Path
from mdpolish.review import build_review_document, render_json_report
review = build_review_document(input_markdown, result)
review_path = Path("artifacts/reviews/example.review.json")
review_path.parent.mkdir(parents=True, exist_ok=True)
review_path.write_text(
render_json_report(review, detail="full") + "\n",
encoding="utf-8",
)
```
文件路径、目录创建、权限、Git 忽略、覆盖和保留周期都属于项目。`full` JSON 重复包含完整文档和阶段全文,不能当作安全日志
或公开产物。
然后显式启动只读页面:
```bash
mdpolish-reviewer --review-dir artifacts/reviews
```
也可以使用等价模块入口:
```bash
python -m mdpolish.reviewer --review-dir artifacts/reviews
```
命令默认绑定 `127.0.0.1` 的系统空闲端口,并打印本机 URL。页面可以选择文档和 Modifier,比较完整 before/after,保留
零修改阶段,并通过 Change 列表跳转。服务只读取所给目录直属的 `*.review.json`,不递归、不跟随符号链接、不运行
Pipeline,也不渲染原文中的 Markdown、HTML、图片或脚本。文件名去掉 `.review.json` 后只是页面标签,不会被解释成输入
路径或业务文档 ID。完整操作与排障见
[`use-local-reviewer.md`](research-wiki/guides/use-local-reviewer.md)。
## 扫描精确物理行
项目 Modifier 如果需要识别独占行、检查相邻行或连同行尾删除一行,可以按原文 code point 范围扫描:
```python
from mdpolish.text_ranges import physical_lines
source = "first\r\n\r\nlast"
lines = physical_lines(source)
assert lines[0].content(source) == "first"
assert lines[0].line_ending(source) == "\r\n"
assert lines[1].is_empty
assert source[lines[2].content_start : lines[2].full_end] == "last"
```
扫描只识别 LF、CR 和 CRLF,不规范化原文,也不判断段落、标题、列表、引用、代码或表格。它不会成为 Pipeline 的全局预处理;
只有显式调用它的 Modifier 才会扫描当前阶段快照。精确空文档、尾换行、offset 和失败关闭口径见
[`physical-line-ranges.md`](research-wiki/reference/physical-line-ranges.md)。
## 编写项目自己的修改器
复杂规则使用普通函数返回精确候选修改,不需要继承库基类:
```python
from mdpolish import DocumentSnapshot, Modifier, ProposedChange, TextEdit, TextSpan
def propose_marker_removal(snapshot: DocumentSnapshot) -> tuple[ProposedChange, ...]:
marker = "[REMOVE-ME]"
start = snapshot.markdown.find(marker)
if start < 0:
return ()
end = start + len(marker)
return (
ProposedChange(
snapshot_sha256=snapshot.sha256,
reason="删除项目确认过的占位标记",
edits=(
TextEdit(
snapshot_sha256=snapshot.sha256,
span=TextSpan(start, end),
expected_text=marker,
replacement="",
),
),
),
)
remove_marker = Modifier(
modifier_id="my_project.remove_marker",
version="1.0.0",
parameters={"marker": "[REMOVE-ME]"},
applicability="只删除项目声明的完整占位标记;不处理近似文本。",
propose=propose_marker_removal,
)
```
修改器决定“建议改哪里、为什么改、改成什么”;公共执行器真正修改内存字符串。每项 `TextEdit` 都绑定当前快照哈希、
半开字符串范围、预期原文和替换文本。任意候选无效时,该修改器当前批次不会产生部分修改。
项目函数应保持确定且无副作用,不读取文件、网络、环境变量、当前时间或随机数。Python 无法沙箱隔离任意函数;项目
函数私下产生的外部副作用不属于本库的验证或审计范围。
## 通用修改器的严格边界
`mapped_line_join()` 保留 `(left, right, replacement)` 三元组,也接受模块
`mdpolish.modifiers.mapped_line_join` 中的不可变规则值。规则可以使用精确片段、两侧命名正则、只约束右侧的行尾正则,
或从行尾与行首自动生成 `JOINED` / `HYPHENATED` / `SPACED` 候选并查询显式选择的本地词典。自动规则不要求逐词维护
映射;精确规则和 `KeepLineJoinRule` 用于项目词、例外与否决。
调用方还要显式决定块范围、换行处理、冲突策略、大小写和 Unicode 规范化。默认区分大小写且不规范化;支持相邻行或
中间最多一个同风格空行,并可在一次提议内完成多行链式合并。保守词法扫描只正向识别段落、ATX 标题 continuation、
列表 continuation 和同深度引用;代码块、GFM pipe table、raw HTML table、混合候选行尾及无法确认的容器保持原文。
完整公共模型、选择流程和限制见
[`0009-generalized-mapped-line-join.md`](research-wiki/design/0009-generalized-mapped-line-join.md),真实 backend 验收、
版本身份和 GitHub 发布边界见
[`0010-first-cross-project-library-delivery.md`](research-wiki/design/0010-first-cross-project-library-delivery.md)。
`html_table_entity_unescape()` 只在严格完整的 `<td>` / `<th>` 文本中处理 `&amp;lt;``&amp;gt;`
`&amp;amp;``html_table_layout()` 只调整严格单行表格的外层行布局,并保留标签、属性和单元格内容。
这两个 HTML 修改器采用保守的词法子集,不识别 Markdown 围栏。混合换行、嵌套标签、`<tbody>`、嵌套表格或损坏
结构会保持原样。
## 仓库结构
```text
src/mdpolish/
├── models.py # 不可变快照、精确编辑、审计和结果模型
├── modifier.py # 函数式 Modifier 契约
├── edits.py # 批次验证与原子应用
├── pipeline.py # 有序执行与最终稳定性复查
├── review.py # 可信评审视图、机器投影、JSON reader 及内存 reporter
├── _review_json.py # 正式 JSON 的私有解析和 full 语义校验
├── reviewer.py # 本地只读服务和公共 CLI
├── _reviewer_static/ # wheel 内的页面 bundle 与第三方许可证
├── regex.py # 正则修改器工厂
├── text_ranges.py # 公共精确物理行范围
└── modifiers/ # 少量项目无关的通用修改器
reviewer/ # React/TypeScript 源码、锁文件与合成界面测试
tests/ # 只使用虚构文本的核心与通用修改器测试
research-wiki/
├── design/ # 已批准决策及被冻结的历史记录
├── explanation/ # 当前有效机制
├── reference/ # 代码无法完整表达的稳定查询事实
├── guides/ # 已实际验证的操作步骤
└── scratch/ # 不作为当前事实的本地草稿
```
## 当前不提供
- 清洗文件适配器、清洗 CLI、配置文件、profile 或批处理协议;
- 自动规则发现、注册表或默认流水线;
- Markdown AST、完整 HTML parser 或必装的第三方运行依赖;
- artifact、自动保存的报告文件、正式 JSON Schema 文件、远程/桌面评审器或项目审核流程;
- 任何业务项目的规则、固定参数、文档 ID、数据或验收统计。
公共边界与原因见
[`0008-generic-functional-library-boundary.md`](research-wiki/design/0008-generic-functional-library-boundary.md),当前机制见
[`functional-modifier-core.md`](research-wiki/explanation/functional-modifier-core.md)。通用内存评审能力的批准边界见
[`0011-generic-review-projection-and-reporting.md`](research-wiki/design/0011-generic-review-projection-and-reporting.md),当前机制见
[`review-projection.md`](research-wiki/explanation/review-projection.md)。正式机器投影的批准边界见
[`0012-review-document-machine-projection.md`](research-wiki/design/0012-review-document-machine-projection.md)schema `1.0` 的稳定
查询口径见 [`review-projection-schema-v1.md`](research-wiki/reference/review-projection-schema-v1.md)。精确物理行公共接口的边界见
[`0013-public-physical-line-ranges.md`](research-wiki/design/0013-public-physical-line-ranges.md),稳定查询口径见
[`physical-line-ranges.md`](research-wiki/reference/physical-line-ranges.md)。旧 design 只保存历史决策,不代表当前交付能力。
项目无关本地评审器的批准边界见
[`0014-generic-local-reviewer.md`](research-wiki/design/0014-generic-local-reviewer.md)。
## 当前可用检查
在已经安装开发依赖的仓库环境中运行:
```bash
.venv/bin/ruff check .
.venv/bin/mypy src tests
.venv/bin/pytest
.venv/bin/python -m pip wheel . --no-deps --wheel-dir /tmp/mdpolish-wheel-check
```
修改前端源码或依赖时,再在 Node.js 24 环境中运行:
```bash
cd reviewer
nvm use 24
npm ci
npm run check
cd ..
```
检查本地变更:
```bash
git diff --check
git status --short
```
上述检查已于 2026-08-28 对 `v0.7.0` 实际运行。Python 3.13.11 开发环境中 Ruff 和 mypy 通过;未安装可选 backend 时
pytest 为 `330 passed, 3 skipped`,三个 skip 分别对应真实 lexical 和 frequency backend 路径。
Node.js 24.19.0、npm 11.17.0 环境中,`npm ci` 未发现漏洞,ESLint、TypeScript、8 项 Vitest 和 Vite 生产构建通过。
生产 bundle 随 wheel 提供;普通使用者不需要 Node.js,页面运行时不从 CDN 下载资源。24 份生产依赖许可证文本和版本清单
已随 bundle 收录。
同一个 Release wheel 在全新 Python 3.11.16 和 Python 3.13.11 环境中安装全部 extras 后,分别得到 `333 passed`,没有
skip;导入路径均确认来自环境的 `site-packages`。另一全新环境只安装 wheel、未安装第三方运行依赖,已实际完成 full JSON
生成、console script 与模块入口启动、集合/文档/Modifier API、UTF-16 定位和静态页面资源 smoke test。
`mdpolish-0.7.0-py3-none-any.whl` 压缩后为 315,279 bytes,共 49 个文件;解压后为 961,976 bytes,其中 29 个页面与许可证
文件为 742,678 bytes。相对未发布的 `0.6.1` 候选增加 32 个文件和 274,510 bytes 压缩体积。wheel 不包含 tests、Wiki、
`node_modules`、source map、真实报告、项目规则或数据。Release wheel 的 SHA-256 是
`b81a9a07fa0479854cd21d5a65f0f485cadf2031b5d731c3658ca10a1d72dc09`
自动检查不能替代真实浏览器中的最终视觉、长文滚动和跨 Modifier 跳转人工确认。上述结果只证明当前版本可安装并按合成契约
运行,不代表任意清洗规则已经在真实业务语料上达到生产准确率。