clrghouz/app/Classes/Page.php

406 lines
13 KiB
PHP

<?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;
return $this;
}
/**
* 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 rest of the string will fit on the current line
} elseif ($text_length-$current_pos < static::MSG_WIDTH-$this->x-$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-$this->x-$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;
}
}