Two foundational PD textbooks ship from Project Gutenberg as
LaTeX source only — no clean HTML edition. Pandoc fails on PG's
custom preamble macros; a focused regex-based stripper is the
right amount of machinery for the well-known PG TeX format.
What landed
===========
- arborist/sources/textbook_tex.py — TextbookTexSource +
strip_tex pipeline. Drops preamble + line comments + structural
envs (tabular, figure, thebibliography, scshape, …); keeps the
argument of structural-but-content-bearing single-arg commands
(textbf, emph, section, chapter, paragraph, PG's custom \\rfa);
drops zero-arg + brace-arg structural commands (noindent,
thispagestyle, setcounter, label, index, …); substitutes
symbol-level macros (\\to → →, \\neg → ¬, \\forall → ∀, \\S → §,
Greek letters, etc.).
- arborist/cli.py — `--source textbook_tex` accepts --url,
--bundle, or --urls-from. Reuses the existing fetch + chunker
+ Merkle commit + audit pipeline; idempotent at the database
layer (same TeX → same prose → same document_root).
- bench/scripts/textbooks_manifest.py — gains `tex-targets`
subcommand emitting tab-separated `<tex_url>\t<id>` rows for
manifest entries with a `tex_url` field.
- Makefile targets:
textbooks-tex — ingest every entry with a tex_url
textbook-hilbert — convenience for PG #17384
textbook-boole — convenience for PG #15114
Each writes to $(CRAWL_SHARDS_DIR)/textbook_<id>.db, idempotent
on re-run.
- tests/test_textbook_tex.py — 20 unit tests covering preamble +
postmatter stripping, line comments, env drops (tabular, figure),
single-arg keepers (\\textbf, \\emph, \\section, \\rfa),
symbol-level macro subs (10 paramerized cases), structural-cmd
drops, whitespace cleanup, idempotence on already-stripped text.
End-to-end verification
=======================
Smoke test on PG #17384 + #15114:
Hilbert Foundations of Geometry: 1 doc / 65 chunks (192K of
plain prose). FTS5 finds "axiom of parallels" → real chapter
content with axiom references intact (≡, §, math fragments).
Boole Laws of Thought: 1 doc / 273 chunks (829K). FTS5 finds
"law of contradiction" → "the principle of contradiction"
passage from Chapter III of Boole's text.
Vital-books coverage now 6/7 pillars
====================================
Pillar I Logic ✓ Levin + Aristotle Prior + Posterior + Boole
Pillar II Set Theory ✓ Levin
Pillar III Arithmetic ✓ Levin
Pillar IV Geometry ✓ Hilbert (PG TeX)
Pillar V Probability ✗ Kolmogorov license analysis pending
Pillar VI Phys. ✓ Newton Principia
Pillar VII Combin. ✓ Bogart + Keller-Trotter + Levin
Pillar IX λ-Calculus ✗ Church + Turing 1936 papers pending
Test suite: 1574 passed / 28 skipped (was 1554 + 20 new TeX tests).
Out of scope: chunk-resolution + derivations.proof_blob warrant
promotion (#000031 follow-up; see also #000032).
215 lines
4.6 KiB
Python
215 lines
4.6 KiB
Python
"""Tests for the TextbookTexSource (#000031 §5).
|
||
|
||
Pure unit tests over the strip_tex pipeline. Network ingest is
|
||
exercised via `make textbook-hilbert` / `make textbook-boole`; this
|
||
file stays offline.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import pytest
|
||
|
||
from arborist.sources.textbook_tex import strip_tex
|
||
|
||
|
||
# --- preamble + postmatter stripping ---------------------------------
|
||
|
||
|
||
def test_strips_preamble_and_postmatter():
|
||
tex = r"""
|
||
% comment line
|
||
\documentclass{book}
|
||
\usepackage{amsmath}
|
||
\title{Test Book}
|
||
\begin{document}
|
||
This is the body.
|
||
\end{document}
|
||
% trailing junk
|
||
"""
|
||
out = strip_tex(tex)
|
||
assert "documentclass" not in out
|
||
assert "amsmath" not in out
|
||
assert "trailing junk" not in out
|
||
assert "This is the body." in out
|
||
|
||
|
||
def test_drops_line_comments():
|
||
tex = r"""
|
||
\begin{document}
|
||
First sentence. % this is a comment
|
||
Second sentence.
|
||
\end{document}
|
||
"""
|
||
out = strip_tex(tex)
|
||
assert "comment" not in out.lower()
|
||
assert "First sentence." in out
|
||
assert "Second sentence." in out
|
||
|
||
|
||
# --- environment dropping --------------------------------------------
|
||
|
||
|
||
def test_drops_tabular_environment():
|
||
tex = r"""
|
||
\begin{document}
|
||
Before table.
|
||
\begin{tabular}{lr}
|
||
foo & bar \\
|
||
\end{tabular}
|
||
After table.
|
||
\end{document}
|
||
"""
|
||
out = strip_tex(tex)
|
||
assert "Before table." in out
|
||
assert "After table." in out
|
||
assert "foo" not in out
|
||
assert "tabular" not in out
|
||
|
||
|
||
def test_drops_figure_environment():
|
||
tex = r"""
|
||
\begin{document}
|
||
Body 1.
|
||
\begin{figure}
|
||
\includegraphics{img.png}
|
||
\caption{Caption text}
|
||
\end{figure}
|
||
Body 2.
|
||
\end{document}
|
||
"""
|
||
out = strip_tex(tex)
|
||
assert "Body 1." in out
|
||
assert "Body 2." in out
|
||
assert "Caption text" not in out
|
||
|
||
|
||
# --- single-arg command stripping ------------------------------------
|
||
|
||
|
||
def test_keeps_arg_for_emphasis_commands():
|
||
tex = r"""
|
||
\begin{document}
|
||
This is \textbf{bold} and \emph{italic} text.
|
||
\end{document}
|
||
"""
|
||
out = strip_tex(tex)
|
||
assert "bold" in out
|
||
assert "italic" in out
|
||
assert "textbf" not in out
|
||
assert "emph" not in out
|
||
|
||
|
||
def test_keeps_arg_for_section_commands():
|
||
tex = r"""
|
||
\begin{document}
|
||
\section{Introduction}
|
||
\subsection{Background}
|
||
Body text.
|
||
\end{document}
|
||
"""
|
||
out = strip_tex(tex)
|
||
assert "Introduction" in out
|
||
assert "Background" in out
|
||
assert "Body text." in out
|
||
|
||
|
||
def test_keeps_pg_custom_commands():
|
||
"""PG uses \\rfa for roman fixed all-caps section headings."""
|
||
tex = r"""
|
||
\begin{document}
|
||
\rfa{CONTENTS}
|
||
Body.
|
||
\end{document}
|
||
"""
|
||
out = strip_tex(tex)
|
||
assert "CONTENTS" in out
|
||
assert "rfa" not in out
|
||
|
||
|
||
# --- macro substitution ----------------------------------------------
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"tex_in,expected",
|
||
[
|
||
(r"\S 5", "§ 5"),
|
||
(r"A \to B", "A → B"),
|
||
(r"\neg P", "¬ P"),
|
||
(r"P \lor Q", "P ∨ Q"),
|
||
(r"P \land Q", "P ∧ Q"),
|
||
(r"\forall x", "∀ x"),
|
||
(r"\exists y", "∃ y"),
|
||
(r"\alpha + \beta", "α + β"),
|
||
(r"\dots", "…"),
|
||
(r"x \in S", "x ∈ S"),
|
||
],
|
||
)
|
||
def test_macro_substitutions(tex_in, expected):
|
||
tex = r"\begin{document}" + "\n" + tex_in + "\n" + r"\end{document}"
|
||
out = strip_tex(tex)
|
||
assert expected in out
|
||
|
||
|
||
# --- structural commands dropped -------------------------------------
|
||
|
||
|
||
def test_drops_structural_commands():
|
||
tex = r"""
|
||
\begin{document}
|
||
\noindent Body. \bigskip
|
||
\thispagestyle{empty}
|
||
\setcounter{page}{1}
|
||
\label{intro}
|
||
\index{topic}
|
||
More body.
|
||
\end{document}
|
||
"""
|
||
out = strip_tex(tex)
|
||
assert "Body." in out
|
||
assert "More body." in out
|
||
assert "noindent" not in out
|
||
assert "thispagestyle" not in out
|
||
assert "setcounter" not in out
|
||
assert "label" not in out
|
||
assert "index" not in out
|
||
|
||
|
||
# --- whitespace cleanup ----------------------------------------------
|
||
|
||
|
||
def test_collapses_multi_blank_lines():
|
||
tex = r"""
|
||
\begin{document}
|
||
First.
|
||
|
||
|
||
|
||
Second.
|
||
|
||
|
||
|
||
Third.
|
||
\end{document}
|
||
"""
|
||
out = strip_tex(tex)
|
||
# No more than 2 consecutive newlines (= 1 blank line)
|
||
assert "\n\n\n" not in out
|
||
|
||
|
||
# --- end-to-end stability --------------------------------------------
|
||
|
||
|
||
def test_idempotent_on_already_stripped_text():
|
||
"""Plain ASCII text without any LaTeX should pass through with
|
||
only whitespace normalization."""
|
||
tex = r"""
|
||
\begin{document}
|
||
This is plain text with no LaTeX commands at all.
|
||
Multiple sentences. No special characters.
|
||
\end{document}
|
||
"""
|
||
out = strip_tex(tex)
|
||
assert "This is plain text" in out
|
||
# Round-trip: stripping again should produce the same output.
|
||
out2 = strip_tex(r"\begin{document}" + out + r"\end{document}")
|
||
assert out2 == out
|