losalamos.documents#

Handle Documents editing, managing and builds.

Class hierarchy#

DataSet
└── Document                    (base for all document types)
    └── DocumentTeX             (base for all TeX documents)
        ├── Preprint            (preprint / arXiv-style)
        ├── Report              (standard report)
        ├── ReportLarge         (multi-chapter report)
        └── Memo                (short internal memo)

Template resolution#

Every new() call merges up to three template layers in priority order:

BASE_TEMPLATE  ←  VARIANT_TEMPLATE  ←  template_overlay (user)
  • BASE_TEMPLATEdefined on DocumentTeX; canonical TeX base shared

    by all document types.

  • VARIANT_TEMPLATEdefined per subclass; files that differentiate the

    document type (cover, class options, etc.).

  • template_overlayoptional user-supplied folder for private or

    client-specific files (logos, confidential covers).

Each layer wins over the one to its left. Files absent from a layer are inherited transparently from the layer below.

Functions

Classes

Document([name, alias])

Base class for all document types.

DocumentTeX([name, alias])

Base class for all TeX-based document types.

Essay([name, alias])

Professional([name, alias])

losalamos.documents.escape_percent_latex(text: str) str[source]#
class losalamos.documents.Document(name='MyDocument', alias='Doc')[source]#

Bases: DataSet

Base class for all document types.

Handles generic file loading and the three-layer template merge used by new(). Subclasses specialise behaviour by overriding BASE_TEMPLATE and VARIANT_TEMPLATE.

BASE_TEMPLATE = PosixPath('/home/runner/work/losalamos/losalamos/src/losalamos/data/templates/documents/txt/demo')#
VARIANT_TEMPLATE = None#
load_data(file_data)[source]#

Load data from file.

Parameters:

file_data (str) – file path to data.

new(name, folder, template_overlay=None)[source]#

Create a new document folder by materializing template files into a target location.

This method creates a folder named name inside folder and populates it by merging up to three template layers in the following priority order:

BASE_TEMPLATE  ←  VARIANT_TEMPLATE  ←  template_overlay

Each layer wins over the one to its left. Layers that are None are skipped transparently. The caller is responsible for ensuring the target path does not already exist; if it does, a FileExistsError is raised immediately and no files are written.

Single-layer mode (only BASE_TEMPLATE, both VARIANT_TEMPLATE and template_overlay are None):

The entire BASE_TEMPLATE directory tree is copied as-is into the target folder, preserving all subdirectories and files.

Multi-layer mode (one or both of VARIANT_TEMPLATE, template_overlay are provided):

All active layers are scanned recursively and merged into the target.

  • Files present in only one layer are copied from that layer.

  • Files present in multiple layers are resolved in favour of the highest-priority layer (right-most in the chain).

Merge example — three active layers
BASE_TEMPLATE/      VARIANT_TEMPLATE/     template_overlay/
├── main.tex        ├── main.tex           └── cover.tex
├── preamble.tex    └── cover.tex
└── refs.bib

# Resolution:
# main.tex     → VARIANT_TEMPLATE  (highest priority)
# cover.tex    → template_overlay  (highest priority)
# preamble.tex → BASE_TEMPLATE     (only source)
# refs.bib     → BASE_TEMPLATE     (only source)

# Result in target:
folder/name/
├── main.tex        # from VARIANT_TEMPLATE
├── cover.tex       # from template_overlay
├── preamble.tex    # from BASE_TEMPLATE
└── refs.bib        # from BASE_TEMPLATE
Parameters:
  • name (str) – Name of the new document folder to be created inside folder.

  • folder (str or pathlib.Path) – Parent directory under which the new document folder is created. Must already exist as a directory on the filesystem.

  • template_overlay (str, pathlib.Path, or None) – Optional user-supplied directory whose files take priority over both BASE_TEMPLATE and VARIANT_TEMPLATE. Intended for private or client-specific files (logos, confidential covers, etc.). When None (default), only the class-level templates are used. When provided, must be a valid existing directory; otherwise NotADirectoryError is raised before any files are written.

Raises:
  • FileExistsError – If the target folder folder/name already exists. No files are created or modified in this case.

  • NotADirectoryError – If template_overlay is provided but does not point to a valid directory. Validated before any files are written.

Returns:

Absolute path to the newly created document folder.

Return type:

pathlib.Path

Usage example
# Single-layer: base template only (Document base class)
doc = Document(name="MyDoc", alias="D")
target = doc.new("my_doc", "/home/user/documents")

