Browse Source

Split into more files, implemented metadata.

pull/28/head
Jan Černohorský 2 years ago
parent
commit
f69e8a7127
  1. 53
      command.py
  2. 9
      context.py
  3. 118
      formatitko.py
  4. 2
      test-import.md
  5. 6
      test-partial.md
  6. 4
      test.md
  7. 74
      transform.py
  8. 4
      util.py

53
command.py

@ -1,7 +1,11 @@
from panflute import Div,Span,Para from panflute import Div,Span,Para
from util import *
from typing import List from typing import List
# Import local files
from util import *
from context import Context
from mj_show import show
class Command: class Command:
pass pass
@ -20,3 +24,50 @@ class BlockCommand(Div, Command):
def replaceSelf(self, content: List[Element]) -> Div: def replaceSelf(self, content: List[Element]) -> Div:
return Div(*content) return Div(*content)
pass 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, '<string>', '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, '<string>', '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 []

9
context.py

@ -36,4 +36,11 @@ class Context:
def unset_flag(self, flag): def unset_flag(self, flag):
del self._flags[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

118
formatitko.py

@ -1,133 +1,19 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from panflute import *
import re import re
import sys import sys
from typing import List from typing import List
# Import local files # Import local files
from whitespace import * from transform import transform
from command import *
from util import * from util import *
from context import * from context import Context
from mj_show import show 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, '<string>', '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, '<string>', '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()) doc = import_md(open(sys.argv[1], "r").read())
print(show(doc)) print(show(doc))
context = Context(doc) context = Context(doc)
doc = doc.walk(transform, context) doc = doc.walk(transform, context)

2
test-import.md

@ -3,5 +3,5 @@ appendChildren(element.content)
``` ```
``` {.python define=opendatatask} ``` {.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.")
``` ```

6
test-partial.md

@ -12,6 +12,12 @@ And things...
ctx.set_flag("cat", True) 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} :::{if=cat}
This should be only shown to included cats. This should be only shown to included cats.
::: :::

4
test.md

@ -1,5 +1,6 @@
--- ---
title: 'Wooooo a title' title: 'Wooooo a title'
subtitle: 'A subtitle'
--- ---
[#test-import.md]{} [#test-import.md]{}
@ -19,6 +20,9 @@ This should only be shown to cats
ctx.set_flag("cat", True) ctx.set_flag("cat", True)
``` ```
``` {.python .run}
println(f"The main document's title is '{ctx.get_metadata('title')}'")
```
::::{if=cat} ::::{if=cat}
This should only be shown to cats the second time This should only be shown to cats the second time

74
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

4
util.py

@ -15,5 +15,5 @@ def nullify(e: Element):
elif isinstance(e, Block): elif isinstance(e, Block):
return Null() return Null()
def import_md(s: str) -> Doc: def import_md(s: str, standalone: bool=True) -> Doc:
return convert_text(s, standalone=True) return convert_text(s, standalone=standalone)

Loading…
Cancel
Save