from pathlib import Path from typing import List, TextIO, Optional, Tuple, Dict from abc import ABC, abstractmethod from .formatting_config import FormattingConfig from .macros import Macro from .pytex_formatter import PyTeXFormatter from .enums import * from ..logger import logger from .errors import * class LineStream: def __init__(self, filename: Path): self._file = filename self._handle = open(filename, 'r') self._cached_lines: List[str] = [] self._file_exhausted: bool = False self._line_number: int = 0 def current_line(self): return self.future_line(0) def set_line(self, line): self.set_future_line(0, line) def pop_line(self) -> str: self.reserve_lines(1) line = self._cached_lines[0] self._cached_lines = self._cached_lines[1:] return line def push_line(self, line): self.push_lines([line]) def push_lines(self, lines: List[str]): self._cached_lines = lines + self._cached_lines def future_line(self, pos: int): self.reserve_lines(pos + 1) return self._cached_lines[pos] def set_future_line(self, pos: int, line: str): self.reserve_lines(pos + 1) self._cached_lines[pos] = line def reserve_lines(self, num_lines): for i in range(0, num_lines - len(self._cached_lines)): self._cached_lines.append( self._handle.readline() ) self._line_number += 1 if self._cached_lines[-1] == '': self._handle.close() self._file_exhausted = True @property def exhausted(self) -> bool: a = self._file_exhausted b = len(self._cached_lines) == 0 return a & b @property def line_number(self) -> int: return self._line_number class TexFormatter(PyTeXFormatter, ABC): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._macros: List[Macro] = [] self._line_stream: Optional[LineStream] = None self._mode = FormatterMode.normal def open_output_stream(self, build_dir: Path, filename: str) -> None: """ :param build_dir: Where to open output stream :param filename: Name of file """ out_file = build_dir / filename if out_file.exists(): raise NotImplementedError else: self._output_file = out_file.open('w') self.mode = FormatterMode.normal def close_output_stream(self) -> None: self._output_file.close() @property @abstractmethod def future_config(self) -> List[Tuple[str, FormattingConfig]]: """ # TODO :return: """ @property def line_stream(self) -> LineStream: if self._line_stream is None: self._line_stream = LineStream(self.input_file) return self._line_stream @property def macros(self) -> List[Macro]: return self._macros @macros.setter def macros(self, macros: List[Macro]): self._macros = macros def _handle_macro(self, macro: Macro): try: replacement, shipout = macro.apply( self.line_stream.current_line(), self ) except PyTeXMacroError as e: e.add_explanation('While applying macro') raise e if shipout: self.line_stream.pop_line() if isinstance(replacement, str): self._shipout_line(replacement) else: if len(replacement) >= 1: self._shipout_line(replacement[0]) self._line_stream.push_lines(replacement[1:]) else: if isinstance(replacement, str): self.line_stream.set_line(replacement) else: self.line_stream.pop_line() self.line_stream.push_lines(replacement) def _post_process_line(self, line: str) -> str: """ Can strip line or add comment symbols etc. :param line: Line, potentially with trailing newline :return: Line without newline symbol """ return line.rstrip() def _shipout_line(self, line) -> None: """ The line might get dropped according to current mode :param line: Line to shipout :return: None """ self.write_line(line.rstrip('\n') + '\n') def write_line(self, line: str): if self._output_file is None: raise NotImplementedError if not self.mode.is_drop(): self._output_file.write(line) def format_pre_header(self) -> None: pass @property def mode(self) -> FormatterMode: return self._mode @mode.setter def mode(self, mode: FormatterMode) -> None: self._mode = mode def _get_provides_text(self, provided_type: str) -> str: if self.config.tex_flavour == TeXFlavour.LaTeX2e: return \ r'\Provides%s{%s}[%s - %s]' \ % ( provided_type, self.name, self.attribute_dict[FormatterProperty.date.value], self.attribute_dict[FormatterProperty.description.value] ) else: return \ '\\ProvidesExpl%s {%s} {%s} {%s}\n {%s}' \ % ( provided_type, self.name, self.attribute_dict[FormatterProperty.date.value], self.config.version, self.config.description ) def format_header(self): if self._output_file is None: raise NotImplementedError self._output_file.write(self.make_header()) def format_post_header(self) -> None: pass def format_document(self) -> None: while not self.line_stream.exhausted: try: recent_replacement = True while recent_replacement: recent_replacement = False for macro in self.macros: if macro.matches(self.line_stream.current_line()): try: self._handle_macro(macro) except PyTeXMacroError as e: e.add_explanation('while handling macro') raise e recent_replacement = True break except PyTeXMacroError as e: e.add_explanation( f'in line {self.line_stream.line_number} ({self.line_stream.current_line().rstrip()})' ) raise e self._shipout_line(self._post_process_line( self.line_stream.pop_line() )) def format(self, build_dir: Path, overwrite: bool = False) -> List[Tuple[str, FormattingConfig]]: for filename in self.output_files: try: self.open_output_stream(build_dir, filename) self.format_pre_header() self.format_header() self.format_post_header() self.format_document() self.close_output_stream() except PyTeXError as e: e.add_explanation(f'while formatting output file {filename}') raise e return self.future_config