88The trick for tracing is a marker: the document handed to pandoc carries a raw-LaTeX
99comment naming the field before each field's markdown, and pandoc copies raw blocks
1010through untouched. ``xelatex -file-line-error`` then reports every error as
11- ``set.tex:<line>: <message>``, and the last marker above that line names the field.
11+ ``set.tex:<line>: <message>``, and the last marker above that line names the field. Not
12+ every error, though: a file LaTeX cannot find is announced with no location at all, and
13+ is traced instead through the ``Emergency stop.`` that follows it, which has one.
1214
1315pandoc and xelatex are both optional, as they are everywhere else in in2lambda: without
1416them this reports what to install rather than raising.
4850)
4951"""One ``-file-line-error`` line. The file is only ``set.tex`` for the set's own text."""
5052
51- _ABORTED = re .compile (r"^! (.+)$" , re .MULTILINE )
52- r"""An error xelatex printed with no file and line to it - see :func:`_aborted`."""
53+ _BARE = re .compile (r"^! (.+)$" , re .MULTILINE )
54+ r"""An error xelatex printed with no file and line to it.
55+
56+ ``-file-line-error`` only rewrites an error raised at a line of a file. A file that
57+ cannot be found is announced through ``\typeout`` rather than raised, and an error
58+ raised once the input has run out - ``File ended while scanning use of \frac`` - has no
59+ line left to name, so both are only ever in the log behind a ``!``.
60+ """
61+
62+ _STOP = "Emergency stop."
63+ """TeX's last line, which says where it gave up rather than what was wrong."""
5364
5465_IMAGE = re .compile (r"(!\[[^\]]*\]\()([^)]*)(\))" )
5566"""A markdown image with its path apart, so that the path can be rewritten or dropped."""
@@ -133,14 +144,13 @@ def render(
133144 latex , log = _compile (fields , images , work )
134145 problems = _reported (log , _locations (latex ))
135146 if not (compiled := work / "set.pdf" ).is_file ():
136- # Nothing traced back to a field is the emergency-stop case: xelatex gave up
137- # where it could name no line, so the log's own `!` lines are the only
138- # account there is of why nothing was typeset, and saying none would leave
139- # the refusal naming no cause at all.
140- said = [str (problem ) for problem in problems ] or _aborted (log )
147+ # Why nothing was typeset and not merely that nothing was: whatever stopped
148+ # xelatex is among the problems like any other error, traced to the field
149+ # the stop happened in where there is one.
150+ said = "; " .join (str (problem ) for problem in problems )
141151 raise CompileFailed (
142152 f"xelatex produced no PDF of { output .name } "
143- + (": " + "; " . join ( said ) if said else "." )
153+ + (f ": { said } " if said else "." )
144154 )
145155 output .parent .mkdir (parents = True , exist_ok = True )
146156 shutil .copy (compiled , output )
@@ -252,17 +262,6 @@ def _marked_document(fields: list[tuple[str, str]], available: set[str]) -> str:
252262 return "\n " .join (blocks )
253263
254264
255- def _aborted (log : str ) -> list [str ]:
256- r"""Everything the xelatex log says went wrong without saying where.
257-
258- ``-file-line-error`` rewrites an error that happened at a line of a file, which is
259- what :func:`_reported` reads. An error that happened as the input ran out -
260- ``File ended while scanning use of \frac`` - has no such line, and xelatex prints it
261- the old way, so it is only ever in the log behind a ``!``.
262- """
263- return [error .strip () for error in _ABORTED .findall (log )]
264-
265-
266265def _locations (latex : str ) -> list [tuple [int , str ]]:
267266 """Each marker in the generated LaTeX as the line it is on and the field it names."""
268267 return [
@@ -272,20 +271,49 @@ def _locations(latex: str) -> list[tuple[int, str]]:
272271 ]
273272
274273
274+ def _where (file : str , line : int , locations : list [tuple [int , str ]]) -> str :
275+ """The field this line of this file is in, or `_SET` if it is in none of them.
276+
277+ Only ``set.tex`` holds the set's own text, so a line of a package or a font is
278+ nothing to do with the markers however it numbers.
279+ """
280+ where = _SET
281+ if file == "set.tex" :
282+ for number , location in locations :
283+ if number <= line :
284+ where = location
285+ return where
286+
287+
275288def _reported (log : str , locations : list [tuple [int , str ]]) -> list [Problem ]:
276289 """The xelatex log's errors as problems, each against the field it happened in.
277290
278291 The same error repeated - a command used twice, say - is one problem, since the
279- author has one thing to go and fix.
292+ author has one thing to go and fix. An error printed bare is reported where the
293+ ``Emergency stop.`` after it says the reading had got to, that being the only line
294+ of an abort with a location on it, and the stop itself only when there is nothing
295+ else to say.
280296 """
281- found = []
297+ bare = [message .strip () for message in _BARE .findall (log )]
298+ stop = _SET if _STOP in bare else None
299+
300+ errors = []
282301 for error in _ERROR .finditer (log ):
283302 file , line , message = error [1 ], int (error [2 ]), error [3 ].strip ()
284- where = _SET
285- if file == "set.tex" :
286- for number , location in locations :
287- if number <= line :
288- where = location
303+ if message == _STOP :
304+ stop = _where (file , line , locations )
305+ else :
306+ errors .append ((_where (file , line , locations ), message ))
307+
308+ # Only the first bare error: whatever follows it is TeX unwinding from it, and the
309+ # author has the one thing to go and fix.
310+ if causes := [message for message in bare if message != _STOP ]:
311+ errors .append ((stop or _SET , causes [0 ]))
312+ elif not errors and stop :
313+ errors .append ((stop , _STOP ))
314+
315+ found : list [Problem ] = []
316+ for where , message in errors :
289317 problem = Problem (where , f"the PDF generator cannot compile this: { message } " )
290318 if problem not in found :
291319 found .append (problem )
0 commit comments