# Two-layer: base + variant (subclass sets VARIANT_TEMPLATE)
report = Report(name="AnnualReport", alias="AR")
target = report.new("annual_report", "/home/user/documents")

# Three-layer: base + variant + private user overlay
target = report.new(
    "client_report",
    "/home/user/documents",
    template_overlay="/home/user/private/client_x",
)
class losalamos.documents.DocumentTeX(name='MyDocTeX', alias='TeX')[source]#

Bases: Document

Base class for all TeX-based document types.

Extends Document with TeX-specific behaviour: main-file detection, PDF compilation via latexmk, and document flattening. Subclasses set VARIANT_TEMPLATE to select their template layer.

BASE_TEMPLATE = PosixPath('/home/runner/work/losalamos/losalamos/src/losalamos/data/templates/documents/tex/base')#
VARIANT_TEMPLATE = None#
load_data(file_data)[source]#

Load data from a TeX file and detect whether it is a main document.

A file is considered a main document if it contains a \documentclass declaration. Partial files (chapters, sections) intended to be \input-ted by a main file will have is_main set to False.

Parameters:

file_data (str or pathlib.Path) – Path to the .tex file to load.

Returns:

None

Return type:

None

to_pdf(file_output=None, cleanup=True, command='pdf')[source]#

Compile the TeX document into a PDF using latexmk.

Only executes when is_main is True; partial files are silently skipped.

The method temporarily switches the working directory to the source file’s parent folder before invoking latexmk, ensuring that all relative paths in the document (\input, \include, figures, etc.) resolve correctly regardless of where the Python process was launched from. The original working directory is always restored, even if compilation raises an exception.

A project-local latexmkrc file is expected at the root of the document folder. It is passed explicitly via -r latexmkrc so that glossaries and nomenclature are built correctly on all platforms without requiring a global latexmkrc installation.

Parameters:
  • file_output (str or pathlib.Path or None) – Desired output path for the compiled PDF. When None (default), the PDF is left next to the source .tex file.

  • cleanup (bool) – If True (default), removes auxiliary files produced by latexmk after a successful compilation (equivalent to latexmk -c).

  • command (str) – latexmk engine flag controlling the PDF backend. Common values: "pdf" (pdflatex), "pdflua" (lualatex), "pdfxe" (xelatex).

Returns:

None

Return type:

None

to_flat(file_output=None, compile_first=True, command='pdf')[source]#

Flatten the TeX document by recursively resolving \input / \include directives and embedding the compiled bibliography.

Delegates the flattening work to make_flat(). Only executes when is_main is True; partial files return None silently.

Parameters:
  • file_output (str or pathlib.Path or None) – Desired output path for the flattened .tex file. When None, the source file is overwritten in-place.

  • compile_first (bool) – If True (default), compiles the document before flattening so that the .bbl file reflects the current bibliography state. The intermediate PDF and .bbl are removed after flattening.

  • command (str) – latexmk engine flag passed to to_pdf() when compile_first=True.

Returns:

The fully flattened LaTeX document as a string, or None if is_main is False.

Return type:

str or None

export(folder_root, name, flatten=False, split=False)[source]#

Export the TeX document to a self-contained folder.

Creates folder_root/name/ and writes the document into it according to the chosen mode:

  • Plain (flatten=False, split=False): the source file is copied as-is, retaining its original filename.

  • Flatten (flatten=True, split=False): \input / \include directives are resolved recursively and the result is written as a single file named <name>.tex.

  • Split (split=True): implies flattening. The document is first flattened into a temporary file, then split into preamble.tex and main.tex. The intermediate flat file is removed after splitting.

Parameters:
  • folder_root (str or pathlib.Path) – Parent directory under which the export folder is created.

  • name (str) – Name of the export folder and, in flatten mode, stem of the output .tex file.

  • flatten (bool) – If True, resolve all \input / \include directives into a single file before exporting. Ignored when split=True (flattening is always performed in that case).

  • split (bool) – If True, flatten the document and split the result into preamble.tex and main.tex. Takes precedence over flatten.

Returns:

Path to the created export folder.

Return type:

pathlib.Path

Raises:
  • RuntimeError – If the document is not a main file (i.e. \documentclass is absent). The is_main attribute is re-evaluated from file_data before the check so stale state does not produce a false negative.

  • FileExistsError – If folder_root/name already exists.

static split_preamble(input_tex, preamble_name='preamble', main_name='main', output_folder=None)[source]#

