<?php declare(strict_types=1);  
  namespace SimpleRecipe; # 
  if (!defined('PmWiki')) exit();
  DEFINE ('SIMPLERECIPENAME', 'SimpleRecipe');
  DEFINE ('VERYSIMPLERECIPENAME', 'VerySimpleRecipe');
  DEFINE ('SIMPLERECIPENEW', ''); # empty string for production version, 'new' for development verion
  define ('SIMPLERECIPEID', SIMPLERECIPENAME . SIMPLERECIPENEW);
  if ($DisableRecipe[SIMPLERECIPENAME] === true) return;
/* SimpleRecipeTemplate  
<describe purpose of recipe>
See https://www.pmwiki.org/wiki/Cookbook/SimpleRecipeTemplate and https://kiwiwiki.nz/pmwiki/pmwiki.php/Cookbook/SimpleRecipeTemplate
+
  Copyright 2022-present Simon Davis
  This software is written for PmWiki; you can redistribute it and/or modify it under the terms
  of the GNU General Public License as published by the Free Software Foundation; 
  either version 3 of the License, or (at your option) any later version. 
  See pmwiki.php for full details and lack of warranty.
  
  The recipe implements a PmWiki directive whose parameters are
      hex=#
      len=#
+
Revision History <reverse chronological order please>
# 2022-04-24 Add more outputs
# 2022-01-22 Initial version

<add additional information here>
Search and replace this template for "SimpleRecipe" with "YourRecipeName"
*/
//=================================================================================================
/*
  This recipe illustrates some simple features of PmWiki recipes.
  * \Markup ():      directive markup (:directive parm=value:) text (:directiveend:)
  * \ParseArgs ():   PmWiki function to create key value array from directive parameters
  * \Keep ():        PmWiki function to return HTML rather than wiki markup for page
  * \SDV ():         override default values from a configuration file
  * $MessageFmt:     log messages to the default PmWiki logging mechanism
  * $HTMLStylesFmt:  insert CSS for the recipe into the page
  * $HTMLHeaderFmt:  insert into HTML <head> metadata element
  * $HTMLFooterFmt:  insert text just before </body> tag
  * dmsg:            a way to debug a recipe
  * $RecipeInfo:     publish recipe version for PmWiki Site Analyzer and Recipe Check
  * $FmtPV:          create a page text variable enabling version display on a page
*/
//=================================================================================================

    \SDV($SimpleRecipeInitialvalue, 'GetAnInitialValue'); # get an initial value from a configuration file
# insert into HTML <head> metadata element
//  $HTMLHeaderFmt [SIMPLERECIPEID] = "<link rel='stylesheet' type='text/css' href='\$PubDirUrl/css/" . SIMPLERECIPENAME . ".css' />\n";
# set default formatting for recipe classes
    \SDV($HTMLStylesFmt[SIMPLERECIPEID],
    '.simplerecipe {display: inline-block; font-family: monospace; white-space: pre-wrap;}' . NL
    . '.simplerecipedebug {font-size: smaller; font-family: monospace;}' . NL
    . '.verysimple {font-size: smaller; font-family: monospace;}' . NL);
# set debug flag
    \SDV($SimpleRecipeDebug, false); # set default debug setting if not defined in an configuration file
    $SimpleRecipe_debugon = boolval ($SimpleRecipeDebug); # if on writes input and output to web page
# Version date
    $RecipeInfo[SIMPLERECIPENAME]['Version'] = '2022-04-24' . SIMPLERECIPENEW; # PmWiki version numbering is done by date
# recipe version page variable
    $FmtPV['$SimpleRecipeVersion'] = "'" . SIMPLERECIPENAME . ' version ' . $RecipeInfo[SIMPLERECIPENAME]['Version'] . "'"; // return version as a custom page variable
  
    if ($SimpleRecipe_debugon) 
		dmsg(__FILE__, $RecipeInfo[SIMPLERECIPENAME]['Version']);

# declare $SimpleRecipe for (:if enabled SimpleRecipe:) recipe installation check
    $SimpleRecipe = true; # enabled
