# mdpolish `mdpolish` 是实验室共用的、项目无关的 Python Markdown 修改库。它提供函数式 `Modifier`、精确文本编辑执行器、 有序 `Pipeline`、正则修改器工厂,以及少量可以用合成样例完整说明的通用修改器。 当前版本是 `0.2.0`。库只处理内存中的 Markdown 字符串,不读取或写入文件,不提供默认流水线,也不包含任何项目的 规则集合、数据清单、实验脚本或评审工具。 ## 当前能力 | 能力 | 作用 | 明确边界 | | --- | --- | --- | | `Modifier` | 把不可变元数据与普通提议函数组合起来 | 函数只提议修改,不直接改字符串或文件 | | 精确编辑执行器 | 校验快照、范围、原文、重复和冲突后原子应用一个批次 | 不判断项目业务语义 | | `Pipeline` | 按调用方顺序运行修改器,并对最终快照做只读稳定性复查 | 不自动选规则、不重排、不循环执行 | | `regex_replace()` | 把非空正则匹配转换为精确编辑 | 不提供规则注册表、配置加载或默认模式 | | `mapped_line_join()` | 用精确、正则或可选本地词典规则合并跨行片段 | 无默认规则;代码、表格、未知结构和歧义失败关闭 | | HTML 表格修改器 | 处理严格表格子集的实体和单行布局 | 不是完整 HTML parser,也不是 HTML→GFM 转换器 | 一次运行会返回 `success`、`failed` 或 `unstable`: - `success`:所选修改器完成运行,且对最终结果不再提出修改; - `failed`:修改器、契约或编辑验证发生错误; - `unstable`:运行没有错误,但最终复查仍发现有效候选修改。 `success` 只代表本次选择的修改器已经稳定,不代表文档不存在其他质量问题。 ## 安装 项目仍在仓库内开发,使用项目可以从本地路径安装: ```bash python -m pip install /path/to/mdpolish ``` 开发环境: ```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" ``` 库不会保存 `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`。 ## 编写项目自己的修改器 复杂规则使用普通函数返回精确候选修改,不需要继承库基类: ```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)。 `html_table_entity_unescape()` 只在严格完整的 `