<?php if (!defined('PmWiki')) exit();
/*
  PmWiki module for generating forms.

  See http://www.pmwiki.org/wiki/Cookbook/Input
  for documentation.

  Copyright (C) 2005 Joachim Durchholz.
  This code is licensed under the
  GNU General Public License version 2.0
  as distributed with PmWiki.
*/

define('INPUT_VERSION', '0.94');

Markup(
  'input',
  '>directives',
  '/(?:^\\(|(\\()):input(.*?):\\)/ei',
  'InputMakeHtml(PSS(\'$1\'), PSS(\'$2\'))'
);


function InputWarning($msg) {
  return
    '<i><b>[PmWiki Warning: '
    . htmlentities($msg, ENT_QUOTES)
    . ']</b></i>';
}

# Helper functions for constructing entries in $InputFmt.

function InputOptionBoolean($option) {
  return " $option=\"$option\"";
}

function InputOptionString($option) {
  return '/^.*$/e/\' ' . $option . '="\' . htmlentities(\'$0\') . \'"\'';
}

SDVA($InputMethods, array(
  'get'
    => ' method="get" enctype="application/x-www-form-urlencoded"',
  'post/urlencoded'
    => ' method="post" enctype="application/x-www-form-urlencoded"',
  'post/binary'
    => ' method="post" enctype="multipart/form-data"',
  'post/text'
    => ' method="post" enctype="text/plain"'
));
SDV($InputMethods['post'], $InputMethods['post/binary']);

SDVA($InputFmt, array(
# All (:input...:) markup is of the form:
#   (:input <verb> <required> <optional>:)
# For each <verb>, this array has the following entries:
#   <verb>.fmt
#     The HTML to emit, with $xxx placeholders
#     (xxx is the name of the placeheld position).
#   <verb>.required
#     A space-separated list of parameter names.
#     These will be filled from the required (positional)
#     parameters of the markup.
#   <verb>.<option>.default
#     What to generate for $<option> if the markup doesn't have
#     <option>=value.
#   <verb>.<option>.present
#     What to generate if just <option> (without =<value>)
#     is supplied by the markup.
#   <verb>.<option>.match
#     A pattern-plus-replacement string that's used to generate
#     the text for $<option> if the markup supplies an
#     <option>=<value> pair.
#     The string is expected to have the form
#       /<pattern>/<modifiers>/<replacement>
#     where
#       <pattern>     is a PCRE pattern that's supposed to
#                     match the markup-supplied <value>,
#       <modifiers>   are PCRE modifiers,
#       <replacement> is a replacement string that will
#                     be substituted into the HTML output.
#     /<pattern>/<modifiers> and <replacement> are sumitted to PHP's
#     preg_replace function (see http://www.php.net/preg_replace).
#     You can use an alternate delimiter instead of /, but Perl-style
#     (), {}, [], or <> delimiters won't work. All other features
#     of preg_replace (in particular subpattern matching,
#     backreferences, and the /e modifier) work as described.
  'start.fmt' => '<form$script$method>',
  'start.required' => 'script',
  'start.script.match' => '/^.*$/e/\' action="\' . PUE(\'$0\') . \'"\'',
  'start.method.default' => $InputMethods['post'],
  'start.method.match' => '/^.*$/ei/$InputMethods[\'$0\']',
  'end.fmt' => '</form>',
  'button.fmt' => '<input type="submit"$name$label$disabled/>',
  'button.required' => 'name label',
  'button.name.match' => InputOptionString('name'),
  'button.label.match' => InputOptionString('value'),
  'button.disabled.present' => InputOptionBoolean ('disabled'),
  'hidden.fmt' => '<input type="hidden"$name$value/>',
  'hidden.required' => 'name value',
  'hidden.name.match' => InputOptionString('name'),
  'hidden.value.match' => InputOptionString('value'),
  'line.fmt' => '<input$password$name$columns$disabled$maxcolumns$readonly$text/>',
  'line.required' => 'name',
  'line.name.match' => InputOptionString('name'),
  'line.password.default' => ' type="text"',
  'line.password.present' => ' type="password"',
  'line.columns.match' => InputOptionString ('size'),
  'line.disabled.present' => ' disabled="disabled"',
  'line.maxcolumns.match' => InputOptionString('maxlength'),
  'line.readonly.present' => InputOptionBoolean('readonly'),
  'line.text.match' => InputOptionString('value'),
  'text.fmt' => '<textarea$name$columns$disabled$readonly$rows>$text</textarea>',
  'text.required' => 'name',
  'text.name.match' => InputOptionString('name'),
  'text.columns.match' => InputOptionString('cols'),
  'text.columns.default' => ' cols="65"',
  'text.disabled.present' => InputOptionBoolean('disabled'),
  'text.readonly.present' => InputOptionBoolean('readonly'),
  'text.rows.match' => InputOptionString('rows'),
  'text.rows.default' => ' rows="5"',
  'text.text.match' => '/^.*$/e/htmlentities(PSS(\'$0\'))',
  'checkbox.fmt' => '<input type="checkbox"$name$disabled$checked$value/>',
  'checkbox.required' => 'name',
  'checkbox.name.match' => InputOptionString('name'),
  'checkbox.disabled.present' => InputOptionBoolean('disabled'),
  'checkbox.checked.present' => InputOptionBoolean('checked'),
  'checkbox.value.match' => InputOptionString('value'),
  'radiobutton.fmt' => '<input type="radio"$name$disabled$checked$value/>',
  'radiobutton.required' => 'name',
  'radiobutton.name.match' => InputOptionString('name'),
  'radiobutton.disabled.present' => InputOptionBoolean('disabled'),
  'radiobutton.checked.present' => InputOptionBoolean('checked'),
  'radiobutton.value.match' => InputOptionString('value'),
  'menu.fmt' => '<select$name$multiple$disabled$lines>',
  'menu.required' => 'name',
  'menu.name.match' => '/^.*$/e/\' name="\' . htmlentities(\'$0\')',
  'menu.multiple.present' => '[]"' . InputOptionBoolean('multiple'),
  'menu.multiple.default' => '"',
  'menu.disabled.present' => InputOptionBoolean('disabled'),
  'menu.lines.match' => InputOptionString('size'),
  'menu.lines.default' => ' size="1"',
  'menuentry.fmt' => '<option$checked$disabled$label>$value</option>',
  'menuentry.required' => 'value',
  'menuentry.value.match' => '/^.*$/e/htmlentities(PSS(\'$0\'))',
  'menuentry.checked.present' => InputOptionBoolean('selected'),
  'menuentry.disabled.present' => InputOptionBoolean('disabled'),
  'menugroup.fmt' => '<optgroup$label$disabled>',
  'menugroup.required' => 'label',
  'menugroup.label.match' => InputOptionString('label'),
  'menugroup.disabled.present' => InputOptionBoolean('disabled'),
  'menugroupend.fmt' => '</optgroup>',
  'menuend.fmt' => '</select>'
  ));

