1 | <?php |
||||
2 | |||||
3 | namespace Bavix\Flow; |
||||
4 | |||||
5 | use Bavix\Exceptions\Invalid; |
||||
6 | use Bavix\Exceptions\Runtime; |
||||
7 | use Bavix\Flow\Directives\WithDirective; |
||||
8 | use Bavix\Flow\Minify\HTML; |
||||
9 | use Bavix\Helpers\Arr; |
||||
10 | use Bavix\Helpers\JSON; |
||||
11 | use Bavix\Helpers\Str; |
||||
12 | use Bavix\Lexer\Lexer; |
||||
13 | use Bavix\Lexer\Token; |
||||
14 | use Bavix\Lexer\Validator; |
||||
15 | use JSMin\JSMin; |
||||
16 | |||||
17 | class Flow |
||||
18 | { |
||||
19 | |||||
20 | const VERSION = '1.0.4'; |
||||
21 | |||||
22 | /** |
||||
23 | * @var string |
||||
24 | */ |
||||
25 | protected $ext; |
||||
26 | |||||
27 | /** |
||||
28 | * @var Lexer |
||||
29 | */ |
||||
30 | protected $lexer; |
||||
31 | |||||
32 | /** |
||||
33 | * @var Lexeme |
||||
34 | */ |
||||
35 | protected $lexeme; |
||||
36 | |||||
37 | /** |
||||
38 | * @var array |
||||
39 | */ |
||||
40 | protected $literals; |
||||
41 | |||||
42 | /** |
||||
43 | * @var array |
||||
44 | */ |
||||
45 | protected $printers; |
||||
46 | |||||
47 | /** |
||||
48 | * @var array |
||||
49 | */ |
||||
50 | protected $operators; |
||||
51 | |||||
52 | /** |
||||
53 | * @var array |
||||
54 | */ |
||||
55 | protected $directives = []; |
||||
56 | |||||
57 | /** |
||||
58 | * @var array |
||||
59 | */ |
||||
60 | protected $mapDirectives = []; |
||||
61 | |||||
62 | /** |
||||
63 | * @var array |
||||
64 | */ |
||||
65 | protected $constructs = []; |
||||
66 | |||||
67 | /** |
||||
68 | * @var array |
||||
69 | */ |
||||
70 | protected $lexemes = []; |
||||
71 | |||||
72 | /** |
||||
73 | * @var array |
||||
74 | */ |
||||
75 | protected $folders = []; |
||||
76 | |||||
77 | /** |
||||
78 | * @var array |
||||
79 | */ |
||||
80 | protected $rows; |
||||
81 | |||||
82 | /** |
||||
83 | * @var Native |
||||
84 | */ |
||||
85 | protected $native; |
||||
86 | |||||
87 | /** |
||||
88 | * @var FileSystem |
||||
89 | */ |
||||
90 | protected $fileSystem; |
||||
91 | |||||
92 | /** |
||||
93 | * @var string |
||||
94 | */ |
||||
95 | protected $tpl; |
||||
96 | |||||
97 | /** |
||||
98 | * @var bool |
||||
99 | */ |
||||
100 | protected $debug; |
||||
101 | |||||
102 | /** |
||||
103 | * @var bool |
||||
104 | */ |
||||
105 | protected $minify; |
||||
106 | |||||
107 | /** |
||||
108 | * @var array |
||||
109 | */ |
||||
110 | protected $extends; |
||||
111 | |||||
112 | /** |
||||
113 | * @var string |
||||
114 | */ |
||||
115 | protected $pathCompile; |
||||
116 | |||||
117 | /** |
||||
118 | * Flow constructor. |
||||
119 | * |
||||
120 | * @param Native $native |
||||
121 | * @param array $options |
||||
122 | */ |
||||
123 | 22 | public function __construct(Native $native = null, array $options = []) |
|||
124 | { |
||||
125 | // configs |
||||
126 | 22 | $this->mapDirectives = $options['directives'] ?? []; |
|||
127 | 22 | $this->folders = $options['folders'] ?? []; |
|||
128 | 22 | $this->lexemes = $options['lexemes'] ?? []; |
|||
129 | 22 | $this->minify = $options['minify'] ?? false; |
|||
130 | 22 | $this->extends = $options['extends'] ?? []; |
|||
131 | 22 | $this->debug = $options['debug'] ?? false; |
|||
132 | 22 | $this->ext = $options['ext'] ?? 'bxf'; |
|||
133 | |||||
134 | 22 | Cache::setPool($options['cache'] ?? null); |
|||
135 | |||||
136 | // props |
||||
137 | 22 | $this->constructs = Property::get('constructs'); |
|||
138 | // /props |
||||
139 | |||||
140 | 22 | $this->pathCompile = $options['compile'] ?? sys_get_temp_dir(); |
|||
141 | |||||
142 | 22 | $this->setNative($native); |
|||
143 | 22 | } |
|||
144 | |||||
145 | 11 | protected function loadLexemes(): self |
|||
146 | { |
||||
147 | 11 | foreach ($this->lexemes as $folder) |
|||
148 | { |
||||
149 | $this->lexeme->addFolder($folder); |
||||
150 | } |
||||
151 | |||||
152 | 11 | return $this; |
|||
153 | } |
||||
154 | |||||
155 | /** |
||||
156 | * @param Lexeme $lexeme |
||||
157 | * |
||||
158 | * @return $this |
||||
159 | */ |
||||
160 | 11 | public function setLexeme(Lexeme $lexeme): self |
|||
161 | { |
||||
162 | 11 | $this->lexeme = $lexeme; |
|||
163 | |||||
164 | 11 | return $this->loadLexemes(); |
|||
165 | } |
||||
166 | |||||
167 | /** |
||||
168 | * @param Lexer $lexer |
||||
169 | * |
||||
170 | * @return $this |
||||
171 | */ |
||||
172 | public function setLexer(Lexer $lexer): self |
||||
173 | { |
||||
174 | $this->lexer = $lexer; |
||||
175 | |||||
176 | return $this; |
||||
177 | } |
||||
178 | |||||
179 | /** |
||||
180 | * @return Lexeme |
||||
181 | */ |
||||
182 | 11 | public function lexeme(): Lexeme |
|||
183 | { |
||||
184 | 11 | if (!$this->lexeme) |
|||
185 | { |
||||
186 | 11 | $this->setLexeme(new Lexeme($this)); |
|||
187 | } |
||||
188 | |||||
189 | 11 | return $this->lexeme; |
|||
190 | } |
||||
191 | |||||
192 | /** |
||||
193 | * @return Lexer |
||||
194 | */ |
||||
195 | 18 | public function lexer(): Lexer |
|||
196 | { |
||||
197 | 18 | if (!$this->lexer) |
|||
198 | { |
||||
199 | 18 | $this->lexer = new Lexer(); |
|||
200 | } |
||||
201 | |||||
202 | 18 | return $this->lexer; |
|||
203 | } |
||||
204 | |||||
205 | /** |
||||
206 | * @return bool |
||||
207 | */ |
||||
208 | 18 | public function debugMode(): bool |
|||
209 | { |
||||
210 | 18 | return $this->debug; |
|||
211 | } |
||||
212 | |||||
213 | /** |
||||
214 | * @return FileSystem |
||||
215 | */ |
||||
216 | 18 | public function fileSystem(): FileSystem |
|||
217 | { |
||||
218 | 18 | if (!$this->fileSystem) |
|||
219 | { |
||||
220 | 18 | $this->fileSystem = new FileSystem( |
|||
221 | 18 | $this, |
|||
222 | 18 | $this->pathCompile |
|||
223 | ); |
||||
224 | } |
||||
225 | |||||
226 | 18 | return $this->fileSystem; |
|||
227 | } |
||||
228 | |||||
229 | /** |
||||
230 | * @return string |
||||
231 | */ |
||||
232 | 18 | public function ext(): string |
|||
233 | { |
||||
234 | 18 | return '.' . $this->ext; |
|||
235 | } |
||||
236 | |||||
237 | 22 | protected function setNative($native) |
|||
238 | { |
||||
239 | 22 | if ($native) |
|||
240 | { |
||||
241 | 18 | $this->native = $native; |
|||
242 | 18 | $this->native->setFlow($this); |
|||
243 | |||||
244 | 18 | foreach ($this->folders as $folder => $path) |
|||
245 | { |
||||
246 | 18 | $this->native->addFolder($folder, $path); |
|||
247 | } |
||||
248 | } |
||||
249 | 22 | } |
|||
250 | |||||
251 | /** |
||||
252 | * @return Native |
||||
253 | */ |
||||
254 | 18 | public function native(): Native |
|||
255 | { |
||||
256 | 18 | if (!$this->native) |
|||
257 | { |
||||
258 | 18 | $this->setNative(new Native()); |
|||
259 | } |
||||
260 | |||||
261 | 18 | return $this->native; |
|||
262 | } |
||||
263 | |||||
264 | /** |
||||
265 | * @param array $tokens |
||||
266 | * |
||||
267 | * @return string |
||||
268 | */ |
||||
269 | protected function fragment(array $tokens): string |
||||
270 | { |
||||
271 | 11 | $data = Arr::map($tokens['tokens'] ?? $tokens, function (Token $token) { |
|||
272 | 11 | return $token->token; |
|||
273 | 11 | }); |
|||
274 | |||||
275 | 11 | return \str_replace( |
|||
276 | 11 | '. ', |
|||
277 | 11 | '.', |
|||
278 | 11 | \implode(' ', $data) |
|||
279 | ); |
||||
280 | } |
||||
281 | |||||
282 | 18 | public function build(array $data): string |
|||
283 | { |
||||
284 | 18 | $self = $this; |
|||
285 | 18 | $_storeKey = __CLASS__ . JSON::encode($data); |
|||
286 | |||||
287 | 18 | return Cache::get($_storeKey, function () use ($self, &$data) { |
|||
288 | 17 | return $self->buildWithoutCache($data); |
|||
289 | 18 | }); |
|||
290 | } |
||||
291 | |||||
292 | /** |
||||
293 | * @param array $data |
||||
294 | * |
||||
295 | * @return string |
||||
296 | */ |
||||
297 | 17 | public function buildWithoutCache(array $data): string |
|||
298 | { |
||||
299 | 17 | $code = []; |
|||
300 | 17 | $lastLast = null; |
|||
301 | 17 | $last = null; |
|||
302 | |||||
303 | /** |
||||
304 | * @var Token $token |
||||
305 | * @var Token $last |
||||
306 | * @var Token $lastLast |
||||
307 | */ |
||||
308 | 17 | foreach ($data['tokens'] as $token) |
|||
309 | { |
||||
310 | 17 | $_token = clone $token; |
|||
311 | |||||
312 | 17 | if ($_token->type === T_OBJECT_OPERATOR) |
|||
313 | { |
||||
314 | throw new Invalid('Undefined object operator `->`!'); |
||||
315 | } |
||||
316 | |||||
317 | 17 | if (Arr::in([T_NEW, T_CLONE, T_INSTEADOF, T_INSTANCEOF, T_AS], $_token->type)) |
|||
318 | { |
||||
319 | $lastLast = $last; |
||||
320 | $last = $_token; |
||||
321 | |||||
322 | if (!Arr::in([T_NEW, T_CLASS], $_token->type)) |
||||
323 | { |
||||
324 | $code[] = ' '; |
||||
325 | } |
||||
326 | |||||
327 | $code[] = $_token->token; |
||||
328 | $code[] = ' '; |
||||
329 | continue; |
||||
330 | } |
||||
331 | |||||
332 | 17 | if ($last && (!$lastLast || |
|||
333 | 2 | ($lastLast->type !== T_VARIABLE && |
|||
334 | 2 | $lastLast->type !== Validator::T_ENDBRACKET && |
|||
335 | 17 | $lastLast->type !== Validator::T_ENDARRAY)) |
|||
336 | 17 | && $last->type === Validator::T_DOT) |
|||
337 | { |
||||
338 | $pop = Arr::pop($code); |
||||
339 | Arr::push($code, '\\' . WithDirective::class . '::last()'); |
||||
340 | Arr::push($code, $pop); |
||||
341 | } |
||||
342 | |||||
343 | 17 | if ($_token->type === Validator::T_CONCAT) |
|||
344 | { |
||||
345 | $_token->token = '.'; |
||||
346 | } |
||||
347 | |||||
348 | 17 | if ((!$last || ($last && $last->type !== Validator::T_DOT)) && $_token->type === T_FUNCTION) |
|||
349 | { |
||||
350 | if (Str::ucFirst($_token->token) !== $_token->token && |
||||
351 | !Arr::in($this->constructs, $_token->token)) |
||||
352 | { |
||||
353 | $_token->token = '$this->helper->' . $_token->token; |
||||
354 | } |
||||
355 | } |
||||
356 | |||||
357 | 17 | if (Arr::in([Validator::T_BRACKET, T_ARRAY], $_token->type)) |
|||
358 | { |
||||
359 | 1 | if ($last && $last->type === Validator::T_DOT) |
|||
360 | { |
||||
361 | Arr::pop($code); |
||||
362 | } |
||||
363 | } |
||||
364 | |||||
365 | 17 | if (Arr::in([T_VARIABLE, T_FUNCTION], $_token->type)) |
|||
366 | { |
||||
367 | 17 | $_token->token = \str_replace('.', '->', $_token->token); |
|||
368 | |||||
369 | 17 | if ($last && $last->type === Validator::T_DOT) |
|||
370 | { |
||||
371 | Arr::pop($code); |
||||
372 | Arr::push($code, '->'); |
||||
373 | } |
||||
374 | |||||
375 | 17 | if (Str::ucFirst($_token->token) === $_token->token) |
|||
376 | { |
||||
377 | $_token->type = T_CLASS; |
||||
378 | } |
||||
379 | |||||
380 | 17 | if (!Arr::in([T_FUNCTION, T_CLASS], $_token->type) && |
|||
381 | 17 | (!$last || !Arr::in([ |
|||
382 | 2 | Validator::T_ENDBRACKET, |
|||
383 | 2 | Validator::T_ENDARRAY, |
|||
384 | 2 | Validator::T_DOT, |
|||
385 | 2 | T_NS_SEPARATOR |
|||
386 | 17 | ], $last->type))) |
|||
387 | { |
||||
388 | 17 | $_token->token = '$this->' . $_token->token; |
|||
389 | } |
||||
390 | } |
||||
391 | |||||
392 | 17 | $lastLast = $last; |
|||
393 | 17 | $last = $_token; |
|||
394 | 17 | $code[] = $_token->token; |
|||
395 | } |
||||
396 | |||||
397 | 17 | return \implode($code); |
|||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
The call to
implode() has too few arguments starting with pieces .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.
Loading history...
|
|||||
398 | } |
||||
399 | |||||
400 | /** |
||||
401 | * @param string $view |
||||
402 | * |
||||
403 | * @return string |
||||
404 | */ |
||||
405 | 18 | protected function minify(string $view): string |
|||
406 | { |
||||
407 | 18 | $html = $this->compile($view); |
|||
408 | |||||
409 | 17 | if ($this->minify) |
|||
410 | { |
||||
411 | 1 | $html = \Minify_HTML::minify(\trim($html), [ |
|||
412 | 1 | 'cssMinifier' => [\Minify_CSSmin::class, 'minify'], |
|||
413 | 'jsMinifier' => [JSMin::class, 'minify'], |
||||
414 | ]); |
||||
415 | } |
||||
416 | |||||
417 | 17 | if (!empty($this->extends)) |
|||
418 | { |
||||
419 | 3 | $html = (new HTML($html, $this->extends)) |
|||
420 | 3 | ->apply(); |
|||
421 | } |
||||
422 | |||||
423 | 17 | return $html; |
|||
424 | } |
||||
425 | |||||
426 | /** |
||||
427 | * @param string $view |
||||
428 | * |
||||
429 | * @return string |
||||
430 | */ |
||||
431 | 18 | public function path(string $view): string |
|||
432 | { |
||||
433 | 18 | if (!$this->fileSystem()->has($view)) |
|||
434 | { |
||||
435 | 18 | $this->fileSystem()->set($view, $this->minify($view)); |
|||
436 | } |
||||
437 | |||||
438 | 17 | return $this->fileSystem()->get($view); |
|||
439 | } |
||||
440 | |||||
441 | /** |
||||
442 | * @param array $rows |
||||
443 | * @param bool $escape |
||||
444 | */ |
||||
445 | 18 | protected function printers(array $rows, $escape = true) |
|||
446 | { |
||||
447 | 18 | $begin = $escape ? '\\htmlspecialchars(' : ''; |
|||
448 | 18 | $end = $escape ? ', ENT_QUOTES, \'UTF-8\')' : ''; |
|||
449 | |||||
450 | 18 | foreach ($rows as $row) |
|||
451 | { |
||||
452 | 16 | $this->tpl = $this->replace( |
|||
453 | 16 | $row['code'], |
|||
454 | 16 | '<?php echo ' . $begin . $this->build($row) . $end . '; ?>' |
|||
455 | ); |
||||
456 | } |
||||
457 | 18 | } |
|||
458 | |||||
459 | /** |
||||
460 | * @param string $key |
||||
461 | * @param array $data |
||||
462 | * @param array $operator |
||||
463 | * |
||||
464 | * @return mixed |
||||
465 | */ |
||||
466 | 11 | protected function directive(string $key, array $data, array $operator) |
|||
467 | { |
||||
468 | 11 | $class = __NAMESPACE__ . '\\Directives\\' . Str::ucFirst($key) . 'Directive'; |
|||
469 | |||||
470 | 11 | if (isset($this->mapDirectives[$key])) |
|||
471 | { |
||||
472 | $class = $this->mapDirectives[$key]; |
||||
473 | } |
||||
474 | |||||
475 | 11 | return new $class($this, $data, $operator); |
|||
476 | } |
||||
477 | |||||
478 | /** |
||||
479 | * @param string $key |
||||
480 | * @param Directive $directive |
||||
481 | */ |
||||
482 | 11 | protected function pushDirective(string $key, Directive $directive) |
|||
483 | { |
||||
484 | 11 | if (empty($this->directives[$key])) |
|||
485 | { |
||||
486 | 11 | $this->directives[$key] = []; |
|||
487 | } |
||||
488 | |||||
489 | 11 | $this->directives[$key][] = $directive; |
|||
490 | 11 | } |
|||
491 | |||||
492 | /** |
||||
493 | * @param string $key |
||||
494 | * |
||||
495 | * @return Directive |
||||
496 | */ |
||||
497 | 6 | protected function popDirective(string $key): Directive |
|||
498 | { |
||||
499 | 6 | return Arr::pop($this->directives[$key]); |
|||
500 | } |
||||
501 | |||||
502 | /** |
||||
503 | * @param string $fragment |
||||
504 | * @param string $code |
||||
505 | * @param string|null $tpl |
||||
506 | * |
||||
507 | * @return string |
||||
508 | */ |
||||
509 | 18 | protected function replace(string $fragment, string $code, string $tpl = null): string |
|||
510 | { |
||||
511 | 18 | if (!$tpl) |
|||
0 ignored issues
–
show
The expression
$tpl of type null|string is loosely compared to false ; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.
In PHP, under loose comparison (like For '' == false // true
'' == null // true
'ab' == false // false
'ab' == null // false
// It is often better to use strict comparison
'' === false // false
'' === null // false
Loading history...
|
|||||
512 | { |
||||
513 | 18 | $tpl = $this->tpl; |
|||
514 | } |
||||
515 | |||||
516 | 18 | return \preg_replace( |
|||
517 | 18 | '~' . \preg_quote($fragment, '~') . '~u', |
|||
518 | 18 | $code, |
|||
519 | 18 | $tpl, |
|||
520 | 18 | 1 |
|||
521 | ); |
||||
522 | } |
||||
523 | |||||
524 | /** |
||||
525 | * @param array $operator |
||||
526 | * @param string $key |
||||
527 | * |
||||
528 | * @return bool |
||||
529 | */ |
||||
530 | 11 | protected function ifEnd($operator, string $key): bool |
|||
531 | { |
||||
532 | 11 | if (0 === Str::pos($key, 'end')) |
|||
533 | { |
||||
534 | 6 | $key = Str::sub($key, 3); |
|||
535 | 6 | $data = $this->lexeme()->data($key); |
|||
536 | |||||
537 | 6 | if (true !== $data && $this->lexeme()->closed($key)) |
|||
538 | { |
||||
539 | 6 | $dir = $this->popDirective($key); |
|||
540 | |||||
541 | 6 | $this->tpl = $this->replace( |
|||
542 | 6 | $operator['code'], |
|||
543 | 6 | $dir->endDirective() |
|||
544 | ); |
||||
545 | } |
||||
546 | |||||
547 | 6 | return !$data; |
|||
548 | } |
||||
549 | |||||
550 | 11 | return false; |
|||
551 | } |
||||
552 | |||||
553 | 18 | protected function operators() |
|||
554 | { |
||||
555 | 18 | foreach ($this->operators as $operator) |
|||
556 | { |
||||
557 | /** |
||||
558 | * @var Token $_token |
||||
559 | */ |
||||
560 | 11 | $_token = current($operator['tokens']); |
|||
561 | 11 | $data = $this->lexeme()->data($_token->token); |
|||
562 | |||||
563 | 11 | $end = !$this->ifEnd($operator, $_token->token); |
|||
564 | |||||
565 | 11 | if ($end && true !== $data) |
|||
566 | { |
||||
567 | 11 | $data = $this->lexeme()->apply( |
|||
568 | 11 | $_token->token, |
|||
569 | 11 | $this->fragment($operator) |
|||
570 | ); |
||||
571 | |||||
572 | /** |
||||
573 | * @var Directive $directive |
||||
574 | */ |
||||
575 | 11 | $directive = $this->directive($_token->token, $data ?: [], $operator); |
|||
576 | 11 | $this->pushDirective($_token->token, $directive); |
|||
577 | |||||
578 | 11 | $this->tpl = $this->replace( |
|||
579 | 11 | $operator['code'], |
|||
580 | 11 | $directive->render() |
|||
581 | ); |
||||
582 | } |
||||
583 | } |
||||
584 | 18 | } |
|||
585 | |||||
586 | /** |
||||
587 | * @param string $view |
||||
588 | * @param array $data |
||||
589 | * |
||||
590 | * @return string |
||||
591 | */ |
||||
592 | 18 | public function render(string $view, array $data = []): string |
|||
593 | { |
||||
594 | 18 | return $this->native()->render( |
|||
595 | 18 | $this->path($view), |
|||
596 | 17 | $data |
|||
597 | ); |
||||
598 | } |
||||
599 | |||||
600 | /** |
||||
601 | * @param string $view |
||||
602 | * |
||||
603 | * @return string |
||||
604 | */ |
||||
605 | 18 | public function compile(string $view): string |
|||
606 | { |
||||
607 | 18 | $path = $this->native()->path($view . $this->ext()); |
|||
608 | 18 | $this->tpl = \file_get_contents($path); |
|||
609 | 18 | $tokens = $this->lexer()->tokens($this->tpl); |
|||
610 | |||||
611 | 18 | $this->literals = $tokens[Lexer::LITERAL]; |
|||
612 | 18 | $this->printers = $tokens[Lexer::PRINTER]; |
|||
613 | 18 | $this->operators = $tokens[Lexer::OPERATOR]; |
|||
614 | 18 | $this->rows = $tokens[Lexer::RAW]; |
|||
615 | |||||
616 | 18 | $this->printers($this->printers); |
|||
617 | 18 | $this->printers($this->rows, false); |
|||
618 | 18 | $this->operators(); |
|||
619 | |||||
620 | // check directives |
||||
621 | 18 | foreach ($this->directives as $name => $items) |
|||
622 | { |
||||
623 | 11 | if ($this->lexeme()->closed($name)) |
|||
624 | { |
||||
625 | 7 | if (!empty($items)) |
|||
626 | { |
||||
627 | 1 | throw new Runtime( |
|||
628 | 1 | \sprintf( |
|||
629 | 1 | 'Directive %s not closed', |
|||
630 | 11 | \get_class(Arr::pop($items)) |
|||
631 | ) |
||||
632 | ); |
||||
633 | } |
||||
634 | } |
||||
635 | } |
||||
636 | |||||
637 | 17 | foreach ($this->literals as $key => $literal) |
|||
638 | { |
||||
639 | $this->tpl = \str_replace($key, $literal, $this->tpl); |
||||
640 | } |
||||
641 | |||||
642 | 17 | return $this->tpl; |
|||
643 | } |
||||
644 | |||||
645 | } |
||||
646 |