Tuesday, November 22, 2011

MAX-Javascript Includes II

Rationale

With the release of MAX 6 I'm restarting the process of rethinking what Im doing with Javascript, and aim to refactor things a bit more sensibly a la Crockford, maybe by hijacking bits of Google's Closure.

However, in the interim, I still need a cheap-n-cheerful include mechanism, so Im back with the one I started with, a very simple dump of all the lines of code from a file into a buffer, with an eval at the end. Ive renamed it crudeinclude since it is indeed pretty crude. Its probably not that well-written either, to be honest.

Implementation

Part of the post-MAX6-tidy means that I've split it into two parts so that I can use multiple versions of it with multiple versions of MAX without editing the javascript that actually does the work, and renamed the files with a 'sxa.jsextension' class-hierarchy prefix a la goog.closure. Hence there's a now a sxa.jsextensions.crudeinclude.js file file and a sxa.jsextensions._setenv_includes_.js, with the latter declaring 'environment variables' used by the include routine. Both of these .js files need to be located the Cycling '74\jsextensions folder so that they're loaded automatically when MAX starts up.

file path : This include mechanism is based on the cpp #define style, so there are two formats for the filename argument.

The first, the 'direct' form "localIncludeLibrary2", uses the same directory as the including code as the included file's expected root folder. The second format, the 'angle-bracket' format "<siteIncludeLibrary1>" uses a global 'library folder' as the included file's expected root folder. This library folder is defined by the path set in SXA_JS_INCLUDEPATH mock 'environment variable, which should normally be initialised when MAX starts via the code in sxa.jsextensions._setenv_includes_.js

file suffix : The code has been written to try and open a file suffixed with .c.js first, or, if that does not exist, then it defaults to a version with the .js suffix. This was implemented to allow a speed up in file reading and eval-ing; the .c.js is expected to be a 'minified' or 'compressed' version of the original javascript. Originally a simple comment-and-whitespace stripper was developed to do this, but something like JSMIN or the Google closure compiler could be used instead.

examples

If SXA_JS_INCLUDEPATH is set to U:\MAX6\global then the following filename arguments to crudeinclude when called from a javascript file U:\MAX\project\example\example.js will result in crudeinclude trying to read the following files in the order described:

"localLibrary" :
    U:\MAX\project\example\localLibrary.c.js
then
    U:\MAX\project\example\localLibrary.js

"<siteLibrary>" :
    U:\MAX6\global\siteLibrary.c.js
then
    U:\MAX6\global\siteLibrary.js

code

This javascript, sxa.jsextensions._setenv_includes_.js, does the setup:


1:  //sxa.jsextensions._setenv_includes_ v0.2  
2:  //------------------------------------------------------------------------  
3:  // 'environment variables' for sxa.jsextensions.crudeinclude  
4:  //------------------------------------------------------------------------  
5:  // v0.2 : 21.11.2011 : add SXA_JS_INCLUDEBUFFER variable  
6:  //------------------------------------------------------------------------  
7:  // useage : this file to be located in Cycling '74/jsxtensions  
8:  //  
9:  //----------------------------------------------------------------------------------------  
10:  var SXA_JS_INCLUDEPATH="U:_MAX6_\\site\\jsincludes\\";  
11:  var SXA_JS_INCLUDEBUFFER = 800;  

And this javascript, sxa.jsextensions.crudinclude.js, does all the work:

