1#!/usr/bin/env python
2
3"""
4Looks for strings with multiple substitution arguments (%d, &s, etc)
5and replaces them with positional arguments (%1$d, %2$s).
6"""
7
8import os.path
9import re
10import xml.parsers.expat
11
12class PositionalArgumentFixer:
13    def matches(self, file_path):
14        dirname, basename = os.path.split(file_path)
15        dirname = os.path.split(dirname)[1]
16        return dirname.startswith("values") and basename.endswith(".xml")
17
18    def consume(self, xml_path, input):
19        parser = xml.parsers.expat.ParserCreate("utf-8")
20        locator = SubstitutionArgumentLocator(parser)
21        parser.returns_unicode = True
22        parser.StartElementHandler = locator.start_element
23        parser.EndElementHandler = locator.end_element
24        parser.CharacterDataHandler = locator.character_data
25        parser.Parse(input)
26
27        if len(locator.arguments) > 0:
28            output = ""
29            last_index = 0
30            for arg in locator.arguments:
31                output += input[last_index:arg.start]
32                output += "%{0}$".format(arg.number)
33                last_index = arg.start + 1
34            output += input[last_index:]
35            print "fixed {0}".format(xml_path)
36            return output
37        return input
38
39class Argument:
40    def __init__(self, start, number):
41        self.start = start
42        self.number = number
43
44class SubstitutionArgumentLocator:
45    """Callback class for xml.parsers.expat which records locations of
46    substitution arguments in strings when there are more than 1 of
47    them in a single <string> tag (and they are not positional).
48    """
49    def __init__(self, parser):
50        self.arguments = []
51        self._parser = parser
52        self._depth = 0
53        self._within_string = False
54        self._current_arguments = []
55        self._next_number = 1
56
57    def start_element(self, tag_name, attrs):
58        self._depth += 1
59        if self._depth == 2 and tag_name == "string" and "translateable" not in attrs:
60            self._within_string = True
61
62    def character_data(self, data):
63        if self._within_string:
64            for m in re.finditer("%[-#+ 0,(]?\d*[bBhHsScCdoxXeEfgGaAtTn]", data):
65                start, end = m.span()
66                self._current_arguments.append(\
67                        Argument(self._parser.CurrentByteIndex + start, self._next_number))
68                self._next_number += 1
69
70    def end_element(self, tag_name):
71        if self._within_string and self._depth == 2:
72            if len(self._current_arguments) > 1:
73                self.arguments += self._current_arguments
74            self._current_arguments = []
75            self._within_string = False
76            self._next_number = 1
77        self._depth -= 1
78