Commits

Mathias Panzenböck committed 723fee5

initial commit

  • Participants

Comments (0)

Files changed (3)

+/**
+ * JavaScript Option Parser (optparse)
+ * Copyright (C) 2010  Mathias Panzenböck <grosser.meister.morti@gmx.net>
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA 
+ */
+
+/**
+ * Construct a new OptionParser.
+ * See the end of this file for example usage.
+ *
+ * @param object params   optional Parameter-Object
+ *
+ * ===== Parameter-Object =====
+ *   {
+ *      minargs: integer, optional
+ *      maxargs: integer, optional
+ *      program: string, per default inferred from process.argv
+ *      strings: object, optional
+ *               Table of strings used in the output. See below.
+ *      options: array, optional
+ *               Array of option definitions. See below.
+ *   }
+ *
+ * ===== String-Table =====
+ *   {
+ *      help:      string, default: 'No help available for this option.'
+ *      usage:     string, default: 'Usage'
+ *      options:   string, default: 'OPTIONS'
+ *      arguments: string, default: 'ARGUMENTS'
+ *      required:  string, default: 'required'
+ *      default:   string, default: 'default'
+ *      base:      string, default: 'base'
+ *      metavars:  object, optional
+ *                 Table of default metavar names per type.
+ *                 Per default the type name in capital letters or derived
+ *                 from the possible values.
+ *   }
+ *
+ * ===== Option Definition =====
+ *   {
+ *      // Only when passed to the OptionParser constructor:
+ *      name:        string or array
+ *      names:       string or array, alias of name
+ *                   Only one of both may be used at the same time.
+ *
+ *                   Names can be long options (e.g. '--foo') and short options
+ *                   (e.g. '-f'). The first name is used to indentify the option.
+ *                   Names musst be unique and may not contain '='.
+ *
+ *                   Short options may be combined when passed to a programm. E.g.
+ *                   the options '-f' and '-b' can be combined to '-fb'. Only one
+ *                   of these combined options may require an argument.
+ *
+ *                   Short options are separated from ther arguments by space,
+ *                   long options per '='. If a long option requires an argument
+ *                   and none is passed using '=' it also uses the next commandline
+ *                   argument as it's argument (like short options).
+ *
+ *                   If '--' is encountered all remaining arguments are treated as
+ *                   arguments and not as options.
+ *
+ *      // General fields:
+ *      target:      string, per deflault inferred from first name
+ *                   This defines the name used in the returned options object.
+ *                   Multiple options may have the same target.
+ *      default:     any, default: undefined
+ *                   The default value associated with a certain target and is
+ *                   overwritten by each new option with the same target.
+ *      type:        string, default: 'string', see below
+ *      required:    boolean, default: false
+ *      redefinable: boolean, default: true
+ *      help:        string, optional
+ *      details:     array, optional
+ *                   short list of details shown in braces after the option name
+ *                   e.g. integer type options add 'base: '+base if base !== undefined
+ *      metavar:     string or array, per deflault inferred from type
+ *      onOption:    function (value) -> boolean, optional
+ *                   Returning true canceles any further option parsing
+ *                   and the parse() method returns null.
+ *
+ *      // Type: string  (alias: str)
+ *      // Type: boolean (alias: bool)
+ *      // Type: object  (alias: obj)
+ *
+ *      // Type: integer (alias: int)
+ *      min:         integer, optional
+ *      max:         integer, optional
+ *      NaN:         boolean, default: false
+ *      base:        integer, optional
+ *
+ *      // Type: float   (alias: number)
+ *      min:         float, optional
+ *      max:         float, optional
+ *      NaN:         boolean, default: false
+ *
+ *      // Type: flag
+ *      value:       boolean, default: true
+ *      default:     boolean, default: false
+ *
+ *      // Type: option
+ *      value:       any, per default inferred from first name
+ *
+ *      // Type: enum
+ *      ignoreCase:  boolean, default: true
+ *      values:      array or object where the user enteres the field name of
+ *                   the object and you get the value of the field
+ *
+ *      // Type: record
+ *      args:        array of type definitions (type part of option definitions)
+ *
+ *      // Type: custom
+ *      argc:        integer, default: -1
+ *                   Number of required arguments.
+ *                   -1 means one optional argument.
+ *      parse:       function (string, ...) -> value
+ *      stringify:   function (value) -> string, optional
+ *   }
+ *
+ * ===== Option-Arguments =====
+ * For the following types exactly one argument is required:
+ *   integer, float, string, boolean, object, enum
+ *
+ * The following types have optional arguments:
+ *   flag
+ *
+ * The following types have no arguments:
+ *   option
+ *
+ * Custom types may set this through the argc field.
+ */
+function OptionParser (params) {
+	this.optionsPerName = {};
+	this.defaultValues  = {};
+	this.options        = [];
+
+	if (params !== undefined) {
+		this.minargs = params.minargs == 0 ? undefined : params.minargs;
+		this.maxargs = params.maxargs;
+		this.program = params.program;
+		this.strings = params.strings;
+
+		if (this.minargs > this.maxargs) {
+			throw new Error('minargs > maxargs');
+		}
+	}
+
+	if (this.strings === undefined) {
+		this.strings = {};
+	}
+
+	defaults(this.strings, {
+		help:      'No help available for this option.',
+		usage:     'Usage',
+		options:   'OPTIONS',
+		arguments: 'ARGUMENTS',
+		required:  'required',
+		default:   'default',
+		base:      'base',
+		metavars:  {}
+	});
+
+	defaults(this.strings.metavars, METAVARS);
+
+	if (this.program === undefined) {
+		this.program = process.argv[0] + ' ' + process.argv[1];
+	}
+
+	if (params !== undefined && params.options !== undefined) {
+		for (i in params.options) {
+			var opt = params.options[i];
+			var names;
+
+			if (opt instanceof Array || typeof(opt) == 'string') {
+				opt = undefined;
+				names = opt;
+			}
+			else {
+				names = opt.names;
+				if (names === undefined) {
+					names = opt.name;
+					delete opt.name;
+				}
+				else {
+					delete opt.names;
+				}
+			}
+			this.add(names, opt);
+		}
+	}
+}
+
+OptionParser.prototype = {
+	/**
+	 * Parse command line options.
+	 * 
+	 * @param array args  Commandline arguments.
+	 *                    If undefined process.argv.slice(2) is used.
+	 *
+	 * @return object
+	 *   {
+	 *      arguments: array
+	 *      options:   object, { target -> value }
+	 *   }
+	 */
+	parse: function (args) {
+		if (args === undefined) {
+			args = process.argv.slice(2);
+		}
+
+		var data = {
+			options:   {},
+			arguments: []
+		};
+
+		for (name in this.defaultValues) {
+			var value = this.defaultValues[name];
+
+			if (value !== undefined) {
+				data.options[this.optionsPerName[name].target] = value;
+			}
+		}
+
+		var got = {};
+		var i = 0;
+		for (; i < args.length; ++ i) {
+			var arg = args[i];
+
+			if (arg == '--') {
+				++ i;
+				break;
+			}
+			else if (/^--.+$/.test(arg)) {
+				var j = arg.indexOf('=');
+				var name, value = undefined;
+
+				if (j == -1) {
+					name  = arg;
+				}
+				else {
+					name  = arg.substring(0,j);
+					value = arg.substring(j+1);
+				}
+
+				var optdef = this.optionsPerName[name];
+				
+				if (optdef === undefined) {
+					throw new Error('unknown option: '+name);
+				}
+
+				if (value === undefined) {
+					if (optdef.argc < 1) {
+						value = optdef.value;
+					}
+					else if ((i + optdef.argc) >= args.length) {
+						throw new Error('option '+name+' needs '+optdef.argc+' arguments');
+					}
+					else {
+						value = optdef.parse.apply(optdef, args.slice(i+1, i+1+optdef.argc));
+						i += optdef.argc;
+					}
+				}
+				else if (optdef.argc == 0) {
+					throw new Error('option '+name+' does not need an argument');
+				}
+				else if (optdef.argc > 1) {
+					throw new Error('option '+name+' needs '+optdef.argc+' arguments');
+				}
+				else {
+					value = optdef.parse(value);
+				}
+
+				if (!optdef.redefinable && optdef.target in got) {
+					throw new Error('cannot redefine option '+name);
+				}
+
+				got[optdef.target] = true;
+				data.options[optdef.target] = value;
+
+				if (optdef.onOption && optdef.onOption(value) === true) {
+					return null;
+				}
+			}
+			else if (/^-.+$/.test(arg)) {
+				if (arg.indexOf('=') != -1) {
+					throw new Error('illegal option syntax: '+arg);
+				}
+
+				var tookarg = false;
+				arg = arg.substring(1);
+
+				for (var j = 0; j < arg.length; ++ j) {
+					var name = '-'+arg[j];
+					var optdef = this.optionsPerName[name];
+					var value;
+					
+					if (optdef === undefined) {
+						throw new Error('unknown option: '+name);
+					}
+
+					if (optdef.argc < 1) {
+						value = optdef.value;
+					}
+					else {
+						if (tookarg || (i+optdef.argc) >= args.length) {
+							throw new Error('option '+name+' needs '+optdef.argc+' arguments');
+						}
+
+						value = optdef.parse.apply(optdef, args.slice(i+1, i+1+optdef.argc));
+						i += optdef.argc;
+						tookarg = true;
+					}
+
+					if (!optdef.redefinable && optdef.target in got) {
+						throw new Error('redefined option: '+name);
+					}
+
+					got[optdef.target] = true;
+					data.options[optdef.target] = value;
+
+					if (optdef.onOption && optdef.onOption(value) === true) {
+						return null;
+					}
+				}
+			}
+			else {
+				data.arguments.push(arg);
+			}
+		}
+
+		for (; i < args.length; ++ i) {
+			data.arguments.push(args[i]);
+		}
+
+		var argc = data.arguments.length;
+		if ((this.maxargs !== undefined && argc > this.maxargs) ||
+				(this.minargs !== undefined && argc < this.minargs)) {
+			var msg = 'illegal number of arguments: ' + argc;
+
+			if (this.minargs !== undefined) {
+				msg += ', minumum is ' + this.minargs;
+				if (this.maxargs !== undefined) {
+					msg += ' and maximum is ' + this.maxargs;
+				}
+			}
+			else {
+				msg += ', maximum is ' + this.maxargs;
+			}
+
+			throw new Error(msg);
+		}
+
+		for (i in this.options) {
+			var optdef = this.options[i];
+			if (optdef.required && !(optdef.target in got)) {
+				throw new Error('missing required option: ' + optdef.names[0]);
+			}
+		}
+		
+		return data;
+	},
+	/**
+	 * Add an option definition.
+	 *
+	 * @param string or array names  Option names
+	 * @param object optdef          Option definition
+	 */
+	add: function (names, optdef) {
+		if (typeof(names) == 'string') {
+			names = [names];
+		}
+		else if (names === undefined || names.length == 0) {
+			throw new Error('no option name given');
+		}
+
+		if (optdef === undefined) {
+			optdef = {};
+		}
+
+		optdef.names = names;
+		
+		for (i in names) {
+			var name = names[i];
+			var match = /(-*)(.*)/.exec(name);
+
+			if (name.length == 0 || match[1].length < 1 ||
+					match[1].length > 2 || match[2].length == 0 ||
+					(match[1].length == 1 && match[2].length > 1) ||
+					match[2].indexOf('=') != -1) {
+				throw new Error('illegal option name: ' + name);
+			}
+
+			if (name in this.optionsPerName) {
+				throw new Error('option already exists: '+name);
+			}
+		}
+
+		if (optdef.target === undefined) {
+			var target = names[0].replace(/^--?/,'');
+			
+			if (target.toUpperCase() == target) {
+				// FOO-BAR -> FOO_BAR
+				target = target.replace(/[^a-zA-Z0-9]+/,'_');
+			}
+			else {
+				// foo-bar -> fooBar
+				target = target.split(/[^a-zA-Z0-9]+/);
+				for (var i = 1; i < target.length; ++ i) {
+					var part = target[i];
+	
+					if (part) {
+						target[i] = part[0].toUpperCase() + part.substring(1);
+					}
+				}
+				target = target.join('');
+			}
+
+			optdef.target = target;
+		}
+
+		this._initType(optdef, optdef.names[0]);
+
+		if (optdef.redefinable === undefined) {
+			optdef.redefinable = true;
+		}
+
+		if (optdef.required === undefined) {
+			optdef.required = false;
+		}
+
+		if (optdef.help === undefined) {
+			optdef.help = this.strings.help;
+		}
+		else {
+			optdef.help = optdef.help.trim();
+		}
+		
+		for (i in names) {
+			this.optionsPerName[names[i]] = optdef;
+		}
+		
+		if (optdef.default !== undefined) {
+			this.defaultValues[names[0]] = optdef.default;
+		}
+
+		this.options.push(optdef);
+	},
+	/**
+	 * Show an error message, usage and exit program with exit code 1.
+	 * 
+	 * @param string msg       The error message
+	 * @param WriteStream out  Where to write the message.
+	 *                         If undefined process.stdout is used.
+	 */
+	error: function (msg, out) {
+		if (!out) {
+			out = process.stdout;
+		}
+		out.write('*** '+msg+'\n\n');
+		this.usage(undefined, out);
+		process.exit(1);
+	},
+	/**
+	 * Print usage message.
+	 *
+	 * @param string help      Optional additional help message.
+	 * @param WriteStream out  Where to write the message.
+	 *                         If undefined process.stdout is used.
+	 */
+	usage: function (help, out) {
+		if (!out) {
+			out = process.stdout;
+		}
+
+		out.write(this.strings.usage+': '+this.program+' ['+
+			this.strings.options+']'+(this.maxargs != 0 ?
+				' ['+this.strings.arguments+']\n' : '\n'));
+		out.write('\n');
+		out.write(this.strings.options+':\n');
+
+		for (i in this.options) {
+			var optdef = this.options[i];
+			var optnames = [];
+			var metavar = optdef.metavar;
+
+			if (metavar instanceof Array) {
+				metavar = metavar.join(' ');
+			}
+
+			for (j in optdef.names) {
+				var optname = optdef.names[j];
+
+				if (metavar !== undefined) {
+					if (optdef.argc < 2 && optname.substring(0,2) == '--') {
+						if (optdef.argc < 0) {
+							optname = optname+'[='+metavar+']';
+						}
+						else {
+							optname = optname+'='+metavar;
+						}
+					}
+					else {
+						optname = optname+' '+metavar;
+					}
+				}
+				optnames.push(optname);
+			}
+
+			var details = optdef.details !== undefined ? optdef.details.slice() : [];
+			if (optdef.required) {
+				details.push(this.strings.required);
+			}
+			else if (optdef.argc > 0 && optdef.default !== undefined) {
+				details.push(this.strings.default+': '+optdef.stringify(optdef.default));
+			}
+
+			if (details.length > 0) {
+				details = '  (' + details.join(', ') + ')';
+			}
+
+			if (metavar !== undefined) {
+				optnames[0] += details;
+				out.write('  '+optnames.join('\n  '));
+			}
+			else {
+				out.write('  '+optnames.join(', ')+details);
+			}
+			if (optdef.help) {
+				var lines = optdef.help.split('\n');
+				for (j in lines) {
+					out.write('\n        '+lines[j]);
+				}
+			}
+			out.write('\n\n');
+		}
+
+		if (help !== undefined) {
+			out.write(help);
+			if (help[help.length-1] != '\n') {
+				out.write('\n');
+			}
+		}
+	},
+	_initType: function (optdef, name) {
+		optdef.name = name;
+	
+		if (optdef.type === undefined) {
+			optdef.type = 'string';
+		}
+		else if (optdef.type in TYPE_ALIAS) {
+			optdef.type = TYPE_ALIAS[optdef.type];
+		}
+		
+		switch (optdef.type) {
+			case 'flag':
+				if (optdef.value === undefined) {
+					optdef.value = true;
+				}
+				optdef.parse = parseBool;
+				optdef.argc  = -1;
+	
+				if (optdef.default === undefined) {
+					optdef.default = this.defaultValues[name];
+
+					if (optdef.default === undefined) {
+						optdef.default = false;
+					}
+				}
+				break;
+	
+			case 'option':
+				optdef.argc = 0;
+	
+				if (optdef.value === undefined) {
+					optdef.value = name.replace(/^--?/,'');
+				}
+				break;
+	
+			case 'enum':
+				this._initEnum(optdef, name);
+				break;
+	
+			case 'integer':
+			case 'float':
+				this._initNumber(optdef, name);
+				break;
+	
+			case 'record':
+				if (optdef.args === undefined || optdef.args.length == 0) {
+					throw new Error('record '+name+' needs at least one argument');
+				}
+				optdef.argc = 0;
+				var metavar = [];
+				for (i in optdef.args) {
+					var arg = optdef.args[i];
+					this._initType(arg, name+'['+i+']');
+	
+					if (arg.argc < 1) {
+						throw new Error('argument '+i+' of option '+name+
+							' has illegal number of arguments');
+					}
+					if (arg.metavar instanceof Array) {
+						for (j in arg.metavar) {
+							metavar.push(arg.metavar[j]);
+						}
+					}
+					else {
+						metavar.push(arg.metavar);
+					}
+					delete arg.metavar;
+					optdef.argc += arg.argc;
+				}
+				if (optdef.metavar === undefined) {
+					optdef.metavar = metavar;
+				}
+				var onOption = optdef.onOption;
+				if (onOption !== undefined) {
+					optdef.onOption = function (values) {
+						return onOption.apply(this, values);
+					};
+				}
+				optdef.parse = function () {
+					var values = [];
+					var parserIndex = 0;
+					for (var i = 0; i < arguments.length;) {
+						var arg = optdef.args[parserIndex ++];
+						var raw = [];
+						for (var j = 0; j < arg.argc; ++ j) {
+							raw.push(arguments[i+j]);
+						}
+						var value = arg.parse.apply(arg, raw);
+						values.push(value);
+						i += arg.argc;
+					}
+					return values;
+				};
+				break;
+	
+			case 'custom':
+				if (optdef.argc === undefined || optdef.argc < -1) {
+					optdef.argc = -1;
+				}
+	
+				if (optdef.parse === undefined) {
+					throw new Error(
+						'no parse function defined for custom type option '+name);
+				}
+				break;
+	
+			default:
+				optdef.argc = 1;
+				optdef.parse = PARSERS[optdef.type];
+	
+				if (optdef.parse === undefined) {
+					throw new Error('type of option '+name+' is unknown: '+optdef.type);
+				}
+		}
+	
+		initStringify(optdef);
+		
+		var count = 1;
+		if (optdef.metavar === undefined) {
+			optdef.metavar = this.strings.metavars[optdef.type];
+		}
+		
+		if (optdef.metavar === undefined) {
+			count = 0;
+		}
+		else if (optdef.metavar instanceof Array) {
+			count = optdef.metavar.length;
+		}
+	
+		if (optdef.argc == -1) {
+			if (count > 1) {
+				throw new Error('illegal number of metavars for option '+name+
+					': '+JSON.stringify(optdef.metavar));
+			}
+		}
+		else if (optdef.argc != count) {
+			throw new Error('illegal number of metavars for option '+name+
+				': '+JSON.stringify(optdef.metavar));
+		}
+	},
+	_initEnum: function (optdef, name) {
+		optdef.argc = 1;
+	
+		if (optdef.ignoreCase === undefined) {
+			optdef.ignoreCase = true;
+		}
+	
+		if (optdef.values === undefined || optdef.values.length == 0) {
+			throw new Error('no values for enum '+name+' defined');
+		}
+	
+		initStringify(optdef);
+
+		var labels = [];
+		var values = {};
+		if (optdef.values instanceof Array) {
+			for (i in optdef.values) {
+				var value = optdef.values[i];
+				var label = String(value);
+				values[optdef.ignoreCase ? label.toLowerCase() : label] = value;
+				labels.push(optdef.stringify(value));
+			}
+		}
+		else {
+			for (label in optdef.values) {
+				var value = optdef.values[label];
+				values[optdef.ignoreCase ? label.toLowerCase() : label] = value;
+				labels.push(optdef.stringify(label));
+			}
+			labels.sort();
+		}
+		optdef.values = values;
+		
+		
+		if (optdef.metavar === undefined) {
+			optdef.metavar = '<'+labels.join(', ')+'>';
+		}
+	
+		optdef.parse = function (s) {
+			var value = values[optdef.ignoreCase ? s.toLowerCase() : s];
+			if (value !== undefined) {
+				return value;
+			}
+			throw new Error('illegal value for option '+name+': '+s);
+		};
+	},
+	_initNumber: function (optdef, name) {
+		optdef.argc = 1;
+	
+		if (optdef.NaN === undefined) {
+			optdef.NaN = false;
+		}
+
+		if (optdef.min > optdef.max) {
+			throw new Error('min > max for option '+name);
+		}
+		
+		var parse, toStr;
+		if (optdef.type == 'integer') {
+			parse = function (s) {
+				var i = NaN;
+				if (s.indexOf('.') == -1) {
+					i = parseInt(s, optdef.base)
+				}
+				return i;
+			};
+			if (optdef.base === undefined) {
+				toStr = dec;
+			}
+			else {
+				switch (optdef.base) {
+					case  8: toStr = oct; break;
+					case 10: toStr = dec; break;
+					case 16: toStr = hex; break;
+					default: toStr = function (val) {
+							return val.toString(optdef.base);
+						};
+						var detail = this.strings.base+': '+optdef.base;
+						if (optdef.details) {
+							optdef.details.push(detail);
+						}
+						else {
+							optdef.details = [detail];
+						}
+				}
+			}
+		}
+		else {
+			parse = parseFloat;
+			toStr = dec;
+		}
+	
+		if (optdef.metavar === undefined) {
+			if (optdef.min === undefined && optdef.max === undefined) {
+				optdef.metavar = this.strings.metavars[optdef.type];
+			}
+			else if (optdef.min === undefined) {
+				optdef.metavar = '...'+toStr(optdef.max);
+			}
+			else if (optdef.max === undefined) {
+				optdef.metavar = toStr(optdef.min)+'...';
+			}
+			else {
+				optdef.metavar = toStr(optdef.min)+'...'+toStr(optdef.max);
+			}
+		}
+		optdef.parse = function (s) {
+			var n = parse(s);
+					
+			if ((!this.NaN && isNaN(n))
+					|| (optdef.min !== undefined && n < optdef.min)
+					|| (optdef.max !== undefined && n > optdef.max)) {
+				throw new Error('illegal value for option '+name+': '+s);
+			}
+	
+			return n;
+		};
+	}
+};
+
+function initStringify (optdef) {
+	if (optdef.stringify === undefined) {
+		optdef.stringify = STRINGIFIERS[optdef.type];
+	}
+	
+	if (optdef.stringify === undefined) {
+		optdef.stringify = stringifyAny;
+	}
+}
+
+function defaults (target, defaults) {
+	for (name in defaults) {
+		if (target[name] === undefined) {
+			target[name] = defaults[name];
+		}
+	}
+}
+
+function dec (val) {
+	return val.toString();
+}
+
+function oct (val) {
+	return '0'+val.toString(8);
+}
+
+function hex (val) {
+	return '0x'+val.toString(16);
+}
+
+const TRUE_VALUES  = {true:  true, on:  true, 1: true, yes: true};
+const FALSE_VALUES = {false: true, off: true, 0: true, no:  true};
+
+function parseBool (s) {
+	s = s.trim().toLowerCase();
+	if (s in TRUE_VALUES) {
+		return true;
+	}
+	else if (s in FALSE_VALUES) {
+		return false;
+	}
+	else {
+		throw new Error('illegal boolean value: '+s);
+	}
+}
+
+function id (x) {
+	return x;
+}
+
+const PARSERS = {
+	boolean: parseBool,
+	string:  id,
+	object:  JSON.parse
+};
+
+const TYPE_ALIAS = {
+	int:    'integer',
+	number: 'float',
+	bool:   'boolean',
+	str:    'string',
+	obj:    'object'
+};
+
+const METAVARS = {
+	string:  'STRING',
+	integer: 'INTEGER',
+	float:   'FLOAT',
+	boolean: 'BOOLEAN',
+	object:  'OBJECT',
+	enum:    'VALUE',
+	custom:  'VALUE'
+};
+
+function stringifyString(s) {
+	if (/[\s'"\\<>,]/.test(s)) {
+//		s = "'"+s.replace(/\\/g,'\\\\').replace(/'/g, "'\\''")+"'";
+		s = JSON.stringify(s);
+	}
+	return s;
+}
+
+function stringifyPrimitive(value) {
+	return ''+value;
+}
+
+function stringifyAny (value) {
+	if (value instanceof Array) {
+		var buf = [];
+		for (i in value) {
+			buf.push(stringifyAny(value[i]));
+		}
+		return buf.join(' ');
+	}
+	else if (typeof(value) == 'string') {
+		return stringifyString(value);
+	}
+	else {
+		return String(value);
+	}
+}
+
+function stringifyInteger (value) {
+	if (this.base === undefined) {
+		return value.toString();
+	}
+
+	switch (this.base) {
+		case  8: return oct(value);
+		case 16: return hex(value);
+		default: return value.toString(this.base);
+	}
+}
+
+function stringifyRecord (record) {
+	var buf = [];
+	for (var i = 0; i < this.args.length; ++ i) {
+		buf.push(this.args[i].stringify(record[i]));
+	}
+	return buf.join(' ');
+}
+
+const STRINGIFIERS = {
+	string:          stringifyString,
+	integer:         stringifyInteger,
+	boolean:         stringifyPrimitive,
+	float:           stringifyPrimitive,
+	object:          JSON.stringify,
+	enum:            stringifyAny,
+	custom:          stringifyAny,
+	record:          stringifyRecord
+};
+
+exports.OptionParser = OptionParser;
+
+// Example usage:
+if (module.parent === undefined) {(function () {
+	var parser = new OptionParser({
+		minargs: 1,
+		maxargs: 10,
+		strings: { help: 'N/A', metavars: { integer: 'INT' } },
+		options: [
+			{
+				names: ['--help', '-h'],
+				type: 'flag',
+				help: 'Show this help message.',
+				onOption: function (value) {
+					if (value) {
+						parser.usage('This is a help text. (Could also be credits etc.)');
+					}
+					// returning true canceles any further option parsing
+					// and parser.parse() returns null
+					return value;
+				}
+			}
+		]
+	});
+
+	parser.add(['--foo', '-f'], {
+		type: 'flag',
+		help: 'This is a flag.',
+		details: ['bla','blub']
+	});
+	
+	parser.add('--bar', {
+		type: 'flag',
+		value: false,
+		target: 'foo',
+		help: 'This is the inverse of --foo.'
+	});
+
+	parser.add('-s');
+	parser.add('--s2', {default: 'blub'});
+	parser.add('-i', {type: 'int', required: true, redefinable: false});
+	parser.add('--int32', {type: 'integer',
+		min: -2147483648, max: 2147483647, metavar: 'INT32'});
+	parser.add('--hex', {type: 'integer', base: 16});
+	parser.add(['-n','-d','--float'], {type: 'float', NaN: true});
+	parser.add('-o', {type: 'object', default: {foo: 'bar', egg: null, spam: true}});
+	parser.add('-b', {type: 'boolean', default: true});
+	parser.add('-x', {type: 'flag', redefinable: false});
+	parser.add('--r1', {type: 'integer', default: 1, min: 0, base: 8});
+	parser.add('--r2', {type: 'integer', default: 3, max: 20, base: 16});
+	parser.add('--r3', {type: 'integer', default: 4, min: -10, max: 10, base: 2});
+	parser.add('--egg', {type: 'option', default: 'none'});
+	parser.add('--spam', {type: 'option', target: 'egg'});
+	parser.add('--tomato', {type: 'option', target: 'egg', value: 'bla'});
+	parser.add('--sel1', {type: 'enum', values: {foo: 42, bar: 23}});
+	parser.add('--sel2', {type: 'enum', values: ['egg', 'spam spam'], default: ' '});
+	parser.add('--foo-bar', {default: 'bla "bla" \t\n \'bla\' \\'});
+	parser.add('--EGG--SPAM');
+	parser.add('--b64', {type: 'custom', argc: 1, metavar: 'BASE64-STRING',
+		parse: function (s) { return new Buffer(s, 'base64'); },
+		stringify: function (buffer) { return buffer.toString('base64'); },
+		help: 'Pass binary data as a base64 encoded string.'});
+	parser.add('-t', {type: 'record',
+		default: [11., 5, 'foo'],
+		args: [
+			{type: 'float'},
+			{type: 'integer', min: 1, max: 22},
+			{type: 'string'}
+		]});
+	parser.add(['--record','-T'], {type: 'record',
+		default: ['bla', [10, 'bleh']],
+		args: [
+			{type: 'string'},
+			{type: 'record', args: [
+				{type: 'integer'},
+				{type: 'string'},
+			]},
+		],
+		onOption: function (str, rec) {
+			console.log('args: '+JSON.stringify(str)+', '+JSON.stringify(rec));
+		}});
+
+	var data = parser.parse();
+
+	if (data) {
+		console.log('Options:');
+		var displayed = {};
+		for (name in parser.options) {
+			var opt = parser.options[name];
+			if (!(opt.target in displayed)) {
+				var value = data.options[opt.target];
+				console.log('\t'+opt.target+': '+ (value === undefined ?
+					value : opt.stringify(value)));
+				displayed[opt.target] = true;
+			}
+		}
+		console.log(''); 
+		console.log('Arguments:');
+		for (i in data.arguments) {
+			console.log('\t'+data.arguments[i]);
+		}
+	}
+})();}

File string-format.js

+/**
+string formatting for JavaScript 0.6
+
+Copyright (c) Alexandru Marasteanu <alexaholic [at) gmail (dot] com>
+Copyright (c) Mathias Panzenböck <grosser (dot] meister (dot] morit (at] gmx [dot) net>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of sprintf() for JavaScript nor the
+      names of its contributors may be used to endorse or promote products
+      derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL Alexandru Marasteanu BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+sprintf Changelog:
+2007.04.03 - 0.1:
+ - initial release
+2007.09.11 - 0.2:
+ - feature: added argument swapping
+2007.09.17 - 0.3:
+ - bug fix: no longer throws exception on empty paramenters (Hans Pufal)
+2007.10.21 - 0.4:
+ - unit test and patch (David Baird)
+2010.05.09 - 0.5:
+ - bug fix: 0 is now preceeded with a + sign
+ - bug fix: the sign was not at the right position on padded results (Kamal Abdali)
+ - switched from GPL to BSD license
+2010.05.22 - 0.6:
+ - reverted to 0.4 and fixed the bug regarding the sign of the number 0
+ Note:
+ Thanks to Raphael Pigulla <raph (at] n3rd [dot) org> (http://www.n3rd.org/)
+ who warned me about a bug in 0.5, I discovered that the last update was
+ a regress. I appologize for that.
+
+strfmt Changelog:
+2010.08.15 - 0.1
+ - changed to more python (str.format) like syntx
+ - added r type which uses JSON.stringify
+ - made into CommonJS module
+**/
+
+const repeat = exports.repeat = function repeat (s, n) {
+	for (var o = []; n > 0; o[--n] = s);
+	return o.join('');
+};
+
+exports.format = function format (f, args) {
+	var i = 0, a, o = [], m, p, c, x, s = '';
+	while (f) {
+		if (m = /^[^{}]+/.exec(f)) {
+			o.push(m[0]);
+		}
+		else if (m = /^{{/.exec(f)) {
+			o.push('{');
+		}
+		else if (m = /^}}/.exec(f)) {
+			o.push('}');
+		}
+		else if (m = /^{([^!}]+)?(?:!(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxXr]))?}/.exec(f)) {
+			var aname = m[1] || i++;
+			if ((a = args[aname]) === undefined) {
+				throw new Error('Argument missing: '+aname);
+			}
+			var t = m[7] || 'r';
+			if (/[^sr]/.test(t) && (typeof(a) != 'number')) {
+				throw new Error('Expecting number but found ' + typeof(a));
+			}
+			switch (t) {
+				case 'b': a = a.toString(2); break;
+				case 'c': a = String.fromCharCode(a); break;
+				case 'd': a = parseInt(a); break;
+				case 'e': a = m[6] ? a.toExponential(m[6]) : a.toExponential(); break;
+				case 'f': a = m[6] ? parseFloat(a).toFixed(m[6]) : parseFloat(a); break;
+				case 'o': a = a.toString(8); break;
+				case 's': a = ((a = String(a)) && m[6] ? a.substring(0, m[6]) : a); break;
+				case 'u': a = Math.abs(a); break;
+				case 'x': a = a.toString(16); break;
+				case 'X': a = a.toString(16).toUpperCase(); break;
+				case 'r': a = JSON.stringify(a); break;
+			}
+			a = (/[def]/.test(t) && m[2] && a >= 0 ? '+'+ a : a);
+			c = m[3] ? m[3] == '0' ? '0' : m[3].charAt(1) : ' ';
+			x = m[5] - String(a).length - s.length;
+			p = m[5] ? repeat(c, x) : '';
+			o.push(s + (m[4] ? a + p : p + a));
+		}
+		else {
+			throw new Error('Invalid format string: '+f);
+		}
+		f = f.substring(m[0].length);
+	}
+	return o.join('');
+};

