ugrás a tartalomhoz

Sablonkezelés PHP-vel

inf · 2009. Szep. 29. (K), 10.58
Sablonkezelés PHP-vel
Jelen cikkben a sablonnyelvekről, illetve a sablonkezelő rendszerek felépítéséről szeretnék leírást adni. Az utóbbi néhány hónapban sokat foglalkoztam egy saját sablonkezelő rendszer fejlesztésével, most pedig megpróbálom összefogni az így szerzett tapasztalataimat. Próbáltam nagyon az alapoktól elindulni – és hogy minél közérthetőbb legyen a dolog – egy már sokak számára jól ismert leíró nyelvet, az XML-t választottam a példákban bemutatott sablonnyelv alapjának.

Mi a sablon?

Úgy gondoltam az elején érdemes tisztázni a sablon fogalmát. Ezt egzakt definíció helyett inkább a saját megközelítésemmel teszem. A sablonok lényege, hogy képesek a bejövő adatokat az általuk leírt formában kimenetté (HTML kód, SQL kérés stb.) alakítani. Általában több paraméter van (bejövő adat), és csak egy kimenet. A sablonokat széles körben használják olyan területeken, ahol adatokat kell szöveges formára hozni, és/vagy a sablon vagy az adatok nem megbízható forrásból jönnek.

Mikor indokolt a sablonnyelvek használata?

A sablonnyelvekkel kapcsolatban (például a Smartynál) olvastam, hogy a megjelenítés szétválasztása az üzleti logikától az elsődleges feladatuk. Erre a célra viszont maga a PHP is teljesen alkalmas, mindössze annyi kell tenni, hogy a megjelenítéssel kapcsolatos dolgokat ki kell emelni külön fájlba.

A sablonnyelvek használatát egyedül biztonsági szempontok indokolják. Például bbcode esetén a HTML tagek kiszűrése, Smarty esetén a PHP kód kiszűrése, SQL statementek esetén a küldött adat egyszerű escape-elése az igazi cél.

Mindez úgy valósul meg, hogy a sablonnak a programozó egy felületet ad meg, ami elhatárolja azt a program többi részétől. Ez a felület gyakorlatilag a bejövő paraméterek listája, és a használható függvények listája. Az előbbieket a változók sablonhoz rendelésével lehet szabályozni, az utóbbiakat pedig a sablon PHP kódra történő fordítása esetén lehet megadni.

A sablonkezelők felépítése

Egy sablonkezelő több részből áll, és nyilván az az ésszerű, ha az egyes feladatokat külön osztályok látják el.

A sablonkezelő funkciói:
  • A fordítást a compiler (fordító) végzi, ami feldarabolja a sablont úgynevezett tokenekre, és az egyes tokeneket az azoknak megfelelő kódra fordítja le. A fordító sebessége leginkább akkor számít, ha a sablont minden egyes megjelenítéskor újrafordítjuk. Ehhez a feladatkörhöz szorosan kapcsolódik a felületi függvények, és esetleg a felületi változók szűrése is.
  • Az előkészítés a nem kívánatos kódok kiszűrésére használatos. Szóval mielőtt PHP-ra fordítanánk, célszerű kivédeni a PHP kódot a sablonból, különben az a kiértékelés során ugyanúgy lefut majd, mint a fordított kód.
  • A PHP kódot célszerű lementeni, és a következőkben újra felhasználni, így megspórolhatjuk a fordítás idejét. Ezzel a módszerrel (compiler cache) gyakorlatilag az include-dal azonos sebességű lesz a sablon kiértékelése.
  • A fordítás során kapott kód kiértékelése a felületi változók kontextusában zajlik. A kapott adatot szintén cache-elni kell, amivel rengeteg időt lehet megtakarítani. Ez a fajta megoldás a sokszor lekért oldalaknál lehet hasznos.

Az én megközelítésemben az előkészítést és a fordítást a Compiler osztály a kiértékelést és a gyorstárazást pedig a Template osztály végzi.

A fordító osztály

A cikk témája elsősorban a fordító osztály, mivel a bonyolultabb munkát ez végzi. Compilert írni elég könnyű feladat. A hagyományos módszer, hogy feldarabolják a sablont egy tömbre, és a tömbön iterálva értékelik ki az egyes részeket: tokeneket. Gyors compilert írni már egy fokkal bonyolultabb feladat. Ehhez a PHP preg motorja biztosít egy elég hatékony megoldást, a rekurzív regexet.

A rekurzív regex

Hagyományosan a reguláris nyelvek használatával nem lehetne lefedni a sablonnyelveket, mivel a sablonfüggvények blokkjaiban tetszőleges számmal lehetnek beágyazott függvények. Egy függvénynek nyilván van blokkja, aminek van nyitó és záró része, a sima regex nem képes összepárosítani ezeket részeket, így előfordulhat vele, hogy az adott blokk nyitását egy másik blokk záró részével párosítja. Emiatt kénytelen az ember a nyitó és záró részeket külön kezelni. A hagyományos nyitó, illetve záró tokenekre darabolás és ezek PHP-val történő összepárosítása egy elég lassú compilert eredményez.

A rekurzív regex használatával a nyitó és záró részeket a PCRE motor, és nem az általunk írt függvények párosítják össze, így sokkal gyorsabb kódot kapunk.

A rekurzív regex használata a következő speciális mintával történik:
(?ismétlődő_gyűjtő_indexe)
Az ismétlődő gyűjtő indexe lehet, hogy több magyarázatra szorul. Egy találatnál a teljes találatot tartalmazó gyűjtő indexe: 0. A preg_match-nél kapott tömbben is a 0 helyen a teljes találat áll. Ezeket az indexeket a kapott értékek ismétlésére is fel lehet használni.

Például:

function traceHTML($object,$color='black')
{
	echo '<pre style="color: '.$color.'">'.htmlentities(var_export($object,true),ENT_NOQUOTES,'utf-8').'</pre>';
}

$pattern='%(\d+)\.\1%';
$test='A HUF-USD árfolyam: 192.192 a HUF-EUR pedig 272.802.';
preg_match_all($pattern,$test,$matches,PREG_SET_ORDER);
traceHTML($matches,'blue');

array (
	0 => array (
		0 => '192.192',
		1 => '192',
	),
)
A traceHTML körülbelül a var_dump színezve. A többi példában is ezt használom változók kiírására. Az utána következő rész pedig mindig a szöveg, amit kiír.

