from pathlib import Path from typing import List, TextIO, Optional, Tuple, Dict from .macros import Macro from .pytex_formatter import PyTeXFormatter 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 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, line, pos): 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) - 1): self._cached_lines.append( self._handle.readline() ) if self._cached_lines[-1] == '': self._handle.close() self._file_exhausted = True @property def exhausted(self) -> bool: return self._file_exhausted & len(self._cached_lines) == 0 class TexFormatter(PyTeXFormatter): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._macros: List[Macro] = [] self._line_stream: Optional[LineStream] = None self._output_file: Optional[TextIO] = None @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 def _handle_macro(self, macro: Macro): res = macro.apply( self.line_stream.current_line(), self ) if isinstance(res, str): self.line_stream.set_line(res) else: self.line_stream.pop_line() self.line_stream.push_lines(res) def _shipout_line(self): line = self.line_stream.pop_line().rstrip() self.write_line(line) def write_line(self, line: str): self._output_file.write(line) def open_output_stream(self): raise NotImplementedError @property def future_config(self) -> Optional[List[Tuple[str, Dict]]]: raise NotImplementedError def format_pre_header(self) -> None: pass def format_header(self): 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: recent_replacement = True while recent_replacement: recent_replacement = False for macro in self.macros: if macro.matches(self.line_stream.current_line()): self._handle_macro(macro) recent_replacement = True break self._shipout_line() def format(self, build_dir: Path, overwrite: bool = False) -> Optional[List[Tuple[str, Dict]]]: self.open_output_stream() self.format_pre_header() self.format_header() self.format_post_header() self.format_document() self._output_file.close() return self.future_config