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ý 1 year 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. 22
      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 inlinify
from .util import nullify, import_md
from .context import Context class InlineError(Exception):
pass
class Command: class Command:
pass pass
@ -14,10 +15,10 @@ class InlineCommand(Span, Command):
try: try:
return Span(*content) return Span(*content)
except TypeError: except TypeError:
if len(content) == 1 and isinstance(content[0], Para): if inlinify(content):
return Span(*content[0].content) return Span(inlinify(content))
else: 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 pass
class BlockCommand(Div, Command): class BlockCommand(Div, Command):
@ -25,69 +26,3 @@ class BlockCommand(Div, Command):
return Div(*content) return Div(*content)
pass 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

22
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 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 # 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, # 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 # This class is basically an extension to panflute's doc, this is why metadata
# is read directly from it. # is read directly from it.
class Context: 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.parent = parent
self._commands = {} self._commands = {}
self.doc = doc self.doc = doc
@ -26,7 +31,7 @@ class Context:
if self.get_metadata("flags", immediate=True) is None: if self.get_metadata("flags", immediate=True) is None:
self.set_metadata("flags", {}) self.set_metadata("flags", {})
def get_command(self, command: str): def get_command(self, command: str) -> Union[CommandCallable, None]:
if command in self._commands: if command in self._commands:
return self._commands[command] return self._commands[command]
elif self.parent: elif self.parent:
@ -34,12 +39,18 @@ class Context:
else: else:
return None return None
def set_command(self, command: str, val): def set_command(self, command: str, val: CommandCallable):
self._commands[command] = val self._commands[command] = val
def unset_command(self, command: str): def unset_command(self, command: str):
del self._commands[command] 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): def is_flag_set(self, flag: str):
if self.get_metadata("flags."+flag): if self.get_metadata("flags."+flag):
if self.get_metadata("flags."+flag): if self.get_metadata("flags."+flag):
@ -91,3 +102,4 @@ class Group(Div):
def __init__(self, *args, metadata={}, **kwargs): def __init__(self, *args, metadata={}, **kwargs):
self.metadata = metadata self.metadata = metadata
super().__init__(*args, **kwargs) 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 # Generate HTML and TeX out of the transformed document
#open(args.output_html, "w").write(html(doc, katexClient, imageProcessor)) #open(args.output_html, "w").write(html(doc, katexClient, imageProcessor))
#open(args.output_tex, "w").write(tex(doc, 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: if args.debug:
print(show(doc)) print(show(doc))

2
src/formatitko/html.py

@ -7,7 +7,7 @@ import os
from typing import Union from typing import Union
from .whitespace import NBSP from .whitespace import NBSP
from .transform import FQuoted from .elements import FQuoted
from .katex import KatexClient from .katex import KatexClient
from .util import inlinify from .util import inlinify
from .context import Group 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 typing import Union
from .whitespace import NBSP from .whitespace import NBSP
from .transform import FQuoted from .elements import FQuoted
from .context import Group from .context import Group
from .output_generator import OutputGenerator from .output_generator import OutputGenerator
from .images import ImageProcessor 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 typing import Union
from .whitespace import NBSP from .whitespace import NBSP
from .transform import FQuoted from .elements import FQuoted
from .context import Group from .context import Group
import re import re

2
src/formatitko/tex.py

@ -3,7 +3,7 @@ import os
from typing import Union from typing import Union
from .whitespace import NBSP from .whitespace import NBSP
from .transform import FQuoted from .elements import FQuoted
from .util import inlinify from .util import inlinify
from .context import Group from .context import Group
from .images import ImageProcessor 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 re
import os
# Import local files # Import local files
from .whitespace import Whitespace, NBSP, bavlna from .whitespace import Whitespace, NBSP, bavlna
from .command import Command, BlockCommand, InlineCommand, handle_command_define, executeCommand
from .util import nullify, import_md from .util import nullify, import_md
from .context import Context, Group from .context import Context, Group
from .command import Command, BlockCommand, InlineCommand
from .command_util import handle_command_define, parse_command
# This is a small extension to the Quoted panflute elements which allows to from .elements import FQuoted
# have language-aware quotation marks.
class FQuoted(Quoted):
def __init__(self, *args, **kwargs):
self.style = kwargs["style"]
del kwargs["style"]
super().__init__(*args, **kwargs)
# This is where tha magic happens. This function transforms a single element, # This is where tha magic happens. This function transforms a single element,
# to transform the entire tree, panflute's walk should be used. # 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 isinstance(e, CodeBlock) and hasattr(e, "classes") and "python" in e.classes and "run" in e.classes:
if not c.trusted: if not c.trusted:
return nullify(e) 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) e = e.walk(transform, c)
# Command defines for calling using BlockCommand and InlineCommand. If # 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 isinstance(e, Command):
if not c.get_command(e.attributes["c"]): if not c.get_command(e.attributes["c"]):
raise NameError(f"Command not defined '{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) e = e.walk(transform, c)
return e 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 import re
from typing import Union
# It sometimes happens that an element contains a single paragraph or even a # 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 # single plaintext line. It can be sometimes useful to extract this single
# paragraph, which is inline. # paragraph, which is inline.
def inlinify(e: Element) -> Element: def inlinify(e: Union[Element, list[Element]]) -> Union[Element, None]:
if len(e.content) == 1 and (isinstance(e.content[0], Para) or isinstance(e.content[0], Plain)): if isinstance(e, Element):
return e.content[0].content 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 # 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 # cannot be removed from the tree entirely, because that would mess up the
# iteration process through the tree. We replace them with null elements # iteration process through the tree. We replace them with null elements
# instead which never make it to the output. # instead which never make it to the output.
def nullify(e: Element): def nullify(e: Element) -> Union[Str, Null]:
if isinstance(e, Inline): if isinstance(e, Inline):
return Str("") return Str("")
elif isinstance(e, Block): else:
return Null() return Null()
# A helper function to import markdown using panflute (which calls pandoc). If # 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, # we ever want to disable or enable some of panflute's markdown extensions,
# this is the place to do it. # 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") 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