Skip to content
Snippets Groups Projects
Unverified Commit 8cdbe1e6 authored by mmcky's avatar mmcky Committed by GitHub
Browse files

:sparkles: NEW: add `--individualpages` option for pdflatex builder (#944)

This commit adds support to build individual `pdf` files for each source document (.md etc) using the generic sphinx `pdflatex` builder. It can be enabled for `builder=pdflatex` using the `cli` option `--individualpages`, for example:

```bash
jb build <project> --builder=pdflatex --individualpages
```

A use case is for lecture sets, where each file is an independent lecture,
and also to support downloadable PDFs for websites. 

Individual `pdf` files can be specified manually in `sphinx` configuration through the [latex_documents](https://www.sphinx-doc.org/en/3.x/usage/configuration.html#confval-latex_documents) tuple configuration. 
This commmit implements an auto-builder that collects the source files and titles and updates the `latex_documents` tuple when calling `sphinx_build`

An example for [myst-nb.example_project](https://github.com/executablebooks/myst-nb.example-project/pull/8/files

) shows how these tuples can be constructed. 

Co-authored-by: default avatarChris Sewell <chrisj_sewell@hotmail.com>
parent 4d2e6052
No related branches found
No related tags found
No related merge requests found
Showing
with 224 additions and 29 deletions
...@@ -89,6 +89,10 @@ This section tries to recommend a few best-practices. ...@@ -89,6 +89,10 @@ This section tries to recommend a few best-practices.
We recommend using the [texlive](https://www.tug.org/texlive/) distribution We recommend using the [texlive](https://www.tug.org/texlive/) distribution
::: :::
The default is to build your project as a single PDF file, however it is possible to build
individual PDF files for each page of the project by enabling the `--individualpages` option
when using the `pdflatex` builder.
### Installation ### Installation
For `Debian`-based `Linux` platforms it is recommended to install the following packages: For `Debian`-based `Linux` platforms it is recommended to install the following packages:
...@@ -129,8 +133,23 @@ jb build mybookname/ --builder latex ...@@ -129,8 +133,23 @@ jb build mybookname/ --builder latex
:::: ::::
**Individual PDF Files:**
To build PDF files for each page of the project,
you can specify the option `--individualpages` for `--builder=pdflatex`.
The individual PDF files will be available in the `_build/latex` build folder.
These files will have the same name as the source file or, if nested in folders, will be named `{folder}-{filename}.pdf`.
:::{note}
When specifying a page using the `build` command,
the `--individualpages` will automatically be set to `True`.
In the future we intend for this to produce latex documents more suitable to single pages
(see [issue #904](https://github.com/executablebooks/jupyter-book/issues/904)).
:::
### Updating the name of the PDF file ### Updating the name of the Global PDF file
To update the name of your `PDF` file you can set the following in `_config.yml` To update the name of your `PDF` file you can set the following in `_config.yml`
......
...@@ -113,6 +113,12 @@ BUILDER_OPTS = { ...@@ -113,6 +113,12 @@ BUILDER_OPTS = {
count=True, count=True,
help="-q means no sphinx status, -qq also turns off warnings ", help="-q means no sphinx status, -qq also turns off warnings ",
) )
@click.option(
"--individualpages",
is_flag=True,
default=False,
help="[pdflatex] Enable build of PDF files for each individual page",
)
def build( def build(
path_source, path_source,
path_output, path_output,
...@@ -125,6 +131,7 @@ def build( ...@@ -125,6 +131,7 @@ def build(
builder, builder,
verbose, verbose,
quiet, quiet,
individualpages,
get_config_only=False, get_config_only=False,
): ):
"""Convert your book's or page's content to HTML or a PDF.""" """Convert your book's or page's content to HTML or a PDF."""
...@@ -141,6 +148,20 @@ def build( ...@@ -141,6 +148,20 @@ def build(
config_overrides = {} config_overrides = {}
found_config = find_config_path(PATH_SRC_FOLDER) found_config = find_config_path(PATH_SRC_FOLDER)
BUILD_PATH = path_output if path_output is not None else found_config[0] BUILD_PATH = path_output if path_output is not None else found_config[0]
# Set config for --individualpages option (pages, documents)
if individualpages:
if builder != "pdflatex":
_error(
"""
Specified option --individualpages only works with the
following builders:
pdflatex
"""
)
# Build Page
if not PATH_SRC_FOLDER.is_dir(): if not PATH_SRC_FOLDER.is_dir():
# it is a single file # it is a single file
build_type = "page" build_type = "page"
...@@ -175,7 +196,10 @@ def build( ...@@ -175,7 +196,10 @@ def build(
"globaltoc_path": "", "globaltoc_path": "",
"exclude_patterns": to_exclude, "exclude_patterns": to_exclude,
"html_theme_options": {"single_page": True}, "html_theme_options": {"single_page": True},
# --individualpages option set to True for page call
"latex_individualpages": True,
} }
# Build Project
else: else:
build_type = "book" build_type = "book"
PAGE_NAME = None PAGE_NAME = None
...@@ -208,6 +232,9 @@ def build( ...@@ -208,6 +232,9 @@ def build(
if builder == "pdfhtml": if builder == "pdfhtml":
config_overrides["html_theme_options"] = {"single_page": True} config_overrides["html_theme_options"] = {"single_page": True}
# --individualpages option passthrough
config_overrides["latex_individualpages"] = individualpages
# Use the specified configuration file, or one found in the root directory # Use the specified configuration file, or one found in the root directory
path_config = config or ( path_config = config or (
found_config[0].joinpath("_config.yml") if found_config[1] else None found_config[0].joinpath("_config.yml") if found_config[1] else None
......
...@@ -129,6 +129,14 @@ def get_final_config( ...@@ -129,6 +129,14 @@ def get_final_config(
# and completely override any defaults (sphinx and yaml) # and completely override any defaults (sphinx and yaml)
sphinx_config.update(user_yaml_update) sphinx_config.update(user_yaml_update)
# This is to deal with a special case, where the override needs to be applied after
# the sphinx app is initialised (since the default is a function)
# TODO I'm not sure if there is a better way to deal with this?
config_meta = {
"latex_doc_overrides": sphinx_config.pop("latex_doc_overrides"),
"latex_individualpages": cli_config.pop("latex_individualpages"),
}
# finally merge in CLI configuration # finally merge in CLI configuration
_recursive_update(sphinx_config, cli_config or {}) _recursive_update(sphinx_config, cli_config or {})
...@@ -138,11 +146,6 @@ def get_final_config( ...@@ -138,11 +146,6 @@ def get_final_config(
paths_static.append("_static") paths_static.append("_static")
sphinx_config["html_static_path"] = paths_static sphinx_config["html_static_path"] = paths_static
# This is to deal with a special case, where the override needs to be applied after
# the sphinx app is initialised (since the default is a function)
# TODO I'm not sure if there is a better way to deal with this?
config_meta = {"latex_doc_overrides": sphinx_config.pop("latex_doc_overrides")}
return sphinx_config, config_meta return sphinx_config, config_meta
......
...@@ -2,8 +2,32 @@ ...@@ -2,8 +2,32 @@
from copy import copy from copy import copy
from pathlib import Path from pathlib import Path
import asyncio import asyncio
import sphinx
import os
from .utils import _error from .utils import _error, _message_box
# LaTeX Documents Tuple Spec
if sphinx.__version__ >= "3.0.0":
# https://www.sphinx-doc.org/en/3.x/usage/configuration.html#confval-latex_documents
LATEX_DOCUMENTS = (
"startdocname",
"targetname",
"title",
"author",
"theme",
"toctree_only",
)
else:
# https://www.sphinx-doc.org/en/2.0/usage/configuration.html#confval-latex_documents
LATEX_DOCUMENTS = (
"startdocname",
"targetname",
"title",
"author",
"documentclass",
"toctree_only",
)
def html_to_pdf(html_file, pdf_file): def html_to_pdf(html_file, pdf_file):
...@@ -60,18 +84,105 @@ async def _html_to_pdf(html_file, pdf_file): ...@@ -60,18 +84,105 @@ async def _html_to_pdf(html_file, pdf_file):
await browser.close() await browser.close()
def update_latex_document(latex_document: tuple, updates: dict): def update_latex_documents(latex_documents, latexoverrides):
"""Apply updates from _config.yml to a latex_document tuple""" """
names = ( Apply latexoverrides from _config.yml to latex_documents tuple
"startdocname", """
"targetname",
"title", if len(latex_documents) > 1:
"author", _message_box(
"theme", "Latex documents specified as a multi element list in the _config",
"toctree_only", "This suggests the user has made custom settings to their build",
) "[Skipping] processing of automatic latex overrides",
updated = list(copy(latex_document)) )
for i, (_, name) in enumerate(zip(latex_document, names)): return latex_documents
if name in updates:
updated[i] = updates[name] # Extract latex document tuple
return tuple(updated) latex_document = latex_documents[0]
# Apply single overrides from _config.yml
updated_latexdocs = []
for loc, item in enumerate(LATEX_DOCUMENTS):
# the last element toctree_only seems optionally included
if loc >= len(latex_document):
break
if item in latexoverrides.keys():
updated_latexdocs.append(latexoverrides[item])
else:
updated_latexdocs.append(latex_document[loc])
return [tuple(updated_latexdocs)]
def latex_document_components(latex_documents):
""" Return a dictionary of latex_document components by name """
latex_tuple_components = {}
for idx, item in enumerate(LATEX_DOCUMENTS):
# skip if latex_documents doesn't doesn't contain all elements
# of the LATEX_DOCUMENT specification tuple
if idx >= len(latex_documents):
continue
latex_tuple_components[item] = latex_documents[idx]
return latex_tuple_components
def latex_document_tuple(components):
""" Return a tuple for latex_documents from named components dictionary """
latex_doc = []
for item in LATEX_DOCUMENTS:
if item not in components.keys():
continue
else:
latex_doc.append(components[item])
return tuple(latex_doc)
def autobuild_singlepage_latexdocs(app):
"""
Build list of tuples for each document in the Project
[((startdocname, targetname, title, author, theme, toctree_only))]
https://www.sphinx-doc.org/en/3.x/usage/configuration.html#confval-latex_documents
"""
latex_documents = app.config.latex_documents
if len(latex_documents) > 1:
_message_box(
"Latex documents specified as a multi element list in the _config",
"This suggests the user has made custom settings to their build",
"[Skipping] --individualpages option",
)
return latex_documents
# Extract latex_documents updated tuple
latex_documents = latex_documents[0]
titles = app.env.titles
master_doc = app.config.master_doc
sourcedir = os.path.dirname(master_doc)
# Construct Tuples
DEFAULT_VALUES = latex_document_components(latex_documents)
latex_documents = []
for doc, title in titles.items():
latex_doc = copy(DEFAULT_VALUES)
# if doc has a subdir relative to src dir
docname = None
parts = Path(doc).parts
latex_doc["startdocname"] = doc
if DEFAULT_VALUES["startdocname"] == doc:
targetdoc = DEFAULT_VALUES["targetname"]
else:
if sourcedir in parts:
parts = list(parts)
# assuming we need to remove only the first instance
parts.remove(sourcedir)
docname = "-".join(parts)
targetdoc = docname + ".tex"
latex_doc["targetname"] = targetdoc
latex_doc["title"] = title.astext()
latex_doc = latex_document_tuple(latex_doc)
latex_documents.append(latex_doc)
return latex_documents
...@@ -10,7 +10,7 @@ from sphinx.cmd.build import handle_exception ...@@ -10,7 +10,7 @@ from sphinx.cmd.build import handle_exception
import yaml import yaml
from .config import get_final_config from .config import get_final_config
from .pdf import update_latex_document from .pdf import update_latex_documents
REDIRECT_TEXT = """ REDIRECT_TEXT = """
<meta http-equiv="Refresh" content="0; url={first_page}" /> <meta http-equiv="Refresh" content="0; url={first_page}" />
...@@ -131,12 +131,19 @@ def build_sphinx( ...@@ -131,12 +131,19 @@ def build_sphinx(
# We have to apply this update after the sphinx initialisation, # We have to apply this update after the sphinx initialisation,
# since default_latex_documents is dynamically generated # since default_latex_documents is dynamically generated
# see sphinx/builders/latex/__init__.py:default_latex_documents # see sphinx/builders/latex/__init__.py:default_latex_documents
# TODO what if the user has specifically set latex_documents? new_latex_documents = update_latex_documents(
default_latex_document = app.config.latex_documents[0] app.config.latex_documents, config_meta["latex_doc_overrides"]
new_latex_document = update_latex_document(
default_latex_document, config_meta["latex_doc_overrides"]
) )
app.config.latex_documents = [new_latex_document] app.config.latex_documents = new_latex_documents
# Build latex_doc tuples based on --individualpages option request
if config_meta["latex_individualpages"]:
from .pdf import autobuild_singlepage_latexdocs
# Ask Builder to read the source files to fetch titles and documents
app.builder.read()
latex_documents = autobuild_singlepage_latexdocs(app)
app.config.latex_documents = latex_documents
app.build(force_all, filenames) app.build(force_all, filenames)
......
# Book settings
title: My sample book
author: The Jupyter Book Community
logo: logo.png
latex:
latex_documents:
targetname: book.tex
title: An interesting Title
author: EBP Developers
- file: source/index
- file: source/content
sections:
- file: source/sections/section1
- file: source/sections/section2
# Sample Contents
# Sample index
# File 1
# File 2
...@@ -44,8 +44,9 @@ from jupyter_book.commands import sphinx ...@@ -44,8 +44,9 @@ from jupyter_book.commands import sphinx
], ],
) )
def test_get_final_config(user_config, data_regression): def test_get_final_config(user_config, data_regression):
cli_config = {"latex_individualpages": False}
final_config, metadata = get_final_config( final_config, metadata = get_final_config(
user_config, validate=True, raise_on_invalid=True user_config, cli_config, validate=True, raise_on_invalid=True
) )
data_regression.check( data_regression.check(
{"_user_config": user_config, "final": final_config, "metadata": metadata} {"_user_config": user_config, "final": final_config, "metadata": metadata}
......
...@@ -65,3 +65,4 @@ final: ...@@ -65,3 +65,4 @@ final:
metadata: metadata:
latex_doc_overrides: latex_doc_overrides:
title: My Jupyter Book title: My Jupyter Book
latex_individualpages: false
...@@ -68,3 +68,4 @@ final: ...@@ -68,3 +68,4 @@ final:
metadata: metadata:
latex_doc_overrides: latex_doc_overrides:
title: My Jupyter Book title: My Jupyter Book
latex_individualpages: false
...@@ -67,3 +67,4 @@ final: ...@@ -67,3 +67,4 @@ final:
metadata: metadata:
latex_doc_overrides: latex_doc_overrides:
title: My Jupyter Book title: My Jupyter Book
latex_individualpages: false
...@@ -73,3 +73,4 @@ final: ...@@ -73,3 +73,4 @@ final:
metadata: metadata:
latex_doc_overrides: latex_doc_overrides:
title: My Jupyter Book title: My Jupyter Book
latex_individualpages: false
...@@ -67,3 +67,4 @@ final: ...@@ -67,3 +67,4 @@ final:
metadata: metadata:
latex_doc_overrides: latex_doc_overrides:
title: My Jupyter Book title: My Jupyter Book
latex_individualpages: false
...@@ -70,3 +70,4 @@ metadata: ...@@ -70,3 +70,4 @@ metadata:
latex_doc_overrides: latex_doc_overrides:
targetname: book.tex targetname: book.tex
title: other title: other
latex_individualpages: false
...@@ -67,3 +67,4 @@ final: ...@@ -67,3 +67,4 @@ final:
metadata: metadata:
latex_doc_overrides: latex_doc_overrides:
title: My Jupyter Book title: My Jupyter Book
latex_individualpages: false
...@@ -67,3 +67,4 @@ final: ...@@ -67,3 +67,4 @@ final:
metadata: metadata:
latex_doc_overrides: latex_doc_overrides:
title: My Jupyter Book title: My Jupyter Book
latex_individualpages: false
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment