# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import re import codecs import logging from HTMLParser import HTMLParser __constructors = [] class Entity(object): ''' Abstraction layer for a localizable entity. Currently supported are grammars of the form: 1: pre white space 2: pre comments 3: entity definition 4: entity key (name) 5: entity value 6: post comment (and white space) in the same line (dtd only) <--[1] <--[2] <-------[3]---------><------[6]------> ''' def __init__(self, contents, pp, span, pre_ws_span, pre_comment_span, def_span, key_span, val_span, post_span): self.contents = contents self.span = span self.pre_ws_span = pre_ws_span self.pre_comment_span = pre_comment_span self.def_span = def_span self.key_span = key_span self.val_span = val_span self.post_span = post_span self.pp = pp pass # getter helpers def get_all(self): return self.contents[self.span[0]:self.span[1]] def get_pre_ws(self): return self.contents[self.pre_ws_span[0]:self.pre_ws_span[1]] def get_pre_comment(self): return self.contents[self.pre_comment_span[0]: self.pre_comment_span[1]] def get_def(self): return self.contents[self.def_span[0]:self.def_span[1]] def get_key(self): return self.contents[self.key_span[0]:self.key_span[1]] def get_val(self): return self.pp(self.contents[self.val_span[0]:self.val_span[1]]) def get_raw_val(self): return self.contents[self.val_span[0]:self.val_span[1]] def get_post(self): return self.contents[self.post_span[0]:self.post_span[1]] # getters all = property(get_all) pre_ws = property(get_pre_ws) pre_comment = property(get_pre_comment) definition = property(get_def) key = property(get_key) val = property(get_val) raw_val = property(get_raw_val) post = property(get_post) def __repr__(self): return self.key class Junk(object): ''' An almost-Entity, representing junk data that we didn't parse. This way, we can signal bad content as stuff we don't understand. And the either fix that, or report real bugs in localizations. ''' junkid = 0 def __init__(self, contents, span): self.contents = contents self.span = span self.pre_ws = self.pre_comment = self.definition = self.post = '' self.__class__.junkid += 1 self.key = '_junk_%d_%d-%d' % (self.__class__.junkid, span[0], span[1]) # getter helpers def get_all(self): return self.contents[self.span[0]:self.span[1]] # getters all = property(get_all) val = property(get_all) def __repr__(self): return self.key class Parser: canMerge = True def __init__(self): if not hasattr(self, 'encoding'): self.encoding = 'utf-8' pass def readFile(self, file): f = codecs.open(file, 'r', self.encoding) try: self.contents = f.read() except UnicodeDecodeError, e: (logging.getLogger('locales') .error("Can't read file: " + file + '; ' + str(e))) self.contents = u'' f.close() def readContents(self, contents): (self.contents, length) = codecs.getdecoder(self.encoding)(contents) def parse(self): l = [] m = {} for e in self: m[e.key] = len(l) l.append(e) return (l, m) def postProcessValue(self, val): return val def __iter__(self): contents = self.contents offset = 0 self.header, offset = self.getHeader(contents, offset) self.footer = '' entity, offset = self.getEntity(contents, offset) while entity: yield entity entity, offset = self.getEntity(contents, offset) f = self.reFooter.match(contents, offset) if f: self.footer = f.group() offset = f.end() if len(contents) > offset: yield Junk(contents, (offset, len(contents))) pass def getHeader(self, contents, offset): header = '' h = self.reHeader.match(contents) if h: header = h.group() offset = h.end() return (header, offset) def getEntity(self, contents, offset): m = self.reKey.match(contents, offset) if m: offset = m.end() entity = self.createEntity(contents, m) return (entity, offset) # first check if footer has a non-empy match, # 'cause then we don't find junk m = self.reFooter.match(contents, offset) if m and m.end() > offset: return (None, offset) m = self.reKey.search(contents, offset) if m: # we didn't match, but search, so there's junk between offset # and start. We'll match() on the next turn junkend = m.start() return (Junk(contents, (offset, junkend)), junkend) return (None, offset) def createEntity(self, contents, m): return Entity(contents, self.postProcessValue, *[m.span(i) for i in xrange(7)]) def getParser(path): for item in __constructors: if re.search(item[0], path): return item[1] raise UserWarning("Cannot find Parser") # Subgroups of the match will: # 1: pre white space # 2: pre comments # 3: entity definition # 4: entity key (name) # 5: entity value # 6: post comment (and white space) in the same line (dtd only) # <--[1] # <--[2] # # # <-------[3]---------><------[6]------> class DTDParser(Parser): # http://www.w3.org/TR/2006/REC-xml11-20060816/#NT-NameStartChar # ":" | [A-Z] | "_" | [a-z] | # [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] # | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | # [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | # [#x10000-#xEFFFF] CharMinusDash = u'\x09\x0A\x0D\u0020-\u002C\u002E-\uD7FF\uE000-\uFFFD' XmlComment = '' % CharMinusDash NameStartChar = u':A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF' + \ u'\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F' + \ u'\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD' # + \U00010000-\U000EFFFF seems to be unsupported in python # NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | # [#x0300-#x036F] | [#x203F-#x2040] NameChar = NameStartChar + ur'\-\.0-9' + u'\xB7\u0300-\u036F\u203F-\u2040' Name = '[' + NameStartChar + '][' + NameChar + ']*' reKey = re.compile('(?:(?P
\s*)(?P(?:' + XmlComment +
                       '\s*)*)(?P' + Name +
                       ')\s+(?P\"[^\"]*\"|\'[^\']*\'?)\s*>)'
                       '(?P[ \t]*(?:' + XmlComment + '\s*)*\n?)?)',
                       re.DOTALL)
    # add BOM to DTDs, details in bug 435002
    reHeader = re.compile(u'^\ufeff?'
                          u'(\s*)?', re.S)
    reFooter = re.compile('\s*(\s*)*$')
    rePE = re.compile('(?:(\s*)((?:' + XmlComment + '\s*)*)'
                      '(\s*%' + Name +
                      ';)([ \t]*(?:' + XmlComment + '\s*)*\n?)?)')

    def getEntity(self, contents, offset):
        '''
        Overload Parser.getEntity to special-case ParsedEntities.
        Just check for a parsed entity if that method claims junk.

        
        %foo;
        '''
        entity, inneroffset = Parser.getEntity(self, contents, offset)
        if (entity and isinstance(entity, Junk)) or entity is None:
            m = self.rePE.match(contents, offset)
            if m:
                inneroffset = m.end()
                entity = Entity(contents, self.postProcessValue,
                                *[m.span(i) for i in xrange(7)])
        return (entity, inneroffset)

    def createEntity(self, contents, m):
        valspan = m.span('val')
        valspan = (valspan[0]+1, valspan[1]-1)
        return Entity(contents, self.postProcessValue, m.span(),
                      m.span('pre'), m.span('precomment'),
                      m.span('entity'), m.span('key'), valspan,
                      m.span('post'))


class PropertiesParser(Parser):
    escape = re.compile(r'\\((?Pu[0-9a-fA-F]{1,4})|'
                        '(?P\n\s*)|(?P.))', re.M)
    known_escapes = {'n': '\n', 'r': '\r', 't': '\t', '\\': '\\'}

    def __init__(self):
        self.reKey = re.compile('^(\s*)'
                                '((?:[#!].*?\n\s*)*)'
                                '([^#!\s\n][^=:\n]*?)\s*[:=][ \t]*', re.M)
        self.reHeader = re.compile('^\s*([#!].*\s*)+')
        self.reFooter = re.compile('\s*([#!].*\s*)*$')
        self._escapedEnd = re.compile(r'\\+$')
        self._trailingWS = re.compile(r'[ \t]*$')
        Parser.__init__(self)

    def getHeader(self, contents, offset):
        header = ''
        h = self.reHeader.match(contents, offset)
        if h:
            candidate = h.group()
            if 'http://mozilla.org/MPL/2.0/' in candidate or \
                    'LICENSE BLOCK' in candidate:
                header = candidate
                offset = h.end()
        return (header, offset)

    def getEntity(self, contents, offset):
        # overwritten to parse values line by line
        m = self.reKey.match(contents, offset)
        if m:
            offset = m.end()
            while True:
                endval = nextline = contents.find('\n', offset)
                if nextline == -1:
                    endval = offset = len(contents)
                    break
                # is newline escaped?
                _e = self._escapedEnd.search(contents, offset, nextline)
                offset = nextline + 1
                if _e is None:
                    break
                # backslashes at end of line, if 2*n, not escaped
                if len(_e.group()) % 2 == 0:
                    break
            # strip trailing whitespace
            ws = self._trailingWS.search(contents, m.end(), offset)
            if ws:
                endval -= ws.end() - ws.start()
            entity = Entity(contents, self.postProcessValue,
                            (m.start(), offset),   # full span
                            m.span(1),  # leading whitespan
                            m.span(2),  # leading comment span
                            (m.start(3), offset),   # entity def span
                            m.span(3),   # key span
                            (m.end(), endval),   # value span
                            (offset, offset))  # post comment span, empty
            return (entity, offset)
        m = self.reKey.search(contents, offset)
        if m:
            # we didn't match, but search, so there's junk between offset
            # and start. We'll match() on the next turn
            junkend = m.start()
            return (Junk(contents, (offset, junkend)), junkend)
        return (None, offset)

    def postProcessValue(self, val):

        def unescape(m):
            found = m.groupdict()
            if found['uni']:
                return unichr(int(found['uni'][1:], 16))
            if found['nl']:
                return ''
            return self.known_escapes.get(found['single'], found['single'])
        val = self.escape.sub(unescape, val)
        return val


class DefinesParser(Parser):
    # can't merge, #unfilter needs to be the last item, which we don't support
    canMerge = False

    def __init__(self):
        self.reKey = re.compile('^(\s*)((?:^#(?!define\s).*\s*)*)'
                                '(#define[ \t]+(\w+)[ \t]+(.*?))([ \t]*$\n?)',
                                re.M)
        self.reHeader = re.compile('^\s*(#(?!define\s).*\s*)*')
        self.reFooter = re.compile('\s*(#(?!define\s).*\s*)*$', re.M)
        Parser.__init__(self)


class IniParser(Parser):
    '''
    Parse files of the form:
    # initial comment
    [cat]
    whitespace*
    #comment
    string=value
    ...
    '''
    def __init__(self):
        self.reHeader = re.compile('^((?:\s*|[;#].*)\n)*\[.+?\]\n', re.M)
        self.reKey = re.compile('(\s*)((?:[;#].*\n\s*)*)((.+?)=(.*))(\n?)')
        self.reFooter = re.compile('\s*')
        Parser.__init__(self)


DECL, COMMENT, START, END, CONTENT = range(5)


class BookmarksParserInner(HTMLParser):

    class Token(object):
        _type = None
        content = ''

        def __str__(self):
            return self.content

    class DeclToken(Token):
        _type = DECL

        def __init__(self, decl):
            self.content = decl
            pass

        def __str__(self):
            return '' % self.content
        pass

    class CommentToken(Token):
        _type = COMMENT

        def __init__(self, comment):
            self.content = comment
            pass

        def __str__(self):
            return '' % self.content
        pass

    class StartToken(Token):
        _type = START

        def __init__(self, tag, attrs, content):
            self.tag = tag
            self.attrs = dict(attrs)
            self.content = content
            pass
        pass

    class EndToken(Token):
        _type = END

        def __init__(self, tag):
            self.tag = tag
            pass

        def __str__(self):
            return '' % self.tag.upper()
        pass

    class ContentToken(Token):
        _type = CONTENT

        def __init__(self, content):
            self.content = content
            pass
        pass

    def __init__(self):
        HTMLParser.__init__(self)
        self.tokens = []

    def parse(self, contents):
        self.tokens = []
        self.feed(contents)
        self.close()
        return self.tokens

    # Called when we hit an end DL tag to reset the folder selections
    def handle_decl(self, decl):
        self.tokens.append(self.DeclToken(decl))

    # Called when we hit an end DL tag to reset the folder selections
    def handle_comment(self, comment):
        self.tokens.append(self.CommentToken(comment))

    def handle_starttag(self, tag, attrs):
        self.tokens.append(self.StartToken(tag, attrs,
                                           self.get_starttag_text()))

    # Called when text data is encountered
    def handle_data(self, data):
        if self.tokens[-1]._type == CONTENT:
            self.tokens[-1].content += data
        else:
            self.tokens.append(self.ContentToken(data))

    def handle_charref(self, data):
        self.handle_data('&#%s;' % data)

    def handle_entityref(self, data):
        self.handle_data('&%s;' % data)

    # Called when we hit an end DL tag to reset the folder selections
    def handle_endtag(self, tag):
        self.tokens.append(self.EndToken(tag))


class BookmarksParser(Parser):
    canMerge = False

    class BMEntity(object):
        def __init__(self, key, val):
            self.key = key
            self.val = val

    def __iter__(self):
        p = BookmarksParserInner()
        tks = p.parse(self.contents)
        i = 0
        k = []
        for i in xrange(len(tks)):
            t = tks[i]
            if t._type == START:
                k.append(t.tag)
                keys = t.attrs.keys()
                keys.sort()
                for attrname in keys:
                    yield self.BMEntity('.'.join(k) + '.@' + attrname,
                                        t.attrs[attrname])
                if i + 1 < len(tks) and tks[i+1]._type == CONTENT:
                    i += 1
                    t = tks[i]
                    v = t.content.strip()
                    if v:
                        yield self.BMEntity('.'.join(k), v)
            elif t._type == END:
                k.pop()


__constructors = [('\\.dtd$', DTDParser()),
                  ('\\.properties$', PropertiesParser()),
                  ('\\.ini$', IniParser()),
                  ('\\.inc$', DefinesParser()),
                  ('bookmarks\\.html$', BookmarksParser())]