Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Instructions: Add a subsection under `[Unreleased]` for additions, fixes, change
- `pdf` targets accept a `method` attribute selecting the route to the PDF: `latex` (the default) or `fo`, the LaTeX-free route through XSL-FO and Apache FOP.
- `lualatex` is now a usable `latex-engine`, with a matching entry in `executables.ptx`.
- `latex-engine` is now permitted by the schema on every target, not just `pdf` ones; it has always been the engine that compiles `latex-image` assets for all formats.
- Naming a journal in the publication file now also selects that journal's bibliography and citation style: references are formatted in that style, in every output format, with nothing else to set. A `citation-stylesheet-language/@style` of your own takes precedence.
- Formatted references are now a generated asset like any other, built when needed and regenerated when the bibliography, the citations, or the chosen style changes. `pretext generate references` is no longer a debugging-only step.

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion pretext/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

VERSION = get_version("pretext", Path(__file__).parent.parent)

CORE_COMMIT = "2c8806b9988f855e94d185fb145226bf6c0a5b20"
CORE_COMMIT = "d2fb936e501f50a7e2fe12a1f0e012278da91251"


def activate() -> None:
Expand Down
12 changes: 11 additions & 1 deletion pretext/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"datafile",
"myopenmath",
"dynamic-subs",
"references",
"qrcode",
"gdscript",
],
Expand All @@ -45,6 +46,7 @@
"mermaid",
"myopenmath",
"dynamic-subs",
"references",
],
"latex": [
"webwork",
Expand All @@ -59,6 +61,7 @@
"mermaid",
"myopenmath",
"dynamic-subs",
"references",
],
"epub": [
"webwork",
Expand All @@ -73,6 +76,7 @@
"mermaid",
"myopenmath",
"dynamic-subs",
"references",
],
"epub_nozip": [
"webwork",
Expand All @@ -87,6 +91,7 @@
"mermaid",
"myopenmath",
"dynamic-subs",
"references",
],
"kindle": [
"webwork",
Expand All @@ -101,6 +106,7 @@
"mermaid",
"myopenmath",
"dynamic-subs",
"references",
],
"braille": [
"webwork",
Expand All @@ -114,9 +120,11 @@
"mermaid",
"myopenmath",
"dynamic-subs",
"references",
],
"revealjs": [
"webwork",
"references",
"latex-image",
"sageplot",
"asymptote",
Expand All @@ -139,6 +147,7 @@
"mermaid",
"myopenmath",
"dynamic-subs",
"references",
],
"webwork": [
"webwork",
Expand All @@ -157,6 +166,7 @@
"mermaid",
"myopenmath",
"dynamic-subs",
"references",
"gdscript",
],
}
Expand All @@ -175,7 +185,7 @@
"mermaid": ".//mermaid",
"myopenmath": ".//myopenmath",
"dynamic-subs": ".//statement[.//fillin and ancestor::exercise/evaluation]",
"references": ".//biblio",
"references": ".//biblio|.//xref",
"stack": ".//stack",
"gdscript": ".//program[@pck]",
}
Expand Down
159 changes: 130 additions & 29 deletions pretext/project/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,6 @@ def original_source_element(self) -> ET._Element:
source_doc = ET.parse(self.source_abspath())
for _ in range(25):
source_doc.xinclude()
print("Type of source_doc: ", type(source_doc))
return source_doc.getroot()

def source_element(self) -> ET._Element:
Expand Down Expand Up @@ -520,6 +519,61 @@ def source_element_with_ids(self) -> ET._Element:
def publication_abspath(self) -> Path:
return self._project.publication_abspath() / self.publication

def publication_csl_settings(self) -> t.Tuple[t.Optional[str], t.Optional[str]]:
"""
The journal name and the CSL style named in the publication file,
either of which may be absent. Between them they decide whether
references and citations are rendered by CSL at all, and which
style does it.

Read from the publication file directly: answering from there
saves a publisher variable report -- a full XSL pass over the
source -- on every build that could not possibly need one.
"""
try:
publication = ET.parse(self.publication_abspath())
except Exception:
# a malformed publication file is reported, loudly, elsewhere
return (None, None)

def first(xpath: str) -> t.Optional[str]:
values = publication.xpath(xpath)
assert isinstance(values, t.List)
return str(values[0]) if values else None

return (
first("/publication/common/journal/@name"),
first("/publication/common/citation-stylesheet-language/@style"),
)

