SMlite   D
last analyzed

Complexity

Total Complexity 124

Size/Duplication

Total Lines 743
Duplicated Lines 6.86 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
dl 51
loc 743
rs 4.4444
c 0
b 0
f 0
wmc 124
lcom 1
cbo 6

33 Methods

Rating   Name   Duplication   Size   Complexity  
A getTagVal() 0 4 2
A getDefaultSettings() 0 9 1
A __clone() 0 13 3
A exception() 0 6 1
A dumpTags() 0 8 1
A appendHTML() 0 4 1
A setHTML() 0 4 1
B setMessage() 0 23 5
A hasTag() 0 8 3
A is_set() 0 4 1
A trySetHTML() 0 4 1
A trySet() 12 12 3
B del() 0 30 5
A tryDel() 8 8 3
C eachTag() 0 35 7
A findTemplate() 0 13 2
A loadTemplateFromString() 0 11 1
A isTopTag() 0 6 3
B rebuildTagsRegion() 0 20 6
A render() 0 7 1
B renderRegion() 17 21 5
A myStrTok() 0 17 3
C append() 7 39 11
D set() 7 82 18
B debugRenderRegion() 0 38 5
A init() 0 8 1
B cloneRegion() 0 41 6
A get() 0 12 4
B loadTemplate() 0 35 5
C parseTemplate() 0 47 10
A registerTag() 0 10 3
A rebuildTags() 0 8 1
A debugRender() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SMlite often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SMlite, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * ==[ About SMlite ]==========================================================
4
 * This class is a lightweight template engine. It's based around operating with
5
 * chunks of HTML code and the main aims are:.
6
 *
7
 *  - completely remove any code from templates
8
 *  - make templates editable with HTML editor
9
 *
10
 * @author      Romans <[email protected]>
11
 * @copyright   LGPL. See http://www.gnu.org/copyleft/lesser.html
12
 *
13
 * @version     1.1
14
 * @compat      php5 (perhaps php4 untested)
15
 *
16
 * ==[ Version History ]=======================================================
17
 * 1.0          First public version (released with AModules3 alpha)
18
 * 1.1          Added support for "_top" tag
19
 *              Removed support for permanent tags
20
 *              Much more comments and other fixes
21
 *
22
 * ==[ Description ]===========================================================
23
 * SMlite templates are HTML pages containing tags to mark certain regions.
24
 * <html><head>
25
 *   <title>MySite.com - <?page_name?>unknown page<?/page_name?></title>
26
 * </head>
27
 *
28
 * Inside your application regions may be manipulated in a few ways:
29
 *
30
 *  - you can replace region with other content. Using this you can replace
31
 *   name of sub-page or put a date on your template.
32
 *
33
 *  - you can clone whole template or part of it. This is useful if you are
34
 *   working with objects
35
 *
36
 *  - you can manipulate with regions from different files.
37
 *
38
 * Traditional recipe to work with lists in our templates are:
39
 *
40
 *  1. clone template of generic line
41
 *  2. delete content of the list
42
 *  3. inside loop
43
 *   3a. insert values into cloned template
44
 *   3b. render cloned template
45
 *   3c. insert rendered HTML into list template
46
 *  4. render list template
47
 *
48
 * Inside the code I use terms 'region' and 'spot'. They refer to the same thing,
49
 * but I use 'spot' to refer to a location inside template (such as <?$date?>),
50
 * however I use 'region' when I am refering to a chunk of HTML code or sub-template.
51
 * Sometimes I also use term 'tag' which is like a pointer to region or spot.
52
 *
53
 * When template is loaded it's parsed and converted into array. It's possible to
54
 * cache parsed template serialized inside array.
55
 *
56
 * Tag name looks like this:
57
 *
58
 *  "misc/listings:student_list"
59
 *
60
 * Which means to seek tag <?student_list?> inside misc/listings.html
61
 *
62
 * You may have same tag several times inside template. For example you can
63
 * use tag <?$title?> inside <head><title> and <h1>.
64
 *
65
 * If you would set('title','My Title'); it will insert that value in
66
 * all those regions.
67
 *
68
 * ==[ AModules3 integration ]=================================================
69
 * Rule of thumb in object oriented programming is data / code separation. In
70
 * our case HTML is data and our PHP files are code. SMlite helps to completely
