Source code for ll.xist.ns.detox

# -*- coding: utf-8 -*-
# cython: language_level=3, always_allow_keywords=True

## Copyright 1999-2024 by LivingLogic AG, Bayreuth/Germany
## Copyright 1999-2024 by Walter Dörwald
##
## All Rights Reserved
##
## See ll/xist/__init__.py for the license


'''
This module is an XIST namespace. It provides a simple template language
based on processing instructions embedded in XML or plain text.

The following example is a simple "Hello, World" type template::

	from ll.xist.ns import detox

	template = """
	<?def helloworld(n=10)?>
		<?for i in range(n)?>
			Hello, World!
		<?end for?>
	<?end def?>
	"""

	module = detox.xml2mod(template)

	print "".join(module.helloworld())
'''


import sys, os, datetime, types

from ll import misc
from ll.xist import xsc


__docformat__ = "reStructuredText"


[docs] class expr(xsc.ProcInst): """ Embed the value of the expression """
class textexpr(xsc.ProcInst): pass class attrexpr(xsc.ProcInst): pass
[docs] class code(xsc.ProcInst): """ Embed the PI data literally in the generated code. For example ``<?code foo = 42?>`` will put the statement ``foo = 42`` into the generated Python source. """
[docs] class if_(xsc.ProcInst): """ Starts an if block. An if block can contain zero or more :class:`elif_` blocks, followed by zero or one :class:`else_` block and must be closed with an :class:`end` PI. For example:: <?code import random?> <?code n = random.choice("123?")?> <?if n == "1"?> One <?elif n == "2"?> Two <?elif n == "3"?> Three <?else?> Something else <?end if?> """ xmlname = "if"
[docs] class elif_(xsc.ProcInst): """ Starts an elif block. """ xmlname = "elif"
[docs] class else_(xsc.ProcInst): """ Starts an else block. """ xmlname = "else"
[docs] class def_(xsc.ProcInst): """ Start a function (or method) definition. A function definition must be closed with an :class:`end` PI. Example:: <?def persontable(persons)?> <table> <tr> <th>first name</th> <th>last name</th> </tr> <?for person in persons?> <tr> <td><?textexpr person.firstname?></td> <td><?textexpr person.lastname?></td> </tr> <?end for?> </table> <?end def?> If the generated function contains output (i.e. if there is text content or :class:`expr`, :class:`textexpr` or :class:`attrexpr` PIs before the matching :class:`end`) the generated function will be a generator function. Output outside of a function definition will be ignored. """ xmlname = "def"
[docs] class class_(xsc.ProcInst): """ Start a class definition. A class definition must be closed with an :class:`end` PI. Example:: <?class mylist(list)?> <?def output(self)?> <ul> <?for item in self?> <li><?textexpr item?></li> <?endfor?> </ul> <?end def?> <?end class?> """ xmlname = "class"
[docs] class for_(xsc.ProcInst): """ Start a ``for`` loop. A for loop must be closed with an :class:`end` PI. For example:: <ul> <?for i in range(10)?> <li><?expr str(i)?></li> <?end for?> </ul> """ xmlname = "for"
[docs] class while_(xsc.ProcInst): """ Start a ``while`` loop. A while loop must be closed with an :class:`end` PI. For example:: <?code i = 0?> <ul> <?while True?> <li><?expr str(i)?><?code i += 1?></li> <?code if i > 10: break?> <?end while?> </ul> """ xmlname = "while"
[docs] class end(xsc.ProcInst): """ Ends a :class:`while_` or :class:`for_` loop or a :class:`if_`, :class:`def_` or :class:`class_` block. """
# The name of all available processing instructions targets = {value.xmlname for value in vars().values() if isinstance(value, xsc._ProcInst_Meta)} # Used for indenting Python source code indent = "\t" def xml2py(source): stack = [] stackoutput = [] # stack containing only True for def and False for class lines = [ f"# generated by {__file__} on {datetime.datetime.utcnow()} UTC", "", "from ll.misc import xmlescape as __detox_xmlescape__", "", ] def endscope(action): if not stack: raise SyntaxError(f"can't end {action or 'unnamed'} scope: no active scope") if action and action != stack[-1][0]: raise SyntaxError(f"can't end {action} scope: active scope is: {stack[-1][0]} {stack[-1][1]}") return stack.pop() for (t, s) in misc.tokenizepi(source): if t is None: # ignore output outside of functions if stackoutput and stackoutput[-1]: lines.append(f"{len(stack)*indent}yield {s!r}") elif t == "expr": # ignore output outside of functions if stackoutput and stackoutput[-1]: lines.append(f"{len(stack)*indent}yield {s}") elif t == "textexpr": # ignore output outside of functions if stackoutput and stackoutput[-1]: lines.append(f"{len(stack)*indent}yield __detox_xmlescape__({s})") elif t == "attrexpr": # ignore output outside of functions if stackoutput and stackoutput[-1]: lines.append(f"{len(stack)*indent}yield __detox_xmlescape__({s})") elif t == "code": lines.append(f"{len(stack)*indent}{s}") elif t == "def": lines.append("") lines.append(f"{len(stack)*indent}def {s}:") stack.append((t, s)) stackoutput.append(True) elif t == "class": lines.append("") lines.append(f"{len(stack)*indent}class {s}:") stack.append((t, s)) stackoutput.append(False) elif t == "for": lines.append(f"{len(stack)*indent}for {s}:") stack.append((t, s)) elif t == "while": lines.append(f"{len(stack)*indent}while {s}:") stack.append((t, s)) elif t == "if": lines.append(f"{len(stack)*indent}if {s}:") stack.append((t, s)) elif t == "else": lines.append(f"{(len(stack)-1)*indent}else:") elif t == "elif": lines.append(f"{(len(stack)-1)*indent}elif {s}:") elif t == "end": scope = endscope(s) if scope in ("def", "class"): stackoutput.pop() else: # unknown PI target => treat as text # ignore output outside of functions if stackoutput and stackoutput[-1]: s = f"<?{t} {s}?>" lines.append(f"{len(stack)*indent}yield {s!r}") if stack: scopes = ", ".join(scope[0] for scope in stack) raise SyntaxError(f"unclosed scopes remaining: {scopes}") return "\n".join(lines) def xml2mod(source, name=None, filename="unnamed.py"): return misc.module(xml2py(source), filename, name) # The following stuff has been copied from Kids import hook DETOX_EXT = ".detox" def enable_import(suffixes=None): class DetoxLoader: def __init__(self, path=None): if path and os.path.isdir(path): self.path = path else: raise ImportError def find_module(self, fullname): path = os.path.join(self.path, fullname.split(".")[-1]) for ext in [cls.DETOX_EXT] + self.suffixes: if os.path.exists(path + ext): self.filename = path + ext return self return None def load_module(self, fullname): try: return sys.modules[fullname] except KeyError: return xml2mod(open(self.filename, "r").read(), name=fullname, filename=self.filename, store=True, loader=self) DetoxLoader.suffixes = suffixes or [] sys.path_hooks.append(DetoxLoader) sys.path_importer_cache.clear()