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 * 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, 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() ) 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 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') @abstractmethod def close_output_stream(self) -> None: """ :return: """ @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): 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 _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): self.write_line(line + '\n') def write_line(self, line: str): if self._output_file is None: raise NotImplementedError if not self._mode == FormatterMode.drop or self._mode == FormatterMode.macrocode_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 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: 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(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: 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() return self.future_config