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), [' ' => '&nbsp;']) . '</span>&nbsp;&nbsp;';
        $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 &lt;br&gt;***' . 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)]);
}