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