#!/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 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" @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 @unique class FileType(IntEnum): """ Type of file in the making process. """ aux = 1 bcf = 2 bbc = 3 bbl = 4 bbx = 5 bib = 6 bst = 7 cbx = 8 cls = 9 dvi = 10 glo = 11 gls = 12 idx = 13 ind = 14 log = 15 pdf = 16 ps = 17 sty = 18 tex = 19 xdv = 20 xdvi = 21 def is_source_type(self) -> bool: """ Replies if the given type is assumed to be a source type. A source type is usually not the result of a tool usage, but it might contain other source types by inclusion. :return: Tue if the type corresponds to a source type; Otherwise, it is a type of file that is the result of a computation. :rtype: bool """ return self == FileType.tex or self == FileType.bib or self == FileType.sty 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.ps, FileType.dvi, FileType.xdvi, FileType.xdv] @staticmethod def output_extensions() -> list[str]: """ Replies all the file types extensions that are related to the output result of TeX tools. :return: the list of file extensions. :rtype: list[str] """ exts = list() for ext in FileType.output_types(): exts.extend(ext.extensions()) return exts @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.bcf, 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 def ensure_extension(self, filename : str) -> str: """ Ensure the given filename has the extension of this file type. Any previous file extension is removed from the given filename. If multiple file extensions are specified in the given filename, only the last one is removed. :param filename: the filename to check. :type filename: str :return: the filename with the correct file extension :rtype: str """ return genutils.ensure_filename_extension(filename, self.extension()) def add_extension(self, filename : str) -> str: """ Ensure the given filename has the extension of this file type. Any previous file extension is NOT removed from the given filename. :param filename: the filename to check. :type filename: str :return: the filename with the correct file extension :rtype: str """ return filename + self.extension() 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 = FileType.tex.ensure_extension(candidate_name) 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.basename_with_path(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.basename_with_path(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