function InputFindDefault($verb, $option) {
  global $InputFmt;
  return IsEnabled($InputFmt["$verb.$option.default"], '');
}

function InputMakeControl($verb, $args) {
  global $InputFmt, $InputMethods;
  $fmt = $InputFmt["$verb.fmt"];
  if (!isset($fmt)) {
    return InputWarning ("'$verb' unknown in (:input $verb ...:).");
  }
  $required_str = $InputFmt["$verb.required"];
  if (isset($required_str)) {
    $required = explode(' ', $required_str);
  } else {
    $required = array();
  }
  $args = array_slice($args ['#'], 2);
  while (count($args) > 0) {
    $param = array_shift($args);
    $arg = array_shift($args);
    $positional_name = array_shift($required);
    if (isset($positional_name)) {
      if (strlen($param) > 0 && $param != $positional_name) {
        return InputWarning("(:input $verb:) needs positional parameters '$required_str', but those starting from '$positional_name' are missing.");
      } else {
        $param = $positional_name;
      }
    } else {
      if (strlen($param) == 0) {
        $param = $arg;
        $arg = 1;
      }
    }
    $text = $InputFmt["$verb.$param.present"];
    if (!isset($text)) {
      $text = $InputFmt["$verb.$param.match"];
      if (isset($text)) {
        preg_match('/^((.).*?\\2.*?)\\2(.*)$/', $text, $matches);
        $pat = $matches['1'];
        $rep = $matches['3'];
        if (preg_match($pat, $arg) > 0) {
          $text = preg_replace($pat, $rep, $arg, -1);
        } else {
          return InputWarning("Unrecognised value '$arg' for option '$param' in (:input $verb...:) markup.");
        }
      } else {
        return InputWarning("Unknown option '$param' in (:input $verb...:) markup.");
      }
    }
    $fmt = str_replace("\$$param", $text, $fmt);
  }
  # $args is processed, now fill in defaults
  $fmt =
    preg_replace(
      '/\\$([a-z]+)/ei',
      'InputFindDefault(\'' . $verb . '\', \'$1\')',
      $fmt);
  if (count($required) > 0) {
    return InputWarning("(:input $verb:) needs positional parameters '$required_str', but those starting from '$required[0]' are missing.");
  }
  return $fmt;
}

$InputActive = 0;

function InputMakeHtml($flag, $argstring) {
  global $InputActive;
  $o = array();
  $at_line_start = $flag == '';
  $args = ParseArgs($argstring);
  $verb = $args[''][0];
  if ($verb == 'start' || $verb == 'end') {
    if ($at_line_start) {
      $o[] = '<:block>';
    } else {
      return Keep(InputWarning ("(:input $verb...:) must be at the start of a line."));
    }
  }
  if (!isset($verb)) {
    $o[] = Keep(InputWarning('(:input:) needs at least one parameter.'));
  } else {
    if ($verb == 'start') {
      if ($InputActive) {
        $o[] = '</form>';
      } else {
        $InputActive = 1;
      }
    } else {
      if (!$InputActive) {
        $o[] = Keep(InputWarning('There should have been an (:input start...:) before this (:input...:) markup.'));
      }
      if ($verb == 'end') {
        $InputActive = 0;
      }
    }
    $o[] = Keep(InputMakeControl($verb, $args));
  }
  return implode("\n", $o);
}