A fenti példában a 192-t az első csoport fogja el. Ennek az értékét ismételjük meg a pont után a \1 mintával. Ha a 192-t elfogó mintát akarjuk megismételni, akkor az többek között rekurzív regexszel lehetséges.

$pattern='%(\d+)\.(?1)%';
$test='A HUF-USD árfolyam: 192.192 a HUF-EUR pedig 272.802.';
preg_match_all($pattern,$test,$matches,PREG_SET_ORDER);
traceHTML($matches,'blue');

array (
	0 => array (
		0 => '192.192',
		1 => '192',
	),
	1 => array (
		0 => '272.802',
		1 => '272',
	),
)
Itt a regex már a 272.802-t is elfogja, mert nem maga az érték ismétlődik a pont után, hanem a \d+ minta. Ez a fajta megközelítés sokkal bonyolultabb ismétlődéseknél is hatékony.

Saját sablonnyelv készítése

A saját sablonnyelv megírásához először tisztában kell lenni egy ilyen nyelv felépítésével. Mindössze néhány dolog elég egy kezdetleges nyelv megalkotásához:

  • A szöveg elkülönítése a nyelvi elemektől
  • A változók és függvények jelölésének elkülönítése
  • A paraméter lista formájának meghatározása
  • A blokk formájának meghatározása

Ezen kívül még vannak extra dolgok, úgy, mint
  • Szöveg és szám típusú változók kezelése
  • Különböző paraméter lista típusok
  • Kommentezés
  • Változók egyszerűsített kiírása
  • Módosítók a változókhoz

A cikkben inkább csak egy kezdetleges nyelv bemutatása a célom, nem pedig egy teljes sablonkezelő rendszer írása. Mint már az elején írtam, az XML megadási módját vettem a példa nyelv alapjául.

Az XML-re illeszkedő kifejezés

Az XML-hez szükséges fordító megalkotásában a legbonyolultabb a regex megírása. Ehhez ad egy kis mankót, ha tudjuk, hogy milyen a nyelv felépítése. Annyiból szerencsés választás az XML, hogy nem kell a szomszédba menni példákért, hiszen az XHTML alapja is ez a leíró nyelv. Az XML fájlok első sorához tartozó fejlécre most nem lesz szükség, szóval azt hagyjuk el. Nézzünk egy példát:

<form action="form_action.php" method="get">
  First name: <input type="text" name="fname" /><br />
  Last name: <input type="text" name="lname" /><br />
  <input type="submit" value="Submit" />
</form>
Ebből már nagyjából nyilvánvaló, hogy milyen részekből kell állnia a mintának:
  • Van egy token nyitó és egy záró rész: < és >
  • A függvények nevei egy szavasak: \w+
  • A paraméter lista a \w+=”\w+” rész ismétlődéséből áll össze
  • A függvényt, ha egy soros, akkor az > előtti / zárja le
  • Több soros függvények esetében a blokk utáni </függvény_neve> a blokk záró rész

A regex kidolgozásánál fontosnak tartom, hogy átlátható legyen a kód, ezért szerintem úgy érdemes nekiállni fejleszteni, hogy feldaraboljuk a mintát változókra, aztán a változókat dolgozzuk ki. Kezdetnek elég egy sablonos regex is:

$token_regex=
	'%'.
		'<'.
			'függvény_neve'.
			'paraméter_lista'.
	'(?:'.
			'/'.
	'|'.
		'>'.
			'blokk'.
		'<'.
			'/'.'függvény_neve'.
	')'.
		'>'.
	'%usD';
Ezen gyakorlatilag minden szerepel, amit eddig az XML felépítéséről megbeszéltünk. Az usD módosítók sorban: unicode; több soros; $ csak a végén illeszkedik. Először írjuk át változókra a megfelelő részeket. A függvény nevénél gyűjtőt kell használni, mert blokkal rendelkező függvénynél a záró részben a gyűjtő tartalma fog állni. Én személy szerint jobban szeretem a nevesített gyűjtőket, mert igaz, hogy lassabb lesz vele a program, de sokkal könnyebben bővíthető, és sokkal átláthatóbb. A módosítások elvégzése után a következőt kapjuk:

$token_open='<';
$token_close='>';
$function_name='\w+';
$arguments;
$block;
$function_close='/';

$token_regex=
	'%'.
		$token_open.
			'(?<function>'.$function_name.')'.
			$arguments.
	'(?:'.
			$function_close.
	'|'.
		$token_close.
			$block.
		$token_open.
			$function_close.'(?P=function)'.
	')'.
		$token_close.
	'%usD';
Ez már egy jó kiindulási alap az illeszkedő minta kidolgozásához. Előrevetítem, hogy érdemes ezeket a változókat és a mintának az összeállítását egy osztályba szervezni. Ez az osztály példányosítja majd compilerhez szükséges regex beállításokat, ezért legyen a neve CompilerConfig. :-)

Sablonkezelő osztályok

Itt ragadom meg az alkalmat, hogy ejtsek néhány szót a többi sablonkezelő osztályról is.

  • A CompilerConfig tartalmazza a fordításhoz szükséges mintákat
  • A Compiler végzi a fordítást a konfigja és a pluginek alapján
  • A CompilerPluginX egy adott függvényt testesít meg a fordításhoz (X az adott függvény neve), tehát a paraméterek és a blokk alapján ez generálja az adott függvényhez tartozó PHP kódot
  • A Template végzi a fájlok és a config betöltését, valamint a cache-elést és a kiértékelést

Nekem ez a szereposztás egészen jól megfelelt, de természetesen össze lehet vonni a Compiler osztályokat, és szét lehet darabolni a Template-et. Nekem ez a felosztás azért vált be, mert így a sablonnyelv alakja viszonylag szabadon módosítható a CompilerConfig segítségével, ha a nyelv logikáját kell módosítani, akkor pedig a Compiler bizonyos részeinek az átírásával. A függvények külön osztályokba (és fájlokba) tagolása azt eredményezi, hogy csak akkor töltődnek be, amikor szükség van rájuk. Nyilván a Compilert is érdemes emiatt külön fájlba tenni.

Nos tehát a kezdetleges CompilerConfig így néz ki:

class CompilerConfig
{
	protected $token_open='<';
	protected $token_close='>';
	protected $function_name='\w+';
	protected $function_close='/';

	public $token_regex;

