Files
mdpolish/README.md
T

354 lines
18 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.4.0`](https://github.com/Bepr4/mdpolish/releases/tag/v0.4.0),当前源码树的下一候选版本是
`0.5.0`。库只处理内存中的 Markdown 字符串,不读取或写入文件,不提供默认流水线,也不包含任何项目的规则集合、
数据清单、实验脚本或评审界面。
## 当前能力
| 能力 | 作用 | 明确边界 |
| --- | --- | --- |
| `Modifier` | 把不可变元数据与普通提议函数组合起来 | 函数只提议修改,不直接改字符串或文件 |
| 精确编辑执行器 | 校验快照、范围、原文、重复和冲突后原子应用一个批次 | 不判断项目业务语义 |
| `Pipeline` | 按调用方顺序运行修改器,并对最终快照做只读稳定性复查 | 不自动选规则、不重排、不循环执行 |
| `build_review_document()` | 验证并重放已有结果,提供可信阶段、位置、全文和错误/残留证据 | 不重新运行修改器,不猜测损坏或不完整的结果 |
| `render_markdown_report()` | 把评审视图编排成完整 Markdown 源码报告字符串 | 只返回内存字符串,不创建文件或业务页面 |
| `review_document_to_dict()` | 按 schema `1.0` 把评审视图投影成普通 JSON 基本值 | 单向投影,不反序列化或重新应用修改 |
| `render_json_report()` | 复用正式 dict 投影生成确定的内存 JSON 字符串 | 不创建文件;默认摘要不等于公开安全日志 |
| `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.4.0'
python -m pip install 'mdpolish[lexical] @ git+https://github.com/Bepr4/mdpolish.git@v0.4.0'
python -m pip install 'mdpolish[frequency] @ git+https://github.com/Bepr4/mdpolish.git@v0.4.0'
```
也可以安装同一 GitHub Release 附带的 wheel
```bash
python -m pip install 'mdpolish[lexical] @ https://github.com/Bepr4/mdpolish/releases/download/v0.4.0/mdpolish-0.4.0-py3-none-any.whl'
```
Release 页面同时提供 wheel 的 SHA-256 校验值。仓库或 Release 如果是私有的,调用方需要自行配置 GitHub 访问权限;
库不会保存凭据。以上命令当前安装的是已发布的 `v0.4.0`,不包含本源码树尚未发布的 `0.5.0` 机器投影。开发环境仍从
本地工作树安装:
```bash
python -m venv .venv
.venv/bin/python -m pip install -e '.[dev]'
```
核心运行时只依赖 Python 标准库,支持 Python 3.11 及以上版本。自动词典规则需要调用方明确安装并选择对应 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")
```
文件读取、输出命名、覆盖策略、批处理和 CLI 都属于调用项目。`mdpolish` 可以生成通用的内存评审视图、机器投影、JSON
字符串与 Markdown 报告字符串,但不会自动保存它们,也不知道报告来自哪个文件。
## 构建内存评审视图和报告
调用方保留原始 Markdown,并把它与 `TransformResult` 一起传给评审构建函数:
```python
from mdpolish.review import (
build_review_document,
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)
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 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` 或重新应用修改。完整 schema、坐标、哈希和兼容口径见
[`review-projection-schema-v1.md`](research-wiki/reference/review-projection-schema-v1.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/Markdown reporter
├── regex.py # 正则修改器工厂
└── modifiers/ # 少量项目无关的通用修改器
tests/ # 只使用虚构文本的核心与通用修改器测试
research-wiki/
├── design/ # 已批准决策及被冻结的历史记录
├── explanation/ # 当前有效机制
├── reference/ # 代码无法完整表达的稳定查询事实
├── guides/ # 已实际验证的操作步骤
└── scratch/ # 不作为当前事实的本地草稿
```
## 当前不提供
- 文件适配器、公共 CLI、配置文件、profile 或批处理协议;
- 自动规则发现、注册表或默认流水线;
- Markdown AST、完整 HTML parser 或必装的第三方运行依赖;
- artifact、自动保存的报告文件、正式 JSON Schema 文件、HTML reporter、Web/桌面评审器或项目审核流程;
- 任何业务项目的规则、固定参数、文档 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)。旧 design 只保存历史
决策,不代表当前交付能力。
## 当前可用检查
在已经安装开发依赖的仓库环境中运行:
```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
```
检查本地变更:
```bash
git diff --check
git status --short
```
上述检查已于 2026-08-27 实际运行。Python 3.13.11 核心开发环境中 Ruff 和 mypy 通过,pytest 为
`211 passed, 3 skipped`;三个 skip 是该环境没有安装的真实 optional backend 路径,不计入发布验收。
`mdpolish-0.4.0-py3-none-any.whl` 共 17 个文件,包含 `review.py``py.typed`,不包含 tests、Wiki、artifact、页面或
真实数据。安装全部 extras 后,Python 3.11.16 和 Python 3.13.11 环境分别得到 `214 passed`,没有 skip;仓库外消费者
smoke test 已覆盖核心正则、`pyspellchecker + Pyphen``wordfreq`、评审视图和 Markdown reporter。核心运行依赖仍只有
Python 标准库。Release wheel 的 SHA-256 是
`12e24863314958130ed082f78e89ab8bc0dad39a3848f2ae42e273d19a409693`
上述结果证明当前版本可安装并按合成契约运行,不代表任意词典阈值已经在真实业务语料上达到生产准确率。
`0.5.0` 候选于 2026-08-28 在 Python 3.13.11 开发环境中实际得到:mypy 通过,pytest 为
`228 passed, 3 skipped`;三个 skip 仍是没有安装的 optional backend。除工作区已有的 `src/mdpolish/regex.py` 中文注释
改动外,Ruff 全部通过;未排除该文件的全仓 Ruff 因其中 30 个 `RUF002` / `RUF003` 失败,本轮没有擅自修改该用户改动。
候选 `mdpolish-0.5.0-py3-none-any.whl` 构建成功,共 17 个文件,包含更新后的 `review.py``py.typed`,不包含 tests、Wiki、
报告或真实数据;在仓库外全新虚拟环境中无依赖安装后,dict 投影与 JSON reporter smoke test 通过。该临时 wheel 不是 Release
资产,其哈希不构成发布身份;完成全仓 Ruff、提交、合并、tag 和 Release 仍需要分别确认。