pytex/PyTeX/format/tex_formatter.py

237 lines
7.4 KiB
Python
Raw Normal View History

2022-02-06 18:12:30 +01:00
from pathlib import Path
2022-02-07 18:36:30 +01:00
from typing import List, TextIO, Optional, Tuple, Dict
2022-02-08 17:04:16 +01:00
from abc import ABC, abstractmethod
2022-02-07 18:36:30 +01:00
from .formatting_config import FormattingConfig
2022-02-06 18:12:30 +01:00
from .macros import Macro
2022-02-07 18:36:30 +01:00
from .pytex_formatter import PyTeXFormatter
2022-02-09 19:25:25 +01:00
from .enums import *
from ..logger import logger
2022-02-17 23:16:41 +01:00
from .errors import *
2022-02-06 18:12:30 +01:00
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
2022-02-17 23:16:41 +01:00
self._line_number: int = 0
2022-02-06 18:12:30 +01:00
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):
2022-02-06 18:12:30 +01:00
self.reserve_lines(pos + 1)
self._cached_lines[pos] = line
def reserve_lines(self, num_lines):
2022-02-09 11:26:01 +01:00
for i in range(0, num_lines - len(self._cached_lines)):
2022-02-06 18:12:30 +01:00
self._cached_lines.append(
self._handle.readline()
)
2022-02-17 23:16:41 +01:00
self._line_number += 1
2022-02-06 18:12:30 +01:00
if self._cached_lines[-1] == '':
self._handle.close()
self._file_exhausted = True
2022-02-06 18:12:30 +01:00
@property
def exhausted(self) -> bool:
2022-02-09 11:26:01 +01:00
a = self._file_exhausted
b = len(self._cached_lines) == 0
return a & b
2022-02-06 15:11:23 +01:00
2022-02-17 23:16:41 +01:00
@property
def line_number(self) -> int:
return self._line_number
2022-02-06 15:11:23 +01:00
2022-02-08 17:04:16 +01:00
class TexFormatter(PyTeXFormatter, ABC):
2022-02-06 18:12:30 +01:00
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._macros: List[Macro] = []
2022-02-08 16:09:56 +01:00
self._line_stream: Optional[LineStream] = None
2022-02-09 19:25:25 +01:00
self._mode = FormatterMode.normal
2022-02-06 18:12:30 +01:00
def open_output_stream(self, build_dir: Path, filename: str) -> None:
2022-02-08 17:04:16 +01:00
"""
:param build_dir: Where to open output stream
:param filename: Name of file
2022-02-08 17:04:16 +01:00
"""
out_file = build_dir / filename
if out_file.exists():
raise NotImplementedError
else:
self._output_file = out_file.open('w')
self.mode = FormatterMode.normal
2022-02-08 17:04:16 +01:00
def close_output_stream(self) -> None:
self._output_file.close()
2022-02-08 17:04:16 +01:00
@property
2022-02-08 19:01:18 +01:00
@abstractmethod
def future_config(self) -> List[Tuple[str, FormattingConfig]]:
2022-02-08 17:04:16 +01:00
"""
# 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
2022-02-06 18:12:30 +01:00
def _handle_macro(self, macro: Macro):
2022-02-17 23:16:41 +01:00
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()
2022-02-17 23:16:41 +01:00
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)
2022-02-06 18:12:30 +01:00
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')
2022-02-06 18:12:30 +01:00
def write_line(self, line: str):
2022-02-08 16:39:20 +01:00
if self._output_file is None:
raise NotImplementedError
2022-02-18 00:23:01 +01:00
if not self.mode.is_drop():
2022-02-09 19:25:25 +01:00
self._output_file.write(line)
2022-02-06 18:51:49 +01:00
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
2022-02-17 23:16:41 +01:00
def _get_provides_text(self, provided_type: str) -> str:
2022-02-18 21:20:32 +01:00
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
)
2022-02-17 20:05:18 +01:00
2022-02-06 18:51:49 +01:00
def format_header(self):
2022-02-08 16:39:20 +01:00
if self._output_file is None:
raise NotImplementedError
2022-02-06 18:51:49 +01:00
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:
2022-02-17 23:16:41 +01:00
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:
2022-02-17 23:27:47 +01:00
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()
))
2022-02-06 18:51:49 +01:00
def format(self, build_dir: Path, overwrite: bool = False) -> List[Tuple[str, FormattingConfig]]:
for filename in self.output_files:
2022-02-17 23:27:47 +01:00
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
2022-02-17 23:27:47 +01:00