|
View:
New views
5 Messages
—
Rating Filter:
Alert me
|
|
|
Rhino: JSDoc?I am curious if anyone familiar somewhat with Rhino's internals would
know how feasible a JavaScript equivalent of javadoc would be to create using Rhino? I know there exists jsdoc (http://jsdoc.sourceforge.net/) but it's entirely written in Perl with regular expressions (no proper parse-tree) and it would be nice to have a closer to home approach. Some sort of meta-data for JavaScript. Any insights would be appreciated. Thanks, Marcello _______________________________________________ dev-tech-js-engine mailing list dev-tech-js-engine@... https://lists.mozilla.org/listinfo/dev-tech-js-engine |
|
|
Re: Rhino: JSDoc?If anyone is interested, I've made a patch to Rhino that records /**
javadoc */ tags into a __jsdoc__ property of all Function objects. From there you can do whatever you like. (I made up a script that analyzes the scope to find all classes/functions and uses the jsdoc information accordingly.) Marcello > I am curious if anyone familiar somewhat with Rhino's internals would > know how feasible a JavaScript equivalent of javadoc would be to create > using Rhino? > > I know there exists jsdoc (http://jsdoc.sourceforge.net/) but it's > entirely written in Perl with regular expressions (no proper parse-tree) > and it would be nice to have a closer to home approach. Some sort of > meta-data for JavaScript. > > Any insights would be appreciated. > > Thanks, > > Marcello dev-tech-js-engine mailing list dev-tech-js-engine@... https://lists.mozilla.org/listinfo/dev-tech-js-engine |
|
|
Re: Rhino: JSDoc?Marcello Bastéa-Forte wrote:
> If anyone is interested, I've made a patch to Rhino that records /** > javadoc */ tags into a __jsdoc__ property of all Function objects. > From there you can do whatever you like. (I made up a script that > analyzes the scope to find all classes/functions and uses the jsdoc > information accordingly.) Wow - I'm very interested! What do I have to do? -- Wired Earp Wunderbyte _______________________________________________ dev-tech-js-engine mailing list dev-tech-js-engine@... https://lists.mozilla.org/listinfo/dev-tech-js-engine |
|
|
Re: Rhino: JSDoc?Apply the attached patch to rhino cvs. (Note, this patch also has
patches for a modified version of dojo's compressor patch.) It presently only works in interpreted mode (perhaps a good thing?), and very simply adds a __jsdoc__ property to every function that has a /** .. */ comment before it. The value of the property is a string with the leading whitespace/asterisks removed as per javadoc spec. The inspector/doc html-generator based on this patch will be released as part of my upcoming alt framework technical preview release (http://marcello.cellosoft.com/projects/alt/). Marcello > Marcello Bastéa-Forte wrote: > >> If anyone is interested, I've made a patch to Rhino that records /** >> javadoc */ tags into a __jsdoc__ property of all Function objects. >> From there you can do whatever you like. (I made up a script that >> analyzes the scope to find all classes/functions and uses the jsdoc >> information accordingly.) > > Wow - I'm very interested! What do I have to do? > > Index: src/org/mozilla/javascript/FunctionNode.java =================================================================== RCS file: /cvsroot/mozilla/js/rhino/src/org/mozilla/javascript/FunctionNode.java,v retrieving revision 1.29 diff -u -r1.29 FunctionNode.java --- src/org/mozilla/javascript/FunctionNode.java 29 Aug 2005 13:25:31 -0000 1.29 +++ src/org/mozilla/javascript/FunctionNode.java 26 Jul 2006 23:16:25 -0000 @@ -77,7 +77,15 @@ public int getFunctionType() { return itsFunctionType; } + + public void setJSDoc(String s) { + this.jsDoc = s; + } + public String getJSDoc() { + return jsDoc; + } + String jsDoc = null; String functionName; boolean itsNeedsActivation; int itsFunctionType; Index: src/org/mozilla/javascript/Interpreter.java =================================================================== RCS file: /cvsroot/mozilla/js/rhino/src/org/mozilla/javascript/Interpreter.java,v retrieving revision 1.307 diff -u -r1.307 Interpreter.java --- src/org/mozilla/javascript/Interpreter.java 19 Nov 2005 22:57:49 -0000 1.307 +++ src/org/mozilla/javascript/Interpreter.java 26 Jul 2006 23:16:27 -0000 @@ -487,6 +487,7 @@ itsData.itsFunctionType = theFunction.getFunctionType(); itsData.itsNeedsActivation = theFunction.requiresActivation(); itsData.itsName = theFunction.getFunctionName(); + itsData.jsDoc = theFunction.getJSDoc(); if (!theFunction.getIgnoreDynamicScope()) { if (compilerEnv.isUseDynamicScope()) { itsData.useDynamicScope = true; Index: src/org/mozilla/javascript/InterpretedFunction.java =================================================================== RCS file: /cvsroot/mozilla/js/rhino/src/org/mozilla/javascript/InterpretedFunction.java,v retrieving revision 1.52 diff -u -r1.52 InterpretedFunction.java --- src/org/mozilla/javascript/InterpretedFunction.java 28 May 2006 17:15:24 -0000 1.52 +++ src/org/mozilla/javascript/InterpretedFunction.java 26 Jul 2006 23:16:25 -0000 @@ -133,6 +133,17 @@ if (idata.itsRegExpLiterals != null) { functionRegExps = createRegExpWraps(cx, scope); } + if (idata.jsDoc != null) { + defineProperty("__jsdoc__", + Context.toString(idata.getJSDoc()), + ScriptableObject.DONTENUM); + defineProperty("__source__", + Context.toString(idata.getSourceName()), + ScriptableObject.DONTENUM); + defineProperty("__lines__", + Context.javaToJS(idata.getLineNumbers(), scope), + ScriptableObject.DONTENUM); + } } public String getFunctionName() Index: src/org/mozilla/javascript/TokenStream.java =================================================================== RCS file: /cvsroot/mozilla/js/rhino/src/org/mozilla/javascript/TokenStream.java,v retrieving revision 1.63 diff -u -r1.63 TokenStream.java --- src/org/mozilla/javascript/TokenStream.java 31 Jul 2005 13:48:46 -0000 1.63 +++ src/org/mozilla/javascript/TokenStream.java 26 Jul 2006 23:16:28 -0000 @@ -284,6 +284,8 @@ final int getLineno() { return lineno; } final String getString() { return string; } + + final String getJSDoc() { String s = jsDoc; jsDoc = null; return s; } final double getNumber() { return number; } @@ -742,6 +744,11 @@ } if (matchChar('*')) { boolean lookForSlash = false; + // Marcello: JSDoc patch + boolean potentialJSDoc = true; + boolean inJSDoc = false; + boolean readJSDoc = false; + int jsDocStep = 1; // 0: space, 1: star, 2: space, 3: text for (;;) { c = getChar(); if (c == EOF_CHAR) { @@ -749,13 +756,47 @@ return Token.ERROR; } else if (c == '*') { lookForSlash = true; + if (potentialJSDoc) { + inJSDoc = true; + potentialJSDoc = false; + stringBufferTop = 0; + continue; + } } else if (c == '/') { if (lookForSlash) { + if (inJSDoc) + this.jsDoc = getStringFromBuffer(); continue retry; } } else { lookForSlash = false; } + potentialJSDoc = false; + if (inJSDoc) { + // If we hit a newline restart step + if (c=='\n' && jsDocStep>=1) { + jsDocStep = 0; + if (readJSDoc) + addToString(c); + continue; + } else if (jsDocStep<3) { + // Ignore asterisks if we're in step 0/1 + if (c=='*') { + if (jsDocStep==0) + jsDocStep=1; + if (jsDocStep==1) + continue; + // Ignore spaces in steps 0,1,2 + } else if (isJSSpace(c)) { + if (jsDocStep==1) + jsDocStep = 2; + continue; + } + } + jsDocStep = 3; + addToString(c); + readJSDoc = true; + } } } @@ -1367,6 +1408,9 @@ // code. private String string = ""; private double number; + + // Marcello: store jsdoc + private String jsDoc = null; private char[] stringBuffer = new char[128]; private int stringBufferTop; Index: src/org/mozilla/javascript/BaseFunction.java =================================================================== RCS file: /cvsroot/mozilla/js/rhino/src/org/mozilla/javascript/BaseFunction.java,v retrieving revision 1.57 diff -u -r1.57 BaseFunction.java --- src/org/mozilla/javascript/BaseFunction.java 30 Aug 2005 10:05:42 -0000 1.57 +++ src/org/mozilla/javascript/BaseFunction.java 26 Jul 2006 23:16:25 -0000 @@ -245,6 +245,8 @@ } else { indent = 0; } + if (args.length>=2) + flags |= ScriptRuntime.toInt32(args[1]); } return realf.decompile(indent, flags); } Index: src/org/mozilla/javascript/Decompiler.java =================================================================== RCS file: /cvsroot/mozilla/js/rhino/src/org/mozilla/javascript/Decompiler.java,v retrieving revision 1.19 diff -u -r1.19 Decompiler.java --- src/org/mozilla/javascript/Decompiler.java 28 Aug 2005 23:25:22 -0000 1.19 +++ src/org/mozilla/javascript/Decompiler.java 26 Jul 2006 23:16:25 -0000 @@ -82,6 +82,17 @@ * Flag to indicate that the decompilation generates toSource result. */ public static final int TO_SOURCE_FLAG = 1 << 1; + + /** + * Flag to indicate that the decompilation generates a compressed result. + */ + public static final int COMPRESS_FLAG = 1 << 2; + + /** + * Flag to indicate that the decompilation generates a compressed result. + */ + public static final int COMPRESS_NEWLINES_FLAG = 1 << 3; + /** * Decompilation property to specify initial ident value. @@ -298,6 +309,10 @@ StringBuffer result = new StringBuffer(); boolean justFunctionBody = (0 != (flags & Decompiler.ONLY_BODY_FLAG)); boolean toSource = (0 != (flags & Decompiler.TO_SOURCE_FLAG)); + // Compress: features + boolean compress = (0 != (flags & Decompiler.COMPRESS_FLAG)); + boolean compressnl = (0 != (flags & Decompiler.COMPRESS_NEWLINES_FLAG)); + TokenMapper tm = new TokenMapper(); // Spew tokens in source, for debugging. // as TYPE number char @@ -329,6 +344,10 @@ int braceNesting = 0; boolean afterFirstEOL = false; int i = 0; + int prevToken = 0; + boolean primeFunctionNesting = false; + boolean inArgsList = false; + boolean primeInArgsList = false; int topFunctionType; if (source.charAt(i) == Token.SCRIPT) { ++i; @@ -339,7 +358,9 @@ if (!toSource) { // add an initial newline to exactly match js. - result.append('\n'); + // Compress: features + if (!compress) + result.append('\n'); for (int j = 0; j < indent; j++) result.append(' '); } else { @@ -349,10 +370,20 @@ } while (i < length) { + // Compress: features + if (i>0) + prevToken = source.charAt(i-1); switch(source.charAt(i)) { case Token.NAME: case Token.REGEXP: // re-wrapped in '/'s in parser... - i = printSourceString(source, i + 1, false, result); + // Compress: + int jumpPos = getSourceStringEnd(source, i+1); + if (!compress || Token.OBJECTLIT == source.charAt(jumpPos)) { + i = printSourceString(source, i + 1, false, result); + } else { + i = tm.printCompressed( source, i + 1, false, result, prevToken, + inArgsList, braceNesting); + } continue; case Token.STRING: @@ -381,7 +412,11 @@ case Token.FUNCTION: ++i; // skip function type - result.append("function "); + primeInArgsList = true; + primeFunctionNesting = true; + result.append("function"); + if (Token.LP != getNext(source, length, i)) + result.append(' '); break; case FUNCTION_END: @@ -389,7 +424,7 @@ break; case Token.COMMA: - result.append(", "); + result.append(compress ? "," : ", "); break; case Token.LC: @@ -400,6 +435,7 @@ break; case Token.RC: { + tm.leaveNestingLevel(braceNesting); --braceNesting; /* don't print the closing RC if it closes the * toplevel function and we're called from @@ -417,18 +453,29 @@ case Token.WHILE: case Token.ELSE: indent -= indentGap; - result.append(' '); + if (!compress) + result.append(' '); break; } break; } case Token.LP: + if (primeInArgsList) { + inArgsList = true; + primeInArgsList = false; + } + if (primeFunctionNesting) { + tm.enterNestingLevel(braceNesting); + primeFunctionNesting = false; + } result.append('('); break; case Token.RP: + if (inArgsList) + inArgsList = false; result.append(')'); - if (Token.LC == getNext(source, length, i)) + if (!compress && Token.LC == getNext(source, length, i)) result.append(' '); break; @@ -454,7 +501,7 @@ newLine = false; } } - if (newLine) { + if (newLine && !compressnl) { result.append('\n'); } @@ -482,8 +529,9 @@ less = indentGap; } - for (; less < indent; less++) - result.append(' '); + if (!compress) + for (; less < indent; less++) + result.append(' '); } break; } @@ -500,15 +548,17 @@ break; case Token.IF: - result.append("if "); + result.append(compress ? "if" : "if "); break; case Token.ELSE: - result.append("else "); + result.append(compress ? "else" : "else "); break; case Token.FOR: - result.append("for "); + result.append("for"); + if (!compress || Token.NAME == getNext(source, length, i)) + result.append(' '); break; case Token.IN: @@ -516,27 +566,27 @@ break; case Token.WITH: - result.append("with "); + result.append(compress ? "with" : "with "); break; case Token.WHILE: - result.append("while "); + result.append(compress ? "while" : "while "); break; case Token.DO: - result.append("do "); + result.append(compress ? "do" : "do "); break; case Token.TRY: - result.append("try "); + result.append(compress ? "try" : "try "); break; case Token.CATCH: - result.append("catch "); + result.append(compress ? "catch" : "catch "); break; case Token.FINALLY: - result.append("finally "); + result.append(compress ? "finally" : "finally "); break; case Token.THROW: @@ -544,7 +594,7 @@ break; case Token.SWITCH: - result.append("switch "); + result.append(compress ? "switch" : "switch "); break; case Token.BREAK: @@ -561,6 +611,8 @@ case Token.CASE: result.append("case "); + if (!compress || Token.NAME == getNext(source, length, i)) + result.append(' '); break; case Token.DEFAULT: @@ -578,63 +630,65 @@ break; case Token.SEMI: - result.append(';'); - if (Token.EOL != getNext(source, length, i)) { + if (Token.EOL == getNext(source, length, i)) { + if (compressnl || !compress) + result.append(';'); + } else { // separators in FOR - result.append(' '); + result.append(compress ? ";" : "; "); } break; case Token.ASSIGN: - result.append(" = "); + result.append(compress ? "=" : " = "); break; case Token.ASSIGN_ADD: - result.append(" += "); + result.append(compress ? "+=" : " += "); break; case Token.ASSIGN_SUB: - result.append(" -= "); + result.append(compress ? "-=" : " -= "); break; case Token.ASSIGN_MUL: - result.append(" *= "); + result.append(compress ? "*=" : " *= "); break; case Token.ASSIGN_DIV: - result.append(" /= "); + result.append(compress ? "/=" : " /= "); break; case Token.ASSIGN_MOD: - result.append(" %= "); + result.append(compress ? "%=" : " %= "); break; case Token.ASSIGN_BITOR: - result.append(" |= "); + result.append(compress ? "|=" : " |= "); break; case Token.ASSIGN_BITXOR: - result.append(" ^= "); + result.append(compress ? "^=" : " ^= "); break; case Token.ASSIGN_BITAND: - result.append(" &= "); + result.append(compress ? "&=" : " &= "); break; case Token.ASSIGN_LSH: - result.append(" <<= "); + result.append(compress ? "<<=" : " <<= "); break; case Token.ASSIGN_RSH: - result.append(" >>= "); + result.append(compress ? ">>=" : " >>= "); break; case Token.ASSIGN_URSH: - result.append(" >>>= "); + result.append(compress ? ">>>=" : " >>>= "); break; case Token.HOOK: - result.append(" ? "); + result.append(compress ? "?" : " ? "); break; case Token.OBJECTLIT: @@ -652,59 +706,59 @@ result.append(':'); else // it's the middle part of a ternary - result.append(" : "); + result.append(compress ? ":" : " : "); break; case Token.OR: - result.append(" || "); + result.append(compress ? "||" : " || "); break; case Token.AND: - result.append(" && "); + result.append(compress ? "&&" : " && "); break; case Token.BITOR: - result.append(" | "); + result.append(compress ? "|" : " | "); break; case Token.BITXOR: - result.append(" ^ "); + result.append(compress ? "^" : " ^ "); break; case Token.BITAND: - result.append(" & "); + result.append(compress ? "&" : " & "); break; case Token.SHEQ: - result.append(" === "); + result.append(compress ? "===" : " === "); break; case Token.SHNE: - result.append(" !== "); + result.append(compress ? "!==" : " !== "); break; case Token.EQ: - result.append(" == "); + result.append(compress ? "==" : " == "); break; case Token.NE: - result.append(" != "); + result.append(compress ? "!=" : " != "); break; case Token.LE: - result.append(" <= "); + result.append(compress ? "<=" : " <= "); break; case Token.LT: - result.append(" < "); + result.append(compress ? "<" : " < "); break; case Token.GE: - result.append(" >= "); + result.append(compress ? ">=" : " >= "); break; case Token.GT: - result.append(" > "); + result.append(compress ? ">" : " > "); break; case Token.INSTANCEOF: @@ -712,15 +766,15 @@ break; case Token.LSH: - result.append(" << "); + result.append(compress ? "<<" : " << "); break; case Token.RSH: - result.append(" >> "); + result.append(compress ? ">>" : " >> "); break; case Token.URSH: - result.append(" >>> "); + result.append(compress ? ">>>" : " >>> "); break; case Token.TYPEOF: @@ -748,31 +802,39 @@ break; case Token.INC: + if (compress && Token.ADD == prevToken) + result.append(' '); result.append("++"); + if (compress && Token.ADD == getNext(source, length, i)) + result.append(' '); break; case Token.DEC: + if (compress && Token.SUB == prevToken) + result.append(' '); result.append("--"); + if (compress && Token.SUB == getNext(source, length, i)) + result.append(' '); break; case Token.ADD: - result.append(" + "); + result.append(compress ? "+" : " + "); break; case Token.SUB: - result.append(" - "); + result.append(compress ? "-" : " - "); break; case Token.MUL: - result.append(" * "); + result.append(compress ? "*" : " * "); break; case Token.DIV: - result.append(" / "); + result.append(compress ? "/" : " / "); break; case Token.MOD: - result.append(" % "); + result.append(compress ? "%" : " % "); break; case Token.COLONCOLON: @@ -800,7 +862,7 @@ if (!toSource) { // add that trailing newline if it's an outermost function. - if (!justFunctionBody) + if (!justFunctionBody && !compressnl) result.append('\n'); } else { if (topFunctionType == FunctionNode.FUNCTION_EXPRESSION) { @@ -890,3 +952,111 @@ private static final boolean printSource = false; } + + +class TokenMapper { + private java.util.ArrayList functionBracePositions = + new java.util.ArrayList(); + private java.util.ArrayList scopeReplacedTokens = new java.util.ArrayList(); + private int tokenCount = 10; + + // FIXME: this isn't the brightest way to accomplish this. Firstly, we need + // to be sure we aren't colliding with other things in the namespace! + private String getMappedToken(String token, boolean newMapping) { + String nt = null; + java.util.HashMap tokens = (java.util.HashMap)scopeReplacedTokens.get(scopeReplacedTokens.size()-1); + if (newMapping) { + nt = new String(Integer.toString(tokenCount++,26)); + tokens.put(token, nt); + return nt; + } + String mapping = getTokenMapping(token); + if (mapping==null) + return token; + return mapping; + } + + private boolean hasLocalTokenMapping(String token) { + if (scopeReplacedTokens.size() < 1) + return false; + java.util.HashMap tokens = (java.util.HashMap)(scopeReplacedTokens.get(scopeReplacedTokens.size()-1)); + if (tokens.containsKey(token)) + return true; + return false; + } + + private String getTokenMapping(String token) { + for (int i=scopeReplacedTokens.size()-1; i>=0; i--) { + java.util.HashMap tokens = (java.util.HashMap)(scopeReplacedTokens.get(i)); + if (tokens.containsKey(token)) + return (String)tokens.get(token); + } + return null; + } + + public int printCompressed(String source, + int offset, + boolean asQuotedString, + StringBuffer sb, + int prevToken, + boolean inArgsList, + int currentLevel) { + boolean newMapping = false; + int length = source.charAt(offset); + ++offset; + if ((0x8000 & length) != 0) { + length = ((0x7FFF & length) << 16) | source.charAt(offset); + ++offset; + } + + if (sb != null) { + String str = source.substring(offset, offset + length); + String sourceStr = new String(str); + if (((prevToken == Token.VAR)&&(!hasLocalTokenMapping(sourceStr)))||(inArgsList)) + newMapping = true; + + + if (((functionBracePositions.size()>0)&&(currentLevel>=(((Integer)functionBracePositions.get(functionBracePositions.size()-1)).intValue())))||(inArgsList)) + if(prevToken != Token.DOT) + str = this.getMappedToken(str, newMapping); + if ((!inArgsList)&&(asQuotedString)) + if((prevToken == Token.LC)||(prevToken == Token.COMMA)) + str = sourceStr; + + if(!asQuotedString){ + sb.append(str); + } else { + sb.append('"'); + sb.append(ScriptRuntime.escapeString(str)); + sb.append('"'); + } + } + + return offset + length; + } + + public void enterNestingLevel(int braceNesting){ + functionBracePositions.add(new Integer(braceNesting+1)); + scopeReplacedTokens.add(new java.util.HashMap()); + } + + public void leaveNestingLevel(int braceNesting){ + Integer bn = new Integer(braceNesting); + if ((functionBracePositions.contains(bn))&&(scopeReplacedTokens.size()>0)) { + // remove our mappings now! + int scopedSize = scopeReplacedTokens.size(); + /* + HashMap tokens = (HashMap)(scopeReplacedTokens.get(scopedSize-1)); + Iterator titer = (tokens.keySet()).iterator(); + String key = null; + while(titer.hasNext()){ + key = (String)titer.next(); + // System.out.println("removing: "+key); + tokenMappings.remove(key); + } + */ + scopeReplacedTokens.remove(scopedSize-1); + functionBracePositions.remove(bn); + } + } +} Index: src/org/mozilla/javascript/InterpreterData.java =================================================================== RCS file: /cvsroot/mozilla/js/rhino/src/org/mozilla/javascript/InterpreterData.java,v retrieving revision 1.53 diff -u -r1.53 InterpreterData.java --- src/org/mozilla/javascript/InterpreterData.java 30 Aug 2005 10:05:42 -0000 1.53 +++ src/org/mozilla/javascript/InterpreterData.java 26 Jul 2006 23:16:27 -0000 @@ -102,6 +102,9 @@ String encodedSource; int encodedSourceStart; int encodedSourceEnd; + + // Marcello: Added JSDoc + String jsDoc; int languageVersion; @@ -154,6 +157,11 @@ return itsSourceFile; } + public String getJSDoc() + { + return jsDoc; + } + public boolean isGeneratedScript() { return ScriptRuntime.isGeneratedScript(itsSourceFile); Index: src/org/mozilla/javascript/Parser.java =================================================================== RCS file: /cvsroot/mozilla/js/rhino/src/org/mozilla/javascript/Parser.java,v retrieving revision 1.104 diff -u -r1.104 Parser.java --- src/org/mozilla/javascript/Parser.java 1 Jun 2006 14:30:19 -0000 1.104 +++ src/org/mozilla/javascript/Parser.java 26 Jul 2006 23:16:28 -0000 @@ -430,6 +430,9 @@ { int syntheticType = functionType; int baseLineno = ts.getLineno(); // line number where source starts + + // Marcello: JSDoc addition + String jsDoc = ts.getJSDoc(); int functionSourceStart = decompiler.markFunctionStart(functionType); String name; @@ -476,6 +479,9 @@ // of with object. fnNode.itsIgnoreDynamicScope = true; } + + // Marcello: JSDoc addition + fnNode.setJSDoc(jsDoc); int functionIndex = currentScriptOrFn.addFunction(fnNode); _______________________________________________ dev-tech-js-engine mailing list dev-tech-js-engine@... https://lists.mozilla.org/listinfo/dev-tech-js-engine |
|
|
Re: Rhino: JSDoc?Marcello Bastéa-Forte wrote:
> Apply the attached patch to rhino cvs... Thanks man. I've had it with JSDoc dictating my coding style, but since there is nothing I can do about Perl, this is exactly what I need for my documentation pratices. I haven't worked with Rhino before, so I have no idea how to apply any patch, but I'm gonna give a go before I panick. Meantine, you might consider coming up with a patch that doesn't barf Rhino on the SpiderMonkey "const" keyword :) Thanks again. This is very good stuff. -- Wired Earp Wunderbyte _______________________________________________ dev-tech-js-engine mailing list dev-tech-js-engine@... https://lists.mozilla.org/listinfo/dev-tech-js-engine |
| Free embeddable forum powered by Nabble | Forum Help |