Split a main TeX file into a preamble fragment and a stub main file.

The preamble fragment receives everything between the \documentclass line and \begin{document} (both exclusive). The stub main file retains \documentclass, replaces the preamble block with a single \input{<preamble_name>} line, and keeps everything from \begin{document} onward intact.

Resulting structure:

% <main_name>.tex
\documentclass{...}

\input{preamble}

\begin{document}
...
\end{document}

% <preamble_name>.tex
\usepackage{...}
\newcommand{...}
...
Parameters:
  • input_tex (str or pathlib.Path) – Path to the root .tex file to split. Must be a main document containing \documentclass and \begin{document}.

  • preamble_name (str) – Stem for the preamble output file, without extension. Defaults to "preamble".

  • main_name (str) – Stem for the stub main output file, without extension. Defaults to "main".

  • output_folder (str or pathlib.Path or None) – Directory where both output files are written. When None (default), the parent directory of input_tex is used.

Returns:

Tuple of (preamble_path, main_path) as resolved pathlib.Path objects.

Return type:

tuple[pathlib.Path, pathlib.Path]

Raises:

ValueError – If input_tex does not contain a \documentclass declaration (i.e. is not a main TeX file), if \begin{document} is not found, or if the input file stem matches an output stem when both resolve to the same directory.

static make_flat(input_tex, output_tex=None)[source]#

Flatten a TeX document by recursively resolving \input / \include directives and embedding the compiled bibliography.

This is a pure file-system operation with no dependency on any DocumentTeX instance. It can be used independently whenever a self-contained .tex file is needed (e.g. arXiv submission).

Parameters:
  • input_tex (str or pathlib.Path) – Path to the root .tex file to flatten.

  • output_tex (str or pathlib.Path or None) – Destination path for the flattened file. When None (default), the result is written back over the source file in-place.

Returns:

The fully flattened LaTeX source as a string.

Return type:

str

static lint_paragraphs(input_tex, output_tex=None)[source]#

Join wrapped prose paragraphs in a LaTeX file into single lines.

Reads input_tex, joins each wrapped prose paragraph in the document body into a single source line, and writes the result to output_tex.

This function performs no backup of its own. When output_tex is None (or equal to input_tex), the input file is overwritten in place; callers that want a safety copy should take one beforehand (see backup_file()).

Parameters:
  • input_tex (str or pathlib.Path) – Path to the source .tex file to read.

  • output_tex (str or pathlib.Path or None) – Path to write the linted result to. If None (the default), input_tex is overwritten in place.

Returns:

The path the linted .tex was written to (equal to input_tex if output_tex was None).

Return type:

pathlib.Path

Raises:
  • FileNotFoundError – If input_tex does not exist.

  • UnicodeDecodeError – If input_tex is not valid UTF-8.

Algorithm#

  1. Everything up to and including \begin{document}, and everything from \end{document} onward, is passed through unchanged. (The preamble’s \newcommand shorthand definitions are one-per-line by convention and must stay that way.)

  2. Within the document body, consecutive lines are grouped into “paragraphs”: maximal runs of lines that are not blank, not \begin{...}/\end{...}, not comment-only, and not inside a non-reflowable environment. Each run is joined into a single line: every line is stripped of leading/trailing whitespace, joined with a single space, and any remaining runs of spaces are collapsed to one.

  3. The following end the current paragraph and are emitted unchanged on their own line, without being joined to anything: blank lines; \begin{...}/\end{...} lines (which also push/pop an environment stack used to track non-reflowable regions, including nested ones, by scanning the whole line – e.g. a pmatrix opened and closed on the same line inside an equation/align nets to zero rather than leaking a permanently elevated non-reflowable count); a comment-only line that appears between paragraphs; and \[/\] display-math delimiters on their own line (tracked like a pseudo-environment), or inline \[ ... \] on one line.

  4. A % comment that appears inside a paragraph – either a comment-only line with real content already buffered, or a trailing % ... after real content – does not split the paragraph. The real content (if any) is kept in the paragraph, and the comment text is deferred and appended to the end of the paragraph’s joined line once it is flushed. This avoids an editorial comment in the middle of a sentence cutting it into two “paragraphs”, the second starting mid-sentence (lower-case).

  5. \item always starts a new paragraph (flush before it), but its own wrapped continuation lines still join onto it – so consecutive list items each end up on their own line.

  6. The following commands always stand alone on their own output line, never merged with the paragraph before or after them, even without a blank line in the source: \tcblower, \newpage, \clearpage, \cleardoublepage, \pagebreak, \newline, \figplaceholder, \boxfigplaceholder, \boxbiofigplaceholder, and \chapter/\section/ \subsection/\subsubsection (starred or not).

  7. Non-reflowable environments (passed through verbatim, line-by-line, including any blank lines inside them): equation, align, gather, multline, eqnarray, alignat and their starred forms; array, cases, matrix/pmatrix/bmatrix/ vmatrix/Vmatrix; tabular, tabular*, tabularx, longtable; verbatim, lstlisting, minted, Verbatim; and \[ ... \] display math.

