upload 28/5

This commit is contained in:
2025-05-28 15:30:26 +07:00
parent 1c2e648b54
commit fdda1c345f
252 changed files with 35084 additions and 1237 deletions

25
package/vendor/autoload.php vendored Normal file
View File

@@ -0,0 +1,25 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitee7e3ad86855a67e00785a425ecb5246::getLoader();

579
package/vendor/composer/ClassLoader.php vendored Normal file
View File

@@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

View File

@@ -0,0 +1,359 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}
}

21
package/vendor/composer/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,10 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
);

View File

@@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

View File

@@ -0,0 +1,10 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Liquid\\' => array($vendorDir . '/liquid/liquid/src/Liquid'),
);

View File

@@ -0,0 +1,38 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInitee7e3ad86855a67e00785a425ecb5246
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInitee7e3ad86855a67e00785a425ecb5246', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInitee7e3ad86855a67e00785a425ecb5246', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInitee7e3ad86855a67e00785a425ecb5246::getInitializer($loader));
$loader->register(true);
return $loader;
}
}

View File

@@ -0,0 +1,36 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInitee7e3ad86855a67e00785a425ecb5246
{
public static $prefixLengthsPsr4 = array (
'L' =>
array (
'Liquid\\' => 7,
),
);
public static $prefixDirsPsr4 = array (
'Liquid\\' =>
array (
0 => __DIR__ . '/..' . '/liquid/liquid/src/Liquid',
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInitee7e3ad86855a67e00785a425ecb5246::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInitee7e3ad86855a67e00785a425ecb5246::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInitee7e3ad86855a67e00785a425ecb5246::$classMap;
}, null, ClassLoader::class);
}
}

78
package/vendor/composer/installed.json vendored Normal file
View File

@@ -0,0 +1,78 @@
{
"packages": [
{
"name": "liquid/liquid",
"version": "1.4.32",
"version_normalized": "1.4.32.0",
"source": {
"type": "git",
"url": "https://github.com/kalimatas/php-liquid.git",
"reference": "f0be8a3e0e0fcee99c0091748dcb21675b3caf46"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/kalimatas/php-liquid/zipball/f0be8a3e0e0fcee99c0091748dcb21675b3caf46",
"reference": "f0be8a3e0e0fcee99c0091748dcb21675b3caf46",
"shasum": ""
},
"require": {
"php": "^7.3 || ^8.0"
},
"require-dev": {
"ergebnis/composer-normalize": "^2.8",
"friendsofphp/php-cs-fixer": "^2.16.4",
"infection/infection": ">=0.17.6",
"php-coveralls/php-coveralls": "^2.2",
"phpunit/phpunit": "^9.2.6"
},
"time": "2022-08-08T01:55:20+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Liquid\\": "src/Liquid"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guz Alexander",
"email": "kalimatas@gmail.com",
"homepage": "http://guzalexander.com"
},
{
"name": "Harald Hanek"
},
{
"name": "Mateo Murphy"
},
{
"name": "Alexey Kopytko",
"email": "alexey@kopytko.com",
"homepage": "https://www.alexeykopytko.com/"
}
],
"description": "Liquid template engine for PHP",
"homepage": "https://github.com/kalimatas/php-liquid",
"keywords": [
"liquid",
"template"
],
"support": {
"issues": "https://github.com/kalimatas/php-liquid/issues",
"source": "https://github.com/kalimatas/php-liquid/tree/1.4.32"
},
"install-path": "../liquid/liquid"
}
],
"dev": true,
"dev-package-names": []
}

32
package/vendor/composer/installed.php vendored Normal file
View File

@@ -0,0 +1,32 @@
<?php return array(
'root' => array(
'name' => '__root__',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '1c2e648b54daf6079ce591d9464594587ffb788d',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'__root__' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '1c2e648b54daf6079ce591d9464594587ffb788d',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'liquid/liquid' => array(
'pretty_version' => '1.4.32',
'version' => '1.4.32.0',
'reference' => 'f0be8a3e0e0fcee99c0091748dcb21675b3caf46',
'type' => 'library',
'install_path' => __DIR__ . '/../liquid/liquid',
'aliases' => array(),
'dev_requirement' => false,
),
),
);

View File

@@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 70300)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.3.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

View File

@@ -0,0 +1,5 @@
- [ ] I've run the tests with `vendor/bin/phpunit`
- [ ] None of the tests were found failing
- [ ] I've seen the coverage report at `build/coverage/index.html`
- [ ] Not a single line left uncovered by tests
- [ ] Any coding standards issues were fixed with `vendor/bin/php-cs-fixer fix`

View File

@@ -0,0 +1,46 @@
name: Code Style
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['7.3']
name: PHP ${{ matrix.php-version }}
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions:
coverage: pcov
tools: composer:v1
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ~/.cache/composer
key: composer-${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }}
restore-keys: |
composer-${{ matrix.php-version }}-
composer-
- name: Install dependencies
run: |
composer update --prefer-dist --no-interaction --no-progress ${{ matrix.dependencies }}
- name: Check code style
run: |
php vendor/bin/php-cs-fixer --using-cache=no --diff --dry-run --stop-on-violation --verbose fix

View File

@@ -0,0 +1,48 @@
name: Mutation Testing
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['7.4']
dependencies: ['']
name: PHP ${{ matrix.php-version }} ${{ matrix.dependencies }}
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions:
coverage: pcov
tools: composer:v2
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ~/.cache/composer
key: composer-${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }}
restore-keys: |
composer-${{ matrix.php-version }}-
composer-
- name: Install dependencies
run: |
composer update --prefer-dist --no-interaction --no-progress ${{ matrix.dependencies }}
- name: Run mutation testing
run: |
php vendor/bin/infection --min-msi=80 --min-covered-msi=80 --show-mutations --threads=$(nproc)

View File

@@ -0,0 +1,56 @@
# yamllint disable rule:line-length
# yamllint disable rule:braces
name: CI
on:
pull_request:
push:
branches:
- master
- main
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['7.3', '7.4', '8.0', '8.1']
include:
- { php-version: '7.3', dependencies: '--prefer-lowest', legend: 'with lowest dependencies' }
name: PHP ${{ matrix.php-version }} ${{ matrix.legend }}
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
coverage: pcov
tools: composer:v2
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: composer-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('composer.*') }}-${{ matrix.composer-flags }}
restore-keys: |
composer-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('composer.*') }}-
composer-${{ runner.os }}-${{ matrix.php-version }}-
composer-${{ runner.os }}-
composer-
- name: Install dependencies
run: |
composer update --prefer-dist --no-interaction --no-progress ${{ matrix.dependencies }}
- name: Run tests
run: |
php vendor/bin/phpunit

View File

@@ -0,0 +1,3 @@
.idea/
vendor/
composer.lock

View File

@@ -0,0 +1,42 @@
<?php
$header = <<<'EOF'
This file is part of the Liquid package.
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
@package Liquid
EOF;
return PhpCsFixer\Config::create()
->setRiskyAllowed(true)
->setRules([
'@PSR2' => true,
'psr4' => true,
'no_unreachable_default_argument_value' => true,
'no_useless_else' => true,
'no_useless_return' => true,
'phpdoc_add_missing_param_annotation' => true,
'phpdoc_order' => true,
'semicolon_after_instruction' => true,
'whitespace_after_comma_in_array' => true,
'header_comment' => ['header' => $header],
'php_unit_construct' => true,
'php_unit_dedicate_assert' => true,
'php_unit_dedicate_assert_internal_type' => true,
'php_unit_expectation' => true,
'php_unit_mock_short_will_return' => true,
'php_unit_mock' => true,
'php_unit_namespaced' => true,
'php_unit_no_expectation_annotation' => true,
'php_unit_ordered_covers' => true,
'php_unit_set_up_tear_down_visibility' => true,
'php_unit_test_case_static_method_calls' => ['call_type' => 'this'],
])
->setIndent("\t")
->setFinder(
PhpCsFixer\Finder::create()
->in(__DIR__)
)
;

View File

@@ -0,0 +1,107 @@
## master
## 1.4.8 (2018-03-22)
* Now we return null for missing properties, like we do for missing keys for arrays.
## 1.4.7 (2018-02-09)
* Paginate tag shall now respect request parameters.
* It is now possible to set a custom query param for the paginate tag.
* Page number will now never go overboard.
## 1.4.6 (2018-02-07)
* TagPaginate shall not pollute the global scope, but work in own scope.
* TagPaginate errors if no collection present instead of vague warning.
## 1.4.5 (2017-12-12)
* Capture tag shall save a variable in the global context.
## 1.4.4 (2017-11-03)
* TagUnless is an inverted TagIf: simplified implementation
* Allow dashes in filenames
## 1.4.3 (2017-10-10)
* `escape` and `escape_once` filters now escape everything, but arrays
* New standard filter for explicit string conversion
## 1.4.2 (2017-10-09)
* Better caching for non-extending templates
* Simplified 'assign' tag to use rules for variables
* Now supporting PHP 7.2
* Different types of exception depending on the case
* Filterbank will not call instance methods statically
* Callback-type filters
## 1.4.1 (2017-09-28)
* Unquoted template names in 'include' tag, as in Jekyll
* Caching now works correctly with 'extends' tag
## 1.4.0 (2017-09-25)
* Dropped support for EOL'ed versions of PHP (< 5.6)
* Arrays won't be silently cast to string as 'Array' anymore
* Complex objects could now be passed between templates and to filters
* Additional test coverage
## 1.3.1 (2017-09-23)
* Support for numeric and variable array indicies
* Support loop break and continue
* Allow looping over extended ranges
* Math filters now work with floats
* Fixed 'default' filter
* Local cache with data stored in a private variable
* Virtual file system to get inversion of control and DI
* Lots of tests with the coverage upped to 97%
* Small bug fixes and various enhancements
## 1.3.0 (2017-07-17)
* Support Traversable loops and filters
* Fix date filter for format with colon
* Various minor improvements and bugs fixes
## 1.2.1 (2016-12-12)
* Remove content injection from $_GET.
* Add PHP 5.6, 7.0, 7.1 to Travis file.
## 1.2 (2016-06-11)
* Added "ESCAPE_BY_DEFAULT" setting for context-aware auto-escaping.
* Made "Context" work with plain objects.
* "escape" now uses "htmlentities".
* Fixed "escape_now".
## 1.1 (2015-06-01)
* New tags: "paginate", "unless", "ifchanged" were added
* Added support for "for in (range)" syntax
* Added support for multiple conditions in if statements
* Added support for hashes/objects in for loops
## 1.0 (2014-09-07)
* Add namespaces
* Add composer support
* Implement new standard filters
* Add 'raw' tag
## 0.9.2 (2012-08-15)
* context->set allows now global vars
* Allow Templatenames with Fileextension
* Tag 'extends' supports now multiple inheritance
* Clean up code, change all variables and methods to camelCase
## 0.9.1 (2012-05-12)
* added the extends and block filter
* Initial release

22
package/vendor/liquid/liquid/LICENSE vendored Normal file
View File

@@ -0,0 +1,22 @@
Copyright (c) 2014 Guz Alexander, http://guzalexander.com
Copyright (c) 2011, 2012 Harald Hanek, http://www.delacap.com
Copyright (c) 2006 Mateo Murphy
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

139
package/vendor/liquid/liquid/README.md vendored Normal file
View File

