PageRenderTime 24ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/ttkit/aresource.py

https://bitbucket.org/bootz/weblate
Python | 326 lines | 264 code | 15 blank | 47 comment | 20 complexity | 3b148563a47b6e87e60d61d7c50b9475 MD5 | raw file
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. #
  4. # Copyright 2012 Michal Čihař
  5. #
  6. # This file is part of the Translate Toolkit.
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation; either version 2 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with this program; if not, see <http://www.gnu.org/licenses/>.
  20. """module for handling Android resource files"""
  21. from lxml import etree
  22. import re
  23. from translate.storage import lisa
  24. from translate.storage import base
  25. from translate.lang import data
  26. EOF = None
  27. WHITESPACE = ' \n\t' # Whitespace that we collapse
  28. MULTIWHITESPACE = re.compile('[ \n\t]{2}')
  29. class AndroidResourceUnit(base.TranslationUnit):
  30. """A single term in the Android resource file."""
  31. rootNode = "string"
  32. languageNode = "string"
  33. def __init__(self, source, empty=False, xmlelement=None, **kwargs):
  34. if xmlelement is not None:
  35. self.xmlelement = xmlelement
  36. else:
  37. self.xmlelement = etree.Element(self.rootNode)
  38. self.xmlelement.tail = '\n'
  39. if source is not None:
  40. self.setid(source)
  41. super(AndroidResourceUnit, self).__init__(source)
  42. def getid(self):
  43. return self.xmlelement.get("name")
  44. def getcontext(self):
  45. return self.xmlelement.get("name")
  46. def setid(self, newid):
  47. return self.xmlelement.set("name", newid)
  48. def unescape(self, text):
  49. '''
  50. Remove escaping from Android resource.
  51. Code stolen from android2po
  52. <https://github.com/miracle2k/android2po>
  53. '''
  54. # Return text for empty elements
  55. if text is None:
  56. return ''
  57. # We need to collapse multiple whitespace while paying
  58. # attention to Android's quoting and escaping.
  59. space_count = 0
  60. active_quote = False
  61. active_percent = False
  62. active_escape = False
  63. formatted = False
  64. i = 0
  65. text = list(text) + [EOF]
  66. while i < len(text):
  67. c = text[i]
  68. # Handle whitespace collapsing
  69. if c is not EOF and c in WHITESPACE:
  70. space_count += 1
  71. elif space_count > 1:
  72. # Remove duplicate whitespace; Pay attention: We
  73. # don't do this if we are currently inside a quote,
  74. # except for one special case: If we have unbalanced
  75. # quotes, e.g. we reach eof while a quote is still
  76. # open, we *do* collapse that trailing part; this is
  77. # how Android does it, for some reason.
  78. if not active_quote or c is EOF:
  79. # Replace by a single space, will get rid of
  80. # non-significant newlines/tabs etc.
  81. text[i-space_count : i] = ' '
  82. i -= space_count - 1
  83. space_count = 0
  84. elif space_count == 1:
  85. # At this point we have a single whitespace character,
  86. # but it might be a newline or tab. If we write this
  87. # kind of insignificant whitespace into the .po file,
  88. # it will be considered significant on import. So,
  89. # make sure that this kind of whitespace is always a
  90. # standard space.
  91. text[i-1] = ' '
  92. space_count = 0
  93. else:
  94. space_count = 0
  95. # Handle quotes
  96. if c == '"' and not active_escape:
  97. active_quote = not active_quote
  98. del text[i]
  99. i -= 1
  100. # If the string is run through a formatter, it will have
  101. # percentage signs for String.format
  102. if c == '%' and not active_escape:
  103. active_percent = not active_percent
  104. elif not active_escape and active_percent:
  105. formatted = True
  106. active_percent = False
  107. # Handle escapes
  108. if c == '\\':
  109. if not active_escape:
  110. active_escape = True
  111. else:
  112. # A double-backslash represents a single;
  113. # simply deleting the current char will do.
  114. del text[i]
  115. i -= 1
  116. active_escape = False
  117. else:
  118. if active_escape:
  119. # Handle the limited amount of escape codes
  120. # that we support.
  121. # TODO: What about \r, or \r\n?
  122. if c is EOF:
  123. # Basically like any other char, but put
  124. # this first so we can use the ``in`` operator
  125. # in the clauses below without issue.
  126. pass
  127. elif c == 'n' or c == 'N':
  128. text[i-1 : i+1] = '\n' # an actual newline
  129. i -= 1
  130. elif c == 't' or c == 'T':
  131. text[i-1 : i+1] = '\t' # an actual tab
  132. i -= 1
  133. elif c == ' ':
  134. text[i-1 : i+1] = ' ' # an actual space
  135. i -= 1
  136. elif c in '"\'@':
  137. text[i-1 : i] = '' # remove the backslash
  138. i -= 1
  139. elif c == 'u':
  140. # Unicode sequence. Android is nice enough to deal
  141. # with those in a way which let's us just capture
  142. # the next 4 characters and raise an error if they
  143. # are not valid (rather than having to use a new
  144. # state to parse the unicode sequence).
  145. # Exception: In case we are at the end of the
  146. # string, we support incomplete sequences by
  147. # prefixing the missing digits with zeros.
  148. # Note: max(len()) is needed in the slice due to
  149. # trailing ``None`` element.
  150. max_slice = min(i+5, len(text)-1)
  151. codepoint_str = "".join(text[i+1 : max_slice])
  152. if len(codepoint_str) < 4:
  153. codepoint_str = u"0" * (4-len(codepoint_str)) + codepoint_str
  154. try:
  155. # We can't trust int() to raise a ValueError,
  156. # it will ignore leading/trailing whitespace.
  157. if not codepoint_str.isalnum():
  158. raise ValueError(codepoint_str)
  159. codepoint = unichr(int(codepoint_str, 16))
  160. except ValueError:
  161. raise ValueError('bad unicode escape sequence')
  162. text[i-1 : max_slice] = codepoint
  163. i -= 1
  164. else:
  165. # All others, remove, like Android does as well.
  166. text[i-1 : i+1] = ''
  167. i -= 1
  168. active_escape = False
  169. i += 1
  170. # Join the string together again, but w/o EOF marker
  171. return "".join(text[:-1])
  172. def escape(self, text):
  173. '''
  174. Escape all the characters which need to be escaped in an Android XML file.
  175. '''
  176. if text is None:
  177. return
  178. if len(text) == 0:
  179. return ''
  180. text = text.replace('\\', '\\\\')
  181. text = text.replace('\n', '\\n')
  182. # This will add non intrusive real newlines to
  183. # ones in translation improving readability of result
  184. text = text.replace(' \\n', '\n\\n')
  185. text = text.replace('\t', '\\t')
  186. text = text.replace('\'', '\\\'')
  187. text = text.replace('"', '\\"')
  188. # @ needs to be escaped at start
  189. if text.startswith('@'):
  190. text = '\\@' + text[1:]
  191. # Quote strings with more whitespace
  192. if text[0] in WHITESPACE or text[-1] in WHITESPACE or len(MULTIWHITESPACE.findall(text)) > 0:
  193. return '"%s"' % text
  194. return text
  195. def setsource(self, source):
  196. super(AndroidResourceUnit, self).setsource(source)
  197. def getsource(self, lang=None):
  198. if (super(AndroidResourceUnit, self).source is None):
  199. return self.target
  200. else:
  201. return super(AndroidResourceUnit, self).source
  202. source = property(getsource, setsource)
  203. def settarget(self, target):
  204. if '<' in target:
  205. # Handle text with possible markup
  206. target = target.replace('&', '&amp;')
  207. try:
  208. # Try as XML
  209. newstring = etree.fromstring('<string>%s</string>' % target)
  210. except:
  211. # Fallback to string with XML escaping
  212. target = target.replace('<', '&lt;')
  213. newstring = etree.fromstring('<string>%s</string>' % target)
  214. # Update text
  215. if newstring.text is None:
  216. self.xmlelement.text = ''
  217. else:
  218. self.xmlelement.text = newstring.text
  219. # Remove old elements
  220. for x in self.xmlelement.iterchildren():
  221. self.xmlelement.remove(x)
  222. # Add new elements
  223. for x in newstring.iterchildren():
  224. self.xmlelement.append(x)
  225. else:
  226. # Handle text only
  227. self.xmlelement.text = self.escape(target)
  228. super(AndroidResourceUnit, self).settarget(target)
  229. def gettarget(self, lang=None):
  230. # Grab inner text
  231. target = self.unescape(self.xmlelement.text or u'')
  232. # Include markup as well
  233. target += u''.join([data.forceunicode(etree.tostring(child, encoding='utf-8')) for child in self.xmlelement.iterchildren()])
  234. return target
  235. target = property(gettarget, settarget)
  236. def getlanguageNode(self, lang=None, index=None):
  237. return self.xmlelement
  238. def createfromxmlElement(cls, element):
  239. term = cls(None, xmlelement = element)
  240. return term
  241. createfromxmlElement = classmethod(createfromxmlElement)
  242. # Notes are handled as previous sibling comments.
  243. def addnote(self, text, origin=None, position="append"):
  244. if origin in ['programmer', 'developer', 'source code', None]:
  245. self.xmlelement.addprevious(etree.Comment(text))
  246. else:
  247. return super(AndroidResourceUnit, self).addnote(text, origin=origin,
  248. position=position)
  249. def getnotes(self, origin=None):
  250. if origin in ['programmer', 'developer', 'source code', None]:
  251. comments = []
  252. if (self.xmlelement is not None):
  253. prevSibling = self.xmlelement.getprevious()
  254. while ((prevSibling is not None) and (prevSibling.tag is etree.Comment)):
  255. comments.insert(0, prevSibling.text)
  256. prevSibling = prevSibling.getprevious()
  257. return u'\n'.join(comments)
  258. else:
  259. return super(AndroidResourceUnit, self).getnotes(origin)
  260. def removenotes(self):
  261. if ((self.xmlelement is not None) and (self.xmlelement.getparent is not None)):
  262. prevSibling = self.xmlelement.getprevious()
  263. while ((prevSibling is not None) and (prevSibling.tag is etree.Comment)):
  264. prevSibling.getparent().remove(prevSibling)
  265. prevSibling = self.xmlelement.getprevious()
  266. super(AndroidResourceUnit, self).removenotes()
  267. def __str__(self):
  268. return etree.tostring(self.xmlelement, pretty_print=True,
  269. encoding='utf-8')
  270. def __eq__(self, other):
  271. return (str(self) == str(other))
  272. class AndroidResourceFile(lisa.LISAfile):
  273. """Class representing a Android resource file store."""
  274. UnitClass = AndroidResourceUnit
  275. Name = _("Android String Resource")
  276. Mimetypes = ["application/xml"]
  277. Extensions = ["xml"]
  278. rootNode = "resources"
  279. bodyNode = "resources"
  280. XMLskeleton = '''<?xml version="1.0" encoding="utf-8"?>
  281. <resources></resources>'''
  282. def initbody(self):
  283. """Initialises self.body so it never needs to be retrieved from the
  284. XML again."""
  285. self.namespace = self.document.getroot().nsmap.get(None, None)
  286. self.body = self.document.getroot()