diff options
Diffstat (limited to 'share/extensions/inkex/inx.py')
-rw-r--r-- | share/extensions/inkex/inx.py | 244 |
1 files changed, 244 insertions, 0 deletions
diff --git a/share/extensions/inkex/inx.py b/share/extensions/inkex/inx.py new file mode 100644 index 0000000..2360bc6 --- /dev/null +++ b/share/extensions/inkex/inx.py @@ -0,0 +1,244 @@ +# coding=utf-8 +# +# Copyright (C) 2018 Martin Owens <doctormo@gmail.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +""" +Parsing inx files for checking and generating. +""" + +import os +from inspect import isclass +from importlib import util +from lxml import etree + +from .base import InkscapeExtension +from .utils import Boolean + +NSS = { + "inx": "http://www.inkscape.org/namespace/inkscape/extension", + "inkscape": "http://www.inkscape.org/namespaces/inkscape", +} +SSN = {b: a for (a, b) in NSS.items()} + + +class InxLookup(etree.CustomElementClassLookup): + """Custom inx xml file lookup""" + + def lookup( + self, node_type, document, namespace, name + ): # pylint: disable=unused-argument + if name == "param": + return ParamElement + return InxElement + + +INX_PARSER = etree.XMLParser() +INX_PARSER.set_element_class_lookup(InxLookup()) + + +class InxFile: + """Open an INX file and provide useful functions""" + + name = property(lambda self: self.xml.get_text("name")) + ident = property(lambda self: self.xml.get_text("id")) + slug = property(lambda self: self.ident.split(".")[-1].title().replace("_", "")) + kind = property(lambda self: self.metadata["type"]) + warnings = property(lambda self: sorted(list(set(self.xml.warnings)))) + + def __init__(self, filename): + if isinstance(filename, str) and "<" in filename: + filename = filename.encode("utf8") + if isinstance(filename, bytes) and b"<" in filename: + self.filename = None + self.doc = etree.ElementTree(etree.fromstring(filename, parser=INX_PARSER)) + else: + self.filename = os.path.basename(filename) + self.doc = etree.parse(filename, parser=INX_PARSER) + self.xml = self.doc.getroot() + self.xml.warnings = [] + + def __repr__(self): + return f"<inx '{self.filename}' '{self.name}'>" + + @property + def script(self): + """Returns information about the called script""" + command = self.xml.find_one("script/command") + if command is None: + return {} + return { + "interpreter": command.get("interpreter", None), + "location": command.get("location", None), + "script": command.text, + } + + @property + def extension_class(self): + """Attempt to get the extension class""" + script = self.script.get("script", None) + if script is not None: + name = script[:-3].replace("/", ".") + spec = util.spec_from_file_location(name, script) + mod = util.module_from_spec(spec) + spec.loader.exec_module(mod) + for value in mod.__dict__.values(): + if ( + "Base" not in name + and isclass(value) + and value.__module__ == name + and issubclass(value, InkscapeExtension) + ): + return value + return None + + @property + def metadata(self): + """Returns information about what type of extension this is""" + effect = self.xml.find_one("effect") + output = self.xml.find_one("output") + inputs = self.xml.find_one("input") + data = {} + if effect is not None: + template = self.xml.find_one("inkscape:templateinfo") + if template is not None: + data["type"] = "template" + data["desc"] = self.xml.get_text( + "templateinfo/shortdesc", nss="inkscape" + ) + data["author"] = self.xml.get_text( + "templateinfo/author", nss="inkscape" + ) + else: + data["type"] = "effect" + data["preview"] = Boolean(effect.get("needs-live-preview", "true")) + data["objects"] = effect.get_text("object-type", "all") + elif inputs is not None: + data["type"] = "input" + data["extension"] = inputs.get_text("extension") + data["mimetype"] = inputs.get_text("mimetype") + data["tooltip"] = inputs.get_text("filetypetooltip") + data["name"] = inputs.get_text("filetypename") + elif output is not None: + data["type"] = "output" + data["dataloss"] = Boolean(output.get_text("dataloss", "false")) + data["extension"] = output.get_text("extension") + data["mimetype"] = output.get_text("mimetype") + data["tooltip"] = output.get_text("filetypetooltip") + data["name"] = output.get_text("filetypename") + return data + + @property + def menu(self): + """Return the menu this effect ends up in""" + + def _recurse_menu(parent): + for child in parent.xpath("submenu"): + yield child.get("name") + for subchild in _recurse_menu(child): + yield subchild + break # Not more than one menu chain? + + menu = self.xml.find_one("effect/effects-menu") + return list(_recurse_menu(menu)) + [self.name] + + @property + def params(self): + """Get all params at all levels""" + # Returns any params at any levels + return list(self.xml.xpath("//param")) + + +class InxElement(etree.ElementBase): + """Any element in an inx file + + .. versionadded:: 1.1""" + + def set_warning(self, msg): + """Set a warning for slightly incorrect inx contents""" + root = self.get_root() + if hasattr(root, "warnings"): + root.warnings.append(msg) + + def get_root(self): + """Get the root document element from any element descendent""" + if self.getparent() is not None: + return self.getparent().get_root() + return self + + def get_default_prefix(self): + """Set default xml namespace prefix. If none is defined, set warning""" + tag = self.get_root().tag + if "}" in tag: + (url, tag) = tag[1:].split("}", 1) + return SSN.get(url, "inx") + self.set_warning("No inx xml prefix.") + return None # no default prefix + + def apply_nss(self, xpath, nss=None): + """Add prefixes to any xpath string""" + if nss is None: + nss = self.get_default_prefix() + + def _process(seg): + if ":" in seg or not seg or not nss: + return seg + return f"{nss}:{seg}" + + return "/".join([_process(seg) for seg in xpath.split("/")]) + + def xpath(self, xpath, nss=None): + """Namespace specific xpath searches + + .. versionadded:: 1.1""" + return super().xpath(self.apply_nss(xpath, nss=nss), namespaces=NSS) + + def find_one(self, name, nss=None): + """Return the first element matching the given name + + .. versionadded:: 1.1""" + for elem in self.xpath(name, nss=nss): + return elem + return None + + def get_text(self, name, default=None, nss=None): + """Get text content agnostically""" + for pref in ("", "_"): + elem = self.find_one(pref + name, nss=nss) + if elem is not None and elem.text: + if pref == "_": + self.set_warning(f"Use of old translation scheme: <_{name}...>") + return elem.text + return default + + +class ParamElement(InxElement): + """ + A param in an inx file. + """ + + name = property(lambda self: self.get("name")) + param_type = property(lambda self: self.get("type", "string")) + + @property + def options(self): + """Return a list of option values""" + if self.param_type == "notebook": + return [option.get("name") for option in self.xpath("page")] + return [option.get("value") for option in self.xpath("option")] + + def __repr__(self): + return f"<param name='{self.name}' type='{self.param_type}'>" |