php/envFiberOld.php
<?php
/* output handling for cli, html or csv
design with fiber and classes
*/
class PutEcho {
public function put($txt) {
echo $txt;
}
}
class PutMem extends PutEcho {
public $out;
function __construct() {
$this->out = fOpen('php://memory', 'w+');
}
public function put($txt) {
fwrite($this->out, $txt);
}
public function close() {
fclose($this->out);
}
public function flush() {
$sz = ftell($this->out);
rewind($this->out);
$c = $sz < 1 ? '' : fread($this->out, $sz);
ftruncate($this->out, 0);
rewind($this->out);
return $c;
}
} # end class PutMem
class OutFiber {
/* handles the stack of the fiber
methods f* (with * the tag, allowing dynamic calls) run within the fiber, add missing end tags, begin tags etc.
and call the methods g* to generate the concrete text from subclasses
*/
public static $fib;
public $o;
public $last = 0; # 0 = nl , 1 nl . offset, 2 = after data
public static function start($oI, $ti='start') { #start the fiber
if (self::$fib and (self::$fib->isSuspended() or self::$fib->isRunning()))
err("OutFiber::start but already started");
$oI->o ??= new PutEcho();
(self::$fib = new Fiber(fn () => $oI->fBegin($ti)))->start();
}
public static function begin($oi, $ti) {
global $dbg;
$b = envAssert($dbg);
$t = array_map('a2str', $ti);
$t[] = basename(envScript()) . ' ' . implode(", ", envArgs());
OutFiber::start($oi ?? (OutHtml ? new OutHtml() : new OutCli()), $t[0]);
outH(array_shift($t));
outUL();
foreach ($t as $l)
out($l);
if ($dbg) {
outLi("phpversion=" . phpversion());
outLi($b);
}
outULEnd();
}
#--- f* methods: run in the (resumed) fiber stack - named after the tag they are handling (e.g. fTb)
function fBegin($ti) {
static $ch = ['O' => 1, 'NL' => 1, 'W' => 1, 'A' => 1, 'H' => 1, 'LL' => 1, 'Tb' => 1];
# $this->o->put("fiber fBegin\n");
$this->gBegin($ti);
$n = Fiber::suspend();
while ($n[0] !== 'End') {
if (isset($ch[$n[0]])) {
$n = $this->{"f$n[0]"}($n);
} elseif ($n[0] === 'Flush') {
$n = Fiber::suspend();
} else {
err("OutFiber->fBegin bad resume request", $n);
}
}
$this->gH('End ' . ($n[1] ?? ''));
$this->gEnd($n[2] ?? '');
}
public function fA($a) { # text without any separators, neither before nor after
assert($a[0] === 'A');
if ($a[1] !== '') {
$this->gA($a[1]);
$this->last = 2;
}
return Fiber::suspend();
}
public function fW($a) { # text with space before if necessary
assert($a[0] === 'W');
if ($a[1] !== '') {
$this->gA(substr(' ', $this->last < 2) . $a[1]);
$this->last = 2;
}
return Fiber::suspend();
}
public function fO($a) { # text with space before if necessary, and an nl after
assert($a[0] === 'O');
$this->gO(substr(' ', $this->last < 2 or $a[1] === '') . $a[1]);
$this->last = 0;
return Fiber::suspend();
}
public function fNL($a) { # text with an nl before
assert($a[0] === 'NL');
$this->gNL($a[1]);
$this->last = ($a[1] !== '') << 1;
return Fiber::suspend();
}
public function fH($a) {
assert($a[0] === 'H');
$this->gH($a[1]);
$this->last = 0;
return Fiber::suspend();
}
public function fLL($a) {
static $ch = ['O' => 1, 'NL' => 1, 'W' => 1, 'A' => 1, 'Tb' => 1, 'TCF' => 1, 'LL' => 1, 'Li' => 1];
assert($a[0] === 'LL');
$oLx = $this->lx ?? 'none';
$oLTy = $this->lTy ?? 'none';
$this->lTy = $a[1];
$this->gLL($a[1]);
$n = Fiber::suspend();
for ($this->lx=1; isset($ch[$n[0]]); $this->lx++) {
$n = $this->fLi($n);
}
$this->gLLEnd($a[1]);
$this->lx = $oLx;
$this->lTy = $oLTy;
if ($n[0] !== 'LLEnd')
return $n;
assert($a[1] === $n[1]);
return Fiber::suspend();
}
public function fLi($a) { # List entry
static $ch = ['O' => 1, 'NL' => 1, 'W' => 1, 'A' => 1, 'LL' => 1, 'Tb' => 1, 'TCF' => 1];
if ($a[0] === 'Li') {
$this->gLi($a[1]);
$this->last = 1 + ($a[1] !== '');
$n = Fiber::suspend();
} else {
$this->gLi('');
$this->last = 1;
$n = $a;
}
while (isset($ch[$n[0]])) {
$n = $this->{"f$n[0]"}($n);
}
$this->gLiEnd($a[1]);
$this->last = 0;
return $n;
}
public function fTb($a) { # table begin
assert($a[0] === 'Tb');
$nn = new (get_class($this))();
$nn->o = $this->o;
$r = $nn->fTbInNew();
if ($r[0] !== null)
$this->oTbEndData($r[0]);
return $r[1];
}
public function fTbInNew() { # table begin in new instance
static $ch = ['O' => 1, 'NL' => 1, 'W' => 1, 'A' => 1, 'LL' => 1, 'Tb' => 1, 'TR' => 1, 'TD' => 1, 'TCF' => 2];
$this->TCF = [];
$this->rx = -1;
$this->cx = -1;
$this->gTb();
$n = Fiber::suspend();
while (isset($ch[$n[0]])) {
if ($n[0] === 'TCF') {
$n = $this->fTCF($n);
} else {
$this->rx++;
$this->cx = -1;
$n = $this->fTR($n);
}
}
return [$this->gTbEnd(), $n[0] === 'TbEnd' ? Fiber::suspend() : $n];
}
public function fTCF($a) { # table row formats
assert($a[0] === 'TCF' and isset($this->TCF));
if (! isset($this->TCF))
error("sytTCF but not in table");
$this->TCF = $a[1];
$this->TbF = $a[2] ?? null;
return Fiber::suspend();
}
public function fTR($a) { # table row
static $ch = ['O' => 1, 'NL' => 1, 'W' => 1, 'A' => 1, 'LL' => 1, 'Tb' => 1, 'TD' => 1, 'TCF' => 2];
assert(isset($this->TCF));
$this->gTR();
$n = $a[0] === 'TR' ? Fiber::suspend() : $a;
while (isset($ch[$n[0]])) {
if ($n[0] === 'TCF') {
$n = $this->fTCF($n);
} else {
$this->cx++;
$n = $this->fTD($n);
}
}
$this->gTREnd();
return $n;
}
public function fTD($a) { # table data
static $ch = ['O' => 1, 'NL' => 1, 'W' => 1, 'A' => 1, 'LL' => 1, 'Tb' => 1, 'TCF' => 2];
assert(isset($this->TCF));
if ($a[0] === 'TD') {
$fmt = $a[1];
$tx = $a[2];
$n = Fiber::suspend();
} else {
$fmt = $tx = '';
$n = $a;
}
$fmt .= $this->TCF[$this->cx] ?? end($this->TCF) ?? '';
$fmt = (strpos($fmt, '!') === false ? '' : '!') . (strpos($fmt, 'r') === false ? 'l' : 'r');
$this->last = 0;
$this->gTD($fmt, $tx);
$this->last = $tx === '' ? 0 : 2;
while (isset($ch[$n[0]])) {
$n = $this->{"f$n[0]"}($n);
}
$this->gTDEnd($fmt);
$this->last = 0;
return $n;
}
} # end class OutFiber
class OutHtml extends OutFiber {
public function gBegin($ti) {
$this->o->put("<html>\n<head>"
. '<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=3.0, user-scalable=yes"/>'
. ($ti === '' ? '' : "<title>$ti</title>")
. "\n</head>\n<body style='font-family: monospace;' >\n");
}
public function gEnd($fn) { $this->o->put(($fn ? "$fn<br><br>" . self::highlightNum($fn) : '') . "\n</body>\n</html>\n"); }
public function gA($txt) { $this->o->put($txt); }
public function gO($txt) { $this->o->put("$txt\n<br>"); }
public function gNL($txt) { $this->o->put("\n<br>$txt"); }
public function gH($txt) { $this->o->put("\n<h2>$txt</h2>"); }
public function gLL($fmt) { $this->o->put("\n<$fmt>"); }
public function gLLEnd($fmt) { $this->o->put("\n</$fmt>"); }
public function gLi($txt) { $this->o->put("\n<li>$txt"); }
public function gLiEnd() { $this->o->put("</li>"); }
public function gTb() { $this->o->put("\n<table border=2>"); }
public function gTbEnd() { $this->o->put("\n</table>"); }
public function gTR() { $this->o->put("\n<tr>"); }
public function gTREnd() { $this->o->put("\n</tr>"); }
public function gTD($f, $txt) { $this->o->put("\n<" . ($f[0] === '!' ? 'th' : 'td') . ($f[-1] === 'r' ? " style='text-align: right;'" : "") . ">$txt"); }
public function gTDEnd($f) { $this->o->put($f[0] === '!' ? '</th>' : '</td>'); }
static public function highlightNum($fi) { # highlight file with lineNumbers
$s = highlight_file($fi, true);
$ln = 1;
$hn = fn($no) => '<span style="color: #ffffff; background-color: #a0e0a0;">'
. strtr(sprintf('%6u ', $no), [' ' => ' ']) . '</span> ';
$cy = strpos($s, '<br />');
if (false !== ($sx = strpos($s, '<span')) and false !== ($sy = strpos($s, '>', $sx)) and $sy < $cy) {
# lineNo 1 after first span and possibly \n which must remain after first span
$cx = $sy + 1 + (substr($s, $sy, 2) === ">\n");
$r = substr($s, 0, $cx) . $hn($ln) . substr($s, $cx, $cy + 6 -$cx);
}
else {
$r = '*** code does not have a span berfore first <br>***' . substr($s, 0, $cy + 6);
}
while (FALSE !== ($cy = strpos($s, '<br />', $cx=$cy+6))) {
$r .= $hn(++$ln) . substr($s, $cx, $cy + 6 - $cx);
}
return $r . substr($s, $cx);
}
} # end class OutHtml
class OutCli extends OutFiber {
protected $lvl = 0;
protected $intend = '';
public function level($l) {
$this->lvl = $l >= 0 ? $l : $this->lvl + 10 + $l;
$this->intend = str_pad('', 4 * $this->lvl);
}
public function gBegin() { }
public function gEnd() { }
public function gA($txt) { $this->o->put($this->last === 0 ? "{$this->intend}$txt" : "$txt"); }
public function gO($txt) { $this->o->put($this->last === 0 ? "{$this->intend}$txt\n" : "$txt\n"); }
public function gNL($txt) { $this->o->put("\n$this->intend$txt"); }
public function gH($txt) {$this->o->put(substr("\n", $this->last < 2) . "\n--- $txt ---\n"); }
public function gLL($fmt) {
$this->level(-9); // +1
}
public function gLLEnd() {
$this->level(-11); // -1
}
public function gLi($txt) {
$rp = max(1, 4-$this->lvl);
$this->o->put(($this->last === 0 ? '' : "\n") . substr($this->intend, 0, -2)
. ($this->lTy === 'ol' ? ($this->lx) . str_repeat('.', $rp) : str_repeat('*', $rp)) . " $txt");
}
public function gLiEnd() {
if ($this->last !== 0)
$this->o->put("\n");
}
public function gTb() {
$this->o = new PutMem();
$this->tb = [];
$this->fmt = [];
}
public function gTbEnd() {
if (ftell($this->o->out))
err("gTbEnd o not empty but", $this->o->flush());
$this->o->close();
return $this->tbGen($this->tb, $this->fmt);
}
public function oTbEndData($data) {
if ($this->last !== 0)
$this->gO('');
foreach ($data as $l)
$this->gO($l);
}
public function gTR() {
$this->tb[] = [];
$this->fmt[] = [];
}
public function gTREnd() {
}
public function gTD($f, $txt) {
$this->fmt[$this->rx][$this->cx] = $f;
$this->o->put($txt);
}
public function gTDEnd() { # store td or th data
$this->tb[$this->rx][$this->cx] = $this->o->flush();
}
function tbGen($tb, $fmt) {
# echo i2str(['tbGen table', $tb, 'fmt', $fmt]);
$cc = '+:*!-='; # data sep (first, next) header sep (first, next) sep sep (data, hdr)
$cLen = [];
$rLin = [];
foreach ($tb as $rx => $rw) {
foreach ($rw as $cx => $ce) {
$s = preg_split("/\R/", $ce);
while ((end($s) ?? 1) === '')
array_pop($s);
if (count($s) > ($rLin[$rx] ?? -1))
$rLin[$rx] = count($s);
$tb[$rx][$cx] = $s;
# $l = array_reduce($s, fn($r, $e) => ($r > $l = iconv_strlen($e)) ? $r : $l, 0);
$l = $cLen[$cx] ?? -1;
foreach ($s as $sx => $se)
$l = max($l, iconv_strlen($se) + (($rx === 0 and (($fmt[$rx][$sx] ?? ' ')[0] === '!'))<<1));
$cLen[$cx] = $l;
}
}
# echo i2str(['tbGen table' , $tb, 'cLen', $cLen, 'rLin', $rLin]);
$tf = $this->TbF ?? '';
$sL = strpos($tf, 'a') !== false;
$sep = function($d, $h, $ff) use($cLen) {
$o = '';
foreach ($cLen as $cx => $cl)
$o .= str_repeat(($ff[$cx][0] ?? '') === '!' ? $h : $d, $cl+3);
return $o . $o[-1];
};
$da = [$sep($cc[4], $cc[5], [])];
for ($rx=0; $rx<count($rLin); $rx++) {
for ($lx=0; $lx< $rLin[$rx]; $lx++) {
$tx = '';
foreach ($cLen as $cx => $cl) {
$f1 = $fmt[$rx][$cx] ?? ' ';
$fx = ($f1[0] === '!' ? 2 : 0) + ($lx === 0 ? 0 : 1);
$t1 = $tb[$rx][$cx][$lx] ?? '';
$p = str_repeat($p1 = ($f1[0] === '!' && $t1 !== '') ? $cc[2] : ' ', $cl - iconv_strlen($t1));
$tx .= $cc[$fx] . (trim($t1) === '' ? "$p1$p$p1" : ($f1[-1] === 'r' ? "$p $t1 " : " $t1 $p"));
}
$da[] = $tx . $cc[$fx];
}
if ($sL)
$da[] = $sep($cc[4], $cc[5], $fmt[$rx]);
}
if (! $sL)
$da[] = $sep($cc[4], $cc[5], []);
return $da;
}
} # end class OutCli
class OutCsv extends OutCli {
public function gTb() {
$this->o->put("\n");
$this->csvO = $this->o;
$this->o = new PutMem();
}
public function gTbEnd() {
if (ftell($this->o->out))
err("gTbEnd o not empty but", $this->o->flush());
$this->o->close();
}
public function gTR() {
$this->row = [];
}
public function gTREnd() {
if ('' !== $c = $this->o->flush())
err("gTREnd o not empty but $c");
# echo "\nrow " . a2str($this->row) . "\n";
if (! $this->csvO instanceof PutEcho) {
fputcsv($this->csvO->out, $this->row);
} else {
fputcsv($this->o->out, $this->row);
$this->csvO->put($this->o->flush());
}
}
public function gTD($f, $txt) {
$this->o->put($txt);
}
public function gTDEnd() { # store td or th data
$this->row[$this->cx] = $this->o->flush();
}
} # end class OutCsv
function outBegin (...$a) { # first call, initialize everything, write title
OutFiber::begin(is_string($a[0]) ? null : array_shift($a), $a);
}
function outFlush() { # flush the fiber stack and continue ...
OutFiber::$fib->resume(['Flush']);
}
function outEnd ($a = false) { # last call, print source ($t) and cleanup
OutFiber::$fib->resume(['End', basename($a ? $a : envScript()) . ' ' . implode(', ', envArgs()), $a]);
}
function outErr($msg) {
global $dbg;
if (OutFiber::$fib and OutFiber::$fib->isSuspended()) {
outNL("(*$msg*)");
outFlush();
outH($msg);
}
}
function highlightNum($fi) {
if (OutHtml)
OutFiber::$fib->resume(['O', OutHTML::highlightNum($fi)]);
}
function outA() { # text without any separators, neither before nor after
OutFiber::$fib->resume(['A', func_num_args() === 1 ? a2str(func_get_arg(0)) : (func_num_args() > 1 ? i2str(func_get_args()) : '')]);
}
function outW() { # text with a separating space before if necessary
OutFiber::$fib->resume(['W', func_num_args() === 1 ? a2str(func_get_arg(0)) : (func_num_args() > 1 ? i2str(func_get_args()) : '')]);
}
function out() { # text with a separating space before if necessary and nl after
OutFiber::$fib->resume(['O', func_num_args() === 1 ? a2str(func_get_arg(0)) : (func_num_args() > 1 ? i2str(func_get_args()) : '')]);
}
function outNL() {# text with a nl before
OutFiber::$fib->resume(['NL', func_num_args() === 1 ? a2str(func_get_arg(0)) : (func_num_args() > 1 ? i2str(func_get_args()) : '')]);
}
function outEC() { # data in format $ = $, ... and nl
OutFiber::$fib->resume(['O', func_num_args() === 1 ? a2str(func_get_arg(0)) : (func_num_args() > 1 ? i2strEC(func_get_args()) : '')]);
}
function outWEC() { # data in format $ = $, ... and nl
OutFiber::$fib->resume(['W', func_num_args() === 1 ? a2str(func_get_arg(0)) : (func_num_args() > 1 ? i2strEC(func_get_args()) : '')]);
}
function outACE() { # data in format $ = $, ... and nl
OutFiber::$fib->resume(['A', ', ' . (func_num_args() === 1 ? a2str(func_get_arg(0)) : (func_num_args() > 1 ? i2strEC(func_get_args()) : ''))]);
}
function outH() { # header
OutFiber::$fib->resume(['H', func_num_args() === 1 ? a2str(func_get_arg(0)) : (func_num_args() > 1 ? i2str(func_get_args()) : '')]);
}
function outOL() { # ordered=numbered list, optional with first item
OutFiber::$fib->resume(['LL', 'ol']);
if (func_num_args() > 0)
OutFiber::$fib->resume(['Li', func_num_args() === 1 ? a2str(func_get_arg(0)) : i2str(func_get_args())]);
}
function outOLEnd() { # end ordered list
if (func_num_args() > 0)
OutFiber::$fib->resume(['Li', func_num_args() === 1 ? a2str(func_get_arg(0)) : i2str(func_get_args())]);
OutFiber::$fib->resume(['LLEnd', 'ol']);
}
function outLi() { # listitem (in ol or ul)
OutFiber::$fib->resume(['Li', func_num_args() === 1 ? a2str(func_get_arg(0)) : (func_num_args() > 1 ? i2str(func_get_args()) : '')]);
}
function outUL() { # undordered= bulleted list, optional with first item
OutFiber::$fib->resume(['LL', 'ul']);
if (func_num_args() > 0)
OutFiber::$fib->resume(['Li', func_num_args() === 1 ? a2str(func_get_arg(0)) : i2str(func_get_args())]);
}
function outULEnd() { # end undordered list
if (func_num_args() > 0)
OutFiber::$fib->resume(['Li', func_num_args() === 1 ? a2str(func_get_arg(0)) : i2str(func_get_args())]);
OutFiber::$fib->resume(['LLEnd', 'ul']);
}
function outTb() { # table begin, optional with first td
OutFiber::$fib->resume(['Tb']);
if (func_num_args() > 0)
OutFiber::$fib->resume(['TD', '', func_num_args() == 1 ? a2str(func_get_arg(0)) : i2str(func_get_args())]);
}
function outTbEnd() { # table end
if (func_num_args() > 0)
OutFiber::$fib->resume(['TD', '', func_num_args() == 1 ? a2str(func_get_arg(0)) : i2str(func_get_args())]);
OutFiber::$fib->resume(['TbEnd']);
}
function outTCF(...$a) { # table column format, currently only l (left align default) and r (right align)
$f = is_array($a[0]) ? $a[0] : $a;
foreach ($f as $e)
if ($e !== 'l' and $e !== 'r' and $e !== '!' and $e !== '!l' and $e !== '!r' )
err("bad format $e in outTCF(", $f, ')');
OutFiber::$fib->resume(['TCF', $f, is_array($a[0]) ? $a[1] ?? null : null]) ;
}
function outTR() { # row begin, optional with first td
OutFiber::$fib->resume(['TR']);
if (func_num_args() > 0)
OutFiber::$fib->resume(['TD', '', func_num_args() == 1 ? a2str(func_get_arg(0)) : i2str(func_get_args())]);
}
function outTRD() { # row begin, with 0 to n datacells
OutFiber::$fib->resume(['TR']);
foreach (func_get_args() as $d)
OutFiber::$fib->resume(['TD', '', a2str($d)]);
}
function outTRH() { # row begin, with 0 to n headercells
OutFiber::$fib->resume(['TR']);
foreach (func_get_args() as $h)
OutFiber::$fib->resume(['TD', '!', a2str($h)]);
}
function outTD() { # td = dataCell begin, optional with first words
OutFiber::$fib->resume(['TD', '', func_num_args() === 1 ? a2str(func_get_arg(0)) : (func_num_args() > 1 ? i2str(func_get_args()) : '')]);
}
function outTDD() { # tdd = 0 to n dataCells
foreach (func_get_args() as $d)
OutFiber::$fib->resume(['TD', '', a2str($d)]);
}
function outTH() { # th = headerCell begin, optional with first words
OutFiber::$fib->resume(['TD', '!', func_num_args() === 1 ? a2str(func_get_arg(0)) : (func_num_args() > 1 ? i2str(func_get_args()) : '')]);
}
function outTHH() { # 0 to n headerCell
foreach (func_get_args() as $h)
OutFiber::$fib->resume(['TD', '!', a2str($h)]);
}