1:  //sxa.jsextensions.crudeinclude v0.5  
2:  //------------------------------------------------------------------------  
3:  // simple brute-force javascript include mechanism for jsextensions folder  
4:  //------------------------------------------------------------------------  
5:  // v0.5 : 23.11.2011 : tidying, better comments, add SXA_JS_INCLUDEBUFFER   
6:  // variable and changed '.h.js' form to '.c.js'  
7:  //------------------------------------------------------------------------  
8:  // useage : this file to be located in Cycling '74/jsxtensions  
9:  //  
10:  // crudeinclude takes the suffix-less name of a javascript file, and loads that  
11:  // file into a buffer for eval() to interpret.  
12:  // crudeinclude is modelled after the cpp #include directive so takes two forms,  
13:  // the angle-bracket form, which expects the source file to be in a folder defined  
14:  // by the SXA_JS_INCLUDEPATH 'pseudo-environment variable' and the direct form, which  
15:  // expects the source file to be in the same folder as the 'parent' javascript file  
16:  // doing the include.  
17:  // crudeinclude has been written to search for a file suffixed with .c.js first, -before-   
18:  // checking for a version with the .js suffix.  This was done to speed up file reading;  
19:  // the .c.js is expected to be a 'compressed' or 'compiled' version of the file   
20:  // (as per the Google closure compiler) which contains a stripped and cleaned version of the  
21:  // original javascript.    
22:  //   
23:  // buffer = crudeinclude("<siteInclude>");  // includes siteInclude.c.js located in path SXA_JS_INCLUDEBUFFER  
24:  // buffer = crudeinclude("<localInclude");  // includes localInclude.js located in same path as calling code  
25:  //  
26:  //------------------------------------------------------------------------  
27:  crudeinclude.local = 1;  
28:  function crudeinclude(filename){  
29:    var includes ="";  
30:    var compressedType = ".c.js";  
31:    var rawType = ".js";  
32:    length = filename.length;  
33:    // determine possible full path names for filename  
34:    if ((filename.substring(0,1) == "<")){                   // '<' indicates possible global library name  
35:     if ((filename.substring(length-1,length) == ">")){            // '>' confirms well-formed global library name  
36:       base_filename = SXA_JS_INCLUDEPATH + filename.substring(1,length-1);  
37:       compiled_filename = base_filename + compressedType;  
38:       raw_filename = base_filename + rawType;  
39:     }else{                                 //badly formed global  
40:       post("ERROR in include; malformed filename '",filename, "'\n");  
41:       return;  
42:     }  
43:    }else{ // ! == "<"  
44:     if ((filename.substring(length-1,length) == ">")){           //<..> not matched, so badly formed include  
45:       post("ERROR in include; malformed filename '",filename, "'\n");  
46:       return;  
47:     }else{ // ! == ">" so not <...> form, thus a local filename  
48:       compiled_filename = filename + compressedType;  
49:       raw_filename = filename + rawType;  
50:     }// done checking angle brackets   
51:    }// done determining path and name options  
52:    // open file to copy into a string  
53:    // prioritise the compiled version first  
54:    file = new File(compiled_filename, "read");  
55:    file.open();    
56:    if (!(file.isopen)){         
57:     file = new File(raw_filename, "read");  
58:     file.open();   
59:    }  
60:    if (file.isopen){  
61:     // read in lines of up to 120 chars at a time  
62:     //(compensates for strange filesize/buffer issue)  
63:     // this is probably slower,  
64:     //but strings get weirdly truncated otherwise.  
65:     fileposition = 0;  
66:     while (fileposition < file.eof){  
67:       includes += file.readline(SXA_JS_INCLUDEBUFFER);  
68:       includes += "\n";  
69:       fileposition = file.position;  
70:     }// end while  
71:     file.close();  
72:    }else{// file not open      
73:     post("ERROR in include; cannot open ", filename, "\n");  
74:    }  
75:    return(includes);  
76:  }// end function include   

Basically this returns a buffer containing the code from an external javascript file. The file can then be interpreted via eval from within the code of a js or jsui object.

useage : A simple way of using this include code in javascript would be


     eval(crudeinclude("<siteIncludeLibrary1>"));  
     eval(crudeinclude("localIncludeLibrary2"));  

or


     includeBuf = (crudeinclude("<siteIncludeLibrary1>"));  
     includeBuf += (crudeinclude("localIncludeLibrary2"));  
     eval(includeBuf);

Friday, October 14, 2011

MAX-Javascript Includes I

Some method for using code from external files is needed when working with the MAX/MSP Javascript implementation.  At present there is a 'static' file inclusion method, in which any .js file within the jsextensions folder is interpreted when MAX initialises, but the limitation of that is that the included files are only interpreted when MAX initialises.
A more dynamic runtime-based system is needed, one which permits a more modular style of code development for Javascript MAX-objects, and which provides a mechanism to support reusable code libraries.   Browser-based Javascript permits the use of libraries (like Google Closure) via  <script src=""> tags that leverage the dynamic nature of an HTML document, but this has no real equivalent inside a MAX/MSP patch. 
The obvious model for file inclusion is the classic C #include file directive, but as there is no pre-processing system within MAX-Javascript, we need to construct an alternative.  Fortunately, Javascript is capable of dynamic code evaluation, via eval(); unfortunately eval() is incredibly slow and much frowned-upon.
A useful file-inclusion system would be designed to achieve the following:
  • simple syntax
  • file inclusion relative to the 'main' code or to a given library location (as per C preprocessor)
  • minimised eval() overhead
    • optimise to single eval
    • remove re-eval of unchanged code
  • dynamic control over inclusion mechanism
  • possible compatibility with other methods (ie could be useable to override goog.global.CLOSURE_IMPORT_SCRIPT in Google Closure)