nur-sery/nur_src/data/template/InterpTemplate.php

205 lines
6.8 KiB
PHP

<?php
namespace nur\data\template;
use nur\A;
use nur\b\io\IOException;
use nur\data\expr\SimpleContext;
use nur\func;
use nur\session;
use ZipArchive;
/**
* Class InterpTemplate: réimplémentation de l'ancienne classe interp
*/
class InterpTemplate extends SimpleContext implements ITemplate {
use TTemplate;
function __construct(?string $text=null, $data=null, $quote=true, bool $allowPattern=true) {
if ($data === null) $data = [];
parent::__construct($data);
$this->context = $this;
$this->setText($text, $data, $quote, $allowPattern);
}
/** @var string */
protected $text;
/** @var bool|array */
protected $quote;
/** @var bool */
protected $allowPattern;
function setText(?string $text, $data=null, $quote=true, bool $allowPattern=true): void {
$this->text = $text;
if ($data !== null) {
$this->data = A::with($data);
$this->quote = $quote;
$this->allowPattern = $allowPattern;
}
}
/**
* Traiter la valeur $value selon la valeur de $quote
*
* D'abord la transformer en chaine, puis:
* - si $quote===true, la mettre en échappement avec htmlspecialchars()
* - si $quote===false ou null, ne pas la mettre en échappement
* - sinon, ce doit être une fonction qui met la valeur en échappement
*/
private static function quote($value, $quote) {
if (A::is_array($value)) $value = print_r(A::with($value), true);
elseif (!is_string($value)) $value = strval($value);
if ($quote === true) return htmlspecialchars($value);
else if ($quote === false || $quote === null) return $value;
else return func::call($quote, $value);
}
/**
* Traiter la valeur $value selon la valeur de $quote. Le traitement final est
* fait avec la méthode quote()
*
* - Si $quote est un tableau, alors $quote[$name] est la valeur utilisée pour
* décider comment traiter la valeur et sa valeur par défaut est true.
* - Sinon prendre la valeur $quote telle quelle
*/
private static function quote_nv(string $name, $value, $quote) {
if (is_array($quote)) {
if (isset($quote[$name])) {
$value = self::quote($value, $quote[$name]);
} else {
# quoter par défaut quand on fournit un tableau
$value = self::quote($value, true);
}
} else {
$value = self::quote($value, $quote);
}
return $value;
}
/**
* Résoudre la valeur du nom $name
* - Si $name est de la forme '+keyp', prendre la valeur dans la configuration
* - Si $name est de la forme '*keyp', prendre la valeur dans la session
* - Sinon, $name est le chemin de clé dans le tableau $values
*/
private function resolve(string $name, ?array $values, $quote) {
switch (substr($name, 0, 1)) {
case "+":
return $this->getConfig(substr($name, 1));
case "*":
if (!session::started()) return "";
return $this->getSession(substr($name, 1));
default:
$value = A::pget_s($values, $name, "");
$value = self::quote_nv($name, $value, $quote);
return $value;
}
}
function apply() {
$text = $this->text;
$data = $this->data;
$quote = $this->quote;
## d'abord les remplacements complexes
if ($this->allowPattern) {
if ($data !== null) {
$names = array_keys($data);
$name_keys_pattern = '('.implode("|", $names).')((?:\.[a-zA-Z0-9_]+)*)';
$or_name_pattern = '|(?:(?:'.implode("|", $names).')(?:\.[a-zA-Z0-9_]+)*)';
} else {
$name_keys_pattern = null;
$or_name_pattern = '';
}
# patterns conditionnels
# autoriser un niveau de variable complexe à l'intérieur de la condition, i.e {{if(cond){{var.value}}}}
$pattern = '/\{\{(if|unless)\(((?:[+*][a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)'.$or_name_pattern.')\)((?:[^{}]*(?:\{\{[^{}]*\}\})?(?:\{[^{}]*\})?)*)\}\}/s';
$text = preg_replace_callback($pattern, function($matches) use($data, $quote) {
$cond = $this->resolve($matches[2], $data, $quote);
if ($matches[1] == "if") {
if ($cond) $value = $matches[3];
else return "";
} elseif ($matches[1] == "unless") {
if (!$cond) $value = $matches[3];
else return "";
} else {
return $matches[0];
}
$value = self::xml($value, $data, $quote);
return $value;
}, $text);
# valeurs de config
$pattern = '/\{\{(\+[a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)\}\}/s';
$text = preg_replace_callback($pattern, function($matches) use($data, $quote) {
$name = $matches[1];
$value = $this->resolve($name, $data, $quote);
return $value;
}, $text);
# valeurs de la session
$pattern = '/\{\{(\*[a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)\}\}/s';
$text = preg_replace_callback($pattern, function($matches) use($data, $quote) {
$name = $matches[1];
$value = $this->resolve($name, $data, $quote);
return $value;
}, $text);
if ($data !== null) {
# indirections
$pattern = '/\{\{'.$name_keys_pattern.'\}\}/s';
$text = preg_replace_callback($pattern, function($matches) use($data, $quote) {
$name = $matches[1];
$value = $this->resolve("$name$matches[2]", $data, $quote);
return $value;
}, $text);
}
}
if ($data !== null) {
## ensuite les remplacements simples
$froms = array();
$tos = array();
foreach ($data as $name => $value) {
$froms[] = "{".$name."}";
$tos[] = self::quote_nv($name, $value, $quote);
}
$text = str_replace($froms, $tos, $text);
}
return $text;
}
function xml(?string $text, $data=null, $quote=true, bool $allowPattern=true): string {
$this->setText($text, $data, $quote, $allowPattern);
return $this->apply();
}
/**
* Comme {@link xml()} mais les valeurs ne sont pas mises en échappement par
* défaut.
*/
function string(?string $text, $data=null): string {
return $this->xml($text, $data, false);
}
/** Comme {@link xml()} pour un fichier au format LibreOffice Writer */
function odt(string $file, $data=null, $quote=true): void {
$zip = new ZipArchive();
$error = $zip->open($file);
if ($error !== true) {
throw new IOException("$file: error $error occured on open");
}
$oldContent = $zip->getFromName("content.xml");
if ($oldContent !== false) {
$newContent = $this->xml($oldContent, $data, $quote);
if ($newContent !== $oldContent) {
$zip->deleteName("content.xml");
$zip->addFromString("content.xml", $newContent);
}
}
$oldStyles = $zip->getFromName("styles.xml");
if ($oldStyles !== false) {
$newStyles = $this->xml($oldStyles, null, $quote);
if ($newStyles != $oldStyles) {
$zip->deleteName("styles.xml");
$zip->addFromString("styles.xml", $newStyles);
}
}
$zip->close();
}
}