Package translate :: Package storage :: Module php

[hide private]
[frames] | no frames]

# Source Code for Module translate.storage.php

      1  #!/usr/bin/env python
      2  # -*- coding: utf-8 -*-
      3  #
      4  # Copyright 2004-2008 Zuza Software Foundation
      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
     21  """Classes that hold units of PHP localisation files L{phpunit} or entire files
     22     L{phpfile}. These files are used in translating many PHP based applications.
     23
     24     Only PHP files written with these conventions are supported::
     25        $lang['item'] = "vale";  # Array of values
     26        $some_entity = "value";  # Named variables
     27
     28     The parser does not support other array conventions such as::
     29        $lang = array(
     30           'item1' => 'value1',
     31           'item2' => 'value2',
     32        );
     33
     34     The working of PHP strings and specifically the escaping conventions which
     35     differ between single quote (') and double quote (") characters are outlined
     36     in the PHP documentation for the U{String type<http://www.php.net/language.types.string>}
     37  """
     38
     39  from translate.storage import base
     40  import re
     41
     42 -def phpencode(text, quotechar="'"):
     43      """convert Python string to PHP escaping
     44
     45      The encoding is implemented for
     46      U{'single quote'<http://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.single>}
     47      and U{"double quote"<http://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.double>}
     48      syntax.
     49
     50      heredoc and nowdoc are not implemented and it is not certain whether this would
     51      ever be needed for PHP localisation needs.
     52      """
     53      if not text:
     54          return text
     55      if quotechar == '"':
     56          # \n may be converted to \\n but we don't.  This allows us to preserve pretty layout that might have appeared in muliline entries
     57          # we might lose some "blah\nblah" layouts but that's probably not the most frequent use case. See bug 588
     58          escapes = (("\\", "\\\\"), ("\r", "\\r"), ("\t", "\\t"), ("\v", "\\v"), ("\f", "\\f"), ("\\\\$", "\\$"), ('"', '\\"'), ("\\\\", "\\"))
     59          for a, b in escapes:
     60              text = text.replace(a, b)
     61          return text
     62      else:
     63          return text.replace("%s" % quotechar, "\\%s" % quotechar)
     64
     65 -def phpdecode(text, quotechar="'"):
     66      """convert PHP escaped string to a Python string"""
     67      def decode_octal_hex(match):
     68          """decode Octal \NNN and Hex values"""
     69          if match.groupdict().has_key("octal"):
     70              return match.groupdict()['octal'].decode("string_escape")
     71          elif match.groupdict().has_key("hex"):
     72              return match.groupdict()['hex'].decode("string_escape")
     73          else:
     74              return match.group
     75
     76      if not text:
     77          return text
     78      if quotechar == '"':
     79          # We do not escape \$ as it is used by variables and we can't roundtrip that item.
     80          text = text.replace('\\"', '"').replace("\\\\", "\\")
     81          text = text.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t").replace("\\v", "\v").replace("\\f", "\f")
     82          text = re.sub(r"(?P<octal>\\[0-7]{1,3})", decode_octal_hex, text)
     83          text = re.sub(r"(?P<hex>\\x[0-9A-Fa-f]{1,2})", decode_octal_hex, text)
     84      else:
     85          text = text.replace("\\'", "'").replace("\\\\", "\\")
     86      return text
     87
     88 -class phpunit(base.TranslationUnit):
     89      """a unit of a PHP file i.e. a name and value, and any comments
     90      associated"""
     91 -    def __init__(self, source=""):
     92          """construct a blank phpunit"""
     93          self.escape_type = None
     94          super(phpunit, self).__init__(source)
     95          self.name = ""
     96          self.value = ""
     97          self._comments = []
     98          self.source = source
     99
    100 -    def setsource(self, source):
    101          """Sets the source AND the target to be equal"""
    102          self.value = phpencode(source, self.escape_type)
    103
    104 -    def getsource(self):
    105          return phpdecode(self.value, self.escape_type)
    106      source = property(getsource, setsource)
    107
    108 -    def settarget(self, target):
    109          """Note: this also sets the .source attribute!"""
    110          # TODO: shouldn't this just call the .source property? no quoting done here...
    111          self.source = target
    112
    113 -    def gettarget(self):
    114          return self.source
    115      target = property(gettarget, settarget)
    116
    117 -    def __str__(self):
    118          """convert to a string. double check that unicode is handled somehow here"""
    119          source = self.getoutput()
    120          if isinstance(source, unicode):
    121              return source.encode(getattr(self, "encoding", "UTF-8"))
    122          return source
    123
    124 -    def getoutput(self):
    125          """convert the unit back into formatted lines for a php file"""
    126          return "".join(self._comments + ["%s='%s';\n" % (self.name, self.value)])
    127
    128 -    def addlocation(self, location):
    129          self.name = location
    130
    131 -    def getlocations(self):
    132          return [self.name]
    133
    134 -    def addnote(self, note, origin=None):
    135          self._comments.append(note)
    136
    137 -    def getnotes(self, origin=None):
    138          return '\n'.join(self._comments)
    139
    140 -    def removenotes(self):
    141          self._comments = []
    142
    143 -    def isblank(self):
    144          """Returns whether this is a blank element, containing only comments."""
    145          return not (self.name or self.value)
    146
    147 -class phpfile(base.TranslationStore):
    148      """This class represents a PHP file, made up of phpunits"""
    149      UnitClass = phpunit
    150 -    def __init__(self, inputfile=None, encoding='utf-8'):
    151          """construct a phpfile, optionally reading in from inputfile"""
    152          super(phpfile, self).__init__(unitclass = self.UnitClass)
    153          self.filename = getattr(inputfile, 'name', '')
    154          self._encoding = encoding
    155          if inputfile is not None:
    156              phpsrc = inputfile.read()
    157              inputfile.close()
    158              self.parse(phpsrc)
    159
    160 -    def parse(self, phpsrc):
    161          """Read the source of a PHP file in and include them as units"""
    162          newunit = phpunit()
    163          lastvalue = ""
    164          value = ""
    165          comment = []
    166          invalue = False
    167          incomment = False
    168          valuequote = "" # either ' or "
    169          for line in phpsrc.decode(self._encoding).split("\n"):
    170              commentstartpos = line.find("/*")
    171              commentendpos = line.rfind("*/")
    172              if commentstartpos != -1:
    173                  incomment = True
    174                  if commentendpos != -1:
    175                      newunit.addnote(line[commentstartpos:commentendpos].strip(), "developer")
    176                      incomment = False
    177                  else:
    178                      newunit.addnote(line[commentstartpos:].strip(), "developer")
    179              if commentendpos != -1 and incomment:
    180                  newunit.addnote(line[:commentendpos+2].strip(), "developer")
    181                  incomment = False
    182              if incomment and commentstartpos == -1:
    183                  newunit.addnote(line.strip(), "developer")
    184                  continue
    185              equalpos = line.find("=")
    186              hashpos = line.find("#")
    187              if 0 <= hashpos < equalpos:
    188                  # Assume that this is a '#' comment line
    189                  newunit.addnote(line.strip(), "developer")
    190                  continue
    191              if equalpos != -1 and not invalue:
    192                  newunit.addlocation(line[:equalpos].strip().replace(" ", ""))
    193                  value = line[equalpos+1:].lstrip()[1:]
    194                  valuequote = line[equalpos+1:].lstrip()[0]
    195                  lastvalue = ""
    196                  invalue = True
    197              else:
    198                  if invalue:
    199                      value = line
    200              colonpos = value.rfind(";")
    201              while colonpos != -1:
    202                  if value[colonpos-1] == valuequote:
    203                      newunit.value = lastvalue + value[:colonpos-1]
    204                      newunit.escape_type = valuequote
    205                      lastvalue = ""
    206                      invalue = False
    207                  if not invalue and colonpos != len(value)-1:
    208                      commentinlinepos = value.find("//", colonpos)
    209                      if commentinlinepos != -1:
    210                          newunit.addnote(value[commentinlinepos+2:].strip(), "developer")
    211                  if not invalue:
    212                      self.addunit(newunit)
    213                      value = ""
    214                      newunit = phpunit()
    215                  colonpos = value.rfind(";", 0, colonpos)
    216              if invalue:
    217                  lastvalue = lastvalue + value + "\n"
    218
    219 -    def __str__(self):
    220          """Convert the units back to lines."""
    221          lines = []
    222          for unit in self.units:
    223              lines.append(str(unit))
    224          return "".join(lines)
    225