From 467b070326b8e8f4c2b1419860cf8d3b1428e221 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Mon, 5 Oct 2026 23:59:02 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Notennummer=20und=20R=C3=BCcklinks=20nic?= =?UTF-8?q?ht=20an=20Codeblock-Zeilen=20anh=C3=A4ngen=20(QA2-02)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- epub2md/html_converter.py | 22 ++++++++++++++++++--- tests/test_qa_regressions.py | 38 ++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/epub2md/html_converter.py b/epub2md/html_converter.py index ae76927..f77f94c 100644 --- a/epub2md/html_converter.py +++ b/epub2md/html_converter.py @@ -152,6 +152,23 @@ def indent_block(text: str, first: str, rest: str) -> str: return "\n".join(out) +_BLOCK_SYNTAX = ("```", "~~~", "|", " ", "#", "$$", "\\[", "* * *") + + +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]: out: list[Any] = [] node = el.getparent() @@ -925,7 +942,7 @@ class Renderer: back = " ".join( f"[↩](#{ref})" for _, ref in self.note_refs.get(f"{self.file}:{note.label}", []) ) - anchor_style.append(indent_block(body, f'**{num}:** ', "") + (f" {back}" if back else "")) + anchor_style.append(_labeled_note(f'**{num}:**', blocks, back)) if anchor_style: title = "Anmerkungen" if self.opt.language.startswith("de") else "Notes" out = [f"**{title}**"] + anchor_style + out @@ -946,8 +963,7 @@ class Renderer: num = int(note.label.split("-")[1]) 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, [])) - body = "\n\n".join(blocks) - out.append(f'**{num}.** ' + body + (f" {back}" if back else "")) + out.append(_labeled_note(f'**{num}.**', blocks, back)) return "\n\n".join(out).strip() + "\n" # ------------------------------------------------------------------ images diff --git a/tests/test_qa_regressions.py b/tests/test_qa_regressions.py index 11793e0..fc52114 100644 --- a/tests/test_qa_regressions.py +++ b/tests/test_qa_regressions.py @@ -21,3 +21,41 @@ def test_pipe_in_inline_code_does_not_split_gfm_table(convert): cells = re.split(r"(?K

Text1.

' + '' + '

Zweite2.

' + '' + "

Danach kommt normaler Text.

") + + +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