php/envFiber.php

<?php
/*  output handling for cli, html or csv
    design with fiber and classes
*/

class PutEcho {
    public function put($txt) {
        echo $txt;
    }
    public function close() { }
} # end class PutEcho

class PutFile extends PutEcho {
    public $out;

    public static function make($f=':E') {
        return ($f === ':E') ? new PutEcho() : new self($f);
    }

    public function __construct($f) {
        $this->out = ($this->doClose = ! is_resource($f)) ? ($f === ':B' ? fOpen('php://memory', 'w+') : fOpen($f, 'w')) : $f;
    }

    public function put($txt) {
        fwrite($this->out, $txt);
    }

    public function clear() { # clear content and return content (before clearing of course)
        $sz = ftell($this->out);
        rewind($this->out);
        $c = $sz < 1 ? '' : fread($this->out, $sz);
        ftruncate($this->out, 0);
        rewind($this->out);
        return $c;
    }

    public function close() {
        if ($this->doClose)
            fclose($this->out);
    }

} # end class PutFile

(OutFiber::$fib = new Fiber('OutFiber::start'))->start();
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
*/
    const cTb = [
          'Html' => ['fmt' => 'OutHtml', 'tbH' => 'OutHtmlTbH' ]
        , 'Cli' => ['fmt' => 'OutCli', 'o' => ':B', 'tbH' => 'OutBufTbH' ]
        ]
    , cFmt = ['Html' => ['fmt' => 'OutHtml', 'tb' => self::cTb['Html']]
        , 'tbRetH' => ['ret' => 1, 'o' => ':B',  'fmt' => 'OutHtml', 'tbH' => 'OutBufTbH', 'tb' => self::cTb['Html']]
        , 'Cli' => ['fmt' => 'OutCli', 'tb' => self::cTb['Cli']]
        , 'tbRetC' => ['ret' => 1, ...self::cTb['Cli'], 'tb' => self::cTb['Cli']]
        ];
    public static $fib;
    public $o;
    public $last = 0; # 0 = nl , 1 nl . offset, 2 = after data

    public static function cnfCheck($cA) {
        $t = $c = is_array($cA) ? $cA : (self::cFmt[$cA] ?? err("cFmt key $cA missing"));
        isset(($c)['tb']) or err("cnfCheck missing tb in" ,$t);
        do {
            is_subclass_of($t['fmt'], __class__) or err("make bad fmt class $t[fmt] in", $c);
            isset($t['tbH']) ? (is_a($t['tbH'], 'OutHtmlTbH', true) or err("make bad tbH class $t[tbH] in", $c)) 
                             : ($t === $c or err("cnfCheck missing tbH in", $c));
        } while ($t = $t['tb'] ?? false );
        return $c;
    }

    public static function make($cA, $p = ':E') {
        /* make a new instance of OutFiber for configuration $cA
            output specified by $cA['o'], if missing in $p
        */
        $c = self::cnfCheck($cA);
        $n = new $c['fmt'];
        $n->cnf = $c;
        $n->o = ($q = $c['o'] ?? $p) instanceof PutEcho ? $q : PutFile::make($q);
        if (isset($c['tbH'])) {
            is_a($c['tbH'], 'OutHtmlTbH', true) or err("make bad tbH class $t[tbH] in", $c);
            $n->tbH = new $c['tbH'];
            $n->tbH->wrk = $n;
       }
        return $n;
    }

    public static function start() { 
        /* this is the start function for the fiber
           wait for the first resume, and then make a working instance of outfiber
        */   
        $n = Fiber::suspend();
        if ($n[0] === 'cfg') {
            $c = $n[1];
            $n = Fiber::suspend();
        } else {
            $c = (defined('OutHtml') ? OutHtml : (php_sapi_name() !== 'cli')) ? 'Html' : 'Cli';
        }
        self::make($c)->fMain($n);
    }

    public static function title($ti) {
        global $dbg;
        $b = envAssert($dbg);  # todo is wrong place!
        $t = array_map('a2str', $ti); 
        $t[] = basename(envScript()) . ' ' . implode(", ", envArgs());
        outH(array_shift($t));
        outUL();
        foreach ($t as $l)
            out($l);
        if ($dbg) {
            outLi("phpversion=" . phpversion());
            outLi($b);
        }
        outULEnd();
    }
    
    public $cnf;

    #--- f* methods: run in the (resumed) fiber stack - named after the tag they are handling (e.g. fTb)

    function fMain($n) {
        $this->gMain($n[0] === 'H' ? $n[1] : basename(envScript()));
        $n = $this->fWork($n);
        if ($n[0] !== 'End')
            err("fMain resume returned bad node", $n);
        array_shift($n);
        ($e = array_shift($n)) and $this->gH('End ' . $e);
        while ($e = array_shift($n))
            is_file($e) ? $this->gHighlightFile($e) : $this->gO($e);
        $this->gMainEnd();
   }

    function fWork($n) {
        static $ch = ['O' => 1, 'NL' => 1, 'W' => 1, 'A' => 1, 'H' => 1, 'LL' => 1, 'Tb' => 1];
        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->fWork bad or out of sequence resume input", $n);
            }
        }
        return $n; 
    }

    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, $a[1] might contain a cnf key or array, return tbH data if 'ret' => 1
        assert($a[0] === 'Tb');
        $nc = isset($a[1]) ? self::cnfCheck($a[1]) : $this->cnf['tb'];
        $nc['tb'] ??= $nc;
        $nn = self::make($nc, $this->o);
        $e = $nn->fTbInNew();
        if (1 !== ($nc['ret'] ?? 0)) { 
            $this->gTbEnd($nn->tbH);
        } elseif ($e[0] === 'TbEnd') {
             return Fiber::suspend($nn->tbH); 
        } else {
            $this->gH('warning could not return buffered Table begin');
            foreach ($nn->tbH->tb as $r)
                $this->gO('[! ' . implode(' ! ', $r) . ' !]');
            $this->gO('warning could not return buffered Table end');
        }  
        return $e[0] !== 'TbEnd' ? $e : Fiber::suspend();   
    }

    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->tbCF= [];
        $this->rx = -1; 
        $this->cx = -1; 
        $this->tbH->hTb();
        $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 $n;
    }

    public function fTCF($a) { # table row formats  
        assert($a[0] === 'TCF' and isset($this->tbCF));
        $this->tbCF= $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->tbH));
        $this->tbH->hTR();
        $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->tbH->hTREnd();
        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->tbH));
        if ($a[0] === 'TD') {
            $fmt = $a[1];
            $tx = $a[2];
            $n = Fiber::suspend();
        } else {
            $fmt = $tx = '';
            $n = $a;
        }
        $fmt .= $this->tbCF[$this->cx] ?? end($this->tbCF) ?? '';
        $fmt = (strpos($fmt, '!') === false ? '' : '!') . (strpos($fmt, 'r') === false ? 'l' : 'r');
        $this->last = 0;
        $this->tbH->hTD($tx, $fmt);
        $this->last = ($tx !== '')<<1;
        while (isset($ch[$n[0]])) {
            $n = $this->{"f$n[0]"}($n);
        }      
        $this->tbH->hTDEnd($fmt);
        $this->last = 0;
        return $n;
    }
}  # end class OutFiber

