root/MiniTemplator.class.php

Revision 2132, 35.6 kB (checked in by Andrew Dolgov <fox@madoka.spb.ru>, 10 months ago)

[project @ digest improvements and bugfixes]

Line 
1 <?php
2 /**
3 * File MiniTemplator.class.php
4 * @package MiniTemplator
5 */
6
7 /**
8 * A compact template engine for HTML files.
9 *
10 * Requires PHP 4.0.4 or newer.
11 *
12 * <pre>
13 * Template syntax:
14 *
15 *   Variables:
16 *     ${VariableName}
17 *
18 *   Blocks:
19 *     &lt;!-- $BeginBlock BlockName --&gt;
20 *     ... block content ...
21 *     &lt;!-- $EndBlock BlockName --&gt;
22 *
23 *   Include a subtemplate:
24 *     &lt;!-- $Include RelativeFileName --&gt;
25 * </pre>
26 *
27 * <pre>
28 * General remarks:
29 *  - Variable names and block names are case-insensitive.
30 *  - The same variable may be used multiple times within a template.
31 *  - Blocks can be nested.
32 *  - Multiple blocks with the same name may occur within a template.
33 * </pre>
34 *
35 * <pre>
36 * Public methods:
37 *   readTemplateFromFile   - Reads the template from a file.
38 *   setTemplateString      - Assigns a new template string.
39 *   setVariable            - Sets a template variable.
40 *   setVariableEsc         - Sets a template variable to an escaped string value.
41 *   variableExists         - Checks whether a template variable exists.
42 *   addBlock               - Adds an instance of a template block.
43 *   blockExists            - Checks whether a block exists.
44 *   reset                  - Clears all variables and blocks.
45 *   generateOutput         - Generates the HTML page and writes it to the PHP output stream.
46 *   generateOutputToFile   - Generates the HTML page and writes it to a file.
47 *   generateOutputToString - Generates the HTML page and writes it to a string.
48 * </pre>
49 *
50 * Home page: {@link http://www.source-code.biz/MiniTemplator}<br>
51 * License: This module is released under the GNU/LGPL license ({@link http://www.gnu.org/licenses/lgpl.html}).<br>
52 * Copyright 2003: Christian d'Heureuse, Inventec Informatik AG, Switzerland. All rights reserved.<br>
53 * This product is provided "as is" without warranty of any kind.<br>
54 *
55 * Version history:<br>
56 * 2001-10-24 Christian d'Heureuse (chdh): VBasic version created.<br>
57 * 2002-01-26 Markus Angst: ported to PHP4.<br>
58 * 2003-04-07 chdh: changes to adjust to Java version.<br>
59 * 2003-07-08 chdh: Method variableExists added.
60 *   Method setVariable changed to trigger an error when the variable does not exist.<br>
61 * 2004-04-07 chdh: Parameter isOptional added to method setVariable.
62 *   Licensing changed from GPL to LGPL.<br>
63 * 2004-04-18 chdh: Method blockExists added.<br>
64 * 2004-10-28 chdh:<br>
65 *   Method setVariableEsc added.<br>
66 *   Multiple blocks with the same name may now occur within a template.<br>
67 *   No error ("unknown command") is generated any more, if a HTML comment starts with "${".<br>
68 * 2004-11-06 chdh:<br>
69 *   "$Include" command implemented.<br>
70 * 2004-11-20 chdh:<br>
71 *   "$Include" command changed so that the command text is not copied to the output file.<br>
72 */
73
74 class MiniTemplator {
75
76 //--- public member variables ---------------------------------------------------------------------------------------
77
78 /**
79 * Base path for relative file names of subtemplates (for the $Include command).
80 * This path is prepended to the subtemplate file names. It must be set before
81 * readTemplateFromFile or setTemplateString.
82 * @access public
83 */
84 var $subtemplateBasePath;
85
86 //--- private member variables --------------------------------------------------------------------------------------
87
88 /**#@+
89 * @access private
90 */
91
92 var $maxNestingLevel = 50;            // maximum number of block nestings
93 var $maxInclTemplateSize = 1000000;   // maximum length of template string when including subtemplates
94 var $template;                        // Template file data
95 var $varTab;                          // variables table, array index is variable no
96     // Fields:
97     //  varName                       // variable name
98     //  varValue                      // variable value
99 var $varTabCnt;                       // no of entries used in VarTab
100 var $varNameToNoMap;                  // maps variable names to variable numbers
101 var $varRefTab;                       // variable references table
102     // Contains an entry for each variable reference in the template. Ordered by TemplatePos.
103     // Fields:
104     //  varNo                         // variable no
105     //  tPosBegin                     // template position of begin of variable reference
106     //  tPosEnd                       // template position of end of variable reference
107     //  blockNo                       // block no of the (innermost) block that contains this variable reference
108     //  blockVarNo                    // block variable no. Index into BlockInstTab.BlockVarTab
109 var $varRefTabCnt;                    // no of entries used in VarRefTab
110 var $blockTab;                        // Blocks table, array index is block no
111     // Contains an entry for each block in the template. Ordered by TPosBegin.
112     // Fields:
113     //  blockName                     // block name
114     //  nextWithSameName;             // block no of next block with same name or -1 (blocks are backward linked in relation to template position)
115     //  tPosBegin                     // template position of begin of block
116     //  tPosContentsBegin             // template pos of begin of block contents
117     //  tPosContentsEnd               // template pos of end of block contents
118     //  tPosEnd                       // template position of end of block
119     //  nestingLevel                  // block nesting level
120     //  parentBlockNo                 // block no of parent block
121     //  definitionIsOpen              // true while $BeginBlock processed but no $EndBlock
122     //  instances                     // number of instances of this block
123     //  firstBlockInstNo              // block instance no of first instance of this block or -1
124     //  lastBlockInstNo               // block instance no of last instance of this block or -1
125     //  currBlockInstNo               // current block instance no, used during generation of output file
126     //  blockVarCnt                   // no of variables in block
127     //  blockVarNoToVarNoMap          // maps block variable numbers to variable numbers
128     //  firstVarRefNo                 // variable reference no of first variable of this block or -1
129 var $blockTabCnt;                     // no of entries used in BlockTab
130 var $blockNameToNoMap;                // maps block names to block numbers
131 var $openBlocksTab;
132     // During parsing, this table contains the block numbers of the open parent blocks (nested outer blocks).
133     // Indexed by the block nesting level.
134 var $blockInstTab;                    // block instances table
135     // This table contains an entry for each block instance that has been added.
136     // Indexed by BlockInstNo.
137     // Fields:
138     //  blockNo                       // block number
139     //  instanceLevel                 // instance level of this block
140     //     InstanceLevel is an instance counter per block.
141     //     (In contrast to blockInstNo, which is an instance counter over the instances of all blocks)
142     //  parentInstLevel               // instance level of parent block
143     //  nextBlockInstNo               // pointer to next instance of this block or -1
144     //     Forward chain for instances of same block.
145     //  blockVarTab                   // block instance variables
146 var $blockInstTabCnt;                 // no of entries used in BlockInstTab
147
148 var $currentNestingLevel;             // Current block nesting level during parsing.
149 var $templateValid;                   // true if a valid template is prepared
150 var $outputMode;                      // 0 = to PHP output stream, 1 = to file, 2 = to string
151 var $outputFileHandle;                // file handle during writing of output file
152 var $outputError;                     // true when an output error occurred
153 var $outputString;                    // string buffer for the generated HTML page
154
155 /**#@-*/
156
157 //--- constructor ---------------------------------------------------------------------------------------------------
158
159 /**
160 * Constructs a MiniTemplator object.
161 * @access public
162 */
163 function MiniTemplator() {
164    $this->templateValid = false; }
165
166 //--- template string handling --------------------------------------------------------------------------------------
167
168 /**
169 * Reads the template from a file.
170 * @param  string   $fileName  name of the file that contains the template.
171 * @return boolean  true on success, false on error.
172 * @access public
173 */
174 function readTemplateFromFile ($fileName) {
175    if (!$this->readFileIntoString($fileName,$s)) {
176       $this->triggerError ("Error while reading template file " . $fileName . ".");
177       return false; }
178    if (!$this->setTemplateString($s)) return false;
179    return true; }
180
181 /**
182 * Assigns a new template string.
183 * @param  string   $templateString  contents of the template file.
184 * @return boolean  true on success, false on error.
185 * @access public
186 */
187 function setTemplateString ($templateString) {
188    $this->templateValid = false;
189    $this->template = $templateString;
190    if (!$this->parseTemplate()) return false;
191    $this->reset();
192    $this->templateValid = true;
193    return true; }
194
195 /**
196 * Loads the template string for a subtemplate (used for the $Include command).
197 * @return boolean  true on success, false on error.
198 * @access private
199 */
200 function loadSubtemplate ($subtemplateName, &$s) {
201    $subtemplateFileName = $this->combineFileSystemPath($this->subtemplateBasePath,$subtemplateName);
202    if (!$this->readFileIntoString($subtemplateFileName,$s)) {
203       $this->triggerError ("Error while reading subtemplate file " . $subtemplateFileName . ".");
204       return false; }
205    return true; }
206
207 //--- template parsing ----------------------------------------------------------------------------------------------
208
209 /**
210 * Parses the template.
211 * @return boolean  true on success, false on error.
212 * @access private
213 */
214 function parseTemplate() {
215    $this->initParsing();
216    $this->beginMainBlock();
217    if (!$this->parseTemplateCommands()) return false;
218    $this->endMainBlock();
219    if (!$this->checkBlockDefinitionsComplete()) return false;
220    if (!$this->parseTemplateVariables()) return false;
221    $this->associateVariablesWithBlocks();
222    return true; }
223
224 /**
225 * @access private
226 */
227 function initParsing() {
228    $this->varTab = array();
229    $this->varTabCnt = 0;
230    $this->varNameToNoMap = array();
231    $this->varRefTab = array();
232    $this->varRefTabCnt = 0;
233    $this->blockTab = array();
234    $this->blockTabCnt = 0;
235    $this->blockNameToNoMap = array();
236    $this->openBlocksTab = array(); }
237
238 /**
239 * Registers the main block.
240 * The main block is an implicitly defined block that covers the whole template.
241 * @access private
242 */
243 function beginMainBlock() {
244    $blockNo = 0;
245    $this->registerBlock('@@InternalMainBlock@@', $blockNo);
246    $bte =& $this->blockTab[$blockNo];
247    $bte['tPosBegin'] = 0;
248    $bte['tPosContentsBegin'] = 0;
249    $bte['nestingLevel'] = 0;
250    $bte['parentBlockNo'] = -1;
251    $bte['definitionIsOpen'] = true;
252    $this->openBlocksTab[0] = $blockNo;
253    $this->currentNestingLevel = 1; }
254
255 /**
256 * Completes the main block registration.
257 * @access private
258 */
259 function endMainBlock() {
260    $bte =& $this->blockTab[0];
261    $bte['tPosContentsEnd'] = strlen($this->template);
262    $bte['tPosEnd'] = strlen($this->template);
263    $bte['definitionIsOpen'] = false;
264    $this->currentNestingLevel -= 1; }
265
266 /**
267 * Parses commands within the template in the format "<!-- $command parameters -->".
268 * @return boolean  true on success, false on error.
269 * @access private
270 */
271 function parseTemplateCommands() {
272    $p = 0;
273    while (true) {
274       $p0 = strpos($this->template,'<!--',$p);
275       if ($p0 === false) break;
276       $p = strpos($this->template,'-->',$p0);
277       if ($p === false) {
278          $this->triggerError ("Invalid HTML comment in template at offset $p0.");
279          return false; }
280       $p += 3;
281       $cmdL = substr($this->template,$p0+4,$p-$p0-7);
282       if (!$this->processTemplateCommand($cmdL,$p0,$p,$resumeFromStart))
283          return false;
284       if ($resumeFromStart) $p = $p0; }
285    return true; }
286
287 /**
288 * @return boolean  true on success, false on error.
289 * @access private
290 */
291 function processTemplateCommand ($cmdL, $cmdTPosBegin, $cmdTPosEnd, &$resumeFromStart) {
292    $resumeFromStart = false;
293    $p = 0;
294    $cmd = '';
295    if (!$this->parseWord($cmdL,$p,$cmd)) return true;
296    $parms = substr($cmdL,$p);
297    switch (strtoupper($cmd)) {
298       case '$BEGINBLOCK':
299          if (!$this->processBeginBlockCmd($parms,$cmdTPosBegin,$cmdTPosEnd))
300             return false;
301          break;
302       case '$ENDBLOCK':
303          if (!$this->processEndBlockCmd($parms,$cmdTPosBegin,$cmdTPosEnd))
304             return false;
305          break;
306       case '$INCLUDE':
307          if (!$this->processincludeCmd($parms,$cmdTPosBegin,$cmdTPosEnd))
308             return false;
309          $resumeFromStart = true;
310          break;
311       default:
312          if ($cmd{0} == '$' && !(strlen($cmd) >= 2 && $cmd{1} == '{')) {
313             $this->triggerError ("Unknown command \"$cmd\" in template at offset $cmdTPosBegin.");
314             return false; }}
315     return true; }
316
317 /**
318 * Processes the $BeginBlock command.
319 * @return boolean  true on success, false on error.
320 * @access private
321 */
322 function processBeginBlockCmd ($parms, $cmdTPosBegin, $cmdTPosEnd) {
323    $p = 0;
324    if (!$this->parseWord($parms,$p,$blockName)) {
325       $this->triggerError ("Missing block name in \$BeginBlock command in template at offset $cmdTPosBegin.");
326       return false; }
327    if (trim(substr($parms,$p)) != '') {
328       $this->triggerError ("Extra parameter in \$BeginBlock command in template at offset $cmdTPosBegin.");
329       return false; }
330    $this->registerBlock ($blockName, $blockNo);
331    $btr =& $this->blockTab[$blockNo];
332    $btr['tPosBegin'] = $cmdTPosBegin;
333    $btr['tPosContentsBegin'] = $cmdTPosEnd;
334    $btr['nestingLevel'] = $this->currentNestingLevel;
335    $btr['parentBlockNo'] = $this->openBlocksTab[$this->currentNestingLevel-1];
336    $this->openBlocksTab[$this->currentNestingLevel] = $blockNo;
337    $this->currentNestingLevel += 1;
338    if ($this->currentNestingLevel > $this->maxNestingLevel) {
339       $trhis->triggerError ("Block nesting overflow in template at offset $cmdTPosBegin.");
340       return false; }
341    return true; }
342
343 /**
344 * Processes the $EndBlock command.
345 * @return boolean  true on success, false on error.
346 * @access private
347 */
348 function processEndBlockCmd ($parms, $cmdTPosBegin, $cmdTPosEnd) {
349    $p = 0;
350    if (!$this->parseWord($parms,$p,$blockName)) {
351       $this->triggerError ("Missing block name in \$EndBlock command in template at offset $cmdTPosBegin.");
352       return false; }
353    if (trim(substr($parms,$p)) != '') {
354       $this->triggerError ("Extra parameter in \$EndBlock command in template at offset $cmdTPosBegin.");
355       return false; }
356    if (!$this->lookupBlockName($blockName,$blockNo)) {
357       $this->triggerError ("Undefined block name \"$blockName\" in \$EndBlock command in template at offset $cmdTPosBegin.");
358       return false; }
359    $this->currentNestingLevel -= 1;
360    $btr =& $this->blockTab[$blockNo];
361    if (!$btr['definitionIsOpen']) {
362       $this->triggerError ("Multiple \$EndBlock command for block \"$blockName\" in template at offset $cmdTPosBegin.");
363       return false; }
364    if ($btr['nestingLevel'] != $this->currentNestingLevel) {
365       $this->triggerError ("Block nesting level mismatch at \$EndBlock command for block \"$blockName\" in template at offset $cmdTPosBegin.");
366       return false; }
367    $btr['tPosContentsEnd'] = $cmdTPosBegin;
368    $btr['tPosEnd'] = $cmdTPosEnd;
369    $btr['definitionIsOpen'] = false;
370    return true; }
371
372 /**
373 * @access private
374 */
375 function registerBlock($blockName, &$blockNo) {
376    $blockNo = $this->blockTabCnt++;
377    $btr =& $this->blockTab[$blockNo];
378    $btr = array();
379    $btr['blockName'] = $blockName;
380    if (!$this->lookupBlockName($blockName,$btr['nextWithSameName']))
381       $btr['nextWithSameName'] = -1;
382    $btr['definitionIsOpen'] = true;
383    $btr['instances'] = 0;
384    $btr['firstBlockInstNo'] = -1;
385    $btr['lastBlockInstNo'] = -1;
386    $btr['blockVarCnt'] = 0;
387    $btr['firstVarRefNo'] = -1;
388    $btr['blockVarNoToVarNoMap'] = array();
389    $this->blockNameToNoMap[strtoupper($blockName)] = $blockNo; }
390
391 /**
392 * Checks that all block definitions are closed.
393 * @return boolean  true on success, false on error.
394 * @access private
395 */
396 function checkBlockDefinitionsComplete() {
397    for ($blockNo=0; $blockNo < $this->blockTabCnt; $blockNo++) {
398       $btr =& $this->blockTab[$blockNo];
399       if ($btr['definitionIsOpen']) {
400          $this->triggerError ("Missing \$EndBlock command in template for block " . $btr['blockName'] . ".");
401          return false; }}
402    if ($this->currentNestingLevel != 0) {
403       $this->triggerError ("Block nesting level error at end of template.");
404       return false; }
405    return true; }
406
407 /**
408 * Processes the $Include command.
409 * @return boolean  true on success, false on error.
410 * @access private
411 */
412 function processIncludeCmd ($parms, $cmdTPosBegin, $cmdTPosEnd) {
413    $p = 0;
414    if (!$this->parseWordOrQuotedString($parms,$p,$subtemplateName)) {
415       $this->triggerError ("Missing or invalid subtemplate name in \$Include command in template at offset $cmdTPosBegin.");
416       return false; }
417    if (trim(substr($parms,$p)) != '') {
418       $this->triggerError ("Extra parameter in \$include command in template at offset $cmdTPosBegin.");
419       return false; }
420    return $this->insertSubtemplate($subtemplateName,$cmdTPosBegin,$cmdTPosEnd); }
421
422 /**
423 * Processes the $Include command.
424 * @return boolean  true on success, false on error.
425 * @access private
426 */
427 function insertSubtemplate ($subtemplateName, $tPos1, $tPos2) {
428    if (strlen($this->template) > $this->maxInclTemplateSize) {
429       $this->triggerError ("Subtemplate include aborted because the internal template string is longer than $this->maxInclTemplateSize characters.");
430       return false; }
431    if (!$this->loadSubtemplate($subtemplateName,$subtemplate)) return false;
432    // (Copying the template to insert a subtemplate is a bit slow. In a future implementation of MiniTemplator,
433    // a table could be used that contains references to the string fragments.)
434    $this->template = substr($this->template,0,$tPos1) . $subtemplate . substr($this->template,$tPos2);
435    return true; }
436
437 /**
438 * Parses variable references within the template in the format "${VarName}".
439 * @return boolean  true on success, false on error.
440 * @access private
441 */
442 function parseTemplateVariables() {
443    $p = 0;
444    while (true) {
445       $p = strpos($this->template, '${', $p);
446       if ($p === false) break;
447       $p0 = $p;
448       $p = strpos($this->template, '}', $p);
449       if ($p === false) {
450          $this->triggerError ("Invalid variable reference in template at offset $p0.");
451          return false; }
452       $p += 1;
453       $varName = trim(substr($this->template, $p0+2, $p-$p0-3));
454       if (strlen($varName) == 0) {
455          $this->triggerError ("Empty variable name in template at offset $p0.");
456          return false; }
457       $this->registerVariableReference ($varName, $p0, $p); }
458    return true; }
459
460 /**
461 * @access private
462 */
463 function registerVariableReference ($varName, $tPosBegin, $tPosEnd) {
464    if (!$this->lookupVariableName($varName,$varNo))
465       $this->registerVariable($varName,$varNo);
466    $varRefNo = $this->varRefTabCnt++;
467    $vrtr =& $this->varRefTab[$varRefNo];
468    $vrtr = array();
469    $vrtr['tPosBegin'] = $tPosBegin;
470    $vrtr['tPosEnd'] = $tPosEnd;
471    $vrtr['varNo'] = $varNo; }
472
473 /**
474 * @access private
475 */
476 function registerVariable ($varName, &$varNo) {
477    $varNo = $this->varTabCnt++;
478    $vtr =& $this->varTab[$varNo];
479    $vtr = array();
480    $vtr['varName'] = $varName;
481    $vtr['varValue'] = '';
482    $this->varNameToNoMap[strtoupper($varName)] = $varNo; }
483
484 /**
485 * Associates variable references with blocks.
486 * @access private
487 */
488 function associateVariablesWithBlocks() {
489    $varRefNo = 0;
490    $activeBlockNo = 0;
491    $nextBlockNo = 1;
492    while ($varRefNo < $this->varRefTabCnt) {
493       $vrtr =& $this->varRefTab[$varRefNo];
494       $varRefTPos = $vrtr['tPosBegin'];
495       $varNo = $vrtr['varNo'];
496       if ($varRefTPos >= $this->blockTab[$activeBlockNo]['tPosEnd']) {
497          $activeBlockNo = $this->blockTab[$activeBlockNo]['parentBlockNo'];
498          continue; }
499       if ($nextBlockNo < $this->blockTabCnt) {
500          if ($varRefTPos >= $this->blockTab[$nextBlockNo]['tPosBegin']) {
501             $activeBlockNo = $nextBlockNo;
502             $nextBlockNo += 1;
503             continue; }}
504       $btr =& $this->blockTab[$activeBlockNo];
505       if ($varRefTPos < $btr['tPosBegin'])
506          $this->programLogicError(1);
507       $blockVarNo = $btr['blockVarCnt']++;
508       $btr['blockVarNoToVarNoMap'][$blockVarNo] = $varNo;
509       if ($btr['firstVarRefNo'] == -1)
510          $btr['firstVarRefNo'] = $varRefNo;
511       $vrtr['blockNo'] = $activeBlockNo;
512       $vrtr['blockVarNo'] = $blockVarNo;
513       $varRefNo += 1; }}
514
515 //--- build up (template variables and blocks) ----------------------------------------------------------------------
516
517 /**
518 * Clears all variables and blocks.
519 * This method can be used to produce another HTML page with the same
520 * template. It is faster than creating another MiniTemplator object,
521 * because the template does not have to be parsed again.
522 * All variable values are cleared and all added block instances are deleted.
52