12 KiB
mdpolish
mdpolish 是实验室共用的、项目无关的 Python Markdown 修改库。它提供函数式 Modifier、精确文本编辑执行器、
有序 Pipeline、正则修改器工厂,以及少量可以用合成样例完整说明的通用修改器。
当前发布版本是 v0.3.0。库只处理内存中的 Markdown
字符串,不读取或写入文件,不提供默认流水线,也不包含任何项目的规则集合、数据清单、实验脚本或评审工具。
当前能力
| 能力 | 作用 | 明确边界 |
|---|---|---|
Modifier |
把不可变元数据与普通提议函数组合起来 | 函数只提议修改,不直接改字符串或文件 |
| 精确编辑执行器 | 校验快照、范围、原文、重复和冲突后原子应用一个批次 | 不判断项目业务语义 |
Pipeline |
按调用方顺序运行修改器,并对最终快照做只读稳定性复查 | 不自动选规则、不重排、不循环执行 |
regex_replace() |
把非空正则匹配转换为精确编辑 | 不提供规则注册表、配置加载或默认模式 |
mapped_line_join() |
用精确、正则或可选本地词典规则合并跨行片段 | 无默认规则;代码、表格、未知结构和歧义失败关闭 |
| HTML 表格修改器 | 处理严格表格子集的实体和单行布局 | 不是完整 HTML parser,也不是 HTML→GFM 转换器 |
一次运行会返回 success、failed 或 unstable:
success:所选修改器完成运行,且对最终结果不再提出修改;failed:修改器、契约或编辑验证发生错误;unstable:运行没有错误,但最终复查仍发现有效候选修改。
success 只代表本次选择的修改器已经稳定,不代表文档不存在其他质量问题。
安装
正式版本只通过 GitHub tag 和 GitHub Release 交付,不发布到 PyPI 或其他 Python 包索引。使用项目可以固定 不可移动的 tag:
python -m pip install 'mdpolish @ git+https://github.com/Bepr4/mdpolish.git@v0.3.0'
python -m pip install 'mdpolish[lexical] @ git+https://github.com/Bepr4/mdpolish.git@v0.3.0'
python -m pip install 'mdpolish[frequency] @ git+https://github.com/Bepr4/mdpolish.git@v0.3.0'
也可以安装同一 GitHub Release 附带的 wheel:
python -m pip install 'mdpolish[lexical] @ https://github.com/Bepr4/mdpolish/releases/download/v0.3.0/mdpolish-0.3.0-py3-none-any.whl'
Release 页面同时提供 wheel 的 SHA-256 校验值。仓库或 Release 如果是私有的,调用方需要自行配置 GitHub 访问权限; 库不会保存凭据。开发环境仍从本地工作树安装:
python -m venv .venv
.venv/bin/python -m pip install -e '.[dev]'
核心运行时只依赖 Python 标准库,支持 Python 3.11 及以上版本。自动词典规则需要调用方明确安装并选择对应 extra:
python -m pip install '/path/to/mdpolish[lexical]' # pyspellchecker + Pyphen
python -m pip install '/path/to/mdpolish[frequency]' # wordfreq,体积和传递依赖更大
安装 extra 不会自动启用规则,也不会触发在线下载或改变精确/正则规则行为。
组装自己的流水线
项目拥有规则、参数和顺序。下面的正则规则与两个通用修改器最终都是 Modifier:
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 排除不合法的断词位置:
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。调用项目应先检查状态,再自行决定写入位置:
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。
编写项目自己的修改器
复杂规则使用普通函数返回精确候选修改,不需要继承库基类:
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,真实 backend 验收、
版本身份和 GitHub 发布边界见
0010-first-cross-project-library-delivery.md。
html_table_entity_unescape() 只在严格完整的 <td> / <th> 文本中处理 &lt;、&gt; 和
&amp;。html_table_layout() 只调整严格单行表格的外层行布局,并保留标签、属性和单元格内容。
这两个 HTML 修改器采用保守的词法子集,不识别 Markdown 围栏。混合换行、嵌套标签、<tbody>、嵌套表格或损坏
结构会保持原样。
仓库结构
src/mdpolish/
├── models.py # 不可变快照、精确编辑、审计和结果模型
├── modifier.py # 函数式 Modifier 契约
├── edits.py # 批次验证与原子应用
├── pipeline.py # 有序执行与最终稳定性复查
├── regex.py # 正则修改器工厂
└── modifiers/ # 少量项目无关的通用修改器
tests/ # 只使用虚构文本的核心与通用修改器测试
research-wiki/
├── design/ # 已批准决策及被冻结的历史记录
├── explanation/ # 当前有效机制
├── reference/ # 代码无法完整表达的稳定查询事实
├── guides/ # 已实际验证的操作步骤
└── scratch/ # 不作为当前事实的本地草稿
当前不提供
- 文件适配器、公共 CLI、配置文件、profile 或批处理协议;
- 自动规则发现、注册表或默认流水线;
- Markdown AST、完整 HTML parser 或必装的第三方运行依赖;
- artifact、报告、Web/桌面评审器;
- 任何业务项目的规则、固定参数、文档 ID、数据或验收统计。
公共边界与原因见
0008-generic-functional-library-boundary.md,当前机制见
functional-modifier-core.md。旧 design 只保存历史决策,不代表当前
交付能力。
当前可用检查
在已经安装开发依赖的仓库环境中运行:
.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
检查本地变更:
git diff --check
git status --short
上述检查已于 2026-08-27 实际运行。Python 3.13.11 核心开发环境中 Ruff 和 mypy 通过,pytest 为
170 passed, 3 skipped;三个 skip 是该环境没有安装的真实 optional backend 路径,不计入发布验收。
从 mdpolish-0.3.0-py3-none-any.whl 安装全部 extras 后,Python 3.11.16 和 Python 3.13.11 环境分别得到
173 passed,没有 skip。仓库外消费者 smoke test 已覆盖核心精确/正则规则、pyspellchecker + Pyphen 和
wordfreq 的最终输出与版本审计。wheel 共 16 个文件,只包含通用 Python 包、类型标记和包元数据;不包含 tests、
Wiki、真实数据或项目规则。v0.3.0 Release wheel 的 SHA-256 是
a510ae1755c281f7b40262e242441882f3ccbc3fbc2174c6f5db3e8a8ae85060。
上述结果证明当前版本可安装并按合成契约运行,不代表任意词典阈值已经在真实业务语料上达到生产准确率。