Files
mdpolish/README.md
T

22 KiB
Raw Blame History

mdpolish

mdpolish 是实验室共用的、项目无关的 Python Markdown 修改库。它提供函数式 Modifier、精确文本编辑执行器、 有序 Pipeline、正则修改器工厂,以及少量可以用合成样例完整说明的通用修改器。

当前发布版本是 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 JSONfull 会验证哈希、阶段链和 Change 重放 不恢复 ReviewDocument,不接收路径
mdpolish-reviewer 在回环地址展示一个明确目录中的 full JSON 只读,不运行 Pipeline,不提供业务审核流程
mdpolish.text_ranges 返回 CR/LF/CRLF 物理行的精确不可变原文范围 不解析 Markdown 块,不自动执行或修改文本
regex_replace() 把非空正则匹配转换为精确编辑 不提供规则注册表、配置加载或默认模式
mapped_line_join() 用精确、正则或可选本地词典规则合并跨行片段 无默认规则;代码、表格、未知结构和歧义失败关闭
HTML 表格修改器 处理严格表格子集的实体和单行布局 不是完整 HTML parser,也不是 HTML→GFM 转换器

一次运行会返回 successfailedunstable

  • success:所选修改器完成运行,且对最终结果不再提出修改;
  • failed:修改器、契约或编辑验证发生错误;
  • unstable:运行没有错误,但最终复查仍发现有效候选修改。

success 只代表本次选择的修改器已经稳定,不代表文档不存在其他质量问题。

安装

正式版本只通过 GitHub tag 和 GitHub Release 交付,不发布到 PyPI 或其他 Python 包索引。使用项目可以固定 不可移动的 tag:

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

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 访问权限; 库不会保存凭据。开发环境仍从本地工作树安装:

python -m venv .venv
.venv/bin/python -m pip install -e '.[dev]'

核心和本地评审服务的 Python 运行时只依赖标准库,支持 Python 3.11 及以上版本。React 和 CodeMirror 已编译为 wheel 内的静态资源,使用评审器不需要 Node.js。自动词典规则需要调用方明确安装并选择对应 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")

清洗输入发现、输出命名、覆盖策略和批处理仍属于调用项目。mdpolish 可以生成通用的内存评审视图、机器投影、JSON 字符串与 Markdown 报告字符串,但不会自动保存它们,也不知道报告来自哪个文件。唯一通用 CLI 是下面的只读评审器;它只 消费调用方已经保存的正式 JSON,不承担项目文件适配。

构建内存评审视图和报告

调用方保留原始 Markdown,并把它与 TransformResult 一起传给评审构建函数:

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_completeTrueunstable 和只有 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

使用本地评审页面

项目先把每份评审结果显式保存为直属的 *.review.json。页面需要完整输入、当前文本和所有 Modifier 阶段,因此必须使用 detail="full"

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 重复包含完整文档和阶段全文,不能当作安全日志 或公开产物。

然后显式启动只读页面:

mdpolish-reviewer --review-dir artifacts/reviews

也可以使用等价模块入口:

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

扫描精确物理行

项目 Modifier 如果需要识别独占行、检查相邻行或连同行尾删除一行,可以按原文 code point 范围扫描:

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

编写项目自己的修改器

复杂规则使用普通函数返回精确候选修改,不需要继承库基类:

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> 文本中处理 &amp;lt;&amp;gt;&amp;amp;html_table_layout() 只调整严格单行表格的外层行布局,并保留标签、属性和单元格内容。

这两个 HTML 修改器采用保守的词法子集,不识别 Markdown 围栏。混合换行、嵌套标签、<tbody>、嵌套表格或损坏 结构会保持原样。

仓库结构

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,当前机制见 functional-modifier-core.md。通用内存评审能力的批准边界见 0011-generic-review-projection-and-reporting.md,当前机制见 review-projection.md。正式机器投影的批准边界见 0012-review-document-machine-projection.mdschema 1.0 的稳定 查询口径见 review-projection-schema-v1.md。精确物理行公共接口的边界见 0013-public-physical-line-ranges.md,稳定查询口径见 physical-line-ranges.md。旧 design 只保存历史决策,不代表当前交付能力。 项目无关本地评审器的批准边界见 0014-generic-local-reviewer.md

当前可用检查

在已经安装开发依赖的仓库环境中运行:

.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 环境中运行:

cd reviewer
nvm use 24
npm ci
npm run check
cd ..

检查本地变更:

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 跳转人工确认。上述结果只证明当前版本可安装并按合成契约 运行,不代表任意清洗规则已经在真实业务语料上达到生产准确率。