From a7963ba824e684c4c1dbbca098f070a7a295637d Mon Sep 17 00:00:00 2001 From: Greenscreener Date: Sat, 6 Jan 2024 19:32:54 +0100 Subject: [PATCH 1/4] WIP: Experimental error handling with snippets of input for OutputGenerator. Would be nice to generalise for TransformProcessor, which is not easy as they don't have a common parent class. --- src/formatitko/html_generator.py | 16 ++-- src/formatitko/katex.py | 2 +- src/formatitko/latex_generator.py | 10 +-- src/formatitko/output_generator.py | 118 +++++++++++++++++++---------- src/formatitko/tex_generator.py | 56 +++++++------- 5 files changed, 120 insertions(+), 82 deletions(-) diff --git a/src/formatitko/html_generator.py b/src/formatitko/html_generator.py index d7dbc03..59db46d 100644 --- a/src/formatitko/html_generator.py +++ b/src/formatitko/html_generator.py @@ -29,10 +29,10 @@ class HTMLGenerator(OutputGenerator): self.imageProcessor = imageProcessor super().__init__(output_file, *args, **kwargs) - def generate(self, e: Union[Element, ListContainer]): + def _generate(self, e: Union[Element, ListContainer]): if hasattr(e, "attributes") and "only" in e.attributes and e.attributes["only"] != "html": return - super().generate(e) + super()._generate(e) def escape_special_chars(self, text: str) -> str: text = text.replace("&", "&") @@ -190,7 +190,7 @@ class HTMLGenerator(OutputGenerator): attributes["alt"] = e.title else: fake_out = io.StringIO() - HTMLGenerator(fake_out, self.katexClient, self.imageProcessor).generate(e.content) + HTMLGenerator(fake_out, self.katexClient, self.imageProcessor)._generate(e.content) attributes["alt"] = fake_out.getvalue() if len(srcset) != 0: @@ -202,7 +202,7 @@ class HTMLGenerator(OutputGenerator): img = RawInline(self.single_tag("img", attributes)) link = Link(img, url=url) - self.generate(link) + self._generate(link) def generate_InlineGroup(self, e: InlineGroup): self.generate_Group(e) @@ -216,10 +216,10 @@ class HTMLGenerator(OutputGenerator): self.katexClient.endgroup() def generate_Plain(self, e: Plain): - self.generate(e.content) + self._generate(e.content) def generate_LineItem(self, e: LineItem): - self.generate(e.content) + self._generate(e.content) self.write("
") self.endln() @@ -229,12 +229,12 @@ class HTMLGenerator(OutputGenerator): tag = self.tagname(e) if inline is not None: self.write(self.start_tag(tag)+" (") - self.generate(inline) + self._generate(inline) self.write(") "+self.end_tag(tag)) else: self.writeln(self.start_tag(tag) + "(") self.indent_more() - self.generate(e.content) + self._generate(e.content) self.indent_less() self.writeln(self.end_tag(tag) + ")") 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/latex_generator.py b/src/formatitko/latex_generator.py index 124cf11..31e0325 100644 --- a/src/formatitko/latex_generator.py +++ b/src/formatitko/latex_generator.py @@ -17,10 +17,10 @@ class LaTeXGenerator(OutputGenerator): self.imageProcessor = imageProcessor super().__init__(output_file, *args, **kwargs) - def generate(self, e: Union[Element, ListContainer]): + def _generate(self, e: Union[Element, ListContainer]): if hasattr(e, "attributes") and "only" in e.attributes and e.attributes["only"] != "tex": return - super().generate(e) + super()._generate(e) def escape_special_chars(self, text: str) -> str: text = text.replace("&", "\\&") @@ -58,14 +58,14 @@ class LaTeXGenerator(OutputGenerator): self.endln() def generate_Para(self, e: Para): - self.generate(e.content) + self._generate(e.content) self.writeln("") # This ensures an empty line def generate_Plain(self, e: Plain): - self.generate(e.content) + self._generate(e.content) def generate_Span(self, e: Plain): - self.generate(e.content) + self._generate(e.content) def generate_Header(self, e: Header): tag = { diff --git a/src/formatitko/output_generator.py b/src/formatitko/output_generator.py index 61df1c6..69db2cf 100644 --- a/src/formatitko/output_generator.py +++ b/src/formatitko/output_generator.py @@ -3,18 +3,31 @@ 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 re, sys class UnknownElementError(Exception): "An unknown Element has been passed to the OutputGenerator, probably because panflute introduced a new one." pass + +class OutputGeneratorError(Exception): + "A generic exception which wraps other exceptions and adds element-based traceback" + elements: list[Union[Element, ListContainer, list[Union[Element, ListContainer]]]] + + def __init__(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]], *args): + self.elements = [e] + super().__init__(args) + + def add_element(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]]): + self.elements.append(e) + class OutputGenerator: _empty_lines: int context: Union[Context, None] @@ -101,28 +114,53 @@ 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: + self._generate(e) + except OutputGeneratorError as err: + def eprint(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + eprint("Error occured in ", end="") + for i in range(len(err.elements)-1, 0, -1): + if hasattr(err.elements[i], "content") and isinstance(err.elements[i].content[0], Inline): + eprint() + eprint('on line: "' + stringify(err.elements[i]) + '"', end="") + break + eprint(type(err.elements[i]).__name__ + "[" + str(err.elements[i-1].index) + "]", end=": ") + eprint() + eprint("in element: " + str(err.elements[0])) + sys.tracebacklimit = 0 + raise err.__cause__ from None + + def _generate(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]]): + 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 OutputGeneratorError as err: + if not isinstance(e, ListContainer): + err.add_element(e) + raise err + except Exception as err: + raise OutputGeneratorError(e) from err def escape_special_chars(self, text: str) -> str: return text @@ -210,13 +248,13 @@ class OutputGenerator: def generate_simple_inline_tag(self, tag: str, content: Union[ListContainer, Element, list[Union[Element, ListContainer]]], attributes: dict[str,str]={}): self.write(self.start_tag(tag, attributes)) - self.generate(content) + self._generate(content) self.write(self.end_tag(tag)) def generate_simple_block_tag(self, tag: str, content: Union[ListContainer, Element, list[Union[Element, ListContainer]]], attributes: dict[str,str]={}): self.writeln(self.start_tag(tag, attributes)) self.indent_more() - self.generate(content) + self._generate(content) self.indent_less() self.writeln(self.end_tag(tag)) @@ -235,27 +273,27 @@ class OutputGenerator: def generate_ListContainer(self, e: ListContainer): for child in e: - self.generate(child) + self._generate(child) def generate_list(self, e: list): for el in e: - self.generate(el) + self._generate(el) def generate_MetaList(self, e: MetaList): for child in e: - self.generate(child) + self._generate(child) def generate_MetaValue(self, e: MetaValue): try: self.TYPE_DICT_META[type(e)](e) except KeyError: - self.generate(e.content) + self._generate(e.content) def generate_MetaBlocks(self, e: MetaBlocks): - self.generate(e.content) + self._generate(e.content) def generate_MetaInlines(self, e: MetaInlines): - self.generate(e.content) + self._generate(e.content) def generate_MetaBool(self, e: MetaBool): self.generate_simple_tag(e) @@ -285,33 +323,33 @@ class OutputGenerator: if e.style == "cs": if e.quote_type == "SingleQuote": self.write("‚") - self.generate(e.content) + self._generate(e.content) self.write("‘") elif e.quote_type == "DoubleQuote": self.write("„") - self.generate(e.content) + self._generate(e.content) self.write("“") elif e.style == "en": if e.quote_type == "SingleQuote": self.write("‘") - self.generate(e.content) + self._generate(e.content) self.write("’") elif e.quote_type == "DoubleQuote": self.write("“") - self.generate(e.content) + self._generate(e.content) self.write("”") else: if e.quote_type == "SingleQuote": self.write("'") - self.generate(e.content) + self._generate(e.content) self.write("'") elif e.quote_type == "DoubleQuote": self.write("\"") - self.generate(e.content) + self._generate(e.content) self.write("\"") else: self.write("\"") - self.generate(e.content) + self._generate(e.content) self.write("\"") @@ -434,10 +472,10 @@ class OutputGenerator: def generate_Doc(self, e: Doc): if "header_content" in e.metadata: - self.generate(e.metadata["header_content"]) + self._generate(e.metadata["header_content"]) self.generate_simple_tag(e) if "footer_content" in e.metadata: - self.generate(e.metadata["footer_content"]) + self._generate(e.metadata["footer_content"]) def generate_BlockGroup(self, e: BlockGroup): self.generate_simple_tag(e) diff --git a/src/formatitko/tex_generator.py b/src/formatitko/tex_generator.py index c97b90d..a1f2268 100644 --- a/src/formatitko/tex_generator.py +++ b/src/formatitko/tex_generator.py @@ -41,10 +41,10 @@ class UCWTexGenerator(OutputGenerator): text = text.replace("​", "") return text - def generate(self, e: Union[Element, ListContainer]): + def _generate(self, e: Union[Element, ListContainer]): if hasattr(e, "attributes") and "only" in e.attributes and e.attributes["only"] != "tex": return - super().generate(e) + super()._generate(e) def writepar(self, text: str): self.ensure_empty(2) @@ -60,7 +60,7 @@ class UCWTexGenerator(OutputGenerator): def generate_Para(self, e: Para): self.ensure_empty(2) - self.generate(e.content) + self._generate(e.content) self.ensure_empty(2) def generate_HorizontalRule(self, e: HorizontalRule): @@ -72,7 +72,7 @@ class UCWTexGenerator(OutputGenerator): self.writeln(r"\ucwmodule{verb}") self.writeln(r"\ucwmodule{link}") self.writeln(r"\input formatitko.tex") - self.generate(e.content) + self._generate(e.content) self.writeln(r"\bye") def get_language_macro(self, lang: str): @@ -88,21 +88,21 @@ class UCWTexGenerator(OutputGenerator): def generate_InlineGroup(self, e: InlineGroup): self.write(r"{") self.write(self.get_language_macro(self.context.get_metadata("lang"))) - self.generate(e.content) + self._generate(e.content) self.write(r"}") def generate_BlockGroup(self, e: BlockGroup): self.writeln(r"\begingroup") self.indent_more() self.writeln(self.get_language_macro(self.context.get_metadata("lang"))) - self.generate(e.content) + self._generate(e.content) self.indent_less() self.writeln(r"\endgroup") def generate_Header(self, e: Header): self.ensure_empty(2) self.write("\\"+"sub"*(e.level-1)+"section{") - self.generate(e.content) + self._generate(e.content) self.write(r"}") self.ensure_empty(2) @@ -156,12 +156,12 @@ class UCWTexGenerator(OutputGenerator): self.writeln(r"\vskip5pt") self.writeln(r"\centerline{") self.indent_more() - self.generate(e.content) + self._generate(e.content) self.indent_less() self.writeln(r"}") self.writeln(r"\centerline{") self.indent_more() - self.generate(e.caption) + self._generate(e.caption) self.indent_less() self.writeln(r"}") self.writeln(r"\vskip5pt{}") @@ -173,7 +173,7 @@ class UCWTexGenerator(OutputGenerator): else: self.write(r"{\I{}") self._italic+=1 - self.generate(e.content) + self._generate(e.content) self._italic-=1 self.write(r"}") @@ -183,7 +183,7 @@ class UCWTexGenerator(OutputGenerator): else: self.write(r"{\bf{}") self._bold+=1 - self.generate(e.content) + self._generate(e.content) self._bold-=1 self.write(r"}") @@ -204,7 +204,7 @@ class UCWTexGenerator(OutputGenerator): def generate_Note(self, e: Note): self.write(r"\fn{") - self.generate(inlinify(e)) + self._generate(inlinify(e)) self.write(r"}") def generate_Table(self, e: Table): @@ -217,11 +217,11 @@ class UCWTexGenerator(OutputGenerator): self.writeln(r"\vskip1em") self.writeln(r"\halign{\strut"+"&".join([aligns[col[0]] for col in e.colspec])+r"\cr") self.indent_more() - self.generate(e.head.content) + self._generate(e.head.content) self.writeln(r"\noalign{\hrule}") - self.generate(e.content[0].content) + self._generate(e.content[0].content) self.writeln(r"\noalign{\hrule}") - self.generate(e.foot.content) + self._generate(e.foot.content) self.indent_less() self.writeln("}") self.writeln(r"\vskip1em") @@ -230,7 +230,7 @@ class UCWTexGenerator(OutputGenerator): for cell in e.content: if cell.colspan > 1: self.write(r"\multispan"+str(cell.colspan)+"{} ") - self.generate(cell.content) + self._generate(cell.content) if cell.next: self.write(" & ") self.write(r"\cr") @@ -245,10 +245,10 @@ class UCWTexGenerator(OutputGenerator): self.writeraw(e.text) def generate_Plain(self, e: Plain): - self.generate(e.content) + self._generate(e.content) def generate_Span(self, e: Span): - self.generate(e.content) + self._generate(e.content) def generate_CodeBlock(self, e: CodeBlock): self.writeln(r"\verbatim{") @@ -256,15 +256,15 @@ class UCWTexGenerator(OutputGenerator): self.writeln(r"}") def generate_Div(self, e: Div): - self.generate(e.content) + self._generate(e.content) def generate_LineBlock(self, e: LineBlock): self.writeln() - self.generate(e.content) + self._generate(e.content) self.writeln() def generate_LineItem(self, e: LineItem): - self.generate(e.content) + self._generate(e.content) if e.next: self.write(r"\\") self.endln() @@ -273,7 +273,7 @@ class UCWTexGenerator(OutputGenerator): self.ensure_empty(2) self.writeln(r"\list{o}") self.indent_more() - self.generate(e.content) + self._generate(e.content) self.indent_less() self.write(r"\endlist") self.ensure_empty(2) @@ -298,7 +298,7 @@ class UCWTexGenerator(OutputGenerator): style = delimiters[e.delimiter] self.writeln(r"\list{"+style+r"}") self.indent_more() - self.generate(e.content) + self._generate(e.content) self.indent_less() self.writeln(r"\endlist") self.ensure_empty(2) @@ -306,13 +306,13 @@ class UCWTexGenerator(OutputGenerator): def generate_ListItem(self, e: ListItem): self.endln() self.write(r"\:") - self.generate(e.content) + self._generate(e.content) self.endln() def generate_BlockQuote(self, e: BlockQuote): self.writeln(r"\blockquote{") self.indent_more() - self.generate(e.content) + self._generate(e.content) self.indent_less() self.writeln(r"}") @@ -321,17 +321,17 @@ class UCWTexGenerator(OutputGenerator): self.write(r"\url{") else: self.write(r"\linkurl{"+e.url+r"}{") - self.generate(e.content) + self._generate(e.content) self.write(r"}") # } def generate_Subscript(self, e: Subscript): self.write(r"\subscript{") - self.generate(e.content) + self._generate(e.content) self.write(r"}") def generate_Superscript(self, e: Superscript): self.write(r"\superscript{") - self.generate(e.content) + self._generate(e.content) self.write(r"}") def generate_simple_tag(self, e: Union[Element, None] = None, tag: str = "", attributes: Union[dict[str, str], None] = None, content: Union[ListContainer, Element, list[Union[Element, ListContainer]], str, None] = None, inline: Union[bool, None] = None): From 42a63b3163a1bb406aab3c6d7a85446844cb3fea Mon Sep 17 00:00:00 2001 From: Greenscreener Date: Thu, 15 Feb 2024 17:43:10 +0100 Subject: [PATCH 2/4] Partial rewrite of error handling Now the error doesn't handle itself, but offers a helper function to do it. --- src/formatitko/formatitko.py | 33 ++++++++++--- src/formatitko/html_generator.py | 16 +++---- src/formatitko/latex_generator.py | 10 ++-- src/formatitko/nop_processor.py | 40 +++++++++------- src/formatitko/output_generator.py | 77 +++++++++++++++--------------- src/formatitko/tex_generator.py | 56 +++++++++++----------- 6 files changed, 129 insertions(+), 103 deletions(-) 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/html_generator.py b/src/formatitko/html_generator.py index 59db46d..d7dbc03 100644 --- a/src/formatitko/html_generator.py +++ b/src/formatitko/html_generator.py @@ -29,10 +29,10 @@ class HTMLGenerator(OutputGenerator): self.imageProcessor = imageProcessor super().__init__(output_file, *args, **kwargs) - def _generate(self, e: Union[Element, ListContainer]): + def generate(self, e: Union[Element, ListContainer]): if hasattr(e, "attributes") and "only" in e.attributes and e.attributes["only"] != "html": return - super()._generate(e) + super().generate(e) def escape_special_chars(self, text: str) -> str: text = text.replace("&", "&") @@ -190,7 +190,7 @@ class HTMLGenerator(OutputGenerator): attributes["alt"] = e.title else: fake_out = io.StringIO() - HTMLGenerator(fake_out, self.katexClient, self.imageProcessor)._generate(e.content) + HTMLGenerator(fake_out, self.katexClient, self.imageProcessor).generate(e.content) attributes["alt"] = fake_out.getvalue() if len(srcset) != 0: @@ -202,7 +202,7 @@ class HTMLGenerator(OutputGenerator): img = RawInline(self.single_tag("img", attributes)) link = Link(img, url=url) - self._generate(link) + self.generate(link) def generate_InlineGroup(self, e: InlineGroup): self.generate_Group(e) @@ -216,10 +216,10 @@ class HTMLGenerator(OutputGenerator): self.katexClient.endgroup() def generate_Plain(self, e: Plain): - self._generate(e.content) + self.generate(e.content) def generate_LineItem(self, e: LineItem): - self._generate(e.content) + self.generate(e.content) self.write("
") self.endln() @@ -229,12 +229,12 @@ class HTMLGenerator(OutputGenerator): tag = self.tagname(e) if inline is not None: self.write(self.start_tag(tag)+" (") - self._generate(inline) + self.generate(inline) self.write(") "+self.end_tag(tag)) else: self.writeln(self.start_tag(tag) + "(") self.indent_more() - self._generate(e.content) + self.generate(e.content) self.indent_less() self.writeln(self.end_tag(tag) + ")") diff --git a/src/formatitko/latex_generator.py b/src/formatitko/latex_generator.py index 31e0325..124cf11 100644 --- a/src/formatitko/latex_generator.py +++ b/src/formatitko/latex_generator.py @@ -17,10 +17,10 @@ class LaTeXGenerator(OutputGenerator): self.imageProcessor = imageProcessor super().__init__(output_file, *args, **kwargs) - def _generate(self, e: Union[Element, ListContainer]): + def generate(self, e: Union[Element, ListContainer]): if hasattr(e, "attributes") and "only" in e.attributes and e.attributes["only"] != "tex": return - super()._generate(e) + super().generate(e) def escape_special_chars(self, text: str) -> str: text = text.replace("&", "\\&") @@ -58,14 +58,14 @@ class LaTeXGenerator(OutputGenerator): self.endln() def generate_Para(self, e: Para): - self._generate(e.content) + self.generate(e.content) self.writeln("") # This ensures an empty line def generate_Plain(self, e: Plain): - self._generate(e.content) + self.generate(e.content) def generate_Span(self, e: Plain): - self._generate(e.content) + self.generate(e.content) def generate_Header(self, e: Header): tag = { diff --git a/src/formatitko/nop_processor.py b/src/formatitko/nop_processor.py index 2f09ffb..5a89228 100644 --- a/src/formatitko/nop_processor.py +++ b/src/formatitko/nop_processor.py @@ -10,6 +10,7 @@ from .elements import FQuoted from .context import Group, InlineGroup, BlockGroup from .whitespace import Whitespace from .command import BlockCommand, InlineCommand, CodeCommand, Command +from .output_generator import FormatitkoRecursiveError ELCl = Union[Element, ListContainer, list[Union[Element, ListContainer]]] @@ -96,23 +97,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) from err def transform_list(self, e: list[Union[Element, ListContainer]]) -> list[Union[Element, ListContainer]]: for i in range(len(e)): diff --git a/src/formatitko/output_generator.py b/src/formatitko/output_generator.py index 69db2cf..9869b87 100644 --- a/src/formatitko/output_generator.py +++ b/src/formatitko/output_generator.py @@ -10,14 +10,15 @@ from .whitespace import NBSP from .elements import FQuoted from .context import Group, InlineGroup, BlockGroup, Context -import re, sys + +import sys class UnknownElementError(Exception): "An unknown Element has been passed to the OutputGenerator, probably because panflute introduced a new one." pass -class OutputGeneratorError(Exception): +class FormatitkoRecursiveError(Exception): "A generic exception which wraps other exceptions and adds element-based traceback" elements: list[Union[Element, ListContainer, list[Union[Element, ListContainer]]]] @@ -28,6 +29,22 @@ class OutputGeneratorError(Exception): 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) + eprint("Error occured in ", end="") + for i in range(len(self.elements)-1, 0, -1): + if hasattr(self.elements[i], "content") and isinstance(self.elements[i].content[0], Inline): + eprint() + eprint('on line: "' + stringify(self.elements[i]) + '"', end="") + break + eprint(type(self.elements[i]).__name__ + "[" + str(self.elements[i-1].index) + "]", end=": ") + eprint() + eprint("in element: " + str(self.elements[0])) + sys.tracebacklimit = 0 + raise self.__cause__ from None + + class OutputGenerator: _empty_lines: int context: Union[Context, None] @@ -114,24 +131,6 @@ class OutputGenerator: } def generate(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]]): - try: - self._generate(e) - except OutputGeneratorError as err: - def eprint(*args, **kwargs): - print(*args, file=sys.stderr, **kwargs) - eprint("Error occured in ", end="") - for i in range(len(err.elements)-1, 0, -1): - if hasattr(err.elements[i], "content") and isinstance(err.elements[i].content[0], Inline): - eprint() - eprint('on line: "' + stringify(err.elements[i]) + '"', end="") - break - eprint(type(err.elements[i]).__name__ + "[" + str(err.elements[i-1].index) + "]", end=": ") - eprint() - eprint("in element: " + str(err.elements[0])) - sys.tracebacklimit = 0 - raise err.__cause__ from None - - def _generate(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]]): try: if isinstance(e, Group): old_context = self.context @@ -155,12 +154,12 @@ class OutputGenerator: raise UnknownElementError(type(e)) from err if isinstance(e, Group): self.context = old_context - except OutputGeneratorError as err: + except FormatitkoRecursiveError as err: if not isinstance(e, ListContainer): err.add_element(e) raise err except Exception as err: - raise OutputGeneratorError(e) from err + raise FormatitkoRecursiveError(e) from err def escape_special_chars(self, text: str) -> str: return text @@ -248,13 +247,13 @@ class OutputGenerator: def generate_simple_inline_tag(self, tag: str, content: Union[ListContainer, Element, list[Union[Element, ListContainer]]], attributes: dict[str,str]={}): self.write(self.start_tag(tag, attributes)) - self._generate(content) + self.generate(content) self.write(self.end_tag(tag)) def generate_simple_block_tag(self, tag: str, content: Union[ListContainer, Element, list[Union[Element, ListContainer]]], attributes: dict[str,str]={}): self.writeln(self.start_tag(tag, attributes)) self.indent_more() - self._generate(content) + self.generate(content) self.indent_less() self.writeln(self.end_tag(tag)) @@ -273,27 +272,27 @@ class OutputGenerator: def generate_ListContainer(self, e: ListContainer): for child in e: - self._generate(child) + self.generate(child) def generate_list(self, e: list): for el in e: - self._generate(el) + self.generate(el) def generate_MetaList(self, e: MetaList): for child in e: - self._generate(child) + self.generate(child) def generate_MetaValue(self, e: MetaValue): try: self.TYPE_DICT_META[type(e)](e) except KeyError: - self._generate(e.content) + self.generate(e.content) def generate_MetaBlocks(self, e: MetaBlocks): - self._generate(e.content) + self.generate(e.content) def generate_MetaInlines(self, e: MetaInlines): - self._generate(e.content) + self.generate(e.content) def generate_MetaBool(self, e: MetaBool): self.generate_simple_tag(e) @@ -323,33 +322,33 @@ class OutputGenerator: if e.style == "cs": if e.quote_type == "SingleQuote": self.write("‚") - self._generate(e.content) + self.generate(e.content) self.write("‘") elif e.quote_type == "DoubleQuote": self.write("„") - self._generate(e.content) + self.generate(e.content) self.write("“") elif e.style == "en": if e.quote_type == "SingleQuote": self.write("‘") - self._generate(e.content) + self.generate(e.content) self.write("’") elif e.quote_type == "DoubleQuote": self.write("“") - self._generate(e.content) + self.generate(e.content) self.write("”") else: if e.quote_type == "SingleQuote": self.write("'") - self._generate(e.content) + self.generate(e.content) self.write("'") elif e.quote_type == "DoubleQuote": self.write("\"") - self._generate(e.content) + self.generate(e.content) self.write("\"") else: self.write("\"") - self._generate(e.content) + self.generate(e.content) self.write("\"") @@ -472,10 +471,10 @@ class OutputGenerator: def generate_Doc(self, e: Doc): if "header_content" in e.metadata: - self._generate(e.metadata["header_content"]) + self.generate(e.metadata["header_content"]) self.generate_simple_tag(e) if "footer_content" in e.metadata: - self._generate(e.metadata["footer_content"]) + self.generate(e.metadata["footer_content"]) def generate_BlockGroup(self, e: BlockGroup): self.generate_simple_tag(e) diff --git a/src/formatitko/tex_generator.py b/src/formatitko/tex_generator.py index a1f2268..c97b90d 100644 --- a/src/formatitko/tex_generator.py +++ b/src/formatitko/tex_generator.py @@ -41,10 +41,10 @@ class UCWTexGenerator(OutputGenerator): text = text.replace("​", "") return text - def _generate(self, e: Union[Element, ListContainer]): + def generate(self, e: Union[Element, ListContainer]): if hasattr(e, "attributes") and "only" in e.attributes and e.attributes["only"] != "tex": return - super()._generate(e) + super().generate(e) def writepar(self, text: str): self.ensure_empty(2) @@ -60,7 +60,7 @@ class UCWTexGenerator(OutputGenerator): def generate_Para(self, e: Para): self.ensure_empty(2) - self._generate(e.content) + self.generate(e.content) self.ensure_empty(2) def generate_HorizontalRule(self, e: HorizontalRule): @@ -72,7 +72,7 @@ class UCWTexGenerator(OutputGenerator): self.writeln(r"\ucwmodule{verb}") self.writeln(r"\ucwmodule{link}") self.writeln(r"\input formatitko.tex") - self._generate(e.content) + self.generate(e.content) self.writeln(r"\bye") def get_language_macro(self, lang: str): @@ -88,21 +88,21 @@ class UCWTexGenerator(OutputGenerator): def generate_InlineGroup(self, e: InlineGroup): self.write(r"{") self.write(self.get_language_macro(self.context.get_metadata("lang"))) - self._generate(e.content) + self.generate(e.content) self.write(r"}") def generate_BlockGroup(self, e: BlockGroup): self.writeln(r"\begingroup") self.indent_more() self.writeln(self.get_language_macro(self.context.get_metadata("lang"))) - self._generate(e.content) + self.generate(e.content) self.indent_less() self.writeln(r"\endgroup") def generate_Header(self, e: Header): self.ensure_empty(2) self.write("\\"+"sub"*(e.level-1)+"section{") - self._generate(e.content) + self.generate(e.content) self.write(r"}") self.ensure_empty(2) @@ -156,12 +156,12 @@ class UCWTexGenerator(OutputGenerator): self.writeln(r"\vskip5pt") self.writeln(r"\centerline{") self.indent_more() - self._generate(e.content) + self.generate(e.content) self.indent_less() self.writeln(r"}") self.writeln(r"\centerline{") self.indent_more() - self._generate(e.caption) + self.generate(e.caption) self.indent_less() self.writeln(r"}") self.writeln(r"\vskip5pt{}") @@ -173,7 +173,7 @@ class UCWTexGenerator(OutputGenerator): else: self.write(r"{\I{}") self._italic+=1 - self._generate(e.content) + self.generate(e.content) self._italic-=1 self.write(r"}") @@ -183,7 +183,7 @@ class UCWTexGenerator(OutputGenerator): else: self.write(r"{\bf{}") self._bold+=1 - self._generate(e.content) + self.generate(e.content) self._bold-=1 self.write(r"}") @@ -204,7 +204,7 @@ class UCWTexGenerator(OutputGenerator): def generate_Note(self, e: Note): self.write(r"\fn{") - self._generate(inlinify(e)) + self.generate(inlinify(e)) self.write(r"}") def generate_Table(self, e: Table): @@ -217,11 +217,11 @@ class UCWTexGenerator(OutputGenerator): self.writeln(r"\vskip1em") self.writeln(r"\halign{\strut"+"&".join([aligns[col[0]] for col in e.colspec])+r"\cr") self.indent_more() - self._generate(e.head.content) + self.generate(e.head.content) self.writeln(r"\noalign{\hrule}") - self._generate(e.content[0].content) + self.generate(e.content[0].content) self.writeln(r"\noalign{\hrule}") - self._generate(e.foot.content) + self.generate(e.foot.content) self.indent_less() self.writeln("}") self.writeln(r"\vskip1em") @@ -230,7 +230,7 @@ class UCWTexGenerator(OutputGenerator): for cell in e.content: if cell.colspan > 1: self.write(r"\multispan"+str(cell.colspan)+"{} ") - self._generate(cell.content) + self.generate(cell.content) if cell.next: self.write(" & ") self.write(r"\cr") @@ -245,10 +245,10 @@ class UCWTexGenerator(OutputGenerator): self.writeraw(e.text) def generate_Plain(self, e: Plain): - self._generate(e.content) + self.generate(e.content) def generate_Span(self, e: Span): - self._generate(e.content) + self.generate(e.content) def generate_CodeBlock(self, e: CodeBlock): self.writeln(r"\verbatim{") @@ -256,15 +256,15 @@ class UCWTexGenerator(OutputGenerator): self.writeln(r"}") def generate_Div(self, e: Div): - self._generate(e.content) + self.generate(e.content) def generate_LineBlock(self, e: LineBlock): self.writeln() - self._generate(e.content) + self.generate(e.content) self.writeln() def generate_LineItem(self, e: LineItem): - self._generate(e.content) + self.generate(e.content) if e.next: self.write(r"\\") self.endln() @@ -273,7 +273,7 @@ class UCWTexGenerator(OutputGenerator): self.ensure_empty(2) self.writeln(r"\list{o}") self.indent_more() - self._generate(e.content) + self.generate(e.content) self.indent_less() self.write(r"\endlist") self.ensure_empty(2) @@ -298,7 +298,7 @@ class UCWTexGenerator(OutputGenerator): style = delimiters[e.delimiter] self.writeln(r"\list{"+style+r"}") self.indent_more() - self._generate(e.content) + self.generate(e.content) self.indent_less() self.writeln(r"\endlist") self.ensure_empty(2) @@ -306,13 +306,13 @@ class UCWTexGenerator(OutputGenerator): def generate_ListItem(self, e: ListItem): self.endln() self.write(r"\:") - self._generate(e.content) + self.generate(e.content) self.endln() def generate_BlockQuote(self, e: BlockQuote): self.writeln(r"\blockquote{") self.indent_more() - self._generate(e.content) + self.generate(e.content) self.indent_less() self.writeln(r"}") @@ -321,17 +321,17 @@ class UCWTexGenerator(OutputGenerator): self.write(r"\url{") else: self.write(r"\linkurl{"+e.url+r"}{") - self._generate(e.content) + self.generate(e.content) self.write(r"}") # } def generate_Subscript(self, e: Subscript): self.write(r"\subscript{") - self._generate(e.content) + self.generate(e.content) self.write(r"}") def generate_Superscript(self, e: Superscript): self.write(r"\superscript{") - self._generate(e.content) + self.generate(e.content) self.write(r"}") def generate_simple_tag(self, e: Union[Element, None] = None, tag: str = "", attributes: Union[dict[str, str], None] = None, content: Union[ListContainer, Element, list[Union[Element, ListContainer]], str, None] = None, inline: Union[bool, None] = None): From 6de4ea2743ca86f044b585c5c4ddf68704102868 Mon Sep 17 00:00:00 2001 From: Greenscreener Date: Thu, 15 Feb 2024 18:19:10 +0100 Subject: [PATCH 3/4] Error handling now contains filename. --- src/formatitko/nop_processor.py | 11 +++++++++-- src/formatitko/output_generator.py | 8 +++++--- src/formatitko/transform_processor.py | 7 +------ 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/formatitko/nop_processor.py b/src/formatitko/nop_processor.py index 5a89228..4cffa90 100644 --- a/src/formatitko/nop_processor.py +++ b/src/formatitko/nop_processor.py @@ -7,16 +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." @@ -120,7 +124,7 @@ class NOPProcessor: err.add_element(e) raise err except Exception as err: - raise FormatitkoRecursiveError(e) from err + raise FormatitkoRecursiveError(e, self.context.filename) from err def transform_list(self, e: list[Union[Element, ListContainer]]) -> list[Union[Element, ListContainer]]: for i in range(len(e)): @@ -301,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 9869b87..46431f7 100644 --- a/src/formatitko/output_generator.py +++ b/src/formatitko/output_generator.py @@ -21,9 +21,11 @@ class UnknownElementError(Exception): class FormatitkoRecursiveError(Exception): "A generic exception which wraps other exceptions and adds element-based traceback" elements: list[Union[Element, ListContainer, list[Union[Element, ListContainer]]]] + file: str - def __init__(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]], *args): + def __init__(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]], file: str, *args): self.elements = [e] + self.file = file super().__init__(args) def add_element(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]]): @@ -32,7 +34,7 @@ class FormatitkoRecursiveError(Exception): def pretty_print(self): def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) - eprint("Error occured in ", end="") + eprint(f"Error occured in file {self.file} in ", end="") for i in range(len(self.elements)-1, 0, -1): if hasattr(self.elements[i], "content") and isinstance(self.elements[i].content[0], Inline): eprint() @@ -159,7 +161,7 @@ class OutputGenerator: err.add_element(e) raise err except Exception as err: - raise FormatitkoRecursiveError(e) from err + raise FormatitkoRecursiveError(e, self.context.filename) 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]] = [] From 50b29b1ae35ad2f9ebc71b5b0bb618869236c593 Mon Sep 17 00:00:00 2001 From: Greenscreener Date: Sat, 17 Feb 2024 18:07:47 +0100 Subject: [PATCH 4/4] Improved error messages --- src/formatitko/nop_processor.py | 2 +- src/formatitko/output_generator.py | 28 +++++++++++++++++----------- test/test-files/test-partial.md | 9 +++++++++ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/formatitko/nop_processor.py b/src/formatitko/nop_processor.py index 4cffa90..bc20a2a 100644 --- a/src/formatitko/nop_processor.py +++ b/src/formatitko/nop_processor.py @@ -124,7 +124,7 @@ class NOPProcessor: err.add_element(e) raise err except Exception as err: - raise FormatitkoRecursiveError(e, self.context.filename) from 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)): diff --git a/src/formatitko/output_generator.py b/src/formatitko/output_generator.py index 46431f7..4f3cd24 100644 --- a/src/formatitko/output_generator.py +++ b/src/formatitko/output_generator.py @@ -21,11 +21,11 @@ class UnknownElementError(Exception): class FormatitkoRecursiveError(Exception): "A generic exception which wraps other exceptions and adds element-based traceback" elements: list[Union[Element, ListContainer, list[Union[Element, ListContainer]]]] - file: str + context: Context - def __init__(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]], file: str, *args): + def __init__(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]], context: Context, *args): self.elements = [e] - self.file = file + self.context = context super().__init__(args) def add_element(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]]): @@ -34,15 +34,21 @@ class FormatitkoRecursiveError(Exception): def pretty_print(self): def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) - eprint(f"Error occured in file {self.file} in ", end="") + + 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 isinstance(self.elements[i].content[0], Inline): - eprint() - eprint('on line: "' + stringify(self.elements[i]) + '"', end="") - break - eprint(type(self.elements[i]).__name__ + "[" + str(self.elements[i-1].index) + "]", end=": ") + 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])) + eprint("in element: " + str(self.elements[0]).replace("\n", "\\n")) sys.tracebacklimit = 0 raise self.__cause__ from None @@ -161,7 +167,7 @@ class OutputGenerator: err.add_element(e) raise err except Exception as err: - raise FormatitkoRecursiveError(e, self.context.filename) from err + raise FormatitkoRecursiveError(e, self.context) from err def escape_special_chars(self, text: str) -> str: return text 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%}