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