	public function __construct()
	{
		$arguments;
		$block;
		$this->token_regex=
			'%'.
				$this->token_open.
					'(?<function>'.$this->function_name.')'.
					$arguments.
			'(?:'.
				$this->token_close.
					$block.
				$this->token_open.
					$this->function_close.'(?P=function)'.
			'|'.
					$this->function_close.
			')'.
				$this->token_close.
			'%usD';
	}
}
Ezt fejlesszük tovább a paraméterek és a blokk kidolgozásával. A paraméterek listája a \w+=”\w+” ismétlődő egységekből épül fel, szóval elég egyszerűen össze lehet dobni. A blokknál a rekurzió alapjának a teljes token regexet vesszük. A függvények közötti rész tetszőleges stringgel kitölthető, viszont csak lusta kvantorokkal érhető el a kívánt hatás. Ha mohó regexet használunk, akkor a tetszőleges string átugorhatja a blokk zárását, és egy másik azonos nevű blokk zárásánál illeszkedne.

class CompilerConfig
{
	protected $token_open='<';
	protected $token_close='>';
	protected $function_name='\w+';
	protected $function_close='/';
	
	protected $arguments_separator='\s';
	protected $arguments_operator='=';
	protected $argument_name='\w+';
	protected $variable_open='"';
	protected $variable_close='"';
	protected $variable_name='\w+';

	public $token_regex;
	
	public function __construct()
	{
		$arguments=
			'(?:'.
				$this->arguments_separator.
					$this->argument_name.
				$this->arguments_operator.
					$this->variable_open.
						$this->variable_name.
					$this->variable_close.
			')*';
			
		$block=
			'.*?(?:(?0).*?)*?';
		
		$this->token_regex=
			'%'.
				$this->token_open.
					'(?<function>'.$this->function_name.')'.
					'(?<arguments>'.$arguments.')'.
			'(?:'.
				$this->token_close.
					'(?<block>'.$block.')'.
				$this->token_open.
					$this->function_close.'(?P=function)'.
			'|'.
					$this->function_close.
			')'.
				$this->token_close.
			'%usD';
	}
}
Most teszteljük a token regexet és írjunk egy kezdetleges Compilert. Egyelőre egy szimpla preg_match_all elég lesz. A kód megírásánál érdemes szem előtt tartani az örökölhetőséget, nekem legalábbis az a koncepcióm, hogy a pluginek gyakorlatilag specifikus függvényekkel bővített compilerek. Térjünk most vissza az első XHTML-es példánkhoz. A form action értékét módosítottam, hogy a variable_name mintája (\w+) illeszkedjen rá.

class Compiler 
{
	protected $config;
	
	public function __construct()
	{
		$this->config=new CompilerConfig;
	}
	
	public function compile(&$template)
	{
		$block=preg_replace('/(<\?|\?>|<%|%>)/sDu','<?php echo \'\1\';?>',$template);
		return $this->compile_block($block);
	}

	protected function compile_block(&$block)
	{
		traceHTML($block,'red');
		preg_match_all($this->config->token_regex,$block,$matches,PREG_SET_ORDER);
		traceHTML($matches,'blue');
		return $block;
	}
}

$test='<form action="test" method="get">
  First name: <input type="text" name="fname" /><br />
  Last name: <input type="text" name="lname" /><br />
  <input type="submit" value="Submit" />
</form>';
$compiler=new Compiler;
traceHTML($compiler->compile($test),'green');
A $matches tartalma pedig:
array (
  0 => array (
    0 => '<form action="action" method="get">
  First name: <input type="text" name="fname" /><br />
  Last name: <input type="text" name="lname" /><br />
  <input type="submit" value="Submit" />
</form>',

    'function' => 'form',
	
    'arguments' => ' action="action" method="get"',
	
    'block' => '
  First name: <input type="text" name="fname" /><br />
  Last name: <input type="text" name="lname" /><br />
  <input type="submit" value="Submit" />
',
  ),
)
Tehát a token regex tökéletesen működik. Bár egyelőre ez nem 100%-os bizonyíték. A további fejlesztésnek arra kell irányulnia, hogy ez teljes bizonyossággal kiderüljön. Ehhez az űrlapba ágyazott input és br függvényekre is le kell futtatni a preg match-et. Mivel ezek ugyanúgy pluginek, mint a form volt, ezért érdemes foglalkozni kicsit a plugin beszúró résszel is.

class Compiler 
{
	protected $config;
	
	public function __construct()
	{
		$this->config=new CompilerConfig;
	}
	
	public function compile(&$template)
	{
		$block=preg_replace('/(<\?|\?>|<%|%>)/sDu','<?php echo \'\1\';?>',$template);
		return $this->compile_block($block);
	}

	protected function compile_block(&$block)
	{
		if (!isset($block))
		{
			return;
}
		preg_match_all($this->config->token_regex,$block,$matches,PREG_SET_ORDER);
		foreach ($matches as &$match)
		{
			$_function=$match['function'];
			$_arguments=$match['arguments'];
			//a blokk a gyűjtő feletti | miatt nem mindig kap értéket
			$_block=isset($match['block'])?$match['block']:null;
			$this->insert_plugin($_function,$_arguments,$_block);
		}
		return $block;
	}
	
	protected function insert_plugin(&$_function,&$_arguments,&$_block)
	{
		traceHTML($_function,'red');
		//a plugin class-hez hozzá kell heggeszteni majd a functiont.
		$plugin_class='Plugin';
		$plugin=new $plugin_class($this->config);
		return $plugin->compile($_arguments,$_block);
	}
}

class Plugin extends Compiler
{
	public function __construct($config)
	{
		$this->config=$config;
	}
	
	public function compile(&$arguments,&$block)
	{
		traceHTML($arguments,'blue');
		//ide jöhetnek majd függvény sepcifikus dolgok
		return $this->compile_block($block);
	}
}

$test='<form action="test" method="get">
  First name: <input type="text" name="fname" /><br />
  Last name: <input type="text" name="lname" /><br />
  <input type="submit" value="Submit" />
