diff --git a/epub2md/html_converter.py b/epub2md/html_converter.py
index f3aeeb8..7490a17 100644
--- a/epub2md/html_converter.py
+++ b/epub2md/html_converter.py
@@ -616,7 +616,7 @@ class Renderer:
if self.ch:
self.ch.stats.tables_html += 1
self.diag.warn("table-html", f"Tabelle enthält {reason}; als HTML erhalten.", self._source(el), self.file)
- return ([anchor] if anchor else []) + [self.clean_html(el)]
+ return [self.clean_html(el)] # clean_html keeps the id on
itself
out = [anchor] if anchor else []
if caption_el is not None:
cap = join_inline(self.inline_children(caption_el), hard_break=" ")
diff --git a/epub2md/linkcheck.py b/epub2md/linkcheck.py
index 78241ff..1503f0b 100644
--- a/epub2md/linkcheck.py
+++ b/epub2md/linkcheck.py
@@ -10,7 +10,9 @@ from urllib.parse import unquote
FENCE = re.compile(r"^(`{3,}|~{3,}).*?$.*?^\1\s*$", re.M | re.S)
# inline code spans never cross a blank line and never start at an escaped backtick
INLINE_CODE = re.compile(r"(?]*>|[^)\s]+)(?:\s+\"[^\"]*\")?\)")
+# link text: never starts at an escaped \[, never spans a blank line, may contain one bracket level
+# (image links like [](target))
+MD_LINK = re.compile(r"(?]*>|[^)\s]+)(?:\s+\"[^\"]*\")?\)")
HTML_ATTR = re.compile(r"""\b(?:href|src)="([^"]*)\"""")
ANCHOR = re.compile(r"""\bid="([^"]+)\"""")
HEADING = re.compile(r"^#{1,6}\s+(.*?)\s*#*\s*$", re.M)
@@ -38,7 +40,14 @@ def anchors_in(text: str) -> set[str]:
def extract_links(text: str) -> list[str]:
body = strip_code(text)
- links = [m.group(1) for m in MD_LINK.finditer(body)]
+ links: list[str] = []
+
+ def collect(text: str) -> None:
+ for m in MD_LINK.finditer(text):
+ links.append(m.group(2))
+ collect(m.group(1)) # links/images inside the link text
+
+ collect(body)
links += HTML_ATTR.findall(body)
return [link[1:-1] if link.startswith("<") and link.endswith(">") else link for link in links]
diff --git a/tests/test_links.py b/tests/test_links.py
index 6b22d8e..6a4bf8f 100644
--- a/tests/test_links.py
+++ b/tests/test_links.py
@@ -66,3 +66,18 @@ def test_link_checker_detects_problems():
}
broken = check_links(files, {"media/images/a.png"})
assert {b["link"] for b in broken} == {"002-b.md#nein", "003.md"}
+
+
+def test_link_checker_ignores_escaped_brackets():
+ """An escaped \\[ must not start a link that swallows the following paragraphs."""
+ files = {
+ "a.md": '\\[»\\] Hinweis\n\nText [kaputt](fehlt.md) und \\[1\\]\n\n[ok](#x)\n',
+ }
+ broken = check_links(files, set())
+ assert [b["link"] for b in broken] == ["fehlt.md"]
+
+
+def test_link_checker_handles_image_links():
+ files = {"a.md": "[](fehlt.md) [](#x) \n"}
+ broken = check_links(files, {"bild.png"})
+ assert [b["link"] for b in broken] == ["fehlt.md"]
diff --git a/tests/test_tables_lists.py b/tests/test_tables_lists.py
index 3fab505..8817fb6 100644
--- a/tests/test_tables_lists.py
+++ b/tests/test_tables_lists.py
@@ -63,3 +63,10 @@ def test_unknown_elements_are_reported_and_keep_html(convert):
kept = convert(Book(docs=[Doc("c.xhtml", body)]), "--keep-html")
assert 'Inhalt bleibt' in kept.chapter("001")
assert "evil" not in kept.chapter("001")
+
+
+def test_complex_table_anchor_is_emitted_once(convert):
+ body = ('T
Tabelle 1
")
+ text = convert(Book(docs=[Doc("c.xhtml", body)])).chapter("001")
+ assert text.count('id="t1"') == 1