diff --git a/command.py b/command.py index 08c7308..1532846 100644 --- a/command.py +++ b/command.py @@ -1,7 +1,11 @@ from panflute import Div,Span,Para -from util import * from typing import List +# Import local files +from util import * +from context import Context +from mj_show import show + class Command: pass @@ -20,3 +24,50 @@ class BlockCommand(Div, Command): def replaceSelf(self, content: List[Element]) -> Div: return Div(*content) pass + + +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) + + +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 += s + + def println(s: str=""): + print(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) + + exec(source) + + if mode == 'text': + return import_md(text, standalone=False) + if mode == 'elements': + return content + + return [] diff --git a/context.py b/context.py index 0f5a42c..9817fdd 100644 --- a/context.py +++ b/context.py @@ -36,4 +36,11 @@ class Context: def unset_flag(self, flag): del self._flags[flag] - + def get_metadata(self, key, simple=True): + value = self.doc.get_metadata(key, None, simple) + if value is not None: + return value + elif self.parent: + return self.parent.get_metadata(key) + else: + return None diff --git a/formatitko.py b/formatitko.py index deaa1a0..b997257 100755 --- a/formatitko.py +++ b/formatitko.py @@ -1,133 +1,19 @@ #!/usr/bin/env python3 -from panflute import * import re import sys from typing import List # Import local files -from whitespace import * -from command import * +from transform import transform from util import * -from context import * +from context import Context from mj_show import show -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 += s - - def println(s: str=""): - print(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) - - exec(source) - - if mode == 'text': - return convert_text(text) - if mode == 'elements': - return content - - return [] - -def transform(e: Element, context: Context) -> Element: # Returns next sibling element to transform - """Transform the AST, making format-agnostic changes.""" - - if isinstance(e, Whitespace) and bavlna(e): - e = NBSP() - - if hasattr(e, "attributes"): - # `if` attribute. Only show this element if flag is set. - if "if" in e.attributes: - if not context.is_flag_set(e.attributes["if"]): - return nullify(e) - # `ifn` attribute. Only show this element if flag is NOT set - if "ifn" in e.attributes: - if context.is_flag_set(e.attributes["ifn"]): - return nullify(e) - - # `c` attribute. Execute a command with the name saved in this attribute. - if (isinstance(e, Div) or isinstance(e, Span)) and "c" in e.attributes: - if isinstance(e, Div): - e = BlockCommand(*e.content, identifier=e.identifier, classes=e.classes, attributes=e.attributes) - else: - e = InlineCommand(*e.content, identifier=e.identifier, classes=e.classes, attributes=e.attributes) - - # `partial` attribute. - # This is for including content from files with their own flags - # and commands without affecting the state of the current - # document. - if (isinstance(e, Div)) and "partial" in e.attributes: - includedDoc = import_md(open(e.attributes["partial"], "r").read()) - nContext = Context(includedDoc, context) - includedDoc = includedDoc.walk(transform, nContext) - e = Div(*includedDoc.content) - - # Execute python code inside source code block - if isinstance(e, CodeBlock) and hasattr(e, "classes") and "python" in e.classes and "run" in e.classes: - e = executeCommand(e.text, None, context) - - ## Command defines - # possible TODO: def/longdef? - if isinstance(e, CodeBlock) and hasattr(e, "classes") and "python" in e.classes and hasattr(e, "attributes"): - if "define" in e.attributes: - if not context.get_command(e.attributes["define"]): - context.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: - context.set_command(e.attributes["redefine"], compile(e.text, '', 'exec')) - return nullify(e) - - ## Shorthands - if isinstance(e, Span) and len(e.content) == 1 and isinstance(e.content[0], Str): - ## Handle special command shorthand [!commandname]{} - if re.match(r"^![\w.]+$", e.content[0].text): - e = InlineCommand(identifier=e.identifier, classes=e.classes, attributes={**e.attributes, "c": e.content[0].text[1:]}) - - ## Handle import [#path/file.md]{} - # This is the exact opposite of include. We take the commands - # and flags but drop the content. - elif re.match(r"^#.+$", e.content[0].text): - importedDoc = import_md(open(e.content[0].text[1:], "r").read()) - importedDoc.walk(transform, context) - return nullify(e) - - ## Execute commands - # Walk transforms the children first, then the root element, - # so the content of the element the command receives is - # already transformed. - if isinstance(e, Command): - if not context.get_command(e.attributes["c"]): - raise NameError(f"Command not defined '{e.attributes['c']}'.") - e = e.replaceSelf(executeCommand(context.get_command(e.attributes["c"]), e, context)) - e.walk(transform, context) - - return e - doc = import_md(open(sys.argv[1], "r").read()) - - print(show(doc)) context = Context(doc) doc = doc.walk(transform, context) diff --git a/test-import.md b/test-import.md index 50b67c3..90da504 100644 --- a/test-import.md +++ b/test-import.md @@ -3,5 +3,5 @@ appendChildren(element.content) ``` ``` {.python define=opendatatask} -print("Toto je praktická open-data úloha. V [odevzdávátku](https://ksp.mff.cuni.cz/h/odevzdavatko/) si necháte vygenerovat vstupy a odevzdáte příslušné výstupy. Záleží jen na vás, jak výstupy vyrobíte.") +println("Toto je praktická open-data úloha. V [odevzdávátku](https://ksp.mff.cuni.cz/h/odevzdavatko/) si necháte vygenerovat vstupy a odevzdáte příslušné výstupy. Záleží jen na vás, jak výstupy vyrobíte.") ``` diff --git a/test-partial.md b/test-partial.md index 8b366f4..0ab00a2 100644 --- a/test-partial.md +++ b/test-partial.md @@ -12,6 +12,12 @@ And things... ctx.set_flag("cat", True) ``` +``` {.python .run} +println(f"The subdocument's title is \n\n# {ctx.get_metadata('title')}") +println() +println(f"The subdocument's subtitle is \n\n## {ctx.get_metadata('subtitle')}") +``` + :::{if=cat} This should be only shown to included cats. ::: diff --git a/test.md b/test.md index 565a5ea..a0f901a 100644 --- a/test.md +++ b/test.md @@ -1,5 +1,6 @@ --- title: 'Wooooo a title' +subtitle: 'A subtitle' --- [#test-import.md]{} @@ -19,6 +20,9 @@ This should only be shown to cats ctx.set_flag("cat", True) ``` +``` {.python .run} +println(f"The main document's title is '{ctx.get_metadata('title')}'") +``` ::::{if=cat} This should only be shown to cats the second time diff --git a/transform.py b/transform.py new file mode 100644 index 0000000..7c85e9b --- /dev/null +++ b/transform.py @@ -0,0 +1,74 @@ +from panflute import * +import re + +# Import local files +from whitespace import * +from command import * +from util import * +from context import * + +def transform(e: Element, c: Context) -> Element: # Returns next sibling element to transform + """Transform the AST, making format-agnostic changes.""" + + if isinstance(e, Whitespace) and bavlna(e): + e = NBSP() + + if hasattr(e, "attributes"): + # `if` attribute. Only show this element if flag is set. + if "if" in e.attributes: + if not c.is_flag_set(e.attributes["if"]): + return nullify(e) + # `ifn` attribute. Only show this element if flag is NOT set + if "ifn" in e.attributes: + if c.is_flag_set(e.attributes["ifn"]): + return nullify(e) + + # `c` attribute. Execute a command with the name saved in this attribute. + if (isinstance(e, Div) or isinstance(e, Span)) and "c" in e.attributes: + if isinstance(e, Div): + e = BlockCommand(*e.content, identifier=e.identifier, classes=e.classes, attributes=e.attributes) + else: + e = InlineCommand(*e.content, identifier=e.identifier, classes=e.classes, attributes=e.attributes) + + # `partial` attribute. + # This is for including content from files with their own flags and + # commands without affecting the state of the current document. + if (isinstance(e, Div)) and "partial" in e.attributes: + includedDoc = import_md(open(e.attributes["partial"], "r").read()) + nContext = Context(includedDoc, c) + includedDoc = includedDoc.walk(transform, nContext) + e = Div(*includedDoc.content) + + # Execute python code inside source code block + if isinstance(e, CodeBlock) and hasattr(e, "classes") and "python" in e.classes and "run" in e.classes: + e = executeCommand(e.text, None, c) + + ## Command defines + # possible TODO: def/longdef? + if isinstance(e, CodeBlock) and hasattr(e, "classes") and "python" in e.classes and hasattr(e, "attributes"): + e = handle_command_define(e, c) + + ## Shorthands + if isinstance(e, Span) and len(e.content) == 1 and isinstance(e.content[0], Str): + ## Handle special command shorthand [!commandname]{} + if re.match(r"^![\w.]+$", e.content[0].text): + e = InlineCommand(identifier=e.identifier, classes=e.classes, attributes={**e.attributes, "c": e.content[0].text[1:]}) + + ## Handle import [#path/file.md]{} + # This is the exact opposite of include. We take the commands + # and flags but drop the content. + elif re.match(r"^#.+$", e.content[0].text): + importedDoc = import_md(open(e.content[0].text[1:], "r").read()) + importedDoc.walk(transform, c) + return nullify(e) + + ## Execute commands + # panflute's walk transforms the children first, then the root element, so + # the content of the element the command receives is already transformed. + 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)) + e.walk(transform, c) + + return e diff --git a/util.py b/util.py index 7e3c04a..ed0173a 100644 --- a/util.py +++ b/util.py @@ -15,5 +15,5 @@ def nullify(e: Element): elif isinstance(e, Block): return Null() -def import_md(s: str) -> Doc: - return convert_text(s, standalone=True) +def import_md(s: str, standalone: bool=True) -> Doc: + return convert_text(s, standalone=standalone)