</form>';
$compiler=new Compiler;
traceHTML($compiler->compile($test),'green');
A trace-elt piros–kék kimeneten most már jól látszik, hogy tökéletesen működik a token feldolgozó regex. Mivel a feldolgozott blokk részre szükség van a pluginek kiértékelésénél, ezért a két token közötti stringeket is össze kell szedni valahogyan. Ez egy elég nehéz dolog. Én az alábbi lehetőségeket vettem számba:

  • Az egyik a preg_replace függvény használata. Ez a megoldás működőképes, viszont gátolja a compiler továbbfejlesztését, ugyanis elágazó függvények esetében nem ad lehetőséget a blokk adott részeinek feldarabolására. Mondok egy példát, hogy érthetőbb legyen.
    
    <if subject="stuff">
    A stuffnak adtak értéket.
    <else />
    Nincs érték beállítva.
    </if>
    
    Itt a blokkot fel kell darabolni két részre. Az egyik az if-hez, a másik az else ághoz tartozik. A feldarabolás helyét nyilván az else függvény hívása jelöli, és nyilván csak a compile_block futtatásakor derül ki, hogy ez pontosan hol van.
  • A második megoldás a regex módosítása úgy, hogy a token előtti stringet is gyűjtse. Ezzel próbálkoztam, mert ez ígérkezett a leggyorsabb módszernek, viszont a PCRE motor hibája miatt sajnos ezen az úton nem megoldható a dolog.
  • Próbálkoztam még a preg_split használatával, ami önmagában nem tudja megfelelően kezelni a gyűjtött csoportokat.
  • A preg_match_all és az offset capture kombinációja viszont alkalmas a köztes stringek substr-rel történő kiemelésére. A dolog mindössze annyiból problémás, hogy rengeteg substr hívással jár, és az offset capture is egy csomó tömböt létrehoz feleslegesen, szóval magában elég lassúnak bizonyult.
  • Emiatt végül a preg_match_all és preg_split kombinációja mellett döntöttem. A preg_split a szöveget emeli ki, a preg_match_all pedig (ahogy eddig is) a tokeneket. Ez a megoldás körülbelül kétszer olyan gyors, mint az offset capture, és nem igényel bonyolult módosításokat az eddigi struktúrán.

A stringek összegyűjtésével gyakorlatilag már semmi akadálya nincsen annak, hogy a compile függvény kimenetet adjon vissza és ne az eredeti blokkot. Egyelőre elég, ha csak a függvények neveit fűzzük össze a köztes szöveggel. Ehhez ideiglenesen (a pluginek kidolgozásáig) módosíthatjuk az insert_plugin-t, hogy adja át a függvények neveit is.

class Compiler 
{
	protected $config;
	
	public function __construct()
	{
		$this->config=new CompilerConfig;
	}
	
	public function compile(&$template)
	{
		$block=preg_replace('/(<\?|\?>|<%|%>)/sDu','<?php echo \'\1\';?>',$template);
		return $this->compile_block($block);
	}

	protected function compile_block(&$block)
	{
		if (!isset($block))
		{
			return;
		}
		preg_match_all($this->config->token_regex,$block,$matches,PREG_SET_ORDER);
		$output='';
		$texts=preg_split($this->config->token_regex,$block);
		foreach ($matches as $pointer => &$match)
		{
			$_text=$texts[$pointer];
			$_function=$match['function'];
			$_arguments=$match['arguments'];
			//a blokk a gyűjtő feletti | miatt nem mindig kap értéket
			$_block=isset($match['block'])?$match['block']:null;
			$output.=$_text.$this->insert_plugin($_function,$_arguments,$_block);
		}
		if (count($texts)>count($matches))
		{
			$output.=end($texts);
		}
		return $output;
	}
	
	protected function insert_plugin(&$_function,&$_arguments,&$_block)
	{
		//a plugin class-hez hozzá kell heggeszteni majd a functiont.
		$plugin_class='Plugin';
		$plugin=new $plugin_class($this->config);
		return $plugin->compile($_arguments,$_block,$_function);
	}
}

class Plugin extends Compiler
{
	public function __construct($config)
	{
		$this->config=$config;
	}
	
	public function compile(&$arguments,&$block,&$function)
	{
		//ide jöhetnek majd függvény sepcifikus dolgok
		return $function.'{'.$this->compile_block($block).'}';
	}
}

$test='<form action="test" method="get">
  First name: <input type="text" name="fname" /><br />
  Last name: <input type="text" name="lname" /><br />
  <input type="submit" value="Submit" />
</form>';
$compiler=new Compiler;
traceHTML($compiler->compile($test),'green');
A kimenet így a következő lesz:
'form{
  First name: input{}br{}
  Last name: input{}br{}
  input{}
}'
Most nézzük meg a paraméter listát. Ahhoz, hogy egy tömböt tudjunk átadni a feldolgozónak szükség lesz egy regexre, ami segít feldarabolni az arguments változót. Ehhez módosítani kell a konfigot.

class CompilerConfig
{
	protected $token_open='<';
	protected $token_close='>';
	protected $function_name='\w+';
	protected $function_close='/';
	
	protected $arguments_separator='\s';
	protected $arguments_operator='=';
	protected $argument_name='\w+';
	protected $variable_open='"';
	protected $variable_close='"';
	protected $variable_name='\w+';

	public $token_regex;
	public $arguments_parser_regex;
	
	public function __construct()
	{
		$arguments=
			'(?:'.
				$this->arguments_separator.
					$this->argument_name.
				$this->arguments_operator.
					$this->variable_open.
						$this->variable_name.
					$this->variable_close.
			')*';
			
		$this->arguments_parser_regex=
			'%'.
				$this->arguments_separator.
					'(?<argument>'.$this->argument_name.')'.
				$this->arguments_operator.
					$this->variable_open.
						'(?<variable>'.$this->variable_name.')'.
					$this->variable_close.
			'%usD';
			
		$block=
			'.*?(?:(?0).*?)*?';
		
		$this->token_regex=
			'%'.
				$this->token_open.
					'(?<function>'.$this->function_name.')'.
					'(?<arguments>'.$arguments.')'.
			'(?:'.
				$this->token_close.
					'(?<block>'.$block.')'.
				$this->token_open.
					$this->function_close.'(?P=function)'.
			'|'.
					'\s'.$this->function_close.
			')'.
				$this->token_close.
			'%usD';
	}
}
Nem kell túl sokat gondolkodni rajta, hogy milyen legyen az új regex, hiszen gyakorlatilag az argumentshez tartozó egységben kell gyűjtőket elhelyezni. A feldolgozó rész sem túl bonyolult, a preg_match_all-lal ki kell nyerni a kulcs–érték párokat, aztán a változókat PHP kóddá alakítani.

class Compiler 
{
	protected $config;
	
	public function __construct()
	{
		$this->config=new CompilerConfig;
	}
	
	public function compile(&$template)
	{
		$block=preg_replace('/(<\?|\?>|<%|%>)/sDu','<?php echo \'\1\';?>',$template);
		return $this->compile_block($block);
	}

