toloframework
Version:
Javascript/HTML/CSS compiler for Firefox OS or nodewebkit apps using modules in the nodejs style.
557 lines (517 loc) • 16.1 kB
JavaScript
/**********************************************************************
* Trois syntaxes possibles :
* * `<wdg:label ...>` : instancie la classe `wdg.label`.
* * `<x-widget name="label" ...>` : instancie la classe `label`.
* * `<x-wdg name="label" ...>` : instancie la classe `label` (juste un alias de la syntaxe précédente).
*
* Pour spécifier les propriétés d'un objet, il existe deux possibilités.
*
* * Forme __inline__ : `<wdg:button $text="Cancel"/>`.
* * Il est possible d'utiliser l'internationalisation :
* `<wdg:button intl:text="cancel-caption"/>`
* * On peut lier la valeur à celle de la propriété d'un autre objet :
* * Lier à l'attribut 'bar' de l'objet dont l'ID est 'foo' :
* `<wdg:button bind:text="foo:bar"/>`
* * Quand on ne spécifie pas le nom de la propriété, c'est `value` qui est utilisée :
* `<wdg:button bind:text="foo"/>`
* * Pour lier à plusieurs sources, on utilise la virgule :
* `<wdg:button bind:text="foo1, foo2, foo3"/>`
* * Pour spécifier une valeur, on utilise le `=` :
* `<wdg:button bind:text="action='ok'"/>`
* * Forme __expanded__ : `<wdg:button><text>Cancel</text></wdg:button>`.
* Il arrive qu'on doive utiliser des valeurs dont le type n'est pas une string.
* Dans ce cas, on met dans le body des tags dont le nom est celui de la propriété.
* Ces tags peuvent avoir des attributs qui spécifient le type.
* Par exemple :
* ```
* <wdg:combo $key="fr">
* <content json>
* {
* "en": "English",
* "fr": "Français",
* "it": "Italiano"
* }
* </content>
* </wdg:combo>
* ```
* * __json__ : le contenu textuel doit être parsé comme du JSON.
*
*
* @example
* <x-widget name="tfw.input" $value="Email" $validator="[^@]+@[^@]\\.[^@.]+"/>
* <x-widget name="tfw.input" $validator="[^@]+@[^@]\\.[^@.]+">Email</x-widget>
* <wdg:checkbox $value="false" $wide="true" />
* <wdg:label intl:value="title-text" $wide="true" />
**********************************************************************/
var ToloframeworkPermissiveJson = require("toloframework-permissive-json");
exports.tags = [ "x-widget", "x-wdg", "wdg:.+" ];
exports.priority = 0;
String.trim = function( x ) {
return x.trim( );
};
var ID = 0;
// When a widget is child of another widget, we will skip it and parse its content.
var SKIP = false;
/**
* Compile a node of the HTML tree.
*/
exports.compile = function( root, libs ) {
if ( SKIP ) {
// For widgets, children of other widgets, we want to compile the properties' children.
root.children.forEach( function( child ) {
if ( child.type == libs.Tree.TAG ) {
libs.compileChildren( child );
}
});
return;
}
SKIP = true;
var com = parseComponent( root, libs, ' ' );
console.log( "Widget: " + com.name.green );
libs.require( "x-widget" );
libs.require( com.name );
libs.addInitJS( "var W = require('x-widget');" );
var cmd = " W('" + com.attr.id
+ "', '" + com.name + "', "
+ stringifyProperties( com.prop, ' ' );
if( com.attr ) {
cmd += "," + JSON.stringify( com.attr );
}
cmd += ")";
libs.addInitJS( cmd );
SKIP = false;
};
/**
* @return `{ attr: {id:...}, prop: {value:...}, bind: {enabled:...}, name: "wdg.text", require: []}`
*/
function parseComponent( root, libs, indent ) {
var com = {
// HTML element attributes.
attr: {},
// Widget properties.
prop: {},
// Widget properties bindings.
bind: {},
// Required modules.
require: [],
// Temporary variables.
temp: {}
};
getModuleName( root, libs, com );
getUniqueIdentifier( root, libs, com );
getRootChildren( root, libs, com );
getPropertiesAndBindings( root, libs, com, indent );
parseChildrenProperties( root, libs, com, indent );
root.children = [ ];
root.name = "div";
delete root.autoclose;
root.attribs = {
id: com.attr.id,
"class": com.attr.class || "",
style: "display:none"
};
return com;
}
var RX_ID = /^[$_a-zA-Z][$_a-zA-Z0-9]*$/g;
function camelCase( text ) {
var result = text.split( '-' ).map( function( itm, idx ) {
if ( idx === 0 )
return itm;
return itm.charAt( 0 ).toUpperCase( ) + itm.substr( 1 );
}).join( '' ).trim();
RX_ID.lastIndex = 0;
if( !RX_ID.test( result ) ) result = JSON.stringify( result );
return result;
}
function stringifyProperties( prop, indent ) {
var count = 0;
var key,
val;
var out;
// We want to know if there is more than one item.
for ( key in prop ) {
count++;
if ( count > 1 )
break;
}
if ( count === 0 )
return "{}";
else if ( count == 1 ) {
for ( key in prop ) {
return "{" + JSON.stringify( key ) + ": " + prop[key] + "}";
}
} else {
var first = true;
out = '{';
for ( key in prop ) {
val = prop[key];
if ( first ) {
first = false;
} else {
out += ",";
}
out += "\n " + indent + camelCase( key ) + ": " + val;
}
return out + '}';
}
}
/**
* @param {array} arr - Array of strings.
*
* If `arr` has more than one element, it will be displayed on several lines.
*/
function stringifyArray( arr, indent ) {
var hasMoreThanOneItem = arr.length > 1;
if ( hasMoreThanOneItem ) {
var out = '[';
arr.forEach( function( itm, idx ) {
if ( idx > 0 )
out += ",";
out += "\n" + indent + itm;
});
return out + "]";
} else {
return "[" + ( arr.length == 1 ? arr[0] : '' ) + "]";
}
}
/**
* Module's name of this component.
* `com == {name: ...}`
*/
function getModuleName( root, libs, com ) {
if ( typeof root.attribs === 'undefined' ) {
root.attribs = {};
}
var name = root.attribs.name;
if ( root.name.substr( 0, 4 ) == 'wdg:' ) {
name = "wdg." + root.name.substr( 4 );
} else {
if ( !name || name.length === 0 ) {
libs.fatal( "[x-widget] Missing attribute \"name\"!", root );
}
}
com.name = name;
}
/**
* Unique identifier.
* `com == {attr: {id:...}}`
*/
function getUniqueIdentifier( root, libs, com ) {
var id = root.attribs.id || ( com.name + ID++ );
com.attr.id = id;
}
/**
* If root has got a `src` attribute, we load a file and put its content as children of `root`.
*/
function getRootChildren( root, libs, com ) {
var src = ( root.attribs.src || "" ).trim( );
if ( src.length > 0 ) {
if (!libs.fileExists( src )) {
libs.fatal( "File not found: \"" + src + "\"!" );
}
// Add a compilation dependency on the include file.
libs.addInclude( src );
var content = libs.readFileContent( src );
root.children = libs.parseHTML( content );
}
}
/**
* Properties and bindings.
*/
function getPropertiesAndBindings( root, libs, com, indent ) {
// Attributes can have post initialization, especially for data bindings.
var postInit = {};
var hasPostInit = false;
var key,
val,
values;
var bindings;
var slots;
for ( key in root.attribs ) {
if ( key.charAt( 0 ) == '$' ) {
// All the attributes that start with a '$' are used as args attributes.
val = root.attribs[key];
com.prop[key.substr( 1 )] = JSON.stringify( val );
}
else if ( key.substr( 0, 5 ) == 'intl:' ) {
// Internationalization.
val = root.attribs[key];
com.prop[key.substr( 5 )] = "APP._(" + JSON.stringify( val ) + ")";
}
else if ( key.substr( 0, 5 ) == 'bind:' ) {
// Syntaxe :
// <bind> := <bind-item> <bind-next>*
// <bind-next> := "," <bind-item>
// <bind-item> := <widget-name> <attribute>? <value>?
// <widget-name> := /[$a-zA-Z_-][$0-9a-zA-Z_-]+/
// <attribute> := ":" <attrib-name>
// <attrib-name> := /[$a-zA-Z_-][$0-9a-zA-Z_-]+/
// <value> := "=" <data>
// <data> := "true" | "false" | "null" | <number> | <string>
//
// @example
// <wdg:checkbox bind:value="btn1:action" />
// <wdg:checkbox bind:value="btn1:action, btn2, action=false" />
if ( typeof postInit[key.substr( 5 )] === 'undefined' ) {
postInit[key.substr( 5 )] = {};
}
postInit[key.substr( 5 )].B = parseBinding(root.attribs[key]);
hasPostInit = true;
}
else if ( key.substr( 0, 5 ) == 'slot:' ) {
// @example
// <wdg:button slot:action="removeOrder" />
// <wdg:button slot:action="removeOrder, changePage" />
// <wdg:button slot:action="my-module:my-function" />
values = root.attribs[key].split( "," );
key = key.substr( 5 );
if ( typeof postInit[key] === 'undefined' ) {
postInit[key] = {};
}
slots = [ ];
values.forEach( function( val ) {
// Before the colon (:) there is the module name. After, there
// is the function name. If there is no colon, `APP` is used
// as module.
val = val.split( ':' );
if ( val.length < 2 ) {
slots.push(val[0].trim( ));
} else {
slots.push(val.map( String.trim ));
libs.require(val[0]);
}
});
postInit[key].S = slots;
hasPostInit = true;
}
else {
com.attr[key] = root.attribs[key];
}
}
if ( hasPostInit ) {
libs.addPostInitJS( " W.bind('" + com.attr.id + "'," + JSON.stringify( postInit ) + ");" );
}
}
/**
@example
<wdg:combo $key="fr">
<content json>
{
"en": "English",
"fr": "Français",
"it": "Italiano"
}
</content>
</wdg:combo>
*/
function parseChildrenProperties( root, libs, com, indent ) {
if (!Array.isArray( root.children )) {
root.children = [ ];
}
root.children.forEach( function( child ) {
debugger;
if ( child.type != libs.Tree.TAG ) {
return;
}
libs.compileChildren( child );
if ( typeof child.attribs === 'undefined' ) {
child.attribs = {};
}
if ( child.attribs.json === null ) {
parsePropertyJSON( child, libs, com );
}
else {
// By default, this is a list.
parsePropertyList( child, libs, com, indent + " " );
}
});
}
function parsePropertyJSON( root, libs, com ) {
var text = libs.Tree.text( root ).trim( );
try {
var obj = ToloframeworkPermissiveJson.parse( text );
com.prop[root.name] = JSON.stringify( obj , null, ' ' );
console.info("[x-widget.com] obj=", obj);
} catch ( ex ) {
libs.fatal( "Unable to parse JSON value of property `" + root.name + "`: " + ex + "\n" + text );
}
}
/**
* @example
*
* <wdg:layout-line>
* <content list>
* <div>
* J'aime bien les <b>crevettes</b>. Pas vous ?
* </div>
* <wdg:button $text="Yes" />
* <wdg:button $text="Nein !" />
* </content>
* </wdg:layout-line>
*/
function parsePropertyList( root, libs, com, indent ) {
var first = true;
var out = '[';
libs.compileChildren( root );
root.children.forEach( function( child ) {
if ( child.type != libs.Tree.TAG ) {
if ( child.type == libs.Tree.TEXT || child.type == libs.Tree.ENTITY ) {
if ( child.text.trim( ).length === 0 ) {
return;
}
if ( first ) {
first = false;
} else {
out += ",";
}
out += "\n" + indent + JSON.stringify( child.text );
}
return;
}
if ( first ) {
first = false;
} else {
out += ",";
}
out += "\n" + indent;
if (isWidget( child )) {
out += parseWidget( child, libs, com, indent + ' ' );
} else {
out += parseElement( child, libs, com, indent + ' ' );
}
});
out += ']';
com.prop[root.name] = out;
}
function parseElement( root, libs, com, indent ) {
var out = "W({\n" + indent + " elem: " + JSON.stringify( root.name );
var attr = {},
hasAttributes = false;
var prop = {},
hasProperties = false;
var attKey,
attVal;
for ( attKey in root.attribs ) {
attVal = root.attribs[attKey];
if ( attKey.charAt( 0 ) == '$' ) {
hasProperties = true;
prop[attKey.substr( 1 )] = JSON.stringify( attVal );
} else {
hasAttributes = true;
attr[attKey] = JSON.stringify( attVal );
}
};
if ( hasAttributes ) {
out += ",\n" + indent + " attr: " + stringifyProperties( attr, indent + ' ' );
}
if ( hasProperties ) {
out += ",\n" + indent + " prop: " + stringifyProperties( prop, indent + ' ' );
}
var children = [ ];
root.children.forEach( function( child ) {
if ( child.type == libs.Tree.TEXT || child.type == libs.Tree.ENTITY ) {
children.push(JSON.stringify( child.text ));
} else if ( child.type == libs.Tree.TAG ) {
if (isWidget( child )) {
children.push(parseWidget( child, libs, com, indent + ' ' ));
} else {
children.push(parseElement( child, libs, com, indent + ' ' ));
}
}
});
if ( children.length > 0 ) {
out += ",\n" + indent + " children: " + stringifyArray( children, indent + ' ' );
}
return out + "})";
}
function parseWidget( root, libs, parent, indent ) {
var com = parseComponent( root, libs, indent );
libs.require( com.name );
var code = "W('" + com.attr.id + "','"
+ com.name + "',"
+ stringifyProperties( com.prop, indent )
+ "," + JSON.stringify(com.attr) + ")";
return code;
}
function isWidget( root ) {
var name = root.name;
if ( name.substr( 0, 4 ) == 'wdg:' || name === 'x-widget' || name === 'x-wdg' ) {
return true;
}
return false;
}
var parseBinding = ( function( ) {
var Lexer = require( 'tlk-lexer' );
var lexer = new Lexer({ value: "(-?(\.[0-9]+|[0-9]+(\.[0-9]+)?))|true|false|null|('(\\.|[^\\']+)*')", comma: "[ \t\n\r]*,[ \t\n\r]*", colon: "[ \t\n\r]*:[ \t\n\r]*", equal: "[ \t\n\r]*=[ \t\n\r]*", name: "[$a-zA-Z_-][$a-zA-Z_0-9-]+" });
/**
* Syntaxe :
* <bind> := <bind-item> <bind-next>*
* <bind-next> := "," <bind-item>
* <bind-item> := <widget-name> <attribute>? <value>?
* <widget-name> := /[$a-zA-Z_-][$0-9a-zA-Z_-]+/
* <attribute> := ":" <attrib-name>
* <attrib-name> := /[$a-zA-Z_-][$0-9a-zA-Z_-]+/
* <value> := "=" <data>
* <data> := "true" | "false" | "null" | <number> | <string>
*/
return function( code ) {
code = code.trim( );
lexer.loadText( code );
var tkn,
widget,
attribute = 'action',
value,
bindings = [ ];
function addBinding( ) {
if ( typeof widget === 'string' ) {
var binding = [ widget, attribute ];
if ( typeof value !== 'undefined' ) {
binding.push( value );
}
bindings.push( binding );
widget = undefined;
attribute = 'action';
value = undefined;
}
}
while ( true ) {
tkn = lexer.next( );
if ( null === tkn )
break;
if ( tkn.id != 'name' )
throw Error( "Expected `name`, but found `" + tkn.id + "`!`" );
widget = lexer.text( tkn );
tkn = lexer.next( );
if ( null === tkn )
break;
if ( tkn.id == 'colon' ) {
tkn = lexer.next( );
if ( null === tkn )
throw Error( "Missing `name` after `:`!`" );
if ( tkn.id != 'name' )
throw Error( "Expected `name` after `:`, but found `" + tkn.id + "`!`" );
attribute = lexer.text( tkn );
tkn = lexer.next( );
if ( null === tkn )
break;
}
if ( tkn.id == 'equal' ) {
tkn = lexer.next( );
if ( null === tkn )
throw Error( "Missing `value` after `=`!`" );
if ( tkn.id != 'value' )
throw Error( "Expected `value` after `=`, but found `" + tkn.id + "`!`" );
value = lexer.text( tkn );
tkn = lexer.next( );
if ( null === tkn )
break;
}
if ( tkn.id != 'comma' )
throw Error( "Expected `comma`, but found `" + tkn.id + "`!`" );
addBinding( );
}
addBinding( );
//console.info("[x-widget.com] bindings=...", bindings);
return bindings;
};
})( );