<?php namespace App\Classes; use Illuminate\Support\Arr; /** * Page layout is as follows * * |----------|--------------| * | LOGO | HEADER | * | LEFT BOX | TEXT | * |----------|--------------| */ class Page { // If false, no debug, if null, show padding, if true, show debug private ?bool $DEBUG = NULL; /** @var int Max width of message */ public const MSG_WIDTH = 78; /** @var int Chars to add between logo and header/text */ private const LOGO_OFFSET_WIDTH = 1; /** @var bool Do we add an "\n" when rendering */ private bool $crlf; /** @var Font Our header text */ private Font $header; /** @var string Text to go below the header */ private string $header_foot = ''; /** @var bool If the header will be right aligned */ private bool $header_right; /** @var int Character to show after the header + header_footer */ private int $header_underline = 0; /** @var Font Left box content, that goes below the logo */ private Font $left_box; /** @var bool If there is no logo, should the left box start below the header */ private bool $left_box_below_header = FALSE; /** @var ANSI ANSI logo to display */ private ANSI $logo; /** @var int Buffer of chars between logo/left box and text, 0 means retain max width */ private int $step; /** @var int The current cursor position when rendering */ private int $x; /** @var int The current line when rendering */ private int $y; private const MAX_LEFTBOX_WIDTH = 28; private string $text = ''; private bool $text_right = FALSE; public function __construct(bool $crlf=FALSE,?bool $debug=FALSE) { $this->header = new Font; $this->logo = new ANSI; $this->left_box = new Font; $this->crlf = $crlf; $this->DEBUG = $debug; } public function __get($key) { switch ($key) { // Total height of the header, including footer+underline case 'drawing_header': return $this->y < $this->header_height; case 'drawing_headcontent': return $this->y < $this->header->height; case 'drawing_headfoot': //return ($this->y < $this->header_height) && $this->header_foot; return ($this->y === $this->header->height) && $this->header_foot; case 'drawing_headunderline': return ($this->y === $this->header->height+($this->header_foot ? 1:0)) && $this->header_underline; case 'drawing_leftbox': return ($this->y >= $this->left_box_start) && ($this->y < $this->left_box_start+$this->left_box->height) && (! $this->drawing_logo); case 'drawing_logo': return $this->y < $this->logo->height; // Height of the header case 'header_height': return max($this->left_box_below_header ? 0 : $this->left_box->height+self::LOGO_OFFSET_WIDTH,$this->header->height+($this->header_foot ? 1 : 0)+($this->header_underline ? 1 : 0)); // Our header width case 'header_width': return max($this->header->width,strlen($this->header_foot)); // The width of the left column case 'left_width': return max($this->logo->width->max(),$this->left_box->width) + self::LOGO_OFFSET_WIDTH*2; // The height of the left column case 'left_height': if ($this->left_box->height && $this->left_box_below_header) { // If the logo is smaller than the header, then its the height header+left box // Otherwise its the height of the logo+spacer+left box height+space if ($this->logo->height < $this->header_height) { return $this->header_height+$this->left_box->height + self::LOGO_OFFSET_WIDTH; } else { return $this->logo->height + self::LOGO_OFFSET_WIDTH + $this->left_box->height + self::LOGO_OFFSET_WIDTH; } } else return ($this->logo->height ? $this->logo->height+self::LOGO_OFFSET_WIDTH : 0) + ($this->left_box->height ? $this->left_box->height+self::LOGO_OFFSET_WIDTH : 0); case 'left_box_start': if ($this->left_box_below_header) { return max($this->logo->height+self::LOGO_OFFSET_WIDTH,$this->header_height); } else { return max(($this->logo->height ? $this->logo->height+self::LOGO_OFFSET_WIDTH : 0),0); } // The right width case 'right_width': return self::MSG_WIDTH-$this->left_width; default: throw new \Exception(sprintf('Unknown key %s',$key)); } } /** * Message header - goes at top, right of logo * * @param Font $text * @param string $foot * @param bool $right * @param int $underline * @throws \Exception */ public function addHeader(Font $text,string $foot='',bool $right=FALSE,int $underline=0): void { if (($text->width > $this->right_width) || (strlen($foot) > $this->right_width)) throw new \Exception(sprintf('Header or Header Footer greater than available width')); $this->header = $text; $this->header_foot = $foot; $this->header_right = $right; $this->header_underline = $underline; } /** * Content that can go below logo, to the left of the text, if text $logo_left_border is TRUE * * @param Font $text * @param bool $below_header * @throws \Exception */ public function addLeftBoxContent(Font $text,bool $below_header=TRUE): void { if ($text->width > self::MAX_LEFTBOX_WIDTH) throw new \Exception(sprintf('Leftbox content greater than %d',self::MAX_LEFTBOX_WIDTH)); $this->left_box = $text; $this->left_box_below_header = $below_header; } /** * Message logo - goes at top left * * @param ANSI $ansi */ public function addLogo(ANSI $ansi): void { $this->logo = $ansi; } /** * Message text, goes after header, and if a logo, to the right of it * * @param string $text Main Body Text * @param bool $right Right Aligned */ public function addText(string $text,bool $right=FALSE) { $this->text .= $text; $this->text_right = $right; } /** * Render the page. * * @return string * @throws \Exception */ public function render(): string { $this->x = 0; $this->y = 0; $result = ''; // The output $current_pos = 0; // Current position of the text being rendered $text_length = strlen($this->text); // Length of text we need to place in the message $this->step = 0; // If step is 0, our left_width is fixed until the end of the left_height, // otherwise, its a buffer of chars between the left item and the text $text_current_color = NULL; // Current text color while (TRUE) { $result_line = ''; // Line being created /* // The buffer represents how many spaces need to pad between the left_width and whatever is drawn on the left if ($this->step) { // If we have a logo + left text box, then the min width is the max width of the left text box, until after we pass the max width // If we only have a logo and a header, we step after the header // If we only have a text box and a header above, we step after the header // If we only have a text box, and header below, we step after passing max width $buffer = $this->step; } else { $buffer = 0; } */ $buffer = 0; if ($this->DEBUG) dump([ 'line'=>$this->y, 'drawing_logo'=>$this->drawing_logo, 'drawing_header'=>$this->drawing_header, 'drawing_headcontent'=>$this->drawing_headcontent, 'drawing_headfoot'=>$this->drawing_headfoot, 'drawing_headunderline'=>$this->drawing_headunderline, 'header_height'=>$this->header_height, 'drawing_leftbox'=>$this->drawing_leftbox, 'left_height'=>$this->left_height, 'left_box_height'=>$this->left_box->height, 'logo_height'=>$this->logo->height, 'left_width'=>$this->left_width, 'left_box_start'=>$this->left_box_start, ]); $size = 0; // We are drawing our logo and/or header if ($this->drawing_logo) { $line = ANSI::bin_to_ansi([$this->logo->line($this->y)],FALSE); $size = $this->logo->line_width($this->logo->line_raw($this->y),FALSE); $result_line = str_repeat((is_null($this->DEBUG) ? 'l' : ' '),self::LOGO_OFFSET_WIDTH) .$line .str_repeat((is_null($this->DEBUG) ? 'L' : ' '),self::LOGO_OFFSET_WIDTH); $this->x += $size+self::LOGO_OFFSET_WIDTH*2; } elseif ($this->drawing_leftbox) { $line = $this->left_box->render_line($this->y-$this->left_box_start); $size = $this->left_box->width; $result_line = str_repeat((is_null($this->DEBUG) ? 'b' : ' '),self::LOGO_OFFSET_WIDTH) .$line .str_repeat((is_null($this->DEBUG) ? 'B' : ' '),self::LOGO_OFFSET_WIDTH); $this->x += $size+self::LOGO_OFFSET_WIDTH*2; // For when there is a line between the logo and left box } elseif (($this->y < $this->left_height) && ((! $this->left_box_below_header) || $this->logo->height)) { $size = $this->left_box->width; $result_line .= str_repeat((is_null($this->DEBUG) ? 'X' : ' '),self::LOGO_OFFSET_WIDTH*2+$this->left_box->width); $this->x += $this->left_box->width+self::LOGO_OFFSET_WIDTH*2; } if (($this->y < $this->left_height) && ((! $this->left_box_below_header) || $this->logo->height)) $buffer = $this->left_width-$size-self::LOGO_OFFSET_WIDTH*2; // Our step buffer $result_line .= str_repeat((is_null($this->DEBUG) ? '+' : ' '),$buffer); $this->x += $buffer; if ($this->drawing_header) { if ($this->drawing_headcontent) { // If the header is on the right, we may need to pad it if ($this->header_right) { // Pad the header with any right justification $result_line .= str_repeat((is_null($this->DEBUG) ? 'H' : ' '),self::MSG_WIDTH-$this->x-$this->header->width) .$this->header->render_line($this->y); } else { $result_line .= $this->header->render_line($this->y); } } elseif ($this->drawing_headfoot) { if ($x=ANSI::line_width($this->header_foot,FALSE)) { $result_line .= str_repeat((is_null($this->DEBUG) ? 'f' : ' '),$this->header_right ? (self::MSG_WIDTH-$this->x-$x) : ($this->left_box_below_header ? 0 : $this->left_width+$buffer-$this->x)) .ANSI::text_to_ansi($this->header_foot); } } elseif ($this->drawing_headunderline) { // Add our header underline $result_line .= str_repeat(chr($this->header_underline),self::MSG_WIDTH-$this->x); } // We are drawing the text } else { $subtext = ''; $subtext_ansi = ''; $subtext_length = 0; // If we have some text to render if ($current_pos < $text_length) { // Look for a return $return_pos = strpos($this->text,"\r",$current_pos); // We have a return if ($return_pos !== FALSE) { // If the remaining text is within our width, we'll use it all. if ($return_pos-$current_pos < self::MSG_WIDTH-$this->x-$buffer) { $subtext = substr($this->text,$current_pos,$return_pos-$current_pos); // Look for the space. } else { $space_pos = strrpos(substr($this->text,$current_pos,static::MSG_WIDTH-$this->x-$buffer),' '); $subtext = substr($this->text,$current_pos,$space_pos); } // If the reset of the string will fit on the current line } elseif ($text_length-$current_pos < static::MSG_WIDTH-$buffer) { $subtext = substr($this->text,$current_pos); // Get the next lines worth of chars, breaking on a space } else { $subtext = $this->text_substr(substr($this->text,$current_pos),static::MSG_WIDTH-$buffer); // Include the text up to the last space if (substr($this->text,$current_pos+strlen($subtext),1) !== ' ') $subtext = substr($subtext,0,strrpos($subtext,' ')); } $current_pos += strlen($subtext)+1; if ($subtext) { $subtext_length = ANSI::line_width($subtext,FALSE); $subtext_ansi = ANSI::text_to_ansi(($text_current_color ? "\x1b".$text_current_color : '').$subtext); // Get our last color used, for the next line. $m = []; preg_match('/^.*(\x1b(.))+(.*?)$/s',$subtext,$m); if (Arr::get($m,2)) $text_current_color = $m[2]; } } if ($result_line || $subtext) { $result_line .= ($subtext_ansi ? str_repeat(($this->DEBUG ? 'F' : ' '), ($this->text_right && ($this->y > $this->header_height-1) ? static::MSG_WIDTH-$subtext_length-$buffer : 0)).$subtext_ansi : ''); } } $result_line .= $this->crlf ? "\n\r" : "\r"; $this->y++; $this->x = 0; //$result_line .= ".........+.........+.........+.........+.........+.........+.........+.........+\r\n"; $result .= $result_line; if ($this->DEBUG) echo $result_line; // If we are processed our logo, left box and text, we are done if (($this->y >= $this->left_height) && ($current_pos >= $text_length)) break; } return $result; } private function text_substr(string $text,int $goal): string { $chars = $goal; while (($x=ANSI::line_width($subtext=substr($text,0,$chars),FALSE)) < $goal) { $chars += ($chars-$x); } // If the last char is an escape, we need to more chars until the last char is no longer an escape. while (preg_match('/\x1b$/',$subtext) && (strlen($subtext) < strlen($text))) { $subtext .= substr($text,strlen($subtext),2); } return $subtext; } }