class OutHtml extends OutFiber {

    public function gMain($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 gMainEnd() { $this->o->put("\n</body>\n</html>\n"); }

    public function gHighlightFile($f) {$this->o->put(self::highlightNum($f, true)); }

    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 gTbEnd($h) { assert($h instanceOf OutHtmlTbH); $this->o->put("\n</table>"); }

    static public function highlightNum($fi, $hdr=false) { # 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);
            }
        if ($hdr)
            $r = '<h3>' . basename($fi) . '</h3>' . realpath($fi) . '<br><br>' . $r; 
        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 OutHtmlTbH { # table handlere for Html
    public function hTb() { ($this->o = $this->wrk->o)->put("\n<table border=2>"); }

    public function hTR() { $this->o->put("\n<tr>"); }

    public function hTREnd() { $this->o->put("\n</tr>"); }

    public function hTD($txt, $f) { $this->o->put("\n<" . ($f[0] === '!' ? 'th' : 'td') . ($f[-1] === 'r' ? " style='text-align: right;'" : "") . ">$txt"); }

    public function hTDEnd($f) { $this->o->put($f[0] === '!' ? '</th>' : '</td>'); }
} # end class OutHtmlTbH

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 gMain($ti) { }

    public function gMainEnd() { }

    public function gHighlightFile($f) { }

    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 gTbEnd($h) { 
        if ('' !== $f = $h->wrk->o->clear())
            err("gTbEnd o not empty but $f");
        $h->wrk->o->close();

        $tb = $h->tb;
        $fmt = $h->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 = $h->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);
            $this->gO($o . $o[-1]);
        };

