feat: Add book viewer at /book with shared design system

- Add html/index.html: book viewer with auto-discovering sidebar,
  prev/next navigation, keyboard shortcuts, and URL hash persistence
- Add html/book-page.css: shared stylesheet for all book pages derived
  from fabula-ultima-sheet.css (dark theme, CSS variables, Cinzel/
  Crimson Text fonts, common class styles)
- Add book.js entry point so webpack injects the shared CSS into the
  book viewer; update webpack.config.js for two entry points, split
  CSS chunk, CopyWebpackPlugin for book pages, and /book dev server
  rewrite rule
- Add scripts/strip_watermark.py: removes "Guest Customer (Order
  #52072168)" watermark artifacts from all 210 book pages
- Add scripts/restyle_book.py: strips per-page <style> blocks and
  injects <link rel="stylesheet" href="book-page.css"> into all pages
- Update Justfile deploy to scp -r dist/* for the new /book subtree

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 03:36:35 +00:00
parent 58552b536f
commit c75cd188c1
220 changed files with 12685 additions and 10 deletions

85
scripts/restyle_book.py Normal file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""
Remove per-page <style> blocks from book pages and replace them with a
shared <link rel="stylesheet" href="book-page.css"> derived from the main
fabula-ultima-sheet.css design system.
"""
import glob
import os
import re
HTML_DIR = os.path.join(os.path.dirname(__file__), "..", "html")
# Remove <style>...</style> blocks (including surrounding blank lines)
STYLE_BLOCK_RE = re.compile(r"\s*<style[^>]*>.*?</style>", re.DOTALL | re.IGNORECASE)
# Remove any existing Google Fonts <link> tags
GFONTS_LINK_RE = re.compile(
r'\s*<link[^>]+fonts\.googleapis\.com[^>]*>',
re.IGNORECASE,
)
CSS_LINK = ' <link rel="stylesheet" href="book-page.css">'
def process_file(filepath: str) -> bool:
with open(filepath, encoding="utf-8") as f:
original = f.read()
content = original
# Strip <style> blocks
content = STYLE_BLOCK_RE.sub("", content)
# Strip any Google Fonts <link> (fonts are now loaded by book-page.css)
content = GFONTS_LINK_RE.sub("", content)
# Inject the shared stylesheet link, handling three head structures:
# 1. Has </title> → insert after it
# 2. Has </head> but no </title> → insert before </head>
# 3. No <head> at all (bare fragment) → prepend link at top of file
if CSS_LINK not in content:
if re.search(r"</title>", content, re.IGNORECASE):
content = re.sub(
r"(</title>)",
r"\1\n" + CSS_LINK,
content, count=1, flags=re.IGNORECASE,
)
elif re.search(r"</head>", content, re.IGNORECASE):
content = re.sub(
r"(</head>)",
CSS_LINK + r"\n\1",
content, count=1, flags=re.IGNORECASE,
)
else:
content = CSS_LINK + "\n" + content
if content == original:
return False
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
return True
def main() -> None:
def sort_key(p):
m = re.search(r"(\d+)", os.path.basename(p))
return int(m.group(1)) if m else -1
html_files = sorted(
glob.glob(os.path.join(HTML_DIR, "[0-9]*.html")),
key=sort_key,
)
changed = 0
for filepath in html_files:
if process_file(filepath):
changed += 1
print(f"Done. {changed}/{len(html_files)} pages updated.")
if __name__ == "__main__":
main()