From 877053c1fb3ea13c0a6f4a22599f758b7fed8815 Mon Sep 17 00:00:00 2001 From: Nick Sagona Date: Tue, 12 Dec 2023 16:25:34 -0600 Subject: [PATCH] Add alertBox and update README --- README.md | 189 ++++++++++++++++++++++++++++++++---------- composer.json | 2 +- src/Color.php | 2 +- src/Command.php | 4 +- src/Console.php | 105 +++++++++++++++++++++-- src/Exception.php | 2 +- tests/ConsoleTest.php | 96 ++++++++++++++++++++- tests/tmp/alerts.png | Bin 0 -> 19050 bytes 8 files changed, 344 insertions(+), 56 deletions(-) create mode 100644 tests/tmp/alerts.png diff --git a/README.md b/README.md index 446eef3..49e4859 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,9 @@ pop-console * [Quickstart](#quickstart) * [Response Buffer](#response-buffer) * [Colors](#colors) +* [Lines](#lines) +* [Headers](#headers) +* [Alerts](#alert) * [Prompt](#prompt) * [Commands](#commands) * [Help Screen](#help-screen) @@ -44,7 +47,7 @@ Install `pop-console` using Composer. Or, require it in your composer.json file "require": { - "popphp/pop-console" : "^4.0.0" + "popphp/pop-console" : "^4.1.0" } [Top](#pop-console) @@ -61,8 +64,8 @@ a prepended header and appended footer. use Pop\Console\Console; $console = new Console(); -$console->setHeader('My Application'); -$console->setFooter('The End'); +$console->setHeader('My Application'); // Set a global header at the start of the script +$console->setFooter('The End'); // Set a global footer at the end of the script $console->append('Here is some console information.'); $console->append('Hope you enjoyed it!'); @@ -80,14 +83,15 @@ The above will output: The End ``` -### Enforcing a terminal width +### Console wrap and margin -The allowed text width can be enforced by passing the `$width` parameter to the constructor: +By default, the console object enforces a wrap width at 80 characters and provides a margin of 4 spaces for readability. +These values can be changed to whatever is needed for the application. ```php use Pop\Console\Console; -$console = new Console(40); +$console = new Console(40, 2); // wrap width of 40, margin of 2 spaces $console->append( 'Here is some console information. This is a really long string. It will have to wrap.' ); @@ -95,29 +99,9 @@ $console->send(); ``` ```text - Here is some console information. This - is a really long string. It will have to - wrap. -``` - -### Setting an indent - -By default, an indent of four spaces is set to provide a margin from the edge of the terminal. This can be adjusted -or turned off by passing it to the constructor: - -```php -use Pop\Console\Console; - -$console = new Console(40, ' '); -$console->append( - 'Here is some console information using a 2 space indent. It will have to wrap.' -); -$console->send(); -``` - -```text - Here is some console information using a - 2 space indent. It will have to wrap. + Here is some console information. This + is a really long string. It will have to + wrap. ``` [Top](#pop-console) @@ -145,9 +129,9 @@ $console->write( ); ``` -### Newlines and Indents +### Newline and Margin -By default, calling the `append()` or `write()` methods will produce the indent value at the beginning +By default, calling the `append()` or `write()` methods will produce the margin value at the beginning of the content and a newline at the end of the content. If this is not the desired behavior, boolean flags can be passed to control this: @@ -155,14 +139,10 @@ can be passed to control this: use Pop\Console\Console; $console = new Console(40); -$console->write('Here ', false); // No new line, but use indent -$console->write('is ', false, false); // No new line, no indent -$console->write('some ', false, false); // No new line, no indent -$console->write('content.', true, false); // Use new line, but no indent -``` - -```text - Here is some content. +$console->write('Here ', false); // No new line, but use margin +$console->write('is ', false, false); // No new line, no margin +$console->write('some ', false, false); // No new line, no margin +$console->write('content.', true, false); // Use new line, but no margin ``` [Top](#pop-console) @@ -171,12 +151,16 @@ Colors ------ On a console terminal that supports it, you can colorize text outputted to the console -with the ``colorize()`` method: +with the ``Pop\Console\Color::colorize()`` method: ```php -$console->append( +use Pop\Console\Console; +use Pop\Console\Color; + +$console = new Console(); +$console->write( 'Here is some ' . - $console->colorize('IMPORTANT', Console::BOLD_RED) . + Color::colorize('IMPORTANT', Color::BOLD_RED) . ' console information.' ); ``` @@ -192,7 +176,15 @@ Available color constants include: - MAGENTA - CYAN - WHITE -- GRAY +- BRIGHT_BLACK +- BRIGHT_RED +- BRIGHT_GREEN +- BRIGHT_YELLOW +- BRIGHT_BLUE +- BRIGHT_MAGENTA +- BRIGHT_CYAN +- BRIGHT_WHITE +- BOLD_BLACK - BOLD_RED - BOLD_GREEN - BOLD_YELLOW @@ -200,6 +192,103 @@ Available color constants include: - BOLD_MAGENTA - BOLD_CYAN - BOLD_WHITE +- BRIGHT_BOLD_BLACK +- BRIGHT_BOLD_RED +- BRIGHT_BOLD_GREEN +- BRIGHT_BOLD_YELLOW +- BRIGHT_BOLD_BLUE +- BRIGHT_BOLD_MAGENTA +- BRIGHT_BOLD_CYAN +- BRIGHT_BOLD_WHITE + +[Top](#pop-console) + +Lines +----- + +The `line()` method provides a way to print a horizontal line rule out to the terminal. The default +character for the line is a dash `-`, but any character can be passed into the method. + +```php +use Pop\Console\Console; + +$console = new Console(); +$console->line(); +``` + +It will default to the wrap width of the console object. If no wrap width is available, it will take on +the width of the terminal, unless a custom width is specified: + +```php +use Pop\Console\Console; + +$console = new Console(); +$console->line('=', 20); +``` + +```text + ==================== +``` + +[Top](#pop-console) + +Headers +------- + +The `header()` method provides a way to output a separate block of text with an underline emphasis: + +```php +use Pop\Console\Console; + +$console = new Console(80); +$console->header('Hello World'); +``` + +```text + Hello World + ----------- +``` + +The character, size and alignment can be controlled as well: + +```php +use Pop\Console\Console; + +$console = new Console(); +$console->header('Hello World', '=', 40, 'center'); +``` + +```text + Hello World + ======================================== +``` + +[Top](#pop-console) + +Alerts +------ + +Alerts are specially formatted boxes that provide style and enhancement to the user's experience +in regard to important information and notifications. + +```php +use Pop\Console\Console; + +$console = new Console(40); +$console->alertDanger('Hello World', 'auto'); +$console->alertWarning('Hello World', 'auto'); +$console->alertSuccess('Hello World', 'auto'); +$console->alertInfo('Hello World', 'auto'); +$console->alertPrimary('Hello World', 'auto'); +$console->alertSecondary('Hello World', 'auto'); +$console->alertDark('Hello World', 'auto'); +$console->alertLight('Hello World', 'auto'); +$console->alertBox('Hello World', '-', '|', 'auto'); +``` + +The above code will produce the following output to the console terminal: + +![Alerts](tests/tmp/alerts.png) [Top](#pop-console) @@ -245,6 +334,18 @@ $ ./app Your favorite letter is B. ``` +### Confirm + +The `confirm()` is a shorthand version of a prompt to ask if the user is sure they want to proceed, +else the application will exit: + +```php +use Pop\Console\Console; + +$console = new Console(); +$console->confirm(); +$console->write('The user said yes.'); +``` [Top](#pop-console) @@ -316,7 +417,7 @@ Let's take a look at the abstract constructor of the `pop-kettle` component. $this->console = $console; $this->console->setHelpColors( - Console::BOLD_CYAN, Console::BOLD_GREEN, Console::BOLD_MAGENTA + Color::BOLD_CYAN, Color::BOLD_GREEN, Color::BOLD_MAGENTA ); $this->console->addCommandsFromRoutes( $application->router()->getRouteMatch(), './kettle' diff --git a/composer.json b/composer.json index 58847f0..510020e 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ ], "require": { "php": ">=8.1.0", - "popphp/popphp": "^4.0.0" + "popphp/popphp": "^4.1.0" }, "require-dev": { "phpunit/phpunit": "^10.0.0" diff --git a/src/Color.php b/src/Color.php index faf126f..3df2570 100644 --- a/src/Color.php +++ b/src/Color.php @@ -21,7 +21,7 @@ * @author Nick Sagona, III * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) * @license http://www.popphp.org/license New BSD License - * @version 4.0.0 + * @version 4.1.0 */ class Color { diff --git a/src/Command.php b/src/Command.php index f96f8a0..78e62dc 100644 --- a/src/Command.php +++ b/src/Command.php @@ -21,7 +21,7 @@ * @author Nick Sagona, III * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) * @license http://www.popphp.org/license New BSD License - * @version 4.0.0 + * @version 4.1.0 */ class Command { @@ -158,4 +158,4 @@ public function __toString(): string return $this->name . ((null !== $this->params) ? ' ' . $this->params : null); } -} \ No newline at end of file +} diff --git a/src/Console.php b/src/Console.php index d5b01e4..c58d7ff 100644 --- a/src/Console.php +++ b/src/Console.php @@ -24,7 +24,7 @@ * @author Nick Sagona, III * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) * @license http://www.popphp.org/license New BSD License - * @version 4.0.0 + * @version 4.1.0 */ class Console { @@ -104,7 +104,7 @@ class Console /** * Instantiate a new console object * - * @param int $wrap + * @param ?int $wrap * @param int|string $margin */ public function __construct(?int $wrap = 80, int|string $margin = 4) @@ -204,11 +204,15 @@ public function setHeight(int $height): Console * Set the console header * * @param string $header + * @param bool $newline * @return Console */ - public function setHeader(string $header): Console + public function setHeader(string $header, bool $newline = true): Console { $this->header = $header; + if ($newline) { + $this->header .= PHP_EOL; + } return $this; } @@ -216,11 +220,15 @@ public function setHeader(string $header): Console * Set the console footer * * @param string $footer + * @param bool $newline * @return Console */ - public function setFooter(string $footer): Console + public function setFooter(string $footer, bool $newline = true): Console { $this->footer = $footer; + if ($newline) { + $this->footer = PHP_EOL . $this->footer; + } return $this; } @@ -314,6 +322,26 @@ public function getHeight(): int return $this->height; } + /** + * Check is console terminal supports color + * + * @return bool + */ + public function isColor(): bool + { + return (isset($_SERVER['TERM']) && (stripos($_SERVER['TERM'], 'color') !== false)); + } + + /** + * Check is console terminal is in a Windows environment + * + * @return bool + */ + public function isWindows(): bool + { + return (stripos(PHP_OS, 'win') === false); + } + /** * Has wrap * @@ -679,7 +707,7 @@ public function headerRight(string $string, string $char = '-', int|string|null } /** - * Print an alert box out to the console + * Print a colored alert box out to the console * * @param string $message * @param int $fg @@ -740,6 +768,73 @@ public function alert( return $this; } + /** + * Print a colorless alert outline box out to the console + * + * @param string $message + * @param string $h + * @param ?string $v + * @param int|string|null $size + * @param string $align + * @param int $innerPad + * @param bool $newline + * @return Console + */ + public function alertBox( + string $message, string $h = '-', ?string $v = '|', int|string|null $size = null, + string $align = 'center', int $innerPad = 4, bool $newline = true + ): Console + { + if ($size === null) { + if (!empty($this->wrap) && (strlen($message) > $this->wrap)) { + $size = $this->wrap; + } else if (!empty($this->width) && (strlen($message) > $this->width)) { + $size = $this->width - ($this->margin * 2); + } else { + $size = strlen($message) + ($innerPad * 2); + } + } else if ($size == 'auto') { + if (!empty($this->wrap)) { + $size = $this->wrap; + } else if (!empty($this->width)) { + $size = $this->width - ($this->margin * 2); + } + } + + $innerSize = $size - ($innerPad * 2); + $messageLines = []; + $lines = (strlen($message) > $innerSize) ? + explode(PHP_EOL, wordwrap($message, $innerSize, PHP_EOL)) : [$message]; + + foreach ($lines as $line) { + $pad = $this->calculatePad($line, $size, $align); + if ($align == 'center') { + $messageLines[] = str_repeat(' ', $pad) . $line . str_repeat(' ', ($size - strlen($line) - $pad)); + } else if ($align == 'left') { + $messageLines[] = str_repeat(' ', $innerPad) . $line . str_repeat(' ', ($size - strlen($line) - $pad - $innerPad)); + } else if ($align == 'right') { + $messageLines[] = str_repeat(' ', ($size - strlen($line) - $innerPad)) . $line . str_repeat(' ', $innerPad); + } + } + + echo PHP_EOL; + echo $this->getIndent() . str_repeat($h, $size) . PHP_EOL; + echo $this->getIndent() . $v . str_repeat(' ', $size - 2) . $v . PHP_EOL; + foreach ($messageLines as $messageLine) { + if (!empty($v) && str_starts_with($messageLine, ' ') && str_ends_with($messageLine, ' ')) { + $messageLine = $v . substr($messageLine, 1, -1) . $v; + } + echo $this->getIndent() . $messageLine . PHP_EOL; + } + echo $this->getIndent() . $v . str_repeat(' ', $size - 2) . $v . PHP_EOL; + echo $this->getIndent() . str_repeat($h, $size) . PHP_EOL; + if ($newline) { + echo PHP_EOL; + } + + return $this; + } + /** * Print a "danger" alert box out to the console * diff --git a/src/Exception.php b/src/Exception.php index 4b534ad..6619a85 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -21,6 +21,6 @@ * @author Nick Sagona, III * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) * @license http://www.popphp.org/license New BSD License - * @version 4.0.0 + * @version 4.1.0 */ class Exception extends \Exception {} diff --git a/tests/ConsoleTest.php b/tests/ConsoleTest.php index 7c58039..678e6b7 100644 --- a/tests/ConsoleTest.php +++ b/tests/ConsoleTest.php @@ -27,6 +27,8 @@ public function testConstructor() $this->assertTrue($console->hasWrap()); $this->assertGreaterThan(0, $console->getWidth()); $this->assertGreaterThan(0, $console->getHeight()); + $this->assertIsBool($console->isColor()); + $this->assertIsBool($console->isWindows()); } public function testSetAndGetHeader() @@ -36,7 +38,7 @@ public function testSetAndGetHeader() $console->setWidth(160)->setHeight(50); } $console->setHeader('header'); - $this->assertEquals('header', $console->getHeader()); + $this->assertEquals('header' . PHP_EOL, $console->getHeader()); } public function testSetAndGetFooter() @@ -46,7 +48,7 @@ public function testSetAndGetFooter() $console->setWidth(160)->setHeight(50); } $console->setFooter('footer'); - $this->assertEquals('footer', $console->getFooter()); + $this->assertEquals(PHP_EOL . 'footer', $console->getFooter()); } public function testSetAndGetHeaderSent() @@ -592,6 +594,96 @@ public function testAlertLight() $this->assertTrue(str_contains($result, "\x1b[1;30m\x1b[47m Hello World \x1b[0m")); } + public function testAlertBox1() + { + $console = new Console(20); + if (!$console->hasWidth()) { + $console->setWidth(160)->setHeight(50); + } + + ob_start(); + $console->alertBox('Hello World'); + $result = ob_get_clean(); + + $this->assertTrue(str_contains($result, " -------------------")); + $this->assertTrue(str_contains($result, " | Hello World |")); + } + + public function testAlertBox2() + { + $console = new Console(20); + if (!$console->hasWidth()) { + $console->setWidth(160)->setHeight(50); + } + + ob_start(); + $console->alertBox('Hello World. This is a longer alert. This is a longer alert.'); + $result = ob_get_clean(); + + $this->assertTrue(str_contains($result, " -------------------")); + $this->assertTrue(str_contains($result, " | Hello World")); + } + + public function testAlertBox3() + { + $console = new Console(null); + if (!$console->hasWidth()) { + $console->setWidth(160)->setHeight(50); + } + + ob_start(); + $console->alertBox('Hello World. This is a longer alert. This is a longer alert. This is a longer alert. This is a longer alert. This is a longer alert. This is a longer alert. This is a longer alert. This is a longer alert. This is a longer alert. This is a longer alert.'); + $result = ob_get_clean(); + + $this->assertTrue(str_contains($result, " -------------------")); + $this->assertTrue(str_contains($result, "Hello World")); + } + + public function testAlertBox4() + { + $console = new Console(80); + if (!$console->hasWidth()) { + $console->setWidth(160)->setHeight(50); + } + + ob_start(); + $console->alertBox('Hello World. This is a longer alert.', '-', '|', 'auto', 'center'); + $result = ob_get_clean(); + + $this->assertTrue(str_contains($result, "-------------------")); + $this->assertTrue(str_contains($result, "Hello World")); + } + + public function testAlertBox5() + { + $console = new Console(null); + if (!$console->hasWidth()) { + $console->setWidth(160)->setHeight(50); + } + + ob_start(); + $console->alertBox('Hello World. This is a longer alert.', '-', '|', 'auto', 'right'); + $result = ob_get_clean(); + + $this->assertTrue(str_contains($result, "-------------------")); + $this->assertTrue(str_contains($result, "Hello World")); + } + + public function testAlertBox6() + { + $console = new Console(null); + if (!$console->hasWidth()) { + $console->setWidth(160)->setHeight(50); + } + + ob_start(); + $console->alertBox('Hello World. This is a longer alert.', '-', '|', 'auto', 'left'); + $result = ob_get_clean(); + + $this->assertTrue(str_contains($result, "-------------------")); + $this->assertTrue(str_contains($result, "Hello World")); + } + public function testAppend() { $console = new Console(); diff --git a/tests/tmp/alerts.png b/tests/tmp/alerts.png new file mode 100644 index 0000000000000000000000000000000000000000..07d1949ceb0510ade45cf09d698c0c6fa42fbdb8 GIT binary patch literal 19050 zcmeIaWmH>TyEclnXn__f#c8qP6n7{sZY}QaPH+eWinTbwiiJ|Fc%fL(0>$0kiW3Mf z!E(}P?|pupvG@Mo{eJIvo-xii`IC`kX3aU*n(Mx<`?{~WKD|?uC%~h`LqkI&P*ixU zfrf?-MMJ|J!NEd(qyN2j3HA2CLq<^x2lWEtSbao&rtp;2^VD>;@$@kVSfkmxxH?;N zdRPLitzA6qTs;pl+aytqIR7+y1F$ytw0Cu(*RprEM$_`Lrhmmt|JK@)o|lK0kDiBL z^c9b&fH3_#Rr)uYS{`mHMrdgCXo_!NYx!pFFZyK}Tbo}YN1&I(+0Y*&R;Kh+$_jd& z#Bb5x>pc0cS5UKR(B?AbvT^}C(O#IpE;TMwb}zkfWS=&0mUY8^EqfvHIF&IhENp*Z zpU+2ZXl! zz>n#&V%WV=-#_%%f3Sytuo-)O8+sdfHIi-VYFG$q#SI_0t;OT8F1o9AuzpwX z>SzH6-=;%%Sq88^`j)o%?1NF??&_;;ybf*`=EYm7FeU;^qOH}I6~;vX-x|y{a$opD zmok_}%uJp;xwF+(S6AP6MhK_=7JO7Sk#ncjsnJ0Mdfk9%!|74|xjh+}?i%!T?To~H zm{Cdw7Hqb0Htuy(q`=i0wgE=Uo+wza)R&f`6^!p7%L_*WEAYaINm=L{Z>(F#fAIju zLrV0pF!HA)Am3#0!d@uub{Xg^VY8^ee5oNKyvf^rh17}2B>#oKPkqEnx zWZjE}4UB{{6u!0>TLh8x&9oA-V|CY;uGE0>cl9=u-cfHiEXj8=Z9h(>#y_=@4lqB< zgS&Ar-dls?13{iEwGNWh%Qx6zKjI>8AB#ghfJHF{xHcQ4xsS4tuR0=!sRKJX)95E2 zvSg_XZ|PmFB40pjeK5M6gJ(b=j^tUSJmT_sFo$oAJNj6D(jlMsz3CKyv;REmMa@vl zY`hc)gjo2Fh-Az$67p{ZKY50`PUX}7Fs$DAcF{w4DEsH?6-HV*2Hib&2YZuzypZS5 z#O(Fi5dLFvI;`ePt4mpx}@gKAx76A?7kc%Q=iZw-3oBKE!E9mDC`CBj!1FXU!ZTH z6>szSKrTa}V#D(Ym-W$^_^FM-PH>0Ic5htJLr-h^N9|=ExFG-Go%JPc99#}zfM>W6 zL|d6NwV67Do?TJ|)3bhE&}-Dh{3T#yeX4_?dtL{ZO-TT>qwXSf@V?ixH6($~w% zi++96qa)3(uzXFZcPFDK-mMSlwPQ59eDX5HVXpb_GG$ZTu&2G7c@`>g5>EPUDh zm)RGhaWr|3@VIQZP9McOQ|X2yt7lkP*!0=K?v5F*!L1=u)F+Aa;IuV96pahsrP!f~ zHf-rBw{JyHUBXU5gLZ`p<)EwLGPV}%{QRw?c07mR19LZL92FIHyh?l0>OaRf5^jFA z!^9{A4@z6dx!rRShN*nM@$a}ak-9$GI!tIj=6hG{-gGdNbUPptcA4{l92n!)?(*gl z7YlT+@z@x9e+8H63sZ<#5H7>dytc5|wD$c6*Zy*sx`*zLfyUFu%7CGVVJi?!^1v)N zbF$v6F@0=Oh9Vs8)eNE}alcn_Nm~cafMe< zpE-ZPo|X(?y8N(^vbI3SI0q0}^BE5BEQ*D&H6!J~+(`B^YKj$p?`#{6vv%%oIIv7y z?xZv}Y$YANrKuI`{kFSfw(CyhNNc>+-2!e}GX~+ohkp9AgW%fu#IjRCwf|{{=vBZUp7Mx=JCblYXva>lfMOU{>y>=Q#kt9 z(NZ!$W?z(x_`L9DSBx^cS=JZ) zFGqP{(WJLO0hCv|U63Mh>&(FOsQBDrngdbU$W`L*P01pp+`UGxXtrWdHfaiPGrkxU zJF2d9+04fm0-OX~`;{5o+=GEKKDH5x*%TQ$+v_If3sVaxmCld({tWsRJ-Z%qKxmjS(m`KFJDj|bSaaTj^ zaR&9J^}ry{GN%jZh&7)ycr>{Z*>L07I{p@gu;^?#kq>&Uh%Z!BZ7>*Ex_&tVqNWQ2 zu&+8m1RewZY?z82gPoMnomgihD5F?N+f@jQE8WHbl5g;!wDi#?FNbjBPwTXLx3v$0 z9v0`o6U11!MtJc^1e2{a>>ZXQpEgv|54|zO6%-^adq*(dGPViZ2PAl$o&GsGbY^ zZW-2g*YsLPmGx-sauXtHtZzrX(lEsmfmjrZn5BhC=-3Ne8-jBk#krNfo#cV$+f7%% zU<^fKy*4(c=2apkZd!(-Q1x#orgV$HKnKT16%W;)JzG0Pigs-Auk8qVo~r9^>{jR? zj<@Sh6EjP0R~I^)&+}8=%k>-*;m@`vkS6)qpWXejbAnt$(nDn>Z_*@y+kL#qi>c>a zw@pt8%a32Y&J}r{6l0PEYXW^fl+vtnUjH8p_TLa?Z;iB^W0Pdia8z(BaBsqW4ij{;9xkgDMlLd-ybivWCDpnXK05zd%4nzbY?@}Pff;76FWRgti~SgNGT)^}ONoaQQHx!>H13xg z`Yj;Ee|SEFm&XP5u;+_=o<1fzK&+?yRCu^|$5emcn(PqFm}I`SVN2_pla-76GCVo^ zPC7PGIfEpFM1jrP#W;05P>yQ5a%a0SKuHA?TsXE3lW-}=!qON=Q{;N1s~mXAITf9# z>6u}`$Itw(U+@S%qP9ijVRB{by|kEE7|>=MU>jkCvB_^eL1=cpvo0p2OMwn%?99I~ zutiVbdgM&8W2n6m*UDj=HP-<(?gYZFUl5Ve+YgU|Cqat_+Y9p<&y%DJjI7rQ&E}V5 zpQ|#q$$Z(_AQuE!@V#R_rOSb>_pp~f(l%?T>@UHN7H{^5r&Hinfm5~^gI330_#EIQ zW0i7%YC6om1vaHxQi>F#X(>04`HabT)*GOfkjK>Ec<@NjA9p&W!dF8 zy03q9x=J491hPsXx*J<{QD@G&0~H?oj9hzY;wq;Kjt8CMw7{?sq3rVYBP#Z51E%)2 ziWS`ljrh&T{wJw{(B8(JbGO|QZ>n5qNPTM{uzs6`lR}j0i|B(T7Qw3gCxA#H_pO@O zt*z+Q&J>~~3VY%%LzM$%Hp@2|=J!X9c#fdPB;|@oCyXg_JpyHJW?ewk_~MMG`}s=E zsaTPqn}V$jS4f0Rgg*WWwSh0(A@638P|{D9MBY1oIZI=w*L{CXB%`O+xZ^`~(-nx$ zVe^BmNFyFDK{$te)?$Y#5I}tR?9h~G&MV_ppWD2>rH|wmrLd!Y>Z5mdBcsAk)~7{R z7(2&(RDMSvI&DpgP~ho*dH!Z5nL0Yo_4kH;fJB*=&|j$j13%DlxcrifNs$}_G^j%G~XMImKKAr6a#T{cnl`+SKeQU~7e~@H$E=??d*4fLX?IP|olJPKJ zfT-`8isibG&AUg|^IX8sihP&}@0tUjQ>6*WTr%63G=2h>Z?jZu^A3@L(Q-dC+^HV+ z)Vk>E;&zFVS0CTY@kUCtKd;;iWahCKTd07HNLRhRk&PdfkeKw@LFLS~iHWt&q@}F- zH-(tNi4)2?fL?za(<)jSNGmMPqNmWbO|96gsgZ7_LdohPMvq=Gn31VZ?PtS{Jqp&3 zGO|2_l>p|R=NB-_B(yNxPg$fSs9Q`+#`=!bAD;Q^XjA*~vtJondwQbl;TwK8tt@SQ%{k13d76o+)YJ+$eUPu%72gfxMH- z;3a%elhX#*p5Fc&Sj>uH!lWg8(bpsk5E3G0GuG6YuEfN&p#R)AL*q}x$6?aa88%<% z7A-E6W@gF_Iq7!sc5x{H!10)A4AK+a2L`sQj;963jSpD2Su4_&j*E5JD)nWzYv%D2 zHoV1*MJD?$sEXdX%(dt9$;s&g6qUeKD-GrWLSl#MZ_o9YRB$$_wM(qJNg76fu8xp@ zKlo(q%0R)k08}0e=~_{s0m@+Btw_b)ubcs^vLRxqVX(&bRNCw*Vp>heVY!<8oKEq> zf**cePbgnEteUj=7ER+*n;0SOa_?UVX$>{1%FdQbS4Ixo_`Bw)40IXVeRE$_k+k-l z!rIj|e&k~Je5f!ctHyrc(^{zzh1jY}HO)YpgNc4C@S~{^ar=p%R~4rFS#J;LsT06b zK>`zM)Qoc?u|qQ|MLP~^*f@V6k>0)xOc5YYxXEF^neKD+9U7WzPOq3-nVH?TM;U10 zdq$}{JEp=KZj@bVTR zcI>gP0xwm0{!ku`-198cQ?d`Q%o*iz&_tq2n^@t9)2YE7uRMQw7b~a8bY5txE$v86 zviv$FKuez3uHrZq3H0)8*Ks!YeG{>Nt6JlfJ^ZsVbqv%19mAOpT+Cq1FEC!$>D_`g zI2OmnWPMGF>3VJzvD@tVFndX$85&4AF&fxq-6sIQs-nLds9ZiAF zD${{T6-*aBTPGMH&&E$z7n{1}a!u!$H~VwqN!ew?h~&pHq#qoJnSMLlFonhTMtWhH z;NQ`hapKypAJaahHPe=-p*DnH6^Fr3iE5q|?q|bQoZw8`@-f-P#n$TOo0prcF807_ z(>veX175y>o^I#D!)31ig0PyRXb83;jx=v=yo@eULfo5mXm-hH~mtGphXkPawjWv}@z0RZM<0QN2~-*CH&6Trpr~3*+q8!lMTkFYK`i97VFN= zS~`Pk#bh23iWgD}J>%}nketedK8%8}{e6=RK!VC{!Ki3BfNoTeqUB!uuVh-ZeggU} zlaol`eUK*?j1HfX(XuG205ddEGYo?u8e%55&3H|_XJh9XbU1l&!G*zMZR+Vj0Xz#x#9YLA&NKI4E{SOq|m@d)_*?WFuj%9FbN z2G%MjVm8^r{V?-v3HjdC404XumnG)g?`IgJ6b8RmZ9Hznu9^#qGs$_iDi--(tvbT2_5f`#B3{9f*E5Q*N9*N@}Fn^mZT3Q51WJN!Wb1v+Ee~%LHM7; z$p1|h`oHWH`e}BzNfHEmyRQQ#g>tuU*EGUM{+7uBZf?->@0g75w1bZG#EK>F6l8+N zZhX7+qjS!8E2j@Es?<+8D=mg%XR54k1T)kTIP!GU{JA}BEq_u7xmkoI{CebYoLn?L zuy5tNn-1Z(@c@{q+hw5Pmm3*1=)T3g1E+dLI?zK*Tcd;_RBIw|v7$ob+T{ix-spp$ z&am8-zDC2d{dSA#`MxX?sIzi;ph$T%HC6dh;MSM17<9DKa5dAilHd2 zY%n+N^rYT|(40y==+d%mR&S~3U@r&=#XWb~voXM2R*#!p&2Od;wCoCbte^0yHHV2rdTS!j zN4S_GAPtbI%a!)Ha(>^T4PiBXw}td-wk9;RyM;@oO}G*m8kf{r@nHu>{!HEkE7 zJwT|ktF2pn*q(fbFr}>qK848My^UDcvEX&6nL#Q{m+14=v7lb|>$ zYstR295c@ad;Q4>S8=hfkC=iJe$yh6R?uejVF_AbR$ z6$GkQBnrIN{tzSCjDe(L$@YsPIl0gj=NFlC1MW#W2DUVc@-uqV%tJKWPMDiRr4%Y5 z`)h-u_iiV159S-~J~w!@k2g?=&;Y^SHbzH|tukoRUqlzx1<=mMnotY;@)qG>f7bZk zn1TEy5fL#O?-HqjPs;=ge~l*?eVj)D`s_=0FB1YopZ-tR=balwO`q)zD=gQOO6`>B z?d=%F(EsF9O76Z+i#jLkq9d#=zA;%Zk`kPqXahY)s5?FkNd9VulZ)ck!(3!>p6_pZ z;}Q&&=M||;WXC+hvQrP1hJwUcrN@<8?Wte&)+Z=`Y4#FXke^QnUg@j2w#J^vORAV+ zCb%-E{Trgr=+UFgc(HW8L7#t=O(-4|EiU;&ZKsu6q@=SJ-t|B+y$ZUV|mNp|b`w0T^vjZycfa=8J?K_Dc+PWL# zI6u*C&aZ370j!73pKe;cNzfD*O7#F;)}Gc5ixr!sgogesEp7f`x$&S|S%Zc1isc>p|?D4P1awBG-Z!v0tGd+Wc1^un^;O7YwD>G{ZX#P^`%Nz2hn4(?7r==; zIxxevHCm{3jYlnKdGH$Mx|TFx0Sx!#ac4pi0GocGyz>yfpLsP6yxB zG0VudJP0U^-+fF)!)EP|6}3?=Z#P*;Loh>a;*9w}F%$`E^iJQ6=DT|^p|HQ-lIS$GE$A88#spcRdo&Df zlYEBC>RmShc+v(0BYs>2XxdSm*YQnlTkL%qvcNjM){aXz>s1noE!j)0s51SQ zPmZ7qVIik)M~eb;t~wPl5caadfP1Yk85sG=5R<+sL6`CQKB%%SVEhADT$aw4CK2T~ z7%3X*L-FOSoyaOk?7DDoepT$Li3e#K%|c&|Q8=feGCu-UPWvk}S1|-oBhMmsRfeU% z>9DfI4nm}&>xXwOy4zNUi|9CG*@bKSc(!Xut|FsZCaPeMOj~~@%8#sr=$3~DE0_D) z%FEAR=$hpx69+p8B-m#T-vs#(dIUgzYa%6t{Tq3;C|JYYV>7c~Mb|?-aFznHLY(Xd zCse~B$IUMlNo-rz81Jt>%MsIuiSyZP9&Z#9GXCKJ`JbZiXP`zkJ)KP_iKK-msWU2; z0FBM6oRv;_P-7@Wh0nI--s=7~%w|~9kr|1Y&G;j?iRc(&Lm0m>Y%leBAg{FyL^t zbvtRCd;e=BQL>^LN^&0C1kJE88oF4{^U!Bjqp}4R|cE5 z5|mKQAi+8iFIU4nzcFn+g4dCDr9ID3Ub)TN@~~lhL9i+54e^)8B)aA1&}=dV@3>J@ zBvWih0HJ%uCIp&YSZZ7#{9 z<6xFdX$AS&$(up3K#*(Vi_rxB(XlLP(^#@+#XO|JPAn{(y;Y|nDT|)HYgry|$qg~y zmGDwCo1qkO!`?*3$K$*b>-rc<+%Fi4c9J9dNiq5J18)~>UDv5HO?HMP3V6NQxckIc z$O^U>xOjNpeI=$(v$t{xz!1Yo7#-98@&qRrh1v6OlH!sn9*qh*^0Ek~HQa@;AP_$} zxJ2xE@uTxbh{CVq4Pij+NcJ!^#kx=S&ba%YxJ8)pm#3ea1f|Ij1#X3?+~t|@(1D*k%p+SkY`I| z*m6b-JH7x^SAf2na{za*HHjOWqI7h+VA%nwz3V+h^+gcmQ>96mI3K)syuI6~>{ z-$ryZv%h-m>9nY;ZYhVE!1#Yl@Q<7}N~Iv}9m<8H61H4)z?H=%NEzT1wAkB|b`X?i znSxEXtcI+bu*(sRR0sMJqS=oa-0vUKuC+VFE}Xk=uuEn4y(d?-*!t9ST*(3#+*cr4 zxo*F586jd|@@G$Q{s8F_JT;?Wg9&*o3`1`(7+(^^%W+NmEoPXemBX&)5BLzJ69(yl zC&$2pd}#qjtGk^t%#UqxQSZ!_4la+eP5r_gY2ntL(kV(fT1rfUF0<{|78fF=u@LOk zQ=u8xt>b&PsmISKs5cT>uqbF}yjdMxp=nar*K96Ed~aP@3p6;Yug3&#JJaA>RIqM@ zP_;~Y_8WAW^rqDKC4}C=dTL~(B5us1R93ZL4FV(egDopC2GU zI%SK4cZQD5v053@e`)a~yU`U_Y5s>A;4|W*qp8Dhf0v~S#Zx-&C2%OABMybE0($If!h?#JawhzX?%Dd!Z%-Q#6 zfN6fwH|0a9fq6T2h@h?gv=MC9Q~+pg&wRJmO{ zf}-t?dHF1Zi>=;tps4NkiZ7&ZXj34^pQcCP(qm@jPRd2^JuJ+zhWm8L3$TM5$;+Ud zD`mLS3BHK7qP7Xep?ny-&cK^LvlD8})QQx{EP;wWod2Jc-iFFJm~ z9iOSa#nl2k%@?);w!GI19tDC=q3?2ymidCg-rGuQiT$IkS8s=X4=kPzIv2f)8U5kS zNp!!e1NZ%wBbu|ipc)4!QQO$urs=vBapq5soUOBXcxe>CLL{kAJp(D(#6wpo4G81N z&$mo0P3{qL8J7&T92Sp4$PZa{4-y&}QuQ*u<0PE8S>*)6qKU@%ATB6`Y!HI<6|)R| z$;(w)>3&Xm`zIuzWC3DPwEzO$)&sYB-p6cYCFxI@X`mll$S4+ixB#FfVUnH8X0!Zu*@ZF7LH+!rlsF=KsIMbWs`W*=0-LNkC^ zYdfHhr6yDA;;C@wi{#i!sT7My-I&X>&djjS{Wa=~kycZEb&v*V|1`vi_FR5uu|wFK zURC^$C_UZ}mI6sBU-(|_UT&u%vW_D{&(sj8W(AyK`qgVeZdc}kK%&C{?e+nL+O@{Q`d?C&5Uw8Bc zJnL0k+Yrhb{t7MM_(4Hwu<*Isjf+q8q5<|IhG*39Au%QSptYK9oKtQ#1%9LVO(`9B zv+?oRR2dC=^@Kj)_=zWZHUF?Mm~SLoQ81)`k40}n_eNX>$vazoog*&`|kwybY|Z%6xfmNxB5-qkkWkg?kL9{rnAP?`54x%zH<<`{Xu#td(D zB@w&n&wu_Wa{Ta?5R;>XoRYlpS?m>yL{_RLk2QPpvjvDuTG`Z9k2!s@imDEnhxHuNG@Vv8hLc^M6XPUfNAz&9Py3;m zBUMxxV#M`y2&vaiqE4WpMDmT4V!kRVmZ;L=VCxyOx`Ht_W(n z27i&1mRYW+H~cHzox+Ez1KD^r|E6szaV@E8+N7PB&ub@G!5^H8@de7NrufbuP}_k)c|UBL)}ZKo>=0 zix$ggm!=!ax`7{rXj99Pqc=P&j6x|4amKxiyEOc&KXVn=H8vrVUCNs+iV1163 zD^uJ|N2ntDu>q`^U+qcfB}i_11c;OLBo62KRp&+Iu@K+Zb53;lMbvzKE8Txlr1@gM zfI~Dtvs;OdN@saURqFPYPZ`zM3oqR-8eAm7dk}VLf`%$)81ZJ2E}%9xvu0%5x>B$I zHv^$1e6Y{6h99hqG4-2%f$p9w=dPdAiK^!}kG*KvL=cQ13-lY<=)tf*yQrQ=m+gi2 z3J~=!LQxr%BNZ@#i!yj?v1#!-tBBodnFt|y3W3MS^}R@}?k8CWo1c&SFOS+*d`Zij zT;}bvel4z#taIYWTkUO2cGZ?f3mlf~v7zS`u_whPHQYK^T1*ccA}(E>g04NStN2^p zfBaBeuQv%0J?MioFl}TQ?BXyyirJP`6cyQ2J>2dmnODRK8|_biv!Gyd#Zo{Xn>f+V zvb&K20+LlOuCE!xvhxvoT+p)zjbLKOAOM+eAoA5UpQbRv2l=s{EBjaRHs_}N07$Tc zOj$K@sw#P<)#`b9zEj(_gCufDY)(gSu;4HV+jcOk6Iv-1x_~_PuiN*#IG({Hjg(h- z^ChE@pIbaUjixmiSn#U@RzVeO^zuDNL<^PBQa8;?V@9UVrN$#agC>><-tQmgNgk!A zSFSFu&nwEL*Gk=SAaC!cv}_?gbhmR|gO}N|*f=0EFO+}2U=0PvaN7~Vy7A@(k?Q^1 zb-l$@>6gUJ+?!r)KS+b~C71|s=jJ>PtN{J3{B2{a>gpr2VA10eh8V3cZu=%N zR9Lg$f4_pv^6Rt~O~-iD;c|Lx^GTqWutO2H?7O88zz1R!l-rQ}PgWbAkN4K6xdT7D zR(#{RCi~E~Ix?ItdSN7EX}GZC(+u|B%Dw_@p?rID;s}n(OJx__@ad<#xMANzoh6z+ z5IEffY-AN5TGxv%+-LP$2tjfM zVw@@T>qyhGN*#rD%MH?WX?wem8o@@5agI{QxIy37!iR|p_Z#aOlKfiPl}74~&Ax$x z9fFO<{hrLLB5o&s|OkLzMOvF!(C3cP@wtQ(&)Z&%L z{H1RvVd&X~4h|#f7R^v1jUOOfaWGrRxxVfCxMLxTqC7onJ8jWO#B$$Ylhdn%(~+%9DYpw3^{3FwQc!mq zeD3aPG%>|mIRM{E z6T-UrGKKH+#fi_!Oi`@K>EIuu{$mggSuR^<5aOZaak{Jf1g7KcxM@BmMv8^h!gRUS zsX|cv!XoCK4=9P-)Q1k{ADy48SHgHIXViDJjmlEHzlcnw5g2xU&%=|?oB78T*kiA81 zcgnPLqsp%yvUNV%UmTp5KU^OhdUbEDPU8gLlF5a$jB__%37jt%Fzzy~V$a-v|GRYY zeCRuzn;UvY#SEOk#o_+H-r4q_sR#eK==X5Y(SLv?`DIF-#4bMabnZ1mRkjMYfH+R%ok<_61J zUs{pA}{{%0v16Ub?O&2GCibN0>yw zKz8E|U01KQN)+L<1Bgi}QJ}D%981=AZwy5#3PC^u0yHb$fS{+Uy;i}D>XN3& z9e5=|>5oIjGuWXwJ_u{bidS6BI(@7+F|TT^ONwjP47GnJv84L0o$BhMw*KfR$pVE^ zM!KlalOj_fE;;Rls&_{>XVoqQhJeSz&F1E5Vm9W}TaFlq1D6oZ7HWL8l}*Hyl#~jq zNY}&R^}LEh@wm3O)6Bs{6}8TS#d_)&PAO5ol64mU_4B zRdr1nyng%o;As=B_UkcvdEvLOo;@bcM}P4c-1iC>x>XN)Y!qXhHAaE6(h7-}zo$o7 zwVn=yZloN#2Jt1X*IXM<`fm8Dlx7<_!5UM#`VBKG3BnDvYw}dq6k0OA%Ed{k9;jt} zTL|)RobRa;@w`mixqHYBo0{rkb9}VZus)$p7khUaV#NUX{!S|(O{!cUq*r-j)92fA z(m2JoRHMZ0JXazibSI;mO&!G`T;(u6-1Woj(j&a&{^V1xthWpMhy#t=h4t1C}&HIk~ zk^#aUMKv8&L_0CzShCUI`OzoK{w_V!B#Yw&e1<#(n&Xn=T3e z!C<}NOj79tch1+U7Cy2g{%?(CDgqRNWC0%vcH)T6<|DNMs!{$BG2?o*eOB8ZLw~br zq@Cl05J=LE@v#VXK#Y9rWWondWI*QS%u7J8NOfU39c-OVmp3R~0^#rH(#qIZe%5Pg zB#`E$tC%z?*&c5@MdmVEbYtANn_ER5_iiR3QRWT5yAxfDQ2jgA1-zOnK#izPkB+W7 z{g+_I9{b5c0z>Vb-jct;f;>%;sP1v}^aGsa8rcs&k zdqp+6SjrjsbXGZ+Ca3V<%}n&^s8jr!;yP{jawiJigvJ-I-8kdC8 zAD_P6oS(m*$fVR-;b6^8^dKvad%367os{P-f*op+oSMYcmj99Jz4osZiLWyz(uhM z_|yB&L}(aRq?dk4+x+WTqZ?OG32|LQi>{xt;yRF#Yk zzez((CbBNF={?W_} z(5|nQredd@)N4}jGW4N4DWO{tCl^0vB*1xA?3bC^T&+{!rCPF3Yrx$C)gG#NuXNZ> zN!pV_!z^LE+pX<)yj_ArFv)s{u_x*D5#V@V=zL?8{;p95@~e_+;g~Y~`a;vEM`5VT z;3VyQeqvRrVj!HwPII=eU6gs9)cCg2QH$R-ufs5P=d#TJ(2IR1>NStq=zf)(rgt?7 z+CeMW1bxdeR`x=k6@VE>-1GQPbE79$KDl$jw2a05YU>8MC*juWuxaZ^4yyg)WAgYI zm<4%KMs5sgF?3kFTk*}*3X6zDgo7TOxTR`ATggD-IYat`N~+VDf^`LBVTS@*Bcqb> z%bdxWX3Tg$IX^t~`N!UX?+JP9Bh6|)D?y3fgs=)iiXf5hUMdCM8+hO(>*%OMy?L_x zJX40G{kKMI#O5>k`?HI^rOG@E7jX|0FWP_>c!s12!*!jm+)s8_dSzpjBJF6SHuKA* z^@^}_0+2Bl#=jB3sVgaJ?A8B$eIqeZN+KV#{`0DrrG!aR$?LSGucj&pTLO3%Ynwb+ z=-b)4=(p{kxs62zWYqsPkQbvh8nGeuzF`r2~y_Io|&!$HEY4Da1&5ZK} zHrn^6iI`}5(Y@$sWwIM+XwxqMXlRa4M9_k3KmJRbsW|ZL&>o1Pqe=6APx@E&c*!BC zkA{X@`scX-|1ZwvUv_5vlKf%a@0Fr-X+2hRTxt|!V}}6gmT@9mpDp2T9=PDi?5v4% zB~Ff)TIOpZ^&ey>CTGn5`ZW&PIl>gBNPv|@_O5TmB=9=t%R6PIrFrA|8o&Arz8-3i z!B;(tYg@#OP}JpJefhhCh?^vdyjQOX(Di|&f`5s9vYdai|DkK8_1mW{Pez}$B|m(*6KCY*mcpvBcgnhO{Olkw2iP1hg(pN3JJ`}0-9(bJNZ5c z1dD%$xRr!@Sx51+SZ?NdG)way-8>zYlZ82GZC{fffdW~!Pj3n`D5oCNuYN~`ISo1c zz>%qQA=3ifqH|3#*OaVa{o7Yl?KR8G@?0&dnN+r!#Qt>@AX0p!E+eHDSM}INYSB!K=c1Na zWYpIsuZvipJ*lnD688>O|3FvXI%&DwwfbGCEmz9@^+!1HmOc5xuD+*QQL#_v%N!EISi|PaSAJ-TN?SXkj8`U|irZ!;&-7JAnY+?Tv}? zyWMfbW?z2*-jf#dmi6z{>(6L!b~E`)xOO62Dy?qng_mg?H&Uagf;KA!;M@3y)5dvZ z>{R?agXzaBvplXdz> zrHiTBYlYDM3oWHUk7Vb3zrPC6=16*O!IrkMZE-Onh7E^io>5J=LloXh=HEvD1w=XaCZ$W3jqQ5R z@VG)rp!PmLgH2YY4sbtAQfZm(!LG9#iXq=QLRF*|3A9c3&huKPKzBx%$pVYczuwf; zFBT224{NLtt&H`5EG;2=>S%oU!^?%c?c!jmXv?aMs^@qs@C8Ae8`%H&H?v(H>h_i# zTBtp`xAnTu!an<*6e_TtqJ6y|O(}b)FWQ<}bG+%JJ-N3Rzm@%v4QFH&F=JYM?jiI| zvNUr(GRBgFFR!*2Agv^T2jxeV)yT zVF@SM`-e?gWpT6+nc%vR-gcP^hiUO|tZ6PY1VToIpr-Afy=MR|)AMji{u}@p^3-@iQ*E3a6RpNi{yYBuzlshGNGLDSGR>D6F07V06g1Fv;Z**~P52lKcxOlbsljTbVs+m5HQvcDwh3oZ|rWlI# jxv$~=A0~z{5PI=o-;Qc}{R-+iP-u#>YHurK%s%{I#DbzL literal 0 HcmV?d00001