#!/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. """ Reader of the program configuration. """ import logging import configparser import os import re from typing import Any import autolatex2.utils.utilfunctions as genutils from autolatex2.config.configobj import Config from autolatex2.config.translator import TranslatorLevel from autolatex2.utils.extlogging import ensure_autolatex_logging_levels from autolatex2.utils.i18n import T class OldStyleConfigReader: """ Reader of the program configuration that is written with the old-style format (ini file). """ def __init__(self): self._base_dir : str = os.getcwd() ensure_autolatex_logging_levels() def _read_viewer(self, content : Any, config : Config): """ Read the configuration section [viewer]. :param content: the configuration file content. :type content: dict :param config: the configuration object to fill up. :type config: Config """ config.view.view = OldStyleConfigReader.to_bool(self._ensure_ascendent_compatibility(content.get('view')), config.view.view) config.view.viewer_cli = self._ensure_ascendent_compatibility(content.get('viewer')) or config.view.viewer_cli def _read_scm(self, content : Any, config : Config): """ Read the configuration section [scm]. :param content: the configuration file content. :type content: dict :param config: the configuration object to fill up. :type config: Config """ config.scm.commit_cli = self._ensure_ascendent_compatibility(content.get('scm commit')) or config.scm.commit_cli config.scm.update_cli = self._ensure_ascendent_compatibility(content.get('scm update')) or config.scm.commit_cli def _read_clean(self, content : Any, config : Config): """ Read the configuration section [clean]. :param content: the configuration file content. :type content: dict :param config: the configuration object to fill up. :type config: Config """ for p in OldStyleConfigReader.to_path_list(self._ensure_ascendent_compatibility(content.get('files to clean'))): config.clean.add_clean_file(p) for p in OldStyleConfigReader.to_path_list(self._ensure_ascendent_compatibility(content.get('files to disintegrate'))): config.clean.add_cleanall_file(p) def _read_generation_prefix(self, content : Any, config : Config): """ Read the path definition in the configuration section [generation]. :param content: the configuration file content. :type content: dict :param config: the configuration object to fill up. :type config: Config """ main_name = self._ensure_ascendent_compatibility(content.get('main file')) if main_name: main_name = genutils.abs_path(main_name, self._base_dir) config.document_directory = os.path.dirname(main_name) config.document_filename = os.path.basename(main_name) def _read_generation(self, content : Any, config : Config): """ Read the configuration section [generation], except the ones "_read_generation_prefix". :param content: the configuration file content. :type content: dict :param config: the configuration object to fill up. :type config: Config """ config.generation.include_extra_macros = OldStyleConfigReader.to_bool(self._ensure_ascendent_compatibility(content.get('include extra macros')), config.generation.include_extra_macros) for p in OldStyleConfigReader.to_path_list(self._ensure_ascendent_compatibility(content.get('image directory'))): config.translators.add_image_path(self.to_path(p)) config.translators.is_translator_enable = OldStyleConfigReader.to_bool(self._ensure_ascendent_compatibility(content.get('generate images')), config.translators.is_translator_enable) generation_type = OldStyleConfigReader.to_kw(self._ensure_ascendent_compatibility(content.get('generation type')), 'pdf' if config.generation.pdf_mode else 'ps') if generation_type == 'dvi' or generation_type == 'ps': config.generation.pdf_mode = False else: config.generation.pdf_mode = True tex_compiler = OldStyleConfigReader.to_kw(self._ensure_ascendent_compatibility(content.get('tex compiler')), config.generation.latex_compiler) if tex_compiler != 'latex' and tex_compiler != 'xelatex' and tex_compiler != 'lualatex': tex_compiler = 'pdflatex' config.generation.latex_compiler = tex_compiler config.generation.synctex = OldStyleConfigReader.to_bool(self._ensure_ascendent_compatibility(content.get('synctex')), config.generation.synctex) for p in OldStyleConfigReader.to_path_list(self._ensure_ascendent_compatibility(content.get('translator include path'))): config.translators.add_include_path(self.to_path(p)) cmd = self._ensure_ascendent_compatibility(content.get('latex_cmd')) if cmd: config.generation.latex_cli = cmd cmd = self._ensure_ascendent_compatibility(content.get('bibtex_cmd')) if cmd: config.generation.bibtex_cli = cmd cmd = self._ensure_ascendent_compatibility(content.get('biber_cmd')) if cmd: config.generation.biber_cli = cmd cmd = self._ensure_ascendent_compatibility(content.get('makeglossaries_cmd')) if cmd: config.generation.makeglossary_cli = cmd cmd = self._ensure_ascendent_compatibility(content.get('makeindex_cmd')) if cmd: config.generation.makeindex_cli = cmd cmd = self._ensure_ascendent_compatibility(content.get('dvi2ps_cmd')) if cmd: config.generation.dvips_cli = cmd flags = self._ensure_ascendent_compatibility(content.get('latex_flags')) if flags: config.generation.latex_flags = flags flags = self._ensure_ascendent_compatibility(content.get('bibtex_flags')) if flags: config.generation.bibtex_flags = flags flags = self._ensure_ascendent_compatibility(content.get('biber_flags')) if flags: config.generation.biber_flags = flags flags = self._ensure_ascendent_compatibility(content.get('makeglossaries_flags')) if flags: config.generation.makeglossary_flags = flags flags = self._ensure_ascendent_compatibility(content.get('makeindex_flags')) if flags: config.generation.makeindex_flags = flags flags = self._ensure_ascendent_compatibility(content.get('dvi2ps_flags')) if flags: config.generation.dvips_flags = flags make_index_style = self.to_list(self._ensure_ascendent_compatibility(content.get('makeindex style'))) config.generation.makeindex_style_filename = None if not make_index_style: make_index_style = ['@detect@system'] for key in make_index_style: kw = OldStyleConfigReader.to_kw(key, '@detect@system') result = None if kw == '@detect': result = self.to_path(self.__detect_ist_file(config)) elif kw == '@system': result = self.to_path(config.get_system_ist_file()) elif kw == '@none': config.generation.makeindex_style_filename = None break elif kw == '@detect@system': ist_file = self.__detect_ist_file(config) if ist_file: result = self.to_path(ist_file) else: result = self.to_path(config.get_system_ist_file()) if result: config.generation.makeindex_style_filename = result break def _read_translator(self, translator_name : str, translator_level : TranslatorLevel, content : Any, config : Config): """ Read the configuration section []. :param translator_name: The name of the translator. :type translator_name: str :param translator_level: The level for the translator. :type translator_level: int :param content: the configuration file content. :type content: dict :param config: the configuration object to fill up. :type config: Config """ raw_value = self._ensure_ascendent_compatibility(content.get('include module')) default_value = config.translators.included(translator_name, translator_level) new_value = OldStyleConfigReader.to_bool(raw_value, default_value) config.translators.set_included(translator_name, translator_level, new_value) raw_files = self._ensure_ascendent_compatibility(content.get('files to convert')) path_list = OldStyleConfigReader.to_path_list(raw_files) for path_element in path_list: formatted_path = self.to_path(path_element) config.translators.add_image_to_convert(formatted_path) # noinspection PyMethodMayBeStatic def _is_translator_section(self, section_name : str) -> bool: """ Replies if the given section name is for a translator or not. :param section_name: Name of the section to test. :type section_name: str :return: True if the given section name is for a translator. :rtype: bool """ if re.match(r'^[a-zA-Z+-]+2[a-zA-Z0-9+-]+(?:_[a-zA-Z0-9_+-]+)?$', section_name, re.S): return True return False def read(self, filename : str, translator_level : TranslatorLevel, config : Config = None) -> Config: """ Read the configuration file. :param filename: the name of the file to read. :type filename: str :param translator_level: the level at which the configuration is located. See TranslatorLevel enumeration. :type translator_level: TranslatorLevel :param config: the configuration object to fill up. Default is None. :type config: Config :return: the configuration object :rtype: Config """ if config is None: config = Config() filename = os.path.abspath(filename) self._base_dir = os.path.dirname(filename) try: config_file = configparser.ConfigParser() config_file.read(filename) for section in config_file.sections(): nsection = section.lower() if nsection == 'generation': content = dict(config_file.items(section)) self._read_generation_prefix(content, config) for section in config_file.sections(): nsection = section.lower() content = dict(config_file.items(section)) if nsection == 'generation': self._read_generation(content, config) elif nsection == 'viewer': self._read_viewer(content, config) elif nsection == 'clean': self._read_clean(content, config) elif nsection == 'scm': self._read_scm(content, config) elif self._is_translator_section(nsection): self._read_translator(section, translator_level, content, config) else: logging.debug(T("Ignore section '%s' in the configuration file: %s") % (section, filename)) finally: self._base_dir = os.getcwd() return config # noinspection PyMethodMayBeStatic def __detect_ist_file(self, config : Config) -> str | None: """ Detect an IST file into the current document. :param config: the configuration. :type config: Config :return: the IST filename or None :rtype: str """ ddir = config.document_directory if not ddir: ddir = os.getcwd() if os.path.isdir(ddir): only_files = [f for f in os.listdir(ddir) if os.path.isfile(os.path.join(ddir, f))] ist_files = list() for file in only_files: if re.match(r'.ist$', file, re.I): ist_files.append(file) if len(ist_files) > 0: filename = ist_files[0] if len(ist_files) > 1: logging.warning(T('Multiple IST files were found into the document directory. Use: %s') % filename) return filename return None # noinspection PyMethodMayBeStatic def _ensure_ascendent_compatibility(self, value : str | None) -> str: if value: m = re.match(r'^(.*?)\s*#.*$', value) if m: return m.group(1) return value or '' def to_path(self, value : str | None) -> str: if value: return genutils.abs_path(value, self._base_dir) return value or '' @staticmethod def to_bool(value : str, default : bool | None) -> bool: """ Convert a string to a bool. This function takes care of strings as "True", "False", "Yes", "No", "t", "f", "y", "n", "1", "0". :param value: the value to convert. :type value: str :param default: the default value. :type: bool :return: the boolean value. :rtype: bool """ if value: v = value.lower() return v == 'true' or v == 'yes' or v == 't' or v =='y' or v =='1' return default or False @staticmethod def to_path_list(value : str) -> list: """ Convert a string to list of paths. According to the operating system, the path separator may be ':' or ';' :param value: the value to convert. :type value: str :return: the list value. :rtype: list """ if value: sep = os.pathsep paths = value.split(sep) return paths return list() @staticmethod def to_list(value : str) -> list[str] | None: """ Convert a string to list of strings. The considered separators are: ',' and ';' :param value: the value to convert. :type value: str :return: the list value. :rtype: list """ if value: return re.split(r'\s*[,;]\s*', value) return None @staticmethod def to_kw(value : str, default : str | None) -> str: """ Convert a string to string-based keyword. :param value: the value to convert. :type value: str | None :param default: the default value. :type: str :return: the keyword. :rtype: str """ if value: return value.lower() return default or '' # noinspection PyBroadException,DuplicatedCode def read_system_config_safely(self, config : Config = None) -> Config: """ Read the configuration file at the system level without failing if the file does not exist. :param config: the configuration object to fill up. Default is None. :type config: Config :return: the configuration object :rtype: Config """ if config is None: config = Config() filename = config.system_config_file if filename and os.path.isfile(filename) and os.access(filename, os.R_OK): try: self.read(filename, TranslatorLevel.SYSTEM, config) except: pass return config # noinspection PyBroadException,DuplicatedCode def read_user_config_safely(self, config : Config = None) -> Config: """ Read the configuration file at the user level without failing if the file does not exist. :param config: the configuration object to fill up. Default is None. :type config: Config :return: the configuration object :rtype: Config """ if config is None: config = Config() filename = config.user_config_file if filename and os.path.isfile(filename) and os.access(filename, os.R_OK): try: self.read(filename, TranslatorLevel.USER, config) except: pass return config # noinspection PyBroadException def read_document_config_safely(self, filename : str, config : Config = None) -> Config: """ Read the configuration file at the document level without failing if the file does not exist. :param filename: the name of the file to read. :type filename: str :param config: the configuration object to fill up. Default is None. :type config: Config :return: the configuration object :rtype: Config """ if config is None: config = Config() assert config is not None if filename and os.path.isfile(filename) and os.access(filename, os.R_OK): try: return self.read(filename, TranslatorLevel.DOCUMENT, config) except: pass return config