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 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
									
									
									
									
									
										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 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
									
									
									
									
									
										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 | 		# 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)) | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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…
	
		Reference in a new issue