diff --git a/src/formatitko/formatitko.py b/src/formatitko/formatitko.py index 9b3b942..9ee76ee 100755 --- a/src/formatitko/formatitko.py +++ b/src/formatitko/formatitko.py @@ -14,7 +14,7 @@ from .katex import KatexClient from .html import html from .tex import tex from .images import ImageProcessor -from .output_generator import OutputGenerator +from .output_generator import OutputGenerator, FormatitkoRecursiveError from .html_generator import HTMLGenerator from .transform_processor import TransformProcessor from .pandoc_processor import PandocProcessor @@ -54,9 +54,15 @@ def main(): doc = import_md(open(args.input_filename, "r").read()) if args.debug: - OutputGenerator(sys.stdout).generate(doc) + try: + OutputGenerator(sys.stdout).generate(doc) + except FormatitkoRecursiveError as e: + e.pretty_print() - doc = TransformProcessor(args.input_filename).transform(doc) + try: + doc = TransformProcessor(args.input_filename).transform(doc) + except FormatitkoRecursiveError as e: + e.pretty_print() # Initialize the image processor (this just keeps some basic state) imageProcessor = ImageProcessor(args.img_public_dir, args.img_web_path, args.img_cache_dir, *args.img_lookup_dirs) @@ -65,11 +71,18 @@ def main(): # Initialize KaTeX client (this runs the node app and connects to a unix socket) with KatexClient(socket=args.katex_socket) as katexClient: with open(args.output_html, "w") as file: - HTMLGenerator(file, katexClient, imageProcessor).generate(doc) + try: + HTMLGenerator(file, katexClient, imageProcessor).generate(doc) + except FormatitkoRecursiveError as e: + e.pretty_print() if args.output_tex is not None: with open(args.output_tex, "w") as file: - UCWTexGenerator(file, imageProcessor).generate(doc) + try: + UCWTexGenerator(file, imageProcessor).generate(doc) + except FormatitkoRecursiveError as e: + e.pretty_print() + if args.output_md is not None: with open(args.output_md, "w") as file: @@ -83,7 +96,10 @@ def main(): if args.output_tex is None: fd = tempfile.NamedTemporaryFile(dir=".", suffix=".tex") with open(fd.name, "w") as file: - UCWTexGenerator(file, imageProcessor).generate(doc) + try: + UCWTexGenerator(file, imageProcessor).generate(doc) + except FormatitkoRecursiveError as e: + e.pretty_print() filename = fd.name else: filename = args.output_tex @@ -93,7 +109,10 @@ def main(): if args.debug: print("-----------------------------------") - OutputGenerator(sys.stdout).generate(doc) + try: + OutputGenerator(sys.stdout).generate(doc) + except FormatitkoRecursiveError as e: + e.pretty_print() if __name__ == "__main__": diff --git a/src/formatitko/katex.py b/src/formatitko/katex.py index ad431e6..39e521f 100644 --- a/src/formatitko/katex.py +++ b/src/formatitko/katex.py @@ -79,7 +79,7 @@ class KatexClient: if "error" in response: raise KatexServerError(response["error"]) if "error" in response["results"][0]: - raise KatexError(response["results"][0]["error"]) + raise KatexError(response["results"][0]["error"] + " in $" + tex + "$") else: return response["results"][0]["html"] diff --git a/src/formatitko/nop_processor.py b/src/formatitko/nop_processor.py index 2f09ffb..bc20a2a 100644 --- a/src/formatitko/nop_processor.py +++ b/src/formatitko/nop_processor.py @@ -7,15 +7,20 @@ from typing import Union, Callable from .whitespace import NBSP from .elements import FQuoted -from .context import Group, InlineGroup, BlockGroup +from .context import Group, InlineGroup, BlockGroup, Context from .whitespace import Whitespace from .command import BlockCommand, InlineCommand, CodeCommand, Command +from .output_generator import FormatitkoRecursiveError ELCl = Union[Element, ListContainer, list[Union[Element, ListContainer]]] +class DoubleDocError(Exception): + "TransformProcessor should only ever see a single Doc." + pass class NOPProcessor: TYPE_DICT: dict[type, Callable] + context: Union[Context, None] = None class UnknownElementError(Exception): f"An unknown Element has been passed to the NOPProcessor, probably because panflute introduced a new one." @@ -96,23 +101,30 @@ class NOPProcessor: return [] def transform(self, e: ELCl) -> ELCl: - if isinstance(e, list): - return self.transform_list(e) - elif isinstance(e, ListContainer): - return self.transform_ListContainer(e) - - for transformer in self.get_pretransformers(): - e = transformer(e) - try: - e = self.TYPE_DICT[type(e)](e) - except KeyError: - raise self.UnknownElementError(type(e)) - - for transformer in self.get_posttransformers(): - e = transformer(e) - - return e + if isinstance(e, list): + return self.transform_list(e) + elif isinstance(e, ListContainer): + return self.transform_ListContainer(e) + + for transformer in self.get_pretransformers(): + e = transformer(e) + + try: + e = self.TYPE_DICT[type(e)](e) + except KeyError: + raise self.UnknownElementError(type(e)) + + for transformer in self.get_posttransformers(): + e = transformer(e) + + return e + except FormatitkoRecursiveError as err: + if not isinstance(e, ListContainer): + err.add_element(e) + raise err + except Exception as err: + raise FormatitkoRecursiveError(e, self.context) from err def transform_list(self, e: list[Union[Element, ListContainer]]) -> list[Union[Element, ListContainer]]: for i in range(len(e)): @@ -293,6 +305,9 @@ class NOPProcessor: return e def transform_Doc(self, e: Doc) -> Doc: + if self.context is not None: + raise DoubleDocError() + self.context = Context(e, self.root_file_path) e.content = self.transform(e.content) return e diff --git a/src/formatitko/output_generator.py b/src/formatitko/output_generator.py index 61df1c6..4f3cd24 100644 --- a/src/formatitko/output_generator.py +++ b/src/formatitko/output_generator.py @@ -3,18 +3,56 @@ from panflute import Cite, Code, Emph, Image, LineBreak, Link, Math, Note, Quote from panflute import BlockQuote, BulletList, Citation, CodeBlock, Definition, DefinitionItem, DefinitionList, Div, Figure, Header, HorizontalRule, LineBlock, LineItem, ListItem, MetaBlocks, MetaBool, MetaInlines, MetaList, MetaMap, MetaString, Null, OrderedList, Para, Plain, RawBlock, Table, TableBody, TableFoot, TableHead from panflute import TableRow, TableCell, Caption, Doc from panflute import MetaValue +from panflute import stringify from typing import Union, Callable from .whitespace import NBSP from .elements import FQuoted from .context import Group, InlineGroup, BlockGroup, Context -import re + +import sys class UnknownElementError(Exception): "An unknown Element has been passed to the OutputGenerator, probably because panflute introduced a new one." pass + +class FormatitkoRecursiveError(Exception): + "A generic exception which wraps other exceptions and adds element-based traceback" + elements: list[Union[Element, ListContainer, list[Union[Element, ListContainer]]]] + context: Context + + def __init__(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]], context: Context, *args): + self.elements = [e] + self.context = context + super().__init__(args) + + def add_element(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]]): + self.elements.append(e) + + def pretty_print(self): + def eprint(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + + def print_filename_recursive(context: Context): + return context.filename +\ + ((" (included from " + print_filename_recursive(context.parent) + ")") if context.parent else "") + eprint(f"Error occured in file {print_filename_recursive(self.context)} in ", end="") + line = None + for i in range(len(self.elements)-1, 0, -1): + if hasattr(self.elements[i], "content") and len(self.elements[i].content) > 0 and isinstance(self.elements[i].content[0], Inline) and line is None: + line = self.elements[i] + eprint(type(self.elements[i]).__name__ + "[" + (str(self.elements[i-1].index) if isinstance(self.elements[i-1].index, int) else "") + "]", end=": ") + if line: + eprint() + eprint('on line: "' + stringify(line).strip() + '"', end="") + eprint() + eprint("in element: " + str(self.elements[0]).replace("\n", "\\n")) + sys.tracebacklimit = 0 + raise self.__cause__ from None + + class OutputGenerator: _empty_lines: int context: Union[Context, None] @@ -101,28 +139,35 @@ class OutputGenerator: } def generate(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]]): - if isinstance(e, Group): - old_context = self.context - self.context = e.context - if isinstance(e, list): - self.generate_list(e) - elif isinstance(e, ListContainer): - self.generate_ListContainer(e) - elif isinstance(e, Inline): - self.generate_Inline(e) - elif isinstance(e, Block): - self.generate_Block(e) - elif isinstance(e, MetaValue): - self.generate_MetaValue(e) - elif isinstance(e, MetaList): - self.generate_MetaList(e) - else: - try: - self.TYPE_DICT_MISC[type(e)](e) - except KeyError: - raise UnknownElementError(type(e)) - if isinstance(e, Group): - self.context = old_context + try: + if isinstance(e, Group): + old_context = self.context + self.context = e.context + if isinstance(e, list): + self.generate_list(e) + elif isinstance(e, ListContainer): + self.generate_ListContainer(e) + elif isinstance(e, Inline): + self.generate_Inline(e) + elif isinstance(e, Block): + self.generate_Block(e) + elif isinstance(e, MetaValue): + self.generate_MetaValue(e) + elif isinstance(e, MetaList): + self.generate_MetaList(e) + else: + try: + self.TYPE_DICT_MISC[type(e)](e) + except KeyError as err: + raise UnknownElementError(type(e)) from err + if isinstance(e, Group): + self.context = old_context + except FormatitkoRecursiveError as err: + if not isinstance(e, ListContainer): + err.add_element(e) + raise err + except Exception as err: + raise FormatitkoRecursiveError(e, self.context) from err def escape_special_chars(self, text: str) -> str: return text diff --git a/src/formatitko/transform_processor.py b/src/formatitko/transform_processor.py index 32a0306..60cba5a 100644 --- a/src/formatitko/transform_processor.py +++ b/src/formatitko/transform_processor.py @@ -20,15 +20,10 @@ from .context import Context, CommandCallable from .whitespace import Whitespace, bavlna from .command import BlockCommand, InlineCommand, CodeCommand, Command from .command_util import handle_command_define, parse_command -from .nop_processor import NOPProcessor, ELCl - -class DoubleDocError(Exception): - "TransformProcessor should only ever see a single Doc." - pass +from .nop_processor import NOPProcessor, ELCl, DoubleDocError class TransformProcessor(NOPProcessor): - context: Union[Context, None] = None root_file_path: str root_highlight_style: str = "default" _command_modules: list[tuple[Union[dict[str, CommandCallable], ModuleType], str]] = [] diff --git a/test/test-files/test-partial.md b/test/test-files/test-partial.md index f1c9ab5..ee2fd6b 100644 --- a/test/test-files/test-partial.md +++ b/test/test-files/test-partial.md @@ -56,6 +56,15 @@ $$ $$ + + + + ![This is a figure, go figure...](logo.svg){width=25%}What ![This is a figure, go figure...](logo.pdf){width=50%}