Browse Source

Částečně předělán systém příkazů. Resolves #9, resolves #18.

Je to furt docela solidní mess, ale IMO alespoň o trochu menší, než to
bylo. Asi by to chtělo trochu zrefaktorovat, k tomu se dostanu možná po
víkendu. Nakonec jsem se rozhodl nepředávat atributy pomocí kwargs, ale
alespoň se commandy volají jako funkce.
Jan Černohorský 11 months ago
parent
commit
1ad200840e
  1. 81
      src/formatitko/command.py
  2. 7
      src/formatitko/command_env.py
  3. 37
      src/formatitko/command_util.py
  4. 26
      src/formatitko/context.py
  5. 15
      src/formatitko/elements.py
  6. 3
      src/formatitko/formatitko.py
  7. 2
      src/formatitko/html.py
  8. 2
      src/formatitko/latex_generator.py
  9. 2
      src/formatitko/output_generator.py
  10. 2
      src/formatitko/tex.py
  11. 23
      src/formatitko/transform.py
  12. 27
      src/formatitko/util.py

81
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, '<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)
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 []

7
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

37
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

26
src/formatitko/context.py

@ -1,7 +1,12 @@
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,
@ -15,7 +20,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 +31,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 +39,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 +85,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 +102,4 @@ class Group(Div):
def __init__(self, *args, metadata={}, **kwargs):
self.metadata = metadata
super().__init__(*args, **kwargs)

15
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)

3
src/formatitko/formatitko.py

@ -57,7 +57,8 @@ def main():
# Generate HTML and TeX out of the transformed document
#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)
# HTMLGenerator(sys.stdout, katexClient, imageProcessor).generate(doc)
OutputGenerator(sys.stdout).generate(doc)
if args.debug:
print(show(doc))

2
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

2
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

2
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

2
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

23
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

27
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)

Loading…
Cancel
Save