#!/usr/bin/env python3 """Build a styled HTML file from chapter markdown drafts for email/review.""" import argparse import re import sys from pathlib import Path import markdown CHAPTER_DIR = Path(__file__).parent / "chapters" / "book1" OUTPUT_DIR = Path(__file__).parent / "exports" HTML_TEMPLATE = """\ The Shade Series - Book 1: The Unbreakable Curse (Draft)

The Unbreakable Curse

Book 1 of The Shade Series
DRAFT - Chapters {chapter_label} - For Review Only
{toc} {content} """ def parse_chapter_range(spec: str) -> list[int]: """Parse a chapter spec like '1-5' or '1,3,5' or '3' into a list of ints.""" chapters = [] for part in spec.split(","): part = part.strip() if "-" in part: start, end = part.split("-", 1) chapters.extend(range(int(start), int(end) + 1)) else: chapters.append(int(part)) return sorted(set(chapters)) def find_chapter_file(num: int) -> Path: """Find the best available file for a chapter number (prefer final, fall back to draft).""" final = CHAPTER_DIR / f"ch{num:02d}-final.md" draft = CHAPTER_DIR / f"ch{num:02d}-draft.md" if final.exists(): return final if draft.exists(): return draft return None def convert_scene_breaks(html: str) -> str: """Replace bare '* * *' paragraphs with styled scene breaks.""" html = html.replace("

* * *

", '

* * *

') return html def extract_title(text: str) -> str: """Extract the chapter title from the first # heading in the markdown.""" match = re.match(r"^#\s+(.+)$", text.strip(), re.MULTILINE) return match.group(1) if match else None def add_heading_id(html: str, anchor_id: str) -> str: """Add an id attribute to the first

tag in the HTML.""" return html.replace("

", f'

', 1) def build_toc(entries: list[tuple[str, str]]) -> str: """Build a table of contents HTML block from (anchor_id, title) pairs.""" items = "\n".join( f'
  • {title}
  • ' for aid, title in entries ) return f'' def build(chapters: list[int]) -> str: """Combine and convert chapters to styled HTML.""" md = markdown.Markdown(extensions=["smarty"]) combined_html = [] toc_entries = [] for num in chapters: path = find_chapter_file(num) if path is None: print(f"Warning: No file found for chapter {num}, skipping.", file=sys.stderr) continue text = path.read_text(encoding="utf-8") title = extract_title(text) anchor_id = f"ch{num:02d}" if title: toc_entries.append((anchor_id, title)) md.reset() html = md.convert(text) html = convert_scene_breaks(html) html = add_heading_id(html, anchor_id) combined_html.append(html) print(f" Added: {path.name}") if not combined_html: print("Error: No chapters found.", file=sys.stderr) sys.exit(1) if len(chapters) == 1: chapter_label = str(chapters[0]) else: chapter_label = f"{chapters[0]}-{chapters[-1]}" toc = build_toc(toc_entries) if len(toc_entries) > 1 else "" content = "\n\n".join(combined_html) return HTML_TEMPLATE.format(content=content, chapter_label=chapter_label, toc=toc) def main(): parser = argparse.ArgumentParser(description="Build HTML from chapter markdown drafts.") parser.add_argument( "-chapters", required=True, help="Chapter range: e.g. '1-5', '1,3,5', or '3'", ) parser.add_argument( "-o", "--output", help="Output filename (default: auto-generated in exports/)", ) args = parser.parse_args() chapters = parse_chapter_range(args.chapters) print(f"Building HTML for chapters: {chapters}") html = build(chapters) OUTPUT_DIR.mkdir(parents=True, exist_ok=True) if args.output: out_path = Path(args.output) else: if len(chapters) == 1: label = f"ch{chapters[0]:02d}" else: label = f"ch{chapters[0]:02d}-{chapters[-1]:02d}" out_path = OUTPUT_DIR / f"book1-{label}-review.html" out_path.write_text(html, encoding="utf-8") print(f"Output: {out_path}") if __name__ == "__main__": main()