avoid dangling structured note anchors

This commit is contained in:
myhloli
2026-08-28 03:52:44 +08:00
parent 58036ae00e
commit 6665024706
2 changed files with 105 additions and 2 deletions
+89 -2
View File
@@ -10,13 +10,31 @@ import hashlib
from lxml import etree # type: ignore[reportMissingImports]
from .._shared.markup import MarkupStylesheet, TextStyle
from .._shared.markup.projector import local_name, visible_raw_text_with_style
from .._shared.markup.projector import BLOCK_TAGS, SKIPPED_TAGS, local_name, visible_raw_text_with_style
_HEADING_TAGS = frozenset({"h1", "h2", "h3", "h4", "h5", "h6"})
_NOTE_TYPES = frozenset({"footnote", "endnote", "rearnote"})
_NOTE_ROLES = frozenset({"doc-footnote", "doc-endnote"})
_XML_ID = "{http://www.w3.org/XML/1998/namespace}id"
_NON_TEXT_BLOCK_TAGS = frozenset(
{
"figure",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"hr",
"math",
"ol",
"pre",
"svg",
"table",
"ul",
}
)
def is_note_element(element: etree._Element) -> bool:
@@ -159,7 +177,9 @@ class HtmlAnchorRegistry:
notes = [
element
for element in root.iter()
if isinstance(element.tag, str) and is_note_element(element) and _visible_element_text(element, stylesheet)
if isinstance(element.tag, str)
and is_note_element(element)
and _note_has_materializable_text_target(element, stylesheet)
]
for ordinal, note in enumerate(notes):
identity = element_id(note) or f"note-{ordinal}"
@@ -226,4 +246,71 @@ def _visible_element_text(element: etree._Element, stylesheet: MarkupStylesheet)
return " ".join(value.split())
def _note_has_materializable_text_target(element: etree._Element, stylesheet: MarkupStylesheet) -> bool:
"""判断 note 是否会投影出可挂载 anchor 的顶层文本 block。"""
inherited = TextStyle()
visibility_hidden = False
chain = [ancestor for ancestor in reversed(list(element.iterancestors())) if isinstance(ancestor.tag, str)]
for ancestor in chain:
resolved = stylesheet.resolve(ancestor, inherited, visibility_hidden)
if resolved.subtree_hidden:
return False
inherited = resolved.text
visibility_hidden = resolved.visibility_hidden
resolved = stylesheet.resolve(element, inherited, visibility_hidden)
if resolved.subtree_hidden:
return False
return _container_materializes_text_block(
element,
stylesheet,
resolved.text,
resolved.visibility_hidden,
)
def _container_materializes_text_block(
element: etree._Element,
stylesheet: MarkupStylesheet,
style: TextStyle,
visibility_hidden: bool,
) -> bool:
"""按共享 projector 的容器分块规则判断是否会产生顶层文本。"""
if not visibility_hidden and (element.text or "").strip():
return True
for child in element:
if isinstance(child.tag, str):
resolved = stylesheet.resolve(child, style, visibility_hidden)
if not resolved.subtree_hidden:
name = local_name(child)
if name == "p":
value = visible_raw_text_with_style(
child,
stylesheet,
resolved.text,
resolved.visibility_hidden,
)
if value.strip():
return True
elif name in BLOCK_TAGS:
if name not in _NON_TEXT_BLOCK_TAGS and _container_materializes_text_block(
child,
stylesheet,
resolved.text,
resolved.visibility_hidden,
):
return True
elif name not in SKIPPED_TAGS:
value = visible_raw_text_with_style(
child,
stylesheet,
resolved.text,
resolved.visibility_hidden,
)
if value.strip():
return True
if not visibility_hidden and (child.tail or "").strip():
return True
return False
__all__ = ["HtmlAnchorRegistry", "append_referenced_notes", "element_id", "is_note_element"]
+16
View File
@@ -400,6 +400,22 @@ def test_html_referenced_external_footnote_keeps_anchor_and_content() -> None:
assert f'id="{footnote.anchor}" class="mineru-page-footnote"' in markdown # type: ignore[union-attr]
def test_html_structured_only_footnote_does_not_create_dangling_anchor() -> None:
"""验证只投影为结构化 block 的脚注不会把正文引用改写为悬空 fragment。"""
payload = b"""<html><body><main><p>Claim <a href="#fn">[1]</a>.</p>
<aside id="fn" role="doc-footnote"><ul><li>Only item</li></ul></aside></main></body></html>"""
middle, model = doc_analyze(payload, file_suffix="html")
markdown = render_markdown(middle)
rendered_html = render_html(middle, standalone=False)
assert model.pages[0][0]["content"] == "Claim [1]."
assert model.pages[0][1]["type"] == BlockType.LIST
assert "Only item" in markdown
assert "](#html-" not in markdown
assert 'href="#html-' not in rendered_html
@pytest.mark.parametrize(
"ancestor_attributes",
['style="display:none"', 'style="opacity:0"', "hidden", 'aria-hidden="true"'],