实现 ClinDB 第一批清洗组件
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
from mdpolish import Pipeline, RunStatus
|
||||
from mdpolish.components import (
|
||||
ArxivSubmissionStampComponent,
|
||||
HtmlTableDoubleEscapeComponent,
|
||||
HtmlTableLayoutComponent,
|
||||
ManuscriptLineNumberComponent,
|
||||
PageBreakWordJoinComponent,
|
||||
ReferenceSpacingComponent,
|
||||
RepeatedRunningHeaderComponent,
|
||||
WordReviewCommentComponent,
|
||||
)
|
||||
|
||||
STAMP = "arXiv:2104.12345v2 [stat.ME] 31 Dec 2021"
|
||||
HEADER = "## Repeated Paper Header"
|
||||
MAPPINGS = (
|
||||
("medi-", "cal", "medical"),
|
||||
("possi-", "bly", "possibly"),
|
||||
("cre-", "ated", "created"),
|
||||
("SOFA-", "based", "SOFA-based"),
|
||||
("life-", "threatening", "life-threatening"),
|
||||
("threshold.", "olds", "thresholds"),
|
||||
)
|
||||
|
||||
|
||||
def _build_pipeline() -> Pipeline:
|
||||
return Pipeline(
|
||||
[
|
||||
WordReviewCommentComponent(),
|
||||
ManuscriptLineNumberComponent(),
|
||||
ArxivSubmissionStampComponent(),
|
||||
RepeatedRunningHeaderComponent(),
|
||||
PageBreakWordJoinComponent(MAPPINGS),
|
||||
HtmlTableDoubleEscapeComponent(),
|
||||
HtmlTableLayoutComponent(),
|
||||
ReferenceSpacingComponent(),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _numbered_manuscript() -> list[str]:
|
||||
return [
|
||||
f"## {number} Section {number}" if number in {5, 15} else f"{number} body {number}"
|
||||
for number in range(1, 21)
|
||||
]
|
||||
|
||||
|
||||
def _combined_markdown() -> str:
|
||||
return "\n".join(
|
||||
(
|
||||
"1 Affiliation",
|
||||
"## Abstract",
|
||||
*_numbered_manuscript(),
|
||||
"Commented [A1]: remove this",
|
||||
"",
|
||||
STAMP,
|
||||
"Sentence continues in",
|
||||
"",
|
||||
HEADER,
|
||||
"",
|
||||
"the next line.",
|
||||
"A word is possi-",
|
||||
"",
|
||||
"bly split.",
|
||||
"<table><tr><td>&lt;5</td></tr><tr><td>B</td></tr></table>",
|
||||
"## References",
|
||||
"",
|
||||
"1. First",
|
||||
"",
|
||||
"2. Second",
|
||||
"",
|
||||
HEADER,
|
||||
"",
|
||||
"3. Third",
|
||||
"4. Fourth",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_script_builds_frozen_component_order_and_parameters() -> None:
|
||||
script_path = Path(__file__).parents[1] / "scripts" / "run_clindb_first_batch_experiment.py"
|
||||
module = ast.parse(script_path.read_text(encoding="utf-8"))
|
||||
build_function = next(
|
||||
node for node in module.body if isinstance(node, ast.FunctionDef) and node.name == "build_pipeline"
|
||||
)
|
||||
component_names = [
|
||||
call.func.id
|
||||
for node in ast.walk(build_function)
|
||||
if isinstance(node, ast.List)
|
||||
for call in node.elts
|
||||
if isinstance(call, ast.Call) and isinstance(call.func, ast.Name)
|
||||
]
|
||||
mapping_assignment = next(
|
||||
node
|
||||
for node in module.body
|
||||
if isinstance(node, ast.Assign)
|
||||
and any(isinstance(target, ast.Name) and target.id == "CLINDB_WORD_JOIN_MAPPINGS" for target in node.targets)
|
||||
)
|
||||
assert component_names == [
|
||||
"WordReviewCommentComponent",
|
||||
"ManuscriptLineNumberComponent",
|
||||
"ArxivSubmissionStampComponent",
|
||||
"RepeatedRunningHeaderComponent",
|
||||
"PageBreakWordJoinComponent",
|
||||
"HtmlTableDoubleEscapeComponent",
|
||||
"HtmlTableLayoutComponent",
|
||||
"ReferenceSpacingComponent",
|
||||
]
|
||||
assert ast.literal_eval(mapping_assignment.value) == MAPPINGS
|
||||
|
||||
pipeline = _build_pipeline()
|
||||
result = pipeline.transform("")
|
||||
|
||||
assert [component.component_id for component in result.components] == [
|
||||
"paper.word_review_comment",
|
||||
"paper.manuscript_line_number",
|
||||
"paper.arxiv_submission_stamp",
|
||||
"paper.repeated_running_header",
|
||||
"paper.page_break_word_join",
|
||||
"markdown.html_table_double_escape",
|
||||
"markdown.html_table_layout",
|
||||
"paper.reference_spacing",
|
||||
]
|
||||
assert len(MAPPINGS) == 6
|
||||
|
||||
|
||||
def test_full_pipeline_is_audited_stable_and_idempotent() -> None:
|
||||
pipeline = _build_pipeline()
|
||||
first = pipeline.transform(_combined_markdown())
|
||||
|
||||
assert first.status is RunStatus.SUCCESS
|
||||
assert first.output_markdown is not None
|
||||
assert first.residual_proposals == ()
|
||||
counts: dict[str, int] = {}
|
||||
for change in first.changes:
|
||||
counts[change.component_id] = counts.get(change.component_id, 0) + 1
|
||||
assert counts == {
|
||||
"paper.word_review_comment": 1,
|
||||
"paper.manuscript_line_number": 20,
|
||||
"paper.arxiv_submission_stamp": 1,
|
||||
"paper.repeated_running_header": 2,
|
||||
"paper.page_break_word_join": 1,
|
||||
"markdown.html_table_double_escape": 1,
|
||||
"markdown.html_table_layout": 1,
|
||||
"paper.reference_spacing": 1,
|
||||
}
|
||||
|
||||
second = pipeline.transform(first.output_markdown)
|
||||
assert second.status is RunStatus.SUCCESS
|
||||
assert second.output_markdown == first.output_markdown
|
||||
assert second.changes == ()
|
||||
|
||||
|
||||
def test_business_components_are_not_exported_from_core_namespace() -> None:
|
||||
import mdpolish
|
||||
|
||||
assert not hasattr(mdpolish, "WordReviewCommentComponent")
|
||||
assert not hasattr(mdpolish, "HtmlTableLayoutComponent")
|
||||
@@ -0,0 +1,59 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from mdpolish import Pipeline, RunStatus
|
||||
from mdpolish.components import HtmlTableDoubleEscapeComponent
|
||||
|
||||
|
||||
def transform(markdown: str): # type: ignore[no-untyped-def]
|
||||
return Pipeline([HtmlTableDoubleEscapeComponent()]).transform(markdown)
|
||||
|
||||
|
||||
def test_unescapes_one_layer_only_in_strict_cell_text() -> None:
|
||||
markdown = (
|
||||
'<table data-note="&lt;"><tr><td>&lt;5</td><td>&gt;2 &amp; x <</td></tr></table>'
|
||||
"\noutside &lt;"
|
||||
)
|
||||
result = transform(markdown)
|
||||
|
||||
assert result.status is RunStatus.SUCCESS
|
||||
assert result.output_markdown == (
|
||||
'<table data-note="&lt;"><tr><td><5</td><td>>2 & x <</td></tr></table>'
|
||||
"\noutside &lt;"
|
||||
)
|
||||
assert len(result.changes) == 3
|
||||
|
||||
|
||||
def test_multiple_tables_and_cells_report_source_order() -> None:
|
||||
markdown = "<table><tr><td>&gt;</td></tr></table> x <table><tr><th>&lt;</th></tr></table>"
|
||||
result = transform(markdown)
|
||||
|
||||
assert result.output_markdown == "<table><tr><td>></td></tr></table> x <table><tr><th><</th></tr></table>"
|
||||
assert [change.span.start for change in result.changes] == sorted(change.span.start for change in result.changes)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"markdown",
|
||||
[
|
||||
"outside &lt;",
|
||||
"<table><tr><td>&lt;</tr></table>",
|
||||
"<table><tr><td><em>&lt;</em></td></tr></table>",
|
||||
"<table><tr><td><table><tr><td>&lt;</td></tr></table></td></tr></table>",
|
||||
"<table><tbody><tr><td>&lt;</td></tr></tbody></table>",
|
||||
],
|
||||
)
|
||||
def test_non_strict_or_outside_content_is_preserved(markdown: str) -> None:
|
||||
assert transform(markdown).output_markdown == markdown
|
||||
|
||||
|
||||
def test_fenced_table_is_not_protected() -> None:
|
||||
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```"
|
||||
|
||||
|
||||
def test_successful_output_is_idempotent() -> None:
|
||||
pipeline = Pipeline([HtmlTableDoubleEscapeComponent()])
|
||||
first = pipeline.transform("<table><tr><td>&lt;</td></tr></table>")
|
||||
assert first.output_markdown is not None
|
||||
assert pipeline.transform(first.output_markdown).changes == ()
|
||||
@@ -0,0 +1,63 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from mdpolish import Pipeline, RunStatus
|
||||
from mdpolish.components import HtmlTableLayoutComponent
|
||||
|
||||
TABLE = '<table class="x"><tr><td colspan="2">A</td></tr><tr><td>B</td><td>C</td></tr></table>'
|
||||
|
||||
|
||||
def transform(markdown: str): # type: ignore[no-untyped-def]
|
||||
return Pipeline([HtmlTableLayoutComponent()]).transform(markdown)
|
||||
|
||||
|
||||
def test_expands_rows_without_changing_tags_attributes_or_cells() -> None:
|
||||
result = transform(f"before\n{TABLE}\nafter")
|
||||
|
||||
assert result.status is RunStatus.SUCCESS
|
||||
assert result.output_markdown == (
|
||||
"before\n<table class=\"x\">\n"
|
||||
" <tr><td colspan=\"2\">A</td></tr>\n"
|
||||
" <tr><td>B</td><td>C</td></tr>\n"
|
||||
"</table>\nafter"
|
||||
)
|
||||
assert len(result.changes) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("line_ending", ["\n", "\r\n", "\r"])
|
||||
def test_uses_the_documents_single_line_ending_style(line_ending: str) -> None:
|
||||
markdown = f"before{line_ending}{TABLE}{line_ending}after"
|
||||
result = transform(markdown)
|
||||
assert result.output_markdown is not None
|
||||
assert f"<table class=\"x\">{line_ending} <tr>" in result.output_markdown
|
||||
|
||||
|
||||
def test_table_only_document_uses_lf() -> None:
|
||||
assert transform(TABLE).output_markdown == (
|
||||
'<table class="x">\n'
|
||||
' <tr><td colspan="2">A</td></tr>\n'
|
||||
" <tr><td>B</td><td>C</td></tr>\n"
|
||||
"</table>"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"markdown",
|
||||
[
|
||||
"before\n<table><tr><td>A</td></tr>\r\nafter",
|
||||
"<table>\n <tr><td>A</td></tr>\n</table>",
|
||||
"<table><tr><td>A</tr></table>",
|
||||
"<table><tbody><tr><td>A</td></tr></tbody></table>",
|
||||
"<table><tr><td><em>A</em></td></tr></table>",
|
||||
],
|
||||
)
|
||||
def test_mixed_multiline_or_non_strict_tables_are_preserved(markdown: str) -> None:
|
||||
assert transform(markdown).output_markdown == markdown
|
||||
|
||||
|
||||
def test_successful_output_is_idempotent() -> None:
|
||||
pipeline = Pipeline([HtmlTableLayoutComponent()])
|
||||
first = pipeline.transform(TABLE)
|
||||
assert first.output_markdown is not None
|
||||
assert pipeline.transform(first.output_markdown).changes == ()
|
||||
@@ -0,0 +1,85 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from mdpolish import Pipeline, RunStatus
|
||||
from mdpolish.components import ManuscriptLineNumberComponent
|
||||
|
||||
|
||||
def _document(
|
||||
*,
|
||||
count: int = 20,
|
||||
heading_numbers: frozenset[int] = frozenset({5, 15}),
|
||||
numbers: tuple[int, ...] | None = None,
|
||||
line_ending: str = "\n",
|
||||
) -> str:
|
||||
values = numbers if numbers is not None else tuple(range(1, count + 1))
|
||||
body = [
|
||||
f"## {number} Section {number}" if number in heading_numbers else f"{number} body {number}"
|
||||
for number in values
|
||||
]
|
||||
return line_ending.join(("1 Affiliation", "2 Institute", "## Abstract", *body))
|
||||
|
||||
|
||||
def transform(markdown: str): # type: ignore[no-untyped-def]
|
||||
return Pipeline([ManuscriptLineNumberComponent()]).transform(markdown)
|
||||
|
||||
|
||||
def test_removes_long_monotonic_sequence_but_preserves_pre_abstract_affiliations() -> None:
|
||||
result = transform(_document())
|
||||
|
||||
assert result.status is RunStatus.SUCCESS
|
||||
assert result.output_markdown is not None
|
||||
assert result.output_markdown.startswith("1 Affiliation\n2 Institute\n## Abstract\nbody 1")
|
||||
assert "## Section 5" in result.output_markdown
|
||||
assert len(result.changes) == 20
|
||||
|
||||
|
||||
@pytest.mark.parametrize("line_ending", ["\n", "\r\n", "\r"])
|
||||
def test_preserves_all_supported_line_endings(line_ending: str) -> None:
|
||||
result = transform(_document(line_ending=line_ending))
|
||||
assert result.output_markdown is not None
|
||||
assert result.output_markdown.count(line_ending) == _document(line_ending=line_ending).count(line_ending)
|
||||
|
||||
|
||||
def test_allows_skipped_numbers_when_sequence_is_strictly_increasing() -> None:
|
||||
numbers = tuple(range(10, 30))
|
||||
result = transform(_document(numbers=numbers, heading_numbers=frozenset({14, 24})))
|
||||
|
||||
assert result.status is RunStatus.SUCCESS
|
||||
assert len(result.changes) == 20
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"markdown",
|
||||
[
|
||||
_document(count=19, heading_numbers=frozenset({5, 15})),
|
||||
_document(heading_numbers=frozenset({5})),
|
||||
_document(numbers=(*tuple(range(1, 20)), 10), heading_numbers=frozenset({5, 15})),
|
||||
_document().replace("## Abstract", "## ABSTRACT"),
|
||||
_document() + "\n## Abstract",
|
||||
],
|
||||
)
|
||||
def test_incomplete_or_ambiguous_evidence_preserves_the_document(markdown: str) -> None:
|
||||
result = transform(markdown)
|
||||
assert result.output_markdown == markdown
|
||||
assert result.changes == ()
|
||||
|
||||
|
||||
def test_lists_years_and_numbers_inside_body_are_not_candidates() -> None:
|
||||
markdown = _document() + "\n1. list\n1) list\n2024 report\nThe panel included 35 experts"
|
||||
result = transform(markdown)
|
||||
|
||||
assert result.output_markdown is not None
|
||||
assert result.output_markdown.endswith("1. list\n1) list\n2024 report\nThe panel included 35 experts")
|
||||
assert len(result.changes) == 20
|
||||
|
||||
|
||||
def test_successful_output_is_idempotent() -> None:
|
||||
pipeline = Pipeline([ManuscriptLineNumberComponent()])
|
||||
first = pipeline.transform(_document())
|
||||
assert first.output_markdown is not None
|
||||
second = pipeline.transform(first.output_markdown)
|
||||
|
||||
assert second.status is RunStatus.SUCCESS
|
||||
assert second.changes == ()
|
||||
@@ -0,0 +1,83 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from mdpolish import Pipeline, RunStatus
|
||||
from mdpolish.component import ComponentContractError
|
||||
from mdpolish.components import PageBreakWordJoinComponent
|
||||
|
||||
MAPPINGS = (
|
||||
("possi-", "bly", "possibly"),
|
||||
("SOFA-", "based", "SOFA-based"),
|
||||
("threshold.", "olds", "thresholds"),
|
||||
)
|
||||
|
||||
|
||||
def transform(markdown: str): # type: ignore[no-untyped-def]
|
||||
return Pipeline([PageBreakWordJoinComponent(MAPPINGS)]).transform(markdown)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("markdown", "expected"),
|
||||
[
|
||||
("except possi-\nbly through care", "except possibly through care"),
|
||||
("except possi-\n\nbly through care", "except possibly through care"),
|
||||
("use SOFA-\nbased criteria", "use SOFA-based criteria"),
|
||||
("at threshold.\n\nolds of eight", "at thresholds of eight"),
|
||||
("except possi-\r\n\r\nbly now", "except possibly now"),
|
||||
("except possi-\r\rbly now", "except possibly now"),
|
||||
],
|
||||
)
|
||||
def test_applies_exact_mapping_across_approved_line_shapes(markdown: str, expected: str) -> None:
|
||||
result = transform(markdown)
|
||||
assert result.status is RunStatus.SUCCESS
|
||||
assert result.output_markdown == expected
|
||||
assert len(result.changes) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"markdown",
|
||||
[
|
||||
"except unknown-\nword here",
|
||||
"except possi-\n\n\nbly here",
|
||||
"except possi-\r\n\nbly here",
|
||||
"except POSSI-\nbly here",
|
||||
"except possi-\nblymore here",
|
||||
"except xSOFA-\nbased here",
|
||||
],
|
||||
)
|
||||
def test_unknown_or_unsafe_boundaries_are_preserved(markdown: str) -> None:
|
||||
assert transform(markdown).output_markdown == markdown
|
||||
|
||||
|
||||
def test_parameters_and_results_are_independent_of_mapping_order() -> None:
|
||||
first = PageBreakWordJoinComponent(MAPPINGS)
|
||||
second = PageBreakWordJoinComponent(reversed(MAPPINGS))
|
||||
markdown = "possibly becomes possi-\nbly"
|
||||
|
||||
first_result = Pipeline([first]).transform(markdown)
|
||||
second_result = Pipeline([second]).transform(markdown)
|
||||
|
||||
assert first_result.components == second_result.components
|
||||
assert first_result.output_markdown == second_result.output_markdown
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mappings",
|
||||
[
|
||||
(("", "right", "word"),),
|
||||
(("left", "right", "two words"),),
|
||||
(("left", "right", "word"), ("left", "right", "other")),
|
||||
(("left", "right"),),
|
||||
],
|
||||
)
|
||||
def test_invalid_mappings_raise_contract_error(mappings: object) -> None:
|
||||
with pytest.raises(ComponentContractError):
|
||||
PageBreakWordJoinComponent(mappings) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_successful_output_is_idempotent() -> None:
|
||||
pipeline = Pipeline([PageBreakWordJoinComponent(MAPPINGS)])
|
||||
first = pipeline.transform("except possi-\n\nbly here")
|
||||
assert first.output_markdown is not None
|
||||
assert pipeline.transform(first.output_markdown).changes == ()
|
||||
@@ -0,0 +1,65 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from mdpolish import Pipeline, RunStatus
|
||||
from mdpolish.components import ReferenceSpacingComponent
|
||||
|
||||
|
||||
def transform(markdown: str): # type: ignore[no-untyped-def]
|
||||
return Pipeline([ReferenceSpacingComponent()]).transform(markdown)
|
||||
|
||||
|
||||
def test_normalizes_missing_and_extra_blank_lines_in_references_only() -> None:
|
||||
markdown = "1. Method\n2. Method\n\n## REFERENCES\n\n1. First\n2. Second\n\n\n3. Third"
|
||||
result = transform(markdown)
|
||||
|
||||
assert result.status is RunStatus.SUCCESS
|
||||
assert result.output_markdown == "1. Method\n2. Method\n\n## REFERENCES\n\n1. First\n\n2. Second\n\n3. Third"
|
||||
assert len(result.changes) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("heading", ["# References", "## REFERENCES", "### references", "#### ReFeReNcEs"])
|
||||
def test_accepts_exact_references_heading_with_ascii_case_folding(heading: str) -> None:
|
||||
markdown = f"{heading}\n\n1. First\n2. Second"
|
||||
assert transform(markdown).output_markdown == f"{heading}\n\n1. First\n\n2. Second"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("line_ending", ["\n", "\r\n", "\r"])
|
||||
def test_preserves_line_ending_style(line_ending: str) -> None:
|
||||
markdown = line_ending.join(("## References", "", "1. First", "2. Second"))
|
||||
expected = line_ending.join(("## References", "", "1. First", "", "2. Second"))
|
||||
assert transform(markdown).output_markdown == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"markdown",
|
||||
[
|
||||
"## Reference\n\n1. First\n2. Second",
|
||||
"## References\n\n1. First\n3. Third",
|
||||
"## References\n\n2. Second\n3. Third",
|
||||
"## References\n\n1. First\n### Subsection\n2. Second",
|
||||
"## References\r\n\r\n1. First\r\n\n2. Second",
|
||||
],
|
||||
)
|
||||
def test_ambiguous_or_mixed_sections_are_preserved(markdown: str) -> None:
|
||||
assert transform(markdown).output_markdown == markdown
|
||||
|
||||
|
||||
def test_same_or_higher_heading_ends_section() -> None:
|
||||
markdown = "## References\n\n1. First\n2. Second\n\n## Appendix\n\n1. Keep\n2. Keep"
|
||||
result = transform(markdown)
|
||||
assert result.output_markdown == "## References\n\n1. First\n\n2. Second\n\n## Appendix\n\n1. Keep\n2. Keep"
|
||||
|
||||
|
||||
def test_multiline_reference_uses_its_last_text_line_as_boundary() -> None:
|
||||
markdown = "## References\n\n1. First line\ncontinuation\n2. Second"
|
||||
result = transform(markdown)
|
||||
assert result.output_markdown == "## References\n\n1. First line\ncontinuation\n\n2. Second"
|
||||
|
||||
|
||||
def test_successful_output_is_idempotent() -> None:
|
||||
pipeline = Pipeline([ReferenceSpacingComponent()])
|
||||
first = pipeline.transform("## References\n\n1. First\n2. Second")
|
||||
assert first.output_markdown is not None
|
||||
assert pipeline.transform(first.output_markdown).changes == ()
|
||||
@@ -0,0 +1,64 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from mdpolish import Pipeline, RunStatus
|
||||
from mdpolish.components import RepeatedRunningHeaderComponent
|
||||
|
||||
HEADER = "## Repeated Paper Header"
|
||||
|
||||
|
||||
def transform(markdown: str): # type: ignore[no-untyped-def]
|
||||
return Pipeline([RepeatedRunningHeaderComponent()]).transform(markdown)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("line_ending", ["\n", "\r\n", "\r"])
|
||||
def test_bridges_interrupted_sentence_and_deletes_other_occurrence(line_ending: str) -> None:
|
||||
markdown = line_ending.join(
|
||||
(
|
||||
"Sentence continues in",
|
||||
"",
|
||||
HEADER,
|
||||
"",
|
||||
"the next line.",
|
||||
"",
|
||||
"18. Reference",
|
||||
"",
|
||||
HEADER,
|
||||
"",
|
||||
"19. Reference",
|
||||
)
|
||||
)
|
||||
result = transform(markdown)
|
||||
|
||||
assert result.status is RunStatus.SUCCESS
|
||||
assert result.output_markdown == line_ending.join(
|
||||
("Sentence continues in the next line.", "", "18. Reference", "", "19. Reference")
|
||||
)
|
||||
assert len(result.changes) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"markdown",
|
||||
[
|
||||
f"before\n\n{HEADER}\n\nafter",
|
||||
f"Sentence ends.\n\n{HEADER}\n\nAfter\n\nAnother sentence.\n\n{HEADER}\n\nOther",
|
||||
f"Sentence continues\n\n{HEADER}\n\nAfter\n\nText ends.\n\n{HEADER}\n\nOther",
|
||||
f"Sentence continues\n\n{HEADER}\n\nafter\n{HEADER}\nnot blank",
|
||||
],
|
||||
)
|
||||
def test_missing_or_unsafe_group_evidence_preserves_document(markdown: str) -> None:
|
||||
assert transform(markdown).output_markdown == markdown
|
||||
|
||||
|
||||
def test_header_matching_is_exact_and_not_keyword_based() -> None:
|
||||
markdown = "continues\n\n## Any Header\n\nfrom here\n\n18. Ref\n\n## Any Header\n\n19. Ref"
|
||||
result = transform(markdown)
|
||||
assert result.output_markdown == "continues from here\n\n18. Ref\n\n19. Ref"
|
||||
|
||||
|
||||
def test_successful_output_is_idempotent() -> None:
|
||||
pipeline = Pipeline([RepeatedRunningHeaderComponent()])
|
||||
first = pipeline.transform(f"continues\n\n{HEADER}\n\nfrom here\n\ntext\n\n{HEADER}\n\nend")
|
||||
assert first.output_markdown is not None
|
||||
assert pipeline.transform(first.output_markdown).changes == ()
|
||||
@@ -0,0 +1,69 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from mdpolish import Pipeline, RunStatus
|
||||
from mdpolish.components import WordReviewCommentComponent
|
||||
|
||||
|
||||
def transform(markdown: str): # type: ignore[no-untyped-def]
|
||||
return Pipeline([WordReviewCommentComponent()]).transform(markdown)
|
||||
|
||||
|
||||
def test_deletes_single_line_comment_and_one_following_blank() -> None:
|
||||
markdown = "before\nCommented [AB1]: review this\n\nafter"
|
||||
result = transform(markdown)
|
||||
|
||||
assert result.status is RunStatus.SUCCESS
|
||||
assert result.output_markdown == "before\nafter"
|
||||
assert len(result.changes) == 1
|
||||
assert result.changes[0].reason == "删除严格单行 Word 审阅批注及其后一个空行"
|
||||
|
||||
|
||||
def test_adjacent_comment_blocks_are_deleted_without_overlap() -> None:
|
||||
markdown = "before\nCommented [A1]: first\n\nCommented [B2R1]: second\n\nafter"
|
||||
result = transform(markdown)
|
||||
|
||||
assert result.output_markdown == "before\nafter"
|
||||
assert len(result.changes) == 2
|
||||
assert result.changes[0].span.end == result.changes[1].span.start
|
||||
|
||||
|
||||
@pytest.mark.parametrize("line_ending", ["\n", "\r\n", "\r"])
|
||||
def test_preserves_line_ending_style(line_ending: str) -> None:
|
||||
markdown = line_ending.join(("before", "Commented [A1]: note", "", "after"))
|
||||
assert transform(markdown).output_markdown == line_ending.join(("before", "after"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"comment",
|
||||
[
|
||||
"prefix Commented [A1]: note",
|
||||
" Commented [A1]: note",
|
||||
"Commented []: note",
|
||||
"Commented [A-1]: note",
|
||||
"Commented [A1]:",
|
||||
"Commented [A1]: ",
|
||||
],
|
||||
)
|
||||
def test_similar_lines_are_preserved(comment: str) -> None:
|
||||
markdown = f"before\n{comment}\n\nafter"
|
||||
assert transform(markdown).output_markdown == markdown
|
||||
|
||||
|
||||
def test_requires_a_following_blank_line_and_does_not_delete_extra_blanks() -> None:
|
||||
without_blank = "Commented [A1]: note\nafter"
|
||||
with_two_blanks = "before\nCommented [A1]: note\n\n\nafter"
|
||||
|
||||
assert transform(without_blank).output_markdown == without_blank
|
||||
assert transform(with_two_blanks).output_markdown == "before\n\nafter"
|
||||
|
||||
|
||||
def test_successful_output_is_idempotent() -> None:
|
||||
pipeline = Pipeline([WordReviewCommentComponent()])
|
||||
first = pipeline.transform("Commented [A1]: note\n\nafter")
|
||||
assert first.output_markdown is not None
|
||||
second = pipeline.transform(first.output_markdown)
|
||||
|
||||
assert second.status is RunStatus.SUCCESS
|
||||
assert second.changes == ()
|
||||
Reference in New Issue
Block a user