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.
This commit is contained in:
parent
cd91750c04
commit
1ad200840e
12 changed files with 121 additions and 106 deletions
|
@ -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
Normal file
7
src/formatitko/command_env.py
Normal file
|
@ -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
Normal file
37
src/formatitko/command_util.py
Normal file
|
@ -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
|
||||
|
|
@ -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
Normal file
15
src/formatitko/elements.py
Normal file
|
@ -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)
|
||||
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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…
Reference in a new issue