You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

240 lines
6.4 KiB

<?php
/**
* li₃: the most RAD framework for PHP (http://li3.me)
*
* Copyright 2009, Union of RAD. All rights reserved. This source
* code is distributed under the terms of the BSD 3-Clause License.
* The full license text can be found in the LICENSE.txt file.
*/
namespace lithium\g11n\catalog\adapter;
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;
use lithium\core\ConfigException;
use lithium\template\view\Compiler;
/**
* The `Code` class is an adapter which treats files containing code as just another source
* of globalized data.
*
* In fact it allows for extracting messages which are needed to build
* message catalog templates. Currently only code written in PHP is supported through a parser
* using the built-in tokenizer.
*
* @see lithium\g11n\Message
* @see lithium\template\View
*/
class Code extends \lithium\g11n\catalog\Adapter {
/**
* Constructor.
*
* @param array $config Available configuration options are:
* - `'path'`: The path to the directory holding the data.
* - `'scope'`: Scope to use.
* @return void
*/
public function __construct(array $config = []) {
$defaults = ['path' => null, 'scope' => null];
parent::__construct($config + $defaults);
}
/**
* Initializer. Checks if the configured path exists.
*
* @throws lithium\core\ConfigException
*/
protected function _init() {
parent::_init();
if (!is_dir($this->_config['path'])) {
$message = "Code directory does not exist at path `{$this->_config['path']}`.";
throw new ConfigException($message);
}
}
/**
* Reads data.
*
* @param string $category A category. `'messageTemplate'` is the only category supported.
* @param string $locale A locale identifier.
* @param string $scope The scope for the current operation.
* @return array Returns the message template. If the scope is not equal to the current scope
* or `$category` is not `'messageTemplate'` null is returned.
*/
public function read($category, $locale, $scope) {
if ($scope !== $this->_config['scope']) {
return null;
}
$path = $this->_config['path'];
switch ($category) {
case 'messageTemplate':
return $this->_readMessageTemplate($path);
default:
return null;
}
}
/**
* Extracts data from files within configured path recursively.
*
* @param string $path Base path to start extracting from.
* @return array
*/
protected function _readMessageTemplate($path) {
$base = new RecursiveDirectoryIterator($path);
$iterator = new RecursiveIteratorIterator($base);
$data = [];
foreach ($iterator as $item) {
$file = $item->getPathname();
switch (pathinfo($file, PATHINFO_EXTENSION)) {
case 'php':
$data += $this->_parsePhp($file);
break;
}
}
return $data;
}
/**
* Parses a PHP file for messages marked as translatable. Recognized as message
* marking are `$t()` and `$tn()` which are implemented in the `View` class. This
* is a rather simple and stupid parser but also fast and easy to grasp. It doesn't
* actively attempt to detect and work around syntax errors in marker functions.
*
* @see lithium\g11n\Message::aliases()
* @param string $file Absolute path to a PHP file.
* @return array
*/
protected function _parsePhp($file) {
$contents = file_get_contents($file);
$contents = Compiler::compile($contents);
$defaults = [
'ids' => [],
'context' => null,
'open' => false,
'position' => 0,
'occurrence' => ['file' => $file, 'line' => null]
];
extract($defaults);
$data = [];
if (strpos($contents, '$t(') === false && strpos($contents, '$tn(') === false) {
return $data;
}
$tokens = token_get_all($contents);
unset($contents);
$findContext = function ($position) use ($tokens) {
$ignore = [T_WHITESPACE, '(', ')', T_ARRAY, ','];
$open = 1;
$options = [];
$depth = 0;
while (isset($tokens[$position]) && $token = $tokens[$position]) {
if (!is_array($token)) {
$token = [0 => null, 1 => $token, 2 => null];
}
if ($token[1] === '[' || $token[1] === '(') {
$open++;
} elseif (($token[1] === ']' || $token[1] === ')') && --$open === 0) {
break;
}
if ($token[1] === '[' || $token[0] === T_ARRAY) {
$depth++;
} elseif ($depth > 1 && ($token[1] === ']' || $token[1] === ')')) {
$depth--;
}
if ($depth === 1 && $open === 2) {
if (!in_array($token[0] ? : $token[1], $ignore)) {
$options[] = $token;
}
}
$position++;
}
foreach ($options as $i => $token) {
if (!(isset($options[$i + 1]) && isset($options[$i + 2]))) {
break;
}
$condition1 = substr($token[1], 1, -1) === 'context';
$condition2 = $options[$i + 1][0] === T_DOUBLE_ARROW;
$condition3 = $options[$i + 2][0] === T_CONSTANT_ENCAPSED_STRING;
if ($condition1 && $condition2 && $condition3) {
return $options[$i + 2][1];
}
}
return null;
};
foreach ($tokens as $key => $token) {
if (!is_array($token)) {
$token = [0 => null, 1 => $token, 2 => null];
}
if ($open) {
if ($position >= ($open === 'singular' ? 1 : 2)) {
$data = $this->_merge($data, [
'id' => $ids['singular'],
'ids' => $ids,
'occurrences' => [$occurrence],
'context' => $context
]);
extract($defaults, EXTR_OVERWRITE);
} elseif ($token[0] === T_CONSTANT_ENCAPSED_STRING) {
$ids[$ids ? 'plural' : 'singular'] = $token[1];
$position++;
}
} else {
if (isset($tokens[$key + 1]) && $tokens[$key + 1] === '(') {
if ($token[1] === '$t') {
$open = 'singular';
} elseif ($token[1] === '$tn') {
$open = 'plural';
} else {
continue;
}
$occurrence['line'] = $token[2];
$context = $findContext($key + 2);
}
}
}
return $data;
}
/**
* Merges an item into given data and removes quotation marks
* from the beginning and end of message strings.
*
* @see lithium\g11n\catalog\Adapter::_merge()
* @param array $data Data to merge item into.
* @param array $item Item to merge into $data.
* @return array The merged data.
*/
protected function _merge(array $data, array $item) {
$filter = function ($value) use (&$filter) {
if (is_array($value)) {
return array_map($filter, $value);
}
return substr($value, 1, -1);
};
$fields = ['id', 'ids', 'translated', 'context'];
foreach ($fields as $field) {
if (isset($item[$field])) {
$item[$field] = $filter($item[$field]);
}
}
return parent::_merge($data, $item);
}
}
?>