	protected function compile_arguments(&$arguments)
	{
		preg_match_all($this->config->arguments_parser_regex,$arguments,$matches,PREG_SET_ORDER);
		$result=array();
		foreach($matches as &$match)
		{
			$result[$match['argument']]='$this->arguments[\''.$match['variable'].'\']';
		}
		return $result;
	}	

	protected function compile_block(&$block)
	{
		if (!isset($block))
		{
			return;
		}
		preg_match_all($this->config->token_regex,$block,$matches,PREG_SET_ORDER);
		$output='';
		$texts=preg_split($this->config->token_regex,$block);
		foreach ($matches as $pointer => &$match)
		{
			$_text=$texts[$pointer];
			$_function=$match['function'];
			$_arguments=$this->compile_arguments($match['arguments']);
			//a blokk a gyűjtő feletti | miatt nem mindig kap értéket
			$_block=isset($match['block'])?$match['block']:null;
			$output.=$_text.$this->insert_plugin($_function,$_arguments,$_block);
		}
		if (count($texts)>count($matches))
		{
			$output.=end($texts);
		}
		return $output;
	}
	
	protected function insert_plugin(&$_function,&$_arguments,&$_block)
	{
		//a plugin class-hez hozzá kell heggeszteni majd a functiont.
		$plugin_class='Plugin';
		$plugin=new $plugin_class($this->config);
		return $plugin->compile($_arguments,$_block,$_function);
	}
}

class Plugin extends Compiler
{
	public function __construct($config)
	{
		$this->config=$config;
	}
	
	public function compile(&$arguments,&$block,&$function)
	{
		//ide jöhetnek majd függvény sepcifikus dolgok
		return $function.'('.str_replace("\n",'',var_export($arguments,true)).'){'.$this->compile_block($block).'}';
	}
}

$test='<form action="test" method="get">
  First name: <input type="text" name="fname" /><br />
  Last name: <input type="text" name="lname" /><br />
  <input type="submit" value="Submit" />
</form>';
$compiler=new Compiler;
traceHTML($compiler->compile($test),'green');
Innentől már nem bonyolult a dolog, csak a plugineket kell megírni úgy, hogy a megfelelő kódot adják vissza. Nyilván minden függvényhez külön bővítmény osztály tartozik majd, ami generálja a megfelelő PHP kódot. A kapott kódot pedig fájlba mentve lehet include-olni, vagy egy eval-lal kiértékelni.

Egy teljes sablonkezelő rendszer kidolgozása elég hosszadalmas lenne, és nem is célja ennek a cikknek, szóval egyelőre csak egy plugint, a foreach-et dolgozom ki.

A sablonnyelvünkkel van egy aprócska probléma; egyelőre a HTML kódját is megpróbálja befordítani. Ezen egy negatív elővizsgálattal könnyedén túlléphetünk.

class CompilerConfig
{
	protected $token_open='<';
	protected $token_close='>';
	protected $function_name='\w+';
	protected $function_close='/';
	
	protected $arguments_separator='\s';
	protected $arguments_operator='=';
	protected $argument_name='\w+';
	protected $variable_open='"';
	protected $variable_close='"';
	protected $variable_name='\w+';

	public $token_regex;
	public $arguments_parser_regex;
	
	public function __construct()
	{
		$arguments=
			'(?:'.
				$this->arguments_separator.
					$this->argument_name.
				$this->arguments_operator.
					$this->variable_open.
						$this->variable_name.
					$this->variable_close.
			')*';
			
		$this->arguments_parser_regex=
			'%'.
				$this->arguments_separator.
					'(?<argument>'.$this->argument_name.')'.
				$this->arguments_operator.
					$this->variable_open.
						'(?<variable>'.$this->variable_name.')'.
					$this->variable_close.
			'%usD';
			
		$block=
			'.*?(?:(?0).*?)*?';
		
		$this->token_regex=
			'%'.
				$this->token_open.
					'(?<function>'.
						'(?!table|thead|tbody|tr|td|br)'.
						'(?:'.$this->function_name.')'.
					')'.
					'(?<arguments>'.$arguments.')'.
			'(?:'.
				$this->token_close.
					'(?<block>'.$block.')'.
				$this->token_open.
					$this->function_close.'(?P=function)'.
			'|'.
					'\s'.$this->function_close.
			')'.
				$this->token_close.
			'%usD';
	}
}
A következő és egyben utolsó példához elég csak néhány taget kihagyni a vizsgálatból. A lényeg, hogy egy tömb tartalmát szeretném kiíratni táblázatba.

Először az insert_plugin-es ideiglenes megoldást kell megszűntetni, helyette a compile egy compile_function nevű függvényt fog hívni, ami a PHP kódot generálja. A tömb tartalmának kiírásához szükség van még egy foreach-re, és egy echo-ra. Ez utóbbinak a display nevet adtam.

class Compiler 
{
	protected $config;
	
	public function __construct()
	{
		$this->config=new CompilerConfig;
	}
	
	public function compile(&$template)
	{
		$block=preg_replace('/(<\?|\?>|<%|%>)/sDu','<?php echo \'\1\';?>',$template);
		return $this->compile_block($block);
	}

	protected function compile_arguments(&$arguments)
	{
		preg_match_all($this->config->arguments_parser_regex,$arguments,$matches,PREG_SET_ORDER);
		$result=array();
		foreach($matches as &$match)
		{
			$result[$match['argument']]='$this->arguments[\''.$match['variable'].'\']';
		}
		return $result;
	}
	
	protected function compile_block(&$block)
	{
		if (!isset($block))
		{
			return;
		}
		preg_match_all($this->config->token_regex,$block,$matches,PREG_SET_ORDER);
		$output='';
		$texts=preg_split($this->config->token_regex,$block);
		foreach ($matches as $pointer => &$match)
		{
			$_text=$texts[$pointer];
			$_function=$match['function'];
			$_arguments=$this->compile_arguments($match['arguments']);
			//a blokk a gyűjtő feletti | miatt nem mindig kap értéket
			$_block=isset($match['block'])?$match['block']:null;
			$output.=$_text.$this->insert_plugin($_function,$_arguments,$_block);
		}
		if (count($texts)>count($matches))
		{
			$output.=end($texts);
		}
		return $output;
	}
	
	protected function insert_plugin(&$_function,&$_arguments,&$_block)
	{
		//a plugin class-hez hozzá kell heggeszteni majd a functiont.
		$plugin_class=ucwords(strtolower($_function)).'Plugin';
		$plugin=new $plugin_class($this->config);
		return $plugin->compile($_arguments,$_block);
	}
}

