Files

431 lines
12 KiB
PHP
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace Sepia\PoParser;
use Sepia\PoParser\Catalog\Catalog;
use Sepia\PoParser\Catalog\CatalogArray;
use Sepia\PoParser\Catalog\EntryFactory;
use Sepia\PoParser\Catalog\Header;
use Sepia\PoParser\Exception\ParseException;
use Sepia\PoParser\SourceHandler\FileSystem;
use Sepia\PoParser\SourceHandler\SourceHandler;
use Sepia\PoParser\SourceHandler\StringSource;
/**
* Copyright (c) 2012 Raúl Ferràs raul.ferras@gmail.com
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of copyright holders nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* https://github.com/raulferras/PHP-po-parser
*
* Class to parse .po file and extract its strings.
*
* @version 5.0
*/
class Parser
{
/** @var SourceHandler */
protected $sourceHandler;
/** @var int */
protected $lineNumber;
/** @var string */
protected $property;
/**
* Reads and parses a string
*
* @param string $string po content
*
* @throws \Exception.
* @return Catalog
*/
public static function parseString($string)
{
$parser = new Parser(new StringSource($string));
return $parser->parse();
}
/**
* Reads and parses a file
*
* @param string $filePath
*
* @throws \Exception.
* @return Catalog
*/
public static function parseFile($filePath)
{
$parser = new Parser(new FileSystem($filePath));
return $parser->parse();
}
public function __construct(SourceHandler $sourceHandler)
{
$this->sourceHandler = $sourceHandler;
}
/**
* Reads and parses strings of a .po file.
*
* @param SourceHandler . Optional
*
* @throws \Exception, \InvalidArgumentException, ParseException
* @return Catalog
*/
public function parse(Catalog $catalog = null)
{
$catalog = $catalog === null ? new CatalogArray() : $catalog;
$this->lineNumber = 0;
$entry = array();
$this->property = null; // current property
// Flags
$headersFound = false;
while (!$this->sourceHandler->ended()) {
$line = trim($this->sourceHandler->getNextLine());
if ($this->shouldIgnoreLine($line, $entry)) {
$this->lineNumber++;
continue;
}
if ($this->shouldCloseEntry($line, $entry)) {
if (!$headersFound && $this->isHeader($entry)) {
$headersFound = true;
$catalog->addHeaders(
$this->parseHeaders($entry['msgstr'])
);
} else {
$catalog->addEntry(EntryFactory::createFromArray($entry));
}
$entry = array();
$this->property = null;
if (empty($line)) {
$this->lineNumber++;
continue;
}
}
$entry = $this->parseLine($line, $entry);
$this->lineNumber++;
continue;
}
$this->sourceHandler->close();
// add final entry
if (count($entry)) {
if ($this->isHeader($entry)) {
$catalog->addHeaders(
$this->parseHeaders($entry['msgstr'])
);
} else {
$catalog->addEntry(EntryFactory::createFromArray($entry));
}
}
return $catalog;
}
/**
* @param string $line
* @param array $entry
*
* @return array
* @throws ParseException
*/
protected function parseLine($line, $entry)
{
$firstChar = strlen($line) > 0 ? $line[0] : '';
switch ($firstChar) {
case '#':
$entry = $this->parseComment($line, $entry);
break;
case 'm':
$entry = $this->parseProperty($line, $entry);
break;
case '"':
$entry = $this->parseMultiline($line, $entry);
break;
}
return $entry;
}
/**
* @param string $line
* @param array $entry
*
* @return array
* @throws ParseException
*/
protected function parseProperty($line, array $entry)
{
list($key, $value) = $this->getProperty($line);
if (!isset($entry[$key])) {
$entry[$key] = '';
}
switch (true) {
case $key === 'msgctxt':
case $key === 'msgid':
case $key === 'msgid_plural':
case $key === 'msgstr':
$entry[$key] .= $this->unquote($value);
$this->property = $key;
break;
case strpos($key, 'msgstr[') !== false:
$entry[$key] .= $this->unquote($value);
$this->property = $key;
break;
default:
throw new ParseException(sprintf('Could not parse %s at line %d', $key, $this->lineNumber));
}
return $entry;
}
/**
* @param string $line
* @param array $entry
*
* @return array
* @throws ParseException
*/
protected function parseMultiline($line, $entry)
{
switch (true) {
case $this->property === 'msgctxt':
case $this->property === 'msgid':
case $this->property === 'msgid_plural':
case $this->property === 'msgstr':
case strpos($this->property, 'msgstr[') !== false:
$entry[$this->property] .= $this->unquote($line);
break;
default:
throw new ParseException(
sprintf('Error parsing property %s as multiline.', $this->property)
);
}
return $entry;
}
/**
* @param string $line
* @param array $entry
*
* @return array
* @throws ParseException
*/
protected function parseComment($line, $entry)
{
$comment = trim(substr($line, 0, 2));
switch ($comment) {
case '#,':
$line = trim(substr($line, 2));
$entry['flags'] = preg_split('/,\s*/', $line);
break;
case '#.':
$entry['ccomment'] = !isset($entry['ccomment']) ? array() : $entry['ccomment'];
$entry['ccomment'][] = trim(substr($line, 2));
break;
case '#|': // Previous string
case '#~': // Old entry
case '#~|': // Previous string old
$mode = array(
'#|' => 'previous',
'#~' => 'obsolete',
'#~|' => 'previous-obsolete'
);
$line = trim(substr($line, 2));
$property = $mode[$comment];
if ($property === 'previous') {
if (!isset($entry[$property])) {
$subEntry = array();
} else {
$subEntry = $entry[$property];
}
$subEntry = $this->parseLine($line, $subEntry);
//$subEntry = $this->parseProperty($line, $subEntry);
$entry[$property] = $subEntry;
} else {
$entry = $this->parseLine($line, $entry);
$entry['obsolete'] = true;
}
break;
// Reference
case '#:':
$entry['reference'][] = trim(substr($line, 2));
break;
case '#':
default:
$entry['tcomment'] = !isset($entry['tcomment']) ? array() : $entry['tcomment'];
$entry['tcomment'][] = trim(substr($line, 1));
break;
}
return $entry;
}
/**
* @param string $msgstr
*
* @return Header
*/
protected function parseHeaders($msgstr)
{
$headers = array_filter(explode('\\n', $msgstr));
return new Header($headers);
}
/**
* @param string $line
* @param array $entry
*
* @return bool
*/
protected function shouldIgnoreLine($line, array $entry)
{
return empty($line) && count($entry) === 0;
}
/**
* @param string $line
* @param array $entry
*
* @return bool
*/
protected function shouldCloseEntry($line, array $entry)
{
$tokens = $this->getProperty($line);
$property = $tokens[0];
return ($line === '' || ($property === 'msgid' && isset($entry['msgid'])));
}
/**
* @param string $value
* @return string
*/
protected function unquote($value)
{
return preg_replace('/^\"|\"$/', '', $value);
}
/**
* Checks if entry is a header by
*
* @param array $entry
*
* @return bool
*/
protected function isHeader(array $entry)
{
if (empty($entry) || !isset($entry['msgstr'])) {
return false;
}
if (!isset($entry['msgid']) || !empty($entry['msgid'])) {
return false;
}
$standardHeaders = array(
'Project-Id-Version:',
'Report-Msgid-Bugs-To:',
'POT-Creation-Date:',
'PO-Revision-Date:',
'Last-Translator:',
'Language-Team:',
'MIME-Version:',
'Content-Type:',
'Content-Transfer-Encoding:',
'Plural-Forms:',
);
$headers = explode('\n', $entry['msgstr']);
// Remove text after double colon
$headers = array_map(
function ($header) {
$pattern = '/(.*?:)(.*)/i';
$replace = '${1}';
return preg_replace($pattern, $replace, $header);
},
$headers
);
if (count(array_intersect($standardHeaders, $headers)) > 0) {
return true;
}
// If it does not contain any of the standard headers
// Let's see if it contains any custom header.
$customHeaders = array_filter(
$headers,
function ($header) {
return preg_match('/^X\-(.*):/i', $header) === 1;
}
);
return count($customHeaders) > 0;
}
/**
* @param string $line
*
* @return array
*/
protected function getProperty($line)
{
$tokens = preg_split('/\s+/ ', $line, 2);
return $tokens;
}
}