fix: Notennummer und Rücklinks nicht an Codeblock-Zeilen anhängen (QA2-02)
Bei Anmerkungen im Anker-Stil (--format commonmark, --footnotes end-of-book) wurde der Rücklink [↩] an die schließende Fence-Zeile angehängt. Der Codeblock blieb dadurch offen und verschluckte den Rest der Datei; epub2md_merge.py schrieb die Links darin nicht mehr um. Beginnt oder endet eine Anmerkung mit einem Block (Code, Tabelle, Liste, Formel), stehen Nummer bzw. Rücklinks jetzt in eigenem Absatz. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8d7f3de3d7
commit
467b070326
2 changed files with 57 additions and 3 deletions
|
|
@ -152,6 +152,23 @@ def indent_block(text: str, first: str, rest: str) -> str:
|
||||||
return "\n".join(out)
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
_BLOCK_SYNTAX = ("```", "~~~", "|", "<table", "- ", "> ", "#", "$$", "\\[", "* * *")
|
||||||
|
|
||||||
|
|
||||||
|
def _is_block_syntax(block: str) -> bool:
|
||||||
|
return block.startswith(_BLOCK_SYNTAX) or re.match(r"\d+\. ", block) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _labeled_note(label: str, blocks: list[str], back: str) -> str:
|
||||||
|
"""Anchor-style note: label before and backlinks after the body, without gluing them onto a code fence,
|
||||||
|
table or list line (which would break that block)."""
|
||||||
|
body = "\n\n".join(blocks)
|
||||||
|
text = f"{label}\n\n{body}" if _is_block_syntax(blocks[0]) else f"{label} {body}"
|
||||||
|
if back:
|
||||||
|
text += f"\n\n{back}" if _is_block_syntax(blocks[-1]) else f" {back}"
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def _ancestors(el: Any, depth: int = 5) -> list[Any]:
|
def _ancestors(el: Any, depth: int = 5) -> list[Any]:
|
||||||
out: list[Any] = []
|
out: list[Any] = []
|
||||||
node = el.getparent()
|
node = el.getparent()
|
||||||
|
|
@ -925,7 +942,7 @@ class Renderer:
|
||||||
back = " ".join(
|
back = " ".join(
|
||||||
f"[↩](#{ref})" for _, ref in self.note_refs.get(f"{self.file}:{note.label}", [])
|
f"[↩](#{ref})" for _, ref in self.note_refs.get(f"{self.file}:{note.label}", [])
|
||||||
)
|
)
|
||||||
anchor_style.append(indent_block(body, f'<a id="{note.label}"></a>**{num}:** ', "") + (f" {back}" if back else ""))
|
anchor_style.append(_labeled_note(f'<a id="{note.label}"></a>**{num}:**', blocks, back))
|
||||||
if anchor_style:
|
if anchor_style:
|
||||||
title = "Anmerkungen" if self.opt.language.startswith("de") else "Notes"
|
title = "Anmerkungen" if self.opt.language.startswith("de") else "Notes"
|
||||||
out = [f"**{title}**"] + anchor_style + out
|
out = [f"**{title}**"] + anchor_style + out
|
||||||
|
|
@ -946,8 +963,7 @@ class Renderer:
|
||||||
num = int(note.label.split("-")[1])
|
num = int(note.label.split("-")[1])
|
||||||
blocks = self.render_note(note) or ["(leer)"]
|
blocks = self.render_note(note) or ["(leer)"]
|
||||||
back = " ".join(f"[↩]({url_path(rel_link(self.file, f))}#{ref})" for f, ref in self.note_refs.get(note.label, []))
|
back = " ".join(f"[↩]({url_path(rel_link(self.file, f))}#{ref})" for f, ref in self.note_refs.get(note.label, []))
|
||||||
body = "\n\n".join(blocks)
|
out.append(_labeled_note(f'<a id="{note.label}"></a>**{num}.**', blocks, back))
|
||||||
out.append(f'<a id="{note.label}"></a>**{num}.** ' + body + (f" {back}" if back else ""))
|
|
||||||
return "\n\n".join(out).strip() + "\n"
|
return "\n\n".join(out).strip() + "\n"
|
||||||
|
|
||||||
# ------------------------------------------------------------------ images
|
# ------------------------------------------------------------------ images
|
||||||
|
|
|
||||||
|
|
@ -21,3 +21,41 @@ def test_pipe_in_inline_code_does_not_split_gfm_table(convert):
|
||||||
cells = re.split(r"(?<!\\)\|", row.strip()[1:-1])
|
cells = re.split(r"(?<!\\)\|", row.strip()[1:-1])
|
||||||
assert len(cells) == 2, f"Zeile zerfällt in GFM in {len(cells)} Zellen: {row}"
|
assert len(cells) == 2, f"Zeile zerfällt in GFM in {len(cells)} Zellen: {row}"
|
||||||
assert "||" in text and "a | b" in text and "logisches ODER" in text
|
assert "||" in text and "a | b" in text and "logisches ODER" in text
|
||||||
|
|
||||||
|
|
||||||
|
NOTE_WITH_CODE = ('<h1>K</h1><p>Text<a epub:type="noteref" href="#n1" id="r1">1</a>.</p>'
|
||||||
|
'<aside epub:type="footnote" id="n1"><p>Note mit Code:</p><pre>code()\n weiter()</pre></aside>'
|
||||||
|
'<p>Zweite<a epub:type="noteref" href="#n2" id="r2">2</a>.</p>'
|
||||||
|
'<aside epub:type="footnote" id="n2"><pre>nur_code()</pre></aside>'
|
||||||
|
"<p>Danach kommt normaler Text.</p>")
|
||||||
|
|
||||||
|
|
||||||
|
def _fences_closed(text: str) -> bool:
|
||||||
|
"""Every opening ``` fence has a closing line that consists only of backticks."""
|
||||||
|
open_fence = None
|
||||||
|
for line in text.splitlines():
|
||||||
|
s = line.strip()
|
||||||
|
if open_fence is None:
|
||||||
|
m = re.match(r"(`{3,})", s)
|
||||||
|
if m:
|
||||||
|
open_fence = m.group(1)
|
||||||
|
elif re.fullmatch(r"`{3,}", s) and len(s) >= len(open_fence):
|
||||||
|
open_fence = None
|
||||||
|
return open_fence is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_backlink_after_code_block_does_not_break_fence_commonmark(convert):
|
||||||
|
"""QA2-02: the ↩ backlink was appended to the closing ``` line, leaving the fence open."""
|
||||||
|
run = convert(Book(docs=[Doc("c.xhtml", NOTE_WITH_CODE)]), "--format", "commonmark")
|
||||||
|
text = run.chapter("001")
|
||||||
|
assert _fences_closed(text), text
|
||||||
|
assert not re.search(r"\*\*\d+[:.]\*\* ```", text), "Fence hinter Notennummer ist kein Codeblock"
|
||||||
|
assert "[↩](#fnref-001)" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_backlink_after_code_block_does_not_break_fence_end_of_book(convert):
|
||||||
|
run = convert(Book(docs=[Doc("c.xhtml", NOTE_WITH_CODE)]), "--footnotes", "end-of-book")
|
||||||
|
notes = run.chapter("999")
|
||||||
|
assert _fences_closed(notes), notes
|
||||||
|
assert not re.search(r"\*\*\d+[:.]\*\* ```", notes), "Fence hinter Notennummer ist kein Codeblock"
|
||||||
|
assert "[↩](001-k.md#fnref-001)" in notes
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue