<?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 http://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-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
  * 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;}'
    . '.simplerecipedebug {font-size: smaller; font-family: monospace;}'
    . '.verysimple {font-size: smaller; font-family: monospace;}');
# set debug flag
    \SDV($SimpleRecipeDebug, false); # set default debug setting if not defined in an configuration file
    $SimpleRecipe_debugon = (bool) $SimpleRecipeDebug; # if on writes input and output to web page
# Version date
    $RecipeInfo[SIMPLERECIPENAME]['Version'] = '2022-01-22' . 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 {
  list ($p0, $p1, $p2, $p3, $p4, $p5, $p6, $p7, $p8, $p9) = $m; 
#
    global $SimpleRecipe_debugon; # import variables

// Initialise variables
    $retval      = '<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, -1))); # skip full text and directive text
// 
  if($SimpleRecipe_debugon) { # display inputs and outputs to wiki page
    dmsg ("<hr>\n", SIMPLERECIPEID);
    dmsg ('m', $m);
    dmsg ('input:', 'p0="' . $p0 . '" p1="' . $p1 . '" p2="' . $p2 . '" p3="' . $p3 . '" p4="' . $p4 . '" p5="' . $p5 . '" p6="' . $p6 . '" p7="' . $p7 . '" p8="' . $p8 . '" p9="' . $p9 . '"');
    dmsg ('args', $args);
    dmsg ('recipetext', $recipetext);
  } # end SimpleRecipe_debugon
//
    $float    = (bool) $args['float']  ? 'float:' . $args['float'] . ';' : ''; # article parameter
    $clear    = (bool) $args['clear']  ? 'clear:' . $args['clear'] . ';' : ''; # article parameter
    $particle = (bool) $args['float'] . $args['clear'] ? ' style="' . $clear . $float . '"' : ''; # article positioning
    
## query string parameters
    $bhex = (boolean) $args['hex'];
    $blen = (boolean) $args['len'];

## extract parameters
    $display_hex   = $args['hex'];
    $display_len = ($blen) ? (int) $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;
    $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 ('opt[]', $args);
    } # end SimpleRecipe_debugon
    $displayopts = (bool) $args['display']  ? explode(',', $args ['display']) : ['']; # defaults to all
    $debugopt = $args['display'] === 'true';
    if ($debugopt) $SimpleRecipe_debugon = true; # set on
    $retval = '';
    foreach ($displayopts as $argval) {
        switch (true) {
            case ($argval == ''):
                $retval .= '!! Very Simple Recipe parameters' . BR 
                    . 'display= all, recipeinfo, handleactions, debug' . BR
                    . 'debug= false, true' . BR;
                break;
            case ($argval == 'all' || $argval == 'recipeinfo'):
                $retval .= '!! RecipeInfo' . BR;
                $retval .= SRDisplayInfo ($RecipeInfo);
                if ($argval == 'recipeinfo') break;
            case ($argval == 'all' || $argval == 'handleactions'):
                $retval .= '!! HandleActions' . BR;
                $retval .= SRDisplayInfo ($HandleActions);
                if ($argval == 'handleactions') break;
            case ($argval == 'all' || $argval == 'debug'):
                $retval .= '!! Debug information' . BR;
                $retval .= SRDebugInfo ($m, $args);
                if ($argval == 'debug') break;
/*          case ($argval == 'all' || $argval == 'entities'):
                $Entities = get_html_translation_table(HTML_ENTITIES);
                $retval .= '!! HTML entities' . BR;
                $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 (array $srinfo): string {
    global $SimpleRecipe_debugon; # import variables
    # sort keys (i.e. recipe name) ascending, case insensitive
    if (!ksort ($srinfo, SORT_FLAG_CASE | SORT_STRING)) return 'Sort failed';
    $retval = '';
    foreach ($srinfo as $key => $value) {
        $retval .= '||[=' . htmlentities ((string) $key) . '=] ';
        if (is_array ($value)) {
            foreach ($value as $rcpkey => $rcpval) {
                # restore html entities now we have calculated string length
                $retval .= ' ||' . htmlentities ($rcpkey) . ': ' . htmlentities ($rcpval) . ', ';
            }
        } else {
            $retval .= ' ||= ' . htmlentities ($value);
        }
        $retval .= '||' . BR;
    }
    return $retval;
} # end SRDisplayInfo
##
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));
}
##
// 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, $dmsgtxt) { 
# 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($dmsgtxt)) : 
            $MessageFmt .= 'null'; 
            break;
        case (is_bool($dmsgtxt)) : 
            $MessageFmt .= var_export($dmsgtxt,true); 
            break;
        case (is_array($dmsgtxt)) :
            array_walk_recursive ($dmsgtxt, __NAMESPACE__ . '\dsanitise');
 		    $MessageFmt .= '<pre>' . print_r($dmsgtxt, true) . '</pre>' . NL; 
            break;
        case (is_string ($dmsgtxt)):
            $MessageFmt .= "'" . strtr (htmlspecialchars ($dmsgtxt), (array) $Transmacrons) . "'";
            break;            
	    default: 
            $MessageFmt .= '"' . htmlspecialchars ($dmsgtxt) . '"'; 
            break;
    }
    $MessageFmt .= '</span>' . BR;
	$MessagesFmt [SIMPLERECIPENAME] .= $MessageFmt;
} # 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