Note

Only line breaks and runs of whitespace within a joined paragraph are changed; no other characters are altered, so the rendered PDF is unaffected.

Example#

from pathlib import Path
lint_paragraphs(Path("chapter07_T12.tex"))  # overwrite in place
lint_paragraphs("chapter07_T12.tex", "chapter07_T12_linted.tex")
static lint_decorations_remove(input_tex, output_tex=None, document_only=False, except_chars=None)[source]#

Remove decoration comments/rulers (no letter characters after %) from a LaTeX file.

Reads input_tex, strips every decorative comment (comment-only lines and trailing comment suffixes on prose lines whose comment text contains no ASCII letter), and writes the result to output_tex.

This function performs no backup of its own. When output_tex is None (or equal to input_tex), the input file is overwritten in place; callers that want a safety copy should take one beforehand (see backup_file()).

Parameters:
  • input_tex (str or pathlib.Path) – Path to the source .tex file to read.

  • output_tex (str or pathlib.Path or None) – Path to write the purged result to. If None (the default), input_tex is overwritten in place.

  • document_only (bool) – If False (the default), the purge runs over the entire file – preamble, body, and postamble alike. If True, only the lines between \begin{document} and \end{document} are processed; the preamble and postamble are passed through unchanged.

  • except_chars (list[str] or None) – Optional list of single characters. A decoration comment (one whose text after % contains no ASCII letter) is nonetheless kept if its text contains at least one character from this list. Use this to preserve specific ruler styles, e.g. except_chars=['#'] keeps % #### lines. None (the default) means no exceptions – all decoration comments are removed.

Returns:

The path the purged .tex was written to (equal to input_tex if output_tex was None).

Return type:

pathlib.Path

Raises:
  • FileNotFoundError – If input_tex does not exist.

  • UnicodeDecodeError – If input_tex is not valid UTF-8.

Algorithm#

  1. When document_only=True, everything up to and including \begin{document}, and everything from \end{document} onward, is passed through unchanged. When document_only=False (default), all lines – including the preamble – are processed uniformly.

  2. Within the processed region each line is classified as one of:

    1. Blank – passed through unchanged.

    2. Comment-only decoration – the entire line’s non-whitespace content is a % whose following text (stripped) contains no ASCII letter and no except_chars character. The line is dropped; if the previous output line was blank, that blank is also removed (blank-line collapse).

    3. Comment-only kept – comment-only but preserved because the text contains a letter or an except_chars character. Passed through unchanged.

    4. Prose with trailing decoration comment – the % suffix qualifies as decoration. The suffix (and any whitespace before it) is stripped; the prose remainder is kept.

    5. Prose with trailing real comment – kept verbatim.

    6. Prose without any comment – kept verbatim.

  3. The % position is found via _find_comment_pos, which correctly handles escaped percent signs (\%).

  4. Non-reflowable environments (equation, align, tabular, verbatim, etc.) receive no special treatment here: comment lines inside math or table environments are still subject to the same rule.

Note

Only comment text is removed; no other characters are altered, so the rendered PDF is unaffected.

static lint_decorations_add(input_tex, output_tex=None, ruler_chars=None, ruler_len=60)[source]#

Inject decoration rulers above section headings in a LaTeX file.

Reads input_tex, inserts a comment ruler (% `` + N repetitions of a level-specific character) on the line immediately before every ``\chapter, \section, \subsection, and \subsubsection command found inside the document body, then writes the result to output_tex.

The preamble (everything up to and including \begin{document}) and the postamble (\end{document} and everything after) are passed through unchanged.

If a ruler with the correct character already exists on the line immediately before a heading, it is replaced rather than duplicated, making the function safe to call multiple times on the same file (idempotent with respect to ruler content; ruler length is always normalised to the current ruler_len).