#
// Initialise constants
    DEFINE ('NL', "\n");
    DEFINE ('BR', '<br/>' . NL);
    $Transmacrons = array ('ā' => '&#257;', 'ē' => '&#275;', 'ī' => '&#299;', 'ō' => '&#333;', 'ū' => '&#363;', 'Ā' => '&#256;', 'Ē' => '&#274;', 'Ī' => '&#298;', 'Ō' => '&#332;', 'Ū' => '&#362;', '’' => '&#8217;');
## Add a PmWiki custom markup 
# (:simplerecipe hex= len= :) (:simplerecipeend:)
## the following builds the regex to parse the SimpleRecipe PmWiki directive input parameters
# directive arguments are
#  type=
    $phex     = 'hex=' . qte ('on|off|only', true);
    $plen     = 'len=' . qte ('(?:100|[1-9][0-9]|9|8)', true);
## 
    $qparm    = '(' . $phex . ')|(' . $plen . ')';

    $qpattern = '(?:(?:' . $qparm . ')\s*){0,2}';

    if ($SimpleRecipe_debugon) dmsg ('qpattern',  mb_strtolower(SIMPLERECIPENAME) . ": " . $qpattern);
    $markup_pattern = '/\\(:' 
      . mb_strtolower(SIMPLERECIPENAME) . '\s*(?:' . $qpattern . ')\s*:\\)'
      . '(.*?)'
      . '\\(:'
      . mb_strtolower(SIMPLERECIPENAME) . 'end\s*'
      . ':\\)/sim';
##
# Markup is an internal PmWiki function that defines the custom markup for the wiki (see https://www.pmwiki.org/wiki/PmWiki/CustomMarkup)
    # (:simplerecipe optional by syntacically defined parameters:) arbitrary text (:simplerecipeend:)
    \Markup(SIMPLERECIPEID, #name
      '[=', # when, e.g. fulltext, directives
      $markup_pattern, # pattern
      __NAMESPACE__ . '\SimpleRecipe_Parse' );
    if ($SimpleRecipe_debugon) dmsg('markup',$markup_pattern);
# s = dot matches all chars including newline
# i = case insensitive
# m = multiline
# uses lazy evaluation, preserves leading and trailing white space
# Keep prevents PmWiki markup being applied
//
# another example of a directive
    # (:verysimplerecipe Optional parameters:)
    $very_simple_markup_pattern = '/\\(:'
      . mb_strtolower(VERYSIMPLERECIPENAME) . '(.*)?:\\)/i';
    # when has to be at least fulltext
    \Markup(VERYSIMPLERECIPENAME . SIMPLERECIPENEW, #name
      'fulltext', # when, e.g. fulltext, directives
      $very_simple_markup_pattern, # pattern
      __NAMESPACE__ . '\VerySimpleRecipe_Parse' );
    if ($SimpleRecipe_debugon) dmsg('very simple markup',$very_simple_markup_pattern);
//
    return; # completed simple recipe setup
/*-----------------------------------------------------------------------------------------------------------*/
#
/** Main SimpleRecipe parser
 *   /param   arguments as documented above
 *   /return  The HTML-formatted information wrapped in a <article> of class "simplerecipe".
 */