def note_csl_file_state(self) -> None:
"""
Record whether the generated file of formatted references and
citations is on disk, for the stylesheets to consult.

XSLT cannot ask, and libxslt evaluates every global variable at
the start of a transformation -- including the one that opens
this file. So any pass at all, a syntax check as much as a
conversion, aborts outright on a project that has opted into CSL
styles but has yet to generate its references. Saying plainly
that the file is absent turns that into a warning and a fall back
to default bibliography handling.

Cheap enough (one stat) to repeat whenever the answer may have
changed, which it does the moment references are generated.
"""
csl_file = self.generated_dir_abspath() / "references" / "csl-bibliography.xml"
self.stringparams["csl.file.missing"] = "" if csl_file.exists() else "yes"

def publication_uses_csl(self) -> bool:
"""
Whether the publication file opts into CSL styles for references
and citations, by naming a style outright or a journal that
implies one.
"""
journal, csl_style = self.publication_csl_settings()
return (journal is not None) or (csl_style is not None)

def output_dir_abspath(self) -> Path:
if self.is_standalone() and self.output_dir is None:
if self.format == Format.PDF or self.compression == Compression.SCORM:
Expand Down Expand Up @@ -699,6 +753,16 @@ def generate_asset_table(self) -> pt.AssetTable:
base_url = self._read_publication_file_subset().baseurl
if base_url is not None:
hash.update(base_url.encode("utf-8"))
# For references, the style is as much an input as the
# bibliography itself: changing journals must regenerate. Both
# publication file entries are hashed, since either can pick the
# style. (A style that changes inside core, for a journal whose
# name stays put, is caught instead by the assembly-time check
# against the style stamped on the generated file.)
if asset == "references":
for setting in self.publication_csl_settings():
if setting is not None:
hash.update(setting.encode("utf-8"))
# Finally, we store the hash as a string in the dictionary.
asset_hash_dict[asset] = hash.hexdigest()
return asset_hash_dict
Expand Down Expand Up @@ -965,6 +1029,9 @@ def build(
# Add cli.version to stringparams. Use only the major and minor version numbers.
self.stringparams["cli.version"] = VERSION[: VERSION.rfind(".")]

# Before the source is assembled for the first time below.
self.note_csl_file_state()

# Check for xml syntax errors and quit if xml invalid:
try:
# Access the source_element to trigger assembly if it hasn't been done yet.
Expand All @@ -991,6 +1058,31 @@ def build(
if generate:
self.generate_assets(xmlid=xmlid, clean_tmp_dirs=clean_tmp_dirs)

# A journal named in the publication file supplies the CSL style for
# bibliographies and citations. Core resolves that into a stringparam
# here, once, so every format's conversion agrees with the generated
# references about which style is in force. A style named in the
# publication file outranks the journal's, and core says so.
#
# This must follow generation, not precede it. Resolving the style
# also records whether the generated references file is on disk, and
# generation is what puts it there: asked any earlier, the answer
# would be "absent" and every conversion below would fall back to
# default bibliography handling despite the file now existing.
self.note_csl_file_state()
if self.publication_uses_csl():
try:
core.get_csl_style(
xml=self.source_abspath(),
pub_file=self.publication_abspath().as_posix(),
stringparams=self.stringparams,
)
except Exception as e:
log.error(
f"Unable to determine the journal's bibliography style:\n {e}"
)
log.debug(e, exc_info=True)

# Ensure the output directories exist.
self.ensure_output_directory()

Expand Down Expand Up @@ -1226,25 +1318,22 @@ def generate_assets(
"""
log.info("Generating any needed assets.")

# Generation runs on its own as well as from a build, and the schema
# check just below assembles the source.
self.note_csl_file_state()

# Warn about schema errors here too, since assets are often generated
# without a build. A no-op when a build already ran the check.
self.check_schema()

# To help with debugging, we are temporarily adding a reference generation step here. The only way this will be called is if `pretext generate references` is called explicitly.
if requested_asset_types == ("references",):
try:
core.references(
xml_source=self.source_abspath(),
pub_file=self.publication_abspath().as_posix(),
stringparams=self.stringparams.copy(),
xmlid_root=xmlid,
dest_dir=self.generated_dir_abspath() / "references",
)
except Exception as e:
log.error(f"Unable to generate some references:\n {e}")
log.debug(e, exc_info=True)
finally:
return
# Whether references were asked for by name, which changes how a
# project that has not opted into CSL styles is answered below.
references_requested = (
requested_asset_types is not None
and "references" in requested_asset_types
and "ALL" not in requested_asset_types
)

# To help with debugging, we are temporarily adding a stack generation step here. The only way this will be called is if `pretext generate stack` is called explicitly.
if requested_asset_types == ("stack",):
try:
Expand Down Expand Up @@ -1294,6 +1383,18 @@ def generate_assets(
log.debug(
f"Based on format {self.format}, assets to be generated are: {requested_asset_types}."
)
# Rendering references and citations with CSL is opt-in: without a
# journal or a style named in the publication file there is nothing
# to render them with, and core would rightly object.
if "references" in requested_asset_types and not self.publication_uses_csl():
if references_requested:
log.warning(
"References are rendered with a Citation Style Language (CSL) style, "
"which this project has not asked for. Name a style as "
"`citation-stylesheet-language/@style`, or a journal as `journal/@name`, "
"in the publication file's `common` element. No references will be generated."
)
requested_asset_types.remove("references")
# We always build the asset hash table, even if only_changed=True: this tells us which assets need to be built, and how to update the saved asset hash table at the end of the method.
# utils.clean_asset_table purges any asset types from the loaded table that are no longer in the target.
source_asset_table = self.generate_asset_table()
Expand Down Expand Up @@ -1585,19 +1686,19 @@ def generate_assets(
log.error(f"Unable to generate some datafiles:\n {e}")
log.debug(e, exc_info=True)
# The following code will eventually be needed, but for now, we leave as a placeholder.
# if "references" in assets_to_generate and debug_references:
# try:
# core.references(
# xml_source=self.source_abspath(),
# pub_file=self.publication_abspath().as_posix(),
# stringparams=stringparams_copy,
# xmlid_root=xmlid,
# dest_dir=self.generated_dir_abspath() / "references",
# )
# successful_assets.append("references")
# except Exception as e:
# log.error(f"Unable to generate some references:\n {e}")
# log.debug(e, exc_info=True)
if "references" in assets_to_generate:
try:
core.references(
xml_source=self.source_abspath(),
pub_file=self.publication_abspath().as_posix(),
stringparams=stringparams_copy,
xmlid_root=xmlid,
dest_dir=self.generated_dir_abspath() / "references",
)
successful_assets.append("references")
except Exception as e:
log.error(f"Unable to generate some references:\n {e}")
log.debug(e, exc_info=True)
# Delete temporary directories left behind by core:
try:
core.release_temporary_directories(any_log_level=clean_tmp_dirs)
Expand Down
22 changes: 22 additions & 0 deletions tests/examples/projects/journal-bibliography/project.ptx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<project>
<targets>
<target name="web">
<format>html</format>
<source>source/main.ptx</source>
<publication>publication.xml</publication>
<output-dir>output/web</output-dir>
</target>
</targets>
<executables>
<latex>latex</latex>
<pdflatex>pdflatex</pdflatex>
<xelatex>xelatex</xelatex>
<asy>asy</asy>
<sage>sage</sage>
<pdfeps>pdftops</pdfeps>
<pdfcrop>pdf-crop-margins</pdfcrop>
<pageres>pageres</pageres>
<node>node</node>
</executables>
</project>
5 changes: 5 additions & 0 deletions tests/examples/projects/journal-bibliography/publication.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<publication>
<common>
<journal name="bull-amer-math-soc"/>
</common>
</publication>
23 changes: 23 additions & 0 deletions tests/examples/projects/journal-bibliography/source/main.ptx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<pretext>
<article xml:id="article-journal-bibliography">
<title>An Article For A Journal</title>
<section xml:id="sec-one">
<title>One</title>
<p>A result appears in <xref ref="biblio-lay"/> and also in <xref ref="biblio-judson"/>.</p>
</section>
<backmatter>
<references xml:id="references-backmatter">
<title>References</title>
<biblio xml:id="biblio-judson" type="book">
<author><name><given>Thomas</given><family>Judson</family></name></author>
<title>Abstract Algebra: Theory and Applications</title>
</biblio>
<biblio xml:id="biblio-lay" type="article">
<author><name><given>David C.</given><family>Lay</family></name></author>
<title>Subspaces and Echelon Forms</title>
</biblio>
</references>
</backmatter>
</article>
</pretext>
Loading