Path: blob/main/src/command/render/latexmk/parse-error.ts
6428 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 PDF/UA accessibility warnings from tagpdf and DocumentMetadata110export interface PdfAccessibilityWarnings {111missingAltText: string[]; // filenames of images missing alt text112missingLanguage: boolean; // document language not set113otherWarnings: string[]; // other tagpdf warnings114}115116export function findPdfAccessibilityWarnings(117logText: string,118): PdfAccessibilityWarnings {119const result: PdfAccessibilityWarnings = {120missingAltText: [],121missingLanguage: false,122otherWarnings: [],123};124125// Match: Package tagpdf Warning: Alternative text for graphic is missing.126// (tagpdf) Using 'filename' instead.127const altTextRegex =128/Package tagpdf Warning: Alternative text for graphic is missing\.\s*\n\(tagpdf\)\s*Using ['`]([^'`]+)['`] instead\./g;129let match;130while ((match = altTextRegex.exec(logText)) !== null) {131result.missingAltText.push(match[1]);132}133134// Match: LaTeX DocumentMetadata Warning: The language has not been set in135if (136/LaTeX DocumentMetadata Warning: The language has not been set in/.test(137logText,138)139) {140result.missingLanguage = true;141}142143// Capture any other tagpdf warnings we haven't specifically handled144const otherTagpdfRegex = /Package tagpdf Warning: ([^\n]+)/g;145while ((match = otherTagpdfRegex.exec(logText)) !== null) {146const warning = match[1];147// Skip the alt text warning we already handle specifically148if (!warning.startsWith("Alternative text for graphic is missing")) {149result.otherWarnings.push(warning);150}151}152153return result;154}155156// Finds missing hyphenation files (these appear as warnings in the log file)157export function findMissingHyphenationFiles(logText: string) {158//ngerman gets special cased159const filterLang = (lang: string) => {160// It seems some languages have no hyphenation files, so we just filter them out161// e.g. `lang: zh` has no hyphenation files162// https://github.com/quarto-dev/quarto-cli/issues/10291163const noHyphen = ["chinese-hans", "chinese"];164if (noHyphen.includes(lang)) {165return;166}167168// NOTE Although the names of the corresponding lfd files match those in this list,169// there are some exceptions, particularly in German and Serbian. So, ngerman is170// called here german, which is the name in the CLDR and, actually, the most logical.171//172// See https://ctan.math.utah.edu/ctan/tex-archive/macros/latex/required/babel/base/babel.pdf173if (lang === "ngerman") {174return "hyphen-german";175}176return `hyphen-${lang.toLowerCase()}`;177};178179const babelWarningRegex = /^Package babel Warning:/m;180const hasWarning = logText.match(babelWarningRegex);181if (hasWarning) {182const languageRegex = /^\(babel\).* language [`'](\S+)[`'].*$/m;183const languageMatch = logText.match(languageRegex);184if (languageMatch) {185return filterLang(languageMatch[1]);186}187}188189// Try an alternative way of parsing190const hyphenRulesRegex =191/Package babel Info: Hyphen rules for '(.*?)' set to \\l@nil/m;192const match = logText.match(hyphenRulesRegex);193if (match) {194const language = match[1];195if (language) {196return filterLang(language);197}198}199}200201// Parse a log file to find latex errors202const kErrorRegex = /^\!\s([\s\S]+)?Here is how much/m;203const kEmptyRegex = /(No pages of output)\./;204205export function findLatexError(206logText: string,207stderr?: string,208): string | undefined {209const errors: string[] = [];210211const match = logText.match(kErrorRegex);212if (match) {213const hint = suggestHint(logText, stderr);214if (hint) {215errors.push(`${match[1]}\n${hint}`);216} else {217errors.push(match[1]);218}219}220221if (errors.length === 0) {222const emptyMatch = logText.match(kEmptyRegex);223if (emptyMatch) {224errors.push(225`${emptyMatch[1]} - the document appears to have produced no output.`,226);227}228}229230return errors.join("\n");231}232233// Find the index error message234const kIndexErrorRegex = /^\s\s\s--\s(.*)/m;235export function findIndexError(logText: string): string | undefined {236const match = logText.match(kIndexErrorRegex);237if (match) {238return match[1];239} else {240return undefined;241}242}243244// Search the missing font log for fonts245function findMissingFonts(dir: string): string[] {246const missingFonts = [];247// Look in the missing font file for any missing fonts248const missFontLog = join(dir, kMissingFontLog);249if (existsSync(missFontLog)) {250const missFontLogText = Deno.readTextFileSync(missFontLog);251const fontSearchTerms = findInMissingFontLog(missFontLogText);252missingFonts.push(...fontSearchTerms);253}254return missingFonts;255}256257const formatFontFilter = (match: string, _text: string) => {258// Remove special prefix / suffix e.g. 'file:HaranoAjiMincho-Regular.otf:-kern;jfm=ujis'259// https://github.com/quarto-dev/quarto-cli/issues/12194260const base = basename(match).replace(/^.*?:|:.*$/g, "");261// return found file directly if it has an extension262return /[.]/.test(base) ? base : fontSearchTerm(base);263};264265const estoPdfFilter = (_match: string, _text: string) => {266return "epstopdf";267};268269const packageMatchers = [270// Fonts271{272regex: /.*! Font [^=]+=([^ ]+).+ not loadable.*/g,273filter: formatFontFilter,274},275{276regex: /.*! .*The font "([^"]+)" cannot be found.*/g,277filter: formatFontFilter,278},279{280regex: /.*!.+ error:.+\(file ([^)]+)\): .*/g,281filter: formatFontFilter,282},283{284regex: /.*Unable to find TFM file "([^"]+)".*/g,285filter: formatFontFilter,286},287{288regex: /.*\(fontspec\)\s+The font "([^"]+)" cannot be.*/g,289filter: formatFontFilter,290},291{292regex: /.*Package widetext error: Install the ([^ ]+) package.*/g,293filter: (match: string, _text: string) => {294return `${match}.sty`;295},296},297{ regex: /.* File [`'](.+eps-converted-to.pdf)'.*/g, filter: estoPdfFilter },298{ regex: /.*xdvipdfmx:fatal: pdf_ref_obj.*/g, filter: estoPdfFilter },299300{301regex: /.* (tikzlibrary[^ ]+?[.]code[.]tex).*/g,302filter: (match: string, text: string) => {303if (text.match(/! Package tikz Error:/)) {304return match;305} else {306return undefined;307}308},309},310{311regex: /module 'lua-uni-normalize' not found:/g,312filter: (_match: string, _text: string) => {313return "lua-uni-algos.lua";314},315},316{317regex: /.* Package pdfx Error: No color profile ([^\s]*).*/g,318filter: (_match: string, _text: string) => {319return "colorprofiles.sty";320},321},322{323regex: /.*No support files for \\DocumentMetadata found.*/g,324filter: (_match: string, _text: string) => {325return "latex-lab";326},327},328{329// PDF/A requires embedded color profiles - pdfmanagement-testphase needs colorprofiles330regex: /.*\(pdf backend\): cannot open file for embedding.*/g,331filter: (_match: string, _text: string) => {332return "colorprofiles";333},334},335{336regex: /.*No file ([^`'. ]+[.]fd)[.].*/g,337filter: (match: string, _text: string) => {338return match.toLowerCase();339},340},341{ regex: /.* Loading '([^']+)' aborted!.*/g },342{ regex: /.*! LaTeX Error: File [`']([^']+)' not found.*/g },343{ regex: /.* [fF]ile ['`]?([^' ]+)'? not found.*/g },344{ regex: /.*the language definition file ([^\s]*).*/g },345{346regex: /.*! Package babel Error: Unknown option [`']([^'`]+)'[.].*/g,347filter: (match: string, _text: string) => {348return `${match}.ldf`;349},350},351{ regex: /.* \\(file ([^)]+)\\): cannot open .*/g },352{ regex: /.*file [`']([^']+)' .*is missing.*/g },353{ regex: /.*! CTeX fontset [`']([^']+)' is unavailable.*/g },354{ regex: /.*: ([^:]+): command not found.*/g },355{ regex: /.*! I can't find file [`']([^']+)'.*/g },356];357358function fontSearchTerm(font: string): string {359const fontPattern = font.replace(/\s+/g, "\\s*");360return `${fontPattern}(-(Bold|Italic|Regular).*)?[.](tfm|afm|mf|otf|ttf)`;361}362363function findMissingPackages(logFileText: string): string[] {364const toInstall: string[] = [];365366packageMatchers.forEach((packageMatcher) => {367packageMatcher.regex.lastIndex = 0;368let match = packageMatcher.regex.exec(logFileText);369while (match != null) {370const file = match[1];371// Apply the filter, if there is one372const filteredFile = packageMatcher.filter373? packageMatcher.filter(file, logFileText)374: file;375376// Capture any matches377if (filteredFile) {378toInstall.push(filteredFile);379}380381match = packageMatcher.regex.exec(logFileText);382}383packageMatcher.regex.lastIndex = 0;384});385386// dedulicated list of packages to attempt to install387return ld.uniq(toInstall);388}389390function findInMissingFontLog(missFontLogText: string): string[] {391const toInstall: string[] = [];392lines(missFontLogText).forEach((line) => {393// Trim the line394line = line.trim();395396// Extract the font from the end of the line397const fontMatch = line.match(/([^\s]*)$/);398if (fontMatch && fontMatch[1].trim() !== "") {399toInstall.push(fontMatch[1]);400}401402// Extract the font install command from the front of the line403// Also request that this be installed404const commandMatch = line.match(/^([^\s]*)/);405if (commandMatch && commandMatch[1].trim() !== "") {406toInstall.push(commandMatch[1]);407}408});409410// deduplicated list of fonts and font install commands411return ld.uniq(toInstall);412}413414const kUnicodePattern = {415regex: /\! Package inputenc Error: Unicode character/,416hint:417"Possible unsupported unicode character in this configuration. Perhaps try another LaTeX engine (e.g. XeLaTeX).",418};419420const kInlinePattern = {421regex: /Missing \$ inserted\./,422hint: "You may need to $ $ around an expression in this file.",423};424425const kGhostPattern = {426regex: /^\!\!\! Error: Cannot open Ghostscript for piped input/m,427hint:428"GhostScript is likely required to compile this document. Please be sure GhostScript (https://ghostscript.com) is installed and try again.",429};430431const kGhostCorruptPattern = {432regex: /^GPL Ghostscript .*: Can't find initialization file gs_init.ps/m,433hint:434"GhostScript is likely required to compile this document. Please be sure GhostScript (https://ghostscript.com) is installed and configured properly and try again.",435};436437const kLogOutputPatterns = [kUnicodePattern, kInlinePattern];438const kStdErrPatterns = [kGhostPattern, kGhostCorruptPattern];439440function suggestHint(441logText: string,442stderr?: string,443): string | undefined {444// Check stderr for hints445const stderrHint = kStdErrPatterns.find((errPattern) =>446stderr?.match(errPattern.regex)447);448449if (stderrHint) {450return stderrHint.hint;451} else {452// Check the log file for hints453const logHint = kLogOutputPatterns.find((logPattern) =>454logText.match(logPattern.regex)455);456if (logHint) {457return logHint.hint;458} else {459return undefined;460}461}462}463464465