function SimpleRecipe_Parse(array $m):string {
#
    global $SimpleRecipe_debugon; # import variables

// Initialise variables
    $retval      = '<:block><article class="simplerecipe">'; # return value
#   extract by line the text between the start and end directive
    $recipetext = explode("\n", $m[array_key_last ($m)]); # direct text is always last
    # see PmWiki documentation https://www.pmwiki.org/wiki/Cookbook/ParseArgs
    $args         = \ParseArgs(implode (' ', array_slice ($m, 1))); # skip full text and directive text, see pmWiki documentation  https://www.pmwiki.org/wiki/Cookbook/ParseArgs
// 
  if($SimpleRecipe_debugon) { # display inputs and outputs to wiki page
    dmsg ("<hr>\n", SIMPLERECIPEID);
    dmsg ('m', $m);
    dmsg ('args', $args);
    dmsg ('recipetext', $recipetext);
  } # end SimpleRecipe_debugon
//
    $float    = array_key_exists ('float', $args)  ? 'float:' . $args['float'] . ';' : ''; # article parameter
    $clear    = array_key_exists ('clear', $args)  ? 'clear:' . $args['clear'] . ';' : ''; # article parameter
    $particle = (array_key_exists ('float', $args) || array_key_exists ('clear', $args)) ? ' style="' . $clear . $float . '"' : ''; # article positioning
    
## query string parameters
    $bhex = array_key_exists ('hex', $args);
    $blen = array_key_exists ('len', $args);

## extract parameters
    $display_hex = boolval ($args['hex']);
    $display_len = ($blen) ? intval ($args['len']) : 16;

$totlines  = count($recipetext);
$nrwidth   = max(2, intdiv($totlines, 10)); # calculate digits for line numbering
if ($SimpleRecipe_debugon) dmsg ('totlines', $totlines . ' ' . $nrwidth . ' #' . count ($m) . ' ^' . array_key_last ($m));
$linecount = 0; # count lines for line numbering
  foreach($recipetext as $txtval) {
      if ($txtval == '<:vspace>') $txtval = ''; # remove PmWiki empty line marker
      $retval .= dline ($txtval, ++$linecount, $nrwidth, $display_len);
  }
  $retval .= "</article>\n";
  # returns html
  return \Keep($retval); # Keep prevents PmWiki markup being applied
} # end SimpleRecipe_Parse
##
/** Very Simple Recipe parser
*   /param   arguments as documented above
*   /return  The PmWiki-formatted information.
*/
function VerySimpleRecipe_Parse (array $m):string { 
#
    global $SimpleRecipe_debugon; # import variables
    global $RecipeInfo, $HandleActions, $FmtV, $CustomSyntax, $MarkupExpr;
    global $HTMLStylesFmt, $HTMLHeaderFmt, $HTMLFooterFmt, $UploadExts, $UploadExtSize, $WikiStyle;
    global $ThumbList, $_SERVER;
    $pagename = $GLOBALS['MarkupToHTML']['pagename'];
    $aladmin = false;
    $aledit  = false;
    $alread  = false;
    switch (PmWikiAuthLevel($pagename)) {
        case 'admin' : $aladmin = true;
        case 'edit'  : $aledit  = true;
        case 'read'  : $alread  = true;
        default: break;
    }
    $UploadInfo = [];
    $args = \ParseArgs($m[1]); # contains all text within directive
// 
    if ($SimpleRecipe_debugon) { # display inputs and outputs to wiki page
        dmsg ("<hr>\n", 'Very' . SIMPLERECIPEID);
        dmsg ('m[]', $m);
        dmsg ('args[]', $args);
    } # end SimpleRecipe_debugon
    $displayopts = array_key_exists ('display', $args)  ? explode(',', $args ['display']) : ['']; # defaults to none
    $debugopt = $args['debug'] === 'true';
    if ($debugopt) $SimpleRecipe_debugon = true; # set on
    $retval = '';
    foreach ($displayopts as $argval) {
        switch (true) {
            case ($argval == ''): # none specified
                $retval .= '!! Very Simple Recipe parameters' . NL 
                    . 'display= all, recipeinfo, customsyntax, defined, fmtv, handleactions, htmlstylesfmt, htmlheaderfmt, htmlfooterfmt, '
                    . 'markupexpr, server, phpinfo, thumblist, uploadexts, uploadextsize, vars, wikistyle, debug' . BR
                    . 'debug= false, true' . BR;
                break;
            case ($argval == 'all' || $argval == 'recipeinfo'):
                $retval .= '!! RecipeInfo' . NL;
                $retval .= SRDisplayInfo ($RecipeInfo);
                if ($argval == 'recipeinfo') break;
            case ($argval == 'all' || $argval == 'handleactions'):
                $retval .= '!! HandleActions' . NL;
                $retval .= SRDisplayInfo ($HandleActions);
                if ($argval == 'handleactions') break;
            case ($argval == 'all' || $argval == 'fmtv'):
                $retval .= '!! FmtV' . NL;
                $retval .= SRDisplayInfo ($FmtV);
                if ($argval == 'fmtv') break;
            case ($argval == 'all' || $argval == 'customsyntax'):
                $retval .= '!! CustomSyntax' . NL;
                $retval .= SRDisplayInfo ($CustomSyntax);
                if ($argval == 'customsyntax') break;
            case ($argval == 'all' || $argval == 'markupexpr'):
                $retval .= '!! MarkupExpr' . NL;
                $retval .= SRDisplayInfo ($MarkupExpr);
                if ($argval == 'markupexpr') break;
            case ($argval == 'all' || $argval == 'htmlstylesfmt'):
                $retval .= '!! HTMLStylesFmt' . NL;
                $retval .= SRDisplayInfo ($HTMLStylesFmt);
                if ($argval == 'htmlstylesfmt') break;
            case ($argval == 'all' || $argval == 'htmlheaderfmt'):
                $retval .= '!! HTMLHeaderFmt' . NL;
                $retval .= SRDisplayInfo ($HTMLHeaderFmt);
                if ($argval == 'htmlheaderfmt') break;
            case ($argval == 'all' || $argval == 'htmlheaderfmt'):
                $retval .= '!! HTMLHeaderFmt' . NL;
                $retval .= SRDisplayInfo ($HTMLHeaderFmt);
                if ($argval == 'htmlheaderfmt') break;
            case ($argval == 'all' || $argval == 'htmlfooterfmt'):
                $retval .= '!! HTMLFooterFmt' . NL;
                $retval .= SRDisplayInfo ($HTMLFooterFmt);
                if ($argval == 'htmlfooterfmt') break;
            case ($argval == 'phpinfo'):  # not included in 'all'
                $retval .= '!! PhpInfo' . NL;
                if ($aladmin) {
                    $retval .= '(:details summary="PhpInfo":)' .NL;
                    foreach (get_phpinfo() as $info) {
                        $retval .= '!!! ' . key ($info) . NL;
                        $retval .= SRDisplayInfo ($info);
                    }
                    $retval .= '(:detailsend:)' . NL;
                } else {$retval .= 'Please login as "admin".' . BR;}
                if ($argval == 'phpinfo') break;
            case ($argval == 'all' || $argval == 'vars'):
                $retval .= '!! Variables' . NL;
                $retval .= SRDisplayInfo (SRVars ());
                if ($argval == 'vars') break;
            case ($argval == 'all' || $argval == 'uploadexts' || $argval == 'uploadextsize'):
                foreach ($UploadExts as $kext => $kval) $UploadInfo [$kext] ['ext'] = $kval;
                foreach ($UploadExtSize as $kext => $kval) $UploadInfo [$kext] ['size'] = $kval;
                $retval .= '!! $UploadExts' . NL;
                $retval .= SRDisplayInfo ($UploadInfo, true);
                if ($argval == 'uploadexts' || $argval == 'uploadextsize') break;
            case ($argval == 'all' || $argval == 'wikistyle'):
                $retval .= '!! WikiStyle' . NL;
                $retval .= SRDisplayInfo ($WikiStyle, true);
                if ($argval == 'wikistyle') break;
            case ($argval == 'all' || $argval == 'server'):
                $retval .= '!! Server' . NL;
                if ($aladmin) {
                    $retval .= SRDisplayInfo ($_SERVER, true);
                } else {$retval .= 'Please login as "admin".' . BR;}
                if ($argval == 'server') break;
            case ($argval == 'all' || $argval == 'thumblist'):
                $retval .= '!! ThumbList' . NL;
                $retval .= SRDisplayInfo ($ThumbList);
                if ($argval == 'thumblist') break;
            case ( $argval == 'defined'): # not included in 'all'
                $retval .= '!! Defined' . NL;
                if ($aladmin) {
                    $retval .= '(:details summary="Defined constants":)' .NL;
                    $retval .= SRDisplayInfo (get_defined_constants());
                    $retval .= '(:detailsend:)' . NL . '(:details summary="Defined vars":)' . NL;
                    //$retval .= SRDisplayInfo (array_slice (get_defined_vars(), 0, 100), true);
                    $retval .= 'Too large to display' . NL;
                    $retval .= '(:detailsend:)' . NL . '(:details summary="Defined functions":)' . NL;
                    $retval .= SRDisplayInfo (get_defined_functions());
                    $retval .= '(:detailsend:)' . NL;
                } else {$retval .= 'Please login as "admin".' . BR;}
                if ($argval == 'defined') break;
            ##
            case ($argval == 'all' || $argval == 'debug'):
                $retval .= '!! Debug information' . NL;
                $retval .= SRDebugInfo ($m, $args);
                if ($argval == 'debug') break;
            ##
/*          case ($argval == 'all' || $argval == 'entities'):
                $Entities = get_html_translation_table(HTML_ENTITIES);
                $retval .= '!! HTML entities' . NL;
                $retval .= SRDisplayInfo (array_values ($Entities)); 
                if ($argval == 'entities') break;*/
            case ($argval == 'all'): break;
            default: $retval .= 'Unknown display option: "' . $argval . '"' . BR;  
        }
    }
    # returns wiki markup
    return $retval;
} # end VerySimpleRecipe_Parse
##
// these are functional parts of the recipe
# display the information for the array supplied
function SRDisplayInfo ($srinfo, bool $details = false): string {
    global $SimpleRecipe_debugon; # import variables
    # sort keys (i.e. recipe name) ascending, case insensitive
    if (empty ($srinfo)) return 'No data' . BR;
    if (!ksort ($srinfo, SORT_FLAG_CASE | SORT_STRING)) return 'Sort failed' . BR;
    $retval = '';
    if ($details) $retval .= '(:details:)' . NL;
    $retval .= NL. '(:table:)'; # can't be simple table as some strings have newlines in them
    foreach ($srinfo as $key => $value) {
        $retval .= NL . '(:cellnr:)' . NL . \Keep (htmlentities (strval ($key), ENT_NOQUOTES)) . NL . '(:cell:)=';
        if (is_array ($value)) {
            $retval .= NL . '(:cell:)' . NL;
            foreach ($value as $rcpkey => $rcpval) {
                $retval .= \Keep (htmlentities (strval ($rcpkey))) . ':&nbsp;';
                if (is_array ($rcpval)) {
                    $retval .= \Keep (htmlentities (print_r($rcpval, true), ENT_NOQUOTES));
                } else {
                    $retval .= \Keep (htmlentities (strval ($rcpval), ENT_NOQUOTES));
                }
            $retval .= BR;
            }
        } else {
            $retval .= NL . '(:cell:)' . NL . \Keep (htmlentities (strval ($value), ENT_NOQUOTES));
        }
    }
    $retval .= NL . '(:tableend:)' . NL;
    if ($details) $retval .= '(:detailsend:)' . NL;
    return $retval;
} # end SRDisplayInfo
##
function PmWikiAuthLevel(string $pagename): string {
  global $AuthLevels;
  \SDV($AuthLevels, array('admin', 'edit', 'read'));
  foreach($AuthLevels as $level)
      if (RetrieveAuthPage($pagename, $level, false, READPAGE_CURRENT))
          return $level;
  return '(none)';
}
// $FmtPV['$AuthLevel'] = 'PmWikiAuthLevel($pn)';
#3
function SRVars (): array {
    global $Author, $AuthorGroup, $CategoryGroup, $CookiePrefix, $DefaultGroup, $DefaultName, $DefaultPage, $RecipeInfo;
    global $SiteAdminGroup, $SiteGroup, $Skin, $Version;
    return array (
        'Author' => $Author,
        'AuthorGroup' => $AuthorGroup,
        'CategoryGroup' => $CategoryGroup,
        'CookiePrefix' => $CookiePrefix, 
        'DefaultGroup' => $DefaultGroup,
        'DefaultName' => $DefaultName,
        'DefaultPage' => $DefaultPage,
        'Skin' => $Skin,
        'SiteAdminGroup' => $SiteAdminGroup,
        'SiteGroup' => $SiteGroup,
        'Version' => $Version,
        'RecipeInfo' => $RecipeInfo[SIMPLERECIPENAME]['Version']
    );
}
##
function SRDebugInfo (array $srmarkup, array $srargs): string {
    $retval = '(:div class="verysimple":)m[]: ';
    $retval .= SRDispArray ($srmarkup);
    $retval .= 'args[]: ';
    unset ($srargs ['#']); # don't display this
    $retval .= SRDispArray ($srargs);
    return $retval . BR . '(:divend:)' . BR;
} # end SRDebugInfo
##
function SRDispArray (array $ArrayVal):string {
    array_walk_recursive ($ArrayVal, __NAMESPACE__ . '\dsanitise');
    $keeptxt = '';
    foreach ($ArrayVal as $key => $value) {
        $keeptxt .= '[' . $key . '] => ';
        if (is_array ($value)) {
            foreach ($value as $k2 => $v2) $keeptxt .=  $v2 . '; ';
        } else {
            $keeptxt .= $value;
        }
        $keeptxt .= BR;
    }
    # use \Keep as markup contains PmWiki markup
    return htmlspecialchars (\Keep($keeptxt));
}
##
// get php information
function get_phpinfo (): array {
    ob_start ();                              // Capturing null, 0,  PHP_OUTPUT_HANDLER_CLEANABLE ^ PHP_OUTPUT_HANDLER_REMOVABLE 
    phpinfo ();                               // phpinfo ()
    $html = trim (ob_get_clean ());           // output and clean
    if (isset($_SERVER['AUTH_USER']))     $html = str_replace($_SERVER['AUTH_USER'], '#####', $html);
    if (isset($_SERVER['AUTH_PASSWORD'])) $html = str_replace($_SERVER['AUTH_PASSWORD'], '#####', $html);
    if (isset($_SERVER['PHP_AUTH_USER'])) $html = str_replace($_SERVER['PHP_AUTH_USER'], '*****', $html);
    if (isset($_SERVER['PHP_AUTH_PW']))   $html = str_replace($_SERVER['PHP_AUTH_PW'], '*****', $html);
    $strt = strpos($html, '<body>') + strlen('<body>'); # Only works when <body> has no attributes
    $fnsh = '</body>';
    $html = trim(substr($html, $strt, strpos($html, $fnsh) - $strt));
    $phpinfo = explode (NL, strip_tags($html, '<tr><td><h2>'));
    $retval = array();
    $cat = 'General';
    foreach($phpinfo as $line) {
        // new cat?
        preg_match('~<h2>(.*)</h2>~', $line, $title) ? $cat = $title[1] : null;
        if(preg_match('~<tr><td[^>]+>([^<]*)</td><td[^>]+>([^<]*)</td></tr>~', $line, $val)) {
            $retval[$cat][$val[1]] = $val[2];
        }
        elseif(preg_match('~<tr><td[^>]+>([^<]*)</td><td[^>]+>([^<]*)</td><td[^>]+>([^<]*)</td></tr>~', $line, $val)) {
            $retval[$cat][$val[1]] = array('local' => $val[2], 'master' => $val[3]);
        }
    }
    return $retval;
}
##
// generate regex for quoted parameters
function qte (string $parm, bool $unqt = false): string {
    # use a named regex capture group to allow a parameter to be single or double quoted, 
    static $namenr = 0; # to generate unique names
    # named capture group is (?<q1>['"]),  named back reference is \k<q1>)
    $retval = '(?<q' . ++$namenr . '>\\\'|\"';
    if ($unqt) $retval .= '|'; # allow unquoted strings
    return $retval . ')(?:' . $parm . ')\k<q' . $namenr . '>';
    # the only drawback in the the quote (or null) is in a captured group and is returned by the regex
} # end qte
##
// Display one line of text from screen
function dline (string $txtvalin, int $linecount, int $nrwidth, int $dsplnlen=16): string {
    global $SimpleRecipe_debugon; # import variables
    $txtval = htmlspecialchars_decode ($txtvalin); # remove quoted entities inserted by Pmwiki
    $llength = strlen ($txtval); # length of text in the line
    $dispnr = intdiv($llength, $dsplnlen); # number of display blocks of text
    $retval = '';
    $lnrval = str_pad(strval($linecount), $nrwidth, '0', STR_PAD_LEFT);
    $hexpad = intval($dsplnlen * 2.5); # hex doubles characters plus space per 4 hex chars
    for ($counter=0; $counter<=$dispnr; $counter++) {
        # display chunk of line in hex, with spacing, and line padded
        $retval .= $lnrval . ': | ';
        $subtxt = substr ($txtval, $counter * $dsplnlen, $dsplnlen);
        $lentxt = strlen ($subtxt);
        $retval .= str_pad (dhexline ($subtxt), $hexpad, ' ');
        $retval .= '| ';
    	# from https://stackoverflow.com/questions/1176904/how-to-remove-all-non-printable-characters-in-a-string
        # /u modifier pattern and subject strings are treated as UTF-8.
        $valtmp = preg_replace ('/[\x00-\x1F\x20\x7F-\xA0\xAD]/u', '&middot;', htmlspecialchars ($subtxt));
        $retval .= $valtmp . str_pad ('', $dsplnlen - $lentxt, ' ');
        $retval .= ' |'. NL;
        if ($SimpleRecipe_debugon) dmsg ('dline ' . $lnrval, $lentxt . '="' . $subtxt . '", "' . $valtmp . '"');
    }
    return $retval;
} # end dline

