feat: epub2md – EPUB nach kapitelweisem Markdown
Kommandozeilenwerkzeug, das DRM-freie EPUB-2/3-Dateien offline in kapitelweise Markdown-Dateien umwandelt. Erzeugt werden außerdem Medien, metadata.yaml, manifest.json, conversion-report.json und Diagnose-Dateien. - Spine-basierte Kapitelbildung mit --chapter-level, epub:type-Abschnitten, Fortsetzungsdateien und Erkennung von Layout-Überschriften - Code mit exakter Einrückung, sicheren Fences und konservativer Spracherkennung - MathML->TeX (konservativ), TeX aus alt-Texten, SVG-/Bildformeln bleiben Bilder - Fußnoten-Modi end-of-chapter, end-of-book, inline und keep-links - Interne Links/Anker werden umgeschrieben, Linkcheck über die Ausgabe - Sicher: kein Entpacken, Schutz vor Path Traversal und XXE, DRM-Erkennung, atomares Schreiben, --force nur für frühere Ausgaben - 82 pytest-Tests mit synthetischen EPUBs und Invariantentests für Beispiele/ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
02449734a2
1 changed files with 7726 additions and 0 deletions
88
tests/test_codeblocks.py
Normal file
88
tests/test_codeblocks.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import re
|
||||
|
||||
from conftest import Book, Doc
|
||||
|
||||
PY = """def read_book(path):
|
||||
if path:
|
||||
return path.read_text(encoding="utf-8") # <utf-8>
|
||||
\treturn None"""
|
||||
|
||||
|
||||
def fenced_blocks(text: str) -> list[tuple[str, str]]:
|
||||
return re.findall(r"^(`{3,})([\w+-]*)\n(.*?)\n\1$", text, re.M | re.S)
|
||||
|
||||
|
||||
def test_python_indentation_and_special_chars(convert):
|
||||
body = f'<h1>Code</h1><pre><code class="language-python">{PY}</code></pre>'
|
||||
run = convert(Book(docs=[Doc("c.xhtml", body)]))
|
||||
blocks = fenced_blocks(run.chapter("001"))
|
||||
assert len(blocks) == 1
|
||||
fence, lang, code = blocks[0]
|
||||
assert lang == "python"
|
||||
assert code == PY.replace("<", "<").replace(">", ">")
|
||||
assert "<" not in code
|
||||
|
||||
|
||||
def test_shell_code_with_pipes_backslashes_and_dollar(convert):
|
||||
shell = "$ cat datei.txt | grep -v '^#' \\\n | sort && echo \"$HOME\" > out.txt"
|
||||
body = f"<h1>Shell</h1><pre>{shell}</pre>"
|
||||
run = convert(Book(docs=[Doc("c.xhtml", body)]))
|
||||
(_, lang, code), = fenced_blocks(run.chapter("001"))
|
||||
assert code == "$ cat datei.txt | grep -v '^#' \\\n | sort && echo \"$HOME\" > out.txt"
|
||||
assert lang == "" # no reliable language signal -> no label
|
||||
|
||||
|
||||
def test_backticks_in_code_get_longer_fence(convert):
|
||||
body = "<h1>T</h1><pre>echo ```nested``` and `x`</pre>"
|
||||
run = convert(Book(docs=[Doc("c.xhtml", body)]))
|
||||
(fence, _, code), = fenced_blocks(run.chapter("001"))
|
||||
assert fence == "````"
|
||||
assert code == "echo ```nested``` and `x`"
|
||||
|
||||
|
||||
def test_br_lines_and_highlight_spans_in_pre(convert):
|
||||
body = ('<h1>C</h1><div class="listing"><pre><span class="kw">#include</span> <stdio.h><br/><br/>'
|
||||
'int main(void) {<br/> printf("Hallo\\n");<br/>} </pre></div>')
|
||||
run = convert(Book(docs=[Doc("c.xhtml", body)]))
|
||||
(_, lang, code), = fenced_blocks(run.chapter("001"))
|
||||
assert lang == "c"
|
||||
assert code == '#include <stdio.h>\n\nint main(void) {\n printf("Hallo\\n");\n} '
|
||||
|
||||
|
||||
def test_listing_div_with_span_pre(convert):
|
||||
body = ('<h1>K</h1><div class="divUnProgramlisting"><span class="pre"><strong>def</strong> f(x):\n'
|
||||
' <strong>return</strong> 2 * x\n\n<strong>def</strong> g(x):\n <strong>return</strong> x</span></div>')
|
||||
run = convert(Book(docs=[Doc("c.xhtml", body)]))
|
||||
(_, lang, code), = fenced_blocks(run.chapter("001"))
|
||||
assert lang == "python"
|
||||
assert code == "def f(x):\n return 2 * x\n\ndef g(x):\n return x"
|
||||
|
||||
|
||||
def test_line_numbers_are_removed(convert):
|
||||
body = '<h1>L</h1><pre><span class="lineno">1</span>a = 1<br/><span class="lineno">2</span>b = 2</pre>'
|
||||
run = convert(Book(docs=[Doc("c.xhtml", body)]))
|
||||
(_, _, code), = fenced_blocks(run.chapter("001"))
|
||||
assert code == "a = 1\nb = 2"
|
||||
|
||||
|
||||
def test_inline_code_variants(convert):
|
||||
body = ('<h1>I</h1><p>Nutze <code>a*b_c</code>, <span class="tt">x | y</span> und <code>`tick`</code> '
|
||||
'sowie <kbd>Strg</kbd>.</p>')
|
||||
run = convert(Book(docs=[Doc("c.xhtml", body)]))
|
||||
text = run.chapter("001")
|
||||
assert "`a*b_c`" in text
|
||||
assert "`x | y`" in text
|
||||
assert "`` `tick` ``" in text
|
||||
assert "<kbd>Strg</kbd>" in text
|
||||
|
||||
|
||||
def test_prose_is_not_forced_into_code(convert):
|
||||
body = "<h1>P</h1><p>Ein normaler Absatz mit Leerzeichen.</p>"
|
||||
run = convert(Book(docs=[Doc("c.xhtml", body)]))
|
||||
assert "```" not in run.chapter("001")
|
||||
assert "Ein normaler Absatz mit Leerzeichen." in run.chapter("001")
|
||||
|
||||
|
||||
def test_unlabeled_code_is_reported(convert):
|
||||
run = convert(Book(docs=[Doc("c.xhtml", "<h1>U</h1><pre>foo bar</pre>")]))
|
||||
assert run.report["code_blocks"]["languages"] == {"(keine)": 1}
|
||||
Loading…
Reference in a new issue