summaryrefslogtreecommitdiff
path: root/support/newcommand
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2019-09-02 13:46:59 +0900
committerNorbert Preining <norbert@preining.info>2019-09-02 13:46:59 +0900
commite0c6872cf40896c7be36b11dcc744620f10adf1d (patch)
tree60335e10d2f4354b0674ec22d7b53f0f8abee672 /support/newcommand
Initial commit
Diffstat (limited to 'support/newcommand')
-rw-r--r--support/newcommand/README58
-rw-r--r--support/newcommand/newcommand.pdfbin0 -> 404178 bytes
-rw-r--r--support/newcommand/newcommand.py711
-rw-r--r--support/newcommand/newcommand.tex742
-rw-r--r--support/newcommand/spark.py566
5 files changed, 2077 insertions, 0 deletions
diff --git a/support/newcommand/README b/support/newcommand/README
new file mode 100644
index 0000000000..09bc10e65f
--- /dev/null
+++ b/support/newcommand/README
@@ -0,0 +1,58 @@
+ +------------------------------------+
+ | NEWCOMMAND.PY |
+ | |
+ | More flexible argument processing |
+ | than what \newcommand provides |
+ | |
+ | By Scott Pakin, scott+nc@pakin.org |
+ +------------------------------------+
+
+
+Description
+-----------
+
+LaTeX's \newcommand is fairly limited in the way it processes optional
+arguments, but the TeX alternative, a batch of \defs and \futurelets,
+can be overwhelming to the casual LaTeX user. newcommand.py is a
+Python program that automatically generates LaTeX macro definitions
+for macros that require more powerful argument processing than
+\newcommand can handle. newcommand.py is intended for LaTeX advanced
+beginners (i.e., those who know how to use \newcommand but not
+internal LaTeX2e commands such as \@ifnextchar) and for more advanced
+users who want to save some typing when defining complex macros.
+
+With newcommand.py, the user specifies a template for a macro's
+arguments. newcommand.py then custom-generates a macro definition
+according to the user's specifications and includes a user-friendly
+"Put code here" comment to indicate where the macro's main code should
+appear. newcommand.py supports arbitrary interleavings of required
+and optional arguments, starred macros, mandatory literal text, macros
+with more than nine arguments, optional arguments delimited by
+parentheses instead of square brackets, and optional arguments whose
+value defaults to the value given for a prior argument. The generated
+macros can easily be pasted into a LaTeX document and edited as
+desired.
+
+
+Installation
+------------
+
+You'll need a Python interpreter (http://www.python.org/). Besides
+that, just make sure that newcommand.py is placed somewhere in your
+path and that spark.py is somewhere that newcommand.py can find it.
+
+
+Copyright and license
+---------------------
+
+Copyright (C) 2010 Scott Pakin, scott+nc@pakin.org
+
+This package may be distributed and/or modified under the conditions
+of the LaTeX Project Public License, either version 1.3c of this
+license or (at your option) any later version. The latest version of
+this license is in:
+
+ http://www.latex-project.org/lppl.txt
+
+and version 1.3c or later is part of all distributions of LaTeX version
+2006/05/20 or later.
diff --git a/support/newcommand/newcommand.pdf b/support/newcommand/newcommand.pdf
new file mode 100644
index 0000000000..3a908ba4da
--- /dev/null
+++ b/support/newcommand/newcommand.pdf
Binary files differ
diff --git a/support/newcommand/newcommand.py b/support/newcommand/newcommand.py
new file mode 100644
index 0000000000..e14995926c
--- /dev/null
+++ b/support/newcommand/newcommand.py
@@ -0,0 +1,711 @@
+#! /usr/bin/env python
+
+# -----------------------------------------------------------------------
+# Convert a macro prototype to a LaTeX \newcommand
+# By Scott Pakin <scott+nc@pakin.org>
+# -----------------------------------------------------------------------
+# Copyright (C) 2010 Scott Pakin, scott+nc@pakin.org
+#
+# This package may be distributed and/or modified under the conditions
+# of the LaTeX Project Public License, either version 1.3c of this
+# license or (at your option) any later version. The latest version of
+# this license is in:
+#
+# http://www.latex-project.org/lppl.txt
+#
+# and version 1.3c or later is part of all distributions of LaTeX version
+# 2006/05/20 or later.
+# -----------------------------------------------------------------------
+
+from spark import GenericScanner, GenericParser, GenericASTTraversal
+import re
+import copy
+
+class ParseError(Exception):
+ "Represent any error that occurs during processing."
+ pass
+
+
+class Token:
+ "Represent a single lexed token."
+
+ def __init__(self, type, charOffset, attr=None):
+ self.type = type
+ self.attr = attr
+ self.charOffset = charOffset
+
+ def __cmp__(self, o):
+ return cmp(self.type, o)
+
+ def __str__(self):
+ return self.attr
+
+
+class AST:
+ "Represent an abstract syntax tree."
+
+ def __init__(self, type, charOffset, attr=None, kids=[]):
+ self.type = type
+ self.charOffset = charOffset
+ self.attr = attr
+ self.kids = kids
+
+ def __getitem__(self, child):
+ return self.kids[child]
+
+ def __len__(self):
+ return len(self.kids)
+
+
+class CmdScanner(GenericScanner):
+ "Defines a lexer for macro prototypes."
+
+ def __init__(self):
+ GenericScanner.__init__(self)
+ self.charOffset = 0
+
+ def tokenize(self, input):
+ self.rv = []
+ GenericScanner.tokenize(self, input)
+ return self.rv
+
+ def t_whitespace(self, whiteSpace):
+ r' [\s\r\n]+ '
+ self.charOffset = self.charOffset + len(whiteSpace)
+
+ def t_command(self, cmd):
+ r' MACRO '
+ self.rv.append(Token(type='command',
+ attr=cmd,
+ charOffset=self.charOffset))
+ self.charOffset = self.charOffset + len(cmd)
+
+ def t_argument_type(self, arg):
+ r' OPT '
+ self.rv.append(Token(type='argtype',
+ attr=arg,
+ charOffset=self.charOffset))
+ self.charOffset = self.charOffset + len(arg)
+
+ def t_argument(self, arg):
+ r' \#\d+ '
+ self.rv.append(Token(type='argument',
+ attr=arg,
+ charOffset=self.charOffset))
+ self.charOffset = self.charOffset + len(arg)
+
+ def t_equal(self, equal):
+ r' = '
+ self.rv.append(Token(type=equal,
+ attr=equal,
+ charOffset=self.charOffset))
+ self.charOffset = self.charOffset + len(equal)
+
+ def t_quoted(self, quoted):
+ r' \{[^\}]*\} '
+ self.rv.append(Token(type='quoted',
+ attr=quoted,
+ charOffset=self.charOffset))
+ self.charOffset = self.charOffset + len(quoted)
+
+ def t_identifier(self, ident):
+ r' [A-Za-z]+ '
+ self.rv.append(Token(type='ident',
+ attr=ident,
+ charOffset=self.charOffset))
+ self.charOffset = self.charOffset + len(ident)
+
+ def t_delimiter(self, delim):
+ r' [()\[\]] '
+ self.rv.append(Token(type='delim',
+ attr=delim,
+ charOffset=self.charOffset))
+ self.charOffset = self.charOffset + len(delim)
+
+ def t_other(self, other):
+ r' [^()\[\]\{\}\#\s\r\n]+ '
+ self.rv.append(Token(type='other',
+ attr=other,
+ charOffset=self.charOffset))
+ self.charOffset = self.charOffset + len(other)
+
+
+class CmdParser(GenericParser):
+ "Defines a parser for macro prototypes."
+
+ def __init__(self, start='decl'):
+ GenericParser.__init__(self, start)
+
+ def error(self, token):
+ raise ParseError, \
+ ("Syntax error", 1+token.charOffset)
+
+ def p_optarg(self, args):
+ ' optarg ::= argtype delim defvals delim '
+ return AST(type='optarg',
+ charOffset=args[0].charOffset,
+ attr=(args[1].attr, args[3].attr),
+ kids=[args[2]])
+
+ def p_rawtext(self, args):
+ ' rawtext ::= other '
+ return AST(type='rawtext',
+ charOffset=args[0].charOffset,
+ attr=args[0].attr)
+
+ def p_defval(self, args):
+ ' defval ::= argument = quoted '
+ return AST(type='defval',
+ charOffset=args[0].charOffset,
+ attr=(args[0].attr, args[2].attr))
+
+ def p_defvals_1(self, args):
+ ' defvals ::= defval '
+ return AST(type='defvals',
+ charOffset=args[0].charOffset,
+ kids=args)
+
+ def p_defvals_2(self, args):
+ '''
+ defvals ::= defval rawtext defvals
+ defvals ::= defval ident defvals
+ defvals ::= defval quoted defvals
+ '''
+ return AST(type='defvals',
+ charOffset=args[0].charOffset,
+ attr=(args[1].type, args[1].attr, args[1].charOffset),
+ kids=[args[0],args[2]])
+
+ # Top-level macro argument
+ def p_arg_1(self, args):
+ '''
+ arg ::= quoted
+ arg ::= argument
+ '''
+ return AST(type='arg',
+ charOffset=args[0].charOffset,
+ attr=[args[0].type]+[args[0].attr])
+
+ def p_arg_2(self, args):
+ ' arg ::= optarg '
+ return AST(type='arg',
+ charOffset=args[0].charOffset,
+ attr=[args[0].type]+[args[0].attr],
+ kids=args[0].kids)
+
+ def p_arg_3(self, args):
+ ' arg ::= rawtext '
+ if args[0].attr != "*":
+ raise ParseError, \
+ ('Literal text must be quoted between "{" and "}"',
+ args[0].charOffset + 1)
+ return AST(type='arg',
+ charOffset=args[0].charOffset,
+ attr=[args[0].type]+[args[0].attr])
+
+ def p_arglist_1(self, args):
+ ' arglist ::= arg '
+ return AST(type='arglist',
+ charOffset=args[0].charOffset,
+ kids=args)
+
+ def p_arglist_2(self, args):
+ ' arglist ::= arg arglist '
+ return AST(type='arglist',
+ charOffset=args[0].charOffset,
+ kids=args)
+
+ def p_decl_1(self, args):
+ ' decl ::= command ident '
+ return AST(type='decl',
+ charOffset=args[0].charOffset,
+ attr=(args[0].attr, args[1].attr),
+ kids=[])
+
+ def p_decl_2(self, args):
+ ' decl ::= command ident arglist '
+ return AST(type='decl',
+ charOffset=args[0].charOffset,
+ attr=(args[0].attr, args[1].attr),
+ kids=[args[2]])
+
+
+def flattenAST(ast):
+ class FlattenAST(GenericASTTraversal):
+ "Flatten an AST into a list of arguments."
+
+ def __init__(self, ast):
+ GenericASTTraversal.__init__(self, ast)
+ self.postorder()
+ self.argList = ast.argList
+
+ def n_defval(self, node):
+ node.argList = (node.attr[0], node.attr[1], node.charOffset)
+
+ def n_defvals(self, node):
+ node.argList = [node.kids[0].argList]
+ if node.attr:
+ node.argList = node.argList + [node.attr] + node.kids[1].argList
+
+ def n_arg(self, node):
+ if node.attr[0] == "optarg":
+ node.argList = node.attr + node.kids[0].argList
+ else:
+ node.argList = tuple(node.attr + [node.charOffset])
+
+ def n_arglist(self, node):
+ node.argList = [node.kids[0].argList]
+ if len(node.kids) == 2:
+ node.argList = node.argList + node.kids[1].argList
+
+ def n_decl(self, node):
+ node.argList = [(node.attr[0], node.attr[1], node.charOffset)]
+ if node.kids != []:
+ node.argList = node.argList + node.kids[0].argList
+
+ def default(self, node):
+ raise ParseError, \
+ ('Internal error -- node type "%s" was unexpected' % node.type,
+ 1+node.charOffset)
+
+ return FlattenAST(ast).argList
+
+
+def checkArgList(argList):
+ "Raise an error if any problems are detected with the given argument list."
+
+ def getFormals(sublist):
+ "Return the formal-parameter numbers in the order in which they appear."
+ if sublist == []:
+ return []
+ head = sublist[0]
+ headval = []
+ if head[0] == "argument":
+ headval = [(int(head[1][1:]), head[2])]
+ elif head[0][0] == "#":
+ headval = [(int(head[0][1:]), head[2])]
+ elif head[0] == "optarg":
+ headval = getFormals(head[2:])
+ return headval + getFormals(sublist[1:])
+
+ # Ensure the formals appear in strict increasing order.
+ formals = getFormals(argList)
+ prevformal = 0
+ for form, pos in formals:
+ if form != prevformal + 1:
+ raise ParseError, \
+ ("Expected parameter %d but saw parameter %d" % (prevformal+1, form), 1+pos)
+ prevformal = form
+
+ # Ensure that "*" appears at most once at the top level.
+ seenStar = False
+ for arg in argList:
+ if arg[0] == "rawtext" and arg[1] == "*":
+ if seenStar:
+ raise ParseError, \
+ ("Only one star parameter is allowed", arg[2])
+ seenStar = True
+
+ # Ensure that no optional argument contains more than nine formals.
+ for arg in argList:
+ if arg[0] == "optarg":
+ optFormals = 0
+ for oarg in arg[2:]:
+ if oarg[0][0] == "#":
+ optFormals += 1
+ if optFormals > 9:
+ raise ParseError, \
+ ("An optional argument can contain at most nine formals",
+ oarg[2])
+
+ # Ensure that "#" is used only where it's allowed.
+ for arg in argList:
+ if arg[0] in ["rawtext", "quoted"]:
+ hashidx = string.find(arg[1], "#")
+ if hashidx == 0 or (hashidx > 0 and arg[1][hashidx-1] != "\\"):
+ if arg[0] == "quoted":
+ hashidx += 1
+ raise ParseError, \
+ ('The "#" character cannot be used as a literal character unless escaped with "\\"',
+ arg[2] + hashidx)
+ elif arg[0] == "optarg":
+ for oarg in arg[2:]:
+ if oarg[0] in ["rawtext", "quoted"]:
+ hashidx = string.find(oarg[1], "#")
+ if hashidx == 0 or (hashidx > 0 and oarg[1][hashidx-1] != "\\"):
+ if oarg[0] == "quoted":
+ hashidx += 1
+ raise ParseError, \
+ ('The "#" character cannot be used as a literal character unless escaped with "\\"',
+ oarg[2] + hashidx)
+
+
+class LaTeXgenerator():
+ "Generate LaTeX code from a list of arguments."
+
+ def __init__(self):
+ "Initialize all of LaTeXgenerator's instance variables."
+ self.argList = [] # List of arguments provided to generate()
+ self.topLevelName = "???" # Base macro name
+ self.haveStar = False # True=need to define \ifNAME@star
+ self.haveAt = False # True=need to use \makeatletter...\makeatother
+ self.numFormals = 0 # Total number of formal arguments
+ self.codeList = [] # List of lines of code to output
+
+ def toRoman(self, num):
+ "Convert a decimal number to roman."
+ dec2rom = [("m", 1000),
+ ("cm", 900),
+ ("d", 500),
+ ("cd", 400),
+ ("c", 100),
+ ("xc", 90),
+ ("l", 50),
+ ("xl", 40),
+ ("x", 10),
+ ("ix", 9),
+ ("v", 5),
+ ("iv", 4),
+ ("i", 1)]
+ romanStr = ""
+ if num > 4000:
+ raise ParseError, ("Too many arguments", 0)
+ for rom, dec in dec2rom:
+ while num >= dec:
+ romanStr += rom
+ num -= dec
+ return romanStr
+
+ def partitionArgList(self):
+ "Group arguments, one per macro to generate."
+ self.argGroups = []
+ argIdx = 1
+
+ # Specially handle the first group because it's limited by
+ # \newcomand's semantics.
+ group = []
+ if len(self.argList) == 1:
+ # No arguments whatsoever
+ self.argGroups.append(group)
+ return
+ arg = self.argList[argIdx]
+ if arg[0] == "optarg" and arg[1] == ("[", "]") and len(arg) == 3:
+ group.append(arg)
+ argIdx += 1
+ while len(group) < 9 and argIdx < len(self.argList) and self.argList[argIdx][0] == "argument":
+ group.append(self.argList[argIdx])
+ argIdx += 1
+ self.argGroups.append(group)
+
+ # Handle the remaining groups, each ending before an optional
+ # argument.
+ group = []
+ numFormals = 0
+ for arg in self.argList[argIdx:]:
+ if arg[0] == "rawtext":
+ # Treat "*" as an optional argument.
+ if arg[1] == "*":
+ if group != []:
+ self.argGroups.append(group)
+ group = []
+ numFormals = 0
+ group.append(arg)
+ elif arg[0] == "quoted":
+ group.append(arg)
+ elif arg[0] == "argument":
+ group.append(arg)
+ numFormals += 1
+ if numFormals == 9:
+ if group != []:
+ self.argGroups.append(group)
+ group = []
+ numFormals = 0
+ elif arg[0] == "optarg":
+ # Note that we know from checkArgList() that there are
+ # no more than 10 formals specified within the
+ # optional argument.
+ if group != []:
+ self.argGroups.append(group)
+ group = []
+ numFormals = 0
+ optarg = arg[0:2]
+ for oarg in arg[2:]:
+ if oarg[0] in ["rawtext", "quoted"]:
+ optarg.append(oarg)
+ elif oarg[0][0] == "#":
+ numFormals += 1
+ optarg.append(oarg)
+ else:
+ optarg.append(oarg)
+ group.append(optarg)
+ if group != []:
+ self.argGroups.append(group)
+
+ def argsToString(self, argList, mode, argSubtract=0):
+ '''
+ Produce a string version of a list of arguments.
+ mode is one of "define", "call", or "calldefault".
+ argSubtract is subtracted from each argument number.
+ '''
+ if mode not in ["define", "call", "calldefault"]:
+ raise ParseError, ('Internal error (mode="%s")' % mode, argList[0][2])
+ argStr = ""
+ findArgRE = re.compile('#(\d+)')
+ for arg in argList:
+ if arg[0] == "argument":
+ if mode == "define":
+ argStr += "#%d" % (int(arg[1][1:]) - argSubtract)
+ else:
+ argStr += "{#%d}" % (int(arg[1][1:]) - argSubtract)
+ elif arg[0] == "rawtext":
+ argStr += arg[1]
+ elif arg[0] == "quoted":
+ argStr += arg[1][1:-1]
+ elif arg[0] == "optarg":
+ argStr += arg[1][0]
+ for oarg in arg[2:]:
+ if oarg[0][0] == "#":
+ if mode == "define":
+ argStr += "#%d" % (int(oarg[0][1:]) - argSubtract)
+ elif mode == "call":
+ argStr += "{#%d}" % (int(oarg[0][1:]) - argSubtract)
+ else:
+ if self.numFormals > 9:
+ argStr += findArgRE.sub(lambda a: "\\"+self.topLevelName+"@arg@"+self.toRoman(int(a.group(0)[1:])),
+ oarg[1])
+ else:
+ argStr += oarg[1]
+ elif oarg[0] == "quoted":
+ argStr += oarg[1][1:-1]
+ elif oarg[0] == "rawtext":
+ argStr += oarg[1]
+ else:
+ raise ParseError, ('Internal error ("%s")' % oarg[0],
+ oarg[2])
+ argStr += arg[1][1]
+ else:
+ raise ParseError, ('Internal error ("%s")' % arg[0], arg[2])
+ return argStr
+
+ def callMacro(self, macroNum):
+ "Return an array of strings suitable for calling macro macroNum."
+ if macroNum >= len(self.argGroups):
+ # No more macros.
+ return []
+ macroName = "\\%s@%s" % (self.topLevelName, self.toRoman(macroNum))
+ nextArg = self.argGroups[macroNum][0]
+ callSeq = []
+ if self.numFormals > 9:
+ # More than nine formal parameters
+ if nextArg[0] == "optarg":
+ callSeq.append(" \\@ifnextchar%s{%s}{%s%s}%%" % \
+ (nextArg[1][0], macroName, macroName,
+ self.argsToString([nextArg], mode="calldefault")))
+ elif nextArg[0] == "rawtext" and nextArg[1] == "*":
+ callSeq.append(" \\@ifstar{\\%s@startrue%s*}{\\%s@starfalse%s*}%%" % \
+ (self.topLevelName, macroName,
+ self.topLevelName, macroName))
+ else:
+ callSeq.append(" %s" % macroName)
+ else:
+ # Nine or fewer formal parameters
+ argStr = ""
+ for g in range(0, macroNum):
+ argStr += self.argsToString(self.argGroups[g], mode="call")
+ if nextArg[0] == "optarg":
+ callSeq.append(" \\@ifnextchar%s{%s%s}{%s%s%s}%%" % \
+ (nextArg[1][0],
+ macroName, argStr, macroName, argStr,
+ self.argsToString([nextArg], mode="calldefault")))
+ elif nextArg[0] == "rawtext" and nextArg[1] == "*":
+ callSeq.append(" \\@ifstar{\\%s@startrue%s%s*}{\\%s@starfalse%s%s*}%%" % \
+ (self.topLevelName, macroName, argStr,
+ self.topLevelName, macroName, argStr))
+ else:
+ callSeq.append(" %s%s%%" % (macroName, argStr))
+ return callSeq
+
+ def putCodeHere(self):
+ 'Return an array of strings representing "Put code here".'
+ code = []
+ if self.haveStar:
+ code.extend([" \\if%s@star" % self.topLevelName,
+ ' % Put code for the "*" case here.',
+ " \\else",
+ ' % Put code for the non-"*" case here.',
+ " \\fi",
+ " %% Put code common to both cases here (and/or above the \\if%s@star)." % self.topLevelName])
+ else:
+ code.append(" % Put your code here.")
+ if self.numFormals == 0:
+ return code
+ if self.numFormals > 9:
+ firstArgName = "\\%s@arg@i" % self.topLevelName
+ lastArgName = "\\%s@arg@%s" % (self.topLevelName, self.toRoman(self.numFormals))
+ else:
+ firstArgName = "#1"
+ lastArgName = "#%d" % self.numFormals
+ if self.numFormals == 1:
+ code.append(" %% You can refer to the argument as %s." % firstArgName)
+ elif self.numFormals == 2:
+ code.append(" %% You can refer to the arguments as %s and %s." % (firstArgName, lastArgName))
+ else:
+ code.append(" %% You can refer to the arguments as %s through %s." % (firstArgName, lastArgName))
+ return code
+
+ def produceTopLevel(self):
+ "Generate the code for the top-level macro definition."
+ # Generate the macro definition.
+ defStr = "\\newcommand{\\%s}" % self.topLevelName
+ argList = self.argGroups[0]
+ if argList != []:
+ defStr += "[%d]" % len(argList)
+ firstArg = argList[0]
+ if firstArg[0] == "optarg":
+ defVal = firstArg[2][1][1:-1]
+ if string.find(defVal, "]") != -1:
+ defVal = "{%s}" % defVal
+ defStr += "[%s]" % defVal
+ defStr += "{%"
+ self.codeList.append(defStr)
+
+ # Generate the macro body.
+ if len(self.argGroups) == 1:
+ # Single macro definition.
+ self.codeList.extend(self.putCodeHere())
+ else:
+ # More macros are forthcoming.
+ if self.numFormals > 9:
+ # More than nine formal parameters
+ for f in range(1, len(argList)+1):
+ self.codeList.append(" \\def\\%s@arg@%s{#%d}%%" % (self.topLevelName, self.toRoman(f), f))
+ self.codeList.extend(self.callMacro(1))
+ self.codeList.append("}")
+
+ def produceRemainingMacros(self):
+ "Generate code for all macros except the first."
+ formalsSoFar = len(self.argGroups[0])
+ for groupNum in range(1, len(self.argGroups)):
+ # Generate the macro header.
+ self.codeList.append("")
+ argList = self.argGroups[groupNum]
+ defStr = "\\def\\%s@%s" % (self.topLevelName, self.toRoman(groupNum))
+ if self.numFormals > 9:
+ defStr += self.argsToString(argList, mode="define", argSubtract=formalsSoFar)
+ else:
+ for g in range (0, groupNum+1):
+ defStr += self.argsToString(self.argGroups[g], mode="define")
+ self.codeList.append(defStr + "{%")
+
+ # Generate the macro body.
+ if self.numFormals > 9:
+ # More than nine formal parameters
+ for arg in argList:
+ if arg[0] == "argument":
+ formalNum = int(arg[1][1:])
+ self.codeList.append(" \\def\\%s@arg@%s{#%d}%%" % \
+ (self.topLevelName,
+ self.toRoman(formalNum),
+ formalNum - formalsSoFar))
+ elif arg[0] == "optarg":
+ for oarg in arg[2:]:
+ if oarg[0][0] == "#":
+ formalNum = int(oarg[0][1:])
+ self.codeList.append(" \\def\\%s@arg@%s{#%d}%%" % \
+ (self.topLevelName,
+ self.toRoman(formalNum),
+ formalNum - formalsSoFar))
+ if groupNum == len(self.argGroups) - 1:
+ self.codeList.extend(self.putCodeHere())
+ else:
+ self.codeList.extend(self.callMacro(groupNum + 1))
+ else:
+ # Nine or fewer formal parameters.
+ if groupNum == len(self.argGroups) - 1:
+ self.codeList.extend(self.putCodeHere())
+ else:
+ self.codeList.extend(self.callMacro(groupNum + 1))
+
+ # Generate the macro trailer.
+ self.codeList.append("}")
+
+ # Increment the count of formals seen so far.
+ for arg in argList:
+ if arg[0] == "argument":
+ formalsSoFar += 1
+ elif arg[0] == "optarg":
+ formalsSoFar += len(filter(lambda o: o[0][0] == "#", arg[2:]))
+
+ def generate(self, argList):
+ "Generate LaTeX code from an argument list."
+ # Group arguments and identify characteristics that affect the output.
+ self.argList = argList
+ self.partitionArgList()
+ self.haveAt = len(self.argGroups) > 1
+ self.haveStar = filter(lambda arg: arg[0]=="rawtext" and arg[1]=="*", self.argList) != []
+ self.topLevelName = self.argList[0][1]
+ for arg in self.argList:
+ if arg[0] == "argument":
+ self.numFormals += 1
+ elif arg[0] == "optarg":
+ for oarg in arg[2:]:
+ if oarg[0][0] == "#":
+ self.numFormals += 1
+
+ # Output LaTeX code.
+ if self.haveAt:
+ self.codeList.append("\\makeatletter")
+ if self.haveStar:
+ self.codeList.append("\\newif\\if%s@star" % self.topLevelName)
+ self.produceTopLevel()
+ self.produceRemainingMacros()
+ if self.haveAt:
+ self.codeList.append("\\makeatother")
+ for codeLine in self.codeList:
+ print codeLine
+
+
+# The buck starts here.
+if __name__ == '__main__':
+ import sys
+ import string
+
+ def processLine():
+ "Parses the current value of oneLine."
+ global oneLine
+ try:
+ sys.stdout.softspace = 0 # Cancel the %$#@! space.
+ oneLine = string.strip(oneLine)
+ if oneLine=="" or oneLine[0]=="%":
+ return
+ if not isStdin:
+ print prompt, oneLine
+ scanner = CmdScanner()
+ parser = CmdParser()
+ tokens = scanner.tokenize(oneLine)
+ ast = parser.parse(tokens)
+ argList = flattenAST(ast)
+ checkArgList(argList)
+ gen = LaTeXgenerator()
+ gen.generate(argList)
+ except ParseError,(message, pos):
+ sys.stderr.write((" "*(len(prompt)+pos)) + "^\n")
+ sys.stderr.write("%s: %s.\n" % (sys.argv[0], message))
+ if isStdin:
+ print ""
+
+ sys.setrecursionlimit(5000)
+ prompt = "% Prototype:"
+ if len(sys.argv) <= 1:
+ isStdin = 1
+ print prompt + " ",
+ while 1:
+ oneLine = sys.stdin.readline()
+ if not oneLine:
+ break
+ processLine()
+ print prompt + " ",
+ else:
+ isStdin = 0
+ oneLine = string.join(sys.argv[1:])
+ processLine()
diff --git a/support/newcommand/newcommand.tex b/support/newcommand/newcommand.tex
new file mode 100644
index 0000000000..4f4265c237
--- /dev/null
+++ b/support/newcommand/newcommand.tex
@@ -0,0 +1,742 @@
+\documentclass{ltxdoc}
+\usepackage[T1]{fontenc}
+\usepackage{tabularx}
+\usepackage{syntax}
+\usepackage{varioref}
+\usepackage{color}
+\usepackage{booktabs}
+\usepackage{alltt}
+\usepackage{textcomp}
+\usepackage[bookmarksopen=true]{hyperref}
+
+% Define this document's metadata.
+\def\ncfileversion{2.0}
+\def\ncfiledate{2010/06/01}
+
+\title{The \textsf{newcommand.py} utility\thanks{\textsf{newcommand.py}
+ has version number \ncfileversion, last revised \ncfiledate.}}
+\author{\href{mailto:scott+nc@pakin.org}{Scott Pakin} \\
+ \href{mailto:scott+nc@pakin.org}{scott+nc@pakin.org}}
+\date{\ncfiledate}
+\hypersetup{%
+ pdftitle={The newcommand.py utility},
+ pdfauthor={Scott Pakin <scott+nc@pakin.org>},
+ pdfsubject={Creating user-defined macros with more flexible argument
+ processing},
+ pdfkeywords={LaTeX macros, optional arguments, newcommand, parenthesized
+ arguments, starred commands, Python}
+}
+
+% Help prevent weird line breaks in URLs
+\def\UrlBreaks{}
+\def\UrlBigBreaks{\do/}
+
+% Define some useful shortcuts.
+\newcommand*{\ncpy}{\texttt{newcommand.py}} % The name of the script
+\newcommand*{\usercmd}[1]{\textcolor{blue}{#1}} % User command entry
+\newcommand{\prototype}[1]{{% % Prompt and user entry
+ \bigskip
+ \noindent
+ \ttfamily\small\raggedright
+ \hangafter=1\hangindent=4em
+ ~~~~\% Prototype:~\textcolor{blue}{#1}\strut\par
+ \vspace*{-0.5\baselineskip}%
+}}
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+\begin{document}
+\maketitle
+\sloppy
+
+\begin{abstract}
+ \LaTeX's \cs{newcommand} is fairly limited in the way it processes
+ optional arguments, but the \TeX\ alternative, a batch of \cs{def}s
+ and \cs{futurelet}s, can be overwhelming to the casual \LaTeX\ user.
+ \ncpy\ is a Python program that automatically generates
+ \LaTeX\ macro definitions for macros that require more powerful
+ argument processing than \cs{newcommand} can handle. \ncpy\ is
+ intended for \LaTeX\ advanced beginners (i.e., those who know how to
+ use \cs{newcommand} but not internal \LaTeXe\ macros like
+ |\@ifnextchar|) and for more advanced users who want to save some
+ typing when defining complex macros.
+\end{abstract}
+
+\section{Introduction}
+
+\LaTeX's \cs{newcommand} is a rather limited way to define new macros.
+Only one argument can be designated as optional, it must be the first
+argument, and it must appear within square brackets. Defining macros
+that take multiple optional arguments or in which an optional argument
+appears in the middle of the argument list is possible but well beyond
+the capabilities of the casual \LaTeX\ user. It requires using
+\TeX\ primitives such as \cs{def} and \cs{futurelet} and/or
+\LaTeXe\ internal macros such as \cs{@ifnextchar}.
+
+\ncpy\ is a Python program that reads a specification of an argument
+list and automatically produces \LaTeX\ code that processes the
+arguments appropriately. \ncpy\ makes it easy to define
+\LaTeX\ macros with more complex parameter parsing than is possible
+with \cs{newcommand} alone. Note that you do need to have Python
+installed on your system to run \ncpy. Python is freely available for
+download from \url{http://www.python.org/}.
+
+To define a \LaTeX\ macro, one gives \ncpy\ a macro description
+written in a simple specification language. The description
+essentially lists the required and optional arguments and, for each
+optional argument, the default value. The next section of this
+document describes the syntax and provides some examples, but for now,
+let's look at how one would define the most trivial macro possible,
+one that takes no arguments. Enter the following at your operating
+system's prompt:
+
+\begin{alltt}
+ \usercmd{newcommand.py "MACRO trivial"}
+\end{alltt}
+
+\noindent
+(Depending on your system, you may need to prefix that command with
+``|python|''.) The program should output the following \LaTeX\ code
+in response:
+
+\begin{verbatim}
+ % Prototype: MACRO trivial
+ \newcommand{\trivial}{%
+ % Put your code here.
+ }
+\end{verbatim}
+
+\noindent
+Alternatively, you can run \ncpy\ interactively, entering macro
+descriptions at the ``|% Prototype:|'' prompt:
+
+\prototype{MACRO trivial}
+\begin{verbatim}
+ \newcommand{\trivial}{%
+ % Put your code here.
+ }
+ % Prototype:
+\end{verbatim}
+
+\noindent
+Enter your operating system's end-of-file character (Ctrl-D in Unix or
+Ctrl-Z in Windows) to exit the program.
+
+While you certainly don't need \ncpy\ to write macros that are as
+trivial as \cs{trivial}, the previous discussion shows how to run the
+program and the sort of output that you should expect. There will
+always be a ``\texttt{Put your code here}'' comment indicating where
+you should fill in the actual macro code. At that location, all of
+the macro's parameters---both optional and required---will be defined
+and can be referred to in the ordinary way: |#1|, |#2|, |#3|, etc.
+
+
+\section{Usage}
+
+As we saw in the previous section, macros are defined by the word
+``|MACRO|'' followed by the macro name, with no preceding backslash.
+In this section we examine how to specify increasingly sophisticated
+argument processing using \ncpy.
+
+
+\subsection{Required arguments}
+
+Required arguments are entered as~|#1|, |#2|, |#3|, \dots, with no
+surrounding braces:
+
+\prototype{MACRO required \#1 \#2 \#3 \#4 \#5}
+\begin{verbatim}
+ \newcommand{\required}[5]{%
+ % Put your code here.
+ % You can refer to the arguments as #1 through #5.
+ }
+\end{verbatim}
+
+Parameters must be numbered in monotonically increasing order,
+starting with~|#1|. Incorrectly ordered parameters will produce an
+error message:
+
+\prototype{MACRO required \#1 \#3 \#4}
+\begin{verbatim}
+ ^
+ newcommand.py: Expected parameter 2 but saw parameter 3.
+\end{verbatim}
+
+
+\subsection{Optional arguments}
+
+Optional arguments\label{par:optional-args} are written as either
+``|OPT[|\meta{param}|=|\linebreak[1]|{|\meta{default}|}]|'' or
+``|OPT(|\meta{param}|=|\linebreak[1]|{|\meta{default}|})|''. In the
+former case, square brackets are used to offset the optional argument;
+in the latter case, parentheses are used. \meta{param} is the
+parameter number (|#1|, |#2|, |#3|, \dots), and \meta{default} is the
+default value for that parameter. Note that curly braces are required
+around \meta{default}.
+
+\prototype{MACRO optional OPT[\#1=\{maybe\}]}
+\begin{verbatim}
+ \newcommand{\optional}[1][maybe]{%
+ % Put your code here.
+ % You can refer to the argument as #1.
+ }
+\end{verbatim}
+
+Up to this point, the examples have been so simple that \ncpy\ is
+overkill for entering them. We can now begin specifying constructs
+that \LaTeX's \cs{newcommand} can't handle, such as a parenthesized
+optional argument, an optional argument that doesn't appear at the
+beginning of the argument list, and multiple optional arguments:
+
+\prototype{MACRO parenthesized OPT(\#1=\{abc\})}
+\begin{verbatim}
+ \makeatletter
+ \newcommand{\parenthesized}{%
+ \@ifnextchar({\parenthesized@i}{\parenthesized@i({abc})}%
+ }
+
+ \def\parenthesized@i(#1){%
+ % Put your code here.
+ % You can refer to the argument as #1.
+ }
+ \makeatother
+\end{verbatim}
+
+
+\prototype{MACRO nonbeginning \#1 OPT[\#2=\{abc\}]}
+\begin{verbatim}
+ \makeatletter
+ \newcommand{\nonbeginning}[1]{%
+ \@ifnextchar[{\nonbeginning@i{#1}}{\nonbeginning@i{#1}[{abc}]}%
+ }
+
+ \def\nonbeginning@i#1[#2]{%
+ % Put your code here.
+ % You can refer to the arguments as #1 and #2.
+ }
+ \makeatother
+\end{verbatim}
+
+\prototype{MACRO multiple OPT[\#1=\{abc\}] OPT[\#2=\{def\}]}
+\begin{verbatim}
+ \makeatletter
+ \newcommand{\multiple}[1][abc]{%
+ \@ifnextchar[{\multiple@i[{#1}]}{\multiple@i[{#1}][{def}]}%
+ }
+
+ \def\multiple@i[#1][#2]{%
+ % Put your code here.
+ % You can refer to the arguments as #1 and #2.
+ }
+ \makeatother
+\end{verbatim}
+
+The template for optional arguments that was
+shown~\vpageref[above]{par:optional-args} stated that optional
+arguments contain a ``\meta{param}|={|\meta{default}|}|''
+specification. In fact, optional arguments can contain
+\emph{multiple} ``\meta{param}|={|\meta{default}|}|'' specifications,
+as long as they are separated by literal text:
+
+\prototype{MACRO multiopt OPT(\#1=\{0\},\#2=\{0\})}
+\begin{verbatim}
+ \makeatletter
+ \newcommand{\multiopt}{%
+ \@ifnextchar({\multiopt@i}{\multiopt@i({0},{0})}%
+ }
+
+ \def\multiopt@i(#1,#2){%
+ % Put your code here.
+ % You can refer to the arguments as #1 and #2.
+ }
+ \makeatother
+\end{verbatim}
+
+\noindent
+In that example, \cs{multiopt} takes an optional parenthesized
+argument. If omitted, it defaults to |(0,0)|. If provided, the
+argument must be of the form ``|(|\meta{x}|,|\meta{y}|)|''. In either
+case, the comma-separated values within the parentheses are parsed
+into~|#1| and~|#2|. Contrast that with the following:
+
+\prototype{MACRO multiopt OPT(\#1=\{0,0\})}
+\begin{verbatim}
+ \makeatletter
+ \newcommand{\multiopt}{%
+ \@ifnextchar({\multiopt@i}{\multiopt@i({0,0})}%
+ }
+
+ \def\multiopt@i(#1){%
+ % Put your code here.
+ % You can refer to the argument as #1.
+ }
+ \makeatother
+\end{verbatim}
+
+The optional argument still defaults to |(0,0)|, but |#1| receives
+\emph{all} of the text that lies between the parentheses;
+\cs{multiopt} does not parse it into two comma-separated values
+in~|#1| and~|#2|, as it did in the previous example.
+
+\bigskip
+
+The \meta{default} text in an |OPT| term can reference any macro
+parameter introduced before the |OPT|\@. Hence, the following defines
+a macro that accepts a required argument followed by an optional
+argument. The default value of the optional argument is the value
+provided for the required argument:
+
+\prototype{MACRO paramdefault \#1 OPT[\#2=\{\#1\}]}
+\begin{verbatim}
+ \makeatletter
+ \newcommand{\paramdefault}[1]{%
+ \@ifnextchar[{\paramdefault@i{#1}}{\paramdefault@i{#1}[{#1}]}%
+ }
+
+ \def\paramdefault@i#1[#2]{%
+ % Put your code here.
+ % You can refer to the arguments as #1 and #2.
+ }
+ \makeatother
+\end{verbatim}
+
+
+\subsection{Literal text}
+
+In addition to required and optional parameters, it is also possible
+to specify text that must appear literally in the macro call. Merely
+specify it within curly braces:
+
+\prototype{MACRO textual \#1 \{ and \} \#2 \{.\}}
+\begin{verbatim}
+ \makeatletter
+ \newcommand{\textual}[1]{%
+ \textual@i{#1}%
+ }
+
+ \def\textual@i#1 and #2.{%
+ % Put your code here.
+ % You can refer to the arguments as #1 and #2.
+ }
+ \makeatother
+\end{verbatim}
+
+\noindent
+A macro such as \cs{textual} can be called like this:
+
+\begin{verbatim}
+ \textual {Milk} and {cookies}.
+\end{verbatim}
+
+\noindent
+Actually, in that example, because both |Milk| and |cookies| are
+delimited on the right by literal text, \TeX\ can figure out how to
+split \cs{textual}'s argument into~|#1| and~|#2| even if the curly
+braces are omitted:
+
+\begin{verbatim}
+ \textual Milk and cookies.
+\end{verbatim}
+
+\subsection{Starred macros}
+
+The names of some \LaTeX\ macros can be followed by an optional
+``|*|'' to indicate a variation on the normal processing. For
+example, \cs{vspace}, which introduces a given amount of vertical
+space, discards the space if it appears at the top of the page.
+|\vspace*|, in contrast, typesets the space no matter where it
+appears. \ncpy\ makes it easy for users to define their own starred
+commands:
+
+\prototype{MACRO starred * \#1 \#2}
+\begin{verbatim}
+ \makeatletter
+ \newif\ifstarred@star
+ \newcommand{\starred}{%
+ \@ifstar{\starred@startrue\starred@i*}{\starred@starfalse\starred@i*}%
+ }
+
+ \def\starred@i*#1#2{%
+ \ifstarred@star
+ % Put code for the "*" case here.
+ \else
+ % Put code for the non-"*" case here.
+ \fi
+ % Put code common to both cases here (and/or above the \ifstarred@star).
+ % You can refer to the arguments as #1 and #2.
+ }
+ \makeatother
+\end{verbatim}
+
+\noindent
+Note that unlike the generated code shown up to this point, the code
+for starred macros includes \emph{multiple} placeholders for user
+code.
+
+The ``|*|'' in a starred macro does not have to immediately follow the
+macro name; it can appear anywhere in the macro specification.
+However, \ncpy\ currently limits macros to at most one asterisk.
+
+Embedding an asterisk within curly braces causes it to be treated not
+as an optional character but as (required) literal text. Contrast the
+preceding example with the following one:
+
+\prototype{MACRO starred \{*\} \#1 \#2}
+\begin{verbatim}
+ \makeatletter
+ \newcommand{\starred}{%
+ \starred@i%
+ }
+
+ \def\starred@i*#1#2{%
+ % Put your code here.
+ % You can refer to the arguments as #1 and #2.
+ }
+ \makeatother
+\end{verbatim}
+
+The asterisk in that definition of \cs{starred} must be included in
+every macro invocation or \TeX\ will abort with a ``\texttt{Use of
+ \string\starred@i doesn't match its definition}'' error.
+
+
+\subsection{More than nine arguments}
+
+\TeX\ imposes a limit of nine arguments per macro. Internally,
+``|#|'' is expected to be followed by exactly one digit, which means
+that ``|#10|'' refers to argument~|#1| followed by the character~|0|.
+Fortunately, it's rare that a macro needs more than nine arguments and
+rarer still that those arguments are not better specified as a list of
+\meta{key}|=|\meta{value} pairs, as supported by the \textsf{keyval}
+package and many other \LaTeX\ packages.
+
+If large numbers of arguments are in fact necessary, \ncpy\ does let
+you specify them. The trick that the generated code uses is to split
+the macro into multiple macros, each of which takes nine or fewer
+arguments and stores the value of each argument in a variable that can
+later be accessed. Because digits are awkward to use in macro names,
+\ncpy\ uses roman numerals to name arguments in the case of more than
+nine arguments: |\|\meta{name}|@arg@i| for |#1|,
+|\|\meta{name}|@arg@ii| for |#2|, |\|\meta{name}|@arg@iii| for |#3|,
+|\|\meta{name}|@arg@iv| for |#4|, and so forth. The following example
+takes 14 required arguments and one optional argument (which defaults
+to the string ``|etc|''):
+
+\prototype{MACRO manyargs \#1 \#2 \#3 \#4 \#5 \#6 \#7 \#8 \#9 \#10
+ \#11 \#12 \#13 \#14 OPT[\#15=\{etc\}]}
+\begin{verbatim}
+ \makeatletter
+ \newcommand{\manyargs}[9]{%
+ \def\manyargs@arg@i{#1}%
+ \def\manyargs@arg@ii{#2}%
+ \def\manyargs@arg@iii{#3}%
+ \def\manyargs@arg@iv{#4}%
+ \def\manyargs@arg@v{#5}%
+ \def\manyargs@arg@vi{#6}%
+ \def\manyargs@arg@vii{#7}%
+ \def\manyargs@arg@viii{#8}%
+ \def\manyargs@arg@ix{#9}%
+ \manyargs@i
+ }
+
+ \def\manyargs@i#1#2#3#4#5{%
+ \def\manyargs@arg@x{#1}%
+ \def\manyargs@arg@xi{#2}%
+ \def\manyargs@arg@xii{#3}%
+ \def\manyargs@arg@xiii{#4}%
+ \def\manyargs@arg@xiv{#5}%
+ \@ifnextchar[{\manyargs@ii}{\manyargs@ii[{etc}]}%
+ }
+
+ \def\manyargs@ii[#1]{%
+ \def\manyargs@arg@xv{#1}%
+ % Put your code here.
+ % You can refer to the arguments as \manyargs@arg@i through \manyargs@arg@xv.
+ }
+ \makeatother
+\end{verbatim}
+
+The current version of \ncpy\ is limited to 4000 arguments, which
+should be more than enough for most purposes.
+
+
+\subsection{Summary}
+
+A macro is defined in \ncpy\ with:
+
+\begin{center}
+ \texttt{MACRO} \meta{name} \meta{arguments}
+\end{center}
+
+\noindent
+in which \meta{name} is the name of the macro, and \meta{arguments} is
+zero or more of the following:
+
+\begin{center}
+\renewcommand{\arraystretch}{1.1}
+\begin{tabularx}{\linewidth}{@{}lXl@{}}
+ \toprule
+ \multicolumn{1}{@{}c}{Argument} &
+ \multicolumn{1}{c}{Meaning} &
+ \multicolumn{1}{c@{}}{Example} \\
+ \midrule
+
+ |#|\meta{number} & Parameter (required) & |#1| \\
+ \marg{text} & Literal text (required) & |{+}| \\
+ |OPT[#|\meta{number}|=|\marg{text}|]| & Parameter (optional, with default) &
+ |OPT[#1={tbp}]| \\
+ |OPT(#|\meta{number}|=|\marg{text}|)| & Same as the above, but with
+ parentheses instead of brackets & |OPT(#1={tbp})| \\
+ |*| & Literal asterisk (optional) & |*| \\
+ \bottomrule
+\end{tabularx}
+\end{center}
+
+Within an |OPT| argument, |#|\meta{number}|=|\marg{text} can be
+repeated any number of times, as long as the various instances are
+separated by literal text.
+
+
+\section{Further examples}
+
+\subsection{Mimicking \LaTeX's \texttt{picture} environment}
+
+The \LaTeX\ |picture| environment takes two, parenthesized,
+coordinate-pair arguments, the second pair being optional. Here's how
+to define a macro that takes the same arguments as the |picture|
+environment and parses them into $x_1$, $y_1$, $x_2$, and~$y_2$
+(i.e.,~|#1|--|#4|):
+
+\prototype{MACRO picturemacro \{(\}\#1\{,\}\#2\{)\} OPT(\#3=\{0\},\#4=\{0\})}
+\begin{verbatim}
+ \makeatletter
+ \newcommand{\picturemacro}{%
+ \picturemacro@i%
+ }
+
+ \def\picturemacro@i(#1,#2){%
+ \@ifnextchar({\picturemacro@ii({#1},{#2})}{\picturemacro@ii({#1},{#2})({0},{0})}%
+ }
+
+ \def\picturemacro@ii(#1,#2)(#3,#4){%
+ % Put your code here.
+ % You can refer to the arguments as #1 through #4.
+ }
+ \makeatother
+\end{verbatim}
+
+The first pair of parentheses and the comma are quoted because they
+represent required, literal text.
+
+
+\subsection{Mimicking \LaTeX's \texttt{\string\parbox} macro}
+
+\LaTeX's \cs{parbox} macro takes three optional arguments and two
+required arguments. Furthermore, the third argument defaults to
+whatever value was specified for the first argument. This is easy to
+express in \LaTeX\ with the help of \ncpy:
+
+\prototype{MACRO parboxlike OPT[\#1=\{s\}] OPT[\#2=\{\string\relax\}]
+ OPT[\#3=\{\#1\}] \#4 \#5}
+\begin{verbatim}
+ \makeatletter
+ \newcommand{\parboxlike}[1][s]{%
+ \@ifnextchar[{\parboxlike@i[{#1}]}{\parboxlike@i[{#1}][{\relax}]}%
+ }
+
+ \def\parboxlike@i[#1][#2]{%
+ \@ifnextchar[{\parboxlike@ii[{#1}][{#2}]}{\parboxlike@ii[{#1}][{#2}][{#1}]}%
+ }
+
+ \def\parboxlike@ii[#1][#2][#3]#4#5{%
+ % Put your code here.
+ % You can refer to the arguments as #1 through #5.
+ }
+ \makeatother
+\end{verbatim}
+
+
+\subsection{Dynamically changing argument formats}
+\label{sec:dynamic-args}
+
+With a little cleverness, it is possible for a macro to accept one of
+two completely different sets of arguments based on the values
+provided for earlier arguments. For example, suppose we want to
+define a macro, \cs{differentargs} that can be called as either
+
+\begin{verbatim}
+ \differentargs*[optarg]{reqarg}
+\end{verbatim}
+
+\noindent
+or
+
+\begin{verbatim}
+ \differentargs{reqarg}(optarg)
+\end{verbatim}
+
+\noindent
+That is, the presence of an asterisk determines whether
+\cs{differentargs} should expect an optional argument in square
+brackets followed by a required argument or to expect a required
+argument followed by an optional argument in parentheses.
+
+The trick is to create two helper macros: one for the ``|*|'' case
+(\cs{withstar}) and the other for the non-``|*|'' case
+(\cs{withoutstar}). \cs{differentargs} can then invoke one of
+\cs{withstar} or \cs{withoutstar} based on whether or not it sees an
+asterisk. The following shows how to use \ncpy\ to define
+\cs{differentargs}, \cs{withstar}, and \cs{withoutstar} and how to
+edit \cs{differentargs} to invoke its helper macros:
+
+\prototype{MACRO differentargs *}
+\begin{verbatim}
+ \makeatletter
+ \newif\ifdifferentargs@star
+ \newcommand{\differentargs}{%
+ \@ifstar{\differentargs@startrue\differentargs@i*}
+ {\differentargs@starfalse\differentargs@i*}%
+ }
+\end{verbatim}
+\begingroup
+\small
+\begin{alltt}
+ \cs{def}\cs{differentargs@i}*\{%
+ \cs{ifdifferentargs@star}
+ % Put code for the "*" case here.
+ \colorbox{yellow}{\cs{let}\cs{next}=\cs{withstar}}
+ \cs{else}
+ % Put code for the non-"*" case here.
+ \colorbox{yellow}{\cs{let}\cs{next}=\cs{withoutstar}}
+ \cs{fi}
+ % Put code common to both cases here (and/or above the \cs{ifdifferentargs@star}).
+ \colorbox{yellow}{\cs{next}}
+ \}
+ \cs{makeatother}
+\end{alltt}
+\endgroup
+
+\prototype{MACRO withstar OPT[\#1=\{starry\}] \#2}
+\begin{verbatim}
+ \newcommand{\withstar}[2][starry]{%
+ % Put your code here.
+ % You can refer to the arguments as #1 and #2.
+ }
+\end{verbatim}
+
+\prototype{MACRO withoutstar \#1 OPT(\#2=\{dark\})}
+\begin{verbatim}
+ \makeatletter
+ \newcommand{\withoutstar}[1]{%
+ \@ifnextchar({\withoutstar@i{#1}}{\withoutstar@i{#1}({dark})}%
+ }
+
+ \def\withoutstar@i#1(#2){%
+ % Put your code here.
+ % You can refer to the arguments as #1 and #2.
+ }
+ \makeatother
+\end{verbatim}
+
+Note that we edited \cs{differentargs@i} to let \cs{next} be
+equivalent to either \cs{withstar} or \cs{withoutstar} based on
+whether an asterisk was encountered. \cs{next} is evaluated outside
+of the \cs{ifdifferentargs@star}\dots\linebreak[0]\cs{fi} control
+structure. This rigmarole is necessary because directly calling
+\cs{withstar} or \cs{withoutstar} would cause those macros to see
+\cs{ifdifferentargs@star}'s \cs{else} or \cs{fi} as their first
+argument when they ought to see the text following the
+\cs{differentargs} call.
+
+
+\section{Grammar}
+
+The following is the formal specification of \ncpy's grammar, written
+in a more-or-less top-down manner. Literal values, shown in a
+typewriter font, are case-sensitive. \meta{letter} refers to a letter
+of the (English) alphabet. \meta{digit} refers to a digit.
+
+\setlength{\grammarindent}{7em}
+\begin{grammar}
+<decl> ::= \[[ "MACRO" <ident> <arglist> \]]
+
+<ident> ::= \[[ \begin{rep} <letter> \end{rep} \]]
+
+<arglist> ::= \[[ \begin{rep} \\ <arg> \end{rep} \]]
+
+<arg> ::= \[[
+ \begin{stack}
+ <formal> \\
+ <quoted> \\
+ <optarg> \\
+ "*"
+ \end{stack}
+ \]]
+
+<formal> ::= \[[ "#" \begin{rep} <digit> \end{rep} \]]
+
+<quoted> ::= \[[ "{" <rawtext> "}" \]]
+
+<rawtext> ::= \[[ \begin{rep} \tok{anything except a "{", "}", or "\#"} \end{rep} \]]
+
+<optarg> ::= \[[ "OPT" <delim> <defvals> <delim> \]]
+
+<delim> ::= \[[ \begin{stack} "[" \\ "]" \\ "(" \\ ")" \end{stack} \]]
+
+<defvals> ::= \[[
+ \begin{rep}
+ <defval> \\
+ \begin{stack}
+ <quoted> \\
+ <rawtext>
+ \end{stack}
+ \end{rep}
+ \]]
+
+<defval> ::= \[[ <formal> "=" <quoted> \]]
+\end{grammar}
+
+
+\section{Acknowledgements}
+
+I'd like to say thank you to the following people:
+
+\begin{itemize}
+ \item John Aycock for writing the
+ \href{http://pages.cpsc.ucalgary.ca/~aycock/spark/}{Scanning,
+ Parsing, and Rewriting Kit (SPARK)}---the lexer and parser
+ underlying \ncpy---and making it freely available and
+ redistributable.
+
+ \item Hendri Adriaens for pointing out a bug in the code generated
+ by \ncpy. Previously, bracketed text within a mandatory argument
+ could be mistaken for an optional argument.
+
+ \item Tom Potts for reporting a spurious error message caused by the
+ processing of |OPT|. This bug has now been fixed. Tom Potts also
+ proposed the example used in Section~\ref{sec:dynamic-args} in
+ which the starred and unstarred versions of a macro take different
+ arguments.
+\end{itemize}
+
+
+\section{Copyright and license}
+
+Copyright~\copyright{} 2010, Scott Pakin
+
+\bigskip
+
+This package may be distributed and/or modified under the conditions
+of the \LaTeX{} Project Public License, either version~1.3c of this
+license or (at your option) any later version. The latest version of
+this license is in:
+
+\begin{center}
+ \url{http://www.latex-project.org/lppl.txt}
+\end{center}
+
+\noindent
+and version~1.3c or later is part of all distributions of \LaTeX{}
+version 2006/05/20 or later.
+
+\end{document}
diff --git a/support/newcommand/spark.py b/support/newcommand/spark.py
new file mode 100644
index 0000000000..ffe9b4bf7c
--- /dev/null
+++ b/support/newcommand/spark.py
@@ -0,0 +1,566 @@
+# Copyright (c) 1998-2000 John Aycock
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+__version__ = 'SPARK-0.6.1'
+
+import re
+import sys
+import string
+
+def _namelist(instance):
+ namelist, namedict, classlist = [], {}, [instance.__class__]
+ for c in classlist:
+ for b in c.__bases__:
+ classlist.append(b)
+ for name in dir(c):
+ if not namedict.has_key(name):
+ namelist.append(name)
+ namedict[name] = 1
+ return namelist
+
+class GenericScanner:
+ def __init__(self):
+ pattern = self.reflect()
+ self.re = re.compile(pattern, re.VERBOSE)
+
+ self.index2func = {}
+ for name, number in self.re.groupindex.items():
+ self.index2func[number-1] = getattr(self, 't_' + name)
+
+ def makeRE(self, name):
+ doc = getattr(self, name).__doc__
+ rv = '(?P<%s>%s)' % (name[2:], doc)
+ return rv
+
+ def reflect(self):
+ rv = []
+ for name in _namelist(self):
+ if name[:2] == 't_' and name != 't_default':
+ rv.append(self.makeRE(name))
+
+ rv.append(self.makeRE('t_default'))
+ return string.join(rv, '|')
+
+ def error(self, s, pos):
+ print "Lexical error at position %s" % pos
+ raise SystemExit
+
+ def tokenize(self, s):
+ pos = 0
+ n = len(s)
+ while pos < n:
+ m = self.re.match(s, pos)
+ if m is None:
+ self.error(s, pos)
+
+ groups = m.groups()
+ for i in range(len(groups)):
+ if groups[i] and self.index2func.has_key(i):
+ self.index2func[i](groups[i])
+ pos = m.end()
+
+ def t_default(self, s):
+ r'( . | \n )+'
+ pass
+
+class GenericParser:
+ def __init__(self, start):
+ self.rules = {}
+ self.rule2func = {}
+ self.rule2name = {}
+ self.collectRules()
+ self.startRule = self.augment(start)
+ self.ruleschanged = 1
+
+ _START = 'START'
+ _EOF = 'EOF'
+
+ #
+ # A hook for GenericASTBuilder and GenericASTMatcher.
+ #
+ def preprocess(self, rule, func): return rule, func
+
+ def addRule(self, doc, func):
+ rules = string.split(doc)
+
+ index = []
+ for i in range(len(rules)):
+ if rules[i] == '::=':
+ index.append(i-1)
+ index.append(len(rules))
+
+ for i in range(len(index)-1):
+ lhs = rules[index[i]]
+ rhs = rules[index[i]+2:index[i+1]]
+ rule = (lhs, tuple(rhs))
+
+ rule, fn = self.preprocess(rule, func)
+
+ if self.rules.has_key(lhs):
+ self.rules[lhs].append(rule)
+ else:
+ self.rules[lhs] = [ rule ]
+ self.rule2func[rule] = fn
+ self.rule2name[rule] = func.__name__[2:]
+ self.ruleschanged = 1
+
+ def collectRules(self):
+ for name in _namelist(self):
+ if name[:2] == 'p_':
+ func = getattr(self, name)
+ doc = func.__doc__
+ self.addRule(doc, func)
+
+ def augment(self, start):
+ #
+ # Tempting though it is, this isn't made into a call
+ # to self.addRule() because the start rule shouldn't
+ # be subject to preprocessing.
+ #
+ startRule = (self._START, ( start, self._EOF ))
+ self.rule2func[startRule] = lambda args: args[0]
+ self.rules[self._START] = [ startRule ]
+ self.rule2name[startRule] = ''
+ return startRule
+
+ def makeFIRST(self):
+ union = {}
+ self.first = {}
+
+ for rulelist in self.rules.values():
+ for lhs, rhs in rulelist:
+ if not self.first.has_key(lhs):
+ self.first[lhs] = {}
+
+ if len(rhs) == 0:
+ self.first[lhs][None] = 1
+ continue
+
+ sym = rhs[0]
+ if not self.rules.has_key(sym):
+ self.first[lhs][sym] = 1
+ else:
+ union[(sym, lhs)] = 1
+ changes = 1
+ while changes:
+ changes = 0
+ for src, dest in union.keys():
+ destlen = len(self.first[dest])
+ self.first[dest].update(self.first[src])
+ if len(self.first[dest]) != destlen:
+ changes = 1
+
+ #
+ # An Earley parser, as per J. Earley, "An Efficient Context-Free
+ # Parsing Algorithm", CACM 13(2), pp. 94-102. Also J. C. Earley,
+ # "An Efficient Context-Free Parsing Algorithm", Ph.D. thesis,
+ # Carnegie-Mellon University, August 1968, p. 27.
+ #
+
+ def typestring(self, token):
+ return None
+
+ def error(self, token):
+ print "Syntax error at or near `%s' token" % token
+ raise SystemExit
+
+ def parse(self, tokens):
+ tree = {}
+ tokens.append(self._EOF)
+ states = { 0: [ (self.startRule, 0, 0) ] }
+
+ if self.ruleschanged:
+ self.makeFIRST()
+
+ for i in xrange(len(tokens)):
+ states[i+1] = []
+
+ if states[i] == []:
+ break
+ self.buildState(tokens[i], states, i, tree)
+
+ #_dump(tokens, states)
+
+ if i < len(tokens)-1 or states[i+1] != [(self.startRule, 2, 0)]:
+ del tokens[-1]
+ self.error(tokens[i-1])
+ rv = self.buildTree(tokens, tree, ((self.startRule, 2, 0), i+1))
+ del tokens[-1]
+ return rv
+
+ def buildState(self, token, states, i, tree):
+ needsCompletion = {}
+ state = states[i]
+ predicted = {}
+
+ for item in state:
+ rule, pos, parent = item
+ lhs, rhs = rule
+
+ #
+ # A -> a . (completer)
+ #
+ if pos == len(rhs):
+ if len(rhs) == 0:
+ needsCompletion[lhs] = (item, i)
+
+ for pitem in states[parent]:
+ if pitem is item:
+ break
+
+ prule, ppos, pparent = pitem
+ plhs, prhs = prule
+
+ if prhs[ppos:ppos+1] == (lhs,):
+ new = (prule,
+ ppos+1,
+ pparent)
+ if new not in state:
+ state.append(new)
+ tree[(new, i)] = [(item, i)]
+ else:
+ tree[(new, i)].append((item, i))
+ continue
+
+ nextSym = rhs[pos]
+
+ #
+ # A -> a . B (predictor)
+ #
+ if self.rules.has_key(nextSym):
+ #
+ # Work on completer step some more; for rules
+ # with empty RHS, the "parent state" is the
+ # current state we're adding Earley items to,
+ # so the Earley items the completer step needs
+ # may not all be present when it runs.
+ #
+ if needsCompletion.has_key(nextSym):
+ new = (rule, pos+1, parent)
+ olditem_i = needsCompletion[nextSym]
+ if new not in state:
+ state.append(new)
+ tree[(new, i)] = [olditem_i]
+ else:
+ tree[(new, i)].append(olditem_i)
+
+ #
+ # Has this been predicted already?
+ #
+ if predicted.has_key(nextSym):
+ continue
+ predicted[nextSym] = 1
+
+ ttype = token is not self._EOF and \
+ self.typestring(token) or \
+ None
+ if ttype is not None:
+ #
+ # Even smarter predictor, when the
+ # token's type is known. The code is
+ # grungy, but runs pretty fast. Three
+ # cases are looked for: rules with
+ # empty RHS; first symbol on RHS is a
+ # terminal; first symbol on RHS is a
+ # nonterminal (and isn't nullable).
+ #
+ for prule in self.rules[nextSym]:
+ new = (prule, 0, i)
+ prhs = prule[1]
+ if len(prhs) == 0:
+ state.append(new)
+ continue
+ prhs0 = prhs[0]
+ if not self.rules.has_key(prhs0):
+ if prhs0 != ttype:
+ continue
+ else:
+ state.append(new)
+ continue
+ first = self.first[prhs0]
+ if not first.has_key(None) and \
+ not first.has_key(ttype):
+ continue
+ state.append(new)
+ continue
+
+ for prule in self.rules[nextSym]:
+ #
+ # Smarter predictor, as per Grune &
+ # Jacobs' _Parsing Techniques_. Not
+ # as good as FIRST sets though.
+ #
+ prhs = prule[1]
+ if len(prhs) > 0 and \
+ not self.rules.has_key(prhs[0]) and \
+ token != prhs[0]:
+ continue
+ state.append((prule, 0, i))
+
+ #
+ # A -> a . c (scanner)
+ #
+ elif token == nextSym:
+ #assert new not in states[i+1]
+ states[i+1].append((rule, pos+1, parent))
+
+ def buildTree(self, tokens, tree, root):
+ stack = []
+ self.buildTree_r(stack, tokens, -1, tree, root)
+ return stack[0]
+
+ def buildTree_r(self, stack, tokens, tokpos, tree, root):
+ (rule, pos, parent), state = root
+
+ while pos > 0:
+ want = ((rule, pos, parent), state)
+ if not tree.has_key(want):
+ #
+ # Since pos > 0, it didn't come from closure,
+ # and if it isn't in tree[], then there must
+ # be a terminal symbol to the left of the dot.
+ # (It must be from a "scanner" step.)
+ #
+ pos = pos - 1
+ state = state - 1
+ stack.insert(0, tokens[tokpos])
+ tokpos = tokpos - 1
+ else:
+ #
+ # There's a NT to the left of the dot.
+ # Follow the tree pointer recursively (>1
+ # tree pointers from it indicates ambiguity).
+ # Since the item must have come about from a
+ # "completer" step, the state where the item
+ # came from must be the parent state of the
+ # item the tree pointer points to.
+ #
+ children = tree[want]
+ if len(children) > 1:
+ child = self.ambiguity(children)
+ else:
+ child = children[0]
+
+ tokpos = self.buildTree_r(stack,
+ tokens, tokpos,
+ tree, child)
+ pos = pos - 1
+ (crule, cpos, cparent), cstate = child
+ state = cparent
+
+ lhs, rhs = rule
+ result = self.rule2func[rule](stack[:len(rhs)])
+ stack[:len(rhs)] = [result]
+ return tokpos
+
+ def ambiguity(self, children):
+ #
+ # XXX - problem here and in collectRules() if the same
+ # rule appears in >1 method. But in that case the
+ # user probably gets what they deserve :-) Also
+ # undefined results if rules causing the ambiguity
+ # appear in the same method.
+ #
+ sortlist = []
+ name2index = {}
+ for i in range(len(children)):
+ ((rule, pos, parent), index) = children[i]
+ lhs, rhs = rule
+ name = self.rule2name[rule]
+ sortlist.append((len(rhs), name))
+ name2index[name] = i
+ sortlist.sort()
+ list = map(lambda (a,b): b, sortlist)
+ return children[name2index[self.resolve(list)]]
+
+ def resolve(self, list):
+ #
+ # Resolve ambiguity in favor of the shortest RHS.
+ # Since we walk the tree from the top down, this
+ # should effectively resolve in favor of a "shift".
+ #
+ return list[0]
+
+#
+# GenericASTBuilder automagically constructs a concrete/abstract syntax tree
+# for a given input. The extra argument is a class (not an instance!)
+# which supports the "__setslice__" and "__len__" methods.
+#
+# XXX - silently overrides any user code in methods.
+#
+
+class GenericASTBuilder(GenericParser):
+ def __init__(self, AST, start):
+ GenericParser.__init__(self, start)
+ self.AST = AST
+
+ def preprocess(self, rule, func):
+ rebind = lambda lhs, self=self: \
+ lambda args, lhs=lhs, self=self: \
+ self.buildASTNode(args, lhs)
+ lhs, rhs = rule
+ return rule, rebind(lhs)
+
+ def buildASTNode(self, args, lhs):
+ children = []
+ for arg in args:
+ if isinstance(arg, self.AST):
+ children.append(arg)
+ else:
+ children.append(self.terminal(arg))
+ return self.nonterminal(lhs, children)
+
+ def terminal(self, token): return token
+
+ def nonterminal(self, type, args):
+ rv = self.AST(type)
+ rv[:len(args)] = args
+ return rv
+
+#
+# GenericASTTraversal is a Visitor pattern according to Design Patterns. For
+# each node it attempts to invoke the method n_<node type>, falling
+# back onto the default() method if the n_* can't be found. The preorder
+# traversal also looks for an exit hook named n_<node type>_exit (no default
+# routine is called if it's not found). To prematurely halt traversal
+# of a subtree, call the prune() method -- this only makes sense for a
+# preorder traversal. Node type is determined via the typestring() method.
+#
+
+class GenericASTTraversalPruningException:
+ pass
+
+class GenericASTTraversal:
+ def __init__(self, ast):
+ self.ast = ast
+
+ def typestring(self, node):
+ return node.type
+
+ def prune(self):
+ raise GenericASTTraversalPruningException
+
+ def preorder(self, node=None):
+ if node is None:
+ node = self.ast
+
+ try:
+ name = 'n_' + self.typestring(node)
+ if hasattr(self, name):
+ func = getattr(self, name)
+ func(node)
+ else:
+ self.default(node)
+ except GenericASTTraversalPruningException:
+ return
+
+ for kid in node:
+ self.preorder(kid)
+
+ name = name + '_exit'
+ if hasattr(self, name):
+ func = getattr(self, name)
+ func(node)
+
+ def postorder(self, node=None):
+ if node is None:
+ node = self.ast
+
+ for kid in node:
+ self.postorder(kid)
+
+ name = 'n_' + self.typestring(node)
+ if hasattr(self, name):
+ func = getattr(self, name)
+ func(node)
+ else:
+ self.default(node)
+
+
+ def default(self, node):
+ pass
+
+#
+# GenericASTMatcher. AST nodes must have "__getitem__" and "__cmp__"
+# implemented.
+#
+# XXX - makes assumptions about how GenericParser walks the parse tree.
+#
+
+class GenericASTMatcher(GenericParser):
+ def __init__(self, start, ast):
+ GenericParser.__init__(self, start)
+ self.ast = ast
+
+ def preprocess(self, rule, func):
+ rebind = lambda func, self=self: \
+ lambda args, func=func, self=self: \
+ self.foundMatch(args, func)
+ lhs, rhs = rule
+ rhslist = list(rhs)
+ rhslist.reverse()
+
+ return (lhs, tuple(rhslist)), rebind(func)
+
+ def foundMatch(self, args, func):
+ func(args[-1])
+ return args[-1]
+
+ def match_r(self, node):
+ self.input.insert(0, node)
+ children = 0
+
+ for child in node:
+ if children == 0:
+ self.input.insert(0, '(')
+ children = children + 1
+ self.match_r(child)
+
+ if children > 0:
+ self.input.insert(0, ')')
+
+ def match(self, ast=None):
+ if ast is None:
+ ast = self.ast
+ self.input = []
+
+ self.match_r(ast)
+ self.parse(self.input)
+
+ def resolve(self, list):
+ #
+ # Resolve ambiguity in favor of the longest RHS.
+ #
+ return list[-1]
+
+def _dump(tokens, states):
+ for i in range(len(states)):
+ print 'state', i
+ for (lhs, rhs), pos, parent in states[i]:
+ print '\t', lhs, '::=',
+ print string.join(rhs[:pos]),
+ print '.',
+ print string.join(rhs[pos:]),
+ print ',', parent
+ if i < len(tokens):
+ print
+ print 'token', str(tokens[i])
+ print