1# coding=utf-8 2# 3# Copyright (C) 2018 Martin Owens <doctormo@gmail.com> 4# 5# This program is free software; you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation; either version 2 of the License, or 8# (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program; if not, write to the Free Software 17# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18# 19""" 20Parsing inx files for checking and generating. 21""" 22 23import os 24from inspect import isclass 25from importlib import util 26from lxml import etree 27 28from .base import InkscapeExtension 29from .utils import Boolean 30 31NSS = { 32 'inx': 'http://www.inkscape.org/namespace/inkscape/extension', 33 'inkscape': 'http://www.inkscape.org/namespaces/inkscape', 34} 35SSN = dict([(b, a) for (a, b) in NSS.items()]) 36 37class InxLookup(etree.CustomElementClassLookup): 38 """Custom inx xml file lookup""" 39 def lookup(self, node_type, document, namespace, name): # pylint: disable=unused-argument 40 if name == 'param': 41 return ParamElement 42 return InxElement 43 44INX_PARSER = etree.XMLParser() 45INX_PARSER.set_element_class_lookup(InxLookup()) 46 47class InxFile: 48 """Open an INX file and provide useful functions""" 49 name = property(lambda self: self.xml._text('name')) 50 ident = property(lambda self: self.xml._text('id')) 51 slug = property(lambda self: self.ident.split('.')[-1].title().replace('_', '')) 52 kind = property(lambda self: self.metadata['type']) 53 warnings = property(lambda self: sorted(list(set(self.xml.warnings)))) 54 55 def __init__(self, filename): 56 if isinstance(filename, str) and '<' in filename: 57 filename = filename.encode('utf8') 58 if isinstance(filename, bytes) and b'<' in filename: 59 self.filename = None 60 self.doc = etree.ElementTree(etree.fromstring(filename, parser=INX_PARSER)) 61 else: 62 self.filename = os.path.basename(filename) 63 self.doc = etree.parse(filename, parser=INX_PARSER) 64 self.xml = self.doc.getroot() 65 self.xml.warnings = [] 66 67 def __repr__(self): 68 return f"<inx '{self.filename}' '{self.name}'>" 69 70 @property 71 def script(self): 72 """Returns information about the called script""" 73 command = self.xml.find_one('script/command') 74 if command is None: 75 return {} 76 return { 77 'interpreter': command.get('interpreter', None), 78 'location': command.get('location', None), 79 'script': command.text, 80 } 81 82 @property 83 def extension_class(self): 84 """Attempt to get the extension class""" 85 script = self.script.get('script', None) 86 if script is not None: 87 name = script[:-3].replace('/', '.') 88 spec = util.spec_from_file_location(name, script) 89 mod = util.module_from_spec(spec) 90 spec.loader.exec_module(mod) 91 for value in mod.__dict__.values(): 92 if 'Base' not in name and isclass(value) and value.__module__ == name \ 93 and issubclass(value, InkscapeExtension): 94 return value 95 return None 96 97 @property 98 def metadata(self): 99 """Returns information about what type of extension this is""" 100 effect = self.xml.find_one('effect') 101 output = self.xml.find_one('output') 102 inputs = self.xml.find_one('input') 103 data = {} 104 if effect is not None: 105 template = self.xml.find_one('inkscape:templateinfo') 106 if template is not None: 107 data['type'] = 'template' 108 data['desc'] = self.xml._text('templateinfo/shortdesc', nss='inkscape') 109 data['author'] = self.xml._text('templateinfo/author', nss='inkscape') 110 else: 111 data['type'] = 'effect' 112 data['preview'] = Boolean(effect.get('needs-live-preview', 'true')) 113 data['objects'] = effect._text('object-type', 'all') 114 elif inputs is not None: 115 data['type'] = 'input' 116 data['extension'] = inputs._text('extension') 117 data['mimetype'] = inputs._text('mimetype') 118 data['tooltip'] = inputs._text('filetypetooltip') 119 data['name'] = inputs._text('filetypename') 120 elif output is not None: 121 data['type'] = 'output' 122 data['dataloss'] = Boolean(output._text('dataloss', 'false')) 123 data['extension'] = output._text('extension') 124 data['mimetype'] = output._text('mimetype') 125 data['tooltip'] = output._text('filetypetooltip') 126 data['name'] = output._text('filetypename') 127 return data 128 129 @property 130 def menu(self): 131 """Return the menu this effect ends up in""" 132 def _recurse_menu(parent): 133 for child in parent.xpath('submenu'): 134 yield child.get('name') 135 for subchild in _recurse_menu(child): 136 yield subchild 137 break # Not more than one menu chain? 138 menu = self.xml.find_one('effect/effects-menu') 139 return list(_recurse_menu(menu)) + [self.name] 140 141 @property 142 def params(self): 143 """Get all params at all levels""" 144 # Returns any params at any levels 145 return list(self.xml.xpath('//param')) 146 147class InxElement(etree.ElementBase): 148 def set_warning(self, msg): 149 root = self.get_root() 150 if hasattr(root, 'warnings'): 151 root.warnings.append(msg) 152 153 def get_root(self): 154 """Get the root document element from any element descendent""" 155 if self.getparent() is not None: 156 return self.getparent().get_root() 157 return self 158 159 def get_default_prefix(self): 160 tag = self.get_root().tag 161 if '}' in tag: 162 (url, tag) = tag[1:].split('}', 1) 163 return SSN.get(url, 'inx') 164 self.set_warning("No inx xml prefix.") 165 return None # no default prefix 166 167 def apply_nss(self, xpath, nss=None): 168 """Add prefixes to any xpath string""" 169 if nss is None: 170 nss = self.get_default_prefix() 171 def _process(seg): 172 if ':' in seg or not seg or not nss: 173 return seg 174 return f"{nss}:{seg}" 175 return "/".join([_process(seg) for seg in xpath.split("/")]) 176 177 def xpath(self, xpath, nss=None): 178 """Namespace specific xpath searches""" 179 return super().xpath(self.apply_nss(xpath, nss=nss), namespaces=NSS) 180 181 def find_one(self, name, nss=None): 182 """Return the first element matching the given name""" 183 for elem in self.xpath(name, nss=nss): 184 return elem 185 return None 186 187 def _text(self, name, default=None, nss=None): 188 """Get text content agnostically""" 189 for pref in ('', '_'): 190 elem = self.find_one(pref + name, nss=nss) 191 if elem is not None and elem.text: 192 if pref == '_': 193 self.set_warning(f"Use of old translation scheme: <_{name}...>") 194 return elem.text 195 return default 196 197class ParamElement(InxElement): 198 """ 199 A param in an inx file. 200 """ 201 name = property(lambda self: self.get('name')) 202 param_type = property(lambda self: self.get('type', 'string')) 203 204 @property 205 def options(self): 206 """Return a list of option values""" 207 if self.param_type == 'notebook': 208 return [option.get('name') 209 for option in self.xpath('page')] 210 return [option.get('value') 211 for option in self.xpath('option')] 212 213 def __repr__(self): 214 return f"<param name='{self.name}' type='{self.param_type}'>" 215