universalisos/tools/extract_pikeos_docs.py
Fábio Coutada 0d101203e4 feat(docs): extend extraction script to support arbitrary base directories
- Add --base-dir argument to extract_pikeos_docs.py
- Preserve source path relative to the chosen base directory
- Fix Path('.') resolution so --base-dir . works correctly
2026-07-06 23:16:27 +01:00

123 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""Extract PikeOS PDF manuals to markdown files.
Usage:
python3 tools/extract_pikeos_docs.py [--base-dir <dir>] <pdf> [<pdf> ...]
Each PDF is converted to a markdown file under docs-extracted/ preserving the
relative path from <dir> (default: docs/). YAML frontmatter contains title,
source, category, and page count.
"""
import argparse
import re
import subprocess
import sys
from datetime import datetime
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
OUTPUT_DIR = REPO_ROOT / "docs-extracted"
def sanitize_title(filename: str) -> str:
"""Convert a filename like 'kernel-reference-manual.pdf' to a title."""
base = Path(filename).stem
base = base.replace("-", " ").replace("_", " ")
return base.title()
def count_pages(pdf_path: Path) -> int:
"""Return the number of pages in a PDF using pdfinfo if available."""
try:
result = subprocess.run(
["pdfinfo", str(pdf_path)],
capture_output=True,
text=True,
check=True,
)
for line in result.stdout.splitlines():
if line.startswith("Pages:"):
return int(line.split(":", 1)[1].strip())
except Exception:
pass
return 0
def extract_pdf(pdf_path: Path, base_dir: Path) -> Path:
"""Extract a single PDF to a markdown file."""
rel_path = pdf_path.resolve().relative_to(base_dir.resolve())
out_path = OUTPUT_DIR / rel_path.with_suffix(".md")
out_path.parent.mkdir(parents=True, exist_ok=True)
title = sanitize_title(pdf_path.name)
pages = count_pages(pdf_path)
category = rel_path.parent.as_posix() if rel_path.parent != Path(".") else "general"
source_prefix = base_dir.resolve().relative_to(REPO_ROOT).as_posix()
source_display = f"{source_prefix}/{rel_path.as_posix()}"
# Extract plain text with layout preservation.
text = subprocess.run(
["pdftotext", "-layout", "-nopgbrk", str(pdf_path), "-"],
capture_output=True,
text=True,
check=True,
).stdout
# Normalize whitespace slightly without destroying structure.
text = re.sub(r"\n{4,}", "\n\n\n", text)
frontmatter = f"""---
title: "{title}"
source: "{source_display}"
category: "{category}"
pages: {pages}
extracted: "{datetime.now().isoformat()}"
---
# {title}
> Extracted from `{source_display}` ({pages} pages).
> Figures, diagrams, and tables may not render accurately in plain text.
{text}
"""
out_path.write_text(frontmatter, encoding="utf-8")
return out_path
def main() -> int:
parser = argparse.ArgumentParser(description="Extract PikeOS PDFs to markdown.")
parser.add_argument(
"--base-dir",
type=Path,
default=REPO_ROOT / "docs",
help="Base directory for resolving PDF relative paths (default: docs/)",
)
parser.add_argument("pdfs", nargs="+", type=Path, help="PDF files to extract.")
args = parser.parse_args()
created = []
errors = []
for pdf in args.pdfs:
if not pdf.exists():
errors.append(f"Not found: {pdf}")
continue
try:
out = extract_pdf(pdf, args.base_dir)
created.append(out)
print(f"EXTRACTED: {out.relative_to(REPO_ROOT)}")
except Exception as exc:
errors.append(f"{pdf}: {exc}")
print(f"ERROR: {pdf}: {exc}", file=sys.stderr)
if errors:
return 1
return 0
if __name__ == "__main__":
sys.exit(main())