        if ($this->last !== 0)    
            $this->gO('');

        $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"));
                }       
                $this->gO($tx . $cc[$fx]);
            }
            if ($sL)       
                $sep($cc[4], $cc[5], $fmt[$rx]);
        }
        if (! $sL)       
            $sep($cc[4], $cc[5], []);
    }
} # end class OutCli

class OutBufTbH extends OutHtmlTbH {
    public function  hTb() { # destination and work OutFiber's
        $this->tb = [];
        $this->fmt = [];
    }

    public function hTR() { 
        $this->tb[] = [];
        $this->fmt[] = [];
    }

    public function hTREnd() { 
    }

    public function hTD($txt, $f) {
        $this->fmt[$this->wrk->rx][$this->wrk->cx] = $f;
        $this->wrk->o->put($txt);
    }

    public function hTDEnd($f) { # store td or th data
        $this->tb[$this->wrk->rx][$this->wrk->cx] = $this->wrk->o->clear();
    }
} # end class OutBufTbH

class OutCsvTbH extends OutBufTbH {

    public function tbGen() {
        $this->dst->gO( '///' . ((($this->fmt[0][0] ?? ' ')[0] === '!' and (end($this->fmt[0]) ?? ' ')[0] === '!') ? 'tbH ' : 'tb ') 
            . ($this->id ?? 'tableId')); 
        if (! $this->dst->o instanceof PutEcho) {
            foreach ($this->tb as $r)
                fputcsv($this->csvO->out, $r);
        } else { 
            foreach ($this->tb as $r) {
                fputcsv($this->wrk->o->out, $r);
                $this->work->o->put($this->o->clear());
            }
        }
        $this->dst->gO( '///tbEnd');
    }

} # end class OutCsvTbH

function outBegin (...$a) { # first call, initialize everything, write title
    OutFiber::title($a);
}

function outFlush() { # flush the fiber stack and continue ...
    OutFiber::$fib->resume(['Flush']);   
}

function outEnd (...$a) { # last call, print source ($t) and cleanup
    OutFiber::$fib->resume(['End', ...(count($a) > 1 ? $a 
            : (('-' === ($t = $a[0] ?? envScript())) ? [] :[basename($t), 'args ' . implode(', ', envArgs()), $t]))]);
}

function outErr($msg) {
    global $dbg;
       $short = "{*** " . (strlen($msg) < 35 ? $msg : substr($msg, 0, 30) . ' ...') . " ***}";

    if (OutFiber::$fib and OutFiber::$fib->isSuspended()) {
        outNL($short);
        outFlush();
        outH($msg);
    } else {
        echo EnvCL ? "\n$msg\n" : "\n<br>\n$msg<br>\n";
    }
}

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 outTbD($c, ...$hh) { # table begin, optional with first 0 ..n TD td       
    OutFiber::$fib->resume(['Tb', $c]);
    foreach ($hh as $h)
        OutFiber::$fib->resume(['TD', '!', a2str($h)]);
}

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())]);
    return OutFiber::$fib->resume(['TbEnd']);
}

function outTbEndCsv($f, $id=null) { # table end (must be buffered) and output to csv       
    $h = OutFiber::$fib->resume(['TbEnd']);
    $o = is_string($f) ? PutFile::make($f) : $f;
    if ($id !== null)
        $o->put( '///' . ((($h->fmt[0][0] ?? ' ')[0] === '!' and (end($h->fmt[0]) ?? ' ')[0] === '!') ? 'tbH' : 'tb') . " $id\n");
    if (! $o instanceof PutEcho) {
        foreach ($this->tb as $r)
            fputcsv($o->out, $r);
    } else { 
        $b = PutFile::make(':B'); 
        foreach ($h->tb as $r) {
            fputcsv($b->out, $r);
            $o->put($b->clear());
        }
    }
    if ($id !== null)
        $o->put( "///tbEnd $id\n");
    if (is_string($f))
        $o->close();
}


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)]);
}