4 | # This program is Free Software see LICENSE file for details
5 |
6 | import os
7 | import logging
8 | from distutils.version import StrictVersion
9 |
10 | from . import utils
11 | from .polib import polib
12 |
13 | SETTINGS_FILE = 'kodidevkit.sublime-settings'
14 |
15 |
16 | class Addon(object):
17 | """
18 | Represents a kodi addon with path *project_path and *settings
19 | """
20 |
21 | RELEASES = [{"gui_version": '5.0.1',
22 | "python_version": "2.14.0",
23 | "name": "gotham"},
24 | {"gui_version": '5.3.0',
25 | "python_version": "2.19.0",
26 | "name": "helix"},
27 | {"gui_version": '5.9.0',
28 | "python_version": "2.20.0",
29 | "name": "isengard"},
30 | {"gui_version": '5.10.0',
31 | "python_version": "2.24.0",
32 | "name": "jarvis"},
33 | {"gui_version": '5.12.0',
34 | "python_version": "2.25.0",
35 | "name": "krypton"},
36 | {"gui_version": '5.13.0',
37 | "python_version": "2.25.0",
38 | "name": "leia"}]
39 |
40 | LANG_START_ID = 32000
41 | LANG_OFFSET = 2
42 |
43 | def __init__(self, *args, **kwargs):
44 | self.type = "python"
45 | self.po_files = []
46 | self.colors = []
47 | self.color_labels = set()
48 | self.fonts = {}
49 | self.xml_folders = []
50 | self.window_files = {}
51 | self.include_files = {}
52 | self.font_file = None
53 | self.includes = {}
54 | self.api_version = None
55 | self.settings = kwargs.get("settings")
56 | self.path = kwargs.get("project_path")
57 | self.xml_file = os.path.join(self.path, "addon.xml")
58 | self.root = utils.get_root_from_file(self.xml_file)
59 | api_import = self.root.find(".//import[@addon='xbmc.python']")
60 | if api_import is not None:
61 | api_version = api_import.attrib.get("version")
62 | for item in self.RELEASES:
63 | if StrictVersion(api_version) <= StrictVersion(item["python_version"]):
64 | self.api_version = item["name"]
65 | break
66 | self.version = self.root.attrib.get("version")
67 | for item in self.root.xpath("/addon[@id]"):
68 | self.name = item.attrib["id"]
69 | break
70 | self.load_xml_folders()
71 | self.update_xml_files()
72 | self.update_labels()
73 |
74 | @property
75 | def default_xml_folder(self):
76 | """
77 | returns the fallback xml folder as a string
78 | """
79 | return self.xml_folders[0]
80 |
81 | def load_xml_folders(self):
82 | """
83 | find and load skin xml folder if existing
84 | """
85 | paths = [os.path.join(self.path, "resources", "skins", "Default", "720p"),
86 | os.path.join(self.path, "resources", "skins", "Default", "1080i")]
87 | folder = utils.check_paths(paths)
88 | self.xml_folders.append(folder)
89 |
90 | @property
91 | def lang_path(self):
92 | """
93 | returns the add-on language folder path
94 | """
95 | return os.path.join(self.path, "resources", "language")
96 |
97 | @property
98 | def changelog_path(self):
99 | """
100 | returns the add-on language folder path
101 | """
102 | return os.path.join(self.path, "changelog.txt")
103 |
104 | @property
105 | def primary_lang_folder(self):
106 | """
107 | returns default language folder (first one from settings file)
108 | """
109 | lang_folder = self.settings.get("language_folders")[0]
110 | lang_path = os.path.join(self.path, "resources", "language", lang_folder)
111 | if not os.path.exists(lang_path):
112 | os.makedirs(lang_path)
113 | return lang_path
114 |
115 | @property
116 | def media_path(self):
117 | """
118 | returns the add-on media folder path
119 | """
120 | return os.path.join(self.path, "resources", "skins", "Default", "media")
121 |
122 | @staticmethod
123 | def by_project(project_path, settings):
124 | """
125 | factory, return proper instance based on addon.xml
126 | """
127 | xml_file = os.path.join(project_path, "addon.xml")
128 | root = utils.get_root_from_file(xml_file)
129 | if root.find(".//import[@addon='xbmc.python']") is None:
130 | from . import skin
131 | return skin.Skin(project_path=project_path,
132 | settings=settings)
133 | else:
134 | return Addon(project_path=project_path,
135 | settings=settings)
136 | # TODO: parse all python skin folders correctly
137 |
138 | def update_labels(self):
139 | """
140 | get addon po files and update po files list
141 | """
142 | self.po_files = self.get_po_files(self.lang_path)
143 |
144 | def get_po_files(self, lang_folder_root):
145 | """
146 | get list with pofile objects
147 | """
148 | po_files = []
149 | folders = self.settings.get("language_folders", ["resource.language.en_gb", "English"])
150 | for item in folders:
151 | path = utils.check_paths([os.path.join(lang_folder_root, item, "strings.po"),
152 | os.path.join(lang_folder_root, item, "resources", "strings.po")])
153 | if path:
154 | po_file = utils.get_po_file(path)
155 | if po_file:
156 | po_file.language = item
157 | po_files.append(po_file)
158 | return po_files
159 |
160 | def update_xml_files(self):
161 | """
162 | update list of all include and window xmls
163 | """
164 | self.window_files = {}
165 | for path in self.xml_folders:
166 | xml_folder = os.path.join(self.path, path)
167 | self.window_files[path] = []
168 | if not os.path.exists(xml_folder):
169 | return []
170 | for xml_file in os.listdir(xml_folder):
171 | filename = os.path.basename(xml_file)
172 | if not filename.endswith(".xml"):
173 | continue
174 | if filename.lower() not in ["font.xml"]:
175 | self.window_files[path].append(xml_file)
176 | logging.info("found %i XMLs in %s" % (len(self.window_files[path]), xml_folder))
177 |
178 | def create_new_label(self, word, filepath):
179 | """
180 | adds a label to the first pofile from settings (or creates new one if non-existing)
181 | """
182 | if not self.po_files:
183 | po_file = utils.create_new_po_file(os.path.join(self.primary_lang_folder, "strings.po"))
184 | po_file.save()
185 | self.po_files.append(po_file)
186 | logging.critical("New language file created")
187 | else:
188 | po_file = self.po_files[0]
189 | string_ids = []
190 | for entry in po_file:
191 | try:
192 | string_ids.append(int(entry.msgctxt[1:]))
193 | except Exception:
194 | string_ids.append(entry.msgctxt)
195 | for label_id in range(self.LANG_START_ID, self.LANG_START_ID + 1000):
196 | if label_id not in string_ids:
197 | logging.info("first free: " + str(label_id))
198 | break
199 | entry = polib.POEntry(msgid=word,
200 | msgstr="",
201 | msgctxt="#%s" % label_id,
202 | occurrences=[(filepath, None)])
203 | po_file.insert(index=int(label_id) - self.LANG_START_ID + self.LANG_OFFSET,
204 | entry=entry)
205 | po_file.save()
206 | self.update_labels()
207 | return label_id
208 |
209 | def attach_occurrence_to_label(self, label_id, rel_path):
210 | """
211 | add *rel_path to label with *label id as a file comment
212 | """
213 | if 31000 <= int(label_id[1:]) < 33000:
214 | entry = self.po_files[0].find(label_id, by="msgctxt")
215 | entry.occurrences.append((rel_path, None))
216 | self.po_files[0].save()
217 |
218 | def translate_path(self, path):
219 | """
220 | return translated path for textures
221 | """
222 | if path.startswith("special://skin/"):
223 | return os.path.join(self.path, path.replace("special://skin/", ""))
224 | else:
225 | return os.path.join(self.media_path, path)
226 |
227 | def return_node(self, keyword=None, folder=False):
228 | """
229 | get value from include list
230 | """
231 | if not keyword or not folder:
232 | return None
233 | if folder in self.fonts:
234 | for node in self.fonts[folder]:
235 | if node["name"] == keyword:
236 | return node
237 | if folder in self.includes:
238 | for node in self.includes[folder]:
239 | if node["name"] == keyword:
240 | return node
241 | return None
242 |
243 | def reload(self, path):
244 | """
245 | update include, color and font infos (not needed yet for python)
246 | """
247 | pass
248 |
249 | def get_xml_files(self):
250 | """
251 | yields absolute paths of all window files
252 | """
253 | if self.xml_folders:
254 | for folder in self.xml_folders:
255 | for xml_file in self.window_files[folder]:
256 | yield os.path.join(self.path, folder, xml_file)
257 |
258 | def bump_version(self, version):
259 | """
260 | bump addon.xml version and create changelog entry
261 | """
262 | self.root.attrib["version"] = version
263 | utils.save_xml(self.xml_file, self.root)
264 | with open(self.changelog_path, "r") as f:
265 | contents = f.readlines()
266 | contents = [version, "", "-", "-", "", ""] + contents
267 | with open(self.changelog_path, "w") as changelog_file:
268 | changelog_file.write("\n".join(contents))
269 |
270 | def get_constants(self, folder):
271 | """
272 | returns empty list because Kodi python add-ons do not support constants yet
273 | """
274 | return []
275 |
--------------------------------------------------------------------------------
/libs/yattag/indentation.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | __all__ = ['indent']
4 |
5 | class TokenMeta(type):
6 |
7 | _token_classes = {}
8 |
9 | def __new__(cls, name, bases, attrs):
10 | kls = type.__new__(cls, name, bases, attrs)
11 | cls._token_classes[name] = kls
12 | return kls
13 |
14 | @classmethod
15 | def getclass(cls, name):
16 | return cls._token_classes[name]
17 |
18 | # need to proceed that way for Python 2/3 compatility:
19 | TokenBase = TokenMeta('TokenBase', (object,), {})
20 |
21 | class Token(TokenBase):
22 | regex = None
23 |
24 | def __init__(self, groupdict):
25 | self.content = groupdict[self.__class__.__name__]
26 |
27 | class Text(Token):
28 | regex = '[^<>]+'
29 | def __init__(self, *args, **kwargs):
30 | super(Text, self).__init__(*args, **kwargs)
31 | self._isblank = None
32 |
33 | @property
34 | def isblank(self):
35 | if self._isblank is None:
36 | self._isblank = not self.content.strip()
37 | return self._isblank
38 |
39 | class Comment(Token):
40 | regex = r').)*.?-->'
41 |
42 | class CData(Token):
43 | regex = r').*).?\]\]>'
44 |
45 | class Doctype(Token):
46 | regex = r'''"']+|"[^"]*"|'[^']*'))*>'''
47 |
48 | _open_tag_start = r'''
49 | <\s*
50 | (?P<{tag_name_key}>{tag_name_rgx})
51 | (\s+[^/><"=\s]+ # attribute
52 | (\s*=\s*
53 | (
54 | [^/><"=\s]+ | # unquoted attribute value
55 | ("[^"]*") | # " quoted attribute value
56 | ('[^']*') # ' quoted attribute value
57 | )
58 | )? # the attribute value is optional (we're forgiving)
59 | )*
60 | \s*'''
61 |
62 | class Script(Token):
63 | _end_script = r'<\s*/\s*script\s*>'
64 |
65 | regex = _open_tag_start.format(
66 | tag_name_key = 'script_ignore',
67 | tag_name_rgx = 'script',
68 | ) + r'>((?!({end_script})).)*.?{end_script}'.format(
69 | end_script = _end_script
70 | )
71 |
72 | class Style(Token):
73 | _end_style = r'<\s*/\s*style\s*>'
74 |
75 | regex = _open_tag_start.format(
76 | tag_name_key = 'style_ignore',
77 | tag_name_rgx = 'style',
78 | ) + r'>((?!({end_style})).)*.?{end_style}'.format(
79 | end_style = _end_style
80 | )
81 |
82 | class XMLDeclaration(Token):
83 | regex = _open_tag_start.format(
84 | tag_name_key = 'xmldecl_ignore',
85 | tag_name_rgx = r'\?\s*xml'
86 | ) + r'\?\s*>'
87 |
88 | class NamedTagTokenMeta(TokenMeta):
89 | def __new__(cls, name, bases, attrs):
90 | kls = TokenMeta.__new__(cls, name, bases, attrs)
91 | if name not in('NamedTagTokenBase', 'NamedTagToken'):
92 | kls.tag_name_key = 'tag_name_%s' % name
93 | kls.regex = kls.regex_template.format(
94 | tag_name_key = kls.tag_name_key,
95 | tag_name_rgx = kls.tag_name_rgx
96 | )
97 | return kls
98 |
99 | # need to proceed that way for Python 2/3 compatility
100 | NamedTagTokenBase = NamedTagTokenMeta(
101 | 'NamedTagTokenBase',
102 | (Token,),
103 | {'tag_name_rgx': r'[^/><"\s]+'}
104 | )
105 |
106 | class NamedTagToken(NamedTagTokenBase):
107 | def __init__(self, groupdict):
108 | super(NamedTagToken, self).__init__(groupdict)
109 | self.tag_name = groupdict[self.__class__.tag_name_key]
110 |
111 | class OpenTag(NamedTagToken):
112 | regex_template = _open_tag_start + '>'
113 |
114 | class SelfTag(NamedTagToken): # a self closing tag
115 | regex_template = _open_tag_start + r'/\s*>'
116 |
117 | class CloseTag(NamedTagToken):
118 | regex_template = r'<\s*/(?P<{tag_name_key}>{tag_name_rgx})(\s[^/><"]*)?>'
119 |
120 | class XMLTokenError(Exception):
121 | pass
122 |
123 | class Tokenizer(object):
124 |
125 | def __init__(self, token_classes):
126 | self.token_classes = token_classes
127 | self.token_names = [kls.__name__ for kls in token_classes]
128 | self.get_token = None
129 |
130 | def _compile_regex(self):
131 | self.get_token = re.compile(
132 | '|'.join(
133 | '(?P<%s>%s)' % (klass.__name__, klass.regex) for klass in self.token_classes
134 | ),
135 | re.X | re.I | re.S
136 | ).match
137 |
138 | def tokenize(self, string):
139 | if not self.get_token:
140 | self._compile_regex()
141 | result = []
142 | append = result.append
143 | while string:
144 | mobj = self.get_token(string)
145 | if mobj:
146 | groupdict = mobj.groupdict()
147 | class_name = next(name for name in self.token_names if groupdict[name])
148 | token = TokenMeta.getclass(class_name)(groupdict)
149 | append(token)
150 | string = string[len(token.content):]
151 | else:
152 | raise XMLTokenError("Unrecognized XML token near %s" % repr(string[:100]))
153 |
154 | return result
155 |
156 | tokenize = Tokenizer(
157 | (Text, Comment, CData, Doctype, XMLDeclaration, Script, Style, OpenTag, SelfTag, CloseTag)
158 | ).tokenize
159 |
160 | class TagMatcher(object):
161 |
162 | class SameNameMatcher(object):
163 | def __init__(self):
164 | self.unmatched_open = []
165 | self.matched = {}
166 |
167 | def sigclose(self, i):
168 | if self.unmatched_open:
169 | open_tag = self.unmatched_open.pop()
170 | self.matched[open_tag] = i
171 | self.matched[i] = open_tag
172 | return open_tag
173 | else:
174 | return None
175 |
176 | def sigopen(self, i):
177 | self.unmatched_open.append(i)
178 |
179 | def __init__(self, token_list, blank_is_text = False):
180 | self.token_list = token_list
181 | self.name_matchers = {}
182 | self.direct_text_parents = set()
183 |
184 | for i in range(len(token_list)):
185 | token = token_list[i]
186 | tpe = type(token)
187 | if tpe is OpenTag:
188 | self._get_name_matcher(token.tag_name).sigopen(i)
189 | elif tpe is CloseTag:
190 | self._get_name_matcher(token.tag_name).sigclose(i)
191 |
192 | # TODO move this somewhere else
193 | current_nodes = []
194 | for i in range(len(token_list)):
195 | token = token_list[i]
196 | tpe = type(token)
197 | if tpe is OpenTag and self.ismatched(i):
198 | current_nodes.append(i)
199 | elif tpe is CloseTag and self.ismatched(i):
200 | current_nodes.pop()
201 | elif tpe is Text and (blank_is_text or not token.isblank):
202 | if current_nodes:
203 | self.direct_text_parents.add(current_nodes[-1])
204 |
205 | def _get_name_matcher(self, tag_name):
206 | try:
207 | return self.name_matchers[tag_name]
208 | except KeyError:
209 | self.name_matchers[tag_name] = name_matcher = self.__class__.SameNameMatcher()
210 | return name_matcher
211 |
212 | def ismatched(self, i):
213 | return i in self.name_matchers[self.token_list[i].tag_name].matched
214 |
215 | def directly_contains_text(self, i):
216 | return i in self.direct_text_parents
217 |
218 |
219 | def indent(string, indentation = ' ', newline = '\n', indent_text = False, blank_is_text = False):
220 | """
221 | takes a string representing a html or xml document and returns
222 | a well indented version of it
223 |
224 | arguments:
225 | - string: the string to process
226 | - indentation: the indentation unit (default to two spaces)
227 | - newline: the string to be use for new lines
228 | (default to '\\n', could be set to '\\r\\n' for example)
229 | - indent_text:
230 |
231 | if True, text nodes will be indented:
232 |
233 | Hello
234 |
235 | would result in
236 |
237 |
238 | hello
239 |
240 |
241 | if False, text nodes won't be indented, and the content
242 | of any node directly containing text will be unchanged:
243 |
244 | Hello
will be unchanged
245 |
246 | Hello world!
will be unchanged
247 | since ' world!' is directly contained in the node.
248 |
249 | This is the default since that's generally what you want for HTML.
250 |
251 | - blank_is_text:
252 | if False, completely blank texts are ignored. That is the default.
253 | """
254 | tokens = tokenize(string)
255 | tag_matcher = TagMatcher(tokens, blank_is_text = blank_is_text)
256 | ismatched = tag_matcher.ismatched
257 | directly_contains_text = tag_matcher.directly_contains_text
258 | result = []
259 | append = result.append
260 | level = 0
261 | sameline = 0
262 | was_just_opened = False
263 | tag_appeared = False
264 | def _indent():
265 | if tag_appeared:
266 | append(newline)
267 | for i in range(level):
268 | append(indentation)
269 | for i,token in enumerate(tokens):
270 | tpe = type(token)
271 | if tpe is Text:
272 | if blank_is_text or not token.isblank:
273 | if not sameline:
274 | _indent()
275 | append(token.content)
276 | was_just_opened = False
277 | elif tpe is OpenTag and ismatched(i):
278 | was_just_opened = True
279 | if sameline:
280 | sameline += 1
281 | else:
282 | _indent()
283 | if not indent_text and directly_contains_text(i):
284 | sameline = sameline or 1
285 | append(token.content)
286 | level += 1
287 | tag_appeared = True
288 | elif tpe is CloseTag and ismatched(i):
289 | level -= 1
290 | tag_appeared = True
291 | if sameline:
292 | sameline -= 1
293 | elif not was_just_opened:
294 | _indent()
295 | append(token.content)
296 | was_just_opened = False
297 | else:
298 | if not sameline:
299 | _indent()
300 | append(token.content)
301 | was_just_opened = False
302 | tag_appeared = True
303 | return ''.join(result)
304 |
305 | if __name__ == '__main__':
306 | import sys
307 | print(indent(sys.stdin.read()))
308 |
309 |
--------------------------------------------------------------------------------