abstract class Plugin extends Compiler
{
	public function __construct($config)
	{
		$this->config=$config;
	}
	
	public function compile(&$arguments,&$block)
	{
		return $this->compile_function($arguments,$this->compile_block($block));
	}
	
	abstract protected function compile_function(&$arguments,&$block);
}

class DisplayPlugin extends Plugin
{
	public function compile_function(&$arguments,&$block)
	{
		return '<?php echo('.$arguments['var'].');?>';
	}
}

class ForeachPlugin extends Plugin
{
	public function compile_function(&$arguments,&$block)
	{
		return
			'<?php '.
				'foreach ('.
					$arguments['subject'].' as '.
					(isset($arguments['key'])?($arguments['key'].' => '):'').
					$arguments['value'].
				')'.
				'{'.
			'?>'.
					$block.
			'<?php '.
				'}'.
			'?>';
	}
}
Ezzel az alap Compiler elkészült, de szükség van még egy Template osztályra, ami a megjelenítést végzi. A Template osztálynak a kiértékelésen kívül most mindössze a sablonváltozók felvétele a feladata.

class Template
{
	protected $compiler;
	protected $arguments=array();
	
	public function __construct()
	{
		$this->compiler=new Compiler;
	}
	
	public function assign($key,$value)
	{
		$this->arguments[$key]=$value;
		return $this;
	}
	
	public function display($template)
	{
		eval('?>'.$this->compiler->compile($template));
		return $this;
	}
}


$template=new Template;
$template->assign('planets',array(
	'1' => 'Mercur',
	'2' => 'Venus',
	'3' => 'Earth',
	'4' => 'Mars',
	'5' => 'Jupiter',
	'6' => 'Saturn',
	'7' => 'Uranus',
	'8' => 'Neptune',
	'9' => 'Pluto'
));
$template->display(
	'<table border="1">
		<thead>
			<tr><td>#</td><td>Name</td></tr>
		</thead>
		<tbody>
		<foreach subject="planets" key="index" value="planet">
			<tr><td><display var="index" /></td><td><display var="planet" /></td></tr>
		</foreach>
		</tbody>
	</table>'
);
Ezt a fajta megközelítést alapnak szántam saját rendszerek kidolgozásához. A cikkben igyekeztem olyan módszereket alkalmazni, olyan osztályokat írni, amikre könnyen lehet építkezni, remélem sikerült néhány gondolatébresztő ötletet átadnom. :-)
 
1

Gratula

janoszen · 2009. Szep. 29. (K), 19.06
Elsőre csak gyorsan átfutottam, gratulálni tudok csak. Bár régen gondolkozom azon, hogy egy nem reguláris nyelveken alapuló elemzőt kellene írni, de mind a mai napig csak gondolat maradt. Azt hiszem, később még egy alaposabb olvasás erejéig előveszem.
2

Kösz

inf · 2009. Szep. 29. (K), 20.09
Elsőre csak gyorsan átfutottam, gratulálni tudok csak.
Köszi! :-)

Bár régen gondolkozom azon, hogy egy nem reguláris nyelveken alapuló elemzőt kellene írni, de mind a mai napig csak gondolat maradt.
Hogyan indulnál neki regex nélkül?
3

Tokenizálás?

Adam · 2009. Szep. 29. (K), 23.07
Tokenizálás? Bár nem hinném, hogy gyorsabb lenne…
4

gyorsaság

winston · 2009. Szep. 30. (Sze), 09.23
A gyorsaság nem is annyira szempont, hisz egy tisztességes sablonrendszer első futásnál nyers php-re fordul, és azt lecacheli, így változásig/adott ideig csak egyszer kell forduljon
5

Nem feltétlen

inf · 2009. Szep. 30. (Sze), 14.58
Ez így nem teljesen igaz.

A hagyományos értelemben vett sablonoknál a sablon paraméterei, amik gyakran változnak, és maga a sablon nagyjából állandó. Szóval mondjuk a db lekérésből jönnek a paraméterek, és azokat helyezik be a sablonba. Ebben az esetben egyszer php-ra fordítod a sablont, és kész.

Ezen kívül viszont sablonkezelőket lehet alkalmazni bejövő adatok validálására is. bbcode esetében például a sablon az, ami gyakran változik, paraméterek pedig nincsenek is hozzá. Ilyenkor mondjuk jól jön ha gyors a compiler. Majd lehet, hogy erről is írok cikket, egyelőre gyakorlatban még nem használtam ilyen compilert validálásra, de fejben már megvan, hogy hogy fog kinézni.
9

html sablon

winston · 2009. Okt. 2. (P), 22.57
Kicsit szűklátókörű volt a válaszom (igen, még nem olvastam végig a cikket, és rögtön szigorúan a html kimenet sablonozására asszociáltam), a megállapításom szigorúan a smarty jellegű sablonozásra érvényes. Ennek az is az oka, hogy leginkább ezen dolgoztam, az egyéb (sql, stb.) sablonozásokat leginkább külső komponensekre bíztuk, ezért abban nincs nagy tapasztalatom.
11

semmi gond

inf · 2009. Okt. 3. (Szo), 00.34
Semmi gond, elárulom, hogy a Smarty féle sablonozáson kívül nekem sincs valami nagy tapasztalatom ilyesmivel. Nyilván tervben van, hogy hasonló alapon építek egy bbcode compilert, de ez egyelőre még nem valósult meg. Azt hiszem a következő cikkem erről fog szólni, hogy kicsit tágítsa a sablonozással kapcsolatos ismereteinket.
6

C-ben...

janoszen · 2009. Szep. 30. (Sze), 19.28
Egyszer láttam egy C-ben írt template rendszert, ami még a nyers PHP-t is verte. Vicces volt látni, hogy valaki erre vette a fáradtságot.
7

:-)

inf · 2009. Szep. 30. (Sze), 19.41
Hát biztos sok ideje volt... :-)
8

xslt?

vbence · 2009. Okt. 2. (P), 11.49
XML alapú sablonnyelv esetén nekem az XSLT, mint XML transzformálására kitlált eszköz jutna eszembe. Kíváncsi lennék, miért döntöttél a PHP fordító mellett.
10

:-)

