#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (C) 1998-2026 Stephane Galland # # This program is free library; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or any later version. # # This library is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; see the file COPYING. If not, # write to the Free Software Foundation, Inc., 59 Temple Place - Suite # 330, Boston, MA 02111-1307, USA. """ General utilities for TeX. """ import os import re from enum import IntEnum, unique from pathlib import Path from typing import Callable from dataclasses import dataclass from autolatex2.utils.i18n import T import autolatex2.utils.utilfunctions as genutils EXTENDED_TEX_CODE_FILENAME_POSTFIX = "_autolatex_autogenerated" @unique class TeXWarnings(IntEnum): """ List of LaTeX warnings supported by TeX maker """ done = 0 undefined_reference = 1 undefined_citation = 2 multiple_definition = 3 other_warning = 4 @unique class FileType(IntEnum): """ Type of file in the making process. """ aux = 1 bbc = 2 bbl = 3 bbx = 4 bib = 5 bst = 6 cbx = 7 cls = 8 glo = 9 gls = 10 idx = 11 ind = 12 pdf = 13 ps = 14 sty = 15 tex = 16 def extension(self) -> str: """ Replies the major filename extension for the current file type. :return: The major filename extension prefixed with '.'. :rtype: str """ return '.' + self.name def extensions(self) -> list[str]: """ Replies the supported filename extensions for current file type. :return: The list of the filename extensions. :rtype: list[str] """ if self == FileType.tex: return ['.tex', '.latex', '.ltx'] else: return [ '.' + self.name ] def is_file(self, filename : str) -> bool: """ Replies the given filename has the filename extensions of the current file type. :rtype: bool """ if filename: ext = os.path.splitext(filename)[-1] if ext: return ext.lower() in self.extensions() return False @staticmethod def output_types() -> list['FileType']: """ Replies all the file types that are related to the output result of TeX tools. :return: the list of file types. :rtype: list[FileType] """ return [FileType.pdf, FileType.sty, FileType.ps] @staticmethod def tex_types() -> list['FileType']: """ Replies all the file types that are related to the TeX code. :return: the list of file types. :rtype: list[FileType] """ return [FileType.tex, FileType.cls, FileType.sty] @staticmethod def tex_extensions() -> list[str]: """ Replies the supported filename extensions for TeX files. :return: The list of the filename extensions. :rtype: list[str] """ exts = list() for ext in FileType.tex_types(): exts.extend(ext.extensions()) return exts @staticmethod def is_tex_extension(ext : str) -> bool: """ Test if a given string is a standard extension for TeX document. The ext must start with a '.'. The test is case-insensitive. :param ext: The extension to test. :type ext: str :return: True if the extension is for a TeX/LaTeX file; otherwise False. :rtype: bool """ ext = ext.lower() for file_type in FileType.tex_types(): if ext in file_type.extensions(): return True return False @staticmethod def is_tex_document(filename : str) -> bool: """ Replies if the given filename is a TeX document. The test is case-insensitive. :param filename: The filename to test. :type filename: str :return: True if the extension is for a TeX/LaTeX file; otherwise False. :rtype: bool """ if filename: ext = os.path.splitext(filename)[-1] return FileType.is_tex_extension(ext) return False @staticmethod def bibliography_types() -> list['FileType']: """ Replies all the file types that are related to the bibliography. :return: the list of file types. :rtype: list[FileType] """ return [FileType.bib, FileType.bbl, FileType.bst, FileType.bbc, FileType.bbx, FileType.cbx] @staticmethod def bibliography_extensions() -> list[str]: """ Replies the supported filename extensions for bibliography files. :return: The list of the filename extensions. :rtype: list[str] """ exts = list() for ext in FileType.bibliography_types(): exts.extend(ext.extensions()) return exts @staticmethod def glossary_types() -> list['FileType']: """ Replies all the file types that are related to the glossary. :return: the list of file types. :rtype: list[FileType] """ return [FileType.glo, FileType.gls] @staticmethod def glossary_extensions() -> list[str]: """ Replies the supported filename extensions for glossary files. :return: The list of the filename extensions. :rtype: list[str] """ exts = list() for ext in FileType.glossary_types(): exts.extend(ext.extensions()) return exts @staticmethod def index_types() -> list['FileType']: """ Replies all the file types that are related to the index. :return: the list of file types. :rtype: list[FileType] """ return [FileType.idx, FileType.ind] @staticmethod def index_extensions() -> list[str]: """ Replies the supported filename extensions for index files. :return: The list of the filename extensions. :rtype: list[str] """ exts = list() for ext in FileType.index_types(): exts.extend(ext.extensions()) return exts @dataclass class TeXMacroParameter: """ Definition of a parameter for a TeX macro. """ text : str index : int = -1 optional : bool = False evaluable : bool = True macro_name : bool = False def extract_tex_warning_from_line(line : str, warnings : set[TeXWarnings]) -> bool: """ Test if the given line contains a typical TeX warning message. This function stores the discovered warning into this maker. True is replied if a new run of the TeX compiler is requested within the warning message. False is replied if the TeX compiler should not be re-run. :param line: The line of text to parse. :type line: str :param warnings: The list of warnings to fill. :type warnings: set[TeXWarnings] :rtype: bool """ line = re.sub(r"[^a-zA-Z:]+", '', line) if re.search(r'Warning.*Reruntoget(?:crossreferencesright|outlinesright)', line, re.I): return True elif re.search(r'Warning:Therewereundefinedreferences', line, re.I): warnings.add(TeXWarnings.undefined_reference) elif re.search(r'Warning:Citation.+undefined', line, re.I): warnings.add(TeXWarnings.undefined_citation) elif re.search(r'Warning:Thereweremultiplydefinedlabels', line, re.I): warnings.add(TeXWarnings.multiple_definition) elif re.search(r'(?:\s|^)Warning', line, re.I | re.M): warnings.add(TeXWarnings.other_warning) return False def __parse_tex_fatal_error_message(error : str) -> str: err0 = re.sub(r'!!!!\[BeginWarning].*?!!!!\[EndWarning].*', '', error, re.S | re.I) m = re.match(r'^.*?:[0-9]+:\s*(.*?)\s*$', err0, re.S) if m: return m.group(1).strip() return '' def parse_tex_log_file(log_filename : str) -> tuple[str,list[str]]: """ Parse the given file as a TeX log file and extract any relevant information. All the block of warnings or errors are detected until a fatal error or the end of the file is reached. This function replies a tuple in which the first member is the fatal error, if one, and the second member is the list of log blocks. :param log_filename: The filename of the log file. :type log_filename: str :return: A tuple with the fatal error, followed by a list of log blocks. :rtype: tuple[str,list[str]] """ blocks = list() fatal_error = '' with open(log_filename, "r") as f: line = f.readline() current_block = '' while line is not None and line != '': line = line.strip() if line is None or line == '': if current_block: blocks.append(current_block) if not fatal_error: fatal_error = __parse_tex_fatal_error_message(current_block) current_block = '' else: current_block = current_block + line line = f.readline() if current_block: blocks.append(current_block) if not fatal_error: fatal_error = __parse_tex_fatal_error_message(current_block) return fatal_error, blocks def find_aux_files(tex_file : str, selector : Callable[[str], bool] | None = None) -> list[str]: """ Recursively find all aux files that are located in the same folder as the given TeX file, or in one of its subfolders. For subfolders, it is mandatory that a TeX file with the name basename as the aux file exists. In the folder of the provided TeX file, all the aux files are considered. :param tex_file: The filename of the TeX file. :type tex_file: str :param selector: A lambda function that permits is used as a filtering function for the auxiliary files. The lambda takes one formal argument that is the auxiliary file's name. It replies True if the auxiliary file is accepted; Otherwise False. :type selector: Callable[[str], bool] | None :return: the list of the aux files that are validated the constraints and the given lambda selector. :rtype: list[str] """ folder_name = os.path.normpath(os.path.dirname(tex_file)) directory = Path(folder_name) if not directory.exists(): raise FileNotFoundError(T("Directory does not exist: %s") % folder_name) if not directory.is_dir(): raise NotADirectoryError(T("Path is not a directory: %s") % folder_name) # Recursively find all .aux files aux_files : list[str] = list() for candidate in directory.rglob("*.aux"): aux_dir = os.path.normpath(os.path.dirname(candidate)) candidate_name = str(candidate) if aux_dir == folder_name: if selector is None or selector(candidate_name): aux_files.append(candidate_name) else: additional_tex_file = genutils.basename2(candidate_name, *FileType.aux.extensions()) + '.tex' if os.path.isfile(additional_tex_file) and (selector is None or selector(candidate_name)): aux_files.append(candidate_name) return aux_files def create_extended_tex_filename(filename : str) -> str: """ Replies the filename of the TeX file when it is extending with code dedicated to the extended warning support. :param filename: the original filename. :type filename: str :return: the filename for the extended TeX code. :rtype: str """ ext = genutils.get_filename_extension_from(filename, *FileType.tex_extensions()) if ext is not None: new_basename = genutils.basename2(filename, ext) else: new_basename = filename new_basename += EXTENDED_TEX_CODE_FILENAME_POSTFIX if ext is not None: new_basename += ext return new_basename def get_original_tex_filename(filename : str) -> str: """ Replies the filename of the TeX file when it is extending with specific code for supporting extended warnings. :param filename: the original filename. :type filename: str :return: the filename for the extended TeX code. :rtype: str """ ext = genutils.get_filename_extension_from(filename, *FileType.tex_extensions()) if ext is not None: new_basename = genutils.basename2(filename, ext) else: new_basename = filename if new_basename.endswith(EXTENDED_TEX_CODE_FILENAME_POSTFIX): new_basename = new_basename[0:-len(EXTENDED_TEX_CODE_FILENAME_POSTFIX)] if ext is not None: new_basename += ext return new_basename