146 lines
4.1 KiB
Python
146 lines
4.1 KiB
Python
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
|
|
|
|
|
|
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, ABC):
|
|
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
|
|
|
|
@abstractmethod
|
|
def open_output_stream(self, build_dir: Path) -> None:
|
|
"""
|
|
|
|
:param build_dir: Where to open output stream
|
|
"""
|
|
|
|
@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
|
|
|
|
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):
|
|
if self._output_file is None:
|
|
raise NotImplementedError
|
|
self._output_file.write(line)
|
|
|
|
def format_pre_header(self) -> None:
|
|
pass
|
|
|
|
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()
|
|
|
|
def format(self, build_dir: Path, overwrite: bool = False) -> List[Tuple[str, FormattingConfig]]:
|
|
self.open_output_stream(build_dir)
|
|
self.format_pre_header()
|
|
self.format_header()
|
|
self.format_post_header()
|
|
self.format_document()
|
|
self.close_output_stream()
|
|
return self.future_config
|