209 lines
8.1 KiB
Markdown
209 lines
8.1 KiB
Markdown
# 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 及以上版本。
|
||
|
||
## 组装自己的流水线
|
||
|
||
项目拥有规则、参数和顺序。下面的正则规则与两个通用修改器最终都是 `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()` 只使用调用方显式传入的三元组:左片段、右片段和最终文本。它只处理相邻物理行或中间恰好一个
|
||
同风格空行的情况,并检查 ASCII 词边界。
|
||
|
||
`html_table_entity_unescape()` 只在严格完整的 `<td>` / `<th>` 文本中处理 `&lt;`、`&gt;` 和
|
||
`&amp;`。`html_table_layout()` 只调整严格单行表格的外层行布局,并保留标签、属性和单元格内容。
|
||
|
||
这两个 HTML 修改器采用保守的词法子集,不识别 Markdown 围栏。混合换行、嵌套标签、`<tbody>`、嵌套表格或损坏
|
||
结构会保持原样。
|
||
|
||
## 仓库结构
|
||
|
||
```text
|
||
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`](research-wiki/design/0008-generic-functional-library-boundary.md),当前机制见
|
||
[`functional-modifier-core.md`](research-wiki/explanation/functional-modifier-core.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-26 在 Python 3.13.11 环境实际运行:Ruff 通过,mypy 检查 21 个源码和测试文件无问题,
|
||
pytest 共 104 项测试通过,`mdpolish-0.2.0-py3-none-any.whl` 构建成功。wheel 内容已单独检查,只包含通用 Python
|
||
包、类型标记和包元数据,不包含项目规则、实验脚本、评审器或 Node.js 文件。
|
||
|
||
`pyproject.toml` 声明的 Python 3.11 及以上为支持范围;本次结果不表示已经在每个受支持版本上完成兼容性验证。
|