# -*- 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
"""
An XIST module that allows embedding Python code in XML.
"""
import sys
from ll.xist import xsc
from ll.xist.ns import html
__docformat__ = "reStructuredText"
class Code:
def __init__(self, text, ignorefirst=False):
# get the individual lines; ignore "\r" as this would mess up whitespace handling later
# use list comprehension to get a list and not a Frag
lines = [ line for line in text.replace("\r", "").splitlines() ]
# split of the whitespace at the beginning of each line
for i in range(len(lines)):
line = lines[i]
rest = line.lstrip()
white = line[:len(line)-len(rest)]
lines[i] = [white, rest]
# drop all empty lines at the beginning; if we drop a line, we no longer have to handle the first specifically
while lines and not lines[0][1]:
del lines[0]
ignorefirst = False
# drop all empty lines at the end
while lines and not lines[-1][1]:
del lines[-1]
# complain, if the first line contains whitespace, although ignorewhitespace said it doesn't
if ignorefirst and lines and lines[0][0]:
raise ValueError("can't ignore the first line, as it does contain whitespace")
# find the shortest whitespace in non empty lines
shortestlen = sys.maxsize
for i in range(ignorefirst, len(lines)):
if lines[i][1]:
shortestlen = min(shortestlen, len(lines[i][0]))
# remove the common whitespace; a check is done, whether the common whitespace is the same in all lines
common = None
if shortestlen:
for i in range(ignorefirst, len(lines)):
if lines[i][1]:
test = lines[i][0][:shortestlen]
if common is not None:
if common != test:
raise SyntaxError("indentation mismatch")
common = test
lines[i][0] = lines[i][0][shortestlen:]
else:
lines[i][0] = ""
self.lines = lines
def indent(self):
for line in self.lines:
line[0] = "\t" + line[0]
def funcify(self, name="__"):
self.indent()
self.lines.insert(0, ["", f"def {name}(converter):"])
def asstring(self):
v = []
for line in self.lines:
v.extend(line)
v.append("\n")
return "".join(v)
class _base(xsc.ProcInst):
register = False
class Context(xsc.ProcInst.Context):
def __init__(self):
xsc.ProcInst.Context.__init__(self)
self.sandbox = {}
[docs]
class pyexec(_base):
"""
When converting a :class:`pyexec` object the content of the processing
instruction is executed as Python code. Execution is done when the node
is converted. When converted such a node will result in an empty
:data:`Null` node.
These processing instructions will be evaluated and executed in the
namespace of the module sandbox (which will be store in the converter
context).
"""
def convert(self, converter):
code = Code(self.content, True)
sandbox = converter[self].sandbox
exec(code.string(), sandbox) # requires Python 2.0b2 (and doesn't really work)
return xsc.Null
[docs]
class pyeval(_base):
"""
The content of a :class:`pyeval` processing instruction will be executed
when the node is converted as if it was the body of a function, so you
can return an expression here. Although the content is used as a function
body no indentation is necessary or allowed. The returned value will be
converted to a node and this resulting node will be converted.
These processing instructions will be evaluated and executed in the
namespace of the module sandbox (which will be store in the converter
context).
Note that you should not define the symbol ``__`` in any of your XIST
processing instructions, as it is used by XIST for internal purposes.
"""
[docs]
def convert(self, converter):
"""
Evaluates the code as if it was the body of a Python function. The
``converter`` argument will be available under the name
``converter`` as an argument to the function.
"""
code = Code(self.content, True)
code.funcify()
sandbox = converter[self].sandbox
exec(code.asstring(), sandbox) # requires Python 2.0b2 (and doesn't really work)
return xsc.tonode(sandbox["__"](converter)).convert(converter)