    Provide parsing support for macro function keyword parameters

    The routines allow a sort of preprocessor for function arguments.
    - Any arguments following an empty string argument are handled as usual,
      although the first empty string argument is dropped.
    - Before an empty string argument, strings of the form "KeyWord=Value"
      or "KeyWord=Value, KeyWord=Value; KeyWord: Value\n ..." etc are treated
      as keywords, and removed from the argument list. Instead they are added
      to a keyword array. Note that this allows nested keyword arrays for
      strings like "KeyWord=[ key:val, ]", with brackets, parentheses or braces
      used to delimit the sub-array.
    - Before an empty string argument, arrays are handled as follows: all
      entries with keys that look like words ([A-Za-z_][A-Za-z_0-9]*) are copied
      to the keyword array; any elements with numeric keys are treated as
      normal arguments (by copying to an argument list array) if the keys
      don't contain whitespace and form an unbroken sequence of increasing
      values starting at 1.

    The set_keywords_and_args_array(...) built-in serves as an example, and
    can also be used for building such arrays in macro code. For example, the
    code
	    s = "a=b, c=d\n e=f; subarray = { x=1; y: 2, label:'right on' }"
	    z[0] = "not there"
	    z[1] = "hello"
	    z[2] = "there"
	    z["zed"] = "dead"
	    a = set_keywords_and_args_array(s, z, "", s, z)

    does the same thing as
	    s = "a=b, c=d\n e=f; subarray = { x=1; y: 2, label:'right on' }"
	    z[0] = "not there"
	    z[1] = "hello"
	    z[2] = "there"
	    z["zed"] = "dead"
	    # --- positioned arguments
	    a[1] = "hello"      # from z before ""
	    a[2] = "there"      # from z
	    a[3] = s            # the s argument following ""
	    a[4] = z            # the z argument following ""
	    # --- keywords
	    a["a"] = "b"
	    a["c"] = "d"
	    a["e"] = "f"
	    a["subarray"] = $empty_array
	    a["subarray"]["x"] = 1
	    a["subarray"]["y"] = 2
	    a["subarray"]["label"] = "right on"

diff -ur nedit_official nedit_mod
diff -ur nedit_official/source/macro.c nedit_mod/source/macro.c
--- nedit_official/source/macro.c	2006-10-13 09:26:02.000000000 +0200
+++ nedit_mod/source/macro.c	2006-10-31 20:30:52.656250000 +0100
@@ -403,6 +403,8 @@
         DataValue *result, char **errMsg);
 static int filenameDialogMS(WindowInfo* window, DataValue* argList, int nArgs,
         DataValue* result, char** errMsg);
+static int setKeywordsAndArgsArrayMS(WindowInfo *window, DataValue *argList,
+        int nArgs, DataValue *result, char **errMsg);
 
 /* Built-in subroutines and variables for the macro language */
 static BuiltInSubr MacroSubrs[] = {lengthMS, getRangeMS, tPrintMS,
@@ -422,6 +424,7 @@
         rangesetGetByNameMS,
         getPatternByNameMS, getPatternAtPosMS,
-        getStyleByNameMS, getStyleAtPosMS, filenameDialogMS
+        getStyleByNameMS, getStyleAtPosMS, filenameDialogMS,
+        setKeywordsAndArgsArrayMS,
     };
 #define N_MACRO_SUBRS (sizeof MacroSubrs/sizeof *MacroSubrs)
 static const char *MacroSubrNames[N_MACRO_SUBRS] = {"length", "get_range", "t_print",
@@ -442,6 +445,7 @@
         "rangeset_get_by_name",
         "get_pattern_by_name", "get_pattern_at_pos",
-        "get_style_by_name", "get_style_at_pos", "filename_dialog"
+        "get_style_by_name", "get_style_at_pos", "filename_dialog",
+        "set_keywords_and_args_array",
     };
 static BuiltInSubr SpecialVars[] = {cursorMV, lineMV, columnMV,
         fileNameMV, filePathMV, lengthMV, selectionStartMV, selectionEndMV,
@@ -5772,3 +5776,509 @@
     *errMsg = "%s called with unknown object";
     return False;
 }