File xml-markup.js

+function XmlMarkup (stream, encoding, standalone) {
+	this._stream = stream;
+	this._open_tag = null;
+	this._prefixes = {xml: 'xml'};
+	this._unflushed_nsdecls = {};
+
+	if (encoding === undefined) {
+		this.encoding = 'UTF-8';
+	}
+	else if (encoding.indexOf('"') != -1) {
+		throw new Error('illegal encoding: '+encoding);
+	}
+	else {
+		this.encoding = encoding;
+	}
+	var content = 'version="1.0" encoding="'+this.encoding+'"';
+	if (standalone !== undefined) {
+		content += ' standalone="' + (standalone ? 'yes"' : 'no"');
+	}
+	this.pinst('xml', content);
+}
+
+XmlMarkup.prototype = {
+	_finish_tag: function () {
+		if (this._open_tag && !this._open_tag.finished) {
+			this._stream.write('>');
+			this._open_tag.finished = true;
+			this._open_tag = null;
+		}
+	},
+	doctype: function (name, systemId, publicId) {
+		if (!isXmlName(name)) {
+			throw new Error('illegal name: '+name);
+		}
+		if (systemId.indexOf('"') != -1) {
+			if (systemId.indexOf("'") != -1) {
+				throw new Error('illegal systemId: '+systemId);
+			}
+			systemId = "'"+systemId+"'";
+		}
+		else {
+			systemId = '"'+systemId+'"';
+		}
+		this._finish_tag();
+		if (publicId === undefined) {
+			this._stream.write('<!DOCTYPE SYSTEM '+systemId+'>');
+		}
+		else if (/[^-'()+,\./:=?;!*#@\$_% \r\na-zA-Z0-9]/.test(publicId)) {
+			throw new Error('illegal publicId: '+publicId);
+		}
+		else {
+			this._stream.write('<!DOCTYPE PUBLIC "'+publicId+'" '+systemId+'>');
+		}
+	},
+	pinst: function (target, content) {
+		if (!isXmlName(target)) {
+			throw new Error('illegal target: '+target);
+		}
+		if (content.indexOf('?>') != -1) {
+			throw new Error('illegal content: '+content);
+		}
+		this._finish_tag();
+		this._stream.write('<?'+target+' '+content+'?>');
+	},
+	text: function (text) {
+		this._finish_tag();
+		this._stream.write(escapeXml(text));
+	},
+	cdata: function (cdata) {
+		if (text.indexOf(']]>') != -1) {
+			throw new Error('illegal cdata: '+cdata);
+		}
+		this._finish_tag();
+		this._stream.write('<[CDATA[');
+		this._stream.write(cdata);
+		this._stream.write(']]>');
+	},
+	space: function (space) {
+		if (/[^\s]/.test(space)) {
+			throw new Error('illegal space argument: '+space);
+		}
+		this._finish_tag();
+		this._stream.write(space);
+	},
+	comment: function (comment) {
+		if (comment.indexOf('--') != -1) {
+			throw new Error('illegal comment argument: '+comment);
+		}
+		this._finish_tag();
+		this._stream.write('<!-- '+comment+' -->');
+	},
+	ns: function (prefixes, closure) {
+		var oldPrefixes = this._prefixes;
+		var newPrefixes = {};
+		var oldUnflushed = this._unflushed_nsdecls;
+		var newUnflushed = {};
+
+		for (uri in oldUnflushed) {
+			newUnflushed[uri] = oldUnflushed[uri];
+		}
+
+		for (uri in oldPrefixes) {
+			newPrefixes[uri] = oldPrefixes[uri];
+		}
+
+		var uris = {};
+		for (prefix in prefixes) {
+			var uri = prefixes[prefix];
+			if (uri in uris) {
+				throw new Error('redeclared namespace uri: '+uri);
+			}
+			if ((prefix.length > 0 && !isXmlNameNS(prefix)) || prefix.toLowerCase() == 'xml') {
+				throw new Error('illegal prefix name: '+prefix);
+			}
+			uris[uri] = true;
+			newPrefixes[uri] = prefix;
+			newUnflushed[uri] = prefix;
+		}
+
+		this._prefixes = newPrefixes;
+		this._unflushed_nsdecls = newUnflushed;
+		closure.call(this);
+		this._prefixes = oldPrefixes;
+		this._unflushed_nsdecls = oldUnflushed;
+	},
+	makePrefix: function (uri) {
+		var prefix = this._prefixes[uri];
+
+		if (prefix === undefined) {
+			var prefixes = {};
+			
+			for (knownuri in this._prefixes) {
+				prefixes[this._prefixes[knownuri]] = true;
+			}
+			
+			for (var i = 0;; ++ i) {
+				prefix = '_'+i;
+				if (!(prefix in prefixes)) {
+					break;
+				}
+			}
+			this._prefixes[uri] = prefix;
+
+			return {prefix: prefix, isNew: true};
+		}
+		else {
+			return {prefix: prefix, isNew: false};
+		}
+	},
+	$: function (name, attrs, content) {
+		if (attrs instanceof Function || typeof(attrs) == 'string') {
+			if (content !== undefined) {
+				throw new Error('illegal attrs argument: '+attrs);
+			}
+			content = attrs;
+			attrs = {};
+		}
+		else if (attrs === undefined) {
+			attrs = {};
+		}
+
+		if (!isXmlNameNS(name)) {
+			throw new Error('illegal name argument: '+name);
+		}
+
+		var oldPrefixes = this._prefixes;
+		var newPrefixes = {};
+		var unflushed = this._unflushed_nsdecls;
+
+		this._prefixes = newPrefixes;
+		this._unflushed_nsdecls = {};
+
+		var sattrs = {};
+		var elemPrefix = null;
+		
+		for (uri in oldPrefixes) {
+			newPrefixes[uri] = oldPrefixes[uri];
+		}
+		
+		for (uri in unflushed) {
+			putXmlns(sattrs, unflushed[uri], uri);
+		}
+
+		for (attrname in attrs) {
+			var attr = attrs[attrname];
+			
+			if (!isXmlNameNS(attrname)) {
+				throw new Error('illegal attribute name: '+attrname);
+			}
+
+			if (attrname == 'xmlns') {
+				elemPrefix = this.makePrefix(attr);
+
+				if (elemPrefix.isNew) {
+					putXmlns(sattrs, elemPrefix.prefix, attr);
+				}
+				elemPrefix = elemPrefix.prefix;
+			}
+			else if (attr instanceof Object) {
+				var prefix = this.makePrefix(attr.uri);
+
+				if (prefix.isNew) {
+					putXmlns(sattrs, prefix.prefix, attr.uri);
+				}
+
+				if (prefix.prefix) {
+					sattrs[prefix.prefix+':'+attrname] = attr.value;
+				}
+				else {
+					sattrs[attrname] = attr.value;
+				}
+			}
+			else {
+				sattrs[attrname] = attr;
+			}
+		}
+
+		this._elem(elemPrefix, name, sattrs, content);
+		
+		this._prefixes = oldPrefixes;
+		this._unflushed_nsdecls = unflushed;
+	},
+	_elem: function (prefix, name, attrs, content) {
+		this._finish_tag();
+		this._stream.write('<');
+		if (prefix) {
+			this._stream.write(prefix+':');
+		}
+		this._stream.write(name);
+
+		for (attrname in attrs) {
+			this._stream.write(' '+attrname+'="'+escapeXml(attrs[attrname])+'"');
+		}
+		
+		if (content === undefined) {
+			this._stream.write('/>');
+		}
+		else {
+			var open_tag = this._open_tag = {finished: false};
+			if (content instanceof Function) {
+				content.call(this);
+			}
+			else {
+				this.text(content);
+			}
+
+			if (!open_tag.finished) {
+				this._stream.write('/>');
+				open_tag.finished = true;
+				this._open_tag = null;
+			}
+			else {
+				this._finish_tag();
+				this._stream.write('</');
+				if (prefix) {
+					this._stream.write(prefix+':');
+				}
+				this._stream.write(name+'>');
+			}
+		}
+	},
+	close: function () {
+		this._stream.close();
+	},
+	// not supported on V8 or JavaScript Core:
+	__noSuchMethod__: function (id, args) {
+		this.elem.apply(this, [id].concat(args));
+	}
+};
+
+function PrettyXmlMarkup (stream, encoding, standalone, indent) {
+	if (indent === undefined) {
+		indent = '\t';
+	}
+	else if (/[^\w]/.test(indent)) {
+		throw new Error('illegal indent: '+indent);
+	}
+	this.indent  = indent;
+	this._indent = '';
+	XmlMarkup.call(this, stream, encoding, standalone);
+}
+
+PrettyXmlMarkup.prototype = {
+	space:            XmlMarkup.prototype.space,
+	ns:               XmlMarkup.prototype.ns,
+	makePrefix:       XmlMarkup.prototype.makePrefix,
+	$:                XmlMarkup.prototype.$,
+	close:            XmlMarkup.prototype.close,
+	__noSuchMethod__: XmlMarkup.prototype.__noSuchMethod__,
+	
+	_doctype:         XmlMarkup.prototype.doctype,
+	_pinst:           XmlMarkup.prototype.pinst,
+	_text:            XmlMarkup.prototype.text,
+	_cdata:           XmlMarkup.prototype.cdata,
+	_comment:         XmlMarkup.prototype.comment,
+
+	_finish_tag: function () {
+		if (this._open_tag && !this._open_tag.finished) {
+			this._stream.write('>\n');
+			this._open_tag.finished = true;
+			this._open_tag = null;
+		}
+	},
+	doctype: function (name, systemId, publicId) {
+		this._doctype(name, systemId, publicId);
+		this._stream.write('\n');
+	},
+	pinst: function (target, content) {
+		this._pinst(target, content);
+		this._stream.write('\n');
+	},
+	text: function (text) {
+		this._text(this._indent+text.split('\n').join('\n'+this._indent));
+		this._stream.write('\n');
+	},
+	cdata: function (cdata) {
+		this._cdata(cdata);
+		this._stream.write('\n');
+	},
+	comment: function (comment) {
+		this._comment(comment);
+		this._stream.write('\n');
+	},
+	_elem: function (prefix, name, attrs, content) {
+		var oldIndent = this._indent;
+		var hasAttrs = !isEmpty(attrs);
+		this._finish_tag();
+		this._stream.write(oldIndent+'<');
+		if (prefix) {
+			this._stream.write(prefix+':');
+		}
+		this._stream.write(name);
+
+		if (hasAttrs) {
+			var count = 0;
+			var multi = false;
+			for (attrname in attrs) {
+				count += 1;
+				if (count >= 2) {
+					multi = true;
+					break;
+				}
+			}
+			
+			var attrsep;
+			if (multi) {
+				attrsep = '\n' + oldIndent + this.indent;
+			}
+			else {
+				attrsep = ' ';
+			}
+
+			for (attrname in attrs) {
+				this._stream.write(attrsep+attrname+'="'+escapeXml(attrs[attrname])+'"');
+			}
+		}
+		
+		if (content === undefined) {
+			this._stream.write('/>\n');
+		}
+		else {
+			if (content instanceof Function) {
+				var open_tag = this._open_tag = {finished: false};
+
+				this._indent += this.indent;
+				content.call(this);
+				this._indent = oldIndent;
+				
+				if (!open_tag.finished) {
+					this._stream.write('/>\n');
+					open_tag.finished = true;
+					this._open_tag = null;
+				}
+				else {
+					this._finish_tag();
+					this._stream.write(oldIndent+'</');
+					if (prefix) {
+						this._stream.write(prefix+':');
+					}
+					this._stream.write(name+'>\n');
+				}
+			}
+			else {
+				this._stream.write('>'+escapeXml(content)+'</');
+				if (prefix) {
+					this._stream.write(prefix+':');
+				}
+				this._stream.write(name+'>\n');
+			}
+		}
+	}
+};
+
+function putXmlns (attrs, prefix, uri) {
+	if (prefix) {
+		attrs['xmlns:'+prefix] = uri;
+	}
+	else {
+		attrs.xmlns = uri;
+	}
+}
+
+function isEmpty (obj) {
+	for (name in obj) {
+		return false;
+	}
+	return true;
+}
+
+function escapeXml (s) {
+	return s
+		.replace(/&/g, '&amp;')
+		.replace(/</g, '&lt;')
+		.replace(/>/g, '&gt;')
+		.replace(/"/g, '&quot;')
+		.replace(/'/g, '&apos;');
+}
+
+function isXmlNameNS (s) {
+	// xml names without ':'
+	// http://www.w3.org/TR/REC-xml/#sec-common-syn
+	var c;
+	if (!s || !((c = s.charCodeAt(0)) == 0x5f ||
+			(c >= 0x41 && c <= 0x5a) ||
+			(c >= 0x61 && c <= 0x7a) ||
+			(c >= 0xC0 && c <= 0xD6) ||
+			(c >= 0xD8 && c <= 0xF6) ||
+			(c >= 0xF8 && c <= 0x2FF) ||
+			(c >= 0x370 && c <= 0x37D) ||
+			(c >= 0x37F && c <= 0x1FFF) ||
+			(c >= 0x200C && c <= 0x200D) ||
+			(c >= 0x2070 && c <= 0x218F) ||
+			(c >= 0x2C00 && c <= 0x2FEF) ||
+			(c >= 0x3001 && c <= 0xD7FF) ||
+			(c >= 0xF900 && c <= 0xFDCF) ||
+			(c >= 0xFDF0 && c <= 0xFFFD) ||
+			(c >= 0x10000 && c <= 0xEFFFF))) {
+		return false;
+	}
+
+	for (var i = 1; i < s.length; ++ i) {
+		c = s.charCodeAt(i);
+
+		if (!(
+				c == 0x5f ||
+				(c >= 0x41 && c <= 0x5a) ||
+				(c >= 0x61 && c <= 0x7a) ||
+				(c >= 0xC0 && c <= 0xD6) ||
+				(c >= 0xD8 && c <= 0xF6) ||
+				(c >= 0x30 && c <= 0x39) ||
+				c == 0x2d ||
+				c == 0x2e ||
+				c == 0xb7 ||
+				(c >= 0xF8 && c <= 0x2FF) ||
+				(c >= 0x370 && c <= 0x37D) ||
+				(c >= 0x37F && c <= 0x1FFF) ||
+				(c >= 0x200C && c <= 0x200D) ||
+				(c >= 0x2070 && c <= 0x218F) ||
+				(c >= 0x2C00 && c <= 0x2FEF) ||
+				(c >= 0x3001 && c <= 0xD7FF) ||
+				(c >= 0xF900 && c <= 0xFDCF) ||
+				(c >= 0xFDF0 && c <= 0xFFFD) ||
+				(c >= 0x10000 && c <= 0xEFFFF) ||
+				(c >= 0x0300 && c <= 0x036F) ||
+				(c >= 0x203F && c <= 0x2040))) {
+			return false;
+		}	
+	}
+
+	return true;
+}
+
+function isXmlName (s) {
+	// http://www.w3.org/TR/REC-xml/#sec-common-syn
+	var c;
+	if (!s || !((c = s.charCodeAt(0)) == 0x5f ||
+			c == 0x3a ||
+			(c >= 0x41 && c <= 0x5a) ||
+			(c >= 0x61 && c <= 0x7a) ||
+			(c >= 0xC0 && c <= 0xD6) ||
+			(c >= 0xD8 && c <= 0xF6) ||
+			(c >= 0xF8 && c <= 0x2FF) ||
+			(c >= 0x370 && c <= 0x37D) ||
+			(c >= 0x37F && c <= 0x1FFF) ||
+			(c >= 0x200C && c <= 0x200D) ||
+			(c >= 0x2070 && c <= 0x218F) ||
+			(c >= 0x2C00 && c <= 0x2FEF) ||
+			(c >= 0x3001 && c <= 0xD7FF) ||
+			(c >= 0xF900 && c <= 0xFDCF) ||
+			(c >= 0xFDF0 && c <= 0xFFFD) ||
+			(c >= 0x10000 && c <= 0xEFFFF))) {
+		return false;
+	}
+
+	for (var i = 1; i < s.length; ++ i) {
+		c = s.charCodeAt(i);
+
+		if (!(
+				c == 0x5f ||
+				(c >= 0x41 && c <= 0x5a) ||
+				(c >= 0x61 && c <= 0x7a) ||
+				(c >= 0xC0 && c <= 0xD6) ||
+				(c >= 0xD8 && c <= 0xF6) ||
+				(c >= 0x30 && c <= 0x39) ||
+				c == 0x2d ||
+				c == 0x2e ||
+				c == 0x3a ||
+				c == 0xb7 ||
+				(c >= 0xF8 && c <= 0x2FF) ||
+				(c >= 0x370 && c <= 0x37D) ||
+				(c >= 0x37F && c <= 0x1FFF) ||
+				(c >= 0x200C && c <= 0x200D) ||
+				(c >= 0x2070 && c <= 0x218F) ||
+				(c >= 0x2C00 && c <= 0x2FEF) ||
+				(c >= 0x3001 && c <= 0xD7FF) ||
+				(c >= 0xF900 && c <= 0xFDCF) ||
+				(c >= 0xFDF0 && c <= 0xFFFD) ||
+				(c >= 0x10000 && c <= 0xEFFFF) ||
+				(c >= 0x0300 && c <= 0x036F) ||
+				(c >= 0x203F && c <= 0x2040))) {
+			return false;
+		}	
+	}
+
+	return true;
+}
+
+exports.XmlMarkup   = XmlMarkup;
+exports.escapeXml   = escapeXml;
+exports.isXmlName   = isXmlName;
+exports.isXmlNameNS = isXmlNameNS;
+
+if (module.parent === undefined) {(function () {
+	var xm  = new XmlMarkup(process.stdout);
+	var ex  = 'http://example.com/';
+	var ex1 = 'http://example.com/1';
+	var ex2 = 'http://example.com/2';
+
+	xm.space('\n');
+	xm.doctype('doc', 'baz', '-//foo bar//-');
+	xm.space('\n');
+	xm.comment('foo bar');
+	xm.space('\n');
+	xm.$('doc', {'lang': {uri: 'xml', value: 'en_GB'}}, function () {
+		xm.space('\n\t');
+		xm.$('e0', {
+			att1: 'value',
+			att2: '"<val> "\' &',
+			att3: {uri: ex, value: 'foo'},
+			att4: ''});
+		xm.space('\n\t');
+		xm.$('e1');
+		xm.space('\n\t');
+		xm.$('e2', {xmlns: ex}, function () {
+			xm.space('\n\t\t');
+			xm.$('e3', {xmlns: ex});
+			xm.space('\n\t');
+		});
+		xm.space('\n\t');
+		xm.ns({ex: ex, ex1: ex1, '': ex2}, function () {
+			xm.$('e4');
+			xm.space('\n\t');
+			xm.$('e5', {xmlns: ex});
+			xm.space('\n\t');
+			xm.$('e6', {xmlns: ex1}, function () {
+				xm.$('e7', {attr1: {uri: ex1, value: 'bla'}});
+			});
+			xm.space('\n\t');
+			xm.$('e8', function () {
+				xm.space('\n\t\t');
+				xm.$('e9', {xmlns: ex2});
+				xm.space('\n\t');
+			});
+			xm.space('\n\t');
+		});
+		xm.$('e10', 'foo bar');
+		xm.space('\n\t');
+		xm.text('<foo> "bar" baz\'s; egg bacon & spam\nbla\nbla');
+		xm.space('\n');
+	});
+	xm.space('\n');
+	xm.space('\n');
+
+	var xm  = new PrettyXmlMarkup(process.stdout);
+	xm.doctype('doc', 'baz', '-//foo bar//-');
+	xm.comment('foo bar');
+	xm.$('doc', {'lang': {uri: 'xml', value: 'en_GB'}}, function () {
+		xm.$('e0', {
+			att1: 'value',
+			att2: '"<val> "\' &',
+			att3: {uri: ex, value: 'foo'},
+			att4: ''});
+		xm.$('e1');
+		xm.$('e2', {xmlns: ex}, function () {
+			xm.$('e3', {xmlns: ex});
+			xm.$('e3', {xmlns: ex1});
+		});
+		xm.ns({ex: ex, ex1: ex1, '': ex2}, function () {
+			xm.$('e4');
+			xm.$('e5', {xmlns: ex});
+			xm.$('e6', {xmlns: ex1}, function () {
+				xm.$('e7', {attr1: {uri: ex1, value: 'bla'}});
+			});
+			xm.$('e8', function () {
+				xm.$('e9', {xmlns: ex2});
+			});
+		});
+		xm.$('e10', 'foo bar');
+		xm.text('<foo> "bar" baz\'s; egg bacon & spam\nbla\nbla');
+	});
+})()}