Passed
Push — master ( d12d76...a43d60 )
by Greg
05:47
created

HtmlRenderer::write()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 23
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 16
nc 4
nop 3
dl 0
loc 23
rs 9.7333
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2019 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Report;
21
22
use Fisharebest\Webtrees\Functions\FunctionsRtl;
23
use Fisharebest\Webtrees\I18N;
24
use Fisharebest\Webtrees\MediaFile;
25
use Fisharebest\Webtrees\Webtrees;
26
use League\Flysystem\FilesystemInterface;
27
28
use function ceil;
29
use function count;
30
use function explode;
31
use function preg_match;
32
use function str_replace;
33
use function stripos;
34
use function substr_count;
35
36
/**
37
 * Class HtmlRenderer
38
 */
39
class HtmlRenderer extends AbstractRenderer
40
{
41
    /**
42
     * Cell padding
43
     *
44
     * @var float
45
     */
46
    public $cPadding = 2;
47
48
    /**
49
     * Cell height ratio
50
     *
51
     * @var float
52
     */
53
    public $cellHeightRatio = 1.8;
54
55
    /**
56
     * Current horizontal position
57
     *
58
     * @var float
59
     */
60
    public $X = 0.0;
61
62
    /**
63
     * Current vertical position
64
     *
65
     * @var float
66
     */
67
    public $Y = 0.0;
68
69
    /**
70
     * Currently used style name
71
     *
72
     * @var string
73
     */
74
    public $currentStyle = '';
75
76
    /**
77
     * Page number counter
78
     *
79
     * @var int
80
     */
81
    public $pageN = 1;
82
83
    /**
84
     * Store the page width without left and right margins
85
     *
86
     * In HTML, we don't need this
87
     *
88
     * @var float
89
     */
90
    public $noMarginWidth = 0.0;
91
92
    /**
93
     * Last cell height
94
     *
95
     * @var float
96
     */
97
    public $lastCellHeight = 0.0;
98
99
    /**
100
     * LTR or RTL alignement; "left" on LTR, "right" on RTL
101
     * Used in <div>
102
     *
103
     * @var string
104
     */
105
    public $alignRTL = 'left';
106
107
    /**
108
     * LTR or RTL entity
109
     *
110
     * @var string
111
     */
112
    public $entityRTL = '&lrm;';
113
114
    /**
115
     * Largest Font Height is used by TextBox etc.
116
     *
117
     * Use this to calculate a the text height.
118
     * This makes sure that the text fits into the cell/box when different font sizes are used
119
     *
120
     * @var float
121
     */
122
    public $largestFontHeight = 0;
123
124
    /**
125
     * Keep track of the highest Y position
126
     *
127
     * Used with Header div / Body div / Footer div / "addpage" / The bottom of the last image etc.
128
     *
129
     * @var float
130
     */
131
    public $maxY = 0;
132
133
    /** @var ReportBaseElement[] Array of elements in the header */
134
    public $headerElements = [];
135
136
    /** @var ReportBaseElement[] Array of elements in the page header */
137
    public $pageHeaderElements = [];
138
139
    /** @var ReportBaseElement[] Array of elements in the footer */
140
    public $footerElements = [];
141
142
    /** @var ReportBaseElement[] Array of elements in the body */
143
    public $bodyElements = [];
144
145
    /** @var ReportHtmlFootnote[] Array of elements in the footer notes */
146
    public $printedfootnotes = [];
147
148
    /**
149
     * HTML Setup - ReportHtml
150
     *
151
     * @return void
152
     */
153
    public function setup(): void
154
    {
155
        parent::setup();
156
157
        // Setting up the correct dimensions if Portrait (default) or Landscape
158
        if ($this->orientation === 'landscape') {
159
            $tmpw              = $this->page_width;
160
            $this->page_width  = $this->page_height;
161
            $this->page_height = $tmpw;
162
        }
163
        // Store the pagewidth without margins
164
        $this->noMarginWidth = $this->page_width - $this->left_margin - $this->right_margin;
165
        // If RTL
166
        if ($this->rtl) {
167
            $this->alignRTL  = 'right';
168
            $this->entityRTL = '&rlm;';
169
        }
170
        // Change the default HTML font name
171
        $this->default_font = 'Arial';
172
173
        if ($this->show_generated_by) {
174
            // The default style name for Generated by.... is 'genby'
175
            $element = new ReportHtmlCell(0, 10, 0, 'C', '', 'genby', 1, ReportBaseElement::CURRENT_POSITION, ReportBaseElement::CURRENT_POSITION, 0, 0, '', '', true);
176
            $element->addText($this->generated_by);
177
            $element->setUrl(Webtrees::URL);
178
            $this->footerElements[] = $element;
179
        }
180
    }
181
182
    /**
183
     * Add an element.
184
     *
185
     * @param ReportBaseElement|string $element
186
     *
187
     * @return void
188
     */
189
    public function addElement($element): void
190
    {
191
        if ($this->processing === 'B') {
192
            $this->bodyElements[] = $element;
193
        } elseif ($this->processing === 'H') {
194
            $this->headerElements[] = $element;
195
        } elseif ($this->processing === 'F') {
196
            $this->footerElements[] = $element;
197
        }
198
    }
199
200
    /**
201
     * Generate the page header
202
     *
203
     * @return void
204
     */
205
    private function runPageHeader(): void
206
    {
207
        foreach ($this->pageHeaderElements as $element) {
208
            if ($element instanceof ReportBaseElement) {
209
                $element->render($this);
210
            } elseif ($element === 'footnotetexts') {
211
                $this->footnotes();
212
            } elseif ($element === 'addpage') {
213
                $this->addPage();
214
            }
215
        }
216
    }
217
218
    /**
219
     * Generate footnotes
220
     *
221
     * @return void
222
     */
223
    public function footnotes(): void
224
    {
225
        $this->currentStyle = '';
226
        if (!empty($this->printedfootnotes)) {
227
            foreach ($this->printedfootnotes as $element) {
228
                $element->renderFootnote($this);
229
            }
230
        }
231
    }
232
233
    /**
234
     * Run the report.
235
     *
236
     * @return void
237
     */
238
    public function run(): void
239
    {
240
        // Setting up the styles
241
        echo '<style type="text/css">';
242
        echo '#bodydiv { font: 10px sans-serif;}';
243
        foreach ($this->styles as $class => $style) {
244
            echo '.', $class, ' { ';
245
            if ($style['font'] === 'dejavusans') {
246
                $style['font'] = $this->default_font;
247
            }
248
            echo 'font-family: ', $style['font'], '; ';
249
            echo 'font-size: ', $style['size'], 'pt; ';
250
            // Case-insensitive
251
            if (stripos($style['style'], 'B') !== false) {
252
                echo 'font-weight: bold; ';
253
            }
254
            if (stripos($style['style'], 'I') !== false) {
255
                echo 'font-style: italic; ';
256
            }
257
            if (stripos($style['style'], 'U') !== false) {
258
                echo 'text-decoration: underline; ';
259
            }
260
            if (stripos($style['style'], 'D') !== false) {
261
                echo 'text-decoration: line-through; ';
262
            }
263
            echo '}', PHP_EOL;
264
        }
265
266
        //-- header divider
267
        echo '</style>', PHP_EOL;
268
        echo '<div id="headermargin" style="position: relative; top: auto; height: ', $this->header_margin, 'pt; width: ', $this->noMarginWidth, 'pt;"></div>';
269
        echo '<div id="headerdiv" style="position: relative; top: auto; width: ', $this->noMarginWidth, 'pt;">';
270
        foreach ($this->headerElements as $element) {
271
            if ($element instanceof ReportBaseElement) {
272
                $element->render($this);
273
            } elseif ($element === 'footnotetexts') {
274
                $this->footnotes();
275
            } elseif ($element === 'addpage') {
276
                $this->addPage();
277
            }
278
        }
279
        //-- body
280
        echo '</div>';
281
        echo '<script>document.getElementById("headerdiv").style.height="', $this->top_margin - $this->header_margin - 6, 'pt";</script>';
282
        echo '<div id="bodydiv" style="position: relative; top: auto; width: ', $this->noMarginWidth, 'pt; height: 100%;">';
283
        $this->Y    = 0;
284
        $this->maxY = 0;
285
        $this->runPageHeader();
286
        foreach ($this->bodyElements as $element) {
287
            if ($element instanceof ReportBaseElement) {
288
                $element->render($this);
289
            } elseif ($element === 'footnotetexts') {
290
                $this->footnotes();
291
            } elseif ($element === 'addpage') {
292
                $this->addPage();
293
            }
294
        }
295
        //-- footer
296
        echo '</div>';
297
        echo '<script>document.getElementById("bodydiv").style.height="', $this->maxY, 'pt";</script>';
298
        echo '<div id="bottommargin" style="position: relative; top: auto; height: ', $this->bottom_margin - $this->footer_margin, 'pt;width:', $this->noMarginWidth, 'pt;"></div>';
299
        echo '<div id="footerdiv" style="position: relative; top: auto; width: ', $this->noMarginWidth, 'pt;height:auto;">';
300
        $this->Y    = 0;
301
        $this->X    = 0;
302
        $this->maxY = 0;
303
        foreach ($this->footerElements as $element) {
304
            if ($element instanceof ReportBaseElement) {
305
                $element->render($this);
306
            } elseif ($element === 'footnotetexts') {
307
                $this->footnotes();
308
            } elseif ($element === 'addpage') {
309
                $this->addPage();
310
            }
311
        }
312
        echo '</div>';
313
        echo '<script>document.getElementById("footerdiv").style.height="', $this->maxY, 'pt";</script>';
314
        echo '<div id="footermargin" style="position: relative; top: auto; height: ', $this->footer_margin, 'pt;width:', $this->noMarginWidth, 'pt;"></div>';
315
    }
316
317
    /**
318
     * Create a new Cell object.
319
     *
320
     * @param int    $width   cell width (expressed in points)
321
     * @param int    $height  cell height (expressed in points)
322
     * @param mixed  $border  Border style
323
     * @param string $align   Text alignement
324
     * @param string $bgcolor Background color code
325
     * @param string $style   The name of the text style
326
     * @param int    $ln      Indicates where the current position should go after the call
327
     * @param mixed  $top     Y-position
328
     * @param mixed  $left    X-position
329
     * @param int    $fill    Indicates if the cell background must be painted (1) or transparent (0). Default value: 1
330
     * @param int    $stretch Stretch carachter mode
331
     * @param string $bocolor Border color
332
     * @param string $tcolor  Text color
333
     * @param bool   $reseth
334
     *
335
     * @return ReportBaseCell
336
     */
337
    public function createCell($width, $height, $border, $align, $bgcolor, $style, $ln, $top, $left, $fill, $stretch, $bocolor, $tcolor, $reseth): ReportBaseCell
338
    {
339
        return new ReportHtmlCell($width, $height, $border, $align, $bgcolor, $style, $ln, $top, $left, $fill, $stretch, $bocolor, $tcolor, $reseth);
340
    }
341
342
    /**
343
     * Create a new TextBox object.
344
     *
345
     * @param float  $width   Text box width
346
     * @param float  $height  Text box height
347
     * @param bool   $border
348
     * @param string $bgcolor Background color code in HTML
349
     * @param bool   $newline
350
     * @param float  $left
351
     * @param float  $top
352
     * @param bool   $pagecheck
353
     * @param string $style
354
     * @param bool   $fill
355
     * @param bool   $padding
356
     * @param bool   $reseth
357
     *
358
     * @return ReportBaseTextbox
359
     */
360
    public function createTextBox(
361
        float $width,
362
        float $height,
363
        bool $border,
364
        string $bgcolor,
365
        bool $newline,
366
        float $left,
367
        float $top,
368
        bool $pagecheck,
369
        string $style,
370
        bool $fill,
371
        bool $padding,
372
        bool $reseth
373
    ): ReportBaseTextbox {
374
        return new ReportHtmlTextbox($width, $height, $border, $bgcolor, $newline, $left, $top, $pagecheck, $style, $fill, $padding, $reseth);
375
    }
376
377
    /**
378
     * Create a text element.
379
     *
380
     * @param string $style
381
     * @param string $color
382
     *
383
     * @return ReportBaseText
384
     */
385
    public function createText(string $style, string $color): ReportBaseText
386
    {
387
        return new ReportHtmlText($style, $color);
388
    }
389
390
    /**
391
     * Create a new Footnote object.
392
     *
393
     * @param string $style Style name
394
     *
395
     * @return ReportBaseFootnote
396
     */
397
    public function createFootnote($style): ReportBaseFootnote
398
    {
399
        return new ReportHtmlFootnote($style);
400
    }
401
402
    /**
403
     * Create a new Page Header object
404
     *
405
     * @return ReportBasePageHeader
406
     */
407
    public function createPageHeader(): ReportBasePageHeader
408
    {
409
        return new ReportHtmlPageHeader();
410
    }
411
412
    /**
413
     * Create a new image object.
414
     *
415
     * @param string $file  Filename
416
     * @param float  $x
417
     * @param float  $y
418
     * @param float  $w     Image width
419
     * @param float  $h     Image height
420
     * @param string $align L:left, C:center, R:right or empty to use x/y
421
     * @param string $ln    T:same line, N:next line
422
     *
423
     * @return ReportBaseImage
424
     */
425
    public function createImage(string $file, float $x, float $y, float $w, float $h, string $align, string $ln): ReportBaseImage
426
    {
427
        return new ReportHtmlImage($file, $x, $y, $w, $h, $align, $ln);
428
    }
429
430
    /**
431
     * Create a new image object from Media Object.
432
     *
433
     * @param MediaFile           $media_file
434
     * @param float               $x
435
     * @param float               $y
436
     * @param float               $w     Image width
437
     * @param float               $h     Image height
438
     * @param string              $align L:left, C:center, R:right or empty to use x/y
439
     * @param string              $ln    T:same line, N:next line
440
     * @param FilesystemInterface $data_filesystem
441
     *
442
     * @return ReportBaseImage
443
     */
444
    public function createImageFromObject(
445
        MediaFile $media_file,
446
        float $x,
447
        float $y,
448
        float $w,
449
        float $h,
450
        string $align,
451
        string $ln,
452
        FilesystemInterface $data_filesystem
453
    ): ReportBaseImage {
454
        return new ReportHtmlImage($media_file->imageUrl((int) $w, (int) $h, ''), $x, $y, $w, $h, $align, $ln);
455
    }
456
457
    /**
458
     * Create a line.
459
     *
460
     * @param float $x1
461
     * @param float $y1
462
     * @param float $x2
463
     * @param float $y2
464
     *
465
     * @return ReportBaseLine
466
     */
467
    public function createLine(float $x1, float $y1, float $x2, float $y2): ReportBaseLine
468
    {
469
        return new ReportHtmlLine($x1, $y1, $x2, $y2);
470
    }
471
472
    /**
473
     * Create an HTML element.
474
     *
475
     * @param string   $tag
476
     * @param string[] $attrs
477
     *
478
     * @return ReportBaseHtml
479
     */
480
    public function createHTML(string $tag, array $attrs): ReportBaseHtml
481
    {
482
        return new ReportHtmlHtml($tag, $attrs);
483
    }
484
485
    /**
486
     * Clear the Header
487
     *
488
     * @return void
489
     */
490
    public function clearHeader(): void
491
    {
492
        $this->headerElements = [];
493
    }
494
495
    /**
496
     * Update the Page Number and set a new Y if max Y is larger - ReportHtml
497
     *
498
     * @return void
499
     */
500
    public function addPage(): void
501
    {
502
        $this->pageN++;
503
504
        // Add a little margin to max Y "between pages"
505
        $this->maxY += 10;
506
507
        // If Y is still heigher by any reason...
508
        if ($this->maxY < $this->Y) {
509
            // ... update max Y
510
            $this->maxY = $this->Y;
511
        } else {
512
            // else update Y so that nothing will be overwritten, like images or cells...
513
            $this->Y = $this->maxY;
514
        }
515
    }
516
517
    /**
518
     * Uppdate max Y to keep track it incase of a pagebreak - ReportHtml
519
     *
520
     * @param float $y
521
     *
522
     * @return void
523
     */
524
    public function addMaxY($y): void
525
    {
526
        if ($this->maxY < $y) {
527
            $this->maxY = $y;
528
        }
529
    }
530
531
    /**
532
     * Add a page header.
533
     *
534
     * @param ReportBaseElement $element
535
     *
536
     * @return void
537
     */
538
    public function addPageHeader($element): void
539
    {
540
        $this->pageHeaderElements[] = $element;
541
    }
542
543
    /**
544
     * Checks the Footnote and numbers them - ReportHtml
545
     *
546
     * @param ReportHtmlFootnote $footnote
547
     *
548
     * @return ReportHtmlFootnote|bool object if already numbered, false otherwise
549
     */
550
    public function checkFootnote(ReportHtmlFootnote $footnote)
551
    {
552
        $ct  = count($this->printedfootnotes);
553
        $i   = 0;
554
        $val = $footnote->getValue();
555
        while ($i < $ct) {
556
            if ($this->printedfootnotes[$i]->getValue() === $val) {
557
                // If this footnote already exist then set up the numbers for this object
558
                $footnote->setNum($i + 1);
559
                $footnote->setAddlink((string) ($i + 1));
560
561
                return $this->printedfootnotes[$i];
562
            }
563
            $i++;
564
        }
565
        // If this Footnote has not been set up yet
566
        $footnote->setNum($ct + 1);
567
        $footnote->setAddlink((string) ($ct + 1));
568
        $this->printedfootnotes[] = $footnote;
569
570
        return false;
571
    }
572
573
    /**
574
     * Clear the Page Header - ReportHtml
575
     *
576
     * @return void
577
     */
578
    public function clearPageHeader(): void
579
    {
580
        $this->pageHeaderElements = [];
581
    }
582
583
    /**
584
     * Count the number of lines - ReportHtml
585
     *
586
     * @param string $str
587
     *
588
     * @return int Number of lines. 0 if empty line
589
     */
590
    public function countLines($str): int
591
    {
592
        if ($str === '') {
593
            return 0;
594
        }
595
596
        return substr_count($str, "\n") + 1;
597
    }
598
599
    /**
600
     * Get the current style.
601
     *
602
     * @return string
603
     */
604
    public function getCurrentStyle(): string
605
    {
606
        return $this->currentStyle;
607
    }
608
609
    /**
610
     * Get the current style height.
611
     *
612
     * @return float
613
     */
614
    public function getCurrentStyleHeight(): float
615
    {
616
        if (empty($this->currentStyle)) {
617
            return $this->default_font_size;
618
        }
619
        $style = $this->getStyle($this->currentStyle);
620
621
        return (float) $style['size'];
622
    }
623
624
    /**
625
     * Get the current footnotes height.
626
     *
627
     * @param float $cellWidth
628
     *
629
     * @return float
630
     */
631
    public function getFootnotesHeight(float $cellWidth): float
632
    {
633
        $h = 0;
634
        foreach ($this->printedfootnotes as $element) {
635
            $h += $element->getFootnoteHeight($this, $cellWidth);
636
        }
637
638
        return $h;
639
    }
640
641
    /**
642
     * Get the maximum width from current position to the margin - ReportHtml
643
     *
644
     * @return float
645
     */
646
    public function getRemainingWidth(): float
647
    {
648
        return $this->noMarginWidth - $this->X;
649
    }
650
651
    /**
652
     * Get the page height.
653
     *
654
     * @return float
655
     */
656
    public function getPageHeight(): float
657
    {
658
        return $this->page_height - $this->top_margin;
659
    }
660
661
    /**
662
     * Get the width of a string.
663
     *
664
     * @param string $text
665
     *
666
     * @return float
667
     */
668
    public function getStringWidth(string $text): float
669
    {
670
        $style = $this->getStyle($this->currentStyle);
671
672
        return mb_strlen($text) * ($style['size'] / 2);
673
    }
674
675
    /**
676
     * Get a text height in points - ReportHtml
677
     *
678
     * @param string $str
679
     *
680
     * @return float
681
     */
682
    public function getTextCellHeight(string $str): float
683
    {
684
        // Count the number of lines to calculate the height
685
        $nl = $this->countLines($str);
686
687
        // Calculate the cell height
688
        return ceil(($this->getCurrentStyleHeight() * $this->cellHeightRatio) * $nl);
689
    }
690
691
    /**
692
     * Get the current X position - ReportHtml
693
     *
694
     * @return float
695
     */
696
    public function getX(): float
697
    {
698
        return $this->X;
699
    }
700
701
    /**
702
     * Get the current Y position - ReportHtml
703
     *
704
     * @return float
705
     */
706
    public function getY(): float
707
    {
708
        return $this->Y;
709
    }
710
711
    /**
712
     * Get the current page number - ReportHtml
713
     *
714
     * @return int
715
     */
716
    public function pageNo(): int
717
    {
718
        return $this->pageN;
719
    }
720
721
    /**
722
     * Set the current style.
723
     *
724
     * @param string $s
725
     *
726
     * @void
727
     */
728
    public function setCurrentStyle(string $s): void
729
    {
730
        $this->currentStyle = $s;
731
    }
732
733
    /**
734
     * Set the X position - ReportHtml
735
     *
736
     * @param float $x
737
     *
738
     * @return void
739
     */
740
    public function setX($x): void
741
    {
742
        $this->X = $x;
743
    }
744
745
    /**
746
     * Set the Y position - ReportHtml
747
     *
748
     * Also updates Max Y position
749
     *
750
     * @param float $y
751
     *
752
     * @return void
753
     */
754
    public function setY($y): void
755
    {
756
        $this->Y = $y;
757
        if ($this->maxY < $y) {
758
            $this->maxY = $y;
759
        }
760
    }
761
762
    /**
763
     * Set the X and Y position - ReportHtml
764
     *
765
     * Also updates Max Y position
766
     *
767
     * @param float $x
768
     * @param float $y
769
     *
770
     * @return void
771
     */
772
    public function setXy($x, $y): void
773
    {
774
        $this->setX($x);
775
        $this->setY($y);
776
    }
777
778
    /**
779
     * Wrap text - ReportHtml
780
     *
781
     * @param string $str   Text to wrap
782
     * @param float  $width Width in points the text has to fit into
783
     *
784
     * @return string
785
     */
786
    public function textWrap(string $str, float $width): string
787
    {
788
        // Calculate the line width
789
        $lw = (int) ($width / ($this->getCurrentStyleHeight() / 2));
790
        // Wordwrap each line
791
        $lines = explode("\n", $str);
792
        // Line Feed counter
793
        $lfct     = count($lines);
794
        $wraptext = '';
795
        foreach ($lines as $line) {
796
            $wtext = FunctionsRtl::utf8WordWrap($line, $lw, "\n", true);
797
            $wraptext .= $wtext;
798
            // Add a new line as long as it’s not the last line
799
            if ($lfct > 1) {
800
                $wraptext .= "\n";
801
            }
802
            $lfct--;
803
        }
804
805
        return $wraptext;
806
    }
807
808
    /**
809
     * Write text - ReportHtml
810
     *
811
     * @param string $text  Text to print
812
     * @param string $color HTML RGB color code (Ex: #001122)
813
     * @param bool   $useclass
814
     *
815
     * @return void
816
     */
817
    public function write($text, $color = '', $useclass = true): void
818
    {
819
        $style    = $this->getStyle($this->getCurrentStyle());
820
        $htmlcode = '<span dir="' . I18N::direction() . '"';
821
        if ($useclass) {
822
            $htmlcode .= ' class="' . $style['name'] . '"';
823
        }
824
        // Check if Text Color is set and if it’s valid HTML color
825
        if (preg_match('/#?(..)(..)(..)/', $color)) {
826
            $htmlcode .= ' style="color:' . $color . ';"';
827
        }
828
829
        $htmlcode .= '>' . $text . '</span>';
830
        $htmlcode = str_replace([
831
            "\n",
832
            '> ',
833
            ' <',
834
        ], [
835
            '<br>',
836
            '>&nbsp;',
837
            '&nbsp;<',
838
        ], $htmlcode);
839
        echo $htmlcode;
840
    }
841
}
842