inf · 2009. Okt. 3. (Szo), 00.30
Hát, mint írtam, csak egy olyan példát kerestem, ami egyértelmű a közönségnek, hogy hogyan működik. XML esetében nem kell megtanulni senkinek egy új sablonnyelvet, ezért döntöttem mellette. Persze néhány plusz nehézség vele járt (mint mondjuk a HTML tagek kizárása), de nem okozott nagy gondot erre fordítót írni. :-)

XSLT-hez szintén XML formátumban kell lennie a sablonnak, így belegondolva abban is lehetne írni valami hasonlót. :-) Ha gondolod, te is megírhatod a cikked az XSLT-s megoldásokról, engem érdekelne.
12

Gratula!

fchris82 · 2009. Okt. 5. (H), 10.02
Gratulálok a cikkhez! Így kell ezt :) Gondolom rengeteget tanultál menet közben. Én 5 éve írtam egy XML alapú "form sablon kezelőt", amiben pl így néz ki egy login:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<form name="login" id="form_login" method="POST">
  <fieldset>
  <legend><t>Bejelentkezés</t></legend>
  <table style="width:auto;"><tr><td>
  <field name="email">
      <label><t>E-mail cím:</t></label> <input type="text" maxlength="100" require="yes" />
    <prefilter type="trim" />
    <validate type="maxlength">
      <error_msg><t>Túl hosszú a megadott e-mail cím!</t></error_msg>
    </validate>
    <validate type="email_test">
      <error_msg><t>Az e-mail cím formátuma nem stimmel!</t></error_msg>
    </validate>
  </field>
  <field name="pass">
      <label><t>Jelszó:</t></label> <input type="password" maxlength="30" require="yes" />
    <prefilter type="trim" />
    <validate type="require">
      <error_msg><t>Nem adott meg jelszót!</t></error_msg>
    </validate>
    <validate type="maxlength">
      <error_msg><t>Túl hosszú a megadott jelszó!</t></error_msg>
    </validate>
    <validate type="minlength" value="5">
      <error_msg><t>Túl rövid a jelszó!</t></error_msg>
    </validate>
  </field>
      <input type="submit" tvalue="yes" value="Mehet" />
  </td></tr></table>
  </fieldset>
</form>
Ez akkor azért kellett, mert rengeteg formot kellett létrehoznom, és a franc akarta mindig leprogramozni az adatok ellenőrzését, ráadásul így egy helyen lehetett látni, hogyan néz ki a form és melyik mezőnél milyen ellenőrzések futnak le. Ennél egyébként XML értelmező fv-ekkel oldottam meg a működést nem reguláris kifejezésekkel. Végül a symfony 1.2-ben is "hasonló" megoldás született. Vagy talán az 1.1-től ... :? Nem ez a lényeg.

Egyébként az előbbi projektnél még használtam a smarty-t, de idővel rájöttem, hogy csak a problémát okozza, mint sem segítené a munkát... Egyelőre nem látom azt a projektet, ahol márpedig igenis használni kellene egy sablon rendszert. Még egy "réteg", amivel foglalkozni kell, ahol hiba lehet, ráadásul a saját sablon rendszernek megvan az a hátránya, hogy nehéz kilépni utána egy munkából és átadni másnak, mert nincs ember, aki ismeri. Most hiába lehet gyorsan megtanulni egy ilyet, akkor is macera.

Ettől függetlenül nem gondolom, hogy feleslegesen dolgoztál vagy dolgoztam én, mert az ilyenekből sokat tanul az ember.
13

:-)

inf · 2009. Okt. 5. (H), 15.02
Köszi!
Én sem gondolom, hogy feleslegesen dolgoztam. Kb egy hónappal a beküldés előtt írtam egy betat ebből a cikkből, aztán 1 hónap után teljesen újrafogalmaztam az egészet, mert annyival jobban összeállt a kép, hogy mit is csinálok, meg hogyan lehetne lépésről-lépésre felépíteni egy ilyen programot. Szóval büszke vagyok rá. :-)

Az XML alapú sablonozás is jó ötlet, a legtöbb esetben ugyanazt meg lehet valósítani vele, mint egy sablonnyelvvel, legalábbis ha bizonyos adatok átalakításáról van szó.

Egyelőre nem látom azt a projektet, ahol márpedig igenis használni kellene egy sablon rendszert. Még egy "réteg", amivel foglalkozni kell, ahol hiba lehet, ráadásul a saját sablon rendszernek megvan az a hátránya, hogy nehéz kilépni utána egy munkából és átadni másnak, mert nincs ember, aki ismeri. Most hiába lehet gyorsan megtanulni egy ilyet, akkor is macera.

Sablonkezelő alkalmazását elsősorban biztonsági kérdések indokolhatják, másodsorban pedig a valamivel jobb átláthatóság. Természetesen mindent meg lehet csinálni php-val, amit sablonokkal. Smarty vagy ahhoz hasonló rendszerek használata akkor indokolt, ha nem szeretnéd, hogy a designer php kódot írjon, vagy ha online módosítható sablonú oldalt szeretnél gyártani. Ezek azért elég extrém esetek szerintem.

A legtöbb sablonkezelő nem használható elég dinamikusan, szóval mondjuk bbcode validálásra vagy hasonlókra nem alkalmas. Én igyekeztem egy olyan sablonkezelőt felvázolni, amiben a sablonnyelv szabadon módosítható, így mondjuk bbcode és smarty is alapszinten lekezelhető ugyanazzal a Compilerrel a Config osztály módosításával. Szóval szerintem a sablonozás nem csak és kizárólag a View rétegnél használható, hanem akár a Controller vagy a Model résznél is egészen eltérő feladatokra. Például egy sql statement is sablon ha belegondolsz. Akár sablon alapú ORM-et is lehetne csinálni, a nyelvenkénti eltérést pedig a Compiler osztály lecserélésével lehetne megoldani. stb... Rengeteg lehetőség van a sablonozás terén, amit még nem használt ki senki.
A validálást fix, hogy megcsinálom, mert egy működő motorra kb 5 perc megírni, abból valszeg lesz majd valamikor új cikk.
Az ORM-hez nem sok kedvem van, mert még nem látom át eléggé az sql nyelveket. Csak mysql-t ismerem, az meg szerintem nem elég. Meg egyébként is rengeteg meló.

(Egyébként ezeket hobbiból csinálom egy oldalhoz, szóval azért időm van rá, különben összedobtam volna már rég Symfony-ban. Programozás technikai szempontból viszont így sokkal többet fejlődtem, mintha keretrendszereket használnék.)
14

Sablon ORM

