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.
Files changed (67) hide show
  1. {pretext-2.43.1 → pretext-2.43.2}/PKG-INFO +1 -1
  2. {pretext-2.43.1 → pretext-2.43.2}/pretext/__init__.py +1 -1
  3. {pretext-2.43.1 → pretext-2.43.2}/pretext/codechat.py +11 -3
  4. {pretext-2.43.1 → pretext-2.43.2}/pretext/core/pretext.py +386 -20
  5. {pretext-2.43.1 → pretext-2.43.2}/pretext/core/webwork.py +7 -2
  6. {pretext-2.43.1 → pretext-2.43.2}/pretext/resources/core.zip +0 -0
  7. {pretext-2.43.1 → pretext-2.43.2}/pretext/resources/pelican.zip +0 -0
  8. {pretext-2.43.1 → pretext-2.43.2}/pretext/resources/resource_hash_table.json +9 -0
  9. {pretext-2.43.1 → pretext-2.43.2}/pretext/resources/rs_cache.zip +0 -0
  10. {pretext-2.43.1 → pretext-2.43.2}/pretext/resources/templates.zip +0 -0
  11. {pretext-2.43.1 → pretext-2.43.2}/pyproject.toml +1 -1
  12. {pretext-2.43.1 → pretext-2.43.2}/LICENSE +0 -0
  13. {pretext-2.43.1 → pretext-2.43.2}/README.md +0 -0
  14. {pretext-2.43.1 → pretext-2.43.2}/pretext/__main__.py +0 -0
  15. {pretext-2.43.1 → pretext-2.43.2}/pretext/cli.py +0 -0
  16. {pretext-2.43.1 → pretext-2.43.2}/pretext/constants.py +0 -0
  17. {pretext-2.43.1 → pretext-2.43.2}/pretext/core/__init__.py +0 -0
  18. {pretext-2.43.1 → pretext-2.43.2}/pretext/core/braille_format.py +0 -0
  19. {pretext-2.43.1 → pretext-2.43.2}/pretext/core/common.py +0 -0
  20. {pretext-2.43.1 → pretext-2.43.2}/pretext/core/stack.py +0 -0
  21. {pretext-2.43.1 → pretext-2.43.2}/pretext/logger.py +0 -0
  22. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Alignment.jinja2s +0 -0
  23. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Arrays.jinja2s +0 -0
  24. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Bibliography.jinja2s +0 -0
  25. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Boxes.jinja2s +0 -0
  26. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Breaking.jinja2s +0 -0
  27. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Crossref.jinja2s +0 -0
  28. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Floats.jinja2s +0 -0
  29. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/FontSelection.jinja2s +0 -0
  30. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Footnotes.jinja2s +0 -0
  31. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Index.jinja2s +0 -0
  32. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Lists.jinja2s +0 -0
  33. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Pictures.jinja2s +0 -0
  34. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Quotations.jinja2s +0 -0
  35. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Sectioning.jinja2s +0 -0
  36. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Sentences.jinja2s +0 -0
  37. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Space.jinja2s +0 -0
  38. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Tabbing.jinja2s +0 -0
  39. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Themes/__init__.py +0 -0
  40. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Themes/default/__init__.py +0 -0
  41. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Themes/default/default-layout.jinja2 +0 -0
  42. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Themes/default/document-layout.jinja2 +0 -0
  43. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Thms.jinja2s +0 -0
  44. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/Verbatim.jinja2s +0 -0
  45. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/__init__.py +0 -0
  46. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/babel.jinja2s +0 -0
  47. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/graphicx.jinja2s +0 -0
  48. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/hyperref.jinja2s +0 -0
  49. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/lipsum.jinja2s +0 -0
  50. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/longtable.jinja2s +0 -0
  51. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/math.jinja2s +0 -0
  52. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/misc.jinja2s +0 -0
  53. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/natbib.jinja2s +0 -0
  54. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/p.jinja2s +0 -0
  55. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/subfig.jinja2s +0 -0
  56. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/textcomp.jinja2s +0 -0
  57. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/tikzcd.jinja2 +0 -0
  58. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/tikzpicture.jinja2 +0 -0
  59. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/url.jinja2s +0 -0
  60. {pretext-2.43.1 → pretext-2.43.2}/pretext/plastex/wrapfig.jinja2s +0 -0
  61. {pretext-2.43.1 → pretext-2.43.2}/pretext/project/__init__.py +0 -0
  62. {pretext-2.43.1 → pretext-2.43.2}/pretext/project/generate.py +0 -0
  63. {pretext-2.43.1 → pretext-2.43.2}/pretext/project/xml.py +0 -0
  64. {pretext-2.43.1 → pretext-2.43.2}/pretext/resources/__init__.py +0 -0
  65. {pretext-2.43.1 → pretext-2.43.2}/pretext/server.py +0 -0
  66. {pretext-2.43.1 → pretext-2.43.2}/pretext/types.py +0 -0
  67. {pretext-2.43.1 → pretext-2.43.2}/pretext/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pretext
3
- Version: 2.43.1
3
+ Version: 2.43.2
4
4
  Summary: A package to author, build, and deploy PreTeXt projects.
5
5
  Home-page: https://pretextbook.org
6
6
  License: GPL-3.0-or-later
@@ -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 = "73e61ec2c3f91468a80a12ce249fa69a734e6d26"
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
- # Previously, we used the following line instead of .xinclude(), but that no longer seems to work in 6.0 lxml.
92
- # lxml.ElementInclude.include(src_tree, loader=my_loader)
93
- src_tree.xinclude()
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
- with fileinput.FileInput(mjinput, inplace=True) as file:
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
- with fileinput.FileInput(html_file, inplace=True) as file:
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
- with fileinput.FileInput(fn, inplace=True) as file:
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, 'portable-html') != "yes"
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 with Python 2 (or less)"""
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.8, not Python 2 or older\n",
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
- # Always use html2xml for this; we don't know the server version yet
213
- version_determination_json = requests.get(url=webwork2_html2xml, params=params_for_version_determination).json()
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(
@@ -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
  }
@@ -11,7 +11,7 @@
11
11
  # ----------------
12
12
  [tool.poetry]
13
13
  name = "pretext"
14
- version = "2.43.1"
14
+ version = "2.43.2"
15
15
  description = "A package to author, build, and deploy PreTeXt projects."
16
16
  readme = "README.md"
17
17
  homepage = "https://pretextbook.org"
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