tcl.js

RS brought this Tcl in Javascript here from http://sarnold.free.fr/pub/jstcl/tcl.js - for a later version, see tcl.js 0.2, tcl.js 0.3, tcl.js 0.4

See also


 /*====================================================
 * tcl.js "A Tcl implementation in Javascript"
 *
 * Released under the same terms as Tcl itself.
 * (BSD license found at https://www.tcl-lang.org/software/tcltk/license.html)
 *
 * Based on Picol by Salvatore Sanfilippo (http://antirez.com/page/picol)
 * (c) Stéphane Arnold 2007
 * vim: syntax=javascript autoindent softtabwidth=4
 */

 function TclInterp () {
    this.callframe = new Array(new Object());
    this.level = 0;
    this.commands = new Object();
    this.procs = new Array();

    this.OK = 0;
    this.RET = 1;
    this.BRK = 2;
    this.CNT = 3;

    this.getVar = function(name) {
        var val = this.callframe[this.level][name];
        if (val == null)
            throw "No such variable: "+name;
        return val;
    }
    this.hasVar = function(name) {
        return (this.callframe[this.level][name] != null);
    }
    this.getObjVar = function (name) {
                return this.objectify(this.getVar(name));
    }
    this.setObjVar = function (name, val) {
                return this.setVar(name, this.objectify(val));
    }
    this.setVar = function(name, val) {
        this.callframe[this.level][name] = val;
        return val;
    }
    this.incrLevel = function() {
        this.callframe[++this.level] = new Object();
        return this.level;
    }
    this.decrLevel = function() {
                this.callframe[this.level] = null;
                this.level--;
                if (this.level<0)
                        throw "Exit application";
                var r = this.result;
                this.result = null;
                return r;
    }        
    this.getCommand = function(name) {
                try {
                        return this.commands[name];
                } catch (e) {
                        throw "No such command '"+name+"'";
                }
    }
    this.registerCommand = function(name, func, privdata) {
            if (func == null)
            {
                    throw "No such function: "+name;
            }        
        this.commands[name] = new TclCommand(func, privdata);
    }
    this.registerEasyCommand = function(name, func) {
        if (func == null)
            {
                    throw "No such function: "+name;
            }

        this.commands[name] = new TclCommand(func, null);
    }
    this.registerProc = function (name, privdata) {
                this.commands[name] = new TclCommand(Tcl.Proc, privdata);
                this.procs[name] = true;
    }
    this.renameCommand = function (name, newname) {
                this.commands[newname] = this.commands[name];
                if (this.procs[name]) {
                    this.procs[name] = null;
                    this.procs[newname] = true;
                }
                this.commands[name] = null;
    }
    this.setCode = function (code) {
                this.code = code;
    }
    this.getCode = function () {return this.code;}

    this.refreshCode = function () {this.code = this.OK;}
    this.registerSubCommand = function(name, subcmd, func, privdata) {
            if (func == null)
            {
                    throw "No such subcommand: "+ name +" " + subcmd;
            }        
            var path = name.split(" ");
            var ens;
            name = path.shift();
            var cmd = this.commands[name];
                if (cmd == null) {
                        ens = new Object();
                        ens["subcommands"] = new TclCommand(Tcl.InfoSubcommands, null);
                    this.commands[name] = new TclCommand(Tcl.EnsembleCommand, null, ens);
                }
                ens = this.commands[name].getEnsemble();
                if (ens == null)
                        throw "Not an ensemble command: '"+name+"'";
                // walks deeply into the subcommands tree
                while (path.length>0) {
                        name = path.shift();
                        cmd = ens[name];
                        if (cmd == null) {
                                cmd = new TclCommand(Tcl.EnsembleCommand, null, new Object());
                                ens[name] = cmd;
                                ens = cmd.getEnsemble();
                                ens["subcommands"] = new TclCommand(Tcl.InfoSubcommands, null);
                        }
                }
                //alert(ens);
                ens[subcmd] = new TclCommand(func, privdata);
    }
    this.registerEasySubCommand = function (name, subcmd, func) {
        this.registerSubCommand(name, subcmd, func, null);
    }        
    this.registerEasyCommand("eval",function (interp, args) {
                this.requireMinArgc(args, 2);
                var code;
                for (var i = 1; i < args.length; i++) {
                    args[i] = args[i].toString();
                }
                if (args.length == 2)
                    code = args[1];
                else
                    code = args.slice(1).join(" ");
                return interp.eval(code);
    });
    this.registerEasyCommand("if", function (interp, args) {
        this.requireMinArgc(args, 3);
        var test = interp.objectify(interp.eval("set _ "+args[1].toString()));
        if (test.toBoolean())
            return interp.eval(args[2].toString());
        if (args.length == 3)
            return;
        for (var i = 3; i < args.length; ) {
            switch (args[i].toString()) {
                case "else":
                    this.requireExactArgc(args, i + 2);
                    return interp.eval(args[i+1].toString());
                case "elseif":
                    this.requireMinArgc(args, i + 3);
                    test = interp.objectify(interp.eval("set _ "+args[i+1].toString()));
                    if (test.toBoolean())
                        return interp.eval(args[i+2].toString());
                    i += 3;
                    break;
                default:
                    throw "Expected 'else' or 'elseif', got "+ args[i];
            }
        }
        // unreached
    });
    this.registerEasyCommand("incr", function (interp, args) {
        this.requireArgcRange(args, 2, 3);
        var name = args[1].toString();
        if (args.length == 2)
            var incr = 1;
        else
            var incr = interp.objectify(args[2]).toInteger();
        incr += interp.getObjVar(name).toInteger();
        return interp.setVar(name, new TclObject(incr, "INTEGER"));
    });
    this.registerEasyCommand("puts", function (interp, args) {
                this.requireExactArgc(args, 2);
                alert(args[1]);
    });
    this.registerEasyCommand("proc", function (interp, args) {
                this.requireExactArgc(args, 4);
                var name = args[1].toString();
                var arglist = interp.parseList(args[2]);
                var body = args[3].toString();
                var priv = new Array();
                priv.push(arglist);
                priv.push(body);
                interp.registerProc(name, priv);
    });

    this.registerEasyCommand("set", function (interp, args) {
                this.requireArgcRange(args, 2, 3);
                var name = args[1];
                if (args.length == 3)
                        interp.setVar(name, args[2]);
                return interp.getVar(name);
    });

    this.registerEasyCommand("unset", function (interp, args) {
                this.requireExactArgc(args, 2);
                        interp.setVar(args[1], null);
    });

    this.math = function (name, a, b) {
                switch (name) {
                        case "+": return a + b;
                        case "-": return a - b;
                        case "*": return a * b;
                        case "/": return a / b;
                        case "%": return a % b;
                        default: throw "Unknown operator: '"+name+"'";
                }
    }
    for (var maths= "+-*/%"; maths != ""; maths=maths.substring(1))
        this.registerEasyCommand(maths.charAt(0),function (interp, args) {
            this.requireExactArgc(args, 3);
            var name = args[0].toString();
            var a = interp.objectify(args[1]);
            var b = interp.objectify(args[2]);
            var x = a.getNumber();
            var y = b.getNumber();
            if (a.isInteger() && b.isInteger())
                return new TclObject(interp.math(name, x, y),"INTEGER");
            if (a.isReal() && b.isReal())
                return new TclObject(interp.math(name, x, y),"REAL");
            return new TclObject(interp.math(name, x, y).toString());
        });
    this.registerEasyCommand("=", function (interp, args) {
        this.requireExactArgc(args, 3);
        var a = args[1].getNumber();
        var b = args[2].getNumber();
        return (a == b);
    });
    this.registerEasyCommand("!=", function (interp, args) {
        this.requireExactArgc(args, 3);
        var a = args[1].getNumber();
        var b = args[2].getNumber();
        return (a != b);
    });
    this.registerEasyCommand("and", function (interp, args) {
        this.requireExactArgc(args, 3);
        var a = interp.objectify(args[1]).toBoolean();
        var b = interp.objectify(args[2]).toBoolean();
        return (a && b);
    });
    this.registerEasyCommand("or", function (interp, args) {
        this.requireExactArgc(args, 3);
        var a = interp.objectify(args[1]).toBoolean();
        var b = interp.objectify(args[2]).toBoolean();
        return (a || b);
    });
 //alert('str5');

    this.registerEasyCommand("not", function (interp, args) {
        this.requireExactArgc(args, 2);
        var a = interp.objectify(args[1]).toBoolean();
        return (!a);
    });
    this.registerEasyCommand("lindex", function (interp, args) {
        this.requireMinArgc(args, 3);
        var list = interp.objectify(args[1]);
        var index;
        for (var i = 2; i < args.length; i++) {
            try {
                index = list.listIndex(args[i]);
            } catch (e) {
                if (e == "Index out of bounds")
                    return "";
                throw e;
            }
            list = list.content[index];
        }
        return interp.objectify(list);
    });
 //alert('str4');

    this.registerEasyCommand("llength", function (interp, args) {
        this.requireExactArgc(args, 2);
        return args[1].toList().length;
    });

    this.registerEasyCommand("list", function (interp, args) {
        args.shift();
        /*for (var i in args) {
            args[i] = interp.objectify(args[i]);
        }*/
        return new TclObject(args);
    });

    this.registerEasyCommand("lappend", function (interp, args) {
        this.requireMinArgc(args, 3);
    var vname = args[1].toString();
    if (interp.hasVar(vname)) {
            var list = interp.getVar(vname);
        } else {
        var list = new TclObject([]);
        }
        list.toList();
        for (var i = 2; i < args.length; i++) {
            list.content.push(interp.objectify(args[i]));
        }
        interp.setVar(vname, list);
        return
    });
 //alert('str3');

    this.registerEasyCommand("lset", function (interp, args) {
        this.requireMinArgc(args, 4);
        var list = interp.getVar(args[1].toString());
        var elt= list;
        for (var i = 2; i < args.length-2; i++) {
                elt.toList();
                elt= interp.objectify(elt.content[elt.listIndex(args[i])]);
        }
        elt.toList();
        i = args.length - 2;
        elt.content[elt.listIndex(args[i])] = interp.objectify(args[i+1]);
        return list;
        });
    this.registerEasyCommand("lrange", function (interp, args) {
                this.requireExactArgc(args, 4);
                var list = interp.objectify(args[1]);
                var start = list.listIndex(args[2]);
                var end = list.listIndex(args[3])+1;
                try {
                        return list.content.slice(start, end);
                } catch (e) {
                        return new Array();
                }
    });
    this.registerEasySubCommand("info", "commands", function (interp, args) {
                var list = new Array();
                for (name in interp.commands) {
                        list.push(name);
                }
                return list;
    });
        //alert("str++");
    this.registerEasySubCommand("info", "procs", function (interp, args) {
                var list = new Array();
                for (name in interp.procs) {
                        list.push(name);
                }
                return list;
    });
    this.registerEasySubCommand("info", "body", function (interp, args) {
                this.requireExactArgc(args, 2);
                var name = args[1].toString();
                if (!interp.procs[name])
                        throw "Not a procedure: "+name;
                return interp.getCommand(name).getPrivData()[1];
    });
        this.registerEasySubCommand("info", "isensemble", function (interp, args) {
                this.requireExactArgc(args, 2);
                var name = args[1].toString();
                return interp.getCommand(name).isEnsemble();
    });
    this.registerEasyCommand("rename", function (interp, args) {
        this.requireExactArgc(args, 3);
        interp.renameCommand(args[1], args[2]);
    });
 //alert('str');
    this.registerEasySubCommand("string", "equal", function (interp, args) {
                this.requireExactArgc(args, 3);
                return (args[1].toString() == args[2].toString());
    });
    this.registerEasySubCommand("string", "index", function (interp, args) {
                this.requireExactArgc(args, 3);
                var s = args[1].toString();
                try {
                        return s.charAt(args[1].stringIndex(args[2].toString()));
                } catch (e) {
                        return "";
                }
    });
    this.registerEasySubCommand("string", "range", function (interp, args) {
                this.requireExactArgc(args, 4);
                var s = args[1];
                try {
                        var b = s.stringIndex(args[2].toString());
                        var e = s.stringIndex(args[3].toString());
                        if (b>e)
                        {
                                return "";
                        }                
                        return s.toString().substring(b, e + 1);
                } catch (e) {
                        return "";
                }
    });
    this.registerEasyCommand("return", function (interp, args) {
        this.requireArgcRange(args, 1, 2);
        var r = args[1];
        interp.setCode(interp.RET);
        return r;
    });
    this.registerEasyCommand("source", function (interp, args) {
                this.requireExactArgc(args, 2);
                return Tcl.Source(interp, args[1]);
        });
    this.objectify = function (text) {
    if (text == null)
        text = "";
        if (text instanceof TclObject || text == null) {
 //alert("TYPE"+text.type);
            return text;
    }
 //alert(text+ "OBJ");
        return new TclObject(text);
    }

    this.parseString = function (text) {
                text = text.toString();
                switch (text.charAt(0)+text.substr(text.length-1)) {
                case "{}":
                case "\"\"":
                        text = text.substr(1,text.length-2);
                        break;
            default:
                        break;
                }
                return this.objectify(text);
    }
    this.parseList = function (text) {
                text = text.toString();
                switch (text.charAt(0)+text.substr(text.length-1)) {
                        case "{}":
                        case "\"\"":
        //                    alert(text);
                                text = new Array(text);
                                break;
                        default:
                                break;
                }
                return this.objectify(text);
    }
    this.call = function(args) {
        var func = this.getCommand(args[0].toString());
        var r = func.call(this,args);
        switch (this.getCode()) {
            case this.OK:
            case this.RET:
                return r;
            case this.BRK:
                if (!this.inLoop())
                    throw "Invoked break outside of a loop";
                break;
            case this.CNT:
                if (!this.inLoop())
                    throw "Invoked continue outside of a loop";
                break;
            default:
                throw "Unknown return code "+this.getCode();
        }
        return r;
    }
    this.eval = function (code) {
                try {
                        return this.eval2(code);
                } catch (e) {
                        alert(e);
                }
    }
    this.eval2 = function(code) {
                this.refreshCode();
                var parser = new TclParser(code);
                var args = new Array(0);
                var first = true;
                var text, prevtype, result;
                result = "";
                while (true) {
                    prevtype = parser.getType();
                    try {
                                parser.getToken();
                    } catch (e) {
                                break;
                    }
                    if (parser.isType(parser.EOF))
                                break;
                    text = parser.getText();
                    if (parser.isType(parser.VAR)) {
                                try {
                                    text = this.getVar(text);
                                } catch (e) {
                                    throw "No such variable '"+text+"'";
                                }
                    } else if (parser.isType(parser.CMD)) {
                                try {
                                        //alert("CMD "+text);
                                    text = this.eval2(text);
                                    //alert("/CMD"+text);
                                } catch (e) {
                                    throw new TclException(e, text);
                                }
                    } else if (parser.isType(parser.ESC)) {
                                // escape handling missing!
                    } else if (parser.isType(parser.SEP)) {
                                prevtype = parser.getType();
                                continue;
                    }
                           text = this.objectify(text);
                    if (parser.isType(parser.EOL) || parser.isType(parser.EOF)) {
                                prevtype = parser.getType();
                                if (args.length > 0) {
                                        //alert("CALL:"+args);
                                    result = this.call(args);
                                    if (this.getCode() != this.OK)
                                                return this.objectify(result);
                                }
                                args = new Array();
                                continue;
                    }
                    if (prevtype == parser.SEP || prevtype == parser.EOL) {
                            args.push(text);
                           } else {
                                args[args.length-1] = args[args.length-1].toString() + text.toString();
                    }
                }
                if (args.length > 0) {
                    result = this.call(args);
                }
                return this.objectify(result);
    }
 //alert("eof interp")
 }
 function TclException(e, origin) {
        this.except = e;
        this.origin = origin;
        this.toString = function() {
                return this.except.toString() + ", while running '"
                + this.origin + "'";
        }
 }
 var Tcl = new Object();

 Tcl.isReal = "^[+\\-]?[0-9]+\\.[0-9]*([eE][+\\-]?[0-9]+)?$";
 Tcl.isReal = new RegExp(Tcl.isReal);

 Tcl.isDecimal = "^[+\\-]?[1-9][0-9]*$";
 Tcl.isDecimal = new RegExp(Tcl.isDecimal);

 Tcl.isHexadecimal = "^0x[0-9a-fA-F]+$";
 Tcl.isHexadecimal = new RegExp(Tcl.isHexadecimal);

 Tcl.isOctal = "^[+\\-]?0[0-7]*$";
 Tcl.isOctal = new RegExp(Tcl.isOctal);

 Tcl.isHexSeq = new RegExp("[0-9a-fA-F]*");
 Tcl.isOctalSeq = new RegExp("[0-7]*");

 Tcl.isList = new RegExp("[\\{\\} ]");
 Tcl.isNested = new RegExp("^\\{.*\\}$");

 Tcl.getVar = new RegExp("^[a-zA-Z0-9_]+", "g");

 Tcl.Source = function (interp, url) {
        var xhr_object = null; 

        if(window.ActiveXObject) // Internet Explorer 
           xhr_object = new ActiveXObject("Microsoft.XMLHTTP"); 
        else if(window.XMLHttpRequest) // Firefox 
           xhr_object = new XMLHttpRequest(); 
        else { // XMLHttpRequest non supporté par le navigateur 
           alert("Your browser does not support XMLHTTP requests. Sorry that we cannot deliver this page."); 
           return; 
        } 

        xhr_object.open("GET", url, false); 
        xhr_object.send(null);
        var text =  xhr_object.responseText;
        return interp.eval(text);
 }
 Tcl.Proc = function (interp, args) {
    var priv = this.getPrivData();
    interp.incrLevel();
    var arglist = priv[0];
    var body = priv[1];
    arglist = arglist.toList();
    args.shift();
    for (var i = 0; i < arglist.length; i++) {
                var name = arglist[i].toString();
                if (i >= args.length) {
                    if (name == "args") {
                                interp.setVar("args", Tcl.empty);
                        break;
                    }
                }
                if (Tcl.isList.test(name)) {
                    name = interp.parseString(name).toList();
                    if (name[0] == "args")
                                throw "'args' defaults to the empty string";
                    if (i >= args.length)
                                interp.setVar(name.shift(), interp.parseString(name.join(" ")));
                    else
                                interp.setVar(name[0], interp.objectify(args[i]));
                } else if (name == "args") {
                    interp.setVar("args", new TclObject(args.slice(i, args.length)));
                    break;
                }
            interp.setVar(name, interp.objectify(args[i]));
    }
    if (name == "args" && i+1 < arglist.length)
                throw "'args' should be the last argument";
    try {
        var r = interp.eval(body);
                interp.refreshCode();
                interp.decrLevel();
                return r;
    } catch (e) {
                interp.decrLevel();
                throw e;
    }
 }
 /** Manage subcommands */
 Tcl.EnsembleCommand = function (interp, args) {
        var sub = args[1].toString();
        var main = args.shift().toString()+sub;
        args[0] = main;
        var ens = this.getEnsemble();
        if (ens == null || ens[sub] == null)
        {
                throw "Not an ensemble command: "+main;
        }
        return ens[sub].call(interp, args);
 }
 /** Get subcommands of the current ensemble command. */            
 Tcl.InfoSubcommands = function(interp, args) {
        var ens = this.getEnsemble();
        var i, r = new Array();
        for (i in ens)
        {
                r.push(i);
        }
        return interp.objectify(r);
 }
 function TclObject(text) {
    this.TEXT = 0;
    this.LIST = 1;
    this.INTEGER = 2;
    this.REAL = 3;
    this.BOOL = 4;
    switch (arguments[0]) {
                case "LIST":
                case "INTEGER":
                case "REAL":
                case "BOOL":
                    this.type = this[arguments[0]];
                    break;
                default:
                    this.type = this.TEXT;
                if (text instanceof Array)
                        this.type = this.LIST;
                else
                            text = text.toString();
                        break;
    }
    this.content = text;

    this.stringIndex = function (i) {
        this.toString();
        return this.index(i, this.content.length);
    }


    this.listIndex = function (i) {
        this.toList();
        return this.index(i, this.content.length);
    }

    this.index = function (i, len) {
        var index = i.toString();
        if (index.substring(0,4) == "end-")
            index = len - parseInt(index.substring(4)) -1;
        else if (index == "end")
            index = len-1;
        else
            index = parseInt(index);
        if (isNaN(index))
            throw "Bad index "+i;
        if (index < 0 || index >= len)
            throw "Index out of bounds";
        return index;
    }

    this.isInteger = function () {return (this.type == this.INTEGER);}
    this.isReal = function () {return (this.type == this.REAL);}

    this.getString = function (list, nested) {
        var res = new Array();
        for (var i in list) {
            res[i] = list[i].toString();
            if (Tcl.isList.test(res[i]) && !Tcl.isNested.test(res[i]))
                res[i] = "{" + res[i] + "}";
        }
        if (res.length == 1)
            return res[0];
        return res.join(" ");
    }

    this.toString = function () {
        if (this.type != this.TEXT) {
            if (this.type == this.LIST)
                this.content = this.getString(this.content);
            else
                this.content = this.content.toString();
            this.type = this.TEXT;
        }
        return this.content;
    }

    this.getList = function (text) {
            if (text.charAt(0) == "{" && text.charAt(text.length-1) == "}")
                text = text.substring(1, text.length-1);
        if (text == "") {
            return [];
        }
            var parser = new TclParser(text.toString());
            var content = new Array(), element;
        for (var i = 0; ; i++) {
                parser.parseList();
                element = new TclObject(parser.getText());
                content[i] = element;
                if (parser.isType(parser.EOL) || parser.isType(parser.ESC))
                    break;
            }
            return content;
    }

    this.toList = function () {
        if (this.type != this.LIST) {
            if (this.type != this.TEXT)
                this.content[0] = this.content;
            else
                this.content = this.getList(this.content);
            this.type = this.LIST;
        }
        return this.content;
    }

    this.toInteger = function () {
        if (this.type == this.INTEGER)
            return this.content;
        this.toString();
        if (this.content.match(Tcl.isHexadecimal))
            this.content = parseInt(this.content.substring(2), 16);
        else if (this.content.match(Tcl.isOctal))
            this.content = parseInt(this.content, 8);
        else if (this.content.match(Tcl.isDecimal))
            this.content = parseInt(this.content);
        else
            throw "Not an integer: '"+this.content+"'";
        if (isNaN(this.content))
            throw "Not an integer: '"+this.content+"'";
        this.type = this.INTEGER;
        return this.content;
    }

    this.getFloat = function (text) {
        if (!text.toString().match(Tcl.isReal))
            throw "Not a real: '"+text+"'";
        return parseFloat(text);
    }

    this.toReal = function () {
        if (this.type == this.REAL)
            return this.content;
        this.toString();
        // parseFloat does not control all the string
        // so we need to check it
        this.content = this.getFloat(this.content);
        if (isNaN(this.content))
            throw "Not a real: '"+this.content+"'";
        this.type = this.REAL;
        return this.content;
    }

    this.getNumber = function () {
        try {
            return this.toInteger();
        } catch (e) {
            return this.toReal();
        }
    }

    this.toBoolean = function () {
        if (this.type == this.BOOL)
            return this.content;
        try {
            this.content = (this.toInteger()!=0);
        }
        catch (e) {
            var t = this.content;
            if (t instanceof Boolean)
                return t;

            switch (t.toString().toLowerCase()) {
                case "yes":case "true":case "on":
                    this.content = true;
                    break;
                case "false":case "off":case "no":
                    this.content = false;
                    break;
                default:
                    throw "Boolean expected, got: '"+this.content+"'";
            }
        }
        this.type = this.BOOL;
        return this.content;
    }
 }
 function TclCommand(func, privdata) {
    this.func = func;
    if (func == null)
            throw "No such function";

    this.privdata = privdata;
        this.ensemble = arguments[2];

    this.call = function(interp, args) {
        var r = (this.func)(interp, args);
        r = interp.objectify(r);
        if (r != null)
            interp.setVar("_", r);
        return r;
    }

        this.isEnsemble = function () { return (this.ensemble != null); }

        this.getEnsemble = function () {return this.ensemble;}

        this.setEnsemble = function (ens) {this.ensemble = ens; return ens;}

    this.getPrivData = function () {return this.privdata;}

    this.setPrivData = function (privdata) {
        this.privdata = privdata; 
        return privdata;
    } 
    this.requireExactArgc = function (args, argc) {
        if (args.length != argc) {
            throw argc+" arguments expected, got "+args.length;
        }
    }
    this.requireMinArgc = function (args, argc) {
        if (args.length < argc) {
            throw argc+" arguments expected at least, got "+args.length;
        }
    }
    this.requireArgcRange = function (args, min, max) {
        if (args.length < min || args.length > max) {
            throw min+" to "+max+" arguments expected, got "+args.length;
        }
    }
 }
 function TclParser(text) {
    this.text = text;
    this.index = 0;
    this.len = 0;
    this.start = 0;
    this.end = 0;
    this.type = null;
    this.insidequote = false;
    this.OK = 0;

    this.SEP = 0;
    this.STR = 1;
    this.EOL = 2;
    this.EOF = 3;
    this.ESC = 4;
    this.CMD = 5;
    this.VAR = 6;

    this.index = 0;
    this.len = text.length;
    this.start = 0;
    this.end = 0;
    this.type = this.EOL;
    this.insidequote = false;
    this.cur = this.text.charAt(0);

    this.getText = function () {
        return this.text.substring(this.start,this.end+1);
    }
    this.parseString = function () {
        var newword = (this.type==this.SEP || 
        this.type == this.EOL || this.type == this.STR);
        if (newword && this.cur == "{") return this.parseBrace();
        else if (newword && this.cur == '"') {
            this.insidequote = true;
            this.feedchar();
        }
        this.start = this.index;
        while (true) {
            if (this.len == 0) {
                this.end = this.index-1;
                this.type = this.ESC;
                return this.OK;
            }
            if (this.cur == "\\") {
                if (this.len >= 2)
                    this.feedSequence();
            }
            else if ("$[ \t\n\r;".indexOf(this.cur)>=0) {
                if ("$[".indexOf(this.cur)>=0 || !this.insidequote) {
                    this.end = this.index-1;
                    this.type = this.ESC;
                    return this.OK;
                }
            }
            else if (this.cur == '"' && this.insidequote) {
                this.end = this.index-1;
                this.type = this.ESC;
                this.feedchar();
                this.insidequote = false;
                return this.OK;
            }
            this.feedchar();
        }
        return this.OK;
    }
    this.parseList = function () {
        level = 0;
        this.start = this.index;
        while (true) {
            if (this.len == 0) {
                        this.end = this.index;
                        this.type = this.EOL;
                        return;
            }
            switch (this.cur) {
            case "\\":
                if (this.len >= 2)
                    this.feedSequence();
                break;
            case " ": case "\t": case "\n": case "\r":
                if (level > 0)
                    break;
                this.end = this.index - 1;
                this.type = this.SEP;
                this.feedchar();
                return;
            case '{':
                level++;
                break;
            case '}':
                level--;
                break;
            }
            this.feedchar();
        }
        if (level != 0)
            throw "Not a list";
        this.end = this.index;
        return;
    }
    this.parseSep = function () {
        this.start = this.index;
        while (" \t\r\n".indexOf(this.cur)>=0)
            this.feedchar();
        this.end = this.index - 1;
        this.type = this.SEP;
        return this.OK;
    }
    this.parseEol = function () {
        this.start = this.index;
        while(" \t\n\r;".indexOf(this.cur)>=0)
            this.feedchar();
            this.end = this.index - 1;
        this.type = this.EOL;
        return this.OK;
    }
    this.parseCommand = function () {
        var level = 1;
        var blevel = 0;
        this.feedcharstart();
        while (true) {
            if (this.len == 0)
                break;
            if (this.cur == "[" && blevel == 0)
                level++;
            else if (this.cur == "]" && blevel == 0) {
                level--;
                if (level == 0)
                    break;
            } else if (this.cur == "\\") {
                this.feedSequence();
            } else if (this.cur == "{") {
                blevel++;
            } else if (this.cur == "}") {
                if (blevel != 0) blevel--;
            }
            this.feedchar();
        }
        this.end = this.index-1;
        this.type = this.CMD;
        if (this.cur == "]")
            this.feedchar();
        return this.OK;
    }
    this.parseVar = function () {
        this.feedcharstart();
        this.end = this.index + this.text.substring(this.index).match(Tcl.getVar).toString().length-1;
        if (this.end == this.index-1) {
            this.end = --this.index;
            this.type = this.STR;
        } else
            this.type = this.VAR;
        this.setPos(this.end+1);
        return this.OK;
    }
    this.parseBrace = function () {
        var level = 1;
        this.feedcharstart();
        while (true) {
            if (this.len > 1 && this.cur == "\\") {
                this.feedSequence();
            } else if (this.len == 0 || this.cur == "}") {
                level--;
                if (level == 0 || this.len == 0) {
                    this.end = this.index-1;
                    if (this.len > 0)
                        this.feedchar();
                    this.type = this.STR;
                    return this.OK;
                }
            } else if (this.cur == "{")
                level++;
            this.feedchar();
        }
        return this.OK; // unreached
    }
    this.parseComment = function () {
        while (this.cur != "\n" && this.cur != "\r")
            this.feedchar();
    }
    this.getToken = function () {
        while (true) {
            if (this.len == 0) {
                if (this.type == this.EOL)
                    this.type = this.EOF;
                if (this.type != this.EOF)
                    this.type = this.EOL;
                return this.OK;
            }
            switch (this.cur) {
                case ' ':
                case '\t':
                    if (this.insidequote)
                        return this.parseString();
                    return this.parseSep();
                case '\n':
                case '\r':
                case ';':
                    if (this.insidequote)
                        return this.parseString();
                    return this.parseEol();
                case '[':
                    return this.parseCommand();
                case '$':
                    return this.parseVar();
            }
            if (this.cur == "#" && this.type == this.EOL) {
                this.parseComment();
                continue;
            }
            return this.parseString();
        }
        return this.OK; // unreached
    }
    this.getType = function() {
                return this.type;
    }
    this.isType = function(type) {
                return (type == this.type);
    }            
    this.feedSequence = function () {
                if (this.cur != "\\")
                        throw "Invalid escape sequence";
                var cur = this.steal(1);
                var specials = new Object();
                specials.a = "\a";
                specials.b = "\b";
                specials.f = "\f";
                specials.n = "\n";
                specials.r = "\r";
                specials.t = "\t";
                specials.v = "\v";
                switch (cur) {
                case 'u':
                        var hex = this.steal(4);
                        if (hex != Tcl.isHexSeq.exec(hex))
                                throw "Invalid unicode escape sequence: "+hex;
                        cur = String.fromCharCode(parseInt(hex,16));                
                        break;
            case 'x':
                        var hex = this.steal(2);
                        if (hex != Tcl.isHexSeq.exec(hex))
                                throw "Invalid unicode escape sequence: "+hex;
                        cur = String.fromCharCode(parseInt(hex,16));                
                        break;
            case "r":
            case "n":
            case "t":
            case "b":
            case "v":
            case "a":
            case "f":
                        cur = specials[cur];
                        break;
            default:
                        if ("0123456789".indexOf(cur) >= 0) {
                                cur = cur + this.steal(2);
                                if (cur != Tcl.isOctalSeq.exec(cur))
                                        throw "Invalid octal escape sequence: "+cur;
                                cur = String.fromCharCode(parseInt(cur, 8));
                        }
                        break;
                }
            this.text[index] = cur;
                this.feedchar();
    }
    this.steal = function (count) {
        var tail = this.text.substring(this.index+1);
        var word = tail.substr(0, count);
        this.text = this.text.substring(0, this.index-1) + tail.substring(count);
        return word;
    }
    this.feedcharstart = function () {
        this.feedchar();
        this.start = this.index;
    }
    this.setPos = function (index) {
        var d = index-this.index;
        this.index = index;
        this.len -= d;
        this.cur = this.text.charAt(this.index);
    }
    this.feedchar = function () {
        this.index++;
        this.len--;
        if (this.len < 0)
            throw "End of file reached";
        this.cur = this.text.charAt(this.index);
    }
 }

jcw 2008-05-19 - There's a line "this.text[index] = cur;" in the feedSequence function which refers to a global "index". I'm not sure what it should be, "this.index" doesn't seem to be right. It might be why "puts a\nb" isn't working properly.


See also A little tcl.js console