Path: blob/main/src/command/render/latexmk/parse-error.ts
12926 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.127// Note: tagpdf wraps long filenames across multiple (tagpdf) continuation128// lines, so we allow optional line breaks with (tagpdf) prefixes.129const altTextRegex =130/Package tagpdf Warning: Alternative text for graphic is missing\.\s*\n\(tagpdf\)\s*Using ['`]([^'`]+)['`]\s*(?:\n\(tagpdf\)\s*)?instead\./g;131let match;132while ((match = altTextRegex.exec(logText)) !== null) {133result.missingAltText.push(match[1]);134}135136// Match: LaTeX DocumentMetadata Warning: The language has not been set in137if (138/LaTeX DocumentMetadata Warning: The language has not been set in/.test(139logText,140)141) {142result.missingLanguage = true;143}144145// Capture any other tagpdf warnings we haven't specifically handled146const otherTagpdfRegex = /Package tagpdf Warning: ([^\n]+)/g;147while ((match = otherTagpdfRegex.exec(logText)) !== null) {148const warning = match[1];149// Skip the alt text warning we already handle specifically150if (!warning.startsWith("Alternative text for graphic is missing")) {151result.otherWarnings.push(warning);152}153}154155return result;156}157158// Finds missing hyphenation files (these appear as warnings in the log file)159export function findMissingHyphenationFiles(logText: string) {160//ngerman gets special cased161const filterLang = (lang: string) => {162// It seems some languages have no hyphenation files, so we just filter them out163// e.g. `lang: zh` has no hyphenation files164// https://github.com/quarto-dev/quarto-cli/issues/10291165const noHyphen = ["chinese-hans", "chinese"];166if (noHyphen.includes(lang)) {167return;168}169170// NOTE Although the names of the corresponding lfd files match those in this list,171// there are some exceptions, particularly in German and Serbian. So, ngerman is172// called here german, which is the name in the CLDR and, actually, the most logical.173//174// See https://ctan.math.utah.edu/ctan/tex-archive/macros/latex/required/babel/base/babel.pdf175if (lang === "ngerman") {176return "hyphen-german";177}178return `hyphen-${lang.toLowerCase()}`;179};180181const babelWarningRegex = /^Package babel Warning:/m;182const hasWarning = logText.match(babelWarningRegex);183if (hasWarning) {184const languageRegex = /^\(babel\).* language [`'](\S+)[`'].*$/m;185const languageMatch = logText.match(languageRegex);186if (languageMatch) {187return filterLang(languageMatch[1]);188}189}190191// Try an alternative way of parsing192const hyphenRulesRegex =193/Package babel Info: Hyphen rules for '(.*?)' set to \\l@nil/m;194const match = logText.match(hyphenRulesRegex);195if (match) {196const language = match[1];197if (language) {198return filterLang(language);199}200}201}202203// Parse a log file to find latex errors204const kErrorRegex = /^\!\s([\s\S]+)?Here is how much/m;205const kEmptyRegex = /(No pages of output)\./;206207export function findLatexError(208logText: string,209stderr?: string,210): string | undefined {211const errors: string[] = [];212213const match = logText.match(kErrorRegex);214if (match) {215const hint = suggestHint(logText, stderr);216if (hint) {217errors.push(`${match[1]}\n${hint}`);218} else {219errors.push(match[1]);220}221}222223if (errors.length === 0) {224const emptyMatch = logText.match(kEmptyRegex);225if (emptyMatch) {226errors.push(227`${emptyMatch[1]} - the document appears to have produced no output.`,228);229}230}231232return errors.join("\n");233}234235// Find the index error message236const kIndexErrorRegex = /^\s\s\s--\s(.*)/m;237export function findIndexError(logText: string): string | undefined {238const match = logText.match(kIndexErrorRegex);239if (match) {240return match[1];241} else {242return undefined;243}244}245246// Search the missing font log for fonts247function findMissingFonts(dir: string): string[] {248const missingFonts = [];249// Look in the missing font file for any missing fonts250const missFontLog = join(dir, kMissingFontLog);251if (existsSync(missFontLog)) {252const missFontLogText = Deno.readTextFileSync(missFontLog);253const fontSearchTerms = findInMissingFontLog(missFontLogText);254missingFonts.push(...fontSearchTerms);255}256return missingFonts;257}258259const formatFontFilter = (match: string, _text: string) => {260// Remove special prefix / suffix e.g. 'file:HaranoAjiMincho-Regular.otf:-kern;jfm=ujis'261// https://github.com/quarto-dev/quarto-cli/issues/12194262const base = basename(match).replace(/^.*?:|:.*$/g, "");263// return found file directly if it has an extension264return /[.]/.test(base) ? base : fontSearchTerm(base);265};266267const estoPdfFilter = (_match: string, _text: string) => {268return "epstopdf";269};270271const packageMatchers = [272// Fonts273{274regex: /.*! Font [^=]+=([^ ]+).+ not loadable.*/g,275filter: formatFontFilter,276},277{278regex: /.*! .*The font "([^"]+)" cannot be found.*/g,279filter: formatFontFilter,280},281{282regex: /.*!.+ error:.+\(file ([^)]+)\): .*/g,283filter: formatFontFilter,284},285{286regex: /.*Unable to find TFM file "([^"]+)".*/g,287filter: formatFontFilter,288},289{290regex: /.*\(fontspec\)\s+The font "([^"]+)" cannot be.*/g,291filter: formatFontFilter,292},293{294regex: /.*Package widetext error: Install the ([^ ]+) package.*/g,295filter: (match: string, _text: string) => {296return `${match}.sty`;297},298},299{ regex: /.* File [`'](.+eps-converted-to.pdf)'.*/g, filter: estoPdfFilter },300{ regex: /.*xdvipdfmx:fatal: pdf_ref_obj.*/g, filter: estoPdfFilter },301302{303regex: /.* (tikzlibrary[^ ]+?[.]code[.]tex).*/g,304filter: (match: string, text: string) => {305if (text.match(/! Package tikz Error:/)) {306return match;307} else {308return undefined;309}310},311},312{313regex: /module 'lua-uni-normalize' not found:/g,314filter: (_match: string, _text: string) => {315return "lua-uni-algos.lua";316},317},318{319regex: /.* Package pdfx Error: No color profile ([^\s]*).*/g,320filter: (_match: string, _text: string) => {321return "colorprofiles.sty";322},323},324{325regex: /.*No support files for \\DocumentMetadata found.*/g,326filter: (_match: string, _text: string) => {327return "latex-lab";328},329},330{331// PDF/A requires embedded color profiles - pdfmanagement-testphase needs colorprofiles332regex: /.*\(pdf backend\): cannot open file for embedding.*/g,333filter: (_match: string, _text: string) => {334return "colorprofiles";335},336},337{338regex: /.*No file ([^`'. ]+[.]fd)[.].*/g,339filter: (match: string, _text: string) => {340return match.toLowerCase();341},342},343{ regex: /.* Loading '([^']+)' aborted!.*/g },344{ regex: /.*! LaTeX Error: File [`']([^']+)' not found.*/g },345{ regex: /.* [fF]ile ['`]?([^' ]+)'? not found.*/g },346{ regex: /.*the language definition file ([^\s]*).*/g },347{348regex: /.*! Package babel Error: Unknown option [`']([^'`]+)'[.].*/g,349filter: (match: string, _text: string) => {350return `${match}.ldf`;351},352},353{ regex: /.* \\(file ([^)]+)\\): cannot open .*/g },354{ regex: /.*file [`']([^']+)' .*is missing.*/g },355{ regex: /.*! CTeX fontset [`']([^']+)' is unavailable.*/g },356{ regex: /.*: ([^:]+): command not found.*/g },357{ regex: /.*! I can't find file [`']([^']+)'.*/g },358];359360function fontSearchTerm(font: string): string {361const fontPattern = font.replace(/\s+/g, "\\s*");362return `${fontPattern}(-(Bold|Italic|Regular).*)?[.](tfm|afm|mf|otf|ttf)`;363}364365function findMissingPackages(logFileText: string): string[] {366const toInstall: string[] = [];367368packageMatchers.forEach((packageMatcher) => {369packageMatcher.regex.lastIndex = 0;370let match = packageMatcher.regex.exec(logFileText);371while (match != null) {372const file = match[1];373// Apply the filter, if there is one374const filteredFile = packageMatcher.filter375? packageMatcher.filter(file, logFileText)376: file;377378// Capture any matches379if (filteredFile) {380toInstall.push(filteredFile);381}382383match = packageMatcher.regex.exec(logFileText);384}385packageMatcher.regex.lastIndex = 0;386});387388// dedulicated list of packages to attempt to install389return ld.uniq(toInstall);390}391392function findInMissingFontLog(missFontLogText: string): string[] {393const toInstall: string[] = [];394lines(missFontLogText).forEach((line) => {395// Trim the line396line = line.trim();397398// Extract the font from the end of the line399const fontMatch = line.match(/([^\s]*)$/);400if (fontMatch && fontMatch[1].trim() !== "") {401toInstall.push(fontMatch[1]);402}403404// Extract the font install command from the front of the line405// Also request that this be installed406const commandMatch = line.match(/^([^\s]*)/);407if (commandMatch && commandMatch[1].trim() !== "") {408toInstall.push(commandMatch[1]);409}410});411412// deduplicated list of fonts and font install commands413return ld.uniq(toInstall);414}415416const kUnicodePattern = {417regex: /\! Package inputenc Error: Unicode character/,418hint:419"Possible unsupported unicode character in this configuration. Perhaps try another LaTeX engine (e.g. XeLaTeX).",420};421422const kInlinePattern = {423regex: /Missing \$ inserted\./,424hint: "You may need to $ $ around an expression in this file.",425};426427const kGhostPattern = {428regex: /^\!\!\! Error: Cannot open Ghostscript for piped input/m,429hint:430"GhostScript is likely required to compile this document. Please be sure GhostScript (https://ghostscript.com) is installed and try again.",431};432433const kGhostCorruptPattern = {434regex: /^GPL Ghostscript .*: Can't find initialization file gs_init.ps/m,435hint:436"GhostScript is likely required to compile this document. Please be sure GhostScript (https://ghostscript.com) is installed and configured properly and try again.",437};438439const kLogOutputPatterns = [kUnicodePattern, kInlinePattern];440const kStdErrPatterns = [kGhostPattern, kGhostCorruptPattern];441442function suggestHint(443logText: string,444stderr?: string,445): string | undefined {446// Check stderr for hints447const stderrHint = kStdErrPatterns.find((errPattern) =>448stderr?.match(errPattern.regex)449);450451if (stderrHint) {452return stderrHint.hint;453} else {454// Check the log file for hints455const logHint = kLogOutputPatterns.find((logPattern) =>456logText.match(logPattern.regex)457);458if (logHint) {459return logHint.hint;460} else {461return undefined;462}463}464}465466467