205 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			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();
 | |
|   }
 | |
| }
 |