fix: 破损 HTML 表格扫描失败关闭
This commit is contained in:
@@ -3,7 +3,8 @@
|
|||||||
`mdpolish` 是实验室共用的、项目无关的 Python Markdown 修改库。它提供函数式 `Modifier`、精确文本编辑执行器、
|
`mdpolish` 是实验室共用的、项目无关的 Python Markdown 修改库。它提供函数式 `Modifier`、精确文本编辑执行器、
|
||||||
有序 `Pipeline`、正则修改器工厂,以及少量可以用合成样例完整说明的通用修改器。
|
有序 `Pipeline`、正则修改器工厂,以及少量可以用合成样例完整说明的通用修改器。
|
||||||
|
|
||||||
当前发布版本是 [`v0.6.0`](https://github.com/Bepr4/mdpolish/releases/tag/v0.6.0)。库只处理内存中的 Markdown 字符串,
|
当前发布版本是 [`v0.6.0`](https://github.com/Bepr4/mdpolish/releases/tag/v0.6.0),当前工作树候选版本是尚未发布的
|
||||||
|
`0.6.1`。库只处理内存中的 Markdown 字符串,
|
||||||
不读取或写入文件,不提供默认流水线,也不包含任何项目的规则集合、
|
不读取或写入文件,不提供默认流水线,也不包含任何项目的规则集合、
|
||||||
数据清单、实验脚本或评审界面。
|
数据清单、实验脚本或评审界面。
|
||||||
|
|
||||||
@@ -373,3 +374,9 @@ Release wheel `mdpolish-0.6.0-py3-none-any.whl` 共 17 个文件,包含 `text_
|
|||||||
`_text_ranges.py`、tests、Wiki、报告或真实数据;在仓库外全新虚拟环境中无依赖安装后,版本、公共导入、精确混合行尾范围、
|
`_text_ranges.py`、tests、Wiki、报告或真实数据;在仓库外全新虚拟环境中无依赖安装后,版本、公共导入、精确混合行尾范围、
|
||||||
行尾集合、空行判断和 wheel 清单 smoke test 通过。Release wheel 的 SHA-256 是
|
行尾集合、空行判断和 wheel 清单 smoke test 通过。Release wheel 的 SHA-256 是
|
||||||
`695502b1a443d4e98dbf63e8bdcee59452baea2185cb7e1e13160127f70c920f`。
|
`695502b1a443d4e98dbf63e8bdcee59452baea2185cb7e1e13160127f70c920f`。
|
||||||
|
|
||||||
|
尚未发布的 `0.6.1` 候选修复了严格 HTML 表格扫描在未闭合或无法解析的外层 `<table>` 中继续处理完整内层表格的问题;
|
||||||
|
两个 HTML Modifier 的版本均为 `1.0.1`。2026-08-28 在 Python 3.13.11 开发环境中 Ruff 和 mypy 通过,pytest 为
|
||||||
|
`294 passed, 3 skipped`。安装候选 wheel 的全部 extras 后,Python 3.11.15 和 Python 3.13.11 环境分别得到
|
||||||
|
`297 passed`,没有 skip。候选 wheel 共 17 个文件并通过内容检查,SHA-256 是
|
||||||
|
`f628658a6d0b2860720e9425d190a477b78723dcda9b2d4b5a5e9f6e252faf07`。
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "mdpolish"
|
name = "mdpolish"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
description = "Deterministic functional core for composing exact Markdown modifiers"
|
description = "Deterministic functional core for composing exact Markdown modifiers"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|||||||
@@ -175,13 +175,19 @@ def strict_html_tables(markdown: str) -> tuple[HtmlTable, ...]:
|
|||||||
if start < 0:
|
if start < 0:
|
||||||
break
|
break
|
||||||
opening = _parse_tag(markdown, start)
|
opening = _parse_tag(markdown, start)
|
||||||
|
name_end = start + len("<table")
|
||||||
|
if opening is None and (
|
||||||
|
name_end == len(markdown) or markdown[name_end].isspace() or markdown[name_end] in "/>"
|
||||||
|
):
|
||||||
|
# 无法解析的 table 形开头同样会使后续嵌套关系不可信。
|
||||||
|
break
|
||||||
if opening is None or opening.closing or opening.name != "table":
|
if opening is None or opening.closing or opening.name != "table":
|
||||||
position = start + 1
|
position = start + 1
|
||||||
continue
|
continue
|
||||||
balanced_end = _balanced_table_end(markdown, opening)
|
balanced_end = _balanced_table_end(markdown, opening)
|
||||||
if balanced_end is None:
|
if balanced_end is None:
|
||||||
position = opening.end
|
# 未闭合外层之后无法证明后续 table 是否位于它内部, 停止发现以失败关闭。
|
||||||
continue
|
break
|
||||||
table = _strict_table_at(markdown, start, balanced_end)
|
table = _strict_table_at(markdown, start, balanced_end)
|
||||||
if table is not None:
|
if table is not None:
|
||||||
tables.append(table)
|
tables.append(table)
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ def html_table_entity_unescape() -> Modifier:
|
|||||||
|
|
||||||
return Modifier(
|
return Modifier(
|
||||||
modifier_id="markdown.html_table_entity_unescape",
|
modifier_id="markdown.html_table_entity_unescape",
|
||||||
version="1.0.0",
|
version="1.0.1",
|
||||||
parameters=(),
|
parameters=(),
|
||||||
applicability=_APPLICABILITY,
|
applicability=_APPLICABILITY,
|
||||||
propose=propose,
|
propose=propose,
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ def html_table_layout() -> Modifier:
|
|||||||
|
|
||||||
return Modifier(
|
return Modifier(
|
||||||
modifier_id="markdown.html_table_layout",
|
modifier_id="markdown.html_table_layout",
|
||||||
version="1.0.0",
|
version="1.0.1",
|
||||||
parameters=(),
|
parameters=(),
|
||||||
applicability=_APPLICABILITY,
|
applicability=_APPLICABILITY,
|
||||||
propose=propose,
|
propose=propose,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ def test_unescapes_one_layer_only_in_strict_cell_text() -> None:
|
|||||||
"\noutside &lt;"
|
"\noutside &lt;"
|
||||||
)
|
)
|
||||||
assert len(result.changes) == 3
|
assert len(result.changes) == 3
|
||||||
|
assert result.modifiers[0].version == "1.0.1"
|
||||||
|
|
||||||
|
|
||||||
def test_multiple_tables_and_cells_report_source_order() -> None:
|
def test_multiple_tables_and_cells_report_source_order() -> None:
|
||||||
@@ -47,6 +48,21 @@ def test_non_strict_or_outside_content_is_preserved(markdown: str) -> None:
|
|||||||
assert transform(markdown).output_markdown == markdown
|
assert transform(markdown).output_markdown == markdown
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"markdown",
|
||||||
|
[
|
||||||
|
"<table>broken<table><tr><td>&lt;</td></tr></table>",
|
||||||
|
"<table broken <table><tr><td>&lt;</td></tr></table>",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_damaged_outer_table_does_not_expose_complete_inner_table(markdown: str) -> None:
|
||||||
|
result = transform(markdown)
|
||||||
|
|
||||||
|
assert result.status is RunStatus.SUCCESS
|
||||||
|
assert result.output_markdown == markdown
|
||||||
|
assert result.changes == ()
|
||||||
|
|
||||||
|
|
||||||
def test_fenced_table_is_not_protected_by_the_lexical_subset() -> None:
|
def test_fenced_table_is_not_protected_by_the_lexical_subset() -> None:
|
||||||
markdown = "```html\n<table><tr><td>&lt;</td></tr></table>\n```"
|
markdown = "```html\n<table><tr><td>&lt;</td></tr></table>\n```"
|
||||||
assert transform(markdown).output_markdown == "```html\n<table><tr><td><</td></tr></table>\n```"
|
assert transform(markdown).output_markdown == "```html\n<table><tr><td><</td></tr></table>\n```"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ def test_expands_rows_without_changing_tags_attributes_or_cells() -> None:
|
|||||||
"</table>\nafter"
|
"</table>\nafter"
|
||||||
)
|
)
|
||||||
assert len(result.changes) == 1
|
assert len(result.changes) == 1
|
||||||
|
assert result.modifiers[0].version == "1.0.1"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("line_ending", ["\n", "\r\n", "\r"])
|
@pytest.mark.parametrize("line_ending", ["\n", "\r\n", "\r"])
|
||||||
@@ -56,6 +57,21 @@ def test_mixed_multiline_or_non_strict_tables_are_preserved(markdown: str) -> No
|
|||||||
assert transform(markdown).output_markdown == markdown
|
assert transform(markdown).output_markdown == markdown
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"markdown",
|
||||||
|
[
|
||||||
|
"<table>broken<table><tr><td>A</td></tr></table>",
|
||||||
|
"<table broken <table><tr><td>A</td></tr></table>",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_damaged_outer_table_does_not_expose_complete_inner_table(markdown: str) -> None:
|
||||||
|
result = transform(markdown)
|
||||||
|
|
||||||
|
assert result.status is RunStatus.SUCCESS
|
||||||
|
assert result.output_markdown == markdown
|
||||||
|
assert result.changes == ()
|
||||||
|
|
||||||
|
|
||||||
def test_successful_output_is_idempotent() -> None:
|
def test_successful_output_is_idempotent() -> None:
|
||||||
pipeline = Pipeline([html_table_layout()])
|
pipeline = Pipeline([html_table_layout()])
|
||||||
first = pipeline.transform(TABLE)
|
first = pipeline.transform(TABLE)
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ def test_empty_document_and_empty_rule_set_are_no_ops() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_installed_package_version_matches_delivery_candidate() -> None:
|
def test_installed_package_version_matches_delivery_candidate() -> None:
|
||||||
assert distribution_version("mdpolish") == "0.6.0"
|
assert distribution_version("mdpolish") == "0.6.1"
|
||||||
|
|
||||||
|
|
||||||
def test_parameters_preserve_rule_order_and_record_all_options() -> None:
|
def test_parameters_preserve_rule_order_and_record_all_options() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user