Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/render/latexmk/parse-error.ts
12926 views
1
/*
2
* log.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { basename, join } from "../../../deno_ral/path.ts";
8
import { existsSync } from "../../../deno_ral/fs.ts";
9
import * as ld from "../../../core/lodash.ts";
10
11
import { lines } from "../../../core/text.ts";
12
13
// The missing font log file name
14
export const kMissingFontLog = "missfont.log";
15
16
// Reads log files and returns a list of search terms to use
17
// to find packages to install
18
export function findMissingFontsAndPackages(
19
logText: string,
20
dir: string,
21
): string[] {
22
// Look for missing fonts
23
const missingFonts = findMissingFonts(dir);
24
25
// Look in the log file itself
26
const missingPackages = findMissingPackages(logText);
27
28
return ld.uniq([...missingPackages, ...missingFonts]);
29
}
30
31
// Does the log file indicate recompilation is neeeded
32
export function needsRecompilation(log: string) {
33
if (existsSync(log)) {
34
const logContents = Deno.readTextFileSync(log);
35
36
// First look for an explicit request to recompile
37
const explicitMatches = explicitMatchers.some((matcher) => {
38
return logContents.match(matcher);
39
});
40
41
// If there are no explicit requests to re-compile
42
// Look for unresolved 'resolving' matches
43
if (explicitMatches) {
44
return true;
45
} else {
46
const unresolvedMatches = resolvingMatchers.some((resolvingMatcher) => {
47
// First see if there is a message indicating a match of something that
48
// might subsequently resolve
49
resolvingMatcher.unresolvedMatch.lastIndex = 0;
50
let unresolvedMatch = resolvingMatcher.unresolvedMatch.exec(
51
logContents,
52
);
53
const unresolvedMatches = [];
54
55
while (unresolvedMatch) {
56
// Now look for a message indicating that the issue
57
// has been resolved
58
const resolvedRegex = new RegExp(
59
resolvingMatcher.resolvedMatch.replace(
60
kCaptureToken,
61
unresolvedMatch[1],
62
),
63
"gm",
64
);
65
66
if (!logContents.match(resolvedRegex)) {
67
unresolvedMatches.push(unresolvedMatch[1]);
68
}
69
70
// Continue looking for other unresolved matches
71
unresolvedMatch = resolvingMatcher.unresolvedMatch.exec(
72
logContents,
73
);
74
}
75
76
if (unresolvedMatches.length > 0) {
77
// There is an unresolved match
78
return true;
79
} else {
80
// There is not an unresolved match
81
return false;
82
}
83
});
84
return !!unresolvedMatches;
85
}
86
}
87
return false;
88
}
89
const explicitMatchers = [
90
/(Rerun to get | Please \(re\)run | [rR]erun LaTeX\.)/, // explicitly request recompile
91
/^No file .*?.aux\.\s*$/gm, // missing aux file from a beamer run using lualatex #6226
92
];
93
94
// Resolving matchers are matchers that may resolve later in the log
95
// So inspect the for the first match, then if there is a match,
96
// inspect for the second match, which will indicate that the issue has
97
// been resolved.
98
// For example:
99
// Package marginnote Info: xpos seems to be \@mn@currxpos on input line 213. <- unpositioned element
100
// Package marginnote Info: xpos seems to be 367.46002pt on input line 213. <- positioned later in the log
101
const kCaptureToken = "${unresolvedCapture}";
102
const resolvingMatchers = [
103
{
104
unresolvedMatch: /^.*xpos seems to be \\@mn@currxpos.*?line ([0-9]*)\.$/gm,
105
resolvedMatch:
106
`^.*xpos seems to be [0-9]*\.[0-9]*pt.*?line ${kCaptureToken}\.$`,
107
},
108
];
109
110
// Finds PDF/UA accessibility warnings from tagpdf and DocumentMetadata
111
export interface PdfAccessibilityWarnings {
112
missingAltText: string[]; // filenames of images missing alt text
113
missingLanguage: boolean; // document language not set
114
otherWarnings: string[]; // other tagpdf warnings
115
}
116
117
export function findPdfAccessibilityWarnings(
118
logText: string,
119
): PdfAccessibilityWarnings {
120
const result: PdfAccessibilityWarnings = {
121
missingAltText: [],
122
missingLanguage: false,
123
otherWarnings: [],
124
};
125
126
// Match: Package tagpdf Warning: Alternative text for graphic is missing.
127
// (tagpdf) Using 'filename' instead.
128
// Note: tagpdf wraps long filenames across multiple (tagpdf) continuation
129
// lines, so we allow optional line breaks with (tagpdf) prefixes.
130
const altTextRegex =
131
/Package tagpdf Warning: Alternative text for graphic is missing\.\s*\n\(tagpdf\)\s*Using ['`]([^'`]+)['`]\s*(?:\n\(tagpdf\)\s*)?instead\./g;
132
let match;
133
while ((match = altTextRegex.exec(logText)) !== null) {
134
result.missingAltText.push(match[1]);
135
}
136
137
// Match: LaTeX DocumentMetadata Warning: The language has not been set in
138
if (
139
/LaTeX DocumentMetadata Warning: The language has not been set in/.test(
140
logText,
141
)
142
) {
143
result.missingLanguage = true;
144
}
145
146
// Capture any other tagpdf warnings we haven't specifically handled
147
const otherTagpdfRegex = /Package tagpdf Warning: ([^\n]+)/g;
148
while ((match = otherTagpdfRegex.exec(logText)) !== null) {
149
const warning = match[1];
150
// Skip the alt text warning we already handle specifically
151
if (!warning.startsWith("Alternative text for graphic is missing")) {
152
result.otherWarnings.push(warning);
153
}
154
}
155
156
return result;
157
}
158
159
// Finds missing hyphenation files (these appear as warnings in the log file)
160
export function findMissingHyphenationFiles(logText: string) {
161
//ngerman gets special cased
162
const filterLang = (lang: string) => {
163
// It seems some languages have no hyphenation files, so we just filter them out
164
// e.g. `lang: zh` has no hyphenation files
165
// https://github.com/quarto-dev/quarto-cli/issues/10291
166
const noHyphen = ["chinese-hans", "chinese"];
167
if (noHyphen.includes(lang)) {
168
return;
169
}
170
171
// NOTE Although the names of the corresponding lfd files match those in this list,
172
// there are some exceptions, particularly in German and Serbian. So, ngerman is
173
// called here german, which is the name in the CLDR and, actually, the most logical.
174
//
175
// See https://ctan.math.utah.edu/ctan/tex-archive/macros/latex/required/babel/base/babel.pdf
176
if (lang === "ngerman") {
177
return "hyphen-german";
178
}
179
return `hyphen-${lang.toLowerCase()}`;
180
};
181
182
const babelWarningRegex = /^Package babel Warning:/m;
183
const hasWarning = logText.match(babelWarningRegex);
184
if (hasWarning) {
185
const languageRegex = /^\(babel\).* language [`'](\S+)[`'].*$/m;
186
const languageMatch = logText.match(languageRegex);
187
if (languageMatch) {
188
return filterLang(languageMatch[1]);
189
}
190
}
191
192
// Try an alternative way of parsing
193
const hyphenRulesRegex =
194
/Package babel Info: Hyphen rules for '(.*?)' set to \\l@nil/m;
195
const match = logText.match(hyphenRulesRegex);
196
if (match) {
197
const language = match[1];
198
if (language) {
199
return filterLang(language);
200
}
201
}
202
}
203
204
// Parse a log file to find latex errors
205
const kErrorRegex = /^\!\s([\s\S]+)?Here is how much/m;
206
const kEmptyRegex = /(No pages of output)\./;
207
208
export function findLatexError(
209
logText: string,
210
stderr?: string,
211
): string | undefined {
212
const errors: string[] = [];
213
214
const match = logText.match(kErrorRegex);
215
if (match) {
216
const hint = suggestHint(logText, stderr);
217
if (hint) {
218
errors.push(`${match[1]}\n${hint}`);
219
} else {
220
errors.push(match[1]);
221
}
222
}
223
224
if (errors.length === 0) {
225
const emptyMatch = logText.match(kEmptyRegex);
226
if (emptyMatch) {
227
errors.push(
228
`${emptyMatch[1]} - the document appears to have produced no output.`,
229
);
230
}
231
}
232
233
return errors.join("\n");
234
}
235
236
// Find the index error message
237
const kIndexErrorRegex = /^\s\s\s--\s(.*)/m;
238
export function findIndexError(logText: string): string | undefined {
239
const match = logText.match(kIndexErrorRegex);
240
if (match) {
241
return match[1];
242
} else {
243
return undefined;
244
}
245
}
246
247
// Search the missing font log for fonts
248
function findMissingFonts(dir: string): string[] {
249
const missingFonts = [];
250
// Look in the missing font file for any missing fonts
251
const missFontLog = join(dir, kMissingFontLog);
252
if (existsSync(missFontLog)) {
253
const missFontLogText = Deno.readTextFileSync(missFontLog);
254
const fontSearchTerms = findInMissingFontLog(missFontLogText);
255
missingFonts.push(...fontSearchTerms);
256
}
257
return missingFonts;
258
}
259
260
const formatFontFilter = (match: string, _text: string) => {
261
// Remove special prefix / suffix e.g. 'file:HaranoAjiMincho-Regular.otf:-kern;jfm=ujis'
262
// https://github.com/quarto-dev/quarto-cli/issues/12194
263
const base = basename(match).replace(/^.*?:|:.*$/g, "");
264
// return found file directly if it has an extension
265
return /[.]/.test(base) ? base : fontSearchTerm(base);
266
};
267
268
const estoPdfFilter = (_match: string, _text: string) => {
269
return "epstopdf";
270
};
271
272
const packageMatchers = [
273
// Fonts
274
{
275
regex: /.*! Font [^=]+=([^ ]+).+ not loadable.*/g,
276
filter: formatFontFilter,
277
},
278
{
279
regex: /.*! .*The font "([^"]+)" cannot be found.*/g,
280
filter: formatFontFilter,
281
},
282
{
283
regex: /.*!.+ error:.+\(file ([^)]+)\): .*/g,
284
filter: formatFontFilter,
285
},
286
{
287
regex: /.*Unable to find TFM file "([^"]+)".*/g,
288
filter: formatFontFilter,
289
},
290
{
291
regex: /.*\(fontspec\)\s+The font "([^"]+)" cannot be.*/g,
292
filter: formatFontFilter,
293
},
294
{
295
regex: /.*Package widetext error: Install the ([^ ]+) package.*/g,
296
filter: (match: string, _text: string) => {
297
return `${match}.sty`;
298
},
299
},
300
{ regex: /.* File [`'](.+eps-converted-to.pdf)'.*/g, filter: estoPdfFilter },
301
{ regex: /.*xdvipdfmx:fatal: pdf_ref_obj.*/g, filter: estoPdfFilter },
302
303
{
304
regex: /.* (tikzlibrary[^ ]+?[.]code[.]tex).*/g,
305
filter: (match: string, text: string) => {
306
if (text.match(/! Package tikz Error:/)) {
307
return match;
308
} else {
309
return undefined;
310
}
311
},
312
},
313
{
314
regex: /module 'lua-uni-normalize' not found:/g,
315
filter: (_match: string, _text: string) => {
316
return "lua-uni-algos.lua";
317
},
318
},
319
{
320
regex: /.* Package pdfx Error: No color profile ([^\s]*).*/g,
321
filter: (_match: string, _text: string) => {
322
return "colorprofiles.sty";
323
},
324
},
325
{
326
regex: /.*No support files for \\DocumentMetadata found.*/g,
327
filter: (_match: string, _text: string) => {
328
return "latex-lab";
329
},
330
},
331
{
332
// PDF/A requires embedded color profiles - pdfmanagement-testphase needs colorprofiles
333
regex: /.*\(pdf backend\): cannot open file for embedding.*/g,
334
filter: (_match: string, _text: string) => {
335
return "colorprofiles";
336
},
337
},
338
{
339
regex: /.*No file ([^`'. ]+[.]fd)[.].*/g,
340
filter: (match: string, _text: string) => {
341
return match.toLowerCase();
342
},
343
},
344
{ regex: /.* Loading '([^']+)' aborted!.*/g },
345
{ regex: /.*! LaTeX Error: File [`']([^']+)' not found.*/g },
346
{ regex: /.* [fF]ile ['`]?([^' ]+)'? not found.*/g },
347
{ regex: /.*the language definition file ([^\s]*).*/g },
348
{
349
regex: /.*! Package babel Error: Unknown option [`']([^'`]+)'[.].*/g,
350
filter: (match: string, _text: string) => {
351
return `${match}.ldf`;
352
},
353
},
354
{ regex: /.* \\(file ([^)]+)\\): cannot open .*/g },
355
{ regex: /.*file [`']([^']+)' .*is missing.*/g },
356
{ regex: /.*! CTeX fontset [`']([^']+)' is unavailable.*/g },
357
{ regex: /.*: ([^:]+): command not found.*/g },
358
{ regex: /.*! I can't find file [`']([^']+)'.*/g },
359
];
360
361
function fontSearchTerm(font: string): string {
362
const fontPattern = font.replace(/\s+/g, "\\s*");
363
return `${fontPattern}(-(Bold|Italic|Regular).*)?[.](tfm|afm|mf|otf|ttf)`;
364
}
365
366
function findMissingPackages(logFileText: string): string[] {
367
const toInstall: string[] = [];
368
369
packageMatchers.forEach((packageMatcher) => {
370
packageMatcher.regex.lastIndex = 0;
371
let match = packageMatcher.regex.exec(logFileText);
372
while (match != null) {
373
const file = match[1];
374
// Apply the filter, if there is one
375
const filteredFile = packageMatcher.filter
376
? packageMatcher.filter(file, logFileText)
377
: file;
378
379
// Capture any matches
380
if (filteredFile) {
381
toInstall.push(filteredFile);
382
}
383
384
match = packageMatcher.regex.exec(logFileText);
385
}
386
packageMatcher.regex.lastIndex = 0;
387
});
388
389
// dedulicated list of packages to attempt to install
390
return ld.uniq(toInstall);
391
}
392
393
function findInMissingFontLog(missFontLogText: string): string[] {
394
const toInstall: string[] = [];
395
lines(missFontLogText).forEach((line) => {
396
// Trim the line
397
line = line.trim();
398
399
// Extract the font from the end of the line
400
const fontMatch = line.match(/([^\s]*)$/);
401
if (fontMatch && fontMatch[1].trim() !== "") {
402
toInstall.push(fontMatch[1]);
403
}
404
405
// Extract the font install command from the front of the line
406
// Also request that this be installed
407
const commandMatch = line.match(/^([^\s]*)/);
408
if (commandMatch && commandMatch[1].trim() !== "") {
409
toInstall.push(commandMatch[1]);
410
}
411
});
412
413
// deduplicated list of fonts and font install commands
414
return ld.uniq(toInstall);
415
}
416
417
const kUnicodePattern = {
418
regex: /\! Package inputenc Error: Unicode character/,
419
hint:
420
"Possible unsupported unicode character in this configuration. Perhaps try another LaTeX engine (e.g. XeLaTeX).",
421
};
422
423
const kInlinePattern = {
424
regex: /Missing \$ inserted\./,
425
hint: "You may need to $ $ around an expression in this file.",
426
};
427
428
const kGhostPattern = {
429
regex: /^\!\!\! Error: Cannot open Ghostscript for piped input/m,
430
hint:
431
"GhostScript is likely required to compile this document. Please be sure GhostScript (https://ghostscript.com) is installed and try again.",
432
};
433
434
const kGhostCorruptPattern = {
435
regex: /^GPL Ghostscript .*: Can't find initialization file gs_init.ps/m,
436
hint:
437
"GhostScript is likely required to compile this document. Please be sure GhostScript (https://ghostscript.com) is installed and configured properly and try again.",
438
};
439
440
const kLogOutputPatterns = [kUnicodePattern, kInlinePattern];
441
const kStdErrPatterns = [kGhostPattern, kGhostCorruptPattern];
442
443
function suggestHint(
444
logText: string,
445
stderr?: string,
446
): string | undefined {
447
// Check stderr for hints
448
const stderrHint = kStdErrPatterns.find((errPattern) =>
449
stderr?.match(errPattern.regex)
450
);
451
452
if (stderrHint) {
453
return stderrHint.hint;
454
} else {
455
// Check the log file for hints
456
const logHint = kLogOutputPatterns.find((logPattern) =>
457
logText.match(logPattern.regex)
458
);
459
if (logHint) {
460
return logHint.hint;
461
} else {
462
return undefined;
463
}
464
}
465
}
466
467