Szalbint · 2009. Nov. 7. (Szo), 19.35
Ez a sablon alapú ORM elképzelés alaposan megpörgette az agyamat.
Készítettem már SQL sablonnyelvet, ami jócskán túlmutat a bind lehetőségein, és egyáltalán nem bántam meg a rá fordított időt még akkor sem, ha a funkciók jó részét végül kiszedtem belőle a sebesség fenntartása miatt.

Ami az ORM-et illeti mindig arra jutottam, hogy php-ben valahogy nagyon nem jók a statikus nyelvekben használható megoldások, valahogy mindig csálé volt a dolog. A sablon gondolata még nem vetődött fel bennem pedig mekkora nagy ötlet.

Elköszönök, mert sürgős töprengeni valóm van. :)
15

:-D

inf · 2009. Nov. 8. (V), 13.45
Örülök, hogy legalább ötletet adtam. Nekem sajnos nincs meg az sql tudásom (csak a MySQL-t ismerem) meg az időm ahhoz, hogy ORMet fejlesszek.
16

Memória gondok

inf · 2009. Nov. 23. (H), 05.13
Szia!

Közben írtam egy komolyabb sablonkezelőt hasonló módon, és elég vicces eredmény jött ki. Egyelőre még nem néztem meg profilerrel, de vagy én rontottam el valamit nagyon, vagy óriási memória lyukak vannak php-ben.

ini-ben 2 helyen lehet memóriát állítani.

pcre.backtrack_limit - háttértárolós cucc
pcre.recursion_limit - rekurzív dolgokért felelős
Na a rekurzívval magával semmi gond nincsen, de a háttértár elég rendesen telik, ez még nem lenne akkora gond, ha nem szállna el duplán exponenciálisan.

5 értékből:

R^2		=	0,9977 (szal elég pontos)
ln ln M	=	0,0787 * N + 1,9304
[M] 	= 	used backtrack memory in bytes
[N] 	= 	number of plugin calls
A kód alapja ugyanaz, amit a cikkben írtam, szóval nem szabadna, hogy exponenciális függvény szerint egye a memóriát, nem hogy duplán exponenciális szerint...

Majd profilert ráeresztek nem sokára. Kíváncsi vagyok hány preg_match_all meg hasonlók vannak benne, és mennyit zabálnak a memóriából.


szerk:
Nos Xdebug nem adott ki semmi infot, annyival okosabb lettem, hogy 3 preg_match_all-t használ minden plugin híváshoz a rendszer, amik gondolom token_parser, arguments_parser, argument_parser.
Xdebug a pcre futási időt nem számolja bele a dolgokba, szóval azt nem tudtam mérni.


Tettem ki bug reportot, bár gondolom úgyis lehurrognak, ahogy szokták, hogy nem bug, hanem én nem tudok regexet írni, meg hogy a pcre motor ilyen, törődjek bele....

Azért szerintem gáz egy olyan motort kiadni az ember keze alól, ami rekurzióval szuperexponenciálisan eszi a memóriát.
18

Megoldódott

inf · 2010. Már. 4. (Cs), 05.51
Közben volt php verziófrissítés, és a rekurzív regexes hibákat végre kijavították. Konkrétan már nem emlékszem melyik verziótól lehet használni őket, de a legfrissebben garantáltan működnek. :-)
A februári cikk gyanítom, hogy ugrott, mert már március van, egyelőre nem is tudom, hogy ebből a templatezős témából még mit tudnék kihozni, mert amit még írni lehetne az már inkább ennek a motornak a bővítése, és nem általánosan használható dolgok. Egy ORM rendszer fejlesztését már elkezdtem, de arra jutottam, hogy felesleges hozzá a sablonozás. Szóval leginkább a bejövő adatok validálásánál, illetve átalakításánál jut majd nagyobb szerephez a téma, de ott még nem tartok a saját frameworkben. Valószínűleg május első felében fogok majd eljutni addig, hogy validálásról írjak egy cikket. Addig minden jót nektek! :-)
17

Új cikk

inf · 2009. Nov. 23. (H), 05.14
Közben átírtam rekurzív regex mentesre a motort a szokásos foreach-es tokenezős megoldással. (50byte backtrace elég neki...) Lemértem a két módszer közötti fordítási idő különbségét, már amennyire lehetett.(A rekurzívnál az első 4 pont lineáris volt. Ez persze nem túl sok, de már lehet rá egyenest illeszteni.) Alapnak a sablon utasítások számát vettem. A rekurzív motor valóban gyorsabb, de nem annyival, mint amire számítottam.

rekurzív idő [msec] = 0,35 * N + 2,55
tokenezős idő[msec] = 0,50 * N + 4,32


Átnéztem pár template fájlt, átlag kb 30 az utasítások (if,switch,foreach etc..) száma.
Szóval egy átlag template fájlt php-re fordítani:
rekurzív idő [msec] = 13,05
tokenezős idő[msec] = 19,32


Szóval a rekurzív regex csak 30-35%-al gyorsabb, mint a hagyományos módszer. Amiért többre számítottam az az, hogy Smarty-val hasonlítottam össze egy a mostaninál valamivel kezdetlegesebb motort (olyasmit, mint ami a cikkben van), és ott 10-20x-os különbség volt a kettő között. Mint most kiderült ez nem elsősorban a rekurzió miatt volt, hanem mert a Smarty-t gány módon írták meg. A mostani Compilerem 600 sor, aminek a fele gyakorlatilag regexek összerakása változókból. Ezt összevetve a Smarty Compilerrel, ami 2500 sor kb ég és föld a kettő...
A legnagyobb hiba sztem a Smarty rendszerében, hogy nem moduláris. A regexek nem módosíthatóak könnyen benne, és a comment, display stb funkciók is bele vannak égetve. A mostani rendszeremben csináltam alias-t, ami gyakorlatilag annyi, hogy preg_replace-el átírom az egyedi utasításokat a fordítás előtt.
pl:
{$valami} => {display var=$valami}
{* valami *} => {comment}valami{/comment}

Ezerszer hatékonyabb, mint beégetni minden egyes ilyen kivételt a motorba.

Majd lehet, hogy írok egy folytatást a cikkhez a továbbfejlesztés irányával kapcsolatban, meg hogy hogyan oldottam meg a tokenezéssel ugyanazt. Egyelőre sajnos nagyon el vagyok havazva, és valószínűleg februárig nem is lesz időm ilyesmire. Szóval valszeg február közepe felé jön majd a folytatás.

Üdv.
inf3rno