pretext 2.43.1__tar.gz → 2.43.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pretext-2.43.1 → pretext-2.43.2}/PKG-INFO +1 -1
- {pretext-2.43.1 → pretext-2.43.2}/pretext/__init__.py +1 -1
- {pretext-2.43.1 → pretext-2.43.2}/pretext/codechat.py +11 -3
- {pretext-2.43.1 → pretext-2.43.2}/pretext/core/pretext.py +386 -20
- {pretext-2.43.1 → pretext-2.43.2}/pretext/core/webwork.py +7 -2
- {pretext-2.43.1 → pretext-2.43.2}/pretext/resources/core.zip +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/resources/pelican.zip +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/resources/resource_hash_table.json +9 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/resources/rs_cache.zip +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/resources/templates.zip +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pyproject.toml +1 -1
- {pretext-2.43.1 → pretext-2.43.2}/LICENSE +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/README.md +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/__main__.py +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/cli.py +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/constants.py +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/core/__init__.py +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/core/braille_format.py +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/core/common.py +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/core/stack.py +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/logger.py +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Alignment.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Arrays.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Bibliography.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Boxes.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Breaking.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Crossref.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Floats.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/FontSelection.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Footnotes.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Index.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Lists.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Pictures.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Quotations.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Sectioning.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Sentences.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Space.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Tabbing.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Themes/__init__.py +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Themes/default/__init__.py +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Themes/default/default-layout.jinja2 +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Themes/default/document-layout.jinja2 +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Thms.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Verbatim.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/__init__.py +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/babel.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/graphicx.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/hyperref.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/lipsum.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/longtable.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/math.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/misc.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/natbib.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/p.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/subfig.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/textcomp.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/tikzcd.jinja2 +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/tikzpicture.jinja2 +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/url.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/wrapfig.jinja2s +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/project/__init__.py +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/project/generate.py +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/project/xml.py +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/resources/__init__.py +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/server.py +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/types.py +0 -0
- {pretext-2.43.1 → pretext-2.43.2}/pretext/utils.py +0 -0
|
@@ -19,7 +19,7 @@ from single_version import get_version
|
|
|
19
19
|
VERSION = get_version("pretext", Path(__file__).parent.parent)
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
CORE_COMMIT = "
|
|
22
|
+
CORE_COMMIT = "ad605bb27faf9f12c2bcb3b98ea953533f1aacb4"
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def activate() -> None:
|
|
@@ -88,9 +88,17 @@ def map_path_to_xml_id(
|
|
|
88
88
|
# Load the XML, performing xincludes using this loader.
|
|
89
89
|
huge_parser = ET.XMLParser(huge_tree=True)
|
|
90
90
|
src_tree = ET.parse(_xml, parser=huge_parser)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
try:
|
|
92
|
+
# ``max_depth=None`` lifts ElementInclude's default 6-level inclusion
|
|
93
|
+
# limit, which deeply divided books can exceed. Cyclic includes still
|
|
94
|
+
# raise, so this cannot loop forever.
|
|
95
|
+
lxml.ElementInclude.include(src_tree, loader=my_loader, max_depth=None)
|
|
96
|
+
except Exception:
|
|
97
|
+
# ``tree.xinclude()`` also performs the inclusions, but libxml2 >= 2.13
|
|
98
|
+
# no longer records ``xml:base`` on included elements, so every xml:id
|
|
99
|
+
# gets attributed to the root source file. Acceptable as a fallback:
|
|
100
|
+
# the mapping stays complete, only its per-file granularity is lost.
|
|
101
|
+
src_tree.xinclude()
|
|
94
102
|
|
|
95
103
|
# Walk though every element with an xml ID. Note: the type stubs don't have the ``iterfind`` method, hence the ignore in the next line.
|
|
96
104
|
for elem in src_tree.iterfind(f".//*[@{xml_id_attrib}]"): # type: ignore
|
|
@@ -20,10 +20,12 @@
|
|
|
20
20
|
# Python Version History
|
|
21
21
|
# vermin is a great linter/checker to check versions required
|
|
22
22
|
# https://github.com/netromdk/vermin.git
|
|
23
|
+
# 2026-07-06: this module expects Python 3.10 or newer
|
|
24
|
+
# FileInput(..., encoding="utf-8") added, required Python 3.10
|
|
25
|
+
# stack.py: str.removeprefix(), str.removesuffix() requires Python 3.9
|
|
23
26
|
# 2023-10-13: this module expects Python 3.8 or newer
|
|
24
27
|
# shutil.copytree now has dirs_exist_ok argument
|
|
25
28
|
# 2021-05-21: this module expects Python 3.6 or newer
|
|
26
|
-
#
|
|
27
29
|
# subprocess.run() requires Python 3.5
|
|
28
30
|
# shutil.which() member requires 3.3
|
|
29
31
|
# otherwise Python 3.0 might be sufficient
|
|
@@ -192,7 +194,14 @@ def mathjax_latex(xml_source, pub_file, out_file, dest_dir, math_format, math_cr
|
|
|
192
194
|
# Trying to correct baseline for inline math in Kindle, so we
|
|
193
195
|
# insert a \mathstrut into all the inline math before feeding to MathJax
|
|
194
196
|
if math_format == "kindle":
|
|
195
|
-
|
|
197
|
+
# fileinput's in-place mode rewrites each file line by line: print() writes
|
|
198
|
+
# back onto the file, and end="" suppresses the extra newline it would add
|
|
199
|
+
# (the line already carries its own). Two Windows pitfalls handled here:
|
|
200
|
+
# - the "with" context manager closes the handle before the file is
|
|
201
|
+
# reopened/replaced, which otherwise raises a PermissionError (PR #1779)
|
|
202
|
+
# - encoding="utf-8" reads/writes UTF-8 rather than the locale default,
|
|
203
|
+
# which would mis-decode UTF-8 math/HTML on a cp1252 locale (PR #3003)
|
|
204
|
+
with fileinput.FileInput(mjinput, inplace=True, encoding="utf-8") as file:
|
|
196
205
|
for line in file:
|
|
197
206
|
print(line.replace(r"\(", r"\(\mathstrut "), end="")
|
|
198
207
|
|
|
@@ -234,15 +243,16 @@ def mathjax_latex(xml_source, pub_file, out_file, dest_dir, math_format, math_cr
|
|
|
234
243
|
orig = " "
|
|
235
244
|
repl = " "
|
|
236
245
|
xhtml_elt = re.compile(orig)
|
|
237
|
-
# the inplace facility of the fileinput module gets
|
|
238
|
-
# confused about temporary backup files if the working
|
|
239
|
-
# directory is not where the file lives
|
|
240
|
-
# Also, print() here actual writes on the file, as
|
|
241
|
-
# another facility of the fileinput module, but we need
|
|
242
|
-
# to kill the "extra" newline that print() creates
|
|
243
246
|
with common.working_directory(tmp_dir):
|
|
244
247
|
html_file = mjoutput
|
|
245
|
-
|
|
248
|
+
# fileinput's in-place mode rewrites each file line by line: print() writes
|
|
249
|
+
# back onto the file, and end="" suppresses the extra newline it would add
|
|
250
|
+
# (the line already carries its own). Two Windows pitfalls handled here:
|
|
251
|
+
# - the "with" context manager closes the handle before the file is
|
|
252
|
+
# reopened/replaced, which otherwise raises a PermissionError (PR #1779)
|
|
253
|
+
# - encoding="utf-8" reads/writes UTF-8 rather than the locale default,
|
|
254
|
+
# which would mis-decode UTF-8 math/HTML on a cp1252 locale (PR #3003)
|
|
255
|
+
with fileinput.FileInput(html_file, inplace=True, encoding="utf-8") as file:
|
|
246
256
|
for line in file:
|
|
247
257
|
print(xhtml_elt.sub(repl, line), end="")
|
|
248
258
|
|
|
@@ -2849,17 +2859,18 @@ def epub(xml_source, pub_file, out_file, dest_dir, file_format, math_format, str
|
|
|
2849
2859
|
# <html xmlns="http://www.w3.org/1999/xhtml">
|
|
2850
2860
|
orig = "<html"
|
|
2851
2861
|
repl = __xml_header + '<html xmlns="http://www.w3.org/1999/xhtml"'
|
|
2852
|
-
# the inoplace facility of the fileinput module gets
|
|
2853
|
-
# confused about temporary backup files if the working
|
|
2854
|
-
# directory is not where the file lives
|
|
2855
|
-
# Also, print() here actual writes on the file, as
|
|
2856
|
-
# another facility of the fileinput module, but we need
|
|
2857
|
-
# to kill the "extra" newline that print() creates
|
|
2858
2862
|
with common.working_directory(xhtml_dir):
|
|
2859
2863
|
html_elt = re.compile(orig)
|
|
2860
2864
|
for root, dirs, files in os.walk(xhtml_dir):
|
|
2861
2865
|
for fn in files:
|
|
2862
|
-
|
|
2866
|
+
# fileinput's in-place mode rewrites each file line by line: print() writes
|
|
2867
|
+
# back onto the file, and end="" suppresses the extra newline it would add
|
|
2868
|
+
# (the line already carries its own). Two Windows pitfalls handled here:
|
|
2869
|
+
# - the "with" context manager closes the handle before the file is
|
|
2870
|
+
# reopened/replaced, which otherwise raises a PermissionError (PR #1779)
|
|
2871
|
+
# - encoding="utf-8" reads/writes UTF-8 rather than the locale default,
|
|
2872
|
+
# which would mis-decode UTF-8 math/HTML on a cp1252 locale (PR #3003)
|
|
2873
|
+
with fileinput.FileInput(fn, inplace=True, encoding="utf-8") as file:
|
|
2863
2874
|
for line in file:
|
|
2864
2875
|
print(html_elt.sub(repl, line), end="")
|
|
2865
2876
|
|
|
@@ -3625,7 +3636,7 @@ def html(xml, pub_file, stringparams, xmlid_root, file_format, extra_xsl, out_fi
|
|
|
3625
3636
|
tmp_dir = common.get_temporary_directory()
|
|
3626
3637
|
|
|
3627
3638
|
pub_vars = common.get_publisher_variable_report(xml, pub_file, stringparams)
|
|
3628
|
-
include_static_files = common.get_publisher_variable(pub_vars, '
|
|
3639
|
+
include_static_files = common.get_publisher_variable(pub_vars, 'b-cdn-resources') != "true"
|
|
3629
3640
|
time_logger.log("pubvars loaded")
|
|
3630
3641
|
|
|
3631
3642
|
if include_static_files:
|
|
@@ -3669,6 +3680,14 @@ def html(xml, pub_file, stringparams, xmlid_root, file_format, extra_xsl, out_fi
|
|
|
3669
3680
|
common.xsltproc(extraction_xslt, xml, None, tmp_dir, stringparams)
|
|
3670
3681
|
time_logger.log("xsltproc complete")
|
|
3671
3682
|
|
|
3683
|
+
if common.get_publisher_variable(pub_vars, 'host-platform') == "runestone":
|
|
3684
|
+
log.info("building Runestone page template for {} in {}".format(xml, tmp_dir))
|
|
3685
|
+
runestone_page_template_xslt = os.path.join(
|
|
3686
|
+
common.get_ptx_xsl_path(), "extract-runestone-page-template.xsl"
|
|
3687
|
+
)
|
|
3688
|
+
common.xsltproc(runestone_page_template_xslt, xml, None, tmp_dir, stringparams)
|
|
3689
|
+
time_logger.log("runestone page template extraction complete")
|
|
3690
|
+
|
|
3672
3691
|
if not(include_static_files):
|
|
3673
3692
|
# remove latex-image generated directories for portable builds
|
|
3674
3693
|
shutil.rmtree(os.path.join(tmp_dir, "generated", "latex-image"), ignore_errors=True)
|
|
@@ -4292,6 +4311,10 @@ def _downgrade_svg2_for_batik(directory):
|
|
|
4292
4311
|
|
|
4293
4312
|
* a "use" element carrying a plain "href" rather than "xlink:href".
|
|
4294
4313
|
"""
|
|
4314
|
+
# TODO (2026-07-03) This function may modify SVG 1.1 files coming
|
|
4315
|
+
# from PreFigure that are known to be good for Batik to ingest.
|
|
4316
|
+
# With access to source, we can determine filenames to avoid.
|
|
4317
|
+
|
|
4295
4318
|
import glob
|
|
4296
4319
|
import copy
|
|
4297
4320
|
import lxml.etree as ET
|
|
@@ -4507,7 +4530,341 @@ def pdf_fo(xml, pub_file, stringparams, out_file, dest_dir):
|
|
|
4507
4530
|
#
|
|
4508
4531
|
########################
|
|
4509
4532
|
|
|
4510
|
-
def validate(xml_source, out_file, dest_dir):
|
|
4533
|
+
def validate(xml_source, pub_file, stringparams, out_file, dest_dir, method):
|
|
4534
|
+
"""Validate source against the RELAX-NG schema, locally or via a server"""
|
|
4535
|
+
|
|
4536
|
+
if method == "server":
|
|
4537
|
+
_validate_server(xml_source, out_file, dest_dir)
|
|
4538
|
+
else:
|
|
4539
|
+
# "local" validates against the production schema, and
|
|
4540
|
+
# "local-dev" against the development schema, each with a
|
|
4541
|
+
# report meant for an author. The "agent" method is the
|
|
4542
|
+
# production schema with terse output meant for a program
|
|
4543
|
+
# (and so is not elective from the "pretext/pretext" script).
|
|
4544
|
+
if method == "local-dev":
|
|
4545
|
+
schema_file = "pretext-dev.rng"
|
|
4546
|
+
else:
|
|
4547
|
+
schema_file = "pretext.rng"
|
|
4548
|
+
terse = method == "agent"
|
|
4549
|
+
_validate_local(
|
|
4550
|
+
xml_source, pub_file, stringparams, out_file, dest_dir, schema_file, terse
|
|
4551
|
+
)
|
|
4552
|
+
|
|
4553
|
+
|
|
4554
|
+
# the "pi" namespace attribute recording an element's originating file
|
|
4555
|
+
PI_SOURCE_URI = "{http://pretextbook.org/2020/pretext/internal}source-uri"
|
|
4556
|
+
|
|
4557
|
+
|
|
4558
|
+
def _validate_local(xml_source, pub_file, stringparams, out_file, dest_dir, schema_file, terse):
|
|
4559
|
+
"""Validate the assembled source with an installed "jing" program"""
|
|
4560
|
+
|
|
4561
|
+
# to ensure provided stringparams aren't mutated unintentionally
|
|
4562
|
+
stringparams = stringparams.copy()
|
|
4563
|
+
|
|
4564
|
+
# "jing" is a Java program, so a configuration can be simply the
|
|
4565
|
+
# name of an executable ("jing", say, from a system package), or a
|
|
4566
|
+
# command with options ("java -jar /usr/share/java/jing.jar", say)
|
|
4567
|
+
jing_exec_cmd = common.get_executable_cmd("jing")
|
|
4568
|
+
|
|
4569
|
+
# the consolidated report, and the assembled source deposited
|
|
4570
|
+
# alongside it (wherever the report lands)
|
|
4571
|
+
reportname = common.get_output_filename(
|
|
4572
|
+
xml_source, out_file, dest_dir, "-validation.txt"
|
|
4573
|
+
)
|
|
4574
|
+
assembled_source = os.path.join(
|
|
4575
|
+
os.path.dirname(os.path.abspath(reportname)),
|
|
4576
|
+
os.path.splitext(os.path.basename(xml_source))[0] + "-assembled.xml",
|
|
4577
|
+
)
|
|
4578
|
+
|
|
4579
|
+
tmp_dir = common.get_temporary_directory()
|
|
4580
|
+
|
|
4581
|
+
# Modular source files are knitted together here, rather than
|
|
4582
|
+
# during assembly, so each included file can be recorded on its
|
|
4583
|
+
# root element (as a @pi:source-uri attribute) and problems can
|
|
4584
|
+
# then be attributed to the file where they lie
|
|
4585
|
+
source_dir = os.path.dirname(os.path.abspath(xml_source))
|
|
4586
|
+
main_file = os.path.basename(xml_source)
|
|
4587
|
+
|
|
4588
|
+
def _stamping_loader(href, parse, encoding=None):
|
|
4589
|
+
if parse == "xml":
|
|
4590
|
+
elt = ET.parse(href).getroot()
|
|
4591
|
+
elt.set(PI_SOURCE_URI, href)
|
|
4592
|
+
return elt
|
|
4593
|
+
with open(href, "r", encoding=(encoding or "utf-8")) as f:
|
|
4594
|
+
return f.read()
|
|
4595
|
+
|
|
4596
|
+
merged_source = os.path.join(tmp_dir, "merged.xml")
|
|
4597
|
+
source_tree = ET.parse(xml_source)
|
|
4598
|
+
try:
|
|
4599
|
+
from lxml import ElementInclude
|
|
4600
|
+
|
|
4601
|
+
ElementInclude.include(
|
|
4602
|
+
source_tree.getroot(), loader=_stamping_loader, max_depth=20
|
|
4603
|
+
)
|
|
4604
|
+
except Exception as e:
|
|
4605
|
+
log.warning(
|
|
4606
|
+
"file attribution of validation problems unavailable ({})".format(e)
|
|
4607
|
+
)
|
|
4608
|
+
source_tree = ET.parse(xml_source)
|
|
4609
|
+
source_tree.xinclude()
|
|
4610
|
+
# The temporary merged file must resolve relative references (a
|
|
4611
|
+
# customizations file, say) as the original source would, which is
|
|
4612
|
+
# exactly the job of an @xml:base (assembly drops it from output)
|
|
4613
|
+
source_tree.getroot().set(
|
|
4614
|
+
"{http://www.w3.org/XML/1998/namespace}base", os.path.abspath(xml_source)
|
|
4615
|
+
)
|
|
4616
|
+
source_tree.write(merged_source, encoding="utf-8", xml_declaration=True)
|
|
4617
|
+
|
|
4618
|
+
# Validation is performed on the "version" tree: the merged source
|
|
4619
|
+
# reduced by any "version" support elected in a publication file.
|
|
4620
|
+
# The paths of any messages refer to this assembled source, so it
|
|
4621
|
+
# is deposited next to the report, for cross-referencing, rather
|
|
4622
|
+
# than evaporating with a temporary directory.
|
|
4623
|
+
stringparams["assembly.file-attribution"] = "yes"
|
|
4624
|
+
attributed_source = os.path.join(tmp_dir, "assembled-attributed.xml")
|
|
4625
|
+
assembly(merged_source, pub_file, stringparams, attributed_source, None, "version")
|
|
4626
|
+
|
|
4627
|
+
# Tree "A" retains the file attributions, which are stripped to
|
|
4628
|
+
# make the deposited assembled source (what "jing" examines, since
|
|
4629
|
+
# the schema knows nothing of the attribute). Tree "B" is that
|
|
4630
|
+
# deposited file re-parsed, so line numbers agree with what "jing"
|
|
4631
|
+
# reports; elements of the two trees correspond in document order.
|
|
4632
|
+
tree_a = ET.parse(attributed_source)
|
|
4633
|
+
file_of_a = {}
|
|
4634
|
+
current_files = {tree_a.getroot(): main_file}
|
|
4635
|
+
for elt in tree_a.iter():
|
|
4636
|
+
if not isinstance(elt.tag, str):
|
|
4637
|
+
continue
|
|
4638
|
+
uri = elt.attrib.pop(PI_SOURCE_URI, None)
|
|
4639
|
+
if uri is not None:
|
|
4640
|
+
filename = os.path.relpath(uri, start=source_dir)
|
|
4641
|
+
else:
|
|
4642
|
+
parent = elt.getparent()
|
|
4643
|
+
filename = file_of_a.get(parent, main_file)
|
|
4644
|
+
file_of_a[elt] = filename
|
|
4645
|
+
tree_a.write(assembled_source, encoding="utf-8", xml_declaration=True)
|
|
4646
|
+
tree_b = ET.parse(assembled_source)
|
|
4647
|
+
a_elements = [e for e in tree_a.iter() if isinstance(e.tag, str)]
|
|
4648
|
+
b_elements = [e for e in tree_b.iter() if isinstance(e.tag, str)]
|
|
4649
|
+
file_of = {b: file_of_a[a] for a, b in zip(a_elements, b_elements)}
|
|
4650
|
+
# the last element to *begin* on each line, in document order
|
|
4651
|
+
opening = {}
|
|
4652
|
+
for elt in b_elements:
|
|
4653
|
+
opening[elt.sourceline] = elt
|
|
4654
|
+
with open(assembled_source) as f:
|
|
4655
|
+
source_lines = f.readlines()
|
|
4656
|
+
|
|
4657
|
+
# fresh schema from the PreTeXt distribution, in XML syntax
|
|
4658
|
+
schema_filename = os.path.join(common.get_ptx_path(), "schema", schema_file)
|
|
4659
|
+
|
|
4660
|
+
full_cmd = jing_exec_cmd + [schema_filename, assembled_source]
|
|
4661
|
+
log.debug("jing command: {}".format(" ".join(full_cmd)))
|
|
4662
|
+
result = subprocess.run(full_cmd, capture_output=True, text=True)
|
|
4663
|
+
# jing exits 0 when the document is valid, 1 when messages result
|
|
4664
|
+
if result.returncode == 0:
|
|
4665
|
+
log.info("the source validates with no schema errors")
|
|
4666
|
+
elif result.returncode > 1:
|
|
4667
|
+
log.warning('the "jing" program failed (code {})'.format(result.returncode))
|
|
4668
|
+
jing_messages = result.stdout.splitlines()
|
|
4669
|
+
|
|
4670
|
+
# The "validation-plus" stylesheet performs checks the RELAX-NG
|
|
4671
|
+
# schema cannot express, and provides advice besides. Applied to
|
|
4672
|
+
# the same assembled source, so locations are consistent with the
|
|
4673
|
+
# schema messages. Single-line output, one message per line.
|
|
4674
|
+
plus_xsl = os.path.join(
|
|
4675
|
+
common.get_ptx_path(), "schema", "pretext-validation-plus.xsl"
|
|
4676
|
+
)
|
|
4677
|
+
plus_scratch = os.path.join(tmp_dir, "validation-plus.txt")
|
|
4678
|
+
params = {"single.line.output": "yes"}
|
|
4679
|
+
common.xsltproc(plus_xsl, assembled_source, plus_scratch, None, params)
|
|
4680
|
+
with open(plus_scratch, "r") as f:
|
|
4681
|
+
plus_messages = [line.strip() for line in f if line.startswith("PTX:")]
|
|
4682
|
+
|
|
4683
|
+
# helpers for locating a message's element in tree "B"
|
|
4684
|
+
|
|
4685
|
+
def _numbered_path(elt):
|
|
4686
|
+
parts = []
|
|
4687
|
+
while elt is not None and isinstance(elt.tag, str):
|
|
4688
|
+
name = ET.QName(elt).localname
|
|
4689
|
+
position = 1 + sum(
|
|
4690
|
+
1
|
|
4691
|
+
for sib in elt.itersiblings(preceding=True)
|
|
4692
|
+
if isinstance(sib.tag, str) and ET.QName(sib).localname == name
|
|
4693
|
+
)
|
|
4694
|
+
parts.append("{}[{}]".format(name, position))
|
|
4695
|
+
elt = elt.getparent()
|
|
4696
|
+
return "/" + "/".join(reversed(parts))
|
|
4697
|
+
|
|
4698
|
+
def _element_at_path(path):
|
|
4699
|
+
# follow a numbered path (as validation-plus produces)
|
|
4700
|
+
elt = tree_b.getroot()
|
|
4701
|
+
segments = path.strip("/").split("/")
|
|
4702
|
+
segment_pattern = re.compile(r"(.*)\[(\d+)\]")
|
|
4703
|
+
first = segment_pattern.match(segments[0])
|
|
4704
|
+
if not first or ET.QName(elt).localname != first.group(1):
|
|
4705
|
+
return None
|
|
4706
|
+
for segment in segments[1:]:
|
|
4707
|
+
match = segment_pattern.match(segment)
|
|
4708
|
+
if not match:
|
|
4709
|
+
return None
|
|
4710
|
+
name, position = match.group(1), int(match.group(2))
|
|
4711
|
+
count = 0
|
|
4712
|
+
found = None
|
|
4713
|
+
for child in elt:
|
|
4714
|
+
if isinstance(child.tag, str) and ET.QName(child).localname == name:
|
|
4715
|
+
count += 1
|
|
4716
|
+
if count == position:
|
|
4717
|
+
found = child
|
|
4718
|
+
break
|
|
4719
|
+
if found is None:
|
|
4720
|
+
return None
|
|
4721
|
+
elt = found
|
|
4722
|
+
return elt
|
|
4723
|
+
|
|
4724
|
+
def _excerpt(line_number):
|
|
4725
|
+
if 0 < line_number <= len(source_lines):
|
|
4726
|
+
text = source_lines[line_number - 1].strip()
|
|
4727
|
+
if len(text) > 100:
|
|
4728
|
+
text = text[:100] + "..."
|
|
4729
|
+
return text
|
|
4730
|
+
return None
|
|
4731
|
+
|
|
4732
|
+
# assemble the consolidated report
|
|
4733
|
+
report = []
|
|
4734
|
+
banner = "=" * 70
|
|
4735
|
+
|
|
4736
|
+
if not terse:
|
|
4737
|
+
report.extend(_validation_report_preamble(schema_filename, assembled_source))
|
|
4738
|
+
report.extend([banner, "Messages: RELAX-NG schema validation, from \"jing\"", banner, ""])
|
|
4739
|
+
# "jing" messages lead with filename:line:column. The line number
|
|
4740
|
+
# refers to the assembled source (which is deposited alongside the
|
|
4741
|
+
# report), so it is reported as such, supplemented by the
|
|
4742
|
+
# originating file, a path into the assembled source, and an
|
|
4743
|
+
# excerpt of the offending text. An element's extent is not
|
|
4744
|
+
# available, so for a message about a line where no element begins,
|
|
4745
|
+
# the location is the closest element beginning on an earlier line:
|
|
4746
|
+
# very often the container, and always a good place to start looking.
|
|
4747
|
+
location = re.compile(r"^.*?:(\d+):(\d+): (.*)$")
|
|
4748
|
+
for message in jing_messages:
|
|
4749
|
+
match = location.match(message)
|
|
4750
|
+
if not match:
|
|
4751
|
+
report.append(message)
|
|
4752
|
+
continue
|
|
4753
|
+
line_number = int(match.group(1))
|
|
4754
|
+
body = match.group(3)
|
|
4755
|
+
near = max((n for n in opening if n <= line_number), default=None)
|
|
4756
|
+
elt = opening[near] if near is not None else None
|
|
4757
|
+
filename = file_of.get(elt, main_file)
|
|
4758
|
+
path = _numbered_path(elt) if elt is not None else ""
|
|
4759
|
+
if terse:
|
|
4760
|
+
report.append("{}\t{}\t{}\t{}".format(filename, path, line_number, body))
|
|
4761
|
+
else:
|
|
4762
|
+
report.append(body)
|
|
4763
|
+
report.append(" file: {}".format(filename))
|
|
4764
|
+
report.append(" path: {}".format(path))
|
|
4765
|
+
report.append(" line: {}".format(line_number))
|
|
4766
|
+
excerpt = _excerpt(line_number)
|
|
4767
|
+
if excerpt:
|
|
4768
|
+
report.append(" text: {}".format(excerpt))
|
|
4769
|
+
report.append("")
|
|
4770
|
+
if not terse and not jing_messages:
|
|
4771
|
+
report.extend(["(no messages)", ""])
|
|
4772
|
+
|
|
4773
|
+
if not terse:
|
|
4774
|
+
report.extend([banner, "Messages: PreTeXt \"validation-plus\" stylesheet", banner, ""])
|
|
4775
|
+
plus_form = re.compile(r"^(PTX:[A-Z]+): (/\S+) (.*)$")
|
|
4776
|
+
for message in plus_messages:
|
|
4777
|
+
match = plus_form.match(message)
|
|
4778
|
+
if not match:
|
|
4779
|
+
report.append(message)
|
|
4780
|
+
continue
|
|
4781
|
+
severity, path, body = match.group(1), match.group(2), match.group(3)
|
|
4782
|
+
elt = _element_at_path(path)
|
|
4783
|
+
filename = file_of.get(elt, main_file)
|
|
4784
|
+
line_number = elt.sourceline if elt is not None else ""
|
|
4785
|
+
if terse:
|
|
4786
|
+
report.append(
|
|
4787
|
+
"{}\t{}\t{}\t{}: {}".format(filename, path, line_number, severity, body)
|
|
4788
|
+
)
|
|
4789
|
+
else:
|
|
4790
|
+
report.append("{}: {}".format(severity, body))
|
|
4791
|
+
report.append(" file: {}".format(filename))
|
|
4792
|
+
report.append(" path: {}".format(path))
|
|
4793
|
+
report.append(" line: {}".format(line_number))
|
|
4794
|
+
if elt is not None:
|
|
4795
|
+
excerpt = _excerpt(elt.sourceline)
|
|
4796
|
+
if excerpt:
|
|
4797
|
+
report.append(" text: {}".format(excerpt))
|
|
4798
|
+
report.append("")
|
|
4799
|
+
if not terse and not plus_messages:
|
|
4800
|
+
report.extend(["(no messages)", ""])
|
|
4801
|
+
|
|
4802
|
+
with open(reportname, "w") as f:
|
|
4803
|
+
f.write("\n".join(report))
|
|
4804
|
+
|
|
4805
|
+
if jing_messages:
|
|
4806
|
+
log.info("schema validation raised {} messages".format(len(jing_messages)))
|
|
4807
|
+
if plus_messages:
|
|
4808
|
+
log.info("validation-plus stylesheet raised {} messages".format(len(plus_messages)))
|
|
4809
|
+
else:
|
|
4810
|
+
log.info("validation-plus stylesheet raised no messages")
|
|
4811
|
+
log.info("consolidated validation report in {}".format(reportname))
|
|
4812
|
+
log.info("locations refer to the assembled source in {}".format(assembled_source))
|
|
4813
|
+
|
|
4814
|
+
|
|
4815
|
+
def _validation_report_preamble(schema_filename, assembled_source):
|
|
4816
|
+
"""The fixed introductory text of a validation report"""
|
|
4817
|
+
|
|
4818
|
+
return [
|
|
4819
|
+
"Validation Report",
|
|
4820
|
+
"=================",
|
|
4821
|
+
"",
|
|
4822
|
+
"Two tools have examined an assembled version of your source:",
|
|
4823
|
+
"",
|
|
4824
|
+
" (1) \"jing\" checked conformance with the RELAX-NG schema at",
|
|
4825
|
+
" {}".format(schema_filename),
|
|
4826
|
+
" (a schema can only describe parent-child relationships,",
|
|
4827
|
+
" plus the attributes of each element)",
|
|
4828
|
+
" (2) the PreTeXt \"validation-plus\" stylesheet made checks that",
|
|
4829
|
+
" no RELAX-NG schema could ever express, and offers advice",
|
|
4830
|
+
" besides",
|
|
4831
|
+
"",
|
|
4832
|
+
"Locations refer to the ASSEMBLED version of your source: your",
|
|
4833
|
+
"modular source files have been knitted together, and any version",
|
|
4834
|
+
"support has been applied (as elected by a \"version\" element",
|
|
4835
|
+
"within the \"source\" element of the publication file you",
|
|
4836
|
+
"supplied). In particular, an element excluded from the version",
|
|
4837
|
+
"being built cannot raise a message here. The assembled source",
|
|
4838
|
+
"has been deposited at",
|
|
4839
|
+
" {}".format(assembled_source),
|
|
4840
|
+
"",
|
|
4841
|
+
"Each message locates its problem four ways:",
|
|
4842
|
+
"",
|
|
4843
|
+
" file: the source file where the problem lies",
|
|
4844
|
+
" path: the location within the assembled source",
|
|
4845
|
+
" line: the line number within the assembled source",
|
|
4846
|
+
" text: an excerpt of the offending content",
|
|
4847
|
+
"",
|
|
4848
|
+
"Only \"file\" points into your own source files. In particular,",
|
|
4849
|
+
"\"line\" is a line number of the deposited assembled source named",
|
|
4850
|
+
"above, never of one of your files.",
|
|
4851
|
+
"",
|
|
4852
|
+
"To read a \"path\", count elements of each name. For example,",
|
|
4853
|
+
"",
|
|
4854
|
+
" /pretext[1]/book[1]/chapter[7]/section[2]/p[13]/em[2]",
|
|
4855
|
+
"",
|
|
4856
|
+
"is the second \"em\" within the thirteenth \"p\" (paragraph) of the",
|
|
4857
|
+
"second \"section\" of the seventh \"chapter\" of the book. Counts",
|
|
4858
|
+
"are of elements with the same name: that paragraph is the",
|
|
4859
|
+
"thirteenth \"p\" of its section, though other elements may precede",
|
|
4860
|
+
"it or intervene.",
|
|
4861
|
+
"",
|
|
4862
|
+
"",
|
|
4863
|
+
]
|
|
4864
|
+
|
|
4865
|
+
|
|
4866
|
+
def _validate_server(xml_source, out_file, dest_dir):
|
|
4867
|
+
"""Validate original source files with a round-trip to a server"""
|
|
4511
4868
|
|
|
4512
4869
|
try:
|
|
4513
4870
|
import requests # post()
|
|
@@ -4610,19 +4967,28 @@ def python_version():
|
|
|
4610
4967
|
|
|
4611
4968
|
|
|
4612
4969
|
def check_python_version():
|
|
4613
|
-
"""Raise error
|
|
4970
|
+
"""Raise an error for Python 2 (or less); warn for Python 3 before 3.10"""
|
|
4614
4971
|
|
|
4615
4972
|
# This test could be more precise,
|
|
4616
4973
|
# but only handling 2to3 switch when introduced
|
|
4617
4974
|
msg = "".join(
|
|
4618
4975
|
[
|
|
4619
|
-
"PreTeXt script/module expects Python 3.
|
|
4976
|
+
"PreTeXt script/module expects Python 3.10, not Python 2 or older\n",
|
|
4620
4977
|
"You have Python {}\n",
|
|
4621
4978
|
"** Try prefixing your command-line with 'python3 ' **",
|
|
4622
4979
|
]
|
|
4623
4980
|
)
|
|
4624
4981
|
if sys.version_info[0] <= 2:
|
|
4625
4982
|
raise (OSError(msg.format(python_version())))
|
|
4983
|
+
# Warn, but do not error, for Python 3 older than the minimum below
|
|
4984
|
+
# 2026-07-06: Python 3.10 or newer
|
|
4985
|
+
if sys.version_info[:2] < (3, 10):
|
|
4986
|
+
log.warning(
|
|
4987
|
+
"PreTeXt expects Python 3.10 or newer\n"
|
|
4988
|
+
"You have Python {}, and some operations may fail".format(
|
|
4989
|
+
python_version()
|
|
4990
|
+
)
|
|
4991
|
+
)
|
|
4626
4992
|
|
|
4627
4993
|
|
|
4628
4994
|
|
|
@@ -209,8 +209,13 @@ def webwork_to_xml(
|
|
|
209
209
|
disableCookies='1',
|
|
210
210
|
outputformat='raw'
|
|
211
211
|
)
|
|
212
|
-
#
|
|
213
|
-
|
|
212
|
+
# Try render_rpc first. If that fails, then fall back to html2xml.
|
|
213
|
+
render_rpc_response = requests.get(url=webwork2_render_rpc, params=params_for_version_determination)
|
|
214
|
+
if render_rpc_response.status_code == 200:
|
|
215
|
+
version_determination_json = render_rpc_response.json()
|
|
216
|
+
else:
|
|
217
|
+
version_determination_json = requests.get(
|
|
218
|
+
url=webwork2_html2xml, params=params_for_version_determination).json()
|
|
214
219
|
if "ww_version" in version_determination_json:
|
|
215
220
|
webwork2_version = version_determination_json["ww_version"]
|
|
216
221
|
webwork2_version_match = re.search(
|
|
Binary file
|
|
Binary file
|
|
@@ -675,5 +675,14 @@
|
|
|
675
675
|
"pretext-cli.yml": "67dbeb4e58a0e2c1540e52f2455fe22448243fedcbe0964552c90c8aa4fd5da2",
|
|
676
676
|
"pretext-deploy.yml": "b062511b40e2957771c97369e3e50fb8c8a1464629c90876be508c3361134d95",
|
|
677
677
|
"installPandoc.sh": "5cd6a5c675403873c0988ce57b95ea8f8ab41769ec6b3ef78e72d8f73041ae76"
|
|
678
|
+
},
|
|
679
|
+
"2.43.2": {
|
|
680
|
+
"project.ptx": "72288c59809503ed7867be3364fec942fa7f535fd6c9db5c756f60ceb7821b24",
|
|
681
|
+
"codechat_config.yaml": "7054d06771ce198b7aa615b2a8a9a0ceee30a1fdb40db308866f0735d37785d1",
|
|
682
|
+
".gitignore": "4f44923063bc4662f7459a4356748413f99b602476bd8f4bbd5e2640306f0ba7",
|
|
683
|
+
"devcontainer.json": "393021f77e0f84141b159aa9c05a41d080a08c681487d30727e81464ad15e494",
|
|
684
|
+
"pretext-cli.yml": "f504dfea6c66e93e78df3b87d9a7bf98af685e41bc6972a0e716d35700ed81f5",
|
|
685
|
+
"pretext-deploy.yml": "2c8e85f3faed49f9c169f7a76d1eedc25fdb3c19fd3f1a2360608d9173ab9133",
|
|
686
|
+
"installPandoc.sh": "68aa11e95f870652c2f6aba40427ac02c0e8fec33b21f06c09d36efe04acb5d2"
|
|
678
687
|
}
|
|
679
688
|
}
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|