Path: blob/main/src/command/render/latexmk/parse-error.ts
3587 views
/*1* log.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*/56import { basename, join } from "../../../deno_ral/path.ts";7import { existsSync } from "../../../deno_ral/fs.ts";8import * as ld from "../../../core/lodash.ts";910import { lines } from "../../../core/text.ts";1112// The missing font log file name13export const kMissingFontLog = "missfont.log";1415// Reads log files and returns a list of search terms to use16// to find packages to install17export function findMissingFontsAndPackages(18logText: string,19dir: string,20): string[] {21// Look for missing fonts22const missingFonts = findMissingFonts(dir);2324// Look in the log file itself25const missingPackages = findMissingPackages(logText);2627return ld.uniq([...missingPackages, ...missingFonts]);28}2930// Does the log file indicate recompilation is neeeded31export function needsRecompilation(log: string) {32if (existsSync(log)) {33const logContents = Deno.readTextFileSync(log);3435// First look for an explicit request to recompile36const explicitMatches = explicitMatchers.some((matcher) => {37return logContents.match(matcher);38});3940// If there are no explicit requests to re-compile41// Look for unresolved 'resolving' matches42if (explicitMatches) {43return true;44} else {45const unresolvedMatches = resolvingMatchers.some((resolvingMatcher) => {46// First see if there is a message indicating a match of something that47// might subsequently resolve48resolvingMatcher.unresolvedMatch.lastIndex = 0;49let unresolvedMatch = resolvingMatcher.unresolvedMatch.exec(50logContents,51);52const unresolvedMatches = [];5354while (unresolvedMatch) {55// Now look for a message indicating that the issue56// has been resolved57const resolvedRegex = new RegExp(58resolvingMatcher.resolvedMatch.replace(59kCaptureToken,60unresolvedMatch[1],61),62"gm",63);6465if (!logContents.match(resolvedRegex)) {66unresolvedMatches.push(unresolvedMatch[1]);67}6869// Continue looking for other unresolved matches70unresolvedMatch = resolvingMatcher.unresolvedMatch.exec(71logContents,72);73}7475if (unresolvedMatches.length > 0) {76// There is an unresolved match77return true;78} else {79// There is not an unresolved match80return false;81}82});83return !!unresolvedMatches;84}85}86return false;87}88const explicitMatchers = [89/(Rerun to get | Please \(re\)run | [rR]erun LaTeX\.)/, // explicitly request recompile90/^No file .*?.aux\.\s*$/gm, // missing aux file from a beamer run using lualatex #622691];9293// Resolving matchers are matchers that may resolve later in the log94// So inspect the for the first match, then if there is a match,95// inspect for the second match, which will indicate that the issue has96// been resolved.97// For example:98// Package marginnote Info: xpos seems to be \@mn@currxpos on input line 213. <- unpositioned element99// Package marginnote Info: xpos seems to be 367.46002pt on input line 213. <- positioned later in the log100const kCaptureToken = "${unresolvedCapture}";101const resolvingMatchers = [102{103unresolvedMatch: /^.*xpos seems to be \\@mn@currxpos.*?line ([0-9]*)\.$/gm,104resolvedMatch:105`^.*xpos seems to be [0-9]*\.[0-9]*pt.*?line ${kCaptureToken}\.$`,106},107];108109// Finds missing hyphenation files (these appear as warnings in the log file)110export function findMissingHyphenationFiles(logText: string) {111//ngerman gets special cased112const filterLang = (lang: string) => {113// It seems some languages have no hyphenation files, so we just filter them out114// e.g. `lang: zh` has no hyphenation files115// https://github.com/quarto-dev/quarto-cli/issues/10291116const noHyphen = ["chinese-hans", "chinese"];117if (noHyphen.includes(lang)) {118return;119}120121// NOTE Although the names of the corresponding lfd files match those in this list,122// there are some exceptions, particularly in German and Serbian. So, ngerman is123// called here german, which is the name in the CLDR and, actually, the most logical.124//125// See https://ctan.math.utah.edu/ctan/tex-archive/macros/latex/required/babel/base/babel.pdf126if (lang === "ngerman") {127return "hyphen-german";128}129return `hyphen-${lang.toLowerCase()}`;130};131132const babelWarningRegex = /^Package babel Warning:/m;133const hasWarning = logText.match(babelWarningRegex);134if (hasWarning) {135const languageRegex = /^\(babel\).* language `(\S+)'.*$/m;136const languageMatch = logText.match(languageRegex);137if (languageMatch) {138return filterLang(languageMatch[1]);139}140}141142// Try an alternative way of parsing143const hyphenRulesRegex =144/Package babel Info: Hyphen rules for '(.*?)' set to \\l@nil/m;145const match = logText.match(hyphenRulesRegex);146if (match) {147const language = match[1];148if (language) {149return filterLang(language);150}151}152}153154// Parse a log file to find latex errors155const kErrorRegex = /^\!\s([\s\S]+)?Here is how much/m;156const kEmptyRegex = /(No pages of output)\./;157158export function findLatexError(159logText: string,160stderr?: string,161): string | undefined {162const errors: string[] = [];163164const match = logText.match(kErrorRegex);165if (match) {166const hint = suggestHint(logText, stderr);167if (hint) {168errors.push(`${match[1]}\n${hint}`);169} else {170errors.push(match[1]);171}172}173174if (errors.length === 0) {175const emptyMatch = logText.match(kEmptyRegex);176if (emptyMatch) {177errors.push(178`${emptyMatch[1]} - the document appears to have produced no output.`,179);180}181}182183return errors.join("\n");184}185186// Find the index error message187const kIndexErrorRegex = /^\s\s\s--\s(.*)/m;188export function findIndexError(logText: string): string | undefined {189const match = logText.match(kIndexErrorRegex);190if (match) {191return match[1];192} else {193return undefined;194}195}196197// Search the missing font log for fonts198function findMissingFonts(dir: string): string[] {199const missingFonts = [];200// Look in the missing font file for any missing fonts201const missFontLog = join(dir, kMissingFontLog);202if (existsSync(missFontLog)) {203const missFontLogText = Deno.readTextFileSync(missFontLog);204const fontSearchTerms = findInMissingFontLog(missFontLogText);205missingFonts.push(...fontSearchTerms);206}207return missingFonts;208}209210const formatFontFilter = (match: string, _text: string) => {211// Remove special prefix / suffix e.g. 'file:HaranoAjiMincho-Regular.otf:-kern;jfm=ujis'212// https://github.com/quarto-dev/quarto-cli/issues/12194213const base = basename(match).replace(/^.*?:|:.*$/g, "");214// return found file directly if it has an extension215return /[.]/.test(base) ? base : fontSearchTerm(base);216};217218const estoPdfFilter = (_match: string, _text: string) => {219return "epstopdf";220};221222const packageMatchers = [223// Fonts224{225regex: /.*! Font [^=]+=([^ ]+).+ not loadable.*/g,226filter: formatFontFilter,227},228{229regex: /.*! .*The font "([^"]+)" cannot be found.*/g,230filter: formatFontFilter,231},232{233regex: /.*!.+ error:.+\(file ([^)]+)\): .*/g,234filter: formatFontFilter,235},236{237regex: /.*Unable to find TFM file "([^"]+)".*/g,238filter: formatFontFilter,239},240{241regex: /.*\(fontspec\)\s+The font "([^"]+)" cannot be.*/g,242filter: formatFontFilter,243},244{245regex: /.*Package widetext error: Install the ([^ ]+) package.*/g,246filter: (match: string, _text: string) => {247return `${match}.sty`;248},249},250{ regex: /.* File `(.+eps-converted-to.pdf)'.*/g, filter: estoPdfFilter },251{ regex: /.*xdvipdfmx:fatal: pdf_ref_obj.*/g, filter: estoPdfFilter },252253{254regex: /.* (tikzlibrary[^ ]+?[.]code[.]tex).*/g,255filter: (match: string, text: string) => {256if (text.match(/! Package tikz Error:/)) {257return match;258} else {259return undefined;260}261},262},263{264regex: /module 'lua-uni-normalize' not found:/g,265filter: (_match: string, _text: string) => {266return "lua-uni-algos.lua";267},268},269{ regex: /.* Loading '([^']+)' aborted!.*/g },270{ regex: /.*! LaTeX Error: File `([^']+)' not found.*/g },271{ regex: /.* file ['`]?([^' ]+)'? not found.*/g },272{ regex: /.*the language definition file ([^\s]*).*/g },273{ regex: /.* \\(file ([^)]+)\\): cannot open .*/g },274{ regex: /.*file `([^']+)' .*is missing.*/g },275{ regex: /.*! CTeX fontset `([^']+)' is unavailable.*/g },276{ regex: /.*: ([^:]+): command not found.*/g },277{ regex: /.*! I can't find file `([^']+)'.*/g },278];279280function fontSearchTerm(font: string): string {281return `${font}(-(Bold|Italic|Regular).*)?[.](tfm|afm|mf|otf|ttf)`;282}283284function findMissingPackages(logFileText: string): string[] {285const toInstall: string[] = [];286287packageMatchers.forEach((packageMatcher) => {288packageMatcher.regex.lastIndex = 0;289let match = packageMatcher.regex.exec(logFileText);290while (match != null) {291const file = match[1];292// Apply the filter, if there is one293const filteredFile = packageMatcher.filter294? packageMatcher.filter(file, logFileText)295: file;296297// Capture any matches298if (filteredFile) {299toInstall.push(filteredFile);300}301302match = packageMatcher.regex.exec(logFileText);303}304packageMatcher.regex.lastIndex = 0;305});306307// dedulicated list of packages to attempt to install308return ld.uniq(toInstall);309}310311function findInMissingFontLog(missFontLogText: string): string[] {312const toInstall: string[] = [];313lines(missFontLogText).forEach((line) => {314// Trim the line315line = line.trim();316317// Extract the font from the end of the line318const fontMatch = line.match(/([^\s]*)$/);319if (fontMatch && fontMatch[1].trim() !== "") {320toInstall.push(fontMatch[1]);321}322323// Extract the font install command from the front of the line324// Also request that this be installed325const commandMatch = line.match(/^([^\s]*)/);326if (commandMatch && commandMatch[1].trim() !== "") {327toInstall.push(commandMatch[1]);328}329});330331// deduplicated list of fonts and font install commands332return ld.uniq(toInstall);333}334335const kUnicodePattern = {336regex: /\! Package inputenc Error: Unicode character/,337hint:338"Possible unsupported unicode character in this configuration. Perhaps try another LaTeX engine (e.g. XeLaTeX).",339};340341const kInlinePattern = {342regex: /Missing \$ inserted\./,343hint: "You may need to $ $ around an expression in this file.",344};345346const kGhostPattern = {347regex: /^\!\!\! Error: Cannot open Ghostscript for piped input/m,348hint:349"GhostScript is likely required to compile this document. Please be sure GhostScript (https://ghostscript.com) is installed and try again.",350};351352const kGhostCorruptPattern = {353regex: /^GPL Ghostscript .*: Can't find initialization file gs_init.ps/m,354hint:355"GhostScript is likely required to compile this document. Please be sure GhostScript (https://ghostscript.com) is installed and configured properly and try again.",356};357358const kLogOutputPatterns = [kUnicodePattern, kInlinePattern];359const kStdErrPatterns = [kGhostPattern, kGhostCorruptPattern];360361function suggestHint(362logText: string,363stderr?: string,364): string | undefined {365// Check stderr for hints366const stderrHint = kStdErrPatterns.find((errPattern) =>367stderr?.match(errPattern.regex)368);369370if (stderrHint) {371return stderrHint.hint;372} else {373// Check the log file for hints374const logHint = kLogOutputPatterns.find((logPattern) =>375logText.match(logPattern.regex)376);377if (logHint) {378return logHint.hint;379} else {380return undefined;381}382}383}384385386