This function performs no backup of its own. When output_tex is None (or equal to input_tex), the input file is overwritten in place; callers that want a safety copy should take one beforehand (see backup_file()).

Parameters:
  • input_tex (str or pathlib.Path) – Path to the source .tex file to read.

  • output_tex (str or pathlib.Path or None) – Path to write the result to. If None (the default), input_tex is overwritten in place.

  • ruler_chars (str or list[str] or None) – Sequence of up to four characters, one per heading level (chapter, section, subsection, subsubsection). Missing positions fall back to the defaults ('#', '*', '=', '-'). May be a string (each character is used positionally) or any sequence of single characters. None uses all defaults.

  • ruler_len (int) – Number of times the ruler character is repeated after the % `` prefix.  Defaults to ``60.

Returns:

The path the result was written to.

Return type:

pathlib.Path

Raises:
  • FileNotFoundError – If input_tex does not exist.

  • UnicodeDecodeError – If input_tex is not valid UTF-8.

  • ValueError – If ruler_len is less than 1.

Algorithm#

  1. Split the file into preamble, body lines, and postamble at \begin{document} / \end{document}. Only the body is processed.

  2. Resolve the four ruler strings from ruler_chars and ruler_len.

  3. Walk body lines. When a line matches a heading command, look at the previous output line:

    • If it is already a ruler whose dominant character matches the current level’s character, replace it with the freshly built ruler (normalises length on re-runs).

    • Otherwise insert the ruler as a new line before the heading.

  4. Reassemble preamble + processed body + postamble and write.

Note

A “ruler line” for replacement detection is defined as a line whose stripped content matches % <char><char>... with at least one repetition of char – the same format this function produces. Any other comment line is left untouched even if it precedes a heading.

static lint_blank_lines(input_tex, output_tex=None)[source]#

Normalize blank-line spacing in a LaTeX file.

Reads input_tex and enforces two rules throughout the document body:

  • Paragraph separation – any run of two or more consecutive blank lines in prose is collapsed to exactly one blank line.

  • Environment padding – every \begin{...} is preceded by exactly two blank lines, and every \end{...} is followed by exactly two blank lines. The interior of every environment (everything between its \begin and \end) is left completely untouched.

Schematically, the target layout is:

<prose>

<blank>
<blank>
\begin{equation}
  ...            <- interior: never touched
\end{equation}
<blank>
<blank>
<prose>

Nested environments produce nested padding:

<blank>
<blank>
\begin{subequations}

  <blank>
  <blank>
  \begin{align}
    ...
  \end{align}
  <blank>
  <blank>

\end{subequations}
<blank>
<blank>

The preamble (everything up to and including \begin{document}) and the document environment boundary itself are exempt from padding.

This function performs no backup of its own. When output_tex is None (or equal to input_tex), the input file is overwritten in place; callers that want a safety copy should take one beforehand.

Parameters:
  • input_tex (str or pathlib.Path) – Path to the source .tex file to read.

  • output_tex (str or pathlib.Path or None) – Path to write the result to. If None (the default), input_tex is overwritten in place.

Returns:

(output_path, stats) where stats is a dict with keys collapsed (excess blank lines removed) and padded (blank lines inserted).

Return type:

tuple[pathlib.Path, dict]

Raises:
  • FileNotFoundError – If input_tex does not exist.

  • UnicodeDecodeError – If input_tex is not valid UTF-8.

Algorithm#

A single forward pass over the lines maintains:

  • env_depth – nesting depth of padded environments (0 = prose).

  • pending_pad – number of blank lines still to be emitted after an \end{...} before the next non-blank content.

When a \begin{...} is encountered (outside the preamble and outside the document environment): any trailing blank lines in the output buffer are stripped, exactly two blank lines are inserted, and then the \begin line itself is emitted. env_depth is incremented, and all subsequent lines (including blank ones) are passed through verbatim until the matching \end{...} is reached. On \end{...}: env_depth is decremented, the \end line is emitted, and pending_pad is set to 2. The next non-blank line flushes those two blank lines first. Any blank lines in the source between \end and the next content are discarded (replaced by the controlled padding).

class losalamos.documents.Essay(name='MyDocTeX', alias='TeX')[source]#

Bases: DocumentTeX

class losalamos.documents.Professional(name='MyDocTeX', alias='TeX')[source]#

Bases: Essay

VARIANT_TEMPLATE = PosixPath('/home/runner/work/losalamos/losalamos/src/losalamos/data/templates/documents/tex/professional')#