#!/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. """ Tools for creating a flattened version of a TeX document. A flattened document contains a single TeX file, and all the other related files are put inside the same directory of the TeX file. """ import os import shutil import re import textwrap import logging from typing import override, Any, Callable from autolatex2.tex.texobservers import Observer from autolatex2.tex.texparsers import Parser from autolatex2.tex.texparsers import TeXParser import autolatex2.utils.utilfunctions as genutils from autolatex2.tex.utils import TeXMacroParameter from autolatex2.utils.i18n import T import autolatex2.tex.extra_macros as extramacros FULL_EXPAND_REGISTRY : dict[str, Callable[[Any, Parser, str, list[TeXMacroParameter]], str]] = dict() EXTRA_FULL_EXPAND_REGISTRY : dict[str, Callable[[Any, Parser, str, list[TeXMacroParameter]], str]] = dict() PREFIX_EXPAND_REGISTRY : dict[str, Callable[[Any, Parser, str, list[TeXMacroParameter]], str]] = dict() EXTRA_PREFIX_EXPAND_REGISTRY : dict[str, Callable[[Any, Parser, str, list[TeXMacroParameter]], str]] = dict() # noinspection DuplicatedCode def expand_function(start_symbol : bool, extra_macro : bool = False): """ Decorator to register functions with __expand__ prefix. :param start_symbol: Marks the function as associated to the prefix of a LaTeX macro name. :type start_symbol: bool :param extra_macro: Marks the function as part of th supporting features for the extra macros. Default is False. :type extra_macro: bool :return: the decorator. """ def decorator(func: Callable) -> Callable: # Store the function and its metadata # Remove "_expand__" prefix if not func.__name__.startswith('_expand__'): raise NameError('Function name must start with \'_expand__\'') func_name = str(func.__name__)[9:] if start_symbol: if extra_macro: EXTRA_PREFIX_EXPAND_REGISTRY[func_name] = func else: PREFIX_EXPAND_REGISTRY[func_name] = func else: if extra_macro: EXTRA_FULL_EXPAND_REGISTRY[func_name] = func else: FULL_EXPAND_REGISTRY[func_name] = func return func return decorator class Flattener(Observer): """ Observer on TeX parsing for creating a flattened version of a TeX document. """ __MACROS : dict[str,str] = { # TeX 'input' : '!{}', 'include' : '!{}', 'usepackage' : '![]!{}', 'RequirePackage' : '![]!{}', 'documentclass' : '![]!{}', # Figures 'includegraphics' : '![]!{}', 'graphicspath' : '![]!{}', 'pgfdeclareimage' : '![]!{}!{}', # Biblatex 'addbibresource' : '![]!{}', 'printbibliography' : '![]', # bibunits 'putbib' : '![]', # S. Galland Templates 'bibliographyslide' : '', # BibTeX and other files 'defaultbibliography' : '!{}', 'defaultbibliographystyle' : '!{}', 'begin' : '![]!{}', 'end' : '![]!{}', } def __init__(self, filename : str, output_directory : str, include_extra_macros : bool): """ Constructor. :param filename: The name of the file to parse. :type filename: str :param output_directory: The name of the directory in which the document must be generated. :type output_directory: ste :param include_extra_macros: Indicates if the extra macros must be included in the dependency analysis. :type include_extra_macros: bool """ self.__include_extra_macros = include_extra_macros if self.__include_extra_macros: self.__full_expand_registry = FULL_EXPAND_REGISTRY | EXTRA_FULL_EXPAND_REGISTRY self.__prefix_expand_registry = PREFIX_EXPAND_REGISTRY | EXTRA_FULL_EXPAND_REGISTRY else: self.__full_expand_registry = FULL_EXPAND_REGISTRY self.__prefix_expand_registry = PREFIX_EXPAND_REGISTRY self.__file_content_counter : int = 0 self.__embedded_files_added : set[str] = set() self.__embedded_files : dict[str,str] = dict() self.__include_paths : list[str] = list() self.__dynamic_preamble : list[str] = list() self.__tex_file_content : str = '' self.__source2target : dict[str,str] = dict() self.__target2source : dict[str,str] = dict() self.__files_to_copy : set[str] = set() # Other attributes self.__filename : str = filename self.__basename : str = os.path.basename(os.path.splitext(filename)[0]) self.__dirname : str = os.path.dirname(filename) self.__output : str = output_directory self.__use_biblio :bool = False self.__included_sty : dict[str,str] = dict() self.__bibunits_aux_index : int = 0 self.__in_bibunit : bool = False self.__explicit_bibliography : bool = False self.__explicit_bibliography_files : list[str] = list() self.__default_bibliography : list[str] = list() self.__explicit_bibliography_style : bool = False self.__default_bibliography_style : str | None = None self.__reset() def __reset(self) : # Inclusion paths for pictures. self.__include_paths = list() if self.__dirname: self.__include_paths.append(self.__dirname) # Preamble entries added by this tool self.__dynamic_preamble = list() # Content of the TeX file to generate self.__tex_file_content = '' # Mapping between the files of the source TeX and the target TeX. self.__source2target = dict() self.__target2source = dict() # Files to copy self.__files_to_copy = set() # Embedded files self.__embedded_files_added = set() self.__embedded_files = dict() self.__file_content_counter = 0 @property def include_paths(self) -> list[str]: """ Replies the paths in which included files are search for. :return: The list of the inclusion path. :rtype: list[str] """ return self.__include_paths @property def use_biblio(self) -> bool: """ Replies if the biblio database is used and kept in the flat version. :return: True if the biblio database may be used. False for inline biliography entries. :rtype: bool """ return self.__use_biblio @use_biblio.setter def use_biblio(self, b : bool): """ Set if the biblio database is used and kept in the flat version. :param b: True if the biblio database may be used. False for inline biliography entries. :type b: bool """ self.__use_biblio = b @property def output_directory(self) -> str: """ Replies the output directory. :return: The name of the output directory. :rtype: str """ return self.__output @output_directory.setter def output_directory(self, n : str): """ Set the output directory. :param n: The name of output directory. :type n: str """ self.__output = n @property def basename(self) -> str: """ Replies the basename of the parsed file. :return: The basename of the parsed file. :rtype: str """ return self.__basename @basename.setter def basename(self, n : str): """ Set the basename of the parsed file. :param n: The basename of the parsed file. :type n: str """ self.__basename = n @property def dirname(self) -> str: """ Replies the dirname of the parsed file. :return: The dirname of the parsed file. :rtype: str """ return self.__dirname @dirname.setter def dirname(self, n : str): """ Set the dirname of the parsed file. :param n: The dirname of the parsed file. :type n: str """ self.__dirname = n @property def filename(self) -> str: """ Replies the filename of the parsed file. :return: The filename of the parsed file. :rtype: str """ return self.__filename @filename.setter def filename(self, n : str): """ Set the filename of the parsed file. :param n: The filename of the parsed file. :type n: str """ self.__filename = n @override def find_macro(self, parser : Parser, name : str, special : bool, math : bool) -> str | None: """ Invoked each time a macro definition is not found in the parser data. :param parser: reference to the parser. :type parser: Parser :param name: Name of the macro. :type name: str :param special: Indicates if the macro is a special macro or not. :type special: bool :param math: Indicates if the math mode is active. :type math: bool :return: the definition of the macro, i.e., the macro prototype. See the class documentation for an explanation about the format of the macro prototype. :rtype: str """ if not special: if name.startswith('bibliographystyle'): return '!{}' elif name.startswith('bibliography'): return '!{}' return None @override def open_block(self, parser : Parser, text : str) -> str | None: """ Invoked when a block is opened. :param parser: reference to the parser. :type parser: Parser :param text: The text used for opening the block. :type text: str :return: the text that must replace the block opening in the output, or None if no replacement is needed. :rtype: str """ return '{' @override def close_block(self, parser : Parser, text : str) -> str | None: """ Invoked when a block is closed. :param parser: reference to the parser. :type parser: Parser :param text: The text used for opening the block. :type text: str :return: the text that must replace the block opening in the output, or None if no replacement is needed. :rtype: str """ return '}' @override def open_math(self, parser : Parser, inline : bool) -> str | None: """ Invoked when a math environment is opened. :param parser: reference to the parser. :type parser: Parser :param inline: Indicates if the math environment is inline or not. :type inline: bool :return: the text that must replace the block opening in the output, or None if no replacement is needed. :rtype: str """ return '$' if inline else '\\[' @override def close_math(self, parser : Parser, inline : bool) -> str | None: """ Invoked when a math environment is closed. :param parser: reference to the parser. :type parser: Parser :param inline: Indicates if the math environment is inline or not. :type inline: bool :return: the text that must replace the block opening in the output, or None if no replacement is needed. :rtype: str """ return '$' if inline else '\\]' @override def text(self, parser : Parser, text : str): """ Invoked when characters were found and must be output. :param parser: reference to the parser. :type parser: Parser :param text: the text to filter. :type text: str """ if text: self.__tex_file_content += text # noinspection DuplicatedCode,PyCallingNonCallable @override def expand(self, parser : Parser, raw_text : str, name : str, *parameters : TeXMacroParameter) -> str | None: """ Expand the given macro on the given parameters. :param parser: reference to the parser. :type parser: Parser :param raw_text: The raw text that is the source of the expansion. :type raw_text: str :param name: Name of the macro. :type name: str :param parameters: Descriptions of the values passed to the TeX macro. :type parameters: dict[str,Any] :return: the result of expansion of the macro, or None to not replace the macro by something (the macro is used as-is) :rtype: str """ if name.startswith('\\'): callback_name = re.sub(r'\*', 'star', name[1:]) if callback_name in self.__full_expand_registry: func = self.__full_expand_registry[callback_name] return func(self, parser, name, list(parameters)) else: largest_size = 0 largest_func = None for prefix, func in self.__prefix_expand_registry.items(): if callback_name.startswith(prefix): l = len(prefix) if l > largest_size: largest_size = l largest_func = func if largest_func is not None: return largest_func(self, parser, name, list(parameters)) return raw_text # noinspection PyUnusedLocal @expand_function(start_symbol=False) def _expand__begin(self, parser : Parser, name : str, parameters : list[TeXMacroParameter]) -> str: assert len(parameters) > 1 tex_name = parameters[1].text if tex_name == 'filecontents*': self.__file_content_counter = self.__file_content_counter + 1 elif tex_name == 'bibunit' or (self.__include_extra_macros and tex_name == 'bibliographysection'): self.__in_bibunit = True self.__bibunits_aux_index = self.__bibunits_aux_index + 1 if not self.use_biblio: return '' ret = name if parameters[0].text: ret += "[%s]" % parameters[0].text ret += "{%s}" % tex_name return ret @expand_function(start_symbol=False) def _expand__end(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: assert len(parameters) > 1 tex_name = parameters[1].text if tex_name == 'filecontents*': self.__file_content_counter = self.__file_content_counter - 1 if self.__file_content_counter <= 0: for key, value in self.__embedded_files.items(): parser.put_back(value) self.__embedded_files = dict() elif tex_name == 'bibunit': self.__in_bibunit = False if not self.use_biblio: return '' elif self.__include_extra_macros and tex_name == 'bibliographysection': self._expand__putbib(parser, name, [ TeXMacroParameter(text='biblio') ]) self.__in_bibunit = False if not self.use_biblio: return '\\bibliographyslide' ret = name if parameters[0].text: ret += "[%s]" % parameters[0].text ret += "{%s}" % tex_name return ret # noinspection PyPep8Naming @expand_function(start_symbol=False) def _expand__RequirePackage(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: return self._expand__usepackage(parser, name, parameters) # noinspection DuplicatedCode @expand_function(start_symbol=False) def _expand__usepackage(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: assert len(parameters) > 1 tex_name = parameters[1].text if tex_name == 'bibunits' and not self.use_biblio: return '' filename = self.__make_filename(tex_name, '.sty') added_file = '' if tex_name == 'biblatex': if not self.use_biblio: filename = self.__make_filename(self.basename, '.bbl', '.tex') if os.path.isfile(filename) and filename not in self.__embedded_files_added: logging.debug(T('Embedding %s'), filename) if not self.__embedded_files_added: self.__dynamic_preamble.append("\\usepackage{filecontents}") self.__embedded_files_added.add(filename) with open(filename) as f: content = f.read() basename = os.path.basename(filename) added_file = textwrap.dedent(""" %%======================================================= %%== BEGIN FILE: %s %%======================================================= \\begin{filecontents*}{%s} %s \\end{filecontents*} %%======================================================= """) % (basename, basename, content) else: logging.error(T('File not found: %s'), filename) elif self.__is_document_file(filename) and filename not in self.__embedded_files_added: logging.debug(T('Embedding %s'), filename) if not self.__embedded_files_added: self.__dynamic_preamble.append("\\usepackage{filecontents}") self.__embedded_files_added.add(filename) with open(filename) as f: content = f.read() basename = os.path.basename(filename) added_file = textwrap.dedent(""" %%======================================================= %%== BEGIN FILE: %s %%======================================================= \\begin{filecontents*}{%s} %s \\end{filecontents*} %%======================================================= """) % (basename, basename, content) ret = name if parameters[0].text: ret += "[%s]" % parameters[0].text ret += "{%s}" % tex_name if added_file: if self.__file_content_counter <= 0: parser.put_back(added_file + ret) return '' else: self.__embedded_files[filename] = added_file return ret else: return ret # noinspection PyUnusedLocal @expand_function(start_symbol=False) def _expand__documentclass(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: assert len(parameters) > 1 tex_name = parameters[1].text filename = self.__make_filename(tex_name, '.cls') if self.__is_document_file(filename): tex_name = self.__create_mapping(filename, '.cls') self.__files_to_copy.add(filename) ret = name if parameters[0].text: ret += "[%s]" % parameters[0].text ret += "{%s}\n\n%%========= AUTOLATEX PREAMBLE\n\n" % tex_name return ret @expand_function(start_symbol=False, extra_macro=True) def _expand__includeanimatedfigure(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: return self._expand__includegraphics(parser, name, parameters) @expand_function(start_symbol=False, extra_macro=True) def _expand__includeanimatedfigurewtex(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: return self._expand__includegraphics(parser, name, parameters) @expand_function(start_symbol=False, extra_macro=True) def _expand__includefigurewtex(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: return self._expand__includegraphics(parser, name, parameters) @expand_function(start_symbol=False, extra_macro=True) def _expand__includegraphicswtex(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: return self._expand__includegraphics(parser, name, parameters) # noinspection PyUnusedLocal @expand_function(start_symbol=False) def _expand__includegraphics(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: assert len(parameters) > 1 tex_name, prefix = self.__find_picture(parameters[1].text) ret = prefix + name if parameters[0].text: ret += "[%s]" % parameters[0].text ret += "{%s}" % tex_name return ret # noinspection PyUnusedLocal @expand_function(start_symbol=False) def _expand__graphicspath(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: assert len(parameters) > 1 t = parameters[1].text if t: r = re.match(r'^\s*(?:\{([^}]+)}|([^,]+))\s*[,;]?\s*(.*)$', t) while r: graphic_path = r.group(1) if not graphic_path: graphic_path = r.group(2) or '' if not os.path.isabs(graphic_path): graphic_path = os.path.join(self.__dirname, graphic_path) t = r.group(3) self.__include_paths.insert(0, graphic_path) r = re.match(r'^\s*(?:\{([^}]+)}|([^,]+))\s*[,;]?\s*(.*)$', t) if t else None return "\\graphicspath{{./}}" @expand_function(start_symbol=False, extra_macro=True) def _expand__mfigurestar(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: return self._expand__mfigure(parser, name, parameters) @expand_function(start_symbol=False, extra_macro=True) def _expand__mfigurewtex(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: return self._expand__mfigure(parser, name, parameters) @expand_function(start_symbol=False, extra_macro=True) def _expand__mfigurewtexstar(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: return self._expand__mfigure(parser, name, parameters) # noinspection PyUnusedLocal @expand_function(start_symbol=False, extra_macro=True) def _expand__mfigure(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: assert len(parameters) > 4 tex_name, prefix = self.__find_picture(parameters[2].text) ret = prefix + name if parameters[0].text: ret += "[%s]" % parameters[0].text ret += "{%s}{%s}{%s}{%s}" % (parameters[1].text, tex_name, parameters[3].text, parameters[4].text) return ret @expand_function(start_symbol=False, extra_macro=True) def _expand__msubfigurestar(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: return self._expand__msubfigure(parser, name, parameters) # noinspection PyUnusedLocal @expand_function(start_symbol=False, extra_macro=True) def _expand__msubfigure(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: assert len(parameters) > 3 tex_name, prefix = self.__find_picture(parameters[2].text) ret = prefix + name if parameters[0].text: ret += "[%s]" % parameters[0].text ret += "{%s}{%s}{%s}" % (parameters[1].text, tex_name, parameters[3].text) return ret # noinspection PyUnusedLocal @expand_function(start_symbol=False) def _expand__pgfdeclareimage(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: assert len(parameters) > 2 tex_name, prefix = self.__find_picture(parameters[2].text) ret = prefix + name if parameters[0].text: ret += "[%s]" % parameters[0].text ret += "{%s}{%s}" % (parameters[1].text, tex_name) return ret @expand_function(start_symbol=False) def _expand__include(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: return self._expand__input(parser, name, parameters) # noinspection PyUnusedLocal,DuplicatedCode @expand_function(start_symbol=False) def _expand__input(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: assert len(parameters) > 0 filename = self.__make_filename(parameters[0].text, '.tex') with open(filename) as f: subcontent = f.read() subcontent += textwrap.dedent(""" %%======================================================= %%== END FILE: %s %%======================================================= """) % (os.path.basename(filename)) parser.put_back(subcontent) return textwrap.dedent(""" %%======================================================= %%== BEGIN FILE: %s %%======================================================= """) % (os.path.basename(filename)) # noinspection PyUnusedLocal,DuplicatedCode @expand_function(start_symbol=True) def _expand__bibliographystyle(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: assert len(parameters) > 0 self.__explicit_bibliography_style = True if self.use_biblio: tex_name = parameters[0].text filename = self.__make_filename(tex_name, '.bst') if self.__is_document_file(filename): tex_name = self.__create_mapping(filename, '.bst') self.__files_to_copy.add(filename) return "%s{%s}" % (name, tex_name) return '' # noinspection PyUnusedLocal,DuplicatedCode @expand_function(start_symbol=False) def _expand__defaultbibliographystyle(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: assert len(parameters) > 0 self.__default_bibliography_style = parameters[0].text if self.use_biblio: tex_name = parameters[0].text filename = self.__make_filename(tex_name, '.bst') if self.__is_document_file(filename): tex_name = self.__create_mapping(filename, '.bst') self.__files_to_copy.add(filename) return "%s{%s}" % (name, tex_name) return '' # noinspection PyUnusedLocal,DuplicatedCode @expand_function(start_symbol=True) def _expand__bibliography(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: assert len(parameters) > 0 if parameters[0].text: files = re.split(r'\s*,\s*', parameters[0].text) self.__explicit_bibliography = True self.__explicit_bibliography_files = files if self.use_biblio: return self.__flatten_biblio_file(name, files) else: ret = self.__flatten_embedded_format() if ret: return ret return "%s{%s}" % (name, ','.join(files)) return '%s{}' % name # noinspection PyUnusedLocal @expand_function(start_symbol=False) def _expand__addbibresource(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: assert len(parameters) > 1 if parameters[1].text: self.__explicit_bibliography = True files = re.split(r'\s*,\s*', parameters[1].text) self.__explicit_bibliography_files = files if self.use_biblio: return self.__flatten_biblio_file(name, files) return '' # noinspection PyUnusedLocal @expand_function(start_symbol=False) def _expand__printbibliography(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: if self.__explicit_bibliography: if self.__explicit_bibliography_files: if self.use_biblio: return name else: return self.__flatten_embedded_format() else: if self.__default_bibliography: if self.use_biblio: return self.__flatten_biblio_file(name, self.__default_bibliography, add_parameter=0) else: return self.__flatten_embedded_format() logging.error(T('No bibliography source provided')) return '' # noinspection DuplicatedCode,PyUnusedLocal @expand_function(start_symbol=False) def _expand__putbib(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: assert len(parameters) > 0 if parameters[0].text: files = re.split(r'\s*,\s*', parameters[0].text) elif self.__explicit_bibliography: files = self.__explicit_bibliography_files else: files = self.__default_bibliography if files: if self.use_biblio: return self.__flatten_biblio_file(name, files, add_parameter=2) elif self.use_biblio: return self.__flatten_biblio_file(name, [self.basename], add_parameter=2) return self.__flatten_embedded_format() # noinspection PyUnusedLocal @expand_function(start_symbol=False, extra_macro=True) def _expand__bibliographyslide(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: self._expand__putbib(parser, name, [ TeXMacroParameter(text='biblio') ]) return name # noinspection PyUnusedLocal,DuplicatedCode @expand_function(start_symbol=False) def _expand__defaultbibliography(self, parser: Parser, name: str, parameters: list[TeXMacroParameter]) -> str: assert len(parameters) > 0 if parameters[0].text: lst = list() for param in re.split(r'\s*,\s*', parameters[0].text): bib_name = self.__make_filename(param, '.bib') if self.__is_document_file(bib_name): lst.append(bib_name) self.__default_bibliography = lst if lst else list() return '' # noinspection DuplicatedCode def __flatten_biblio_file(self, macro_name : str, files : list[str], add_parameter : int = 1) -> str: """ Do the bibliography output by considering that the biblio is stored in a local bib file. :param macro_name: The name of the TeX macro that should be used for including the bibliography. :type macro_name: str :param files: A list of bibliography file to use for rendering the bibliography. :type files: list[str] :param add_parameter: Indicates if the files are added as parameter of the macro. 0 means no parameter. 1 means one regular parameter. 2 means optional parameter. Default is 1. :type add_parameter: int :return: the text to be put back in the flattened version of the document. :rtype: str """ assert self.use_biblio new_tex_names = list() for file in files: filename = self.__make_filename(file, '.bib') if self.__is_document_file(filename): tex_name = self.__create_mapping(filename, '.bib') self.__files_to_copy.add(filename) new_tex_names.append(tex_name) else: logging.warning(T('File not found in document folder: %s'), filename) new_tex_names.append(file) if add_parameter == 1: return "%s{%s}" % (macro_name, ','.join(new_tex_names)) elif add_parameter == 2: return "%s[%s]" % (macro_name, ','.join(new_tex_names)) else: return macro_name def __flatten_embedded_format(self) -> str: """ Do the bibliography output by output the BBL content in the TeX file. :return: the text to be put back in the flattened version of the document. :rtype: str """ assert not self.use_biblio if self.__in_bibunit: bbl_name = self.basename + '.' + str(self.__bibunits_aux_index) else: bbl_name = self.basename bbl_file = self.__make_filename(bbl_name, '.bbl') if os.path.isfile(bbl_file): with open(bbl_file) as f: content = f.read() return textwrap.dedent(""" %%======================================================= %%== BEGIN FILE: %s %%======================================================= %s %%======================================================= """) % (os.path.basename(bbl_file), content) else: logging.error(T('File not found: %s'), bbl_file) return '' def __make_filename(self, basename : str, *ext : str) -> str: """ Create a valid filename for the flattening process. :param basename: The basename. :param basename: str :param ext: The filename extension (default: None). :param ext: list(str) """ name = basename for one_ext in ext: if one_ext and not basename.endswith(ext): name = basename + one_ext continue if not os.path.isabs(name): return os.path.join(self.dirname, name) return name def __is_document_file(self, filename : str) -> bool: """ Replies if the given file is a part of the document. :param filename: The filename to test. :type filename: str :return: True if the file is a part of the document; otherwise False. :rtype: bool """ if not os.path.isabs(filename): filename = os.path.join(self.dirname, filename) if os.path.isfile(filename): return filename.startswith(self.dirname) return False # noinspection DuplicatedCode def __create_mapping(self, filename : str, ext : str) -> str: """ Compute a unique filename, and map it to the source file. :param filename: The filename to translate. :type filename: str :param ext: The filename extension to remove. :type ext: str :return: The unique basename. :rtype: str """ name = os.path.basename(filename) if ext and name.endswith(ext): name = name[0:(-len(ext))] bn = name i = 0 while (name + ext) in self.__target2source: name = "%s_%d" % (bn, i) i += 1 self.__target2source[name + ext] = filename self.__source2target[filename] = name + ext return name # noinspection DuplicatedCode def __find_picture(self, tex_name : str) -> tuple[str,str]: """ Find a picture. :param tex_name: The name of the picture in the TeX document. :type tex_name: str :return: the tuple (target filename, the prefix to add before the macro) :rtype: tuple[str,str] """ # Search in the registered/found bitmaps if self.__source2target: for src in self.__source2target: if src == tex_name: return os.path.basename(self.__source2target[tex_name]), '' prefix = '' filename = self.__make_filename(tex_name) if not os.path.isfile(filename): texexts = ('.pdftex_t', '.pstex_t', '.pdf_tex', '.ps_tex', '.tex') figexts = ( '.pdf', '.eps', '.ps', '.png', '.jpeg', '.jpg', '.gif', '.bmp') exts = figexts + texexts ofilename = filename obasename = genutils.basename(tex_name, *exts) filenames = {} # Search in the registered paths template = obasename for path in self.include_paths: for ext in figexts: fullname = os.path.join(path, template + ext) fullname = self.__make_filename(fullname) if os.path.isfile(fullname): filenames[fullname] = False for ext in texexts: fullname = os.path.join(path, template + ext) fullname = self.__make_filename(fullname, '') if os.path.isfile(fullname): filenames[fullname] = True # Search in the folder, i.e. the document directory. if not filenames: template = os.path.join(os.path.dirname(ofilename), genutils.basename(ofilename, *exts)) for ext in figexts: fn = template + ext if os.path.isfile(fn): filenames[fn] = False for ext in texexts: fn = template + ext if os.path.isfile(fn): filenames[fn] = True if not filenames: logging.error(T('Picture not found: %s'), tex_name) else: selected_name1 = None selected_name2 = None for filename in filenames: bn, ext = os.path.splitext(filename) tex_name = self.__create_mapping(filename, ext) + ext if filenames[filename]: if not selected_name1: selected_name1 = (tex_name, filename) else: self.__files_to_copy.add(filename) selected_name2 = tex_name if selected_name1: tex_name, filename = selected_name1 logging.debug(T('Embedding %s'), filename) with open(filename) as f: filecontent = f.read() # Replacing the filename in the newly embedded TeX file if self.__source2target: for source in self.__source2target: filecontent = filecontent.replace('{' + source + '}', '{' + self.__source2target[source] + '}') bsn = os.path.basename(tex_name) prefix += textwrap.dedent(""" %%======================================================= %%== BEGIN FILE: %s %%======================================================= \\begin{filecontents*}{%s} %s \\end{filecontents*} %%======================================================= """) % (bsn, bsn, filecontent) self.__dynamic_preamble.append('\\usepackage{filecontents}') elif selected_name2: tex_name = selected_name2 else: ext = os.path.splitext(tex_name)[1] or '' tex_name = self.__create_mapping(filename, ext) + ext self.__files_to_copy.add(filename) return tex_name, prefix # noinspection DuplicatedCode def _analyze_document(self) -> str: """ Analyze the tex document for extracting information. :return: The content of the file. """ with open(self.filename) as f: content = f.read() parser = TeXParser() parser.observer = self parser.filename = self.filename for k, v in self.__MACROS.items(): parser.add_text_mode_macro(k, v) parser.add_math_mode_macro(k, v) if self.__include_extra_macros: for k, v in extramacros.ALL_EXTRA_MACROS.items(): parser.add_text_mode_macro(k, v) parser.add_math_mode_macro(k, v) parser.parse(content) # Replace PREAMBLE content if self.__tex_file_content: preamble = '\n'.join(self.__dynamic_preamble) if not preamble: preamble = '' content = self.__tex_file_content.replace('%========= AUTOLATEX PREAMBLE', preamble, 1) # Clean the content by removing empty lines content = content.replace('\t', ' ').strip() content = re.sub("\n+[ \t]*", "\n", content, re.S) return content def _generate_flat_document(self, content : str) -> bool: """ Generate the flat document. :param content: The content of the file. :type content: str :return: The success status of the generation. :rtype: bool """ # Create the output directory os.makedirs(self.output_directory) # Create the main TeX file output_file = os.path.join(self.output_directory, self.basename) + '.tex' logging.debug(T('Writing %s') % output_file) with open(output_file, 'w') as f: f.write(content) # Copy the resources for source in self.__files_to_copy: target = self.__source2target[source] target = os.path.join(self.output_directory, target) logging.debug(T('Copying resource %s to %s') % (source, target)) target_directory = os.path.dirname(target) if not os.path.isdir(target_directory): os.makedirs(target_directory) shutil.copy(source, target) return True def run(self) -> bool: """ Make the input file standalone. :return: True if the execution is a success, otherwise False. """ self.__reset() content = self._analyze_document() logging.debug(T("File content: %s") % content) return self._generate_flat_document(content) @override def comment(self, parser: Parser, raw: str, comment: str) -> str | None: return None