+
+/* like isalpha, but returns non-zero for underscore */
+static int is_alpha(int ch)
+{
+    return (isalpha((unsigned char)ch) || ch == '_');
+}
+/* like isalnum, but returns non-zero for underscore */
+static int is_alnum(int ch)
+{
+    return (isalnum((unsigned char)ch) || ch == '_');
+}
+
+/*
+static const char *keywordLetters()
+{
+    return "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_";
+}
+static const char *keywordLetterNum()
+{
+    return "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_";
+}
+*/
+
+/* Matches a string if it looks like a word with no spaces (is_alpha() followed
+** by a sequence of is_alnum()).
+*/
+static Boolean isAKeyword(const char *str)
+{
+    int ok;
+    const char *p = str;
+    for (ok = is_alpha(*p); ok; ok = is_alnum(*p))
+        ++p;
+    return (p != str && *p == '\0');
+}
+
+/* Scans over a word, returning the scanned length, which is zero for non-words.
+*/
+static ptrdiff_t scanAKeyword(const char *str)
+{
+    int ok;
+    const char *p = str;
+    for (ok = is_alpha(*p); ok; ok = is_alnum(*p))
+        ++p;
+    return p - str;
+}
+
+/* Returns the matching bracketing or quote character or nul-char (false).
+*/
+static char isAStartBracket(char ch)
+{
+    switch (ch)
+      {
+      case '[':  return ']';
+      case '{':  return '}';
+      case '(':  return ')';
+      case '\'': return '\'';
+      case '\"': return '\"';
+      default:   break;
+      }
+    return '\0';
+}
+
+/* Run through a string checking its format for keyword assignments; return the
+** scanned length if the whole string looks like possibly multiple assignments.
+*/
+static ptrdiff_t isKeywords(const char *str, const char stopper)
+{
+    char quote;
+    const char *start = str;
+
+    Boolean need_sep = False;
+    ptrdiff_t len;
+
+    while (*str == '\t' || *str == ' ')
+        ++str;
+
+    if (!*str)
+        return 0;
+
+    while (*str) {
+        if (need_sep) {
+            if (*str == ';' || *str == ',' || *str == '\n') {
+                need_sep = False;
+                ++str;          /* skip keyword separator */
+                while (*str == '\t' || *str == ' ')
+                    ++str;
+            }
+            if (!*str) {
+                if (stopper)    /* we need a close bracket or whatever */
+                    return 0;   /*     but we didn't get one - fail */
+                break;          /* ok, we're done */
+            }
+        }
+
+        /* this allows for empty sub-arrays as values */
+        if (stopper && *str == stopper) {
+            ++str;              /* skip closing bracket or whatever */
+            break;              /* ok, we're done */
+        }
+
+        len = scanAKeyword(str);
+        if (len == 0)
+            return 0;           /* found no keyword */
+        str += len;
+        while (*str == '\t' || *str == ' ')
+            ++str;
+        if (!(*str == ':' || *str == '='))
+            return 0;           /* found no assignment char ':' or '=' */
+        ++str;                  /* skip assignment char ':' or '=' */
+        while (*str == '\t' || *str == ' ')
+            ++str;
+        quote = isAStartBracket(*str);
+        if (quote == '\'' || quote == '\"') {
+            ++str;              /* skip until next matching quote */
+            while (*str != quote) {
+                if (*str == '\\')
+                    ++str;      /* allowing for backspace escapes */
+                if (!*str)
+                    return 0;
+                ++str;
+            }
+            ++str;              /* skip quote */
+        }
+        else if (quote) {       /* we have a backet */
+            ++str;              /* skip until next matching backet */
+            len = isKeywords(str, quote); /* recurse */
+            if (!len)
+                return 0;
+            str += len;
+        }
+        else {
+            while (isgraph(*str) &&
+                   *str != ';' && *str != ',' && *str != stopper) {
+                if (*str == '\\') {
+                    ++str;      /* allowing for backspace escapes */
+                    if (!*str)
+                        return 0;
+                }
+                ++str;          /* scan non-';'/','/space/stopper printables */
+            }
+        }
+        while (*str == '\t' || *str == ' ')
+            ++str;
+        need_sep = True;
+    }
+    return str - start;
+}
+
+/* Run through a string of keyword assignments, loading the assignments into
+** the array *dvArray (which must be properly set up). Returns successfully
+** scanned length or zero on failure. You should call isKeywords() first to
+** avoid trying to parse a non-keywords string.
+*/
+static ptrdiff_t getKeywords(const char *str, DataValue *dvArray, char **errMsg,
+        const char stopper)
+{
+    char quote;
+
+    const char *start = str;
+    Boolean need_sep = False, result = False;
+    ptrdiff_t len;
+    char *buff; /* in which to copy keyword or value */
+    char *key, *val, *end;
+    DataValue dvElement;
+    char *indexString;
+
+    buff = XtMalloc(strlen(str) + 1); /* to copy keyword and value */
+    if (!buff) {
+        *errMsg = "allocation failure in keyword assignment argument: %s";
+        return False;
+    }
+
+    while (*str == '\t' || *str == ' ')
+        ++str;
+
+    if (!*str)
+        return 0;
+
+    while (*str) {
+        if (need_sep) {
+            *errMsg = "expected keyword assignment separator not found: %s";
+            if (*str == ';' || *str == ',' || *str == '\n') {
+                ++str;          /* skip keyword separator */
+                while (*str == '\t' || *str == ' ')
+                    ++str;
+            }
+            if (!*str) {
+                *errMsg = "expected end of keyword subarray: %s";
+                if (stopper)    /* we need a close bracket or whatever */
+                    goto Fail;  /*     but we didn't get one - fail */
+                break;          /* ok, we're done */
+            }
+        }
+
+        /* this allows for empty sub-arrays as values */
+        if (stopper && *str == stopper) {
+            ++str;              /* skip closing bracket or whatever */
+            break;              /* ok, we're done */
+        }
+
+        dvElement.tag = STRING_TAG;
+        *errMsg = "expected keyword assignment keyword not found: %s";
+        len = scanAKeyword(str);
+        if (len == 0)
+            goto Fail;          /* found no keyword */
+
+        /* get the keyword */
+        key = buff;
+        memcpy(key, str, len);
+        end = key + len;
+        *end++ = '\0';
+        str += len;
+
+        while (*str == '\t' || *str == ' ')
+            ++str;
+
+        *errMsg = "expected keyword assignment operator not found: %s";
+        if (!(*str == ':' || *str == '='))
+            goto Fail;          /* found no assignment char ':' or '=' */
+
+        ++str;                  /* skip assignment char ':' or '=' */
+
+        while (*str == '\t' || *str == ' ')
+            ++str;
+
+        /* now for the value */
+        val = end;
+        quote = isAStartBracket(*str);
+        if (quote == '\'' || quote == '\"') {
+            str++;              /* skip until next matching quote */
+            while (*str != quote) {
+                if (*str == '\\')
+                    ++str;      /* allowing for backspace escapes */
+                if (!*str)
+                    goto Fail;
+                *end++ = *str++;
+            }
+            ++str;  /* skip quote */
+            *end = '\0';
+        }
+        else if (quote) {       /* we have a backet: sub-array */
+            ++str;              /* skip until next matching bracket */
+            dvElement.tag = ARRAY_TAG;
+            dvElement.val.arrayPtr = ArrayNew();
+            *errMsg = "could not allocate keyword sub-array: %s";
+            if (!dvElement.val.arrayPtr)
+                goto Fail;
+            len = getKeywords(str, &dvElement, errMsg, quote); /* recurse */
+            if (!len)
+                goto Fail;
+            str += len;
+        }
+        else {
+            while (isgraph(*str) &&
+                   *str != ';' && *str != ',' && *str != stopper) {
+                if (*str == '\\')
+                    ++str;      /* allowing for backspace escapes */
+                *errMsg = "cannot escape end of string in keyword value: %s";
+                if (!*str)
+                    goto Fail;
+                *end++ = *str++; /* copy non-';'/','/space/stopper printables */
+            }
+            *end = '\0';
+        }
+
+        /* OK to assign a key/val pair */
+        indexString = AllocStringCpy(key);
+        if (!indexString) {
+            *errMsg = "failed to allocate keyword argument name: %s";
+            goto Fail;
+        }
+        if (dvElement.tag == STRING_TAG) {
+            int n;
+            if (StringToNum(val, &n)) {
+                /* treat as integer if we can */
+                dvElement.tag = INT_TAG;
+                dvElement.val.n = n;
+            }
+            else if (!AllocNStringCpy(&dvElement.val.str, val)) {
+                *errMsg = "failed to allocate keyword argument value: %s";
+                goto Fail;
+            }
+        }
+        if (!ArrayInsert(dvArray, indexString, &dvElement)) {
+            *errMsg = "failed to add keyword argument assignment to array: %s";
+            goto Fail;
+        }
+        while (*str == '\t' || *str == ' ')
+            ++str;
+        need_sep = True;
+    }
+    result = True;
+    *errMsg = NULL;
+Fail:
+    XtFree(buff);
+    return result ? str - start : 0;
+}
+
+/* Counts the elements in the array with consecutive integer keys starting at
+** start, and copies their values into the args array, which must be big enough.
+** If args is NULL, no copy is made. The maximum integer index value is end,
+** unless end is less than start, in which case it is ignored. Returns the
+** number of such values found.
+*/
+static int arrayElemWithNumKeys(DataValue *dvArray, int start, int end,
+        DataValue *args)
+{
+    char stringStorage[TYPE_INT_STR_SIZE(int)];
+    DataValue dvEntry;
+    int i, n;
+
+    if (args == NULL)
+        args = &dvEntry;
+
+    n = 0;
+    for (i = start; i < end || end < start; ++i) {
+        sprintf(stringStorage, "%d", i);
+        if (!ArrayGet(dvArray, stringStorage, args))
+            break;
+        if (args != &dvEntry)
+            ++args;
+        ++n;
+    }
+    return n;
+}
+
+/* Runs through the elements in the array whose keys look like single words and
+** adds them to the keyword table.
+*/
+static Boolean copyKeywordElems(DataValue *dvArray, DataValue *dvDest,
+        char **errMsg)
+{
+    SparseArrayEntry *iter;
+    Boolean allOK = True;
+
+    for (iter = arrayIterateFirst(dvArray);
+         iter && allOK;
+         iter = arrayIterateNext(iter)) {
+        if (isAKeyword(iter->key)) {
+            allOK = ArrayInsert(dvDest, iter->key, &iter->value);
+        }
+    }
+    if (!allOK)
+        *errMsg = "failed to add keyword element to array: %s";
+    return allOK;
+}
+
+/*
+** Read a list of macro arguments to produce an array of keywords and a list
+** of other arguments. It works like this: the input list can take arrays or
+** scalars. If the list contains at least one empty string, "", this acts as
+** a division between keyword interpretation arguments and others; this first
+** empty string is dropped from the list, and all following arguments are passed
+** without further ado. For arguments before the empty string, interpretation is
+** as follows:
+**  a string of form keyword *[=:] *value *([\n;,] keyword *[=:] *value *)*
+**      each keyword (\w+) is used as an index into a keywords array, and the
+**      following value is assigned to the corresponding entry. The value must
+**      be of the form [^\s;"']* or a quoted form "[^"*]" or '[^']*'. Allowance
+**      is made for \backslash escaping. If the whole string matches this, it is
+**      converted into keywords and the argument otherwise ignored; otherwise
+**      it is not interpreted, and added to the argument list.
+**  a string of another form is added to the argument list.
+**  an integer is added to the argument list
+**  an array
+**      the keys are scanned for index values 1, 2, 3... in sequence. The
+**          corresponding values are added to the argument list in the same
+**          order.
+**      all single word keys are treated as keywords; their values are added to
+**          the keyword table.
+**      all remaining keys (negative or zero valued, or not in sequence from 1,
+**          or not in keyword form) are ignored.
+*/
+static Boolean argListToArgListAndKeys(DataValue *args, int nArgs,
+        DataValue *dvKeywords, DataValue **dvNewList, int *nLen, char **errMsg)
+{
+    /* the input list is scanned twice: once to retrieve all available keywords
+       and to count non-keywords; then again to build the non-keyword list. */
+    int n, i, j;
+    DataValue *dv;
+    DataValue *newArgs, *newdv;
+    DataValue dummy;
+    Boolean allOK = True;
+
+    /* first loop counts ordinary values to put in the new args list */
+    for (n = i = 0; i < nArgs; ++i) {
+        dv = &args[i];
+
+        if (dv->tag == ARRAY_TAG) {
+            n += arrayElemWithNumKeys(dv, 1, 0, NULL);
+        }
+        else if (dv->tag == STRING_TAG) {
+            if (dv->val.str.len == 0) {
+                /* skip this one, take the remaining args as they are */
+                ++i;
+                break;
+            }
+            else if (isKeywords(dv->val.str.rep, '\0'))
+                continue;
+            else
+                ++n;
+        }
+        else {
+            ++n; /* datatype is INT_TAG most probably */
+        }
+    }
+    if (i < nArgs) {
+        /* we saw an empty string argument, stopping keyword interpretation */
+        n += nArgs - i;
+    }
+
+    /* create the new args array */
+    *errMsg = "failed to allocate non-keyword argument list: %s";
+    if (n > 0)
+        newArgs = *dvNewList = (DataValue *)XtMalloc(n * sizeof (DataValue));
+    else {
+        *dvNewList = NULL;
+        newArgs = &dummy;
+    }
+    *nLen = n;
+    allOK = (newArgs != NULL);
+
+    /* second loop populates the new args list and keywords array */
+    for (j = i = 0; i < nArgs && allOK; ++i) {
+        dv = &args[i];
+        newdv = &newArgs[j];
+
+        if (dv->tag == ARRAY_TAG) {
+            j += arrayElemWithNumKeys(dv, 1, 0, newdv);
+            allOK = copyKeywordElems(dv, dvKeywords, errMsg);
+        }
+        else if (dv->tag == STRING_TAG) {
+            if (dv->val.str.len == 0) {
+                /* skip this one, take the remaining args as they are */
+                ++i;
+                break;
+            }
+            else if (isKeywords(dv->val.str.rep, '\0')) {
+                allOK = getKeywords(dv->val.str.rep, dvKeywords, errMsg, '\0');
+            }
+            else {
+                *newdv = *dv;
+                ++j;
+            }
+        }
+        else {
+            *newdv = *dv;
+            ++j; /* datatype is INT_TAG most probably */
+        }
+    }
+    while (i < nArgs && allOK) {
+        /* we saw an empty string argument, stopping keyword interpretation */
+        newArgs[j++] = args[i++];
+    }
+
+    if (!allOK) {
+        XtFree((char *)newArgs);
+        *dvNewList = NULL;
+    }
+    else {
+        *errMsg = NULL;
+    }
+    return allOK;
+}
+
+/*
+** Returns an array of all the keywords and non-keyword arguments in its
+** argument list.
+*/
+static int setKeywordsAndArgsArrayMS(WindowInfo *window, DataValue *argList,
+        int nArgs, DataValue *result, char **errMsg)
+{
+    Boolean allOK = True;
+    char *indexString;
+    int i;
+    DataValue *dv;
+
+    /* initialize array */
+    result->tag = ARRAY_TAG;
+    result->val.arrayPtr = ArrayNew();
+
+    if (!argListToArgListAndKeys(argList, nArgs, result,
+                                 &argList, &nArgs, errMsg))
+        return False;
+
+    /* *result now holds all (and only) keyword-value pairs,
+       all other (non-keyword) arguments in argList[0...nArgs-1] */
+
+    /* now inject the argList arguments into the array too, starting at 1 */
+    for (i = 1, dv = argList; i <= nArgs && allOK; ++i, ++dv) {
+        char numString[TYPE_INT_STR_SIZE(int)];
+        sprintf(numString, "%d", i);
+        indexString = AllocStringCpy(numString);
+        if (!indexString) {
+            *errMsg = "failed to allocate argument index string: %s";
+            allOK = False;
+        }
+        else if (!ArrayInsert(result, indexString, dv)) {
+            *errMsg = "failed to add indexed argument to array: %s";
+            allOK = False;
+        }
+    }
+    /* tidy up */
+    XtFree((char *)argList);
+    return allOK;
+}