71
 * cut out the code from templates (smarty promotes idea about integrating
72
 * logic inside templates and I decided not to use it for that reason)
73
 *
74
 * Inside AModules3, each object have it's own template or may have even several
75
 * templates. When object is created, it's assigned to region inside template.
76
 * Later object operates with assigned template.
77
 *
78
 * Each object is also assigned to a spot on their parent's template. When
79
 * object is rendered, it's HTML is inserted into parent's template.
80
 *
81
 * ==[ Non-AModules3 integration ]=============================================
82
 * SMlite have no strict bindings or requirements for AModules3. You are free
83
 * to use it inside any other library as long as you follow license agreements.
84
 */
85
class SMlite extends AbstractModel
86
{
87
    /**
88
     * This array contains list of all tags found inside template.
89
     */
90
    public $tags = array();
91
92
    /**
93
     * When cloning region inside a template, it's tag becomes a top_tag of a new
94
     * template. Since SMlite 1.1 it's present in new template and can be used.
95
     */
96
    public $top_tag = null;
97
98
    /**
99
     * This is a parsed contents of the template.
100
     */
101
    public $template = array();  // private
102
103
    public $settings = array();
104
105
    /**
106
     * Type of resource to look for pathFinder
107
     */
108
    public $template_type = 'template';
109
110
    /**
111
     * list of updated tags with values.
112
     */
113
    public $updated_tag_list = array();
114
115
    private $cache;
116
117
    /**
118
     * Which file template is coming from.
119
     */
120
    public $origin_filename = null;
121
122
    protected $tmp_template;
123
124
    public function getTagVal($tag)
125
    {
126
        return (isset($this->updated_tag_list[$tag])) ? $this->updated_tag_list[$tag] : null;
127
    }
128
129
    /**
130
     * This function specifies default settings for SMlite. Use
131
     * 2nd argument for constructor to redefine those settings.
132
     *
133
     * A small note why I decided on .html extension. I want to
134
     * point out that template files are and should be valid HTML
135
     * documents. With .html extension those files will be properly
136
     * rendered inside web browser, properly understood inside text
137
     * editor or will be properly treated with wysiwyg html editors.
138
     *
139
     * @return array
140
     */
141
    public function getDefaultSettings()
142
    {
143
        return array(
144
                // by separating them with ':'
145
                'ldelim' => '<?',                // tag delimiter
146
                'rdelim' => '?>',
147
                'extension' => '.html',          // template file extension
148
                );
149
    }
150
151
    // Template creation, interface functions
152
    public function init()
153
    {
154
        parent::init();
155
        $this->cache = &$this->app->smlite_cache;
0 ignored issues
show
Bug introduced by
The property smlite_cache does not seem to exist in App_CLI.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
156
157
        $this->settings = $this->getDefaultSettings();
158
        $this->settings['extension'] = $this->app->getConfig('smlite/extension', '.html');
159
    }
160
161
    public function __clone()
162
    {
163
        if (!is_null($this->top_tag) && is_object($this->top_tag)) {
164
            $this->top_tag = clone $this->top_tag;
165
        }
166
        // may be some of the following lines are unneeded...
167
        $this->template = unserialize(serialize($this->template));
168
        $this->tags = unserialize(serialize($this->tags));
169
        $this->settings = unserialize(serialize($this->settings));
170
        $this->updated_tag_list = unserialize(serialize($this->updated_tag_list));
171
        // ...
172
        $this->rebuildTags();
173
    }
174
175
    public function exception($message = 'Undefined Exception', $type = null, $code = null)
176
    {
177
        return parent::exception($message, $type, $code)
178
            ->addMoreInfo('SMlite_file', $this->origin_filename)
179
            ;
180
    }
181
182
    public function cloneRegion($tag)
183
    {
184
        /*
185
         * Sometimes you will want to put branch into different class. This function will create
186
         * new class for you.
187
         */
188
        if ($this->isTopTag($tag)) {
189
            /** @type self $new */
190
            $new = $this->newInstance();
191
            $new->template = unserialize(serialize($this->template));
192
            $new->top_tag = $tag;
193
            $new->settings = $this->settings;
194
            $new->origin_filename = $this->origin_filename;
195
            $new->rebuildTags();
196
197
            return $new;
198
        }
199
200
        if (!$this->hasTag($tag)) {
201
            $o = $this->owner ? ' for '.$this->owner->__toString() : '';
202
            throw new BaseException("No such tag ($tag) in template$o. Tags are: ".
203
                implode(', ', array_keys($this->tags)));
204
        }
205
        $class_name = get_class($this);
206
        /** @type self $new */
207
        $new = $this->add($class_name);
208
        try {
209
            $new->template = unserialize(serialize($this->tags[$tag][0]));
210
            if (is_string($new->template)) {
211
                $new->template = array($new->template);
212
            }
213
        } catch (PDOException $e) {
214
            throw $this->exception('PDO got stuck in template')
215
                ->addMoreInfo('tag', $tag)
216
                ->addMoreInfo('tags', var_export($this->tags, true));
217
        }
218
        $new->top_tag = $tag;
219
        $new->settings = $this->settings;
220
221
        return $new->rebuildTags();
222
    }
223
224
    // Misc functions
225
    public function dumpTags()
226
    {
227
        /*
228
         * This function is used for debug. It will output all tag names inside
229
         * current templates
230
         */
231
        echo '<pre>'.var_export(array_keys($this->tags), true).'</pre>';
232
    }
233
234
    // Operation with regions inside template
235
    /**
236
     * Finds tag and returns contents.
237
     *
238
     * THIS FUNTION IS DANGEROUS!
239
     *  - if you want a rendered region, use renderRegion()
240
     *  - if you want a sub-template use cloneRegion()
241
     *
242
     *  - if you want to copy part of template to other SMlite object,
243
     *   do not forget to call rebuildTags() if you plan to refer them.
244
     *   Not calling rebuildTags() will render template properly anyway.
245
     *
246
     * If tag is defined multiple times, first region is returned.
247
     */
248
    public function get($tag)
249
    {
250
        if ($this->isTopTag($tag)) {
251
            return $this->template;
252
        }
253
        $v = $this->tags[$tag][0];
254
        if (is_array($v) && count($v) == 1) {
255
            $v = array_shift($v);
256
        }
257
258
        return $v;
259
    }
260
    public function appendHTML($tag, $value)
261
    {
262
        return $this->append($tag, $value, false);
263
    }
264
265
    /**
266
     * This appends static content to region refered by a tag. This function
267
     * is useful when you are adding more rows to a list or table.
268
     *
269
     * If tag is used for several regions inside template, they all will be
270
     * appended with new data.
271
     */
272
    public function append($tag, $value, $encode = true)
273
    {
274
        if ($value instanceof URL) {
275
            $value = $value->__toString();
276
        }
277
        // Temporary here until we finish testing
278 View Code Duplication
        if ($encode
279
            && $value != $this->app->encodeHtmlChars($value, ENT_NOQUOTES)
280
            && $this->app->getConfig('html_injection_debug', false)
281
        ) {
282
            throw $this->exception('Attempted to supply html string through append()')
283
                ->addMoreInfo('val', var_export($value, true))
284
                ->addMoreInfo('enc', var_export($this->app->encodeHtmlChars($value, ENT_NOQUOTES), true))
285
                //->addAction('ignore','Ignore tag'.$tag)
286
                ;
287
        }
288
        if ($encode) {
289
            $value = $this->app->encodeHtmlChars($value, ENT_NOQUOTES);
290
        }
291
        if ($this->isTopTag($tag)) {
292
            $this->template[] = $value;
293
294
            return $this;
295
        }
296
        if (!isset($this->tags[$tag]) || !is_array($this->tags[$tag])) {
297
            throw $this->exception("Cannot append to tag $tag")
298
                ->addMoreInfo('by', $this->owner);
299
        }
300
        foreach ($this->tags[$tag] as $key => $_) {
301
            if (!is_array($this->tags[$tag][$key])) {
302
                //throw new BaseException("Problem appending '".
303
                //      $this->app->encodeHtmlChars($value)."' to '$tag': key=$key");
304
                $this->tags[$tag][$key] = array($this->tags[$tag][$key]);
305
            }
306
            $this->tags[$tag][$key][] = $value;
307
        }
308
309
        return $this;
310
    }
311
    public function setHTML($tag, $value = null)
312
    {
313
        return $this->set($tag, $value, false);
314
    }
315
    /**
316
     * Provided that the HTML tag contains ICU-compatible message format
317
     * string, it will be localized then integrated with passed arguments.
318
     */
319
    public function setMessage($tag, $args = array())
320
    {
321
        if (!is_array($args)) {
322
            $args = array($args);
323
        }
324
        $fmt = $this->app->_($this->get($tag));
325
326
        // Try to analyze format and see which formatter to use
327
        if (class_exists('MessageFormatter', false) && strpos($fmt, '{') !== null) {
328
            $fmt = new MessageFormatter($this->app->locale, $fmt);
329
            $str = $fmt->format($args);
330
        } elseif (strpos($fmt, '%') !== null) {
331
            // Else, perhaps it's a sprintf?
332
            array_unshift($args, $fmt);
333
            $str = call_user_func_array('sprintf', $args);
334
        } else {
335
            throw $this->exception('Unclear how to format this')
336
                ->addMoreInfo('fmt', $fmt)
337
                ;
338
        }
339
340
        return $this->set($tag, $str);
341
    }
342
    public function set($tag, $value = null, $encode = true)
343
    {
344
        /*
345
         * This function will replace region refered by $tag to a new content.
346
         *
347
         * If tag is used several times, all regions are replaced.
348
         *
349
         * ALTERNATIVE USE(2) of this function is to pass associative array as
350
         * a single argument. This will assign multiple tags with one call.
351
         * Sample use is:
352
         *
353
         *  set($_GET);
354
         *
355
         * would read and set multiple region values from $_GET array.
356
         *
357
         * ALTERNATIVE USE(3) of this function is to pass 2 arrays. First array
358
         * will contain tag names and 2nd array will contain their values.
359
         */
360
        if (is_object($tag)) {
361
            $tag = $tag->get();
362
        }
363
        if (is_array($tag)) {
364
            if (is_null($value)) {
365
                // USE(2)
366
                foreach ($tag as $s => $v) {
367
                    $this->trySet($s, $v, $encode);
368
                }
369
370
                return $this;
371
            }
372
            if (is_array($value)) {
373
                // USE(2)
374
                reset($tag);
375
                reset($value);
376
                while (list(, $s) = each($tag)) {
377
                    list(, $v) = each($value);
378
                    $this->set($s, $v, $encode);
379
                }
380
381
                return $this;
382
            }
383
            $this->fatal('Incorrect argument types when calling SMlite::set(). Check documentation.');
384
        }
385
        if ($value instanceof URL) {
386
            $value = $value->__toString();
387
        }
388
        if (is_array($value)) {
389
            return $this;
390
        }
391
392 View Code Duplication
        if ($encode
393
            && $value != $this->app->encodeHtmlChars($value, ENT_NOQUOTES)
394
            && $this->app->getConfig('html_injection_debug', false)
395
        ) {
396
            throw $this->exception('Attempted to supply html string through set()')
397
                ->addMoreInfo('val', var_export($value, true))
398
                ->addMoreInfo('enc', var_export($this->app->encodeHtmlChars($value, ENT_NOQUOTES), true))
399
                //->addAction('ignore','Ignore tag'.$tag)
400
                ;
401
        }
402
        if ($encode) {
403
            $value = $this->app->encodeHtmlChars($value, ENT_NOQUOTES);
404
        }
405
        if ($this->isTopTag($tag)) {
406
            $this->template = $value;
407
408
            return $this;
409
        }
410
        if (!isset($this->tags[$tag]) || !is_array($this->tags[$tag])) {
411
            $o = $this->owner ? $this->owner->__toString() : 'none';
412
            throw $this->exception('No such tag in template')
413
                ->addMoreInfo('tag', $tag)
414
                ->addMoreInfo('owner', $o)
415
                ->addMoreInfo('tags', implode(', ', array_keys($this->tags)));
416
        }
417
        foreach ($this->tags[$tag] as $key => $_) {
418
            $this->tags[$tag][$key] = $value;
419
        }
420
        $this->updated_tag_list[$tag] = $value;
421
422
        return $this;
423
    }
424
    /** Check if tag is present inside template */
425
    public function hasTag($tag)
426
    {
427
        if ($this->isTopTag($tag)) {
428
            return true;
429
        }
430
431
        return isset($this->tags[$tag]) && is_array($this->tags[$tag]);
432
    }
433
    public function is_set($tag)
434
    {
435
        return $this->hasTag($tag);
436
    }
437
    public function trySetHTML($tag, $value = null)
438
    {
439
        return $this->trySet($tag, $value, false);
440
    }
441 View Code Duplication
    public function trySet($tag, $value = null, $encode = true)
442
    {
443
        /*
444
         * Check if tag is present inside template. If it does, execute set();
445
         * See documentation for set()
446
         */
447
        if (is_array($tag)) {
448
            return $this->set($tag, $value, $encode);
449
        }
450
451
        return $this->hasTag($tag) ? $this->set($tag, $value, $encode) : $this;
452
    }
453
    public function del($tag)
454
    {
455
        /*
456
         * This deletes content of a region, however tag remains and you can still refer to it.
457
         *
458
         * If tag is defined multiple times, content of all regions are deleted.
459
         */
460
        if ($this->isTopTag($tag)) {
461
            $this->loadTemplateFromString('<?$'.$tag.'?>');
462
463
            return $this;
464
            //return $this->fatal("SMlite::del() is trying to delete top tag: $tag");
465
        }
466
        if (empty($this->tags[$tag])) {
467
            //$o = $this->owner ? ' for '.$this->owner->__toString() : '';
468
            $e = $this->exception('No such tag in template')
469
                ->addMoreInfo('tag', $tag);
470
            if ($this->owner) {
471
                $e->addMoreInfo('owner', $this->owner->__toString());
472
            }
473
            $e->addMoreInfo('tags', implode(', ', array_keys($this->tags)));
474
            throw $e;
475
        }
476
        foreach ($this->tags[$tag] as $key => $val) {
477
            $this->tags[$tag][$key] = array();
478
        }
479
        unset($this->updated_tag_list[$tag]);
480
481
        return $this;
482
    }
483 View Code Duplication
    public function tryDel($tag)
484
    {
485
        if (is_array($tag)) {
486
            return $this->del($tag);
487
        }
488
489
        return $this->hasTag($tag) ? $this->del($tag) : $this;
490
    }
491
    public function eachTag($tag, $callable)
492
    {
493
        /*
494
         * This function will execute $callable($text,$tag) for each
495
         * occurance of $tag. This is handy if one tag appears several times on the page,
496
         * but needs custom processing. $text will be rendered part of the template. $tag
497
         * will be unique reference to a tag, containing #<num> allowing you to add objects
498
         * from the functions
499
         */
500
        if (!isset($this->tags[$tag])) {
501
            return;
502
        }
503
504
        foreach ($this->tags as $tagx => $arr) {
505
            $tag_split = explode('#', $tagx);
506
            $t = $tag_split[0];
507
            if (!isset($tag_split[1]) || $t != $tag) {
508
                continue;
509
            }
510
            $text = $this->tags[$tagx][0][0];
511
            try {
512
                $ret = call_user_func($callable, $this->renderRegion($text), $tagx);
513
            } catch (BaseException $e) {
514
                $e
515
                    ->addMoreInfo('SMlite_tag', $tagx)
516
                    ->addMoreInfo('SMlite_file', $this->origin_filename)
517
                    ;
518
                throw $e;
519
            }
520
            if ($ret instanceof URL) {
521
                $ret = $ret->__toString();
522
            }
523
            $this->tags[$tagx][0][0] = $ret;
524
        }
525
    }
526
527
    // template loading and parsing
528
    public function findTemplate($template_name)
529
    {
530
        /*
531
         * Find template location inside search directory path
532
         */
533
        if (!$this->app) {
534
            throw new Exception_InitError('You should use add() to add objects!');
535
        }
536
        $f = $this->app->locatePath($this->template_type, $template_name.$this->settings['extension']);
537
        $this->origin_filename = $f;
538
539
        return implode('', file($f));
540
    }
541
    /**
542
     * @param string $template_string
543
     * @return $this
544
     */
545
    public function loadTemplateFromString($template_string)
546
    {
547
        $this->template = array();
548
        $this->tags = array();
549
        $this->updated_tag_list = array();
550
551
        $this->tmp_template = $template_string;
552
        $this->parseTemplate($this->template);
553
554
        return $this;
555
    }
556
    /**
557
     * @param  string $template_name
558
     * @param  string $ext
559
     * @return $this
560
     */
561
    public function loadTemplate($template_name, $ext = null)
562
    {
563
        /*
564
         * Load template from file
565
         */
566
        if (!$this->app) {
567
            throw new Exception('Broken Link');
568
        }
569
        if ($this->cache[$template_name.$ext]) {
570
            $this->template = unserialize($this->cache[$template_name.$ext]);
571
            $this->rebuildTags();
572
573
            return $this;
574
        }
575
576
        $tempext = null;
577
        if ($ext !== null) {
578
            $tempext = $this->settings['extension'];
579
            $this->settings['extension'] = $ext;
580
        }
581
        $this->tmp_template = $this->findTemplate($template_name);
582
583
        $this->template = array();
584
        $this->tags = array();
585
        $this->updated_tag_list = array();
586
587
        $this->parseTemplate($this->template);
588
        if ($ext !== null) {
589
            $this->settings['extension'] = $tempext;
590
        }
591
592
        $this->cache[$template_name.$ext] = serialize($this->template);
593
594
        return $this;
595
    }
596
    /**
597
     * @param array &$template
598
     * @param int $level
599
     * @param int $pc
600
     * @return string
601
     */
602
    public function parseTemplate(&$template, $level = 0, $pc = 0)
603
    {
604
        /*
605
         * private function
606
         *
607
         * This is a main function, which actually parses template. It's recursive and it
608
         * calls itself. Empty array should be passed
609
         */
610
        // TODO when we go into sublevel, we should set the number of
611
        // the tag so that there is NO double numbers in template COMPLETELY
612
        // May be this way is dirty, need to look for better solution...
613
        $c = pow(100, $level) + $pc;
614
        while (strlen($this->tmp_template)) {
615
            $text = $this->myStrTok($this->tmp_template, $this->settings['ldelim']);
616
            if ($text !== '') {
617
                $template[] = $text;
618
            }
619
            $tag = trim($this->myStrTok($this->tmp_template, $this->settings['rdelim']));
620
            if (isset($tag) && $tag) {
621
                if ($tag[0] == '$') {
622
                    $tag = substr($tag, 1);
623
                    $template[$tag.'#'.$c] = array();
624
                    $this->registerTag($tag, $c, $template[$tag.'#'.$c]);
625
                } elseif ($tag[0] == '/') {
626
                    $tag = substr($tag, 1);
627
628
                    return $tag;
629
                } elseif (substr($tag, -1) == '/') {
630
                    $tag = substr($tag, 0, -1);
631
                    $template[$tag.'#'.$c] = array();
632
                    $this->registerTag($tag, $c, $template[$tag.'#'.$c]);
633
                } else {
634
                    $template[$tag.'#'.$c] = array();
635
                    $this->registerTag($tag, $c, $template[$tag.'#'.$c]);
636
                    $xtag = $this->parseTemplate($template[$tag.'#'.$c], $level + 1, $c);
637
                    if ($xtag && $tag != $xtag) {
638
                        throw $this->exception('Closing tag missmatch.')
639
                            ->addMoreInfo('opening', $tag)
640
                            ->addMoreInfo('closing', $xtag);
641
                    }
642
                }
643
            }
644
            ++$c;
645
        }
646
647
        return 'end_of_file';
648
    }
649
    public function registerTag($key, $npk, &$ref)
650
    {
651
        if (!$key) {
652
            return;
653
        }
654
        if (isset($npk)) {
655
            $this->tags[$key.'#'.$npk][] = &$ref;
656
        }
657
        $this->tags[$key][] = &$ref;
658
    }
659
    /**
660
     * @return boolean
661
     */
662
    public function isTopTag($tag)
663
    {
664
        return
665
            (isset($this->top_tag) && ($tag == $this->top_tag)) ||
666
            ($tag == '_top');
667
    }
668
669
    /**
670
     * Rebuild tags of existing array structure
671
     *
672
     * This function walks through template and rebuilds list of tags. You need it in case you
673
     * changed already parsed template.
674
     *
675
     * @return $this
676
     */
677
    public function rebuildTags()
678
    {
679
        $this->tags = array();
680
        $this->updated_tag_list = array();
681
        $this->rebuildTagsRegion($this->template);
682
683
        return $this;
684
    }
685
686
    /**
687
     * @param array &$branch
688
     */
689
    public function rebuildTagsRegion(&$branch)
690
    {
691
        if (!isset($branch)) {
692
            throw new BaseException('Cannot rebuild tags, because template is empty');
693
        }
694
        if (!is_array($branch)) {
695
            throw $this->exception('System problem with SMLite. Incorrect use of branch');
696
        }
697
        foreach ($branch as $key => $val) {
698
            if (is_int($key)) {
699
                continue;
700
            }
701
            list($real_key,) = explode('#', $key);
702
            $this->registerTag($real_key, null, $branch[$key]);
703
            $this->registerTag($key, null, $branch[$key]);
704
            if (is_array($branch[$key])) {
705
                $this->rebuildTagsRegion($branch[$key]);
706
            }
707
        }
708
    }
709
710
    /**
711
     * Template rendering (array -> string)
712
     *
713
     * @return string|array
714
     */
715
    public function render()
716
    {
717
        /*
718
         * This function should be used to convert template into string representation.
719
         */
720
        return $this->renderRegion($this->template);
721
    }
722
    /**
723
     * @param string|array &$chunk
724
     * @return string|array
725
     */
726 View Code Duplication
    public function renderRegion(&$chunk)
727
    {
728
        $result = ''; // you can replace this with array() for debug purposes
729
        if (!is_array($chunk)) {
730
            return $chunk;
731
        }
732
        foreach ($chunk as $key => $_chunk) {
733
            $tmp = $this->renderRegion($_chunk);
734
            if (is_array($result)) {
735
                $result[] = $tmp;
736
            } else {
737
                if (is_array($tmp)) {
738
                    $result = array($result, $tmp);
739
                } else {
740
                    $result .= $tmp;
741
                }
742
            }
743
        }
744
745
        return $result;
746
    }
747
748
    // For debuging of template. Only allow to debug initial template.
749
    // In future should be extended somehow with recursiveRender to also allow
750
    // debugging of templates of entire object tree.
751
    /**
752
     * @return string
753
     */
754
    public function debugRender()
755
    {
756
        return $this->debugRenderRegion($this->template);
757
    }
758
    /**
759
     * @param  string|array &$chunk
760
     * @return string
761
     */
762
    public function debugRenderRegion(&$chunk)
763
    {
764
        // output templates
765
        $t = array(
766
            'tag-html' => '<span class="tag-html" style="color:black;">%s</span>',
767
            'tag-open' => '<span class="tag-container" '.
768
                        'onmouseover="$(this).css(\'background\',\'lightgray\');" '.
769
                        'onmouseout="$(this).css(\'background\',\'transparent\');">'.
770
                    '<span style="color:blue;cursor:pointer;" title="Start tag" '.
771
                        'onclick="$(this).next().toggle();">[%s]</span>'.
772
                    '<span>',
773
            'tag-close' => '</span>'.
774
                    '<span style="color:blue;cursor:pointer;" title="End tag" '.
775
                        'onclick="$(this).prev().toggle();">[/%s]</span>'.
776
                '</span>',
777
        );
778
        $result = '';
779
780
        // simple HTML
781
        if (!is_array($chunk)) {
782
            $s = preg_replace('/[\n|\r]{1,}/', '<br>', htmlentities($chunk));
783
784
            return sprintf($t['tag-html'], $s);
785
        }
786
        // recursion
787
        foreach ($chunk as $key => $_chunk) {
788
            $tag = substr($key, 0, strpos($key, '#'));
789
            if (!is_numeric($key)) {
790
                $result .= sprintf($t['tag-open'], $tag);
791
            }
792
            $result .= $this->debugRenderRegion($_chunk);
793
            if (!is_numeric($key)) {
794
                $result .= sprintf($t['tag-close'], $tag);
795
            }
796
        }
797
798
        return $result;
799
    }
800
801
    // {{{ Misc functions
802
803
    /**
804
     * @param  string &$string
805
     * @param  string $tok
806
     * @return string
807
     */
808
    public function myStrTok(&$string, $tok)
809
    {
810
        if (!$string) {
811
            return '';
812
        }
813
        $pos = strpos($string, $tok);
814
        if ($pos === false) {
815
            $chunk = $string;
816
            $string = '';
817
818
            return $chunk;  // nothing left
819
        }
820
        $chunk = substr($string, 0, $pos);
821
        $string = substr($string, $pos + strlen($tok));
822
823
        return $chunk;
824
    }
825
826
    // }}}
827
}
828