// Space line into blocks of characters
function dhexline (string $valtxt, int $blen=4): string {
    global $SimpleRecipe_debugon; # import variables
    $hexval = bin2hex ($valtxt); # convert to hex
    $hexlen = strlen ($hexval); # length of hex string
    $blocknr = intdiv($hexlen, $blen); # calculate number of hex text blocks 
    if ($blocknr == 0) return $hexval;
    for ($counter=$blocknr; $counter>0; $counter--) {
        $hexval = substr_replace($hexval, ' ', $counter * $blen, 0);
    }
    return $hexval;
} # end dhexline
##
// Debug function
function dmsg(string $dmsgprefix, $dmsgdata) { 
# see https://www.pmwiki.org/wiki/Cookbook/DebuggingForCookbookAuthors
# add debug text to message array
    global $Transmacrons, $MessagesFmt, $SimpleRecipe_debugon;
    $MessageFmt  = '<span class="simplerecipedebug"><i>' . $dmsgprefix . '</i>: ';
    switch (true) {
        case (is_null($dmsgdata)) : 
            $MessageFmt .= 'null'; 
            break;
        case (is_bool($dmsgdata)) : 
            $MessageFmt .= var_export($dmsgdata,true); 
            break;
        case (is_array($dmsgdata)) :
            array_walk_recursive ($dmsgdata, __NAMESPACE__ . '\dsanitise');
 		    $MessageFmt .= '<pre>' . print_r($dmsgdata, true) . '</pre>' . NL; 
            break;
        case (is_string ($dmsgdata)):
            $MessageFmt .= "'" . strtr (htmlspecialchars ($dmsgdata), (array) $Transmacrons) . "'";
            break;            
	    default: 
            $MessageFmt .= '"' . htmlspecialchars (strval ($dmsgdata)) . '"'; 
            break;
    }
    $MessageFmt .= '</span>' . BR;
	$MessagesFmt [SIMPLERECIPENAME] .= $MessageFmt;
    return;
} # end dmsg
##
    function dsanitise(&$item, $itemkey) {
# escape html special characters   
        global $Transmacrons;
        if (is_string($item)) {
            $item = strtr (htmlspecialchars ($item, ENT_NOQUOTES), $Transmacrons); 
        }
} # end dsanitise