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
6428 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
const altTextRegex =
129
/Package tagpdf Warning: Alternative text for graphic is missing\.\s*\n\(tagpdf\)\s*Using ['`]([^'`]+)['`] instead\./g;
130
let match;
131
while ((match = altTextRegex.exec(logText)) !== null) {
132
result.missingAltText.push(match[1]);
133
}
134
135
// Match: LaTeX DocumentMetadata Warning: The language has not been set in
136
if (
137
/LaTeX DocumentMetadata Warning: The language has not been set in/.test(
138
logText,
139
)
140
) {
141
result.missingLanguage = true;
142
}
143
144
// Capture any other tagpdf warnings we haven't specifically handled
145
const otherTagpdfRegex = /Package tagpdf Warning: ([^\n]+)/g;
146
while ((match = otherTagpdfRegex.exec(logText)) !== null) {
147
const warning = match[1];
148
// Skip the alt text warning we already handle specifically
149
if (!warning.startsWith("Alternative text for graphic is missing")) {
150
result.otherWarnings.push(warning);
151
}
152
}
153
154
return result;
155
}
156
157
// Finds missing hyphenation files (these appear as warnings in the log file)
158
export function findMissingHyphenationFiles(logText: string) {
159
//ngerman gets special cased
160
const filterLang = (lang: string) => {
161
// It seems some languages have no hyphenation files, so we just filter them out
162
// e.g. `lang: zh` has no hyphenation files
163
// https://github.com/quarto-dev/quarto-cli/issues/10291
164
const noHyphen = ["chinese-hans", "chinese"];
165
if (noHyphen.includes(lang)) {
166
return;
167
}
168
169
// NOTE Although the names of the corresponding lfd files match those in this list,
170
// there are some exceptions, particularly in German and Serbian. So, ngerman is
171
// called here german, which is the name in the CLDR and, actually, the most logical.
172
//
173
// See https://ctan.math.utah.edu/ctan/tex-archive/macros/latex/required/babel/base/babel.pdf
174
if (lang === "ngerman") {
175
return "hyphen-german";
176
}
177
return `hyphen-${lang.toLowerCase()}`;
178
};
179
180
const babelWarningRegex = /^Package babel Warning:/m;
181
const hasWarning = logText.match(babelWarningRegex);
182
if (hasWarning) {
183
const languageRegex = /^\(babel\).* language [`'](\S+)[`'].*$/m;
184
const languageMatch = logText.match(languageRegex);
185
if (languageMatch) {
186
return filterLang(languageMatch[1]);
187
}
188
}
189
190
// Try an alternative way of parsing
191
const hyphenRulesRegex =
192
/Package babel Info: Hyphen rules for '(.*?)' set to \\l@nil/m;
193
const match = logText.match(hyphenRulesRegex);
194
if (match) {
195
const language = match[1];
196
if (language) {
197
return filterLang(language);
198
}
199
}
200
}
201
202
// Parse a log file to find latex errors
203
const kErrorRegex = /^\!\s([\s\S]+)?Here is how much/m;
204
const kEmptyRegex = /(No pages of output)\./;
205
206
export function findLatexError(
207
logText: string,
208
stderr?: string,
209
): string | undefined {
210
const errors: string[] = [];
211
212
const match = logText.match(kErrorRegex);
213
if (match) {
214
const hint = suggestHint(logText, stderr);
215
if (hint) {
216
errors.push(`${match[1]}\n${hint}`);
217
} else {
218
errors.push(match[1]);
219
}
220
}
221
222
if (errors.length === 0) {
223
const emptyMatch = logText.match(kEmptyRegex);
224
if (emptyMatch) {
225
errors.push(
226
`${emptyMatch[1]} - the document appears to have produced no output.`,
227
);
228
}
229
}
230
231
return errors.join("\n");
232
}
233
234
// Find the index error message
235
const kIndexErrorRegex = /^\s\s\s--\s(.*)/m;
236
export function findIndexError(logText: string): string | undefined {
237
const match = logText.match(kIndexErrorRegex);
238
if (match) {
239
return match[1];
240
} else {
241
return undefined;
242
}
243
}
244
245
// Search the missing font log for fonts
246
function findMissingFonts(dir: string): string[] {
247
const missingFonts = [];
248
// Look in the missing font file for any missing fonts
249
const missFontLog = join(dir, kMissingFontLog);
250
if (existsSync(missFontLog)) {
251
const missFontLogText = Deno.readTextFileSync(missFontLog);
252
const fontSearchTerms = findInMissingFontLog(missFontLogText);
253
missingFonts.push(...fontSearchTerms);
254
}
255
return missingFonts;
256
}
257
258
const formatFontFilter = (match: string, _text: string) => {
259
// Remove special prefix / suffix e.g. 'file:HaranoAjiMincho-Regular.otf:-kern;jfm=ujis'
260
// https://github.com/quarto-dev/quarto-cli/issues/12194
261
const base = basename(match).replace(/^.*?:|:.*$/g, "");
262
// return found file directly if it has an extension
263
return /[.]/.test(base) ? base : fontSearchTerm(base);
264
};
265
266
const estoPdfFilter = (_match: string, _text: string) => {
267
return "epstopdf";
268
};
269
270
const packageMatchers = [
271
// Fonts
272
{
273
regex: /.*! Font [^=]+=([^ ]+).+ not loadable.*/g,
274
filter: formatFontFilter,
275
},
276
{
277
regex: /.*! .*The font "([^"]+)" cannot be found.*/g,
278
filter: formatFontFilter,
279
},
280
{
281
regex: /.*!.+ error:.+\(file ([^)]+)\): .*/g,
282
filter: formatFontFilter,
283
},
284
{
285
regex: /.*Unable to find TFM file "([^"]+)".*/g,
286
filter: formatFontFilter,
287
},
288
{
289
regex: /.*\(fontspec\)\s+The font "([^"]+)" cannot be.*/g,
290
filter: formatFontFilter,
291
},
292
{
293
regex: /.*Package widetext error: Install the ([^ ]+) package.*/g,
294
filter: (match: string, _text: string) => {
295
return `${match}.sty`;
296
},
297
},
298
{ regex: /.* File [`'](.+eps-converted-to.pdf)'.*/g, filter: estoPdfFilter },
299
{ regex: /.*xdvipdfmx:fatal: pdf_ref_obj.*/g, filter: estoPdfFilter },
300
301
{
302
regex: /.* (tikzlibrary[^ ]+?[.]code[.]tex).*/g,
303
filter: (match: string, text: string) => {
304
if (text.match(/! Package tikz Error:/)) {
305
return match;
306
} else {
307
return undefined;
308
}
309
},
310
},
311
{
312
regex: /module 'lua-uni-normalize' not found:/g,
313
filter: (_match: string, _text: string) => {
314
return "lua-uni-algos.lua";
315
},
316
},
317
{
318
regex: /.* Package pdfx Error: No color profile ([^\s]*).*/g,
319
filter: (_match: string, _text: string) => {
320
return "colorprofiles.sty";
321
},
322
},
323
{
324
regex: /.*No support files for \\DocumentMetadata found.*/g,
325
filter: (_match: string, _text: string) => {
326
return "latex-lab";
327
},
328
},
329
{
330
// PDF/A requires embedded color profiles - pdfmanagement-testphase needs colorprofiles
331
regex: /.*\(pdf backend\): cannot open file for embedding.*/g,
332
filter: (_match: string, _text: string) => {
333
return "colorprofiles";
334
},
335
},
336
{
337
regex: /.*No file ([^`'. ]+[.]fd)[.].*/g,
338
filter: (match: string, _text: string) => {
339
return match.toLowerCase();
340
},
341
},
342
{ regex: /.* Loading '([^']+)' aborted!.*/g },
343
{ regex: /.*! LaTeX Error: File [`']([^']+)' not found.*/g },
344
{ regex: /.* [fF]ile ['`]?([^' ]+)'? not found.*/g },
345
{ regex: /.*the language definition file ([^\s]*).*/g },
346
{
347
regex: /.*! Package babel Error: Unknown option [`']([^'`]+)'[.].*/g,
348
filter: (match: string, _text: string) => {
349
return `${match}.ldf`;
350
},
351
},
352
{ regex: /.* \\(file ([^)]+)\\): cannot open .*/g },
353
{ regex: /.*file [`']([^']+)' .*is missing.*/g },
354
{ regex: /.*! CTeX fontset [`']([^']+)' is unavailable.*/g },
355
{ regex: /.*: ([^:]+): command not found.*/g },
356
{ regex: /.*! I can't find file [`']([^']+)'.*/g },
357
];
358
359
function fontSearchTerm(font: string): string {
360
const fontPattern = font.replace(/\s+/g, "\\s*");
361
return `${fontPattern}(-(Bold|Italic|Regular).*)?[.](tfm|afm|mf|otf|ttf)`;
362
}
363
364
function findMissingPackages(logFileText: string): string[] {
365
const toInstall: string[] = [];
366
367
packageMatchers.forEach((packageMatcher) => {
368
packageMatcher.regex.lastIndex = 0;
369
let match = packageMatcher.regex.exec(logFileText);
370
while (match != null) {
371
const file = match[1];
372
// Apply the filter, if there is one
373
const filteredFile = packageMatcher.filter
374
? packageMatcher.filter(file, logFileText)
375
: file;
376
377
// Capture any matches
378
if (filteredFile) {
379
toInstall.push(filteredFile);
380
}
381
382
match = packageMatcher.regex.exec(logFileText);
383
}
384
packageMatcher.regex.lastIndex = 0;
385
});
386
387
// dedulicated list of packages to attempt to install
388
return ld.uniq(toInstall);
389
}
390
391
function findInMissingFontLog(missFontLogText: string): string[] {
392
const toInstall: string[] = [];
393
lines(missFontLogText).forEach((line) => {
394
// Trim the line
395
line = line.trim();
396
397
// Extract the font from the end of the line
398
const fontMatch = line.match(/([^\s]*)$/);
399
if (fontMatch && fontMatch[1].trim() !== "") {
400
toInstall.push(fontMatch[1]);
401
}
402
403
// Extract the font install command from the front of the line
404
// Also request that this be installed
405
const commandMatch = line.match(/^([^\s]*)/);
406
if (commandMatch && commandMatch[1].trim() !== "") {
407
toInstall.push(commandMatch[1]);
408
}
409
});
410
411
// deduplicated list of fonts and font install commands
412
return ld.uniq(toInstall);
413
}
414
415
const kUnicodePattern = {
416
regex: /\! Package inputenc Error: Unicode character/,
417
hint:
418
"Possible unsupported unicode character in this configuration. Perhaps try another LaTeX engine (e.g. XeLaTeX).",
419
};
420
421
const kInlinePattern = {
422
regex: /Missing \$ inserted\./,
423
hint: "You may need to $ $ around an expression in this file.",
424
};
425
426
const kGhostPattern = {
427
regex: /^\!\!\! Error: Cannot open Ghostscript for piped input/m,
428
hint:
429
"GhostScript is likely required to compile this document. Please be sure GhostScript (https://ghostscript.com) is installed and try again.",
430
};
431
432
const kGhostCorruptPattern = {
433
regex: /^GPL Ghostscript .*: Can't find initialization file gs_init.ps/m,
434
hint:
435
"GhostScript is likely required to compile this document. Please be sure GhostScript (https://ghostscript.com) is installed and configured properly and try again.",
436
};
437
438
const kLogOutputPatterns = [kUnicodePattern, kInlinePattern];
439
const kStdErrPatterns = [kGhostPattern, kGhostCorruptPattern];
440
441
function suggestHint(
442
logText: string,
443
stderr?: string,
444
): string | undefined {
445
// Check stderr for hints
446
const stderrHint = kStdErrPatterns.find((errPattern) =>
447
stderr?.match(errPattern.regex)
448
);
449
450
if (stderrHint) {
451
return stderrHint.hint;
452
} else {
453
// Check the log file for hints
454
const logHint = kLogOutputPatterns.find((logPattern) =>
455
logText.match(logPattern.regex)
456
);
457
if (logHint) {
458
return logHint.hint;
459
} else {
460
return undefined;
461
}
462
}
463
}
464
465