diff --git a/src/formatitko/command.py b/src/formatitko/command.py index 99ff04f..249e080 100644 --- a/src/formatitko/command.py +++ b/src/formatitko/command.py @@ -1,8 +1,9 @@ -from panflute import Div, Span, Para, Element +from panflute import Span, Div, Element, Plain, Para -# Import local files -from .util import nullify, import_md -from .context import Context +from .util import inlinify + +class InlineError(Exception): + pass class Command: pass @@ -14,10 +15,10 @@ class InlineCommand(Span, Command): try: return Span(*content) except TypeError: - if len(content) == 1 and isinstance(content[0], Para): - return Span(*content[0].content) + if inlinify(content): + return Span(inlinify(content)) else: - raise SyntaxError(f"The command {self.attributes['c']} returned multiple Paragraphs and must be executed using `::: {{c={self.attributes['c']}}}\\n:::`.") + raise InlineError(f"The command {self.attributes['c']} returned multiple Paragraphs and must be executed using `::: {{c={self.attributes['c']}}}\\n:::`.\n\n{content}") pass class BlockCommand(Div, Command): @@ -25,69 +26,3 @@ class BlockCommand(Div, Command): return Div(*content) pass -# This function is called in trasform.py, defining a command which can be -# called later using the function below -def handle_command_define(e: Element, c: Context): - if "define" in e.attributes: - if not c.get_command(e.attributes["define"]): - c.set_command(e.attributes["define"], compile(e.text, '', 'exec')) - return nullify(e) - else: - raise NameError(f"Command already defined: '{e.attributes['define']}'") - if "redefine" in e.attributes: - c.set_command(e.attributes["redefine"], compile(e.text, '', 'exec')) - return nullify(e) - return e - -# This function executes commands and inline runnable code blocks (see -# transform.py for their syntax). Context can be accessed using `ctx` and there -# are four functions available to create output from these commands and the -# element the command has been called on (including its .content) can be -# accessed using `element`. Arguments can be passed down to the comand using -# the element's attributes. -# -# print and println append text to a buffer which is then interpreted as -# markdown with the current context. -# -# appendChild and appendChildren append panflute elements to a list which is -# then transformed. A command which does nothing looks like this: -# ```python {define=nop} -# appendChildren(element.content) -# ``` -# -# These two types, appending and printing, cannot be mixed. - -def executeCommand(source, element: Element, ctx: Context) -> list[Element]: - mode = 'empty' - text = "" - content = [] - def print(s: str=""): - nonlocal mode, text - if mode == 'elements': - raise SyntaxError("Cannot use `print` and `appendChild` in one command at the same time.") - mode = 'text' - text += str(s) - - def println(s: str=""): - print(str(s)+"\n") - - def appendChild(e: Element): - nonlocal mode, content - if mode == 'text': - raise SyntaxError("Cannot use `print` and `appendChild` in one command at the same time.") - mode = 'elements' - content.append(e) - - def appendChildren(l: list[Element]): - for e in l: - appendChild(e) - - import panflute as pf - exec(source) - - if mode == 'text': - return import_md(text, standalone=False) - if mode == 'elements': - return content - - return [] diff --git a/src/formatitko/command_env.py b/src/formatitko/command_env.py new file mode 100644 index 0000000..15529ff --- /dev/null +++ b/src/formatitko/command_env.py @@ -0,0 +1,7 @@ +import panflute as pf +import formatitko.elements as fe +from formatitko.util import import_md_list + +from formatitko.context import Context +from formatitko.command import Command +from panflute import Element diff --git a/src/formatitko/command_util.py b/src/formatitko/command_util.py new file mode 100644 index 0000000..d68815e --- /dev/null +++ b/src/formatitko/command_util.py @@ -0,0 +1,37 @@ +from .context import Context, CommandCallable # This is there because of a wild circular import dependency between many functions and classes +from panflute import CodeBlock + + +from . import command_env +from .util import nullify + +def parse_command(code: str) -> CommandCallable: + code_lines = code.split("\n") + tabs = False + for line in code_lines: + if len(line) != 0 and line[0] == "\t": + tabs = True + break + indented_code_lines = [] + for line in code_lines: + indented_code_lines.append(("\t" if tabs else " ")+line) + code = "def command(element: Command, context: Context) -> list[Element]:\n"+"\n".join(indented_code_lines) + globals = command_env.__dict__ + exec(code, globals) + return globals["command"] + +# This function is called in trasform.py, defining a command which can be +# called later +def handle_command_define(e: CodeBlock, c: Context): + command = parse_command(e.text) + if "define" in e.attributes: + if not c.get_command(e.attributes["define"]): + c.set_command(e.attributes["define"], command) + return nullify(e) + else: + raise NameError(f"Command already defined: '{e.attributes['define']}'") + if "redefine" in e.attributes: + c.set_command(e.attributes["redefine"], command) + return nullify(e) + return e + diff --git a/src/formatitko/context.py b/src/formatitko/context.py index 24f98ee..96e112e 100644 --- a/src/formatitko/context.py +++ b/src/formatitko/context.py @@ -1,8 +1,14 @@ +from panflute import Doc, Element, Div -from panflute import Doc, Div +from typing import Union, Callable +from types import ModuleType import os import warnings +from .command import Command + +CommandCallable = Callable[[Command, 'Context'], list[Element]] # This is here because of a wild circular import dependency between many functions and classes + # This class is used to keep state while transforming the document using # transform.py. For the context to be available to the html and TeX generators, # individual keys must be manually assigned to the individual elements. This is @@ -15,7 +21,7 @@ import warnings # This class is basically an extension to panflute's doc, this is why metadata # is read directly from it. class Context: - def __init__(self, doc: Doc, path: str, parent: 'Context'=None, trusted: bool=True): + def __init__(self, doc: Doc, path: str, parent: Union['Context', None]=None, trusted: bool=True): self.parent = parent self._commands = {} self.doc = doc @@ -26,7 +32,7 @@ class Context: if self.get_metadata("flags", immediate=True) is None: self.set_metadata("flags", {}) - def get_command(self, command: str): + def get_command(self, command: str) -> Union[CommandCallable, None]: if command in self._commands: return self._commands[command] elif self.parent: @@ -34,12 +40,18 @@ class Context: else: return None - def set_command(self, command: str, val): + def set_command(self, command: str, val: CommandCallable): self._commands[command] = val - + def unset_command(self, command: str): del self._commands[command] + def add_commands_from_module(self, module: ModuleType, module_name: str=""): + prefix = module_name+"." if module_name else "" + for name, func in module.__dict__.items(): + if isinstance(func, CommandCallable): + self.set_command(prefix+name, func) + def is_flag_set(self, flag: str): if self.get_metadata("flags."+flag): if self.get_metadata("flags."+flag): @@ -74,7 +86,7 @@ class Context: for k in keys[:-1]: meta = meta[k] meta[keys[-1]] = value - + def unset_metadata(self, key: str): meta = self.doc.metadata keys = key.split(".") @@ -91,3 +103,4 @@ class Group(Div): def __init__(self, *args, metadata={}, **kwargs): self.metadata = metadata super().__init__(*args, **kwargs) + diff --git a/src/formatitko/elements.py b/src/formatitko/elements.py new file mode 100644 index 0000000..25aea17 --- /dev/null +++ b/src/formatitko/elements.py @@ -0,0 +1,15 @@ +from panflute import Quoted + + +from .command import Command, InlineCommand, BlockCommand +from .context import Group +from .whitespace import Whitespace, NBSP + +# This is a small extension to the Quoted panflute elements which allows to +# have language-aware quotation marks. +class FQuoted(Quoted): + def __init__(self, *args, **kwargs): + self.style = kwargs["style"] + del kwargs["style"] + super().__init__(*args, **kwargs) + diff --git a/src/formatitko/formatitko.py b/src/formatitko/formatitko.py index 1d0d3ca..03dfe49 100755 --- a/src/formatitko/formatitko.py +++ b/src/formatitko/formatitko.py @@ -58,6 +58,7 @@ def main(): #open(args.output_html, "w").write(html(doc, katexClient, imageProcessor)) #open(args.output_tex, "w").write(tex(doc, imageProcessor)) HTMLGenerator(sys.stdout, katexClient, imageProcessor).generate(doc) + # OutputGenerator(sys.stdout).generate(doc) if args.debug: print(show(doc)) diff --git a/src/formatitko/html.py b/src/formatitko/html.py index 28e408b..0403082 100644 --- a/src/formatitko/html.py +++ b/src/formatitko/html.py @@ -7,7 +7,7 @@ import os from typing import Union from .whitespace import NBSP -from .transform import FQuoted +from .elements import FQuoted from .katex import KatexClient from .util import inlinify from .context import Group diff --git a/src/formatitko/latex_generator.py b/src/formatitko/latex_generator.py index 92f9082..40ae889 100644 --- a/src/formatitko/latex_generator.py +++ b/src/formatitko/latex_generator.py @@ -5,7 +5,7 @@ from panflute import TableRow, TableCell, Caption, Doc from typing import Union from .whitespace import NBSP -from .transform import FQuoted +from .elements import FQuoted from .context import Group from .output_generator import OutputGenerator from .images import ImageProcessor diff --git a/src/formatitko/output_generator.py b/src/formatitko/output_generator.py index 7abdcd2..783e790 100644 --- a/src/formatitko/output_generator.py +++ b/src/formatitko/output_generator.py @@ -5,7 +5,7 @@ from panflute import TableRow, TableCell, Caption, Doc from typing import Union from .whitespace import NBSP -from .transform import FQuoted +from .elements import FQuoted from .context import Group import re diff --git a/src/formatitko/tex.py b/src/formatitko/tex.py index 5352fd3..23a2d62 100644 --- a/src/formatitko/tex.py +++ b/src/formatitko/tex.py @@ -3,7 +3,7 @@ import os from typing import Union from .whitespace import NBSP -from .transform import FQuoted +from .elements import FQuoted from .util import inlinify from .context import Group from .images import ImageProcessor diff --git a/src/formatitko/transform.py b/src/formatitko/transform.py index d422060..c96c6e6 100644 --- a/src/formatitko/transform.py +++ b/src/formatitko/transform.py @@ -1,21 +1,14 @@ -from panflute import Element, Div, Span, Quoted, Image, CodeBlock, Str, MetaInlines, MetaString, MetaBool +from panflute import Element, Div, Span, Quoted, Image, CodeBlock, Str, MetaInlines, MetaString, MetaBool, RawBlock import re +import os # Import local files from .whitespace import Whitespace, NBSP, bavlna -from .command import Command, BlockCommand, InlineCommand, handle_command_define, executeCommand from .util import nullify, import_md from .context import Context, Group - - -# This is a small extension to the Quoted panflute elements which allows to -# have language-aware quotation marks. -class FQuoted(Quoted): - def __init__(self, *args, **kwargs): - self.style = kwargs["style"] - del kwargs["style"] - super().__init__(*args, **kwargs) - +from .command import Command, BlockCommand, InlineCommand +from .command_util import handle_command_define, parse_command +from .elements import FQuoted # This is where tha magic happens. This function transforms a single element, # to transform the entire tree, panflute's walk should be used. @@ -117,7 +110,8 @@ def transform(e: Element, c: Context) -> Element: if isinstance(e, CodeBlock) and hasattr(e, "classes") and "python" in e.classes and "run" in e.classes: if not c.trusted: return nullify(e) - e = Div(*executeCommand(e.text, None, c)) + command_output = parse_command(e.text)(BlockCommand(), c) + e = Div(*([] if command_output is None else command_output)) e = e.walk(transform, c) # Command defines for calling using BlockCommand and InlineCommand. If @@ -169,7 +163,8 @@ def transform(e: Element, c: Context) -> Element: if isinstance(e, Command): if not c.get_command(e.attributes["c"]): raise NameError(f"Command not defined '{e.attributes['c']}'.") - e = e.replaceSelf(executeCommand(c.get_command(e.attributes["c"]), e, c)) + command_output = c.get_command(e.attributes["c"])(e, c) + e = e.replaceSelf([] if command_output is None else command_output) e = e.walk(transform, c) return e diff --git a/src/formatitko/util.py b/src/formatitko/util.py index 676c1cc..3710c72 100644 --- a/src/formatitko/util.py +++ b/src/formatitko/util.py @@ -1,25 +1,38 @@ -from panflute import Element, Block, Inline, Null, Str, Doc, convert_text, Para, Plain +from panflute import Element, Block, Inline, Null, Str, Doc, convert_text, Para, Plain, Span import re +from typing import Union # It sometimes happens that an element contains a single paragraph or even a # single plaintext line. It can be sometimes useful to extract this single # paragraph, which is inline. -def inlinify(e: Element) -> Element: - if len(e.content) == 1 and (isinstance(e.content[0], Para) or isinstance(e.content[0], Plain)): - return e.content[0].content +def inlinify(e: Union[Element, list[Element]]) -> Union[Element, None]: + if isinstance(e, Element): + content = e.content + else: + content = e + if len(content) == 0: + return Str("") + if len(content) == 1 and (isinstance(content[0], Para) or isinstance(content[0], Plain)): + return Span(*content[0].content) + if len(content) == 1 and inlinify(content[0]) is not None: + return inlinify(content[0]) + return None # In transform, inline elements cannot be replaced with Block ones and also # cannot be removed from the tree entirely, because that would mess up the # iteration process through the tree. We replace them with null elements # instead which never make it to the output. -def nullify(e: Element): +def nullify(e: Element) -> Union[Str, Null]: if isinstance(e, Inline): return Str("") - elif isinstance(e, Block): + else: return Null() # A helper function to import markdown using panflute (which calls pandoc). If # we ever want to disable or enable some of panflute's markdown extensions, # this is the place to do it. -def import_md(s: str, standalone: bool=True) -> Doc: +def import_md(s: str, standalone: bool=True) -> Union[Doc, list[Element]]: return convert_text(s, standalone=standalone, input_format="markdown-definition_lists-citations-latex_macros") + +def import_md_list(s: str) -> list[Element]: + return import_md(s, standalone=False)