@@ -0,0 +1,139 @@
# Liquid template engine for PHP [![CI](https://github.com/kalimatas/php-liquid/actions/workflows/tests.yaml/badge.svg)](https://github.com/kalimatas/php-liquid/actions/workflows/tests.yaml) [![Coverage Status](https://coveralls.io/repos/github/kalimatas/php-liquid/badge.svg?branch=master)](https://coveralls.io/github/kalimatas/php-liquid?branch=master) [![Total Downloads](https://poser.pugx.org/liquid/liquid/downloads.svg)](https://packagist.org/packages/liquid/liquid)
Liquid is a PHP port of the [Liquid template engine for Ruby](https://github.com/Shopify/liquid), which was written by Tobias Lutke. Although there are many other templating engines for PHP, including Smarty (from which Liquid was partially inspired), Liquid had some advantages that made porting worthwhile:
* Readable and human friendly syntax, that is usable in any type of document, not just html, without need for escaping.
* Quick and easy to use and maintain.
* 100% secure, no possibility of embedding PHP code.
* Clean OO design, rather than the mix of OO and procedural found in other templating engines.
* Seperate compiling and rendering stages for improved performance.
* Easy to extend with your own "tags and filters":https://github.com/harrydeluxe/php-liquid/wiki/Liquid-for-programmers.
* 100% Markup compatibility with a Ruby templating engine, making templates usable for either.
* Unit tested: Liquid is fully unit-tested. The library is stable and ready to be used in large projects.
## Why Liquid?
Why another templating library?
Liquid was written to meet three templating library requirements: good performance, easy to extend, and simply to use.
## Installing
You can install this lib via [composer](https://getcomposer.org/):
composer require liquid/liquid
## Example template
{% if products %}
<ul id="products">
{% for product in products %}
<li>
<h2>{{ product.name }}</h2>
Only {{ product.price | price }}
{{ product.description | prettyprint | paragraph }}
{{ 'it rocks!' | paragraph }}
</li>
{% endfor %}
</ul>
{% endif %}
## How to use Liquid
The main class is `Liquid::Template` class. There are two separate stages of working with Liquid templates: parsing and rendering. Here is a simple example:
use Liquid\Template;
$template = new Template();
$template->parse("Hello, {{ name }}!");
echo $template->render(array('name' => 'Alex'));
// Will echo
// Hello, Alex!
To find more examples have a look at the `examples` directory or at the original Ruby implementation repository's [wiki page](https://github.com/Shopify/liquid/wiki).
## Advanced usage
You would probably want to add a caching layer (at very least a request-wide one), enable context-aware automatic escaping, and do load includes from disk with full file names.
use Liquid\Liquid;
use Liquid\Template;
use Liquid\Cache\Local;
Liquid::set('INCLUDE_SUFFIX', '');
Liquid::set('INCLUDE_PREFIX', '');
Liquid::set('INCLUDE_ALLOW_EXT', true);
Liquid::set('ESCAPE_BY_DEFAULT', true);
$template = new Template(__DIR__.'/protected/templates/');
$template->parse("Hello, {% include 'honorific.html' %}{{ plain-html | raw }} {{ comment-with-xss }}");
$template->setCache(new Local());
echo $template->render([
'name' => 'Alex',
'plain-html' => '<b>Your comment was:</b>',
'comment-with-xss' => '<script>alert();</script>',
]);
Will output:
Hello, Mx. Alex
<b>Your comment was:</b> &lt;script&gt;alert();&lt;/script&gt;
Note that automatic escaping is not a standard Liquid feature: use with care.
Similarly, the following snippet will parse and render `templates/home.liquid` while storing parsing results in a class-local cache:
\Liquid\Liquid::set('INCLUDE_PREFIX', '');
$template = new \Liquid\Template(__DIR__ . '/protected/templates');
$template->setCache(new \Liquid\Cache\Local());
echo $template->parseFile('home')->render();
If you render the same template over and over for at least a dozen of times, the class-local cache will give you a slight speed up in range of some milliseconds per render depending on a complexity of your template.
You should probably extend `Liquid\Template` to initialize everything you do with `Liquid::set` in one place.
### Custom filters
Adding filters has never been easier.
$template = new Template();
$template->registerFilter('absolute_url', function ($arg) {
return "https://www.example.com$arg";
});
$template->parse("{{ my_url | absolute_url }}");
echo $template->render(array(
'my_url' => '/test'
));
// expect: https://www.example.com/test
## Requirements
* PHP 7.0+
Some earlier versions could be used with PHP 5.3/5.4/5.5/5.6, though they're not supported anymore.
## Issues
Have a bug? Please create an issue here on GitHub!
[https://github.com/kalimatas/php-liquid/issues](https://github.com/kalimatas/php-liquid/issues)
## Fork notes
This fork is based on [php-liquid](https://github.com/harrydeluxe/php-liquid) by Harald Hanek.
It contains several improvements:
* namespaces
* installing via composer
* new standard filters
* `raw` tag added
Any help is appreciated!

View File

@@ -0,0 +1,58 @@
{
"name": "liquid/liquid",
"type": "library",
"description": "Liquid template engine for PHP",
"keywords": [
"liquid",
"template"
],
"homepage": "https://github.com/kalimatas/php-liquid",
"license": "MIT",
"authors": [
{
"name": "Guz Alexander",
"email": "kalimatas@gmail.com",
"homepage": "http://guzalexander.com"
},
{
"name": "Harald Hanek"
},
{
"name": "Mateo Murphy"
},
{
"name": "Alexey Kopytko",
"email": "alexey@kopytko.com",
"homepage": "https://www.alexeykopytko.com/"
}
],
"require": {
"php": "^7.3 || ^8.0"
},
"require-dev": {
"ergebnis/composer-normalize": "^2.8",
"friendsofphp/php-cs-fixer": "^2.16.4",
"infection/infection": ">=0.17.6",
"php-coveralls/php-coveralls": "^2.2",
"phpunit/phpunit": "^9.2.6"
},
"config": {
"sort-packages": true,
"allow-plugins": true
},
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Liquid\\": "src/Liquid"
}
},
"autoload-dev": {
"psr-4": {
"Liquid\\": "tests/Liquid"
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
require __DIR__ . '/../vendor/autoload.php';
use Liquid\Liquid;
use Liquid\Template;
use Liquid\Cache\Local;
Liquid::set('INCLUDE_SUFFIX', '');
Liquid::set('INCLUDE_PREFIX', '');
Liquid::set('INCLUDE_ALLOW_EXT', true);
Liquid::set('ESCAPE_BY_DEFAULT', true);
$template = new Template(__DIR__.'/protected/templates/');
$template->parse("Hello, {% include 'honorific.html' %}{{ plain-html | raw }} {{ comment-with-xss }}\n");
$template->setCache(new Local());
echo $template->render([
'name' => 'Alex',
'plain-html' => '<b>Your comment was:</b>',
'comment-with-xss' => '<script>alert();</script>',
]);

View File

@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
require __DIR__ . '/../vendor/autoload.php';
use Liquid\Liquid;
use Liquid\Template;
Liquid::set('INCLUDE_ALLOW_EXT', true);
$protectedPath = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'protected' . DIRECTORY_SEPARATOR;
$liquid = new Template($protectedPath . 'templates' . DIRECTORY_SEPARATOR);
// Uncomment the following lines to enable cache
//$cache = array('cache' => 'file', 'cache_dir' => $protectedPath . 'cache' . DIRECTORY_SEPARATOR);
// or if you have APC installed
//$cache = array('cache' => 'apc');
//$liquid->setCache($cache);
$liquid->parse(file_get_contents($protectedPath . 'templates' . DIRECTORY_SEPARATOR . 'child.tpl'));
$assigns = array(
'document' => array(
'title' => 'This is php-liquid',
'content' => 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.',
'copyright' => '&copy; Copyright 2014 Guz Alexander - All rights reserved.',
),
);
echo $liquid->render($assigns);

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
require __DIR__ . '/../vendor/autoload.php';
use Liquid\Liquid;
use Liquid\Template;
$template = new Template();
$template->registerFilter('absolute_url', function ($arg) {
return "https://www.example.com$arg";
});
$template->parse("{{ my_url | absolute_url }}");
echo $template->render(array(
'my_url' => '/test'
));
// expect: https://www.example.com/test

View File

@@ -0,0 +1,70 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
require __DIR__ . '/../vendor/autoload.php';
use Liquid\Liquid;
use Liquid\Template;
Liquid::set('INCLUDE_SUFFIX', 'tpl');
Liquid::set('INCLUDE_PREFIX', '');
$protectedPath = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'protected' . DIRECTORY_SEPARATOR;
$liquid = new Template($protectedPath . 'templates' . DIRECTORY_SEPARATOR);
// Uncomment the following lines to enable cache
//$cache = array('cache' => 'file', 'cache_dir' => $protectedPath . 'cache' . DIRECTORY_SEPARATOR);
// or if you have APC installed
//$cache = array('cache' => 'apc');
//$liquid->setCache($cache);
$liquid->parse(file_get_contents($protectedPath . 'templates' . DIRECTORY_SEPARATOR . 'index.tpl'));
$assigns = array(
'document' => array(
'title' => 'This is php-liquid',
'content' => 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.',
'copyright' => 'Guz Alexander - All rights reserved.',
),
'blog' => array(
array(
'title' => 'Blog Title 1',
'content' => 'Nunc putamus parum claram',
'tags' => array('claram', 'parum'),
'comments' => array(
array(
'title' => 'First Comment',
'message' => 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr',
),
),
),
array(
'title' => 'Blog Title 2',
'content' => 'Nunc putamus parum claram',
'tags' => array('claram', 'parum', 'freestyle'),
'comments' => array(
array(
'title' => 'First Comment',
'message' => 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr',
),
array(
'title' => 'Second Comment',
'message' => 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr',
),
),
),
),
'array' => array('one', 'two', 'three', 'four'),
);
echo $liquid->render($assigns);

View File

@@ -0,0 +1,2 @@
!.gitignore
liquid_*

View File

@@ -0,0 +1,15 @@
{% comment %} This is the base template. {% endcomment %}
<!DOCTYPE HTML>
<html>
<head>
{% include 'header.tpl' %}
</head>
<body>
<div id="content">{% block content %}{% endblock %}</div>
<div id="footer">
{% block footer %}
&copy; Copyright 2014 by <a href="http://guzalexander.com/">Guz Alexander</a>.
{% endblock %}
</div>
</body>
</html>

View File

@@ -0,0 +1,6 @@
{% comment %} This is the child template. {% endcomment %}
{% extends "base.tpl" %}
{% block footer %}
{{ document.copyright }}
{% endblock %}

View File

@@ -0,0 +1,7 @@
{% comment %} This is the child template. {% endcomment %}
{% extends "blocks/child.tpl" %}
{% block content %}
<h2>Entry one</h2>
<p>This is my first entry.</p>
{% endblock %}

View File

@@ -0,0 +1,3 @@
<div id="footer">
&copy; {{ 'now' | date: "%Y" }} {% include 'subfooter' %}
</div>

View File

@@ -0,0 +1,2 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>{{ document.title }}</title>

View File

@@ -0,0 +1 @@
Mx. {{ name }}

View File

@@ -0,0 +1,44 @@
{% comment %}
This is a comment block
(c) 2014 Guz Alexander
{% endcomment %}
<!DOCTYPE HTML>
<html>
<head>
{% include 'header' %}
</head>
<body>
<h1>{{ document.title }}</h1>
<p>{{ document.content }}</p>
<p><a href="simple.php">Link to simple.php</a></p>
{% if blog %}
Total Blogentrys: {{ blog | size }}
<ul id="products">
{% for entry in blog %}
<li>
<h3>{{ entry.title | upcase }}</h3>
<p>{{ entry.content }}</p>
Comments: {{ entry.comments | size }}
{% assign uzu = 'dudu2' %}
{% assign freestyle = false %}
{% for t in entry.tags %}
{% if t == 'freestyle' %}
{% assign freestyle = true %}
{% endif %}
{% endfor %}
{% if freestyle %}
<p>Blogentry has tag: freestyle</p>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
{% include 'footer' %}
</body>
</html>

View File

@@ -0,0 +1 @@
{{ document.copyright }}

View File

@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
require __DIR__ . '/../vendor/autoload.php';
use Liquid\Liquid;
use Liquid\Template;
Liquid::set('INCLUDE_SUFFIX', 'tpl');
Liquid::set('INCLUDE_PREFIX', '');
$liquid = new Template();
$liquid->parse('{{ hello }} {{ goback }}');
echo $liquid->render(array('hello' => 'hello world', 'goback' => '<a href=".">index</a>'));

View File

@@ -0,0 +1,8 @@
{
"timeout": 2,
"source": {
"directories": [
"src"
]
}
}

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
colors="true"
verbose="true"
executionOrder="random"
resolveDependencies="true"
failOnRisky="true"
failOnWarning="true"
backupStaticAttributes="true"
>
<testsuites>
<testsuite name="Main">
<directory>tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">src/</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -0,0 +1,264 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
use Liquid\Exception\ParseException;
use Liquid\Exception\RenderException;
/**
* Base class for blocks.
*/
class AbstractBlock extends AbstractTag
{
const TAG_PREFIX = '\Liquid\Tag\Tag';
/**
* @var AbstractTag[]|Variable[]|string[]
*/
protected $nodelist = array();
/**
* Whenever next token should be ltrim'med.
*
* @var bool
*/
protected static $trimWhitespace = false;
/**
* @return array
*/
public function getNodelist()
{
return $this->nodelist;
}
/**
* Parses the given tokens
*
* @param array $tokens
*
* @throws \Liquid\LiquidException
* @return void
*/
public function parse(array &$tokens)
{
$startRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '/');
$tagRegexp = new Regexp('/^' . Liquid::get('TAG_START') . Liquid::get('WHITESPACE_CONTROL') . '?\s*(\w+)\s*(.*?)' . Liquid::get('WHITESPACE_CONTROL') . '?' . Liquid::get('TAG_END') . '$/s');
$variableStartRegexp = new Regexp('/^' . Liquid::get('VARIABLE_START') . '/');
$this->nodelist = array();
$tags = Template::getTags();
while (count($tokens)) {
$token = array_shift($tokens);
if ($startRegexp->match($token)) {
$this->whitespaceHandler($token);
if ($tagRegexp->match($token)) {
// If we found the proper block delimitor just end parsing here and let the outer block proceed
if ($tagRegexp->matches[1] == $this->blockDelimiter()) {
$this->endTag();
return;
}
$tagName = null;
if (array_key_exists($tagRegexp->matches[1], $tags)) {
$tagName = $tags[$tagRegexp->matches[1]];
} else {
$tagName = self::TAG_PREFIX . ucwords($tagRegexp->matches[1]);
$tagName = (class_exists($tagName) === true) ? $tagName : null;
}
if ($tagName !== null) {
$this->nodelist[] = new $tagName($tagRegexp->matches[2], $tokens, $this->fileSystem);
if ($tagRegexp->matches[1] == 'extends') {
return;
}
} else {
$this->unknownTag($tagRegexp->matches[1], $tagRegexp->matches[2], $tokens);
}
} else {
throw new ParseException("Tag $token was not properly terminated (won't match $tagRegexp)");
}
} elseif ($variableStartRegexp->match($token)) {
$this->whitespaceHandler($token);
$this->nodelist[] = $this->createVariable($token);
} else {
// This is neither a tag or a variable, proceed with an ltrim
if (self::$trimWhitespace) {
$token = ltrim($token);
}
self::$trimWhitespace = false;
$this->nodelist[] = $token;
}
}
$this->assertMissingDelimitation();
}
/**
* Handle the whitespace.
*
* @param string $token
*/
protected function whitespaceHandler($token)
{
/*
* This assumes that TAG_START is always '{%', and a whitespace control indicator
* is exactly one character long, on a third position.
*/
if (mb_substr($token, 2, 1) === Liquid::get('WHITESPACE_CONTROL')) {
$previousToken = end($this->nodelist);
if (is_string($previousToken)) { // this can also be a tag or a variable
$this->nodelist[key($this->nodelist)] = rtrim($previousToken);
}
}
/*
* This assumes that TAG_END is always '%}', and a whitespace control indicator
* is exactly one character long, on a third position from the end.
*/
self::$trimWhitespace = mb_substr($token, -3, 1) === Liquid::get('WHITESPACE_CONTROL');
}
/**
* Render the block.
*
* @param Context $context
*
* @return string
*/
public function render(Context $context)
{
return $this->renderAll($this->nodelist, $context);
}
/**
* Renders all the given nodelist's nodes
*
* @param array $list
* @param Context $context
*
* @return string
*/
protected function renderAll(array $list, Context $context)
{
$result = '';
foreach ($list as $token) {
if (is_object($token) && method_exists($token, 'render')) {
$value = $token->render($context);
} else {
$value = $token;
}
if (is_array($value)) {
$value = htmlspecialchars(print_r($value, true));
}
$result .= $value;
if (isset($context->registers['break'])) {
break;
}
if (isset($context->registers['continue'])) {
break;
}
$context->tick();
}
return $result;
}
/**
* An action to execute when the end tag is reached
*/
protected function endTag()
{
// Do nothing by default
}
/**
* Handler for unknown tags
*
* @param string $tag
* @param string $params
* @param array $tokens
*
* @throws \Liquid\Exception\ParseException
*/
protected function unknownTag($tag, $params, array $tokens)
{
switch ($tag) {
case 'else':
throw new ParseException($this->blockName() . " does not expect else tag");
case 'end':
throw new ParseException("'end' is not a valid delimiter for " . $this->blockName() . " tags. Use " . $this->blockDelimiter());
default:
throw new ParseException("Unknown tag $tag");
}
}
/**
* This method is called at the end of parsing, and will throw an error unless
* this method is subclassed, like it is for Document
*
* @throws \Liquid\Exception\ParseException
* @return bool
*/
protected function assertMissingDelimitation()
{
throw new ParseException($this->blockName() . " tag was never closed");
}
/**
* Returns the string that delimits the end of the block
*
* @return string
*/
protected function blockDelimiter()
{
return "end" . $this->blockName();
}
/**
* Returns the name of the block
*
* @return string
*/
private function blockName()
{
$reflection = new \ReflectionClass($this);
return str_replace('tag', '', strtolower($reflection->getShortName()));
}
/**
* Create a variable for the given token
*
* @param string $token
*
* @throws \Liquid\Exception\ParseException
* @return Variable
*/
private function createVariable($token)
{
$variableRegexp = new Regexp('/^' . Liquid::get('VARIABLE_START') . Liquid::get('WHITESPACE_CONTROL') . '?(.*?)' . Liquid::get('WHITESPACE_CONTROL') . '?' . Liquid::get('VARIABLE_END') . '$/s');
if ($variableRegexp->match($token)) {
return new Variable($variableRegexp->matches[1]);
}
throw new ParseException("Variable $token was not properly terminated");
}
}

View File

@@ -0,0 +1,100 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
/**
* Base class for tags.
*/
abstract class AbstractTag
{
/**
* The markup for the tag
*
* @var string
*/
protected $markup;
/**
* Filesystem object is used to load included template files
*
* @var FileSystem
*/
protected $fileSystem;
/**
* Additional attributes
*
* @var array
*/
protected $attributes = array();
/**
* Constructor.
*
* @param string $markup
* @param array $tokens
* @param FileSystem $fileSystem
*/
public function __construct($markup, array &$tokens, FileSystem $fileSystem = null)
{
$this->markup = $markup;
$this->fileSystem = $fileSystem;
$this->parse($tokens);
}
/**
* Parse the given tokens.
*
* @param array $tokens
*/
public function parse(array &$tokens)
{
// Do nothing by default
}
/**
* Render the tag with the given context.
*
* @param Context $context
*
* @return string
*/
abstract public function render(Context $context);
/**
* Extracts tag attributes from a markup string.
*
* @param string $markup
*/
protected function extractAttributes($markup)
{
$this->attributes = array();
$attributeRegexp = new Regexp(Liquid::get('TAG_ATTRIBUTES'));
$matches = $attributeRegexp->scan($markup);
foreach ($matches as $match) {
$this->attributes[$match[0]] = $match[1];
}
}
/**
* Returns the name of the tag.
*
* @return string
*/
protected function name()
{
return strtolower(get_class($this));
}
}

View File

@@ -0,0 +1,78 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
/**
* Base class for Cache.
*/
abstract class Cache
{
/** @var int */
protected $expire = 3600;
/** @var string */
protected $prefix = 'liquid_';
/** @var string */
protected $path;
/**
* @param array $options
*/
public function __construct(array $options = array())
{
if (isset($options['cache_expire'])) {
$this->expire = $options['cache_expire'];
}
if (isset($options['cache_prefix'])) {
$this->prefix = $options['cache_prefix'];
}
}
/**
* Retrieves a value from cache with a specified key.
*
* @param string $key a unique key identifying the cached value
* @param bool $unserialize
*
* @return mixed|boolean the value stored in cache, false if the value is not in the cache or expired.
*/
abstract public function read($key, $unserialize = true);
/**
* Check if specified key exists in cache.
*
* @param string $key a unique key identifying the cached value
*
* @return boolean true if the key is in cache, false otherwise
*/
abstract public function exists($key);
/**
* Stores a value identified by a key in cache.
*
* @param string $key the key identifying the value to be cached
* @param mixed $value the value to be cached
* @param bool $serialize
*
* @return boolean true if the value is successfully stored into cache, false otherwise
*/
abstract public function write($key, $value, $serialize = true);
/**
* Deletes all values from cache.
*
* @param bool $expiredOnly
*
* @return boolean whether the flush operation was successful.
*/
abstract public function flush($expiredOnly = false);
}

View File

@@ -0,0 +1,74 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Cache;
use Liquid\Cache;
use Liquid\LiquidException;
/**
* Implements cache stored in Apc.
*
* @codeCoverageIgnore
*/
class Apc extends Cache
{
/**
* Constructor.
*
* It checks the availability of apccache.
*
* @param array $options
*
* @throws LiquidException if APC cache extension is not loaded or is disabled.
*/
public function __construct(array $options = array())
{
parent::__construct($options);
if (!function_exists('apc_fetch')) {
throw new LiquidException(get_class($this).' requires PHP apc extension or similar to be loaded.');
}
}
/**
* {@inheritdoc}
*/
public function read($key, $unserialize = true)
{
return apc_fetch($this->prefix . $key);
}
/**
* {@inheritdoc}
*/
public function exists($key)
{
apc_fetch($this->prefix . $key, $success);
return (bool) $success;
}
/**
* {@inheritdoc}
*/
public function write($key, $value, $serialize = true)
{
return apc_store($this->prefix . $key, $value, $this->expire);
}
/**
* {@inheritdoc}
*/
public function flush($expiredOnly = false)
{
return apc_clear_cache('user');
}
}

View File

@@ -0,0 +1,106 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Cache;
use Liquid\Cache;
use Liquid\Exception\NotFoundException;
/**
* Implements cache stored in files.
*/
class File extends Cache
{
/**
* Constructor.
*
* It checks the availability of cache directory.
*
* @param array $options
*
* @throws NotFoundException if Cachedir not exists.
*/
public function __construct(array $options = array())
{
parent::__construct($options);
if (isset($options['cache_dir']) && is_writable($options['cache_dir'])) {
$this->path = realpath($options['cache_dir']) . DIRECTORY_SEPARATOR;
} else {
throw new NotFoundException('Cachedir not exists or not writable');
}
}
/**
* {@inheritdoc}
*/
public function read($key, $unserialize = true)
{
if (!$this->exists($key)) {
return false;
}
if ($unserialize) {
return unserialize(file_get_contents($this->path . $this->prefix . $key));
}
return file_get_contents($this->path . $this->prefix . $key);
}
/**
* {@inheritdoc}
*/
public function exists($key)
{
$cacheFile = $this->path . $this->prefix . $key;
if (!file_exists($cacheFile) || filemtime($cacheFile) + $this->expire < time()) {
return false;
}
return true;
}
/**
* {@inheritdoc}
*/
public function write($key, $value, $serialize = true)
{
$bytes = file_put_contents($this->path . $this->prefix . $key, $serialize ? serialize($value) : $value);
$this->gc();
return $bytes !== false;
}
/**
* {@inheritdoc}
*/
public function flush($expiredOnly = false)
{
foreach (glob($this->path . $this->prefix . '*') as $file) {
if ($expiredOnly) {
if (filemtime($file) + $this->expire < time()) {
unlink($file);
}
} else {
unlink($file);
}
}
}
/**
* {@inheritdoc}
*/
protected function gc()
{
$this->flush(true);
}
}

View File

@@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Cache;
use Liquid\Cache;
/**
* Implements cache with data stored in an embedded variable with no handling of expiration dates for simplicity
*/
class Local extends Cache
{
private $cache = array();
/**
* {@inheritdoc}
*/
public function read($key, $unserialize = true)
{
if (isset($this->cache[$key])) {
return $this->cache[$key];
}
return false;
}
/**
* {@inheritdoc}
*/
public function exists($key)
{
return isset($this->cache[$key]);
}
/**
* {@inheritdoc}
*/
public function write($key, $value, $serialize = true)
{
$this->cache[$key] = $value;
return true;
}
/**
* {@inheritdoc}
*/
public function flush($expiredOnly = false)
{
$this->cache = array();
return true;
}
}

View File

@@ -0,0 +1,458 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
/**
* Context keeps the variable stack and resolves variables, as well as keywords.
*/
class Context
{
/**
* Local scopes
*
* @var array
*/
protected $assigns;
/**
* Registers for non-variable state data
*
* @var array
*/
public $registers;
/**
* The filterbank holds all the filters
*
* @var Filterbank
*/
protected $filterbank;
/**
* Global scopes
*
* @var array
*/
public $environments = array();
/**
* Called "sometimes" while rendering. For example to abort the execution of a rendering.
*
* @var null|callable
*/
private $tickFunction = null;
/**
* Constructor
*
* @param array $assigns
* @param array $registers
*/
public function __construct(array $assigns = array(), array $registers = array())
{
$this->assigns = array($assigns);
$this->registers = $registers;
$this->filterbank = new Filterbank($this);
// first empty array serves as source for overrides, e.g. as in TagDecrement
$this->environments = array(array(), array());
if (Liquid::get('EXPOSE_SERVER')) {
$this->environments[1] = $_SERVER;
} else {
$this->environments[1] = array_filter(
$_SERVER,
function ($key) {
return in_array(
$key,
(array)Liquid::get('SERVER_SUPERGLOBAL_WHITELIST')
);
},
ARRAY_FILTER_USE_KEY
);
}
}
/**
* Sets a tick function, this function is called sometimes while liquid is rendering a template.
*
* @param callable $tickFunction
*/
public function setTickFunction(callable $tickFunction)
{
$this->tickFunction = $tickFunction;
}
/**
* Add a filter to the context
*
* @param mixed $filter
*/
public function addFilters($filter, callable $callback = null)
{
$this->filterbank->addFilter($filter, $callback);
}
/**
* Invoke the filter that matches given name
*
* @param string $name The name of the filter
* @param mixed $value The value to filter
* @param array $args Additional arguments for the filter
*
* @return string
*/
public function invoke($name, $value, array $args = array())
{
try {
return $this->filterbank->invoke($name, $value, $args);
} catch (\TypeError $typeError) {
throw new LiquidException($typeError->getMessage(), 0, $typeError);
}
}
/**
* Merges the given assigns into the current assigns
*
* @param array $newAssigns
*/
public function merge($newAssigns)
{
$this->assigns[0] = array_merge($this->assigns[0], $newAssigns);
}
/**
* Push new local scope on the stack.
*
* @return bool
*/
public function push()
{
array_unshift($this->assigns, array());
return true;
}
/**
* Pops the current scope from the stack.
*
* @throws LiquidException
* @return bool
*/
public function pop()
{
if (count($this->assigns) == 1) {
throw new LiquidException('No elements to pop');
}
array_shift($this->assigns);
}
/**
* Replaces []
*
* @param string
* @param mixed $key
*
* @return mixed
*/
public function get($key)
{
return $this->resolve($key);
}
/**
* Replaces []=
*
* @param string $key
* @param mixed $value
* @param bool $global
*/
public function set($key, $value, $global = false)
{
if ($global) {
for ($i = 0; $i < count($this->assigns); $i++) {
$this->assigns[$i][$key] = $value;
}
} else {
$this->assigns[0][$key] = $value;
}
}
/**
* Returns true if the given key will properly resolve
*
* @param string $key
*
* @return bool
*/
public function hasKey($key)
{
return (!is_null($this->resolve($key)));
}
/**
* Resolve a key by either returning the appropriate literal or by looking up the appropriate variable
*
* Test for empty has been moved to interpret condition, in Decision
*
* @param string $key
*
* @throws LiquidException
* @return mixed
*/
private function resolve($key)
{
// This shouldn't happen
if (is_array($key)) {
throw new LiquidException("Cannot resolve arrays as key");
}
if (is_null($key) || $key == 'null') {
return null;
}
if ($key == 'true') {
return true;
}
if ($key == 'false') {
return false;
}
if (preg_match('/^\'(.*)\'$/', $key, $matches)) {
return $matches[1];
}
if (preg_match('/^"(.*)"$/', $key, $matches)) {
return $matches[1];
}
if (preg_match('/^(-?\d+)$/', $key, $matches)) {
return $matches[1];
}
if (preg_match('/^(-?\d[\d\.]+)$/', $key, $matches)) {
return $matches[1];
}
return $this->variable($key);
}
/**
* Fetches the current key in all the scopes
*
* @param string $key
*
* @return mixed
*/
private function fetch($key)
{
// TagDecrement depends on environments being checked before assigns
foreach ($this->environments as $environment) {
if (array_key_exists($key, $environment)) {
return $environment[$key];
}
}
foreach ($this->assigns as $scope) {
if (array_key_exists($key, $scope)) {
$obj = $scope[$key];
if ($obj instanceof Drop) {
$obj->setContext($this);
}
return $obj;
}
}
return null;
}
/**
* Resolved the namespaced queries gracefully.
*
* @param string $key
*
* @see Decision::stringValue
* @see AbstractBlock::renderAll
*
* @throws LiquidException
* @return mixed
*/
private function variable($key)
{
// Support numeric and variable array indicies
if (preg_match("|\[[0-9]+\]|", $key)) {
$key = preg_replace("|\[([0-9]+)\]|", ".$1", $key);
} elseif (preg_match("|\[[0-9a-z._]+\]|", $key, $matches)) {
$index = $this->get(str_replace(array("[", "]"), "", $matches[0]));
if (strlen($index)) {
$key = preg_replace("|\[([0-9a-z._]+)\]|", ".$index", $key);
}
}
$parts = explode(Liquid::get('VARIABLE_ATTRIBUTE_SEPARATOR'), $key);
$object = $this->fetch(array_shift($parts));
while (count($parts) > 0) {
// since we still have a part to consider
// and since we can't dig deeper into plain values
// it can be thought as if it has a property with a null value
if (!is_object($object) && !is_array($object) && !is_string($object)) {
return null;
}
// first try to cast an object to an array or value
if (is_object($object)) {
if (method_exists($object, 'toLiquid')) {
$object = $object->toLiquid();
} elseif (method_exists($object, 'toArray')) {
$object = $object->toArray();
}
}
if (is_null($object)) {
return null;
}
if ($object instanceof Drop) {
$object->setContext($this);
}
$nextPartName = array_shift($parts);
if (is_string($object)) {
if ($nextPartName == 'size') {
// if the last part of the context variable is .size we return the string length
return mb_strlen($object);
}
// no other special properties for strings, yet
return null;
}
if (is_array($object)) {
// if the last part of the context variable is .first we return the first array element
if ($nextPartName == 'first' && count($parts) == 0 && !array_key_exists('first', $object)) {
return StandardFilters::first($object);
}
// if the last part of the context variable is .last we return the last array element
if ($nextPartName == 'last' && count($parts) == 0 && !array_key_exists('last', $object)) {
return StandardFilters::last($object);
}
// if the last part of the context variable is .size we just return the count
if ($nextPartName == 'size' && count($parts) == 0 && !array_key_exists('size', $object)) {
return count($object);
}
// no key - no value
if (!array_key_exists($nextPartName, $object)) {
return null;
}
$object = $object[$nextPartName];
continue;
}
if (!is_object($object)) {
// we got plain value, yet asked to resolve a part
// think plain values have a null part with any name
return null;
}
if ($object instanceof \Countable) {
// if the last part of the context variable is .size we just return the count
if ($nextPartName == 'size' && count($parts) == 0) {
return count($object);
}
}
if ($object instanceof Drop) {
// if the object is a drop, make sure it supports the given method
if (!$object->hasKey($nextPartName)) {
return null;
}
$object = $object->invokeDrop($nextPartName);
continue;
}
// if it has `get` or `field_exists` methods
if (method_exists($object, Liquid::get('HAS_PROPERTY_METHOD'))) {
if (!call_user_func(array($object, Liquid::get('HAS_PROPERTY_METHOD')), $nextPartName)) {
return null;
}
$object = call_user_func(array($object, Liquid::get('GET_PROPERTY_METHOD')), $nextPartName);
continue;
}
// if it's just a regular object, attempt to access a public method
if (is_callable(array($object, $nextPartName))) {
$object = call_user_func(array($object, $nextPartName));
continue;
}
// if a magic accessor method present...
if (is_object($object) && method_exists($object, '__get')) {
$object = $object->$nextPartName;
continue;
}
// Inexistent property is a null, PHP-speak
if (!property_exists($object, $nextPartName)) {
return null;
}
// then try a property (independent of accessibility)
if (property_exists($object, $nextPartName)) {
$object = $object->$nextPartName;
continue;
}
// we'll try casting this object in the next iteration
}
// lastly, try to get an embedded value of an object
// value could be of any type, not just string, so we have to do this
// conversion here, not later in AbstractBlock::renderAll
if (is_object($object) && method_exists($object, 'toLiquid')) {
$object = $object->toLiquid();
}
/*
* Before here were checks for object types and object to string conversion.
*
* Now we just return what we have:
* - Traversable objects are taken care of inside filters
* - Object-to-string conversion is handled at the last moment in Decision::stringValue, and in AbstractBlock::renderAll
*
* This way complex objects could be passed between templates and to filters
*/
return $object;
}
public function tick()
{
if ($this->tickFunction === null) {
return;
}
$tickFunction = $this->tickFunction;
$tickFunction($this);
}
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
/**
* A selection of custom filters.
*/
class CustomFilters
{
/**
* Sort an array by key.
*
* @param array $input
*
* @return array
*/
public static function sort_key(array $input)
{
ksort($input);
return $input;
}
}

View File

@@ -0,0 +1,161 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
use Liquid\Exception\RenderException;
/**
* Base class for blocks that make logical decisions.
*/
class Decision extends AbstractBlock
{
/**
* The current left variable to compare
*
* @var string
*/
public $left;
/**
* The current right variable to compare
*
* @var string
*/
public $right;
/**
* Returns a string value of an array for comparisons
*
* @param mixed $value
*
* @throws \Liquid\Exception\RenderException
* @return string
*/
private function stringValue($value)
{
// Objects should have a __toString method to get a value to compare to
if (is_object($value)) {
if (method_exists($value, '__toString')) {
return (string) $value;
}
if ($value instanceof \Generator) {
return (string) $value->valid();
}
// toLiquid is handled in Context::variable
$class = get_class($value);
throw new RenderException("Value of type $class has no `toLiquid` nor `__toString` methods");
}
// Arrays simply return true
if (is_array($value)) {
return $value;
}
return $value;
}
/**
* Check to see if to variables are equal in a given context
*
* @param string $left
* @param string $right
* @param Context $context
*
* @return bool
*/
protected function equalVariables($left, $right, Context $context)
{
$left = $this->stringValue($context->get($left));
$right = $this->stringValue($context->get($right));
return ($left == $right);
}
/**
* Interpret a comparison
*
* @param string $left
* @param string $right
* @param string $op
* @param Context $context
*
* @throws \Liquid\Exception\RenderException
* @return bool
*/
protected function interpretCondition($left, $right, $op, Context $context)
{
if (is_null($op)) {
$value = $this->stringValue($context->get($left));
return $value;
}
// values of 'empty' have a special meaning in array comparisons
if ($right == 'empty' && is_array($context->get($left))) {
$left = count($context->get($left));
$right = 0;
} elseif ($left == 'empty' && is_array($context->get($right))) {
$right = count($context->get($right));
$left = 0;
} else {
$left = $context->get($left);
$right = $context->get($right);
$left = $this->stringValue($left);
$right = $this->stringValue($right);
}
// special rules for null values
if (is_null($left) || is_null($right)) {
// null == null returns true
if ($op == '==' && is_null($left) && is_null($right)) {
return true;
}
// null != anything other than null return true
if ($op == '!=' && (!is_null($left) || !is_null($right))) {
return true;
}
// everything else, return false;
return false;
}
// regular rules
switch ($op) {
case '==':
return ($left == $right);
case '!=':
return ($left != $right);
case '>':
return ($left > $right);
case '<':
return ($left < $right);
case '>=':
return ($left >= $right);
case '<=':
return ($left <= $right);
case 'contains':
return is_array($left) ? in_array($right, $left) : (strpos($left, $right) !== false);
default:
throw new RenderException("Error in tag '" . $this->name() . "' - Unknown operator $op");
}
}
}

View File

@@ -0,0 +1,95 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
use Liquid\Tag\TagInclude;
use Liquid\Tag\TagExtends;
use Liquid\Tag\TagBlock;
/**
* This class represents the entire template document.
*/
class Document extends AbstractBlock
{
/**
* Constructor.
*
* @param array $tokens
* @param FileSystem $fileSystem
*/
public function __construct(array &$tokens, FileSystem $fileSystem = null)
{
$this->fileSystem = $fileSystem;
$this->parse($tokens);
}
/**
* Check for cached includes; if there are - do not use cache
*
* @see \Liquid\Tag\TagInclude::hasIncludes()
* @see \Liquid\Tag\TagExtends::hasIncludes()
* @return bool if need to discard cache
*/
public function hasIncludes()
{
$seenExtends = false;
$seenBlock = false;
foreach ($this->nodelist as $token) {
if ($token instanceof TagExtends) {
$seenExtends = true;
} elseif ($token instanceof TagBlock) {
$seenBlock = true;
}
}
/*
* We try to keep the base templates in cache (that not extend anything).
*
* At the same time if we re-render all other blocks we see, we avoid most
* if not all related caching quirks. This may be suboptimal.
*/
if ($seenBlock && !$seenExtends) {
return true;
}
foreach ($this->nodelist as $token) {
// check any of the tokens for includes
if ($token instanceof TagInclude && $token->hasIncludes()) {
return true;
}
if ($token instanceof TagExtends && $token->hasIncludes()) {
return true;
}
}
return false;
}
/**
* There isn't a real delimiter
*
* @return string
*/
protected function blockDelimiter()
{
return '';
}
/**
* Document blocks don't need to be terminated since they are not actually opened
*/
protected function assertMissingDelimitation()
{
}
}

View File

@@ -0,0 +1,107 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
/**
* A drop in liquid is a class which allows you to to export DOM like things to liquid.
* Methods of drops are callable.
* The main use for liquid drops is the implement lazy loaded objects.
* If you would like to make data available to the web designers which you don't want loaded unless needed then
* a drop is a great way to do that
*
* Example:
*
* class ProductDrop extends LiquidDrop {
* public function topSales() {
* Products::find('all', array('order' => 'sales', 'limit' => 10 ));
* }
* }
*
* tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
* tmpl.render('product' => ProductDrop.new ) // will invoke topSales query.
*
* Your drop can either implement the methods sans any parameters or implement the beforeMethod(name) method which is a
* catch all.
*/
abstract class Drop
{
/**
* @var Context
*/
protected $context;
/**
* Catch all method that is invoked before a specific method
*
* @param string $method
*
* @return null
*/
protected function beforeMethod($method)
{
return null;
}
/**
* @param Context $context
*/
public function setContext(Context $context)
{
$this->context = $context;
}
/**
* Invoke a specific method
*
* @param string $method
*
* @return mixed
*/
public function invokeDrop($method)
{
$result = $this->beforeMethod($method);
if (is_null($result) && is_callable(array($this, $method))) {
$result = $this->$method();
}
return $result;
}
/**
* Returns true if the drop supports the given method
*
* @param string $name
*
* @return bool
*/
public function hasKey($name)
{
return true;
}
/**
* @return Drop
*/
public function toLiquid()
{
return $this;
}
/**
* @return string
*/
public function __toString()
{
return get_class($this);
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Exception;
use Liquid\LiquidException;
/**
* CacheException class.
*/
class CacheException extends LiquidException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Exception;
use Liquid\LiquidException;
/**
* FilesystemException class.
*/
class FilesystemException extends LiquidException
{
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Exception;
use Liquid\Exception\ParseException;
/**
* Class MissingFilesystemException
* @package Liquid\Exception
*/
class MissingFilesystemException extends ParseException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Exception;
use Liquid\Exception\FilesystemException;
/**
* NotFoundException class.
*/
class NotFoundException extends FilesystemException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Exception;
use Liquid\LiquidException;
/**
* ParseException class.
*/
class ParseException extends LiquidException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Exception;
use Liquid\LiquidException;
/**
* RenderException class.
*/
class RenderException extends LiquidException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Exception;
use Liquid\LiquidException;
/**
* WrongArgumentException class.
*/
class WrongArgumentException extends LiquidException
{
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
/**
* A Liquid file system is way to let your templates retrieve other templates for use with the include tag.
*
* You can implement subclasses that retrieve templates from the database, from the file system using a different
* path structure, you can provide them as hard-coded inline strings, or any manner that you see fit.
*
* You can add additional instance variables, arguments, or methods as needed.
*/
interface FileSystem
{
/**
* Retrieve a template file.
*
* @param string $templatePath
*
* @return string
*/
public function readTemplateFile($templatePath);
}

View File

@@ -0,0 +1,110 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\FileSystem;
use Liquid\Exception\NotFoundException;
use Liquid\Exception\ParseException;
use Liquid\FileSystem;
use Liquid\Regexp;
use Liquid\Liquid;
/**
* This implements an abstract file system which retrieves template files named in a manner similar to Rails partials,
* ie. with the template name prefixed with an underscore. The extension ".liquid" is also added.
*
* For security reasons, template paths are only allowed to contain letters, numbers, and underscore.
*/
class Local implements FileSystem
{
/**
* The root path
*
* @var string
*/
private $root;
/**
* Constructor
*
* @param string $root The root path for templates
* @throws \Liquid\Exception\NotFoundException
*/
public function __construct($root)
{
// since root path can only be set from constructor, we check it once right here
if (!empty($root)) {
$realRoot = realpath($root);
if ($realRoot === false) {
throw new NotFoundException("Root path could not be found: '$root'");
}
$root = $realRoot;
}
$this->root = $root;
}
/**
* Retrieve a template file
*
* @param string $templatePath
*
* @return string template content
*/
public function readTemplateFile($templatePath)
{
return file_get_contents($this->fullPath($templatePath));
}
/**
* Resolves a given path to a full template file path, making sure it's valid
*
* @param string $templatePath
*
* @throws \Liquid\Exception\ParseException
* @throws \Liquid\Exception\NotFoundException
* @return string
*/
public function fullPath($templatePath)
{
if (empty($templatePath)) {
throw new ParseException("Empty template name");
}
$nameRegex = Liquid::get('INCLUDE_ALLOW_EXT')
? new Regexp('/^[^.\/][a-zA-Z0-9_\.\/-]+$/')
: new Regexp('/^[^.\/][a-zA-Z0-9_\/-]+$/');
if (!$nameRegex->match($templatePath)) {
throw new ParseException("Illegal template name '$templatePath'");
}
$templateDir = dirname($templatePath);
$templateFile = basename($templatePath);
if (!Liquid::get('INCLUDE_ALLOW_EXT')) {
$templateFile = Liquid::get('INCLUDE_PREFIX') . $templateFile . '.' . Liquid::get('INCLUDE_SUFFIX');
}
$fullPath = join(DIRECTORY_SEPARATOR, array($this->root, $templateDir, $templateFile));
$realFullPath = realpath($fullPath);
if ($realFullPath === false) {
throw new NotFoundException("File not found: $fullPath");
}
if (strpos($realFullPath, $this->root) !== 0) {
throw new NotFoundException("Illegal template full path: {$realFullPath} not under {$this->root}");
}
return $realFullPath;
}
}

View File

@@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\FileSystem;
use Liquid\Exception\FilesystemException;
use Liquid\FileSystem;
/**
* This implements a virtual file system with actual code used to find files injected from outside thus achieving inversion of control.
*/
class Virtual implements FileSystem
{
/**
* @var callable
*/
private $callback;
/**
* Constructor
*
* @param callable $callback Callback is responsible for providing content of requested templates. Should return template's text.
* @throws \Liquid\Exception\FilesystemException
*/
public function __construct($callback)
{
// Since a callback can only be set from the constructor, we check it once right here.
if (!is_callable($callback)) {
throw new FilesystemException("Not a callback provided");
}
$this->callback = $callback;
}
/**
* Retrieve a template file
*
* @param string $templatePath
*
* @return string template content
*/
public function readTemplateFile($templatePath)
{
return call_user_func($this->callback, $templatePath);
}
public function __sleep()
{
// we cannot serialize a closure
if ($this->callback instanceof \Closure) {
throw new FilesystemException("Virtual file system with a Closure as a callback cannot be used with a serializing cache");
}
return array_keys(get_object_vars($this));
}
}

View File

@@ -0,0 +1,153 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
use Liquid\Exception\WrongArgumentException;
/**
* The filter bank is where all registered filters are stored, and where filter invocation is handled
* it supports a variety of different filter types; objects, class, and simple methods.
*/
class Filterbank
{
/**
* The registered filter objects
*
* @var array
*/
private $filters;
/**
* A map of all filters and the class that contain them (in the case of methods)
*
* @var array
*/
private $methodMap;
/**
* Reference to the current context object
*
* @var Context
*/
private $context;
/**
* Constructor
*
* @param $context
*/
public function __construct(Context $context)
{
$this->context = $context;
$this->addFilter(\Liquid\StandardFilters::class);
$this->addFilter(\Liquid\CustomFilters::class);
}
/**
* Adds a filter to the bank
*
* @param mixed $filter Can either be an object, the name of a class (in which case the
* filters will be called statically) or the name of a function.
*
* @throws \Liquid\Exception\WrongArgumentException
* @return bool
*/
public function addFilter($filter, callable $callback = null)
{
// If it is a callback, save it as it is
if (is_string($filter) && $callback) {
$this->methodMap[$filter] = $callback;
return true;
}
// If the filter is a class, register all its static methods
if (is_string($filter) && class_exists($filter)) {
$reflection = new \ReflectionClass($filter);
foreach ($reflection->getMethods(\ReflectionMethod::IS_STATIC) as $method) {
$this->methodMap[$method->name] = $method->class;
}
return true;
}
// If it's a global function, register it simply
if (is_string($filter) && function_exists($filter)) {
$this->methodMap[$filter] = false;
return true;
}
// If it isn't an object an isn't a string either, it's a bad parameter
if (!is_object($filter)) {
throw new WrongArgumentException("Parameter passed to addFilter must be an object or a string");
}
// If the passed filter was an object, store the object for future reference.
$filter->context = $this->context;
$className = get_class($filter);
$this->filters[$className] = $filter;
// Then register all public static and not methods as filters
foreach (get_class_methods($filter) as $method) {
if (strtolower($method) === '__construct') {
continue;
}
$this->methodMap[$method] = $className;
}
return true;
}
/**
* Invokes the filter with the given name
*
* @param string $name The name of the filter
* @param string $value The value to filter
* @param array $args The additional arguments for the filter
*
* @return string
*/
public function invoke($name, $value, array $args = array())
{
// workaround for a single standard filter being a reserved keyword - we can't use overloading for static calls
if ($name == 'default') {
$name = '_default';
}
array_unshift($args, $value);
// Consult the mapping
if (!isset($this->methodMap[$name])) {
return $value;
}
$class = $this->methodMap[$name];
// If we have a callback
if (is_callable($class)) {
return call_user_func_array($class, $args);
}
// If we have a registered object for the class, use that instead
if (isset($this->filters[$class])) {
$class = $this->filters[$class];
}
// If we're calling a function
if ($class === false) {
return call_user_func_array($name, $args);
}
// Call a class or an instance method
return call_user_func_array(array($class, $name), $args);
}
}

View File

@@ -0,0 +1,170 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
/**
* Liquid for PHP.
*/
class Liquid
{
/**
* We cannot make settings constants, because we cannot create compound
* constants in PHP (before 5.6).
*
* @var array configuration array
*/
public static $config = array(
// The method is called on objects when resolving variables to see
// if a given property exists.
'HAS_PROPERTY_METHOD' => 'field_exists',
// This method is called on object when resolving variables when
// a given property exists.
'GET_PROPERTY_METHOD' => 'get',
// Separator between filters.
'FILTER_SEPARATOR' => '\|',
// Separator for arguments.
'ARGUMENT_SEPARATOR' => ',',
// Separator for argument names and values.
'FILTER_ARGUMENT_SEPARATOR' => ':',
// Separator for variable attributes.
'VARIABLE_ATTRIBUTE_SEPARATOR' => '.',
// Allow template names with extension in include and extends tags.
'INCLUDE_ALLOW_EXT' => false,
// Suffix for include files.
'INCLUDE_SUFFIX' => 'liquid',
// Prefix for include files.
'INCLUDE_PREFIX' => '_',
// Whitespace control.
'WHITESPACE_CONTROL' => '-',
// Tag start.
'TAG_START' => '{%',
// Tag end.
'TAG_END' => '%}',
// Variable start.
'VARIABLE_START' => '{{',
// Variable end.
'VARIABLE_END' => '}}',
// Variable name.
'VARIABLE_NAME' => '[a-zA-Z_][a-zA-Z_0-9.-]*',
'QUOTED_STRING' => '(?:"[^"]*"|\'[^\']*\')',
'QUOTED_STRING_FILTER_ARGUMENT' => '"[^"]*"|\'[^\']*\'',
// Automatically escape any variables unless told otherwise by a "raw" filter
'ESCAPE_BY_DEFAULT' => false,
// The name of the key to use when building pagination query strings e.g. ?page=1
'PAGINATION_REQUEST_KEY' => 'page',
// The name of the context key used to denote the current page number
'PAGINATION_CONTEXT_KEY' => 'page',
// Whenever variables from $_SERVER should be directly available to templates
'EXPOSE_SERVER' => false,
// $_SERVER variables whitelist - exposed even when EXPOSE_SERVER is false
'SERVER_SUPERGLOBAL_WHITELIST' => [
'HTTP_ACCEPT',
'HTTP_ACCEPT_CHARSET',
'HTTP_ACCEPT_ENCODING',
'HTTP_ACCEPT_LANGUAGE',
'HTTP_CONNECTION',
'HTTP_HOST',
'HTTP_REFERER',
'HTTP_USER_AGENT',
'HTTPS',
'REQUEST_METHOD',
'REQUEST_URI',
'SERVER_NAME',
],
);
/**
* Get a configuration setting.
*
* @param string $key setting key
*
* @return string
*/
public static function get($key)
{
// backward compatibility
if ($key === 'ALLOWED_VARIABLE_CHARS') {
return substr(self::$config['VARIABLE_NAME'], 0, -1);
}
if (array_key_exists($key, self::$config)) {
return self::$config[$key];
}
// This case is needed for compound settings
switch ($key) {
case 'QUOTED_FRAGMENT':
return '(?:' . self::get('QUOTED_STRING') . '|(?:[^\s,\|\'"]|' . self::get('QUOTED_STRING') . ')+)';
case 'TAG_ATTRIBUTES':
return '/(\w+)\s*\:\s*(' . self::get('QUOTED_FRAGMENT') . ')/';
case 'TOKENIZATION_REGEXP':
return '/(' . self::$config['TAG_START'] . '.*?' . self::$config['TAG_END'] . '|' . self::$config['VARIABLE_START'] . '.*?' . self::$config['VARIABLE_END'] . ')/s';
default:
return null;
}
}
/**
* Changes/creates a setting.
*
* @param string $key
* @param string $value
*/
public static function set($key, $value)
{
// backward compatibility
if ($key === 'ALLOWED_VARIABLE_CHARS') {
$key = 'VARIABLE_NAME';
$value .= '+';
}
self::$config[$key] = $value;
}
/**
* Flatten a multidimensional array into a single array. Does not maintain keys.
*
* @param array $array
*
* @return array
*/
public static function arrayFlatten($array)
{
$return = array();
foreach ($array as $element) {
if (is_array($element)) {
$return = array_merge($return, self::arrayFlatten($element));
} else {
$return[] = $element;
}
}
return $return;
}
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
/**
* LiquidException class.
*/
class LiquidException extends \Exception
{
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
/**
* @deprecated Left for backward compatibility reasons. Use \Liquid\FileSystem\Local instead.
*/
class LocalFileSystem extends \Liquid\FileSystem\Local
{
}

View File

@@ -0,0 +1,134 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
/**
* A support class for regular expressions and
* non liquid specific support classes and functions.
*/
class Regexp
{
/**
* The regexp pattern
*
* @var string
*/
private $pattern;
/**
* The matches from the last method called
*
* @var array;
*/
public $matches;
/**
* Constructor
*
* @param string $pattern
*
* @return Regexp
*/
public function __construct($pattern)
{
$this->pattern = (substr($pattern, 0, 1) != '/')
? '/' . $this->quote($pattern) . '/'
: $pattern;
}
/**
* Quotes regular expression characters
*
* @param string $string
*
* @return string
*/
public function quote($string)
{
return preg_quote($string, '/');
}
/**
* Returns an array of matches for the string in the same way as Ruby's scan method
*
* @param string $string
*
* @return array
*/
public function scan($string)
{
preg_match_all($this->pattern, $string, $matches);
if (count($matches) == 1) {
return $matches[0];
}
array_shift($matches);
$result = array();
foreach ($matches as $matchKey => $subMatches) {
foreach ($subMatches as $subMatchKey => $subMatch) {
$result[$subMatchKey][$matchKey] = $subMatch;
}
}
return $result;
}
/**
* Matches the given string. Only matches once.
*
* @param string $string
*
* @return int 1 if there was a match, 0 if there wasn't
*/
public function match($string)
{
return preg_match($this->pattern, $string, $this->matches);
}
/**
* Matches the given string. Matches all.
*
* @param string $string
*
* @return int The number of matches
*/
public function matchAll($string)
{
return preg_match_all($this->pattern, $string, $this->matches);
}
/**
* Splits the given string
*
* @param string $string
* @param int $limit Limits the amount of results returned
*
* @return array
*/
public function split($string, $limit = -1)
{
return preg_split($this->pattern, $string, $limit);
}
/**
* Returns the original pattern primarily for debugging purposes
*
* @return string
*/
public function __toString()
{
return $this->pattern;
}
}

View File

@@ -0,0 +1,727 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
use Liquid\Exception\RenderException;
/**
* A selection of standard filters.
*/
class StandardFilters
{
/**
* Add one string to another
*
* @param string $input
* @param string $string
*
* @return string
*/
public static function append($input, $string)
{
return $input . $string;
}
/**
* Capitalize words in the input sentence
*
* @param string $input
*
* @return string
*/
public static function capitalize($input)
{
return preg_replace_callback("/(^|[^\p{L}'])([\p{Ll}])/u", function ($matches) {
$first_char = mb_substr($matches[2], 0, 1);
return $matches[1] . mb_strtoupper($first_char) . mb_substr($matches[2], 1);
}, ucwords($input));
}
/**
* @param mixed $input number
*
* @return int
*/
public static function ceil($input)
{
return (int) ceil((float)$input);
}
/**
* Formats a date using strftime
*
* @param mixed $input
* @param string $format
*
* @return string
*/
public static function date($input, $format)
{
if (!is_numeric($input)) {
$input = strtotime($input);
}
if ($format == 'r') {
return date($format, $input);
}
return strftime($format, $input);
}
/**
* Default
*
* @param string $input
* @param string $default_value
*
* @return string
*/
public static function _default($input, $default_value)
{
$isBlank = $input == '' || $input === false || $input === null;
return $isBlank ? $default_value : $input;
}
/**
* division
*
* @param float $input
* @param float $operand
*
* @return float
*/
public static function divided_by($input, $operand)
{
return (float)$input / (float)$operand;
}
/**
* Convert an input to lowercase
*
* @param string $input
*
* @return string
*/
public static function downcase($input)
{
return is_string($input) ? mb_strtolower($input) : $input;
}
/**
* Pseudo-filter: negates auto-added escape filter
*
* @param string $input
*
* @return string
*/
public static function raw($input)
{
return $input;
}
/**
* Escape a string
*
* @param string $input
*
* @return string
*/
public static function escape($input)
{
// Arrays are taken care down the stack with an error
if (is_array($input)) {
return $input;
}
return htmlentities($input, ENT_QUOTES);
}
/**
* Escape a string once, keeping all previous HTML entities intact
*
* @param string $input
*
* @return string
*/
public static function escape_once($input)
{
// Arrays are taken care down the stack with an error
if (is_array($input)) {
return $input;
}
return htmlentities($input, ENT_QUOTES, null, false);
}
/**
* Returns the first element of an array
*
* @param array|\Iterator $input
*
* @return mixed
*/
public static function first($input)
{
if ($input instanceof \Iterator) {
$input->rewind();
return $input->current();
}
return is_array($input) ? reset($input) : $input;
}
/**
* @param mixed $input number
*
* @return int
*/
public static function floor($input)
{
return (int) floor((float)$input);
}
/**
* Joins elements of an array with a given character between them
*
* @param array|\Traversable $input
* @param string $glue
*
* @return string
*/
public static function join($input, $glue = ' ')
{
if ($input instanceof \Traversable) {
$str = '';
foreach ($input as $elem) {
if ($str) {
$str .= $glue;
}
$str .= $elem;
}
return $str;
}
return is_array($input) ? implode($glue, $input) : $input;
}
/**
* Returns the last element of an array
*
* @param array|\Traversable $input
*
* @return mixed
*/
public static function last($input)
{
if ($input instanceof \Traversable) {
$last = null;
foreach ($input as $elem) {
$last = $elem;
}
return $last;
}
return is_array($input) ? end($input) : $input;
}
/**
* @param string $input
*
* @return string
*/
public static function lstrip($input)
{
return ltrim($input);
}
/**
* Map/collect on a given property
*
* @param array|\Traversable $input
* @param string $property
*
* @return string
*/
public static function map($input, $property)
{
if ($input instanceof \Traversable) {
$input = iterator_to_array($input);
}
if (!is_array($input)) {
return $input;
}
return array_map(function ($elem) use ($property) {
if (is_callable($elem)) {
return $elem();
} elseif (is_array($elem) && array_key_exists($property, $elem)) {
return $elem[$property];
}
return null;
}, $input);
}
/**
* subtraction
*
* @param float $input
* @param float $operand
*
* @return float
*/
public static function minus($input, $operand)
{
return (float)$input - (float)$operand;
}
/**
* modulo
*
* @param float $input
* @param float $operand
*
* @return float
*/
public static function modulo($input, $operand)
{
return fmod((float)$input, (float)$operand);
}
/**
* Replace each newline (\n) with html break
*
* @param string $input
*
* @return string
*/
public static function newline_to_br($input)
{
return is_string($input) ? str_replace("\n", "<br />\n", $input) : $input;
}
/**
* addition
*
* @param float $input
* @param float $operand
*
* @return float
*/
public static function plus($input, $operand)
{
return (float)$input + (float)$operand;
}
/**
* Prepend a string to another
*
* @param string $input
* @param string $string
*
* @return string
*/
public static function prepend($input, $string)
{
return $string . $input;
}
/**
* Remove a substring
*
* @param string $input
* @param string $string
*
* @return string
*/
public static function remove($input, $string)
{
return str_replace($string, '', $input);
}
/**
* Remove the first occurrences of a substring
*
* @param string $input
* @param string $string
*
* @return string
*/
public static function remove_first($input, $string)
{
if (($pos = strpos($input, $string)) !== false) {
$input = substr_replace($input, '', $pos, strlen($string));
}
return $input;
}
/**
* Replace occurrences of a string with another
*
* @param string $input
* @param string $string
* @param string $replacement
*
* @return string
*/
public static function replace($input, $string, $replacement = '')
{
return str_replace($string, $replacement, $input);
}
/**
* Replace the first occurrences of a string with another
*
* @param string $input
* @param string $string
* @param string $replacement
*
* @return string
*/
public static function replace_first($input, $string, $replacement = '')
{
if (($pos = strpos($input, $string)) !== false) {
$input = substr_replace($input, $replacement, $pos, strlen($string));
}
return $input;
}
/**
* Reverse the elements of an array
*
* @param array|\Traversable $input
*
* @return array
*/
public static function reverse($input)
{
if ($input instanceof \Traversable) {
$input = iterator_to_array($input);
}
return array_reverse($input);
}
/**
* Round a number
*
* @param float $input
* @param int $n precision
*
* @return float
*/
public static function round($input, $n = 0)
{
return round((float)$input, (int)$n);
}
/**
* @param string $input
*
* @return string
*/
public static function rstrip($input)
{
return rtrim($input);
}
/**
* Return the size of an array or of an string
*
* @param mixed $input
* @throws RenderException
* @return int
*/
public static function size($input)
{
if ($input instanceof \Iterator) {
return iterator_count($input);
}
if (is_array($input)) {
return count($input);
}
if (is_object($input)) {
if (method_exists($input, 'size')) {
return $input->size();
}
if (!method_exists($input, '__toString')) {
$class = get_class($input);
throw new RenderException("Size of $class cannot be estimated: it has no method 'size' nor can be converted to a string");
}
}
// only plain values and stringable objects left at this point
return strlen($input);
}
/**
* @param array|\Iterator|string $input
* @param int $offset
* @param int $length
*
* @return array|\Iterator|string
*/
public static function slice($input, $offset, $length = null)
{
if ($input instanceof \Iterator) {
$input = iterator_to_array($input);
}
if (is_array($input)) {
$input = array_slice($input, $offset, $length);
} elseif (is_string($input)) {
$input = mb_substr($input, $offset, $length);
}
return $input;
}
/**
* Sort the elements of an array
*
* @param array|\Traversable $input
* @param string $property use this property of an array element
*
* @return array
*/
public static function sort($input, $property = null)
{
if ($input instanceof \Traversable) {
$input = iterator_to_array($input);
}
if ($property === null) {
asort($input);
} else {
$first = reset($input);
if ($first !== false && is_array($first) && array_key_exists($property, $first)) {
uasort($input, function ($a, $b) use ($property) {
if ($a[$property] == $b[$property]) {
return 0;
}
return $a[$property] < $b[$property] ? -1 : 1;
});
}
}
return $input;
}
/**
* Explicit string conversion.
*
* @param mixed $input
*
* @return string
*/
public static function string($input)
{
return strval($input);
}
/**
* Split input string into an array of substrings separated by given pattern.
*
* @param string $input
* @param string $pattern
*
* @return array
*/
public static function split($input, $pattern)
{
if ($input === '' || $input === null) {
return [];
}
return explode($pattern, $input);
}
/**
* @param string $input
*
* @return string
*/
public static function strip($input)
{
return trim($input);
}
/**
* Removes html tags from text
*
* @param string $input
*
* @return string
*/
public static function strip_html($input)
{
return is_string($input) ? strip_tags($input) : $input;
}
/**
* Strip all newlines (\n, \r) from string
*
* @param string $input
*
* @return string
*/
public static function strip_newlines($input)
{
return is_string($input) ? str_replace(array(
"\n", "\r"
), '', $input) : $input;
}
/**
* multiplication
*
* @param float $input
* @param float $operand
*
* @return float
*/
public static function times($input, $operand)
{
return (float)$input * (float)$operand;
}
/**
* Truncate a string down to x characters
*
* @param string $input
* @param int $characters
* @param string $ending string to append if truncated
*
* @return string
*/
public static function truncate($input, $characters = 100, $ending = '...')
{
if (is_string($input) || is_numeric($input)) {
if (strlen($input) > $characters) {
return mb_substr($input, 0, $characters) . $ending;
}
}
return $input;
}
/**
* Truncate string down to x words
*
* @param string $input
* @param int $words
* @param string $ending string to append if truncated
*
* @return string
*/
public static function truncatewords($input, $words = 3, $ending = '...')
{
if (is_string($input)) {
$wordlist = explode(" ", $input);
if (count($wordlist) > $words) {
return implode(" ", array_slice($wordlist, 0, $words)) . $ending;
}
}
return $input;
}
/**
* Remove duplicate elements from an array
*
* @param array|\Traversable $input
*
* @return array
*/
public static function uniq($input)
{
if ($input instanceof \Traversable) {
$input = iterator_to_array($input);
}
return array_unique($input);
}
/**
* Convert an input to uppercase
*
* @param string $input
*
* @return string
*/
public static function upcase($input)
{
return is_string($input) ? mb_strtoupper($input) : $input;
}
/**
* URL encodes a string
*
* @param string $input
*
* @return string
*/
public static function url_encode($input)
{
return urlencode($input);
}
/**
* Decodes a URL-encoded string
*
* @param string $input
*
* @return string
*/
public static function url_decode($input)
{
return urldecode($input);
}
}

View File

@@ -0,0 +1,76 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Tag;
use Liquid\AbstractTag;
use Liquid\Exception\ParseException;
use Liquid\Liquid;
use Liquid\FileSystem;
use Liquid\Regexp;
use Liquid\Context;
use Liquid\Variable;
/**
* Performs an assignment of one variable to another
*
* Example:
*
* {% assign var = var %}
* {% assign var = "hello" | upcase %}
*/
class TagAssign extends AbstractTag
{
/**
* @var string The variable to assign from
*/
private $from;
/**
* @var string The variable to assign to
*/
private $to;
/**
* Constructor
*
* @param string $markup
* @param array $tokens
* @param FileSystem $fileSystem
*
* @throws \Liquid\Exception\ParseException
*/
public function __construct($markup, array &$tokens, FileSystem $fileSystem = null)
{
$syntaxRegexp = new Regexp('/(\w+)\s*=\s*(.*)\s*/');
if ($syntaxRegexp->match($markup)) {
$this->to = $syntaxRegexp->matches[1];
$this->from = new Variable($syntaxRegexp->matches[2]);
} else {
throw new ParseException("Syntax Error in 'assign' - Valid syntax: assign [var] = [source]");
}
}
/**
* Renders the tag
*
* @param Context $context
*
* @return string|void
*/
public function render(Context $context)
{
$output = $this->from->render($context);
$context->set($this->to, $output, true);
}
}

View File

@@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Tag;
use Liquid\AbstractBlock;
use Liquid\Exception\ParseException;
use Liquid\FileSystem;
use Liquid\Regexp;
/**
* Marks a section of a template as being reusable.
*
* Example:
*
* {% block foo %} bar {% endblock %}
*/
class TagBlock extends AbstractBlock
{
/**
* The variable to assign to
*
* @var string
*/
private $block;
/**
* Constructor
*
* @param string $markup
* @param array $tokens
* @param FileSystem $fileSystem
*
* @throws \Liquid\Exception\ParseException
* @return \Liquid\Tag\TagBlock
*/
public function __construct($markup, array &$tokens, FileSystem $fileSystem = null)
{
$syntaxRegexp = new Regexp('/(\w+)/');
if ($syntaxRegexp->match($markup)) {
$this->block = $syntaxRegexp->matches[1];
parent::__construct($markup, $tokens, $fileSystem);
} else {
throw new ParseException("Syntax Error in 'block' - Valid syntax: block [name]");
}
}
}

View File

@@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Tag;
use Liquid\AbstractTag;
use Liquid\Context;
/**
* Break iteration of the current loop
*
* Example:
*
* {% for i in (1..5) %}
* {% if i == 4 %}
* {% break %}
* {% endif %}
* {{ i }}
* {% endfor %}
*/
class TagBreak extends AbstractTag
{
/**
* Renders the tag
*
* @param Context $context
*
* @return string|void
*/
public function render(Context $context)
{
$context->registers['break'] = true;
}
}

View File

@@ -0,0 +1,71 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Tag;
use Liquid\AbstractBlock;
use Liquid\Context;
use Liquid\Exception\ParseException;
use Liquid\FileSystem;
use Liquid\Regexp;
/**
* Captures the output inside a block and assigns it to a variable
*
* Example:
*
* {% capture foo %} bar {% endcapture %}
*/
class TagCapture extends AbstractBlock
{
/**
* The variable to assign to
*
* @var string
*/
private $to;
/**
* Constructor
*
* @param string $markup
* @param array $tokens
* @param FileSystem $fileSystem
*
* @throws \Liquid\Exception\ParseException
*/
public function __construct($markup, array &$tokens, FileSystem $fileSystem = null)
{
$syntaxRegexp = new Regexp('/(\w+)/');
if ($syntaxRegexp->match($markup)) {
$this->to = $syntaxRegexp->matches[1];
parent::__construct($markup, $tokens, $fileSystem);
} else {
throw new ParseException("Syntax Error in 'capture' - Valid syntax: capture [var] [value]");
}
}
/**
* Renders the block
*
* @param Context $context
*
* @return string
*/
public function render(Context $context)
{
$output = parent::render($context);
$context->set($this->to, $output, true);
return '';
}
}

View File

@@ -0,0 +1,171 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Tag;
use Liquid\Decision;
use Liquid\Context;
use Liquid\Exception\ParseException;
use Liquid\Liquid;
use Liquid\FileSystem;
use Liquid\Regexp;
/**
* A switch statement
*
* Example:
*
* {% case condition %}{% when foo %} foo {% else %} bar {% endcase %}
*/
class TagCase extends Decision
{
/**
* Stack of nodelists
*
* @var array
*/
public $nodelists;
/**
* The nodelist for the else (default) nodelist
*
* @var array
*/
public $elseNodelist;
/**
* The left value to compare
*
* @var string
*/
public $left;
/**
* The current right value to compare
*
* @var mixed
*/
public $right;
/**
* Constructor
*
* @param string $markup
* @param array $tokens
* @param FileSystem $fileSystem
*
* @throws \Liquid\Exception\ParseException
*/
public function __construct($markup, array &$tokens, FileSystem $fileSystem = null)
{
$this->nodelists = array();
$this->elseNodelist = array();
parent::__construct($markup, $tokens, $fileSystem);
$syntaxRegexp = new Regexp('/' . Liquid::get('QUOTED_FRAGMENT') . '/');
if ($syntaxRegexp->match($markup)) {
$this->left = $syntaxRegexp->matches[0];
} else {
throw new ParseException("Syntax Error in tag 'case' - Valid syntax: case [condition]"); // harry
}
}
/**
* Pushes the last nodelist onto the stack
*/
public function endTag()
{
$this->pushNodelist();
}
/**
* Unknown tag handler
*
* @param string $tag
* @param string $params
* @param array $tokens
*
* @throws \Liquid\Exception\ParseException
*/
public function unknownTag($tag, $params, array $tokens)
{
$whenSyntaxRegexp = new Regexp('/' . Liquid::get('QUOTED_FRAGMENT') . '/');
switch ($tag) {
case 'when':
// push the current nodelist onto the stack and prepare for a new one
if ($whenSyntaxRegexp->match($params)) {
$this->pushNodelist();
$this->right = $whenSyntaxRegexp->matches[0];
$this->nodelist = array();
} else {
throw new ParseException("Syntax Error in tag 'case' - Valid when condition: when [condition]"); // harry
}
break;
case 'else':
// push the last nodelist onto the stack and prepare to receive the else nodes
$this->pushNodelist();
$this->right = null;
$this->elseNodelist = &$this->nodelist;
$this->nodelist = array();
break;
default:
parent::unknownTag($tag, $params, $tokens);
}
}
/**
* Pushes the current right value and nodelist into the nodelist stack
*/
public function pushNodelist()
{
if (!is_null($this->right)) {
$this->nodelists[] = array($this->right, $this->nodelist);
}
}
/**
* Renders the node
*
* @param Context $context
*
* @return string
*/
public function render(Context $context)
{
$output = ''; // array();
$runElseBlock = true;
foreach ($this->nodelists as $data) {
list($right, $nodelist) = $data;
if ($this->equalVariables($this->left, $right, $context)) {
$runElseBlock = false;
$context->push();
$output .= $this->renderAll($nodelist, $context);
$context->pop();
}
}
if ($runElseBlock) {
$context->push();
$output .= $this->renderAll($this->elseNodelist, $context);
$context->pop();
}
return $output;
}
}

View File

@@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Tag;
use Liquid\AbstractBlock;
use Liquid\Context;
/**
* Creates a comment; everything inside will be ignored
*
* Example:
*
* {% comment %} This will be ignored {% endcomment %}
*/
class TagComment extends AbstractBlock
{
/**
* Renders the block
*
* @param Context $context
*
* @return string empty string
*/
public function render(Context $context)
{
return '';
}
}

View File

@@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Tag;
use Liquid\AbstractTag;
use Liquid\Context;
/**
* Skips the current iteration of the current loop
*
* Example:
*
* {% for i in (1..5) %}
* {% if i == 4 %}
* {% continue %}
* {% endif %}
* {{ i }}
* {% endfor %}
*/
class TagContinue extends AbstractTag
{
/**
* Renders the tag
*
* @param Context $context
*
* @return string|void
*/
public function render(Context $context)
{
$context->registers['continue'] = true;
}
}

View File

@@ -0,0 +1,130 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Tag;
use Liquid\AbstractTag;
use Liquid\Exception\ParseException;
use Liquid\Liquid;
use Liquid\Context;
use Liquid\Regexp;
use Liquid\Variable;
use Liquid\FileSystem;
/**
* Cycles between a list of values; calls to the tag will return each value in turn
*
* Example:
* {%cycle "one", "two"%} {%cycle "one", "two"%} {%cycle "one", "two"%}
*
* this will return:
* one two one
*
* Cycles can also be named, to differentiate between multiple cycle with the same values:
* {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %}
*
* will return
* one one two two
*/
class TagCycle extends AbstractTag
{
/**
* @var string The name of the cycle; if none is given one is created using the value list
*/
private $name;
/**
* @var Variable[] The variables to cycle between
*/
private $variables = array();
/**
* Constructor
*
* @param string $markup
* @param array $tokens
* @param FileSystem $fileSystem
*
* @throws \Liquid\Exception\ParseException
*/
public function __construct($markup, array &$tokens, FileSystem $fileSystem = null)
{
$simpleSyntax = new Regexp("/" . Liquid::get('QUOTED_FRAGMENT') . "/");
$namedSyntax = new Regexp("/(" . Liquid::get('QUOTED_FRAGMENT') . ")\s*\:\s*(.*)/");
if ($namedSyntax->match($markup)) {
$this->variables = $this->variablesFromString($namedSyntax->matches[2]);
$this->name = $namedSyntax->matches[1];
} elseif ($simpleSyntax->match($markup)) {
$this->variables = $this->variablesFromString($markup);
$this->name = "'" . implode($this->variables) . "'";
} else {
throw new ParseException("Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]");
}
}
/**
* Renders the tag
*
* @var Context $context
* @return string
*/
public function render(Context $context)
{
$context->push();
$key = $context->get($this->name);
if (isset($context->registers['cycle'][$key])) {
$iteration = $context->registers['cycle'][$key];
} else {
$iteration = 0;
}
$result = $context->get($this->variables[$iteration]);
$iteration += 1;
if ($iteration >= count($this->variables)) {
$iteration = 0;
}
$context->registers['cycle'][$key] = $iteration;
$context->pop();
return $result;
}
/**
* Extract variables from a string of markup
*
* @param string $markup
*
* @return array;
*/
private function variablesFromString($markup)
{
$regexp = new Regexp('/\s*(' . Liquid::get('QUOTED_FRAGMENT') . ')\s*/');
$parts = explode(',', $markup);
$result = array();
foreach ($parts as $part) {
$regexp->match($part);
if (!empty($regexp->matches[1])) {
$result[] = $regexp->matches[1];
}
}
return $result;
}
}

View File

@@ -0,0 +1,83 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Tag;
use Liquid\AbstractTag;
use Liquid\Exception\ParseException;
use Liquid\Liquid;
use Liquid\Context;
use Liquid\FileSystem;
use Liquid\Regexp;
/**
* Used to decrement a counter into a template
*
* Example:
*
* {% decrement value %}
*
* @author Viorel Dram
*/
class TagDecrement extends AbstractTag
{
/**
* Name of the variable to decrement
*
* @var int
*/
private $toDecrement;
/**
* Constructor
*
* @param string $markup
* @param array $tokens
* @param FileSystem $fileSystem
*
* @throws \Liquid\Exception\ParseException
*/
public function __construct($markup, array &$tokens, FileSystem $fileSystem = null)
{
$syntax = new Regexp('/(' . Liquid::get('VARIABLE_NAME') . ')/');
if ($syntax->match($markup)) {
$this->toDecrement = $syntax->matches[0];
} else {
throw new ParseException("Syntax Error in 'decrement' - Valid syntax: decrement [var]");
}
}
/**
* Renders the tag
*
* @param Context $context
*
* @return string|void
*/
public function render(Context $context)
{
// if the value is not set in the environment check to see if it
// exists in the context, and if not set it to 0
if (!isset($context->environments[0][$this->toDecrement])) {
// check for a context value
$fromContext = $context->get($this->toDecrement);
// we already have a value in the context
$context->environments[0][$this->toDecrement] = (null !== $fromContext) ? $fromContext : 0;
}
// decrement the environment value
$context->environments[0][$this->toDecrement]--;
return '';
}
}

View File

@@ -0,0 +1,214 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Tag;
use Liquid\AbstractTag;
use Liquid\Document;
use Liquid\Exception\MissingFilesystemException;
use Liquid\Exception\ParseException;
use Liquid\Liquid;
use Liquid\Context;
use Liquid\FileSystem;
use Liquid\Regexp;
use Liquid\Template;
/**
* Extends a template by another one.
*
* Example:
*
* {% extends "base" %}
*/
class TagExtends extends AbstractTag
{
/**
* @var string The name of the template
*/
private $templateName;
/**
* @var Document The Document that represents the included template
*/
private $document;
/**
* @var string The Source Hash
*/
protected $hash;
/**
* Constructor
*
* @param string $markup
* @param array $tokens
* @param FileSystem $fileSystem
*
* @throws \Liquid\Exception\ParseException
*/
public function __construct($markup, array &$tokens, FileSystem $fileSystem = null)
{
$regex = new Regexp('/("[^"]+"|\'[^\']+\')?/');
if ($regex->match($markup) && isset($regex->matches[1])) {
$this->templateName = substr($regex->matches[1], 1, strlen($regex->matches[1]) - 2);
} else {
throw new ParseException("Error in tag 'extends' - Valid syntax: extends '[template name]'");
}
parent::__construct($markup, $tokens, $fileSystem);
}
/**
* @param array $tokens
*
* @return array
*/
private function findBlocks(array $tokens)
{
$blockstartRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*block (\w+)\s*(.*)?' . Liquid::get('TAG_END') . '$/');
$blockendRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*endblock\s*?' . Liquid::get('TAG_END') . '$/');
$b = array();
$name = null;
foreach ($tokens as $token) {
if ($blockstartRegexp->match($token)) {
$name = $blockstartRegexp->matches[1];
$b[$name] = array();
} elseif ($blockendRegexp->match($token)) {
$name = null;
} else {
if ($name !== null) {
array_push($b[$name], $token);
}
}
}
return $b;
}
/**
* Parses the tokens
*
* @param array $tokens
*
* @throws \Liquid\Exception\MissingFilesystemException
*/
public function parse(array &$tokens)
{
if ($this->fileSystem === null) {
throw new MissingFilesystemException("No file system");
}
// read the source of the template and create a new sub document
$source = $this->fileSystem->readTemplateFile($this->templateName);
// tokens in this new document
$maintokens = Template::tokenize($source);
$eRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*extends (.*)?' . Liquid::get('TAG_END') . '$/');
foreach ($maintokens as $maintoken) {
if ($eRegexp->match($maintoken)) {
$m = $eRegexp->matches[1];
break;
}
}
if (isset($m)) {
$rest = array_merge($maintokens, $tokens);
} else {
$childtokens = $this->findBlocks($tokens);
$blockstartRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*block (\w+)\s*(.*)?' . Liquid::get('TAG_END') . '$/');
$blockendRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*endblock\s*?' . Liquid::get('TAG_END') . '$/');
$name = null;
$rest = array();
$keep = false;
for ($i = 0; $i < count($maintokens); $i++) {
if ($blockstartRegexp->match($maintokens[$i])) {
$name = $blockstartRegexp->matches[1];
if (isset($childtokens[$name])) {
$keep = true;
array_push($rest, $maintokens[$i]);
foreach ($childtokens[$name] as $item) {
array_push($rest, $item);
}
}
}
if (!$keep) {
array_push($rest, $maintokens[$i]);
}
if ($blockendRegexp->match($maintokens[$i]) && $keep === true) {
$keep = false;
array_push($rest, $maintokens[$i]);
}
}
}
$cache = Template::getCache();
if (!$cache) {
$this->document = new Document($rest, $this->fileSystem);
return;
}
$this->hash = md5($source);
$this->document = $cache->read($this->hash);
if ($this->document == false || $this->document->hasIncludes() == true) {
$this->document = new Document($rest, $this->fileSystem);
$cache->write($this->hash, $this->document);
}
}
/**
* Check for cached includes; if there are - do not use cache
*
* @see Document::hasIncludes()
* @return boolean
*/
public function hasIncludes()
{
if ($this->document->hasIncludes() == true) {
return true;
}
$source = $this->fileSystem->readTemplateFile($this->templateName);
if (Template::getCache()->exists(md5($source)) && $this->hash === md5($source)) {
return false;
}
return true;
}
/**
* Renders the node
*
* @param Context $context
*
* @return string
*/
public function render(Context $context)
{
$context->push();
$result = $this->document->render($context);
$context->pop();
return $result;
}
}

View File

@@ -0,0 +1,236 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Tag;
use Liquid\AbstractBlock;
use Liquid\Exception\ParseException;
use Liquid\Liquid;
use Liquid\Context;
use Liquid\FileSystem;
use Liquid\Regexp;
/**
* Loops over an array, assigning the current value to a given variable
*
* Example:
*
* {%for item in array%} {{item}} {%endfor%}
*
* With an array of 1, 2, 3, 4, will return 1 2 3 4
*
* or
*
* {%for i in (1..10)%} {{i}} {%endfor%}
* {%for i in (1..variable)%} {{i}} {%endfor%}
*
*/
class TagFor extends AbstractBlock
{
/**
* @var array The collection to loop over
*/
private $collectionName;
/**
* @var string The variable name to assign collection elements to
*/
private $variableName;
/**
* @var string The name of the loop, which is a compound of the collection and variable names
*/
private $name;
/**
* @var string The type of the loop (collection or digit)
*/
private $type = 'collection';
/**
* Constructor
*
* @param string $markup
* @param array $tokens
* @param FileSystem $fileSystem
*
* @throws \Liquid\Exception\ParseException
*/
public function __construct($markup, array &$tokens, FileSystem $fileSystem = null)
{
parent::__construct($markup, $tokens, $fileSystem);
$syntaxRegexp = new Regexp('/(\w+)\s+in\s+(' . Liquid::get('VARIABLE_NAME') . ')/');
if ($syntaxRegexp->match($markup)) {
$this->variableName = $syntaxRegexp->matches[1];
$this->collectionName = $syntaxRegexp->matches[2];
$this->name = $syntaxRegexp->matches[1] . '-' . $syntaxRegexp->matches[2];
$this->extractAttributes($markup);
} else {
$syntaxRegexp = new Regexp('/(\w+)\s+in\s+\((\d+|' . Liquid::get('VARIABLE_NAME') . ')\s*\.\.\s*(\d+|' . Liquid::get('VARIABLE_NAME') . ')\)/');
if ($syntaxRegexp->match($markup)) {
$this->type = 'digit';
$this->variableName = $syntaxRegexp->matches[1];
$this->start = $syntaxRegexp->matches[2];
$this->collectionName = $syntaxRegexp->matches[3];
$this->name = $syntaxRegexp->matches[1].'-digit';
$this->extractAttributes($markup);
} else {
throw new ParseException("Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]");
}
}
}
/**
* Renders the tag
*
* @param Context $context
*
* @return null|string
*/
public function render(Context $context)
{
if (!isset($context->registers['for'])) {
$context->registers['for'] = array();
}
if ($this->type == 'digit') {
return $this->renderDigit($context);
}
// that's the default
return $this->renderCollection($context);
}
private function renderCollection(Context $context)
{
$collection = $context->get($this->collectionName);
if ($collection instanceof \Generator && !$collection->valid()) {
return '';
}
if ($collection instanceof \Traversable) {
$collection = iterator_to_array($collection);
}
if (is_null($collection) || !is_array($collection) || count($collection) == 0) {
return '';
}
$range = array(0, count($collection));
if (isset($this->attributes['limit']) || isset($this->attributes['offset'])) {
$offset = 0;
if (isset($this->attributes['offset'])) {
$offset = ($this->attributes['offset'] == 'continue') ? $context->registers['for'][$this->name] : $context->get($this->attributes['offset']);
}
$limit = (isset($this->attributes['limit'])) ? $context->get($this->attributes['limit']) : null;
$rangeEnd = $limit ? $limit : count($collection) - $offset;
$range = array($offset, $rangeEnd);
$context->registers['for'][$this->name] = $rangeEnd + $offset;
}
$result = '';
$segment = array_slice($collection, $range[0], $range[1]);
if (!count($segment)) {
return null;
}
$context->push();
$length = count($segment);
$index = 0;
foreach ($segment as $key => $item) {
$value = is_numeric($key) ? $item : array($key, $item);
$context->set($this->variableName, $value);
$context->set('forloop', array(
'name' => $this->name,
'length' => $length,
'index' => $index + 1,
'index0' => $index,
'rindex' => $length - $index,
'rindex0' => $length - $index - 1,
'first' => (int)($index == 0),
'last' => (int)($index == $length - 1)
));
$result .= $this->renderAll($this->nodelist, $context);
$index++;
if (isset($context->registers['break'])) {
unset($context->registers['break']);
break;
}
if (isset($context->registers['continue'])) {
unset($context->registers['continue']);
}
}
$context->pop();
return $result;
}
private function renderDigit(Context $context)
{
$start = $this->start;
if (!is_integer($this->start)) {
$start = $context->get($this->start);
}
$end = $this->collectionName;
if (!is_integer($this->collectionName)) {
$end = $context->get($this->collectionName);
}
$range = array($start, $end);
$context->push();
$result = '';
$index = 0;
$length = $range[1] - $range[0];
for ($i = $range[0]; $i <= $range[1]; $i++) {
$context->set($this->variableName, $i);
$context->set('forloop', array(
'name' => $this->name,
'length' => $length,
'index' => $index + 1,
'index0' => $index,
'rindex' => $length - $index,
'rindex0' => $length - $index - 1,
'first' => (int)($index == 0),
'last' => (int)($index == $length - 1)
));
$result .= $this->renderAll($this->nodelist, $context);
$index++;
if (isset($context->registers['break'])) {
unset($context->registers['break']);
break;
}
if (isset($context->registers['continue'])) {
unset($context->registers['continue']);
}
}
$context->pop();
return $result;
}
}

View File

@@ -0,0 +1,168 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Tag;
use Liquid\Decision;
use Liquid\Context;
use Liquid\Exception\ParseException;
use Liquid\Liquid;
use Liquid\FileSystem;
use Liquid\Regexp;
/**
* An if statement
*
* Example:
*
* {% if true %} YES {% else %} NO {% endif %}
*
* will return:
* YES
*/
class TagIf extends Decision
{
/**
* Array holding the nodes to render for each logical block
*
* @var array
*/
private $nodelistHolders = array();
/**
* Array holding the block type, block markup (conditions) and block nodelist
*
* @var array
*/
protected $blocks = array();
/**
* Constructor
*
* @param string $markup
* @param array $tokens
* @param FileSystem $fileSystem
*/
public function __construct($markup, array &$tokens, FileSystem $fileSystem = null)
{
$this->nodelist = & $this->nodelistHolders[count($this->blocks)];
array_push($this->blocks, array('if', $markup, &$this->nodelist));
parent::__construct($markup, $tokens, $fileSystem);
}
/**
* Handler for unknown tags, handle else tags
*
* @param string $tag
* @param array $params
* @param array $tokens
*/
public function unknownTag($tag, $params, array $tokens)
{
if ($tag == 'else' || $tag == 'elsif') {
// Update reference to nodelistHolder for this block
$this->nodelist = & $this->nodelistHolders[count($this->blocks) + 1];
$this->nodelistHolders[count($this->blocks) + 1] = array();
array_push($this->blocks, array($tag, $params, &$this->nodelist));
} else {
parent::unknownTag($tag, $params, $tokens);
}
}
/**
* Render the tag
*
* @param Context $context
*
* @throws \Liquid\Exception\ParseException
* @return string
*/
public function render(Context $context)
{
$context->push();
$logicalRegex = new Regexp('/\s+(and|or)\s+/');
$conditionalRegex = new Regexp('/(' . Liquid::get('QUOTED_FRAGMENT') . ')\s*([=!<>a-z_]+)?\s*(' . Liquid::get('QUOTED_FRAGMENT') . ')?/');
$result = '';
foreach ($this->blocks as $block) {
if ($block[0] == 'else') {
$result = $this->renderAll($block[2], $context);
break;
}
if ($block[0] == 'if' || $block[0] == 'elsif') {
// Extract logical operators
$logicalRegex->matchAll($block[1]);
$logicalOperators = $logicalRegex->matches;
$logicalOperators = $logicalOperators[1];
// Extract individual conditions
$temp = $logicalRegex->split($block[1]);
$conditions = array();
foreach ($temp as $condition) {
if ($conditionalRegex->match($condition)) {
$left = (isset($conditionalRegex->matches[1])) ? $conditionalRegex->matches[1] : null;
$operator = (isset($conditionalRegex->matches[2])) ? $conditionalRegex->matches[2] : null;
$right = (isset($conditionalRegex->matches[3])) ? $conditionalRegex->matches[3] : null;
array_push($conditions, array(
'left' => $left,
'operator' => $operator,
'right' => $right
));
} else {
throw new ParseException("Syntax Error in tag 'if' - Valid syntax: if [condition]");
}
}
if (count($logicalOperators)) {
// If statement contains and/or
$display = $this->interpretCondition($conditions[0]['left'], $conditions[0]['right'], $conditions[0]['operator'], $context);
foreach ($logicalOperators as $k => $logicalOperator) {
if ($logicalOperator == 'and') {
$display = ($display && $this->interpretCondition($conditions[$k + 1]['left'], $conditions[$k + 1]['right'], $conditions[$k + 1]['operator'], $context));
} else {
$display = ($display || $this->interpretCondition($conditions[$k + 1]['left'], $conditions[$k + 1]['right'], $conditions[$k + 1]['operator'], $context));
}
}
} else {
// If statement is a single condition
$display = $this->interpretCondition($conditions[0]['left'], $conditions[0]['right'], $conditions[0]['operator'], $context);
}
// hook for unless tag
$display = $this->negateIfUnless($display);
if ($display) {
$result = $this->renderAll($block[2], $context);
break;
}
}
}
$context->pop();
return $result;
}
protected function negateIfUnless($display)
{
// no need to negate a condition in a regular `if` tag (will do that in `unless` tag)
return $display;
}
}

View File

@@ -0,0 +1,61 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Tag;
use Liquid\AbstractBlock;
use Liquid\Context;
use Liquid\FileSystem;
/**
* Quickly create a table from a collection
*/
class TagIfchanged extends AbstractBlock
{
/**
* The last value
*
* @var string
*/
private $lastValue = '';
/**
* Constructor
*
* @param string $markup
* @param array $tokens
* @param FileSystem $fileSystem
*
* @throws \Liquid\LiquidException
*/
public function __construct($markup, array &$tokens, FileSystem $fileSystem = null)
{
parent::__construct($markup, $tokens, $fileSystem);
}
/**
* Renders the block
*
* @param Context $context
*
* @return string
*/
public function render(Context $context)
{
$output = parent::render($context);
if ($this->lastValue == $output) {
return '';
}
$this->lastValue = $output;
return $this->lastValue;
}
}

View File

@@ -0,0 +1,200 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Tag;
use Liquid\AbstractTag;
use Liquid\Document;
use Liquid\Context;
use Liquid\Exception\MissingFilesystemException;
use Liquid\Exception\ParseException;
use Liquid\Liquid;
use Liquid\LiquidException;
use Liquid\FileSystem;
use Liquid\Regexp;
use Liquid\Template;
/**
* Includes another, partial, template
*
* Example:
*
* {% include 'foo' %}
*
* Will include the template called 'foo'
*
* {% include 'foo' with 'bar' %}
*
* Will include the template called 'foo', with a variable called foo that will have the value of 'bar'
*
* {% include 'foo' for 'bar' %}
*
* Will loop over all the values of bar, including the template foo, passing a variable called foo
* with each value of bar
*/
class TagInclude extends AbstractTag
{
/**
* @var string The name of the template
*/
private $templateName;
/**
* @var bool True if the variable is a collection
*/
private $collection;
/**
* @var mixed The value to pass to the child template as the template name
*/
private $variable;
/**
* @var Document The Document that represents the included template
*/
private $document;
/**
* @var string The Source Hash
*/
protected $hash;
/**
* Constructor
*
* @param string $markup
* @param array $tokens
* @param FileSystem $fileSystem
*
* @throws \Liquid\Exception\ParseException
*/
public function __construct($markup, array &$tokens, FileSystem $fileSystem = null)
{
$regex = new Regexp('/("[^"]+"|\'[^\']+\'|[^\'"\s]+)(\s+(with|for)\s+(' . Liquid::get('QUOTED_FRAGMENT') . '+))?/');
if (!$regex->match($markup)) {
throw new ParseException("Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]");
}
$unquoted = (strpos($regex->matches[1], '"') === false && strpos($regex->matches[1], "'") === false);
$start = 1;
$len = strlen($regex->matches[1]) - 2;
if ($unquoted) {
$start = 0;
$len = strlen($regex->matches[1]);
}
$this->templateName = substr($regex->matches[1], $start, $len);
if (isset($regex->matches[1])) {
$this->collection = (isset($regex->matches[3])) ? ($regex->matches[3] == "for") : null;
$this->variable = (isset($regex->matches[4])) ? $regex->matches[4] : null;
}
$this->extractAttributes($markup);
parent::__construct($markup, $tokens, $fileSystem);
}
/**
* Parses the tokens
*
* @param array $tokens
*
* @throws \Liquid\Exception\MissingFilesystemException
*/
public function parse(array &$tokens)
{
if ($this->fileSystem === null) {
throw new MissingFilesystemException("No file system");
}
// read the source of the template and create a new sub document
$source = $this->fileSystem->readTemplateFile($this->templateName);
$cache = Template::getCache();
if (!$cache) {
// tokens in this new document
$templateTokens = Template::tokenize($source);
$this->document = new Document($templateTokens, $this->fileSystem);
return;
}
$this->hash = md5($source);
$this->document = $cache->read($this->hash);
if ($this->document == false || $this->document->hasIncludes() == true) {
$templateTokens = Template::tokenize($source);
$this->document = new Document($templateTokens, $this->fileSystem);
$cache->write($this->hash, $this->document);
}
}
/**
* Check for cached includes; if there are - do not use cache
*
* @see Document::hasIncludes()
* @return boolean
*/
public function hasIncludes()
{
if ($this->document->hasIncludes() == true) {
return true;
}
$source = $this->fileSystem->readTemplateFile($this->templateName);
if (Template::getCache()->exists(md5($source)) && $this->hash === md5($source)) {
return false;
}
return true;
}
/**
* Renders the node
*
* @param Context $context
*
* @return string
*/
public function render(Context $context)
{
$result = '';
$variable = $context->get($this->variable);
$context->push();
foreach ($this->attributes as $key => $value) {
$context->set($key, $context->get($value));
}
if ($this->collection) {
foreach ($variable as $item) {
$context->set($this->templateName, $item);
$result .= $this->document->render($context);
}
} else {
if (!is_null($this->variable)) {
$context->set($this->templateName, $variable);
}
$result .= $this->document->render($context);
}
$context->pop();
return $result;
}
}

View File

@@ -0,0 +1,83 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Tag;
use Liquid\AbstractTag;
use Liquid\Exception\ParseException;
use Liquid\Liquid;
use Liquid\Context;
use Liquid\FileSystem;
use Liquid\Regexp;
/**
* Used to increment a counter into a template
*
* Example:
*
* {% increment value %}
*
* @author Viorel Dram
*/
class TagIncrement extends AbstractTag
{
/**
* Name of the variable to increment
*
* @var string
*/
private $toIncrement;
/**
* Constructor
*
* @param string $markup
* @param array $tokens
* @param FileSystem $fileSystem
*
* @throws \Liquid\Exception\ParseException
*/
public function __construct($markup, array &$tokens, FileSystem $fileSystem = null)
{
$syntax = new Regexp('/(' . Liquid::get('VARIABLE_NAME') . ')/');
if ($syntax->match($markup)) {
$this->toIncrement = $syntax->matches[0];
} else {
throw new ParseException("Syntax Error in 'increment' - Valid syntax: increment [var]");
}
}
/**
* Renders the tag
*
* @param Context $context
*
* @return string|void
*/
public function render(Context $context)
{
// If the value is not set in the environment check to see if it
// exists in the context, and if not set it to -1
if (!isset($context->environments[0][$this->toIncrement])) {
// check for a context value
$from_context = $context->get($this->toIncrement);
// we already have a value in the context
$context->environments[0][$this->toIncrement] = (null !== $from_context) ? $from_context : -1;
}
// Increment the value
$context->environments[0][$this->toIncrement]++;
return '';
}
}

View File

@@ -0,0 +1,206 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Tag;
use Liquid\AbstractBlock;
use Liquid\Exception\ParseException;
use Liquid\Liquid;
use Liquid\Context;
use Liquid\FileSystem;
use Liquid\Regexp;
use Liquid\Exception\RenderException;
/**
* The paginate tag works in conjunction with the for tag to split content into numerous pages.
*
* Example:
*
* {% paginate collection.products by 5 %}
* {% for product in collection.products %}
* <!--show product details here -->
* {% endfor %}
* {% endpaginate %}
*
*/
class TagPaginate extends AbstractBlock
{
/**
* @var array The collection to paginate
*/
private $collectionName;
/**
* @var array The collection object
*/
private $collection;
/**
* @var int The size of the collection
*/
private $collectionSize;
/**
* @var int The number of items to paginate by
*/
private $numberItems;
/**
* @var int The current page
*/
private $currentPage;
/**
* @var int The current offset (no of pages times no of items)
*/
private $currentOffset;
/**
* @var int Total pages
*/
private $totalPages;
/**
* Constructor
*
* @param string $markup
* @param array $tokens
* @param FileSystem $fileSystem
*
* @throws \Liquid\Exception\ParseException
*
*/
public function __construct($markup, array &$tokens, FileSystem $fileSystem = null)
{
parent::__construct($markup, $tokens, $fileSystem);
$syntax = new Regexp('/(' . Liquid::get('VARIABLE_NAME') . ')\s+by\s+(\w+)/');
if ($syntax->match($markup)) {
$this->collectionName = $syntax->matches[1];
$this->numberItems = $syntax->matches[2];
$this->extractAttributes($markup);
} else {
throw new ParseException("Syntax Error - Valid syntax: paginate [collection] by [items]");
}
}
/**
* Renders the tag
*
* @param Context $context
*
* @return string
*
*/
public function render(Context $context)
{
$this->collection = $context->get($this->collectionName);
if ($this->collection instanceof \Traversable) {
$this->collection = iterator_to_array($this->collection);
}
if (!is_array($this->collection)) {
// TODO do not throw up if error mode allows, see #83
throw new RenderException("Missing collection with name '{$this->collectionName}'");
}
// How many pages are there?
$this->collectionSize = count($this->collection);
$this->totalPages = ceil($this->collectionSize / $this->numberItems);
// Whatever there is in the context, we need a number
$this->currentPage = intval($context->get(Liquid::get('PAGINATION_CONTEXT_KEY')));
// Page number can only be between 1 and a number of pages
$this->currentPage = max(1, min($this->currentPage, $this->totalPages));
// Find the offset and select that part
$this->currentOffset = ($this->currentPage - 1) * $this->numberItems;
$paginatedCollection = array_slice($this->collection, $this->currentOffset, $this->numberItems);
// We must work in a new scope so we won't pollute a global scope
$context->push();
// Sets the collection if it's a key of another collection (ie search.results, collection.products, blog.articles)
$segments = explode('.', $this->collectionName);
if (count($segments) == 2) {
$context->set($segments[0], array($segments[1] => $paginatedCollection));
} else {
$context->set($this->collectionName, $paginatedCollection);
}
$paginate = array(
'page_size' => $this->numberItems,
'current_page' => $this->currentPage,
'current_offset' => $this->currentOffset,
'pages' => $this->totalPages,
'items' => $this->collectionSize
);
// Get the name of the request field to use in URLs
$pageRequestKey = Liquid::get('PAGINATION_REQUEST_KEY');
if ($this->currentPage > 1) {
$paginate['previous']['title'] = 'Previous';
$paginate['previous']['url'] = $this->currentUrl($context, [
$pageRequestKey => $this->currentPage - 1,
]);
}
if ($this->currentPage < $this->totalPages) {
$paginate['next']['title'] = 'Next';
$paginate['next']['url'] = $this->currentUrl($context, [
$pageRequestKey => $this->currentPage + 1,
]);
}
$context->set('paginate', $paginate);
$result = parent::render($context);
$context->pop();
return $result;
}
/**
* Returns the current page URL
*
* @param Context $context
* @param array $queryPart
*
* @return string
*
*/
public function currentUrl($context, $queryPart = [])
{
// From here we have $url->path and $url->query
$url = (object) parse_url($context->get('REQUEST_URI'));
// Let's merge the query part
if (isset($url->query)) {
parse_str($url->query, $url->query);
$url->query = array_merge($url->query, $queryPart);
} else {
$url->query = $queryPart;
}
$url->query = http_build_query($url->query);
$scheme = $context->get('HTTPS') == 'on' ? 'https' : 'http';
return "$scheme://{$context->get('HTTP_HOST')}{$url->path}?{$url->query}";
}
}

View File

@@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Tag;
use Liquid\Liquid;
use Liquid\AbstractBlock;
use Liquid\Regexp;
/**
* Allows output of Liquid code on a page without being parsed.
*
* Example:
*
* {% raw %}{{ 5 | plus: 6 }}{% endraw %} is equal to 11.
*
* will return:
* {{ 5 | plus: 6 }} is equal to 11.
*/
class TagRaw extends AbstractBlock
{
/**
* @param array $tokens
*/
public function parse(array &$tokens)
{
$tagRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*(\w+)\s*(.*)?' . Liquid::get('TAG_END') . '$/');
$this->nodelist = array();
while (count($tokens)) {
$token = array_shift($tokens);
if ($tagRegexp->match($token)) {
// If we found the proper block delimiter just end parsing here and let the outer block proceed
if ($tagRegexp->matches[1] == $this->blockDelimiter()) {
break;
}
}
$this->nodelist[] = $token;
}
}
}

View File

@@ -0,0 +1,152 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Tag;
use Liquid\AbstractBlock;
use Liquid\Exception\ParseException;
use Liquid\Exception\RenderException;
use Liquid\Liquid;
use Liquid\Context;
use Liquid\FileSystem;
use Liquid\Regexp;
/**
* Quickly create a table from a collection
*/
class TagTablerow extends AbstractBlock
{
/**
* The variable name of the table tag
*
* @var string
*/
public $variableName;
/**
* The collection name of the table tags
*
* @var string
*/
public $collectionName;
/**
* Additional attributes
*
* @var array
*/
public $attributes;
/**
* Constructor
*
* @param string $markup
* @param array $tokens
* @param FileSystem $fileSystem
*
* @throws \Liquid\Exception\ParseException
*/
public function __construct($markup, array &$tokens, FileSystem $fileSystem = null)
{
parent::__construct($markup, $tokens, $fileSystem);
$syntax = new Regexp('/(\w+)\s+in\s+(' . Liquid::get('VARIABLE_NAME') . ')/');
if ($syntax->match($markup)) {
$this->variableName = $syntax->matches[1];
$this->collectionName = $syntax->matches[2];
$this->extractAttributes($markup);
} else {
throw new ParseException("Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols:3");
}
}
/**
* Renders the current node
*
* @param Context $context
* @throws \Liquid\Exception\RenderException
* @return string
*/
public function render(Context $context)
{
$collection = $context->get($this->collectionName);
if ($collection instanceof \Traversable) {
$collection = iterator_to_array($collection);
}
if (!is_array($collection)) {
throw new RenderException("Not an array");
}
// discard keys
$collection = array_values($collection);
if (isset($this->attributes['limit']) || isset($this->attributes['offset'])) {
$limit = $context->get($this->attributes['limit']);
$offset = $context->get($this->attributes['offset']);
$collection = array_slice($collection, $offset, $limit);
}
$length = count($collection);
$cols = isset($this->attributes['cols']) ? $context->get($this->attributes['cols']) : PHP_INT_MAX;
$row = 1;
$col = 0;
$result = "<tr class=\"row1\">\n";
$context->push();
foreach ($collection as $index => $item) {
$context->set($this->variableName, $item);
$context->set('tablerowloop', array(
'length' => $length,
'index' => $index + 1,
'index0' => $index,
'rindex' => $length - $index,
'rindex0' => $length - $index - 1,
'first' => (int)($index == 0),
'last' => (int)($index == $length - 1)
));
$text = $this->renderAll($this->nodelist, $context);
$break = isset($context->registers['break']);
$continue = isset($context->registers['continue']);
if ((!$break && !$continue) || strlen(trim($text)) > 0) {
$result .= "<td class=\"col" . (++$col) . "\">$text</td>";
}
if ($col == $cols && !($index == $length - 1)) {
$col = 0;
$result .= "</tr>\n<tr class=\"row" . (++$row) . "\">\n";
}
if ($break) {
unset($context->registers['break']);
break;
}
if ($continue) {
unset($context->registers['continue']);
}
}
$context->pop();
$result .= "</tr>\n";
return $result;
}
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Tag;
/**
* An if statement
*
* Example:
*
* {% unless true %} YES {% else %} NO {% endunless %}
*
* will return:
* NO
*/
class TagUnless extends TagIf
{
protected function negateIfUnless($display)
{
return !$display;
}
}

View File

@@ -0,0 +1,270 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
use Liquid\Exception\CacheException;
use Liquid\Exception\MissingFilesystemException;
/**
* The Template class.
*
* Example:
*
* $tpl = new \Liquid\Template();
* $tpl->parse(template_source);
* $tpl->render(array('foo'=>1, 'bar'=>2);
*/
class Template
{
const CLASS_PREFIX = '\Liquid\Cache\\';
/**
* @var Document The root of the node tree
*/
private $root;
/**
* @var FileSystem The file system to use for includes
*/
private $fileSystem;
/**
* @var array Globally included filters
*/
private $filters = array();
/**
* @var callable|null Called "sometimes" while rendering. For example to abort the execution of a rendering.
*/
private $tickFunction = null;
/**
* @var array Custom tags
*/
private static $tags = array();
/**
* @var Cache
*/
private static $cache;
/**
* Constructor.
*
* @param string $path
* @param array|Cache $cache
*
* @return Template
*/
public function __construct($path = null, $cache = null)
{
$this->fileSystem = $path !== null
? new LocalFileSystem($path)
: null;
$this->setCache($cache);
}
/**
* @param FileSystem $fileSystem
*/
public function setFileSystem(FileSystem $fileSystem)
{
$this->fileSystem = $fileSystem;
}
/**
* @param array|Cache $cache
*
* @throws \Liquid\Exception\CacheException
*/
public static function setCache($cache)
{
if (is_array($cache)) {
if (isset($cache['cache']) && class_exists($classname = self::CLASS_PREFIX . ucwords($cache['cache']))) {
self::$cache = new $classname($cache);
} else {
throw new CacheException('Invalid cache options!');
}
}
if ($cache instanceof Cache) {
self::$cache = $cache;
}
if (is_null($cache)) {
self::$cache = null;
}
}
/**
* @return Cache
*/
public static function getCache()
{
return self::$cache;
}
/**
* @return Document
*/
public function getRoot()
{
return $this->root;
}
/**
* Register custom Tags
*
* @param string $name
* @param string $class
*/
public static function registerTag($name, $class)
{
self::$tags[$name] = $class;
}
/**
* @return array
*/
public static function getTags()
{
return self::$tags;
}
/**
* Register the filter
*
* @param string $filter
*/
public function registerFilter($filter, callable $callback = null)
{
// Store callback for later use
if ($callback) {
$this->filters[] = [$filter, $callback];
} else {
$this->filters[] = $filter;
}
}
public function setTickFunction(callable $tickFunction)
{
$this->tickFunction = $tickFunction;
}
/**
* Tokenizes the given source string
*
* @param string $source
*
* @return array
*/
public static function tokenize($source)
{
return empty($source)
? array()
: preg_split(Liquid::get('TOKENIZATION_REGEXP'), $source, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
}
/**
* Parses the given source string
*
* @param string $source
*
* @return Template
*/
public function parse($source)
{
if (!self::$cache) {
return $this->parseAlways($source);
}
$hash = md5($source);
$this->root = self::$cache->read($hash);
// if no cached version exists, or if it checks for includes
if ($this->root == false || $this->root->hasIncludes() == true) {
$this->parseAlways($source);
self::$cache->write($hash, $this->root);
}
return $this;
}
/**
* Parses the given source string regardless of caching
*
* @param string $source
*
* @return Template
*/
private function parseAlways($source)
{
$tokens = Template::tokenize($source);
$this->root = new Document($tokens, $this->fileSystem);
return $this;
}
/**
* Parses the given template file
*
* @param string $templatePath
* @throws \Liquid\Exception\MissingFilesystemException
* @return Template
*/
public function parseFile($templatePath)
{
if (!$this->fileSystem) {
throw new MissingFilesystemException("Could not load a template without an initialized file system");
}
return $this->parse($this->fileSystem->readTemplateFile($templatePath));
}
/**
* Renders the current template
*
* @param array $assigns an array of values for the template
* @param array $filters additional filters for the template
* @param array $registers additional registers for the template
*
* @return string
*/
public function render(array $assigns = array(), $filters = null, array $registers = array())
{
$context = new Context($assigns, $registers);
if ($this->tickFunction) {
$context->setTickFunction($this->tickFunction);
}
if (!is_null($filters)) {
if (is_array($filters)) {
$this->filters = array_merge($this->filters, $filters);
} else {
$this->filters[] = $filters;
}
}
foreach ($this->filters as $filter) {
if (is_array($filter)) {
// Unpack a callback saved as second argument
$context->addFilters(...$filter);
} else {
$context->addFilters($filter);
}
}
return $this->root->render($context);
}
}

View File

@@ -0,0 +1,173 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
/**
* Implements a template variable.
*/
class Variable
{
/**
* @var array The filters to execute on the variable
*/
private $filters;
/**
* @var string The name of the variable
*/
private $name;
/**
* @var string The markup of the variable
*/
private $markup;
/**
* Constructor
*
* @param string $markup
*/
public function __construct($markup)
{
$this->markup = $markup;
$filterSep = new Regexp('/' . Liquid::get('FILTER_SEPARATOR') . '\s*(.*)/m');
$syntaxParser = new Regexp('/(' . Liquid::get('QUOTED_FRAGMENT') . ')(.*)/ms');
$filterParser = new Regexp('/(?:\s+|' . Liquid::get('QUOTED_FRAGMENT') . '|' . Liquid::get('ARGUMENT_SEPARATOR') . ')+/');
$filterArgsRegex = new Regexp('/(?:' . Liquid::get('FILTER_ARGUMENT_SEPARATOR') . '|' . Liquid::get('ARGUMENT_SEPARATOR') . ')\s*((?:\w+\s*\:\s*)?' . Liquid::get('QUOTED_FRAGMENT') . ')/');
$this->filters = [];
if ($syntaxParser->match($markup)) {
$nameMarkup = $syntaxParser->matches[1];
$this->name = $nameMarkup;
$filterMarkup = $syntaxParser->matches[2];
if ($filterSep->match($filterMarkup)) {
$filterParser->matchAll($filterSep->matches[1]);
foreach ($filterParser->matches[0] as $filter) {
$filter = trim($filter);
if (preg_match('/\w+/', $filter, $matches)) {
$filterName = $matches[0];
$filterArgsRegex->matchAll($filter);
$matches = Liquid::arrayFlatten($filterArgsRegex->matches[1]);
$this->filters[] = $this->parseFilterExpressions($filterName, $matches);
}
}
}
}
if (Liquid::get('ESCAPE_BY_DEFAULT')) {
// if auto_escape is enabled, and
// - there's no raw filter, and
// - no escape filter
// - no other standard html-adding filter
// then
// - add a mandatory escape filter
$addEscapeFilter = true;
foreach ($this->filters as $filter) {
// with empty filters set we would just move along
if (in_array($filter[0], array('escape', 'escape_once', 'raw', 'newline_to_br'))) {
// if we have any raw-like filter, stop
$addEscapeFilter = false;
break;
}
}
if ($addEscapeFilter) {
$this->filters[] = array('escape', array());
}
}
}
/**
* @param string $filterName
* @param array $unparsedArgs
* @return array
*/
private static function parseFilterExpressions($filterName, array $unparsedArgs)
{
$filterArgs = array();
$keywordArgs = array();
$justTagAttributes = new Regexp('/\A' . trim(Liquid::get('TAG_ATTRIBUTES'), '/') . '\z/');
foreach ($unparsedArgs as $a) {
if ($justTagAttributes->match($a)) {
$keywordArgs[$justTagAttributes->matches[1]] = $justTagAttributes->matches[2];
} else {
$filterArgs[] = $a;
}
}
if (count($keywordArgs)) {
$filterArgs[] = $keywordArgs;
}
return array($filterName, $filterArgs);
}
/**
* Gets the variable name
*
* @return string The name of the variable
*/
public function getName()
{
return $this->name;
}
/**
* Gets all Filters
*
* @return array
*/
public function getFilters()
{
return $this->filters;
}
/**
* Renders the variable with the data in the context
*
* @param Context $context
*
* @return mixed|string
*/
public function render(Context $context)
{
$output = $context->get($this->name);
foreach ($this->filters as $filter) {
list($filtername, $filterArgKeys) = $filter;
$filterArgValues = array();
$keywordArgValues = array();
foreach ($filterArgKeys as $arg_key) {
if (is_array($arg_key)) {
foreach ($arg_key as $keywordArgName => $keywordArgKey) {
$keywordArgValues[$keywordArgName] = $context->get($keywordArgKey);
}
$filterArgValues[] = $keywordArgValues;
} else {
$filterArgValues[] = $context->get($arg_key);
}
}
$output = $context->invoke($filtername, $output, $filterArgValues);
}
return $output;
}
}

View File

@@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Tag;
use Liquid\TestCase;
class AbstractBlockTest extends TestCase
{
public function testUnterminatedBlockError()
{
$this->expectException(\Liquid\Exception\ParseException::class);
$this->assertTemplateResult('', '{% block }');
}
public function testWhitespaceHandler()
{
$this->assertTemplateResult('foo', '{% if true %}foo{% endif %}');
$this->assertTemplateResult(' foo ', '{% if true %} foo {% endif %}');
$this->assertTemplateResult(' foo ', ' {% if true %} foo {% endif %} ');
$this->assertTemplateResult('foo ', '{% if true -%} foo {% endif %}');
$this->assertTemplateResult('foo', '{% if true -%} foo {%- endif %}');
$this->assertTemplateResult('foo', ' {%- if true -%} foo {%- endif %}');
$this->assertTemplateResult('foo', ' {%- if true -%} foo {%- endif -%} ');
$this->assertTemplateResult('foo', ' {%- if true -%} foo {%- endif -%} {%- if false -%} bar {%- endif -%} ');
$this->assertTemplateResult('foobar', ' {%- if true -%} foo {%- endif -%} {%- if true -%} bar {%- endif -%} ');
$this->assertTemplateResult('-> foo', '{% if true %}-> {% endif %} {%- if true -%} foo {%- endif -%}');
}
}

View File

@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Cache;
use Liquid\TestCase;
class ApcTest extends TestCase
{
/** @var \Liquid\Cache\Apc */
protected $cache;
protected function setUp(): void
{
parent::setUp();
if (!function_exists('apc_fetch')) {
$this->markTestSkipped("Alternative PHP Cache (APC) not available");
}
if (!ini_get('apc.enable_cli')) {
$this->markTestSkipped("APC not enabled with cli. Run with: php -d apc.enable_cli=1");
}
$this->cache = new Apc();
}
public function testNotExists()
{
$this->assertFalse($this->cache->exists('no_such_key'));
}
public function testReadNotExisting()
{
$this->assertFalse($this->cache->read('no_such_key'));
}
public function testSetGetFlush()
{
$this->assertTrue($this->cache->write('test', 'example'), "Failed to set value.");
$this->assertSame('example', $this->cache->read('test'));
$this->assertTrue($this->cache->flush());
$this->assertFalse($this->cache->read('test'));
}
}

View File

@@ -0,0 +1,173 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Cache;
use Liquid\TestCase;
class FileTest extends TestCase
{
/** @var \Liquid\Cache\File */
protected $cache;
protected $cacheDir;
protected function setUp(): void
{
parent::setUp();
$this->cacheDir = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'cache_dir';
// Remove tmp cache files because they may remain after a failed test run
$this->removeOldCachedFiles();
$this->cache = new File(array(
'cache_dir' => $this->cacheDir,
'cache_expire' => 3600,
'cache_prefix' => 'liquid_',
));
}
protected function tearDown(): void
{
parent::tearDown();
$this->removeOldCachedFiles();
}
private function removeOldCachedFiles(): void
{
if ($files = glob($this->cacheDir . DIRECTORY_SEPARATOR . '*')) {
array_map('unlink', $files);
}
}
public function testConstructInvalidOptions()
{
$this->expectException(\Liquid\Exception\FilesystemException::class);
new File();
}
public function testConstructNoSuchDirOrNotWritable()
{
$this->expectException(\Liquid\Exception\FilesystemException::class);
new File(array('cache_dir' => '/no/such/dir/liquid/cache'));
}
public function testGetExistsNoFile()
{
$this->assertFalse($this->cache->exists('no_key'));
}
public function testGetExistsExpired()
{
$key = 'test';
$cacheFile = $this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_' . $key;
touch($cacheFile, time() - 1000000); // long ago
$this->assertFalse($this->cache->exists($key));
}
public function testGetExistsNotExpired()
{
$key = 'test';
$cacheFile = $this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_' . $key;
touch($cacheFile);
$this->assertTrue($this->cache->exists($key));
}
public function testFlushAll()
{
touch($this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_test');
touch($this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_test_two');
$this->assertGreaterThanOrEqual(2, count(glob($this->cacheDir . DIRECTORY_SEPARATOR . '*')));
$this->cache->flush();
$this->assertCount(0, glob($this->cacheDir . DIRECTORY_SEPARATOR . '*'));
}
public function testFlushExpired()
{
touch($this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_test');
touch($this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_test_two', time() - 1000000);
$files = join(', ', glob($this->cacheDir . DIRECTORY_SEPARATOR . '*'));
$this->assertGreaterThanOrEqual(2, count(glob($this->cacheDir . DIRECTORY_SEPARATOR . '*')), "Found more than two files: $files");
$this->cache->flush(true);
$this->assertCount(1, glob($this->cacheDir . DIRECTORY_SEPARATOR . '*'));
}
public function testWriteNoSerialize()
{
$key = 'test';
$value = 'test_value';
$this->assertTrue($this->cache->write($key, $value, false));
$this->assertEquals($value, file_get_contents($this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_' . $key));
}
public function testWriteSerialized()
{
$key = 'test';
$value = 'test_value';
$this->assertTrue($this->cache->write($key, $value));
$this->assertEquals(serialize($value), file_get_contents($this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_' . $key));
}
/**
* @depends testWriteSerialized
*/
public function testWriteGc()
{
$key = 'test';
$value = 'test_value';
// This cache file must be removed by GC
touch($this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_test_two', time() - 1000000);
$this->assertTrue($this->cache->write($key, $value, false));
$this->assertCount(1, glob($this->cacheDir . DIRECTORY_SEPARATOR . '*'));
}
public function testReadNonExisting()
{
$this->assertFalse($this->cache->read('no_such_key'));
}
public function testReadNoUnserialize()
{
$key = 'test';
$value = 'test_value';
file_put_contents($this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_' . $key, $value);
$this->assertSame($value, $this->cache->read($key, false));
}
public function testReadSerialize()
{
$key = 'test';
$value = 'test_value';
file_put_contents($this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_' . $key, serialize($value));
$this->assertSame($value, $this->cache->read($key));
}
}

View File

@@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid\Cache;
use Liquid\TestCase;
class LocalTest extends TestCase
{
/** @var \Liquid\Cache\Local */
protected $cache;
protected function setUp(): void
{
parent::setUp();
$this->cache = new Local();
}
public function testNotExists()
{
$this->assertFalse($this->cache->exists('no_such_key'));
}
public function testReadNotExisting()
{
$this->assertFalse($this->cache->read('no_such_key'));
}
public function testSetGetFlush()
{
$this->assertTrue($this->cache->write('test', 'example'));
$this->assertSame('example', $this->cache->read('test'));
$this->assertTrue($this->cache->flush());
$this->assertFalse($this->cache->read('test'));
}
}

View File

@@ -0,0 +1,503 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
class HundredCentes
{
public function toLiquid()
{
return 100;
}
}
class ToLiquidNotObject
{
public function toLiquid()
{
return STDIN;
}
}
class CentsDrop extends Drop
{
public function amount()
{
return new HundredCentes();
}
}
class NoToLiquid
{
public $answer = 42;
private $name = null;
public function name()
{
return 'example';
}
public function count()
{
return 1;
}
public function __toString()
{
return "forty two";
}
}
class ToLiquidWrapper
{
public $value = null;
public function toLiquid()
{
return $this->value;
}
}
class NestedObject
{
public $property;
public $value = -1;
public function toLiquid()
{
// we intentionally made the value different so
// that we could see where it is coming from
return array(
'property' => $this->property,
'value' => 42,
);
}
}
class CountableObject implements \Countable
{
public function count()
{
return 2;
}
}
class ToArrayObject
{
public $property;
public $value = -1;
public function toArray()
{
// we intentionally made the value different so
// that we could see where it is coming from
return array(
'property' => $this->property,
'value' => 42,
);
}
}
class GetSetObject
{
public function field_exists($name)
{
return $name == 'answer';
}
public function get($prop)
{
if ($prop == 'answer') {
return 42;
}
}
}
class GetSetMagic
{
public function __get($prop)
{
if ($prop == 'prime') {
return 2;
}
}
}
class HiFilter
{
public function hi($value)
{
return $value . ' hi!';
}
}
class GlobalFilter
{
public function notice($value)
{
return "Global $value";
}
}
class LocalFilter
{
public function notice($value)
{
return "Local $value";
}
}
class ContextTest extends TestCase
{
/** @var Context */
public $context;
protected function setUp(): void
{
parent::setUp();
$this->context = new Context();
}
public function testScoping()
{
$this->context->push();
$this->assertNull($this->context->pop());
}
/**
*/
public function testNoScopeToPop()
{
$this->expectException(\Liquid\LiquidException::class);
$this->context->pop();
}
/**
*/
public function testGetArray()
{
$this->expectException(\Liquid\LiquidException::class);
$this->context->get(array());
}
public function testGetNotVariable()
{
$data = array(
null => null,
'null' => null,
'true' => true,
'false' => false,
"'quoted_string'" => 'quoted_string',
'"double_quoted_string"' => "double_quoted_string",
);
foreach ($data as $key => $expected) {
$this->assertEquals($expected, $this->context->get($key));
}
$this->assertEquals(42.00, $this->context->get(42.00));
}
public function testVariablesNotExisting()
{
$this->assertNull($this->context->get('test'));
}
public function testVariableIsObjectWithNoToLiquid()
{
$this->context->set('test', new NoToLiquid());
$this->assertEquals(42, $this->context->get('test.answer'));
$this->assertEquals(1, $this->context->get('test.count'));
$this->assertNull($this->context->get('test.invalid'));
$this->assertEquals("forty two", $this->context->get('test'));
$this->assertEquals("example", $this->context->get('test.name'));
}
public function testToLiquidNull()
{
$object = new ToLiquidWrapper();
$this->context->set('object', $object);
$this->assertNull($this->context->get('object.key'));
}
public function testToLiquidStringKeyMustBeNull()
{
$object = new ToLiquidWrapper();
$object->value = 'foo';
$this->context->set('object', $object);
$this->assertNull($this->context->get('object.foo'));
$this->assertNull($this->context->get('object.foo.bar'));
}
public function testNestedObject()
{
$object = new NestedObject();
$object->property = new NestedObject();
$this->context->set('object', $object);
$this->assertEquals(42, $this->context->get('object.value'));
$this->assertEquals(42, $this->context->get('object.property.value'));
$this->assertNull($this->context->get('object.property.value.invalid'));
}
public function testToArrayObject()
{
$object = new ToArrayObject();
$object->property = new ToArrayObject();
$this->context->set('object', $object);
$this->assertEquals(42, $this->context->get('object.value'));
$this->assertEquals(42, $this->context->get('object.property.value'));
$this->assertNull($this->context->get('object.property.value.invalid'));
}
public function testGetSetObject()
{
$this->context->set('object', new GetSetObject());
$this->assertEquals(42, $this->context->get('object.answer'));
$this->assertNull($this->context->get('object.invalid'));
}
public function testGetSetMagic()
{
$this->context->set('object', new GetSetMagic());
$this->assertEquals(2, $this->context->get('object.prime'));
$this->assertNull($this->context->get('object.invalid'));
}
public function testFinalVariableCanBeObject()
{
$this->context->set('test', (object) array('value' => (object) array()));
$this->assertInstanceOf(\stdClass::class, $this->context->get('test.value'));
}
public function testVariables()
{
$this->context->set('test', 'test');
$this->assertTrue($this->context->hasKey('test'));
$this->assertFalse($this->context->hasKey('test.foo'));
$this->assertEquals('test', $this->context->get('test'));
// We add this text to make sure we can return values that evaluate to false properly
$this->context->set('test_0', 0);
$this->assertEquals('0', $this->context->get('test_0'));
}
public function testLengthQuery()
{
$this->context->set('numbers', array(1, 2, 3, 4));
$this->assertEquals(4, $this->context->get('numbers.size'));
}
public function testStringLength()
{
$this->context->set('name', 'Foo Bar');
$this->assertEquals(7, $this->context->get('name.size'));
$this->context->set('name', 'テスト');
$this->assertEquals(3, $this->context->get('name.size'));
}
public function testCountableLength()
{
$this->context->set('countable', new CountableObject());
$this->assertEquals(2, $this->context->get('countable.size'));
}
public function testOverrideSize()
{
$this->context->set('hash', array('a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'size' => '5000'));
$this->assertEquals(5000, $this->context->get('hash.size'));
}
public function testArrayFirst()
{
$this->context->set('array', array(11, 'jack', 43, 74, 5, 'tom'));
$this->assertEquals(11, $this->context->get('array.first'));
}
public function testOverrideFirst()
{
$this->context->set('array', array(11, 'jack', 43, 'first' => 74, 5, 'tom'));
$this->assertEquals(74, $this->context->get('array.first'));
}
public function testArrayLast()
{
$this->context->set('array', array(11, 'jack', 43, 74, 5, 'tom'));
$this->assertEquals('tom', $this->context->get('array.last'));
}
public function testOverrideLast()
{
$this->context->set('array', array(11, 'jack', 43, 'last' => 74, 5, 'tom'));
$this->assertEquals(74, $this->context->get('array.last'));
}
public function testDeepValueNotObject()
{
$this->context->set('example', array('foo' => new ToLiquidNotObject()));
$this->assertNull($this->context->get('example.foo.bar'));
}
public function testHierchalData()
{
$this->context->set('hash', array('name' => 'tobi'));
$this->assertEquals('tobi', $this->context->get('hash.name'));
}
public function testHierchalDataNoKey()
{
$this->context->set('hash', array('name' => 'tobi'));
$this->assertNull($this->context->get('hash.no_key'));
}
public function testAddFilter()
{
$context = new Context();
$context->addFilters(new HiFilter());
$this->assertEquals('hi? hi!', $context->invoke('hi', 'hi?'));
$context = new Context();
$this->assertEquals('hi?', $context->invoke('hi', 'hi?'));
$context->addFilters(new HiFilter());
$this->assertEquals('hi? hi!', $context->invoke('hi', 'hi?'));
}
public function testOverrideGlobalFilter()
{
$template = new Template();
$template->registerFilter(new GlobalFilter());
$template->parse("{{'test' | notice }}");
$this->assertEquals('Global test', $template->render());
$this->assertEquals('Local test', $template->render(array(), new LocalFilter()));
}
public function testCallbackFilter()
{
$template = new Template();
$template->registerFilter('foo', function ($arg) {
return "Foo $arg";
});
$template->parse("{{'test' | foo }}");
$this->assertEquals('Foo test', $template->render());
}
public function testAddItemInOuterScope()
{
$this->context->set('test', 'test');
$this->context->push();
$this->assertEquals('test', $this->context->get('test'));
$this->context->pop();
$this->assertEquals('test', $this->context->get('test'));
}
public function testAddItemInInnerScope()
{
$this->context->push();
$this->context->set('test', 'test');
$this->assertEquals('test', $this->context->get('test'));
$this->context->pop();
$this->assertNull($this->context->get('test'));
}
public function testMerge()
{
$this->context->merge(array('test' => 'test'));
$this->assertEquals('test', $this->context->get('test'));
$this->context->merge(array('test' => 'newvalue', 'foo' => 'bar'));
$this->assertEquals('newvalue', $this->context->get('test'));
$this->assertEquals('bar', $this->context->get('foo'));
}
public function testCents()
{
$this->context->merge(array('cents' => new HundredCentes()));
$this->assertEquals(100, $this->context->get('cents'));
}
public function testNestedCents()
{
$this->context->merge(array('cents' => array('amount' => new HundredCentes())));
$this->assertEquals(100, $this->context->get('cents.amount'));
$this->context->merge(array('cents' => array('cents' => array('amount' => new HundredCentes()))));
$this->assertEquals(100, $this->context->get('cents.cents.amount'));
}
public function testCentsThroughDrop()
{
$this->context->merge(array('cents' => new CentsDrop()));
$this->assertEquals(100, $this->context->get('cents.amount'));
}
public function testCentsThroughDropNestedly()
{
$this->context->merge(array('cents' => array('cents' => new CentsDrop())));
$this->assertEquals(100, $this->context->get('cents.cents.amount'));
$this->context->merge(array('cents' => array('cents' => array('cents' => new CentsDrop()))));
$this->assertEquals(100, $this->context->get('cents.cents.cents.amount'));
}
public function testGetNoOverride()
{
$_GET['test'] = '<script>alert()</script>';
// Previously $_GET would override directly set values
// It happend during class construction - we need to create a brand new instance right here
$context = new Context();
$context->set('test', 'test');
$this->assertEquals('test', $context->get('test'));
}
public function testServerOnlyExposeWhitelistByDefault()
{
$_SERVER['AWS_SECRET_ACCESS_KEY'] = 'super_secret';
$context = new Context();
$this->assertNull($context->get('AWS_SECRET_ACCESS_KEY'));
$context->set('AWS_SECRET_ACCESS_KEY', 'test');
$this->assertEquals('test', $context->get('AWS_SECRET_ACCESS_KEY'));
$_SERVER['FOO'] = 'foo';
$_SERVER['BAR'] = 'bar';
Liquid::set('SERVER_SUPERGLOBAL_WHITELIST', ['FOO']);
$context = new Context();
$this->assertEquals('foo', $context->get('FOO'));
$this->assertNull($context->get('BAR'));
$context->set('BAR', 'bar');
$this->assertEquals('bar', $context->get('BAR'));
}
public function testServerExposedWhenRequested()
{
Liquid::set('EXPOSE_SERVER', true);
$_SERVER['AWS_SECRET_ACCESS_KEY'] = 'super_secret';
$context = new Context();
$this->assertEquals('super_secret', $context->get('AWS_SECRET_ACCESS_KEY'));
$context->set('AWS_SECRET_ACCESS_KEY', 'test');
$this->assertEquals('super_secret', $context->get('AWS_SECRET_ACCESS_KEY'), '$_SERVER should take precedence in this case');
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
class CustomFiltersTest extends TestCase
{
/**
* The current context
*
* @var Context
*/
public $context;
protected function setUp(): void
{
parent::setUp();
$this->context = new Context();
}
public function testSortKey()
{
$data = array(
array(
array(),
array(),
),
array(
array('b' => 1, 'c' => 5, 'a' => 3, 'z' => 4, 'h' => 2),
array('a' => 3, 'b' => 1, 'c' => 5, 'h' => 2, 'z' => 4),
),
);
foreach ($data as $item) {
$this->assertEquals($item[1], CustomFilters::sort_key($item[0]));
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
use Liquid\Tag\TagComment;
class TagFoo extends TagComment
{
}
class CustomTagTest extends TestCase
{
public function testUnknownTag()
{
$template = new Template();
if (array_key_exists('foo', $template->getTags())) {
$this->markTestIncomplete("Test tag already registered. Are you missing @depends?");
}
$this->expectException(\Liquid\Exception\ParseException::class);
$this->expectExceptionMessage('Unknown tag foo');
$template->parse('[ba{% foo %} Comment {% endfoo %}r]');
}
/**
* @depends testUnknownTag
*/
public function testCustomTag()
{
$template = new Template();
$template->registerTag('foo', TagFoo::class);
$template->parse('[ba{% foo %} Comment {% endfoo %}r]');
$this->assertEquals('[bar]', $template->render());
}
}

View File

@@ -0,0 +1,140 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
class ContextDrop extends Drop
{
public function beforeMethod($method)
{
return $this->context->get($method);
}
}
class TextDrop extends Drop
{
public function get_array()
{
return array('text1', 'text2');
}
public function text()
{
return 'text1';
}
}
class CatchallDrop extends Drop
{
public function beforeMethod($method)
{
return 'method: ' . $method;
}
}
class ProductDrop extends Drop
{
public function top_sales()
{
throw new \Exception("worked");
}
public function texts()
{
return new TextDrop();
}
public function catchall()
{
return new CatchallDrop();
}
public function context()
{
return new ContextDrop();
}
public function callmenot()
{
return "protected";
}
public function hasKey($name)
{
return $name != 'unknown' && $name != 'false';
}
}
class DropTest extends TestCase
{
/**
*/
public function testProductDrop()
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('worked');
$template = new Template();
$template->parse(' {{ product.top_sales }} ');
$template->render(array('product' => new ProductDrop));
}
public function testNoKeyDrop()
{
$template = new Template();
$template->parse(' {{ product.invalid.unknown }}{{ product.false }} ');
$output = $template->render(array('product' => new ProductDrop));
$this->assertEquals(' ', $output);
}
public function testTextDrop()
{
$template = new Template();
$template->parse(' {{ product.texts.text }} ');
$output = $template->render(array('product' => new ProductDrop()));
$this->assertEquals(' text1 ', $output);
$template = new Template();
$template->parse(' {{ product.catchall.unknown }} ');
$output = $template->render(array('product' => new ProductDrop()));
$this->assertEquals(' method: unknown ', $output);
}
public function testTextArrayDrop()
{
$template = new Template();
$template->parse('{% for text in product.texts.get_array %} {{text}} {% endfor %}');
$output = $template->render(array('product' => new ProductDrop()));
$this->assertEquals(' text1 text2 ', $output);
}
public function testContextDrop()
{
$template = new Template();
$template->parse(' {{ context.bar }} ');
$output = $template->render(array('context' => new ContextDrop(), 'bar' => 'carrot'));
$this->assertEquals(' carrot ', $output);
}
public function testNestedContextDrop()
{
$template = new Template();
$template->parse(' {{ product.context.foo }} ');
$output = $template->render(array('product' => new ProductDrop(), 'foo' => 'monkey'));
$this->assertEquals(' monkey ', $output);
}
public function testToString()
{
$this->assertEquals(ProductDrop::class, strval(new ProductDrop()));
}
}

View File

@@ -0,0 +1,127 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
class ObjectWithToString
{
private $string = '';
public function __construct($string)
{
$this->string = $string;
}
public function __toString()
{
return $this->string;
}
}
class EscapeByDefaultTest extends TestCase
{
const XSS = "<script>alert()</script>";
const XSS_FAILED = "&lt;script&gt;alert()&lt;/script&gt;";
protected $assigns = array();
protected function setUp(): void
{
parent::setUp();
$this->assigns = array(
'xss' => self::XSS,
);
}
public function testUnescaped()
{
$text = "{{ xss }}";
$expected = self::XSS;
$this->assertTemplateResult($expected, $text, $this->assigns);
}
public function testEscapedManually()
{
$text = "{{ xss | escape }}";
$expected = self::XSS_FAILED;
$this->assertTemplateResult($expected, $text, $this->assigns);
}
public function testRawWithoutAutoEscape()
{
$text = "{{ xss | raw }}";
$expected = self::XSS;
$this->assertTemplateResult($expected, $text, $this->assigns);
}
public function testEscapedAutomatically()
{
Liquid::set('ESCAPE_BY_DEFAULT', true);
$text = "{{ xss }}";
$expected = self::XSS_FAILED;
$this->assertTemplateResult($expected, $text, $this->assigns);
}
public function testEscapedManuallyInAutoMode()
{
Liquid::set('ESCAPE_BY_DEFAULT', true);
// text should only be escaped once
$text = "{{ xss | escape }}";
$expected = self::XSS_FAILED;
$this->assertTemplateResult($expected, $text, $this->assigns);
}
public function testRawInAutoMode()
{
Liquid::set('ESCAPE_BY_DEFAULT', true);
$text = "{{ xss | raw }}";
$expected = self::XSS;
$this->assertTemplateResult($expected, $text, $this->assigns);
}
public function testNlToBr()
{
Liquid::set('ESCAPE_BY_DEFAULT', true);
$text = "{{ xss | newline_to_br }}";
$expected = self::XSS."<br />\n".self::XSS;
$this->assertTemplateResult($expected, $text, array('xss' => self::XSS."\n".self::XSS));
}
public function testToStringEscape()
{
$this->assertTemplateResult(self::XSS_FAILED, "{{ xss | escape }}", array('xss' => new ObjectWithToString(self::XSS)));
}
public function testToStringEscapeDefault()
{
Liquid::set('ESCAPE_BY_DEFAULT', true);
$this->assertTemplateResult(self::XSS_FAILED, "{{ xss }}", array('xss' => new ObjectWithToString(self::XSS)));
}
/** System default value for the escape flag */
private static $escapeDefault;
public static function setUpBeforeClass(): void
{
// save system default value for the escape flag before all tests
self::$escapeDefault = Liquid::get('ESCAPE_BY_DEFAULT');
}
protected function tearDown(): void
{
// reset to the default after each test
Liquid::set('ESCAPE_BY_DEFAULT', self::$escapeDefault);
}
}

View File

@@ -0,0 +1,244 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace {
/**
* Global function acts as a filter.
*
* @param $value
*
* @return string
*/
function functionFilter($value)
{
return 'worked';
}
/**
* Global filter class
*/
class ClassFilter
{
private $variable = 'not set';
public function __construct()
{
}
public static function static_test()
{
return "worked";
}
public function instance_test_one()
{
$this->variable = 'set';
return 'set';
}
public function instance_test_two()
{
return $this->variable;
}
}
} // global namespace
namespace Liquid {
use Liquid\Cache\File;
class NamespacedClassFilter
{
public static function static_test2($var)
{
return "good {$var}";
}
}
class FilterbankTest extends TestCase
{
/** @var FilterBank */
private $filterBank;
/** @var Context */
private $context;
protected function setUp(): void
{
parent::setUp();
$this->context = new Context();
$this->filterBank = new FilterBank($this->context);
}
protected function tearDown(): void
{
// have to destroy these else PHP goes nuts
unset($this->context);
unset($this->filterBank);
}
/**
*/
public function testAddFilterNotObjectAndString()
{
$this->expectException(\Liquid\Exception\WrongArgumentException::class);
$this->filterBank->addFilter(array());
}
/**
*/
public function testAddFilterNoFunctionOrClass()
{
$this->expectException(\Liquid\Exception\WrongArgumentException::class);
$this->filterBank->addFilter('no_such_function_or_class');
}
public function testTypeErrorExceptionAndCallDateFilterWithoutArguments()
{
if (\PHP_VERSION_ID < 70100) {
$this->markTestSkipped('TypeError is not thrown in PHP 7.0');
}
$var = new Variable('var | date');
$this->context->set('var', 1000);
$this->expectException(\Liquid\LiquidException::class);
$var->render($this->context);
}
public function testInvokeNoFilter()
{
$value = 'value';
$this->assertEquals($value, $this->filterBank->invoke('non_existing_filter', $value));
}
/**
* Test using a simple function
*/
public function testFunctionFilter()
{
$var = new Variable('var | functionFilter');
$this->context->set('var', 1000);
$this->context->addFilters('functionFilter');
$this->assertEquals('worked', $var->render($this->context));
}
/**
* Test using a namespaced static class
*/
public function testNamespacedStaticClassFilter()
{
$var = new Variable('var | static_test2');
$this->context->set('var', 1000);
$this->context->addFilters(NamespacedClassFilter::class);
$this->assertEquals('good 1000', $var->render($this->context));
}
/**
* Test using a static class
*/
public function testStaticClassFilter()
{
$var = new Variable('var | static_test');
$this->context->set('var', 1000);
$this->context->addFilters(\ClassFilter::class);
$this->assertEquals('worked', $var->render($this->context));
}
/**
* Test with instance method on a static class
*/
public function testStaticMixedClassFilter()
{
$var = new Variable('var | instance_test_one');
$this->context->set('var', 'foo');
$this->context->addFilters(\ClassFilter::class);
$this->assertEquals('foo', $var->render($this->context));
}
/**
* Test using an object as a filter; an object fiter will retain its state
* between calls to its filters.
*/
public function testObjectFilter()
{
$var = new Variable('var | instance_test_one');
$this->context->set('var', 1000);
$this->context->addFilters(new \ClassFilter());
$this->assertEquals('set', $var->render($this->context));
$var = new Variable('var | instance_test_two');
$this->assertEquals('set', $var->render($this->context));
$var = new Variable('var | static_test');
$this->assertEquals('worked', $var->render($this->context));
$var = new Variable('var | __construct');
$this->assertEquals('1000', $var->render($this->context));
}
public function testObjectFilterDontCallConstruct()
{
$this->context->set('var', 1000);
$this->context->addFilters(new \ClassFilter());
$filterbankReflectionClass = new \ReflectionClass(Context::class);
$methodMapProperty = $filterbankReflectionClass->getProperty('filterbank');
$methodMapProperty->setAccessible(true);
$filterbank = $methodMapProperty->getValue($this->context);
$filterbankReflectionClass = new \ReflectionClass(Filterbank::class);
$methodMapProperty = $filterbankReflectionClass->getProperty('methodMap');
$methodMapProperty->setAccessible(true);
$methodMap = $methodMapProperty->getValue($filterbank);
$this->assertArrayNotHasKey('__construct', $methodMap);
$var = new Variable('var | __construct');
$this->assertEquals('1000', $var->render($this->context));
}
public function testCallbackFilter()
{
$var = new Variable('var | my_callback');
$this->context->set('var', 1000);
$this->context->addFilters('my_callback', function ($var) {
return $var * 2;
});
$this->assertEquals('2000', $var->render($this->context));
}
/**
* Closures are not to be serialized. Let's check that.
*/
public function testWithSerializingCache()
{
$template = new Template();
$template->registerFilter('foo', function ($arg) {
return "Foo $arg";
});
$template->setCache(new File(array(
'cache_dir' => __DIR__.'/cache_dir/',
)));
$template->parse("{{'test' | foo }}");
$this->assertEquals('Foo test', $template->render());
$template->parse("{{'bar' | foo }}");
$this->assertEquals('Foo bar', $template->render());
}
}
} // Liquid namespace

View File

@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
use Liquid\FileSystem\Virtual;
class FixturesTest extends TestCase
{
/**
* @dataProvider fixtures
* @param string $liquid
* @param string $data
* @param string $expected
*/
public function testFixture($liquid, $data, $expected)
{
$template = new Template();
$template->setFileSystem(new Virtual(function ($filename) {
if (is_file(__DIR__.'/fixtures/'.$filename)) {
return file_get_contents(__DIR__.'/fixtures/'.$filename);
}
}));
$template->parse(file_get_contents($liquid));
$result = $template->render(include $data);
if (getenv('GOLDEN') !== false) {
file_put_contents($expected, $result);
$this->markTestIncomplete("Saved golden fixture");
}
$this->assertEquals(file_get_contents($expected), $result);
}
public function fixtures()
{
foreach (array_map(null, glob(__DIR__.'/fixtures/*.liquid'), glob(__DIR__.'/fixtures/*.php'), glob(__DIR__.'/fixtures/*.html')) as $files) {
yield basename($files[0], '.liquid') => $files;
};
}
}

View File

@@ -0,0 +1,88 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
class LiquidTest extends TestCase
{
public function testGetNonExistingPropery()
{
$this->assertNull(Liquid::get('no_such_value'));
}
public function testSetProperty()
{
$key = 'test_key';
$value = 'test_value';
Liquid::set($key, $value);
$this->assertSame($value, Liquid::get($key));
}
public function testGetSetAllowedChars()
{
Liquid::set('ALLOWED_VARIABLE_CHARS', 'abc');
$this->assertSame('abc', Liquid::get('ALLOWED_VARIABLE_CHARS'));
$this->assertSame('abc+', Liquid::get('VARIABLE_NAME'));
}
public function testArrayFlattenEmptyArray()
{
$this->assertSame(array(), Liquid::arrayFlatten(array()));
}
public function testArrayFlattenFlatArray()
{
$object = new \stdClass();
// Method does not maintain keys.
$original = array(
'one' => 'one_value',
42,
$object,
);
$expected = array(
'one_value',
42,
$object
);
$this->assertEquals($expected, Liquid::arrayFlatten($original));
}
public function testArrayFlattenNestedArray()
{
$object = new \stdClass();
// Method does not maintain keys.
$original = array(
'one' => 'one_value',
42 => array(
'one_value',
array(
'two_value',
10
),
),
$object,
);
$expected = array(
'one_value',
'one_value',
'two_value',
10,
$object
);
$this->assertEquals($expected, Liquid::arrayFlatten($original));
}
}

View File

@@ -0,0 +1,169 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
use Liquid\FileSystem\Local;
class LocalFileSystemTest extends TestCase
{
protected $root;
protected function setUp(): void
{
$this->root = __DIR__ . DIRECTORY_SEPARATOR . self::TEMPLATES_DIR . DIRECTORY_SEPARATOR;
// reset to defaults
Liquid::set('INCLUDE_ALLOW_EXT', false);
}
/**
*/
public function testIllegalTemplateNameEmpty()
{
$this->expectException(\Liquid\LiquidException::class);
$fileSystem = new Local('');
$fileSystem->fullPath('');
}
/**
*/
public function testIllegalRootPath()
{
$this->expectException(\Liquid\LiquidException::class);
$fileSystem = new Local('invalid/not/found');
$fileSystem->fullPath('');
}
/**
*/
public function testIllegalTemplateNameIncludeExtension()
{
$this->expectException(\Liquid\LiquidException::class);
Liquid::set('INCLUDE_ALLOW_EXT', false);
$fileSystem = new Local('');
$fileSystem->fullPath('has_extension.ext');
}
/**
*/
public function testIllegalTemplateNameNotIncludeExtension()
{
$this->expectException(\Liquid\LiquidException::class);
Liquid::set('INCLUDE_ALLOW_EXT', true);
$fileSystem = new Local('');
$fileSystem->fullPath('has_extension');
}
/**
*/
public function testIllegalTemplatePathNoRoot()
{
$this->expectException(\Liquid\LiquidException::class);
$fileSystem = new Local('');
$fileSystem->fullPath('mypartial');
}
/**
*/
public function testIllegalTemplatePathNoFileExists()
{
$this->expectException(\Liquid\LiquidException::class);
$fileSystem = new Local(dirname(__DIR__));
$fileSystem->fullPath('no_such_file_exists');
}
/**
*/
public function testIllegalTemplatePathNotUnderTemplateRoot()
{
$this->expectException(\Liquid\LiquidException::class);
$this->expectExceptionMessage('not under');
Liquid::set('INCLUDE_ALLOW_EXT', true);
$fileSystem = new Local(dirname($this->root));
// find any fail under deeper under the root, so all other checks would pass
$filesUnderCurrentDir = array_map('basename', glob(dirname(__DIR__).'/../*'));
// path relative to root; we can't start it with a dot since it isn't allowed anyway
$fileSystem->fullPath(self::TEMPLATES_DIR."/../../../{$filesUnderCurrentDir[0]}");
}
public function testValidPathWithDefaultExtension()
{
$templateName = 'mypartial';
$fileSystem = new Local($this->root);
$this->assertEquals($this->root . Liquid::get('INCLUDE_PREFIX') . $templateName . '.' . Liquid::get('INCLUDE_SUFFIX'), $fileSystem->fullPath($templateName));
}
public function testValidPathWithCustomExtension()
{
Liquid::set('INCLUDE_PREFIX', '');
Liquid::set('INCLUDE_SUFFIX', 'tpl');
$templateName = 'mypartial';
$fileSystem = new Local($this->root);
$this->assertEquals($this->root . Liquid::get('INCLUDE_PREFIX') . $templateName . '.' . Liquid::get('INCLUDE_SUFFIX'), $fileSystem->fullPath($templateName));
}
/**
*/
public function testReadIllegalTemplatePathNoFileExists()
{
$this->expectException(\Liquid\LiquidException::class);
$this->expectExceptionMessage('File not found');
$fileSystem = new Local(dirname(__DIR__));
$fileSystem->readTemplateFile('no_such_file_exists');
}
public function testReadTemplateFile()
{
Liquid::set('INCLUDE_PREFIX', '');
Liquid::set('INCLUDE_SUFFIX', 'tpl');
$fileSystem = new Local($this->root);
$this->assertEquals('test content', trim($fileSystem->readTemplateFile('mypartial')));
}
public function testDeprecatedLocalFileSystemExists()
{
$this->assertInstanceOf(Local::class, new LocalFileSystem($this->root));
}
public function testParseTemplateFile()
{
Liquid::set('INCLUDE_PREFIX', '');
Liquid::set('INCLUDE_SUFFIX', 'tpl');
$template = new Template($this->root);
$this->assertEquals("test content\n", $template->parseFile('mypartial')->render());
}
/**
*/
public function testParseTemplateFileError()
{
$this->expectException(\Liquid\LiquidException::class);
$this->expectExceptionMessage('Could not load a template');
$template = new Template();
$template->parseFile('mypartial');
}
}

View File

@@ -0,0 +1,194 @@
<?php
/*
* This file is part of the Liquid package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @package Liquid
*/
namespace Liquid;
class FunnyFilter
{
public function make_funny($input)
{
return 'LOL';
}
public function cite_funny($input)
{
return 'LOL: ' . $input;
}
public function add_smiley($input, $smiley = ":-)")
{
return $input . ' ' . $smiley;
}
public function add_tag($input, $tag = "p", $id = "foo")
{
return "<" . $tag . " id=\"" . $id . "\">" . $input . "</" . $tag . ">";
}
public function paragraph($input)
{
return "<p>" . $input . "</p>";
}
public function link_to($name, $url, $protocol)
{
return "<a href=\"" . $protocol . '://' .$url . "\">" . $name . "</a>";
}
public function str_replace($input, $data)
{
foreach ($data as $k => $v) {
$input = str_replace("[" . $k . "]", $v, $input);
}
return $input;
}
public function img_url($input, $size, $opts = null)
{
$output = "image_" . $size;
if (isset($opts['crop'])) {
$output .= "_cropped_" . $opts['crop'];
}
if (isset($opts['scale'])) {
$output .= "@" . $opts['scale'] . 'x';
}
return $output . ".png";
}
}
class OutputTest extends TestCase
{
protected $assigns = array();
protected function setUp(): void
{
parent::setUp();
$this->assigns = array(
'best_cars' => 'bmw',
'car' => array('bmw' => 'good', 'gm' => 'bad')
);
$this->filters = new FunnyFilter();
}
public function testVariable()
{
$text = " {{best_cars}} ";
$expected = " bmw ";
$this->assertTemplateResult($expected, $text, $this->assigns);
}
public function testVariableTrasversing()
{
$text = " {{car.bmw}} {{car.gm}} {{car.bmw}} ";
$expected = " good bad good ";
$this->assertTemplateResult($expected, $text, $this->assigns);
}
public function testVariablePiping()
{
$text = " {{ car.gm | make_funny }} ";
$expected = " LOL ";
$this->assertTemplateResult($expected, $text, $this->assigns);
}
public function testVariablePipingWithInput()
{
$text = " {{ car.gm | cite_funny }} ";
$expected = " LOL: bad ";
$this->assertTemplateResult($expected, $text, $this->assigns);
}
public function testVariablePipingWithArgs()
{
$text = " {{ car.gm | add_smiley : '=(' }} ";
$expected = " bad =( ";
$this->assertTemplateResult($expected, $text, $this->assigns);
}
public function textVariablePipingWithNoArgs()
{
$text = " {{ car.gm | add_smile }} ";
$expected = " bad =( ";
$this->assertTemplateResult($expected, $text, $this->assigns);
}
public function testMultipleVariablePipingWithArgs()
{
$text = " {{ car.gm | add_smiley : '=(' | add_smiley : '=('}} ";
$expected = " bad =( =( ";
$this->assertTemplateResult($expected, $text, $this->assigns);
}
public function testVariablePipingWithTwoArgs()
{
$text = " {{ car.gm | add_tag : 'span', 'bar'}} ";
$expected = " <span id=\"bar\">bad</span> ";
$this->assertTemplateResult($expected, $text, $this->assigns);
}
public function testVariablePipingWithVariableArgs()
{
$text = " {{ car.gm | add_tag : 'span', car.bmw}} ";
$expected = " <span id=\"good\">bad</span> ";
$this->assertTemplateResult($expected, $text, $this->assigns);
}
public function testVariablePipingWithKeywordArg()
{
$text = " {{ 'Welcome, [name]' | str_replace: name: 'Santa' }} ";
$expected = " Welcome, Santa ";
$this->assertTemplateResult($expected, $text, $this->assigns);
}
public function testVariablePipingWithArgsAndKeywordArgs()
{
$text = " {{ car.gm | img_url: '450x450', crop: 'center', scale: 2 }} ";
$expected = " image_450x450_cropped_center@2x.png ";
$this->assertTemplateResult($expected, $text, $this->assigns);
}
public function testMultiplePipings()
{
$text = " {{ best_cars | cite_funny | paragraph }} ";
$expected = " <p>LOL: bmw</p> ";
$this->assertTemplateResult($expected, $text, $this->assigns);
}
public function testLinkTo()
{
$text = " {{ 'Typo' | link_to: 'typo.leetsoft.com':'http' }} ";
$expected = " <a href=\"http://typo.leetsoft.com\">Typo</a> ";
$this->assertTemplateResult($expected, $text, $this->assigns);
}
/**
*/
public function testVariableWithANewLine()
{
$text = "{{ aaa\n }}";
$this->assertTemplateResult('', $text, $this->assigns);
}
}

Some files were not shown because too many files have changed in this diff Show More