Files
docview/tests/generate_web_fixtures.py
T
2026-09-21 13:41:40 +09:00

72 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Generate deterministic, project-authored HTML/ZIP/EPUB acceptance fixtures."""
from pathlib import Path
import hashlib
import json
import zipfile
ROOT = Path(__file__).resolve().parent / "fixtures" / "web"
ROOT.mkdir(parents=True, exist_ok=True)
def write(path, data):
target = ROOT / path
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(data.encode("utf-8") if isinstance(data, str) else data)
def archive(name, entries):
with zipfile.ZipFile(ROOT / name, "w") as output:
for path, content in entries.items():
info = zipfile.ZipInfo(path, (2026, 1, 1, 0, 0, 0))
info.create_system = 3
info.external_attr = 0o100600 << 16
output.writestr(info, content, compress_type=zipfile.ZIP_STORED if path == "mimetype" else zipfile.ZIP_DEFLATED)
style = "body{font-family:serif;margin:32px;line-height:1.6;color:#16293a}h1{border-bottom:3px solid #147d79}section{min-height:720px}.hidden{display:none}nav{background:#eef8f8;padding:16px}"
svg = '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="80"><rect width="200" height="80" fill="#147d79"/><text x="12" y="46" font-size="24" fill="white">Local SVG</text></svg>'
html = """<!doctype html><html lang="ja"><head><meta charset="UTF-8"><title>DocView local document</title><link rel="stylesheet" href="assets/style.css"></head><body>
<h1 id="start">文書リーダー検証 / Local document</h1><nav role="doc-toc"><a href="#second">第二節へ</a> · <a href="next.html">別のHTML</a></nav>
<img src="assets/local.svg" alt="Local asset"><section><h2 id="first">第一節</h2><p>日本語、Latin, <ruby>漢字<rt>かんじ</rt></ruby>。This is locally authored acceptance text.</p><h3 class="hidden">Hidden heading</h3></section>
<section><h2 id="second">第二節</h2><p>同じ文書内の資源とアンカーを検証します。</p><div role="heading" aria-level="3">ARIA見出し</div><p><a href="#start">先頭へ戻る</a></p></section></body></html>"""
write("html/index.html", html)
write("html/assets/style.css", style)
write("html/assets/local.svg", svg)
write("html/next.html", '<!doctype html><html><meta charset="UTF-8"><title>Second resource</title><h1>別HTML</h1><a href="index.html#second">第二節へ戻る</a></html>')
write("html/vertical.html", '<!doctype html><html lang="ja"><meta charset="UTF-8"><style>body{writing-mode:vertical-rl;height:85vh;line-height:2;font-size:22px}section{min-width:900px}</style><section><h1>縦書き第一節</h1><p>縦書きの読書方向と物理スクロールを分けて検証する。</p></section><section><h2>縦書き第二節</h2><p>章内進捗は右から左へ増加する。</p></section></html>')
write("html/security.html", """<!doctype html><html><meta charset="UTF-8"><title>Static security fixture</title>
<meta http-equiv="refresh" content="1;url=next.html"><style>@import url('http://127.0.0.1:18765/style.css');@font-face{font-family:blocked;src:url('http://127.0.0.1:18765/font.woff')}</style>
<h1>Static security fixture</h1><p id="script-marker">SCRIPT DID NOT RUN</p>
<script>document.getElementById('script-marker').textContent='SCRIPT EXECUTED';fetch('http://127.0.0.1:18765/script');</script>
<img src="http://127.0.0.1:18765/image"><iframe src="file:///etc/passwd"></iframe><iframe src="http://127.0.0.1:18765/frame"></iframe>
<form action="http://127.0.0.1:18765/form"><input value="READ ONLY"><button>Submit</button></form>
<a href="file:///etc/passwd">Blocked file</a><a href="doc://different-session/index.html">Blocked other session</a>
<a href="http://127.0.0.1:18765/external">Explicit external link</a></html>""")
entries = {"book/index.html": html, "book/assets/style.css": style, "book/assets/local.svg": svg,
"book/next.html": (ROOT / "html/next.html").read_text()}
archive("html-book.zip", entries)
archive("html-entry-choice.zip", {"one.html": "<h1>One</h1>", "two.html": "<h1>Two</h1>"})
archive("unsafe-traversal.zip", {"../outside.html": "<h1>Must not extract</h1>"})
container = '<container xmlns="urn:oasis:names:tc:opendocument:xmlns:container" version="1.0"><rootfiles><rootfile full-path="OPS/package.opf" media-type="application/oebps-package+xml"/></rootfiles></container>'
package = '''<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="uid"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>DocView EPUB verification</dc:title><dc:identifier id="uid">urn:uuid:docview-fixture</dc:identifier><dc:language>ja</dc:language></metadata><manifest><item id="first" href="first.xhtml" media-type="application/xhtml+xml"/><item id="second" href="second.xhtml" media-type="application/xhtml+xml"/><item id="extra" href="extra.xhtml" media-type="application/xhtml+xml"/><item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/><item id="css" href="style.css" media-type="text/css"/></manifest><spine page-progression-direction="rtl"><itemref idref="first"/><itemref idref="extra" linear="no"/><itemref idref="second"/></spine></package>'''
nav = '''<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops"><head><title>目次</title></head><body><nav epub:type="toc"><ol><li><a href="second.xhtml">第二章(目次では先)</a></li><li><a href="first.xhtml">第一章(通常読書では先)</a></li><li><a href="extra.xhtml">補遺 / nonlinear</a></li></ol></nav><nav epub:type="page-list"><ol><li><a href="first.xhtml#p1">i</a></li><li><a href="second.xhtml#p2">1</a></li></ol></nav></body></html>'''
chapter = lambda title, anchor: f'<html xmlns="http://www.w3.org/1999/xhtml" lang="ja"><head><title>{title}</title><link rel="stylesheet" href="style.css"/></head><body><h1 id="{anchor}">{title}</h1><p>著者指定の本文とルビを表示します。<ruby>読書<rt>どくしょ</rt></ruby>。</p><section><h2>章内の見出し</h2><p>Static chapter content.</p></section></body></html>'
epub = {"mimetype": "application/epub+zip", "META-INF/container.xml": container, "OPS/package.opf": package,
"OPS/nav.xhtml": nav, "OPS/first.xhtml": chapter("第一章", "p1"), "OPS/second.xhtml": chapter("第二章", "p2"),
"OPS/extra.xhtml": chapter("補遺", "extra"), "OPS/style.css": style}
archive("reflow-rtl.epub", epub)
fixed = dict(epub)
fixed["OPS/package.opf"] = package.replace('</metadata>', '<meta property="rendition:layout">pre-paginated</meta></metadata>').replace('<itemref idref="second"/>', '<itemref idref="second" properties="rendition:layout-reflowable"/>')
fixed["OPS/first.xhtml"] = '<html xmlns="http://www.w3.org/1999/xhtml"><head><title>Fixed page</title><meta name="viewport" content="width=900,height=650"/></head><body style="margin:0;width:900px;height:650px;background:#147d79;color:white"><h1 style="position:absolute;left:60px;top:70px">Fixed layout 900 × 650</h1><p style="position:absolute;left:60px;top:190px;font-size:26px">Second chapter switches to reflow.</p></body></html>'
archive("fixed-mixed.epub", fixed)
epub2 = dict(epub)
epub2["OPS/package.opf"] = package.replace('version="3.0"', 'version="2.0"').replace('<spine ', '<spine toc="ncx" ').replace('</manifest>', '<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/></manifest>')
epub2["OPS/nav.xhtml"] = '<malformed'
epub2["OPS/toc.ncx"] = '<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/"><navMap><navPoint id="chapter"><navLabel><text>NCX chapter</text></navLabel><content src="first.xhtml"/></navPoint></navMap></ncx>'
archive("epub2-ncx.epub", epub2)
manifest = {str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(ROOT.rglob("*")) if p.is_file() and p.name != "manifest.json"}
write("manifest.json", json.dumps({"origin": "Original project-authored fixtures; no external documents", "license": "Same as the DocView source repository", "sha256": manifest}, ensure_ascii=False, indent=2) + "\n")
print(f"Generated {len(manifest)} fixtures under {ROOT}")