diff --git a/benchmark.js b/benchmark.js
index 8e4c7bc3..e39ac7c1 100644
--- a/benchmark.js
+++ b/benchmark.js
@@ -8,17 +8,19 @@ var HAWKEJSbase = "\n
\n <% assign('main') %>\n
\n";
var HAWKEJSmain = "\n<% expands('base') %>\n<% start('main') %>\n\t<% implement('topbar') %>\n\t\n\t\t<% assign('container-fluid') %>\n\t
\n<% end('main') %>\n";
var HAWKEJStopbar = "\n\n";
var HAWKEJSindex = "\n<% expands('main') %>\n<% start('container-fluid') %>\nWelcome on the index page!
\nTimestamp: <%= timestamp %>\n<% end('container-fluid') %>\n";
+var HAWKEJSlink = "<% add_link('/admin', {title: 'Dashboard', matchref: {class: 'active'}}) %>";
hawkejs.storeTemplate("base", HAWKEJSbase);
hawkejs.storeTemplate("main", HAWKEJSmain);
hawkejs.storeTemplate("topbar", HAWKEJStopbar);
hawkejs.storeTemplate("index", HAWKEJSindex);
+hawkejs.storeTemplate("link", HAWKEJSlink);
var timestamp = new Date().getTime();
var username = "Skerit";
// add tests
-suite.add('Hawkejs', function() {
+suite.add('Hawkejs (big template)', function() {
hawkejs.render('index',
{username: username,
@@ -27,10 +29,15 @@ suite.add('Hawkejs', function() {
// Receive the result object
});
})
-/*.add('String#indexOf', function() {
- 'Hello World!'.indexOf('o') > -1;
+.add('Hawkejs#add_link', function() {
+ hawkejs.render('link',
+ {username: username,
+ timestamp: timestamp},
+ false, function($result, ne, payload) {
+ // Receive the result object
+ });
})
-.add('String#match', function() {
+/*.add('String#match', function() {
!!'Hello World!'.match(/o/);
})*/
// add listeners
@@ -38,7 +45,7 @@ suite.add('Hawkejs', function() {
console.log(String(event.target));
})
.on('complete', function() {
- console.log('Fastest is ' + this.filter('fastest').pluck('name'));
+ //console.log('Fastest is ' + this.filter('fastest').pluck('name'));
})
// run async
.run({ 'async': true });
\ No newline at end of file
diff --git a/lib/hawkejs-client.js b/lib/hawkejs-client.js
index 11cd2566..97093711 100644
--- a/lib/hawkejs-client.js
+++ b/lib/hawkejs-client.js
@@ -15,7 +15,7 @@ window.onload=function() {
delete payload[i];
}
}
-
+ console.log(payload);
payload.__reAddHelpers = true;
history.pushState(payload, null, url);
diff --git a/lib/hawkejs.js b/lib/hawkejs.js
index 2fe4782a..0477dcd3 100644
--- a/lib/hawkejs.js
+++ b/lib/hawkejs.js
@@ -3,6 +3,7 @@ if (ClientSide === undefined) var ClientSide = false;
// Noclient>
var uglify = require('uglify-js')
var cheerio = require('cheerio');
+//var ce = require('cloneextend');
var jQuery = require('jquery');
var path = require('path');
var ent = require('ent');
@@ -63,6 +64,15 @@ var µ = {};
//
//// Overwrite the root
//clone._root = _root;
+
+ // Version 3: +/- 492 ops/s
+ //µ.getObject(object);
+ //var clone = cheerio.load(object.$.root());
+
+ // Version 4: +/- 570 ops/s
+ // Using cloneextend
+ //µ.getObject(object);
+ //var clone = ce.clone(object.$);
return clone;
}
@@ -231,6 +241,19 @@ hp._clientBrowserPath = false;
*/
hp._debug = false;
+// Place to store values on the client side
+hp.storage = {
+
+ // Match instructions
+ match: {},
+
+ // Previous match states AS instructions
+ previous: [],
+
+ // Next match states AS instructions
+ next: []
+};
+
// Noclient>
/**
* Enable client-side suport
@@ -606,7 +629,7 @@ hp.render = function render (template, variables, $update, callback) {
// If this isn't an ajax call, and client side support is enabled,
// inject the hawkejs-client-side.js file
- if (thisHawk._client /*&& !variables.hawkejs.ajax*/) {
+ if (thisHawk._client && !variables.hawkejs.ajax) {
thisHawk._addScript({path: thisHawk._clientBrowserPath}, $result);
}
@@ -864,6 +887,18 @@ hp._preparePayload = function _preparePayload (variables, forAjax) {
// Tags to be injected are stored here
payload.request.tags = {};
+ // Update instructions when the given location matches our url
+ // The client needs to know about this, so it is injected as javascript
+ payload.request.match = {};
+
+ // History saves
+ payload.history = {};
+
+ // Previous page
+ // @todo: implement this!
+ payload.history.previous = false;
+ payload.history.next = false;
+
// Also create a link to the variables in the payload,
// even though we'll also add them one level further down
payload.scope.variables = variables;
@@ -936,10 +971,191 @@ hp.applyChanges = function applyChanges ($document, payload) {
// Insert blocks into spaces
this._insertBlock($document, payload);
+ // Before doing url matches, revert to the previous state
+ if (ClientSide && this.storage.previous.length) {
+ // @todo: check for history action (going back/forward)
+ this._applyAttributes($document, this.storage.previous[0], false);
+ }
+
+ // Do url match events
+ this._matchLocation($document, payload);
+
// Return the element
return $document;
}
+/**
+ * Perform all the href matching.
+ * Also stores the instruction on the client side
+ *
+ * @author Jelle De Loecker
+ * @since 2013.01.25
+ * @version 2013.01.25
+ *
+ * @param {Cheerio} $origin The origin element
+ * @param {Object} payload The payload
+ */
+hp._matchLocation = function _matchLocation ($origin, payload) {
+
+ // Store the match instructions on the client
+ this._extendClientVar('match', payload.request.match, $origin);
+
+ if (ClientSide) {
+ var matchObject = this.storage.match;
+ } else {
+ var matchObject = payload.request.match;
+ }
+
+ // The source page href we are on now
+ var here = payload.hawkejs.matchSource;
+
+ // Store the previous status in here
+ var previous = {here: here, $this: false, status: {}};
+
+ // See if there is an entry for this href
+ if (matchObject[here]) {
+
+ for (var elementid in matchObject[here]) {
+
+ // Create a previous entry
+ previous.status[elementid] = {};
+
+ // Get the instructions
+ var instructions = matchObject[here][elementid];
+
+ // Select the destination
+ var $this = µ.select($origin, '#' + elementid);
+
+ // Store this object if we're on the client side
+ if (ClientSide) previous.$this = $this;
+
+ // Apply the changes
+ this._applyAttributes($this, instructions, previous.status[elementid]);
+
+ }
+ }
+
+ // Store the previous settings on the client
+ this._unshiftClientVar('previous', previous, $origin);
+
+ // Delete $this, history api doesn't allow it
+ var undoMatch = jQuery.extend({}, previous);
+ delete undoMatch.$this;
+
+ // And also store them in the payload, for history stuff
+ payload.request.undoMatch = undoMatch;
+}
+
+/**
+ * Apply attributes to the given $element
+ *
+ * @author Jelle De Loecker
+ * @since 2013.01.25
+ * @version 2013.01.25
+ *
+ * @param {object} $this The document to update
+ * @param {object} ins The instructions to execute
+ * @param {object} previous Place where previous states can be stored
+ * AS instructions
+ */
+hp._applyAttributes = function _applyAttributes ($this, ins, previous) {
+
+ var instructions = ins;
+
+ // If a status entry is available, then this is
+ // an array of elementid's we need to apply attributes to
+ if (ins && ins.status) {
+
+ // We were passed an entire object of elements to update ...
+ for (var elementid in ins.status) {
+
+ var givePrev = false;
+
+ // Create an entry for this element
+ if (previous && !previous[elementid]) {
+ previous[elementid] = {};
+ givePrev = previous[elementid];
+ }
+
+ // Select the target
+ var $target = µ.select($this, '#' + elementid);
+
+ // Recursively apply attributes
+ this._applyAttributes($target, ins.status[elementid], givePrev);
+ }
+
+ // Do not execute the code any further
+ return;
+ }
+
+ // Store the attributes as they are now
+ for (var attribute in instructions) {
+
+ // Certain attributes do not have to be processed
+ if (attribute == 'sourceId') continue;
+
+ // Get the new value
+ var value = instructions[attribute];
+
+ // Recurse when we reach a parent object
+ if (attribute == 'parent') {
+
+ var prevlink = false;
+
+ // Create a previous entry for the parent
+ if (previous) {
+ previous[attribute] = {};
+ prevlink = previous[attribute];
+ }
+
+ hp._applyAttributes($this.parent(), value, prevlink);
+ continue;
+ }
+
+ if (attribute == 'content') {
+
+ var oldVal = $this.text();
+
+ } else {
+
+ // Get the old value
+ var oldVal = $this.attr(attribute);
+
+ // If oldVal is undefined, we need to add an empty string
+ if (oldVal === undefined) oldVal = ' ';
+ }
+
+ // Do not replace class, but append
+ if (attribute == 'class') {
+
+ $this.addClass(value);
+
+ // History, however, must remove this class
+ if (previous) previous['replaceClass'] = oldVal;
+ continue;
+
+ } if (attribute == 'replaceClass') {
+
+ // Remove all current classes
+ $this.removeClass();
+
+ // Add the new class
+ $this.addClass(value);
+
+ if (previous) previous['replaceClass'] = oldVal;
+
+ } else if (attribute == 'content') {
+ $this.html(value);
+ } else {
+ $this.attr(attribute, value);
+ }
+
+ // Add it to the previous entry
+ if (previous) previous[attribute] = oldVal;
+ }
+
+}
+
/**
* Fill in all implementation spaces
* You pass the Cheerio document, and it looks for things inside the payload
@@ -1052,12 +1268,142 @@ hp._insertBlock = function _insertBlock ($origin, payload) {
}
}
+/**
+ * Extend a hawkejs variable on the client side *RECURSIVELY*
+ *
+ * @author Jelle De Loecker
+ * @since 2013.01.24
+ * @version 2013.01.24
+ *
+ * @param {string} name The name of the variable
+ * @param {object} value The value
+ * @param {object} $element doc we can add the script to (if from server)
+ */
+hp._extendClientVar = function _extendClientVar (name, value, $element) {
+
+ if (ClientSide) {
+ // Make a deep extend
+ $.extend(true, this.storage[name], value);
+ } else {
+ // This code is sent to the client, he will then do the code above us
+ this.execOnClient('hawkejs._extendClientVar', [name, value], $element);
+ }
+}
+
+/**
+ * Merge a hawkejs array on the client side
+ *
+ * @author Jelle De Loecker
+ * @since 2013.01.24
+ * @version 2013.01.24
+ *
+ * @param {string} name The name of the variable
+ * @param {object} value The value
+ * @param {object} $element doc we can add the script to (if from server)
+ */
+hp._mergeClientVar = function _mergeClientVar (name, value, $element) {
+
+ if (ClientSide) {
+ $.merge(this.storage[name], value);
+ } else {
+ // This code is sent to the client, he will then do the code above us
+ this.execOnClient('hawkejs._mergeClientVar', [name, value], $element);
+ }
+}
+
+/**
+ * Unshift a value to a hawkejs array on the client side
+ *
+ * @author Jelle De Loecker
+ * @since 2013.01.25
+ * @version 2013.01.25
+ *
+ * @param {string} name The name of the variable
+ * @param {object} value The value
+ * @param {object} $element doc we can add the script to (if from server)
+ */
+hp._unshiftClientVar = function _unshiftClientVar (name, value, $element) {
+
+ if (ClientSide) {
+ if (this.storage[name] === undefined) this.storage[name] = [];
+ this.storage[name].unshift(value);
+
+ log('Unshifted ' + name);
+ log(this.storage[name]);
+
+ } else {
+ // This code is sent to the client, he will then do the code above us
+ this.execOnClient('hawkejs._unshiftClientVar', [name, value], $element);
+ }
+}
+
+/**
+ * Push a value to a hawkejs array on the client side
+ *
+ * @author Jelle De Loecker
+ * @since 2013.01.24
+ * @version 2013.01.24
+ *
+ * @param {string} name The name of the variable
+ * @param {object} value The value
+ * @param {object} $element doc we can add the script to (if from server)
+ */
+hp._pushClientVar = function _pushClientVar (name, value, $element) {
+
+ if (ClientSide) {
+ if (this.storage[name] === undefined) this.storage[name] = [];
+ this.storage[name].push(value);
+ } else {
+ // This code is sent to the client, he will then do the code above us
+ this.execOnClient('hawkejs._pushClientVar', [name, value], $element);
+ }
+}
+
+/**
+ * Execute this function on the client, from the server.
+ * Basically a simple function call.
+ *
+ * @author Jelle De Loecker
+ * @since 2013.01.24
+ * @version 2013.01.24
+ *
+ * @param {string} functionname The function to be called
+ * @param {array} parameters The parameters to be passed
+ * @param {object} $element doc we can add the script to
+ * @param {string} destination Where to put the code
+ */
+hp.execOnClient = function execOnClient (functionname, parameters, $element, destination) {
+
+ // Destination is the bottom of the document by default
+ if (destination === undefined) destination = 'bottom';
+
+ var passParam = '';
+
+ // Create the parameters part of the call
+ for (var i in parameters) {
+ var p = parameters[i];
+
+ if (passParam) passParam += ', ';
+
+ passParam += JSON.stringify(p);
+ }
+
+ var code = functionname + '(' + passParam + ');';
+
+ // Add the code to the page
+ this._addScript({code: code, destination: destination}, $element);
+}
+
/**
* Add a script to the element
*
+ * script:
+ * - path {string} Is this a link to a javascript file?
+ * - code {string} Or is this a code block?
+ *
* @author Jelle De Loecker
* @since 2013.01.19
- * @version 2013.01.20
+ * @version 2013.01.24
*
* @param {object} script The script object
* @param {Cheerio} $element The element to insert it into
@@ -1068,7 +1414,15 @@ hp._addScript = function _addScript (script, $element) {
if (!script.destination) script.destination = 'anywhere';
- var html = '';
+ var html = '';
var newElement = this._addHtml($element, script.destination, html);
@@ -1958,13 +2312,47 @@ helpers.parse_element = function parse_element (elementname, options) {
}
+/**
+ * Events that need to happen to a DOM object when the given href
+ * matches the location of the current page
+ *
+ * These events are added to the payload.
+ *
+ * options:
+ * - sourceHref {string} The href WITHOUT replaced vars (so ":name")
+ *
+ * @param {string} elementid The name of the element to update
+ * @param {object} options The options object
+ */
+helpers._add_match_options = function _add_match_options (elementid, instructions) {
+
+ // Get the sourceHref we want to match against
+ var href = instructions.sourceHref;
+ delete instructions.sourceHref;
+
+ // If the parent entry isn't an object, delete it!
+ if (instructions.parent && typeof instructions.parent != "object") {
+ delete instructions.parent;
+ }
+
+ // Also add the source id to the object
+ instructions.sourceId = elementid;
+
+ // Create the match entry if it doesn't exist
+ if (this.request.match[href] === undefined) {
+ this.request.match[href] = {};
+ }
+
+ this.request.match[href][elementid] = instructions;
+}
+
/**
* Add an anchor tag to the buffer
*
* options:
* - name {string} The text inside the anchor tag. Uses the href if absent
* - title {string} The title (hover) text. Uses name if absent
- * - id {string} The id of the tag. Empty if absent
+ * - id {string} The id of the tag. If absent an id is composed.
* - class {string} The classes of the tag. Empty if absent
* - match {string} The express route string to match against (eg /a/:id)
* - return {string} What this function should return. Defaults to 'print'
@@ -1981,7 +2369,7 @@ helpers.parse_element = function parse_element (elementname, options) {
*
* @author Jelle De Loecker
* @since 2013.01.20
- * @version 2013.01.24
+ * @version 2013.01.25
*
* @param {string} href The link
* @param {object} options
@@ -1990,51 +2378,50 @@ helpers.parse_element = function parse_element (elementname, options) {
* the html string, or nothing.
*/
helpers.add_link = function add_link (href, options) {
-
+
// Store the original href
- var hrefSource = href;
+ var sourceHref = href;
if (options === undefined) options = {};
if (!options.name) options.name = options.title ? options.title : href;
if (!options.title) options.title = options.name;
- if (!options.id) options.id = '';
if (!options.class) options.class = '';
if (!options.return) options.return = 'print';
if (!options.urlvars) options.urlvars = {};
- if (!options.matchref) options.matchref = {};
// Implement variables in urls
for (var varName in options.urlvars) {
href = href.replace(':' + varName, options.urlvars[varName]);
}
- // See if there are special match conditions
- for (var matchSource in options.matchref) {
-
- if (this.hawkejs.matchSource == hrefSource) {
-
- var eo = options.matchref;
-
- // Do what it tells us!
- if (eo.class) options.class += ' ' + eo.class;
- if (eo.title) options.title = eo.title;
- if (eo.href) href = eo.href;
- if (eo.id) options.id = eo.id;
- if (eo.name) options.name = eo.name;
- }
+ // Always add an id!
+ if (!options.id) {
+ options.id = 'hawkejs-link-' + this.filename.replace('/', '-') + '-' + href.replace(/[^a-z0-9]/gi,'');
+ }
+
+ // Name is actually content of the element
+ if (!options.content) options.content = options.name;
+
+ // Escape the content if needed
+ if (options.escape) options.content = ent.encode(options.content);
+
+ if (options.match) {
+
+ // Add the source href
+ options.match.sourceHref = sourceHref;
+
+ this._add_match_options(options.id, options.match);
}
- var a = '' + ent.encode(options.name) + '';
+ a += '>' + options.content + '';
// Add the result to the options
options.html = a;