aboutsummaryrefslogtreecommitdiff
path: root/src/js/fiveui/js
diff options
context:
space:
mode:
authorGravatar Trevor Elliott <trevor@galois.com>2013-06-07 11:59:57 -0700
committerGravatar Trevor Elliott <trevor@galois.com>2013-06-07 11:59:57 -0700
commit8690d56ff0bbd1031e8cc6788dd2159aac6b7adb (patch)
treed573336305dc756f357f20dd986f77cb3ceafb9b /src/js/fiveui/js
parentf42930c2226d07482725b03ad522c106c0bdec8b (diff)
Reorganize the build system
* Move all javascript source to src/js * Update the test runner, etc. * Do less javascript compilation
Diffstat (limited to 'src/js/fiveui/js')
-rw-r--r--src/js/fiveui/js/background.js210
-rw-r--r--src/js/fiveui/js/chan.js53
-rw-r--r--src/js/fiveui/js/entry.js277
-rw-r--r--src/js/fiveui/js/ffcheck.js3
-rw-r--r--src/js/fiveui/js/messenger.js141
-rw-r--r--src/js/fiveui/js/options.js217
-rw-r--r--src/js/fiveui/js/rules.js292
-rw-r--r--src/js/fiveui/js/set.js58
-rw-r--r--src/js/fiveui/js/settings.js389
-rw-r--r--src/js/fiveui/js/state.js202
-rw-r--r--src/js/fiveui/js/update-manager.js51
-rw-r--r--src/js/fiveui/js/url-pat.js140
-rw-r--r--src/js/fiveui/js/utils.js145
13 files changed, 2178 insertions, 0 deletions
diff --git a/src/js/fiveui/js/background.js b/src/js/fiveui/js/background.js
new file mode 100644
index 0000000..c0d4e07
--- /dev/null
+++ b/src/js/fiveui/js/background.js
@@ -0,0 +1,210 @@
+/*
+ * Module : background.js
+ * Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer :
+ * Stability : Provisional
+ * Portability: Portable
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var fiveui = fiveui || {};
+
+(function() {
+/**
+ * @constructor
+ *
+ * @param {!function(!string):!string} dataLoader
+ */
+fiveui.Background = function(settings, updateWidget, loadScripts, dataLoader) {
+ // Initialize a storage mechanism for FiveUI.
+ // Chrome utilizes HTML5's localStorage API as a backend, but other
+ // contexts (such as Firefox) would supply a different backend
+ // storage to the settings constructor.
+ //
+ // The data in settings is persisted across executions of the
+ // context and the plugin. It is used for long-term storage of user
+ // preferences, rule sets, and similarly long-lived FiveUI settings.
+ this.settings = settings;
+
+ // the state of all managed tabs
+ this.state = new fiveui.State(settings);
+
+ // how we communicate with the status widget
+ this.updateWidget = updateWidget;
+
+ // how we signal the browser to load a content script
+ this.loadScripts = loadScripts;
+
+ this.dataLoader = dataLoader;
+};
+
+fiveui.Background.prototype._registerComputeListeners = function(port, tabState){
+ var bg = this;
+ port.on('ReportProblem', function(request) {
+ var problem = fiveui.Problem.fromJSON(request);
+ if(tabState.addProblem(problem) && tabState.uiPort != null) {
+ bg.updateWidget(tabState);
+ tabState.uiPort.emit('ShowProblem', problem);
+ }
+ });
+ port.on('ReportStats', function (stats) {
+ if (tabState.addStats(stats) && tabState.uiPort != null) {
+ bg.updateWidget(tabState);
+ tabState.uiPort.emit('ShowStats', stats);
+ }
+ });
+};
+
+fiveui.Background.prototype._registerUiListeners = function(port, tabState){
+ var bg = this;
+ port.on('Position', function(request) {
+ tabState.winState.x = request.left;
+ tabState.winState.y = request.top;
+ });
+ port.on('Size', function(request) {
+ tabState.winState.width = request.width;
+ tabState.winState.height = request.height;
+ });
+ port.on('CloseUI', function(request) {
+ tabState.winState.closed = true;
+ });
+ port.on('ClearProblems', function(request) {
+ tabState.clearProblems();
+ bg.updateWidget(tabState);
+ });
+ port.on('MaskRules', function(request) {
+ _.each(tabState.computePorts, function(cp) {
+ cp.emit('MaskRules', null);
+ });
+ });
+ port.on('UnmaskRules', function(request) {
+ _.each(tabState.computePorts, function(cp) {
+ cp.emit('UnmaskRules', null);
+ });
+ });
+};
+
+/**
+ * Accept a new connection from a content script.
+ */
+fiveui.Background.prototype.connect = function(tabId, port, url, isUiPort) {
+
+ var tabState = this.state.acquireTabState(tabId);
+
+ if (isUiPort) {
+ tabState.uiPort = port;
+ this._registerUiListeners(port, tabState);
+ port.emit('RestoreUI', tabState.toEmit());
+ this.updateWidget(tabState);
+ } else {
+ tabState.computePorts.push(port);
+ this._registerComputeListeners(port, tabState);
+
+ // get the rule set and send it down to the injected page:
+ var pat = this.settings.checkUrl(url);
+ if (!pat) {
+ console.err('could not find url pattern for tab.url, but one was strongly expected');
+ } else {
+ var ruleSet = this.settings.getRuleSet(pat.rule_id);
+
+ port.emit('SetRules', ruleSet);
+ }
+ }
+};
+
+/**
+ * @param {!number} tabId
+ * @param {!string} url
+ * @param {*} data
+ */
+fiveui.Background.prototype.pageLoad = function(tabId, url, data) {
+ var pat = this.settings.checkUrl(url);
+
+ if (null == pat) {
+ this.updateWidget(null);
+ } else {
+ var tabState = this.state.acquireTabState(tabId);
+ tabState.computePorts = [];
+
+ this.updateWidget(tabState);
+
+ var dependencies = [];
+ var ruleSet = this.settings.getRuleSet(pat.rule_id);
+
+ if (ruleSet && ruleSet.dependencies ) {
+ dependencies = ruleSet.dependencies;
+ }
+
+ var computeScripts = _.flatten(
+ [ [ this.dataLoader('jquery/jquery-1.8.3.js')
+ , this.dataLoader('md5.js')
+ , this.dataLoader('injected/prelude.js')
+ , this.dataLoader('injected/jquery-plugins.js')
+ ]
+ , dependencies
+ , [ this.dataLoader('injected/compute.js')
+ ]
+ ]);
+ this.loadScripts(tabId, computeScripts, true, data);
+
+ var uiScripts = _.flatten(
+ [ this.dataLoader('jquery/bundled.css')
+ , this.dataLoader('jquery/jquery-1.8.3.js')
+ , this.dataLoader('jquery/jquery-ui-1.9.2.custom.js')
+ , this.dataLoader('injected/injected.css')
+ , this.dataLoader('injected/prelude.js')
+ , this.dataLoader('injected/ui.js')
+ , this.dataLoader('injected/jquery-plugins.js')
+ ]);
+ this.loadScripts(tabId, uiScripts, false, data);
+ }
+};
+
+/**
+ * Updates the widget according to the tab state of the specified tab.
+ *
+ * @param {!number} tabId Id of the tab that is currently active, and
+ * thus, dictating the widget display.
+ */
+fiveui.Background.prototype.activate = function(tabId) {
+ var tabState = this.state.getTabState(tabId);
+ this.updateWidget(tabState);
+};
+
+/**
+ * Stop tracking the state of a tab.
+ *
+ * @param {!number} tabId Id of the tab to free the state of.
+ */
+fiveui.Background.prototype.removeTab = function(tabId) {
+ this.state.removeTabState(tabId);
+};
+
+/**
+ * Request that the user interface be restored, if it is closed.
+ */
+fiveui.Background.prototype.showUI = function(tabId) {
+ var tabState = this.state.getTabState(tabId);
+ if(null == tabState) {
+ return;
+ }
+
+ if(tabState.winState.closed) {
+ tabState.winState.closed = false;
+ tabState.uiPort.emit('ShowUI', null);
+ }
+};
+
+})();
diff --git a/src/js/fiveui/js/chan.js b/src/js/fiveui/js/chan.js
new file mode 100644
index 0000000..47c6145
--- /dev/null
+++ b/src/js/fiveui/js/chan.js
@@ -0,0 +1,53 @@
+/*
+ * Module : chan.js
+ * Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer :
+ * Stability : Provisional
+ * Portability: Portable
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var fiveui = fiveui || {};
+
+(function() {
+
+/**
+ * @constructor
+ */
+fiveui.Chan = function() {
+ this.fns = {};
+};
+
+_.extend(fiveui.Chan.prototype, {
+
+ /**
+ * @param {!string} type
+ * @param {!function(*)} fn
+ */
+ on: function(type, fn) {
+ this.fns[type] = fn;
+ },
+
+ /**
+ * @param {!string} type
+ * @param {*} data
+ */
+ emit: function(type, data) {
+ this.chan.fns[type](data);
+ }
+
+});
+
+})();
diff --git a/src/js/fiveui/js/entry.js b/src/js/fiveui/js/entry.js
new file mode 100644
index 0000000..025034a
--- /dev/null
+++ b/src/js/fiveui/js/entry.js
@@ -0,0 +1,277 @@
+/*
+ * Module : entry.js
+ * Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer :
+ * Stability : Provisional
+ * Portability: Portable
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+var fiveui = fiveui || {};
+
+(function() {
+
+var editable = function(el, model, placeholder) {
+
+ el.prop('contenteditable', true).addClass('editable');
+
+ // prevent newlines
+ el.on('keypress', function(e) {
+ return e.which != 13;
+ });
+
+ var addPlaceholder = function() {
+ el.addClass('placeholder')
+ .text(placeholder)
+ .one('click keypress paste', remPlaceholder);
+ };
+
+ var remPlaceholder = function() {
+ el.removeClass('placeholder').text('');
+ };
+
+ // if the model is new, set the placeholder, and a listener to clear it
+ if(model.isNew()) {
+ addPlaceholder();
+ }
+
+ el.on('blur', function() {
+ if(_.isEmpty(el.text())) {
+ addPlaceholder();
+ }
+ });
+
+ el.focus();
+
+};
+
+
+/** UrlPat Entry Elements ****************************************************/
+
+fiveui.UrlPatEntry = Backbone.View.extend({
+
+ tagName: 'li',
+
+ className: 'entry',
+
+ events: {
+ 'click span.save' : 'save',
+ 'click span.remove' : 'remove',
+ 'click span.edit' : 'edit',
+ },
+
+ initialize:function() {
+ this.listenTo(this.model, 'remove', function() {
+ this.$el.remove();
+ this.stopListening();
+ });
+ },
+
+ viewTemplate: _.template(
+ [ '<div>'
+ , ' <span class="button remove">x</span>'
+ , ' <span class="button edit">edit</span>'
+ , ' <span><%= regex %></span>'
+ , ' <span><%= rule_name %></span>'
+ , '</div>'
+ ].join('')),
+
+ render:function() {
+ var attrs = _.clone(this.model.attributes);
+ var ruleSet = this.options.rules.model.findWhere({ id: attrs.rule_id })
+ attrs.rule_name = ruleSet.get('name');
+ this.$el.html(this.viewTemplate(attrs));
+ return this;
+ },
+
+ editTemplate: _.template(
+ [ '<div>'
+ , ' <span class="button remove">x</span>'
+ , ' <span class="button save">save</span>'
+ , ' <span class="regex"></span>'
+ , ' <span class="rules"></span>'
+ , '</div>'
+ ].join('')),
+
+ edit:function() {
+ var attrs = this.model.attributes;
+ this.$el.html(this.editTemplate(attrs));
+
+ this.$el.find('.rules').append(this.options.rules.render().$el);
+ editable(this.$el.find('.regex'), this.model, 'url pattern');
+ return this;
+ },
+
+ remove: function() {
+ this.model.destroy();
+ },
+
+ save:function() {
+ var regex = this.$el.find('.regex').text();
+ var rule_id = parseInt(this.options.rules.$el.val());
+ this.model.save({ regex : regex, rule_id : rule_id }, {
+ success: _.bind(this.render, this),
+ error: _.bind(this.edit, this)
+ });
+ },
+
+});
+
+
+/** Rule Set View ************************************************************/
+
+fiveui.RulesView = Backbone.View.extend({
+
+ tagName: 'select',
+
+ initialize:function() {
+ this.listenTo(this.model, 'sync', this.update);
+ this.listenTo(this.model, 'remove', this.update);
+ },
+
+ optionTemplate:_.template(
+ '<option value="<%= id %>"><%= name %></option>'
+ ),
+
+ update:function() {
+ if(this.model.length == 0) {
+ return this.remove();
+ } else {
+ return this.render();
+ }
+ },
+
+ remove:function() {
+ this.stopListening();
+ this.$el.remove();
+
+ this.trigger('remove');
+
+ return this;
+ },
+
+ render:function() {
+
+ var scope = this;
+
+ this.$el.children().remove();
+
+ var text = this.model.foldl(function(body,ruleSet) {
+ return body + scope.optionTemplate(ruleSet.attributes);
+ }, '');
+
+ this.$el.html(text);
+
+ return this;
+ },
+
+});
+
+
+/** Rule Entry Elements ******************************************************/
+
+fiveui.RuleSetEntry = Backbone.View.extend({
+
+ tagName: 'li',
+
+ className: 'entry',
+
+ events: {
+ 'click .save' : 'save',
+ 'click .remove' : 'remove',
+ 'click .edit' : 'edit',
+ 'click .reload' : 'reload',
+ },
+
+ viewTemplate: _.template(
+ [ '<div class="content">'
+ , ' <span class="button remove">x</span>'
+ , ' <span class="button edit">edit</span>'
+ , ' <span class="button reload">reload</span>'
+ , ' <span class="title"><%= name %></span>'
+ , '</div>'
+ ].join('')),
+
+ render:function() {
+ var attrs = this.model.attributes;
+ this.$el.html(this.viewTemplate(attrs));
+ return this;
+ },
+
+ editTemplate: _.template(
+ [ '<div class="content">'
+ , ' <span class="button remove">x</span>'
+ , ' <span class="button save">save</span>'
+ , ' <span class="source"><%= source %></span>'
+ , '</div>'
+ ].join('')),
+
+ edit:function() {
+ var attrs = this.model.attributes;
+ this.$el.html(this.editTemplate(attrs));
+
+ editable(this.$el.find('.source'), this.model,
+ 'http://example.com/manifest.json')
+
+ return this;
+ },
+
+ errorTemplate: _.template('<div class="error"><%= message %></div>'),
+
+ editError:function(target, message) {
+ this.edit();
+
+ this.$el.append(this.errorTemplate({ message: message }));
+
+ return this;
+ },
+
+ save: function() {
+ var source = this.$el.find('.source').text();
+ this.model.set('source', source);
+ this.model.save({}, {
+ success: _.bind(this.render, this),
+ error: _.bind(this.editError, this)
+ });
+ },
+
+ reload:function() {
+ this.model.save({}, {
+ success: _.bind(this.render, this),
+ error: _.bind(this.edit, this)
+ });
+ },
+
+ remove:function() {
+ var self = this;
+
+ this.model.destroy({
+ wait:true,
+
+ success:function() {
+ self.$el.remove();
+ self.stopListening();
+ },
+
+ error:function() {
+ console.log('communicate failure somehow...');
+ }
+ });
+ },
+
+});
+
+})();
diff --git a/src/js/fiveui/js/ffcheck.js b/src/js/fiveui/js/ffcheck.js
new file mode 100644
index 0000000..2157ed0
--- /dev/null
+++ b/src/js/fiveui/js/ffcheck.js
@@ -0,0 +1,3 @@
+if (! /Firefox[\/\s](\d+\.\d+)/.test(navigator.userAgent)) {
+ window.onload=fiveui.chrome.options.init;
+}
diff --git a/src/js/fiveui/js/messenger.js b/src/js/fiveui/js/messenger.js
new file mode 100644
index 0000000..fc1962c
--- /dev/null
+++ b/src/js/fiveui/js/messenger.js
@@ -0,0 +1,141 @@
+/*
+ * Module : messenger.js
+ * Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer :
+ * Stability : Provisional
+ * Portability: Portable
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var fiveui = fiveui || {};
+
+(function() {
+
+/**
+ * @constructor
+ * @param {{on: function(!string, function(*)), emit: function(!string, *)}}
+ * channel The object containing on and emit.
+ */
+fiveui.Messenger = function(channel) {
+ this.callbacks = {};
+ this.handlers = {};
+
+ this.channel = channel;
+ this.channel.on(fiveui.Messenger.type, _.bind(this._handler, this));
+};
+
+fiveui.Messenger.type = "fiveui_messaging_type";
+
+_.extend(fiveui.Messenger.prototype, {
+
+ /**
+ * @param {!string} type The message type to send.
+ * @param {?Object} data The payload (which can be null).
+ * @param {?function(?Object)} callback An optional callback to be
+ * invoked in response.
+ */
+ send: function(type, data, callback){
+ var id = null;
+ if (callback) {
+ id = this._newId();
+ this.callbacks[id] = callback;
+ }
+
+ var payload = new fiveui.Messenger.Payload(false, type, data, id);
+ this.channel.emit(fiveui.Messenger.type, payload);
+ },
+
+ /**
+ * Register a handler for the specified message type.
+ *
+ * @param {!string} type The message type.
+ * @param {!function(*)} callback The function to call when a message
+ * of the specified type is received.
+ */
+ register: function(type, callback) {
+ if(null == this.handlers[type]) {
+ this.handlers[type] = [];
+ }
+ this.handlers[type].push(callback);
+ },
+
+ _handler: function(payload) {
+ if (payload.isCallback && payload.id != null) {
+ // this is a callback invocation, lookup the callback and invoke it:
+ this.callbacks[payload.id](payload.data);
+
+ // remove the callback:
+ this._remove(payload.id);
+ } else {
+ // look up a handler and invoke it, passing in the response fn:
+ var hs = this.handlers[payload.type];
+ if (hs && hs.length > 0) {
+
+ // this is a new incomming message.
+ // create a response function:
+ var respond = function(respData) {
+ this.channel.emit(fiveui.Messenger.type,
+ new fiveui.Messenger.Payload(true, payload.type, respData, payload.id));
+ };
+
+ // iterate over the handlers, invoking them with the response callback.
+ _.each(hs, function(h) {
+ h(payload.data, _.bind(respond, this));
+ }, this);
+ }
+ }
+ },
+
+ /**
+ * Remove a callback from the map of callbacks.
+ *
+ * @param {!number} callbackId The id of the callback to remove.
+ */
+ _remove: function(callbackId) {
+ delete this.callbacks[callbackId];
+ },
+
+ /**
+ * @return {!number} The next unique id for a callback.
+ */
+ _newId: function() {
+ var list = Object.keys(this.callbacks);
+ return fiveui.utils.getNewId(list);
+ }
+
+});
+
+/**
+ * @constructor
+ * @param {!boolean} isCallback True if this is in response to a
+ * message, false if this is requesting a callback.
+ * @param {!string} type
+ * @param {?Object} data
+ * @param {!number} id Callback id to invoke, or in which this is a response.
+ */
+fiveui.Messenger.Payload = function(isCallback, type, data, id) {
+ this.isCallback = isCallback;
+ this.type = type;
+ this.id = id;
+ this.__defineGetter__('data', function() {
+ return JSON.parse(this.rawData);
+ });
+ this.__defineSetter__('data', function(obj){
+ this.rawData = JSON.stringify(obj);
+ });
+ this.data = data;
+};
+
+})();
diff --git a/src/js/fiveui/js/options.js b/src/js/fiveui/js/options.js
new file mode 100644
index 0000000..2b29f8d
--- /dev/null
+++ b/src/js/fiveui/js/options.js
@@ -0,0 +1,217 @@
+/*
+ * Module : options.js
+ * Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer :
+ * Stability : Provisional
+ * Portability: Portable
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var fiveui = fiveui || {};
+
+(function() {
+
+
+/*****************************************************************
+ *
+ * Misc. utility functions
+ *
+ ****************************************************************/
+
+var setClickHandler = function(sel, fn, scope) {
+ var boundFn = fn;
+ if (scope) {
+ boundFn = _.bind(fn, scope);
+ }
+
+ sel.on('click', boundFn)
+};
+
+/**
+ * @param {!string} selectEltId The id of the select element to
+ * search for selected items.
+ * @return {!Array.<!number>} An array of 'value' entries from the
+ * selected elements. This may be empty, but it will not be null.
+ */
+var findSelectedIds = function(elt) {
+ var opts = elt.find('option:checked');
+ return _.map(opts, function(o) { return o.value; });
+};
+
+
+fiveui.options = fiveui.options || {};
+
+/**
+ * @param {{on: function(!string, function(*)), emit: function(!string, *)}} port
+ */
+fiveui.options.init = function(port) {
+
+ var msg = new fiveui.Messenger(port);
+ var update = new fiveui.UpdateManager(msg);
+
+ ruleSets = new fiveui.RuleSets([], { url: msg });
+ urlPats = new fiveui.UrlPats([], { url: msg });
+
+
+ /** UrlPat list entries ****************************************************/
+
+ var urlPatEntries = jQuery('#urlPatEntries');
+ var addUrlPat = jQuery('#addUrlPat');
+
+ addUrlPat.prop('disabled', true);
+
+ addUrlPat.on('click', function() {
+ urlPats.add(new fiveui.UrlPatModel({}, { url : msg }));
+ });
+
+ // when a new rule set is sync'd, make sure that the add url pattern button is
+ // enabled.
+ ruleSets.on('sync', function() {
+ if(ruleSets.length > 0) {
+ addUrlPat.prop('disabled', false);
+ }
+ });
+
+ // when a rule set is destroyed, and the collection is now empty, disable the
+ // add url pattern button.
+ ruleSets.on('destroy', function(model,col) {
+ if(col.length <= 0) {
+ addUrlPat.prop('disabled', true);
+ }
+ });
+
+ // handle new url patterns being added to the collection.
+ urlPats.on('add', function(model) {
+ var view = new fiveui.UrlPatEntry({
+ model: model,
+ rules: new fiveui.RulesView({ model: ruleSets })
+ });
+ urlPatEntries.append(view.$el);
+
+ if(model.isNew()) {
+ view.edit();
+ } else {
+ view.render();
+ }
+ });
+
+
+ /** RuleSet list entries ***************************************************/
+
+ var ruleSetEntries = jQuery('#ruleSetEntries');
+
+ // handle clicks to the 'add' button on the rule sets page
+ jQuery('#addRsButton').on('click', function() {
+ ruleSets.add(new fiveui.RuleSetModel({}, { url : msg }));
+ });
+
+ // render a ruleset added to the collection
+ ruleSets.on('add', function(model) {
+ var entry = new fiveui.RuleSetEntry({ model: model })
+ ruleSetEntries.append(entry.$el);
+
+ if(model.isNew()) {
+ entry.edit();
+ } else {
+ addUrlPat.prop('disabled', false);
+ entry.render();
+ }
+ });
+
+
+ /** Basics *****************************************************************/
+
+ var windowDisplayDefault = jQuery('#windowDisplayDefault');
+
+ windowDisplayDefault.on('change', function() {
+ msg.send('setDisplayDefault', windowDisplayDefault.prop('checked'))
+ });
+
+
+ /** Tab Management *********************************************************/
+
+ /**
+ * Select a tab header by Element reference.
+ *
+ * @param {!Element} clicked The navigation element to focus.
+ * @return {void}
+ */
+ var selectNav = function(clicked) {
+ var nav = clicked.parent();
+
+ nav.find('div.selected', nav).removeClass('selected');
+
+ jQuery('div.editorPane').hide();
+
+ clicked.addClass('selected');
+ };
+
+ /**
+ * Focus a tab, by Element reference.
+ *
+ * @param {!Element} el The tab element to focus.
+ * @return {void}
+ */
+ var selectSection = function(el) {
+ var cont = el.parent();
+
+ // hide all sections
+ cont.find('>section').removeClass('selected').hide();
+
+ // display this section
+ el.addClass('selected').show();
+ };
+
+ /**
+ * A combination of selectNav and selectSection that will first lookup the
+ * elements associated with a tab, then return a new function that when
+ * called, will focus both the navigation element, and the tab it controls.
+ *
+ * @param {!string} id The id of the tab to focus.
+ * @return {function()}
+ */
+ var select = function(id) {
+ var sel = jQuery(id);
+ return function() {
+ selectNav(jQuery(this));
+ selectSection(sel);
+ };
+ };
+
+ // listen to click events on navigation elements
+ setClickHandler(jQuery('#url-defaults'), select('#tab-url-defaults'));
+ setClickHandler(jQuery('#rule-sets'), select('#tab-rule-sets'));
+ setClickHandler(jQuery('#basics'), select('#tab-basics'));
+
+ // select the url patterns tab by default
+ selectNav(jQuery('#url-defaults'));
+ selectSection(jQuery('#tab-url-defaults'));
+
+
+ /** Pre-populate UI elements ***********************************************/
+
+ msg.send('getDisplayDefault', null, function(def) {
+ jQuery('#windowDisplayDefault').prop('checked', def);
+ });
+
+ // pre-populate the rule set and url pattern lists
+ ruleSets.fetch({
+ success:function() {
+ urlPats.fetch();
+ }
+ });
+};
+
+})();
diff --git a/src/js/fiveui/js/rules.js b/src/js/fiveui/js/rules.js
new file mode 100644
index 0000000..314d50b
--- /dev/null
+++ b/src/js/fiveui/js/rules.js
@@ -0,0 +1,292 @@
+/*
+ * Module : rules.js
+ * Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer :
+ * Stability : Provisional
+ * Portability: Portable
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var fiveui = fiveui || {};
+
+(function() {
+
+/**
+ * @constructor
+ * @param {!string} module A Javascript module that defines the rule.
+ */
+fiveui.Rule = function(module) {
+ this.module = module;
+};
+
+fiveui.Rule.defaults = function(obj) {
+ return _.defaults(obj, {
+ module: '',
+ });
+};
+
+/**
+ * Create a Rule from a JSON object.
+ *
+ * @param {!Object} obj The object to take settings from.
+ * @return {!fiveui.Rule} A populated Rule object.
+ */
+fiveui.Rule.fromJSON = function(obj) {
+ return new fiveui.Rule(obj.module);
+};
+
+/**
+ * @constructor
+ * @param {!number} id The unique RuleSet identifier.
+ * @param {!string} name A human-readable name for this RuleSet.
+ * @param {!string} desc A human-readable description of the Rule Set.
+ * @param {!string} source The url where the manifest can be retrieved
+ * @param {!Array.<fiveui.Rule>} rules An Array of Rules.
+ * @param {?Array.<string>} deps Dependencies that this RuleSet requires.
+ */
+fiveui.RuleSet = function(id, name, desc, source, rules, deps) {
+ this.id = id;
+ this.name = name;
+ this.description = desc;
+ this.source = source;
+ this.rules = rules || [];
+ this.dependencies = deps || [];
+};
+
+/**
+ * Create a Rule Setfrom a JSON object.
+ *
+ * @param {!number} id A unique id for the rehydrated Rule.
+ * @param {!Object} obj The object to take settings from.
+ * @return {!fiveui.RuleSet} A populated RuleSet object.
+ */
+fiveui.RuleSet.fromJSON = function(id, obj) {
+ var rules = (/** @type {!Array.<!fiveui.Rule>} */
+ _.map(obj.rules, fiveui.Rule.fromJSON));
+
+ return new fiveui.RuleSet(id, obj.name, obj.description, obj.source,
+ rules, obj.dependencies);
+};
+
+
+fiveui.RuleSet.defaults = function(obj) {
+ return _.defaults(obj, {
+ name: '',
+ description: '',
+ rules: [],
+ dependencies: []
+ });
+};
+
+
+/**
+ * Options is an object that can contain a success and error continuation.
+ */
+fiveui.RuleSet.load = function(manifest_url, options) {
+
+ _.defaults(options, {
+ success: function() {},
+ error: function() { throw "failed when loading url"; }
+ });
+
+ var match = manifest_url.match(/\/[^\/]*$/);
+
+ if(match) {
+ var base_url = manifest_url.substring(0,match.index);
+
+ // iterate over rules, retrieving the
+ var loadRules = function(manifest, rules) {
+
+ if(rules.length == 0) {
+ options.success(manifest);
+ } else {
+
+ // XXX there's likely problems here, how should we make sure that the
+ // url is what we expect?
+ var rule_file = fiveui.Rule.defaults(rules.pop());
+ var rule_url = base_url + '/' + rule_file;
+
+ fiveui.ajax.get(rule_url, {
+
+ success: function(text) {
+ manifest.rules.push(new fiveui.Rule(text));
+ loadRules(manifest, rules);
+ },
+
+ error: options.error
+ });
+
+ }
+ };
+
+ // fetch the manifest, and load its rules
+ fiveui.ajax.get(manifest_url, {
+
+ success: function(text) {
+ try {
+ var sanitized = fiveui.utils.filterJSON(text,'json');
+ var manifest = JSON.parse(sanitized);
+
+ } catch(e) {
+ // XXX incoming error continuation is empty
+ // (and we may have syntax error details in e)
+ options.error('failed to parse manifest');
+ return;
+ }
+
+ fiveui.RuleSet.defaults(manifest);
+
+ var rules = manifest.rules;
+ manifest.rules = [];
+ loadRules(manifest, rules);
+ },
+
+ error: function() {
+ options.error('failed to retrieve manifest');
+ },
+ });
+
+
+ } else {
+ options.error("unable to parse manifest url");
+ }
+
+};
+
+
+/*******************************************************************************
+ * Models for RuleSet
+ ******************************************************************************/
+
+/**
+ * The model for an single set of rules.
+ */
+fiveui.RuleSetModel = Backbone.Model.extend({
+
+ defaults: {
+ id: null,
+ name: '',
+ description: '',
+ source: '',
+ rules: [],
+ dependencies: [],
+ },
+
+ sync: function(method, model, options) {
+
+ _.defaults(options, {
+ success:function() {},
+ error: function() {}
+ });
+
+ var msg = this.url;
+ var id = model.get('id');
+ var source = model.get('source');
+
+ switch(method) {
+
+ case 'update':
+ case 'create':
+ var rsMethod = method == 'update' ? 'updateRuleSet' : 'addRuleSet';
+
+ msg.send('loadRuleSet', source, function(obj) {
+ if(!obj.error) {
+ obj.id = id;
+ obj.source = source;
+
+ msg.send(rsMethod, obj, options.success);
+ } else {
+ options.error(obj.error);
+ }
+ });
+ break;
+
+ case 'delete':
+ msg.send('remRuleSet', id, function(obj) {
+ if(obj.removed) {
+ options.success();
+ } else {
+ options.error();
+ }
+ });
+ break;
+
+ case 'read':
+ msg.send('getRuleSet', id, function(rs) {
+ model.set({
+ title: rs.name,
+ descr: rs.description,
+ source: rs.source,
+ });
+ });
+ break;
+
+ default:
+ break;
+ }
+ }
+
+}, {
+
+ /**
+ * Generate a RuleSetModel from a RuleSet
+ */
+ fromRuleSet: function(ruleSet,msg) {
+ return new fiveui.RuleSetModel({
+ id: ruleSet.id,
+ name: ruleSet.name,
+ description: ruleSet.description,
+ rules: ruleSet.rules,
+ dependencies:ruleSet.dependencies,
+ source: ruleSet.source,
+ }, { url : msg });
+ },
+
+});
+
+
+/**
+ * The model for a collection of rule sets
+ */
+fiveui.RuleSets = Backbone.Collection.extend({
+
+ model: fiveui.RuleSetModel,
+
+ sync: function(method, collection, options) {
+ _.defaults(options, {
+ success:function() {},
+ error:function() {}
+ });
+
+ var self = this;
+ var msg = this.url;
+
+ switch(method) {
+
+ case 'read':
+ msg.send('getRuleSets', null, function(ruleSets) {
+ options.success(_.map(ruleSets, function(rs) {
+ return fiveui.RuleSetModel.fromRuleSet(rs, msg);
+ }));
+ });
+ break;
+
+ }
+ }
+
+});
+
+
+})();
diff --git a/src/js/fiveui/js/set.js b/src/js/fiveui/js/set.js
new file mode 100644
index 0000000..7015a3e
--- /dev/null
+++ b/src/js/fiveui/js/set.js
@@ -0,0 +1,58 @@
+
+
+(function() {
+
+Set = function() {
+ this.elems = {};
+};
+
+_.extend(Set.prototype, {
+
+ add: function(obj) {
+ var hash = this._getHash(obj);
+ if(this.elems[hash] == undefined) {
+ this.elems[hash] = obj;
+ }
+ },
+
+ remove: function(obj) {
+ var hash = this._getHash(obj);
+ if(this.elems[hash]) {
+ delete this.elems[hash];
+ }
+ },
+
+ member: function(obj) {
+ var hash = this._getHash(obj);
+ return !!this.elems[hash];
+ },
+
+ contains: function(obj) {
+ return this.member(obj);
+ },
+
+ size: function () {
+ return _.size(this.elems);
+ },
+
+ isEmpty: function() {
+ return this.size() == 0;
+ },
+
+ each: function(k, cxt) {
+ _.each(this.elems, k, cxt);
+ },
+
+ _getHash: function(obj) {
+ var str = obj.toString();
+
+ // the same hash function that java uses for String.hashCode
+ return _.reduce(str, function(hash, c) {
+ hash = ((hash << 5) - hash) + c.charCodeAt();
+ return hash & hash;
+ }, 0);
+ }
+
+});
+
+})();
diff --git a/src/js/fiveui/js/settings.js b/src/js/fiveui/js/settings.js
new file mode 100644
index 0000000..a17a9e0
--- /dev/null
+++ b/src/js/fiveui/js/settings.js
@@ -0,0 +1,389 @@
+/*
+ * Module : settings.js
+ * Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer :
+ * Stability : Provisional
+ * Portability: Portable
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var fiveui = fiveui || {};
+
+(function() {
+
+/**
+ * Create a new instance of the Settings object
+ *
+ * @constructor
+ * @param {!Storage} store The Storage instance to use.
+ */
+fiveui.Settings = function(store) {
+ this.store = store;
+};
+
+_.extend(fiveui.Settings.prototype, {
+
+ /**
+ * Retrieve an object associated with the key.
+ *
+ * @param {!string} key The key to look up.
+ * @return {?Object} The returned JavaScript object.
+ */
+ get: function(key) {
+ var value = this.store.getItem(key);
+ if (value == null) {
+ return null;
+ } else {
+ return JSON.parse(value);
+ }
+ },
+
+ /**
+ * Store an object with the given key.
+ *
+ * @param {string} key the key.
+ * @param {Object} value the value.
+ * @return {void}
+ */
+ set: function(key, value) {
+ this.store.setItem(key, JSON.stringify(value));
+ },
+
+ /**
+ * @param {string} key The local storage entry to remove.
+ * @return {void}
+ */
+ remove: function(key) {
+ this.store.removeItem(key);
+ },
+
+ /**
+ * Set the display default.
+ *
+ * @param {!boolean} def Whether or not to display the FiveUI Window
+ * when problems are reported.
+ */
+ setDisplayDefault: function(def) {
+ this.set('displayDefault', def);
+ },
+
+ /**
+ * Get the display default.
+ *
+ * @return {!boolean} true to show the window as soon as problems are
+ * reported, false otherwise.
+ */
+ getDisplayDefault: function() {
+ var def = this.get('displayDefault');
+
+ // double negation to normalize funny things like null
+ return !!def;
+ },
+
+ /**
+ * Add a mapping from url patterns to RuleSets (via rule set ids)
+ *
+ * @param {!string} url_pat A regular expression (actually, a glob) to match URLs against.
+ * @param {!number} rule_id The id of the RuleSet to use with any matching URL.
+ * @return {!number} The id of the new url pattern.
+ */
+ addUrl: function(url_pat, rule_id) {
+ var pats = this.getUrls();
+
+ var new_id = fiveui.utils.getNewId(pats);
+
+ this.updateUrl(new_id, url_pat, rule_id);
+
+ // add it to the patterns list
+ pats.push(new_id);
+ this.set('urls', pats);
+
+ return new_id;
+ },
+
+ updateUrl: function(id, url_pat, rule_id) {
+ this.set('urls.' + id, new fiveui.UrlPat(id, url_pat, rule_id));
+ return id;
+ },
+
+ /**
+ * Retrieve the list of url patterns.
+ *
+ * @return {Array.<number>} An ordered list of the currently active
+ * url patterns.
+ */
+ getUrls: function() {
+ return (/** @type {Array.<number>} */ this.get('urls')) || [];
+ },
+
+ /**
+ * Retrieve a specific url pattern.
+ *
+ * @param {!number} url_id The id of the url pattern to retrieve.
+ * @return {?fiveui.UrlPat} The matching UrlPat or null,
+ * if no pattern exists for url_id.
+ */
+ getUrlPat: function(url_id) {
+ return /** @type {?fiveui.UrlPat} */ this.getById(url_id, 'urls', fiveui.UrlPat.fromJSON);
+ },
+
+ /**
+ * Remove a UrlPat from the persistent storage.
+ *
+ * @param {!number} pat_id The id of the UrlPat to remove.
+ * @return {void}
+ */
+ remUrlPat: function(pat_id) {
+ this.remById(pat_id, 'urls');
+ },
+
+ /**
+ * @param {!string} url A url to compare against the list of ordered
+ * url patterns in local storage.
+ * @return {?fiveui.UrlPat} The matching pattern, or null, if no
+ * mapping was found.
+ */
+ checkUrl: function(url) {
+ var pats = this.getUrls();
+
+ // check for a possible match
+ for (var i = 0; i < pats.length; ++i) {
+ var pat = this.getUrlPat(pats[i]);
+ if (pat.match(url)) {
+ return pat;
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Retrieve the list of rule sets.
+ *
+ * @return {!Array.<number>} An ordered list of the configured rule sets.
+ */
+ getRuleSets: function() {
+ return (/** @type {!Array.<number>} */ this.get('ruleSet')) || [];
+ },
+
+
+ /**
+ * @param {!Object} ruleSet The new rule set, as an anonymous JSON object.
+ * @return {!fiveui.RuleSet} The new RuleSet object.
+ */
+ addRuleSet: function(ruleSet) {
+ var ids = this.getRuleSets();
+ var id = fiveui.utils.getNewId(ids);
+
+ var newRS = this.updateRuleSet(id, ruleSet);
+ ids.push(id);
+ this.set('ruleSet', ids);
+
+ return newRS;
+ },
+
+ /**
+ * Change a rule set without generating a new id.
+ *
+ * @param {!number} ruleSetId The id of the ruleset that is being modified.
+ * @param {!Object} ruleSet The rule set, as an anonymous JSON object.
+ * @return {!fiveui.RuleSet} The new RuleSet object.
+ */
+ updateRuleSet: function(ruleSetId, ruleSet) {
+ var newRS = fiveui.RuleSet.fromJSON(ruleSetId, ruleSet);
+ this.set('ruleSet.'+ruleSetId, newRS);
+ return newRS;
+ },
+
+ /**
+ * @param {!number} id The id of the RuleSet to retrieve.
+ * @return {?fiveui.RuleSet} The RuleSet, or null, if no RuleSet was found.
+ */
+ getRuleSet: function(id) {
+ return /** @type {?fiveui.RuleSet} */ this.getById(id, 'ruleSet', fiveui.RuleSet.fromJSON);
+ },
+
+ /**
+ * @param {!number} id The id of the rule set to remove.
+ * @return {!Array.<fiveui.UrlPat>} null if the remove succeeded, otherwise,
+ * returns the list of UrlPats that use this
+ * rule set, if any.
+ */
+ remRuleSet: function(id) {
+ var matches = _.map(this.getRuleSetUrlPats(id), function(id) {
+ return this.getUrlPat(id);
+ }, this);
+
+ if (0 == matches.length) {
+ this.remById(id, 'ruleSet');
+ }
+
+ return matches;
+ },
+
+ /**
+ * @param {!number} ruleSetId The rule set to retrieve url patterns for.
+ * @return {Array.<number>} Url pattern ids associated with this rule set.
+ */
+ getRuleSetUrlPats: function(ruleSetId) {
+ var urls = this.getUrls();
+ var patIds = [];
+
+ _.each(urls, function(patId) {
+ var pat = this.getUrlPat(patId);
+ if(pat.rule_id == ruleSetId) {
+ patIds.push(patId);
+ }
+ }, this);
+
+ return patIds;
+ },
+
+ /**
+ * @param {!number} id The nuber of the element to retrieve.
+ * @param {!string} listName The name of the portion of the localstorage hierarchy to search for id.
+ * @param {!function(number, !Object): *} fromJSON A deserialization function.
+ *
+ * @return {*} Either null, or the result of fromJSON.
+ */
+ getById: function(id, listName, fromJSON) {
+ var obj = this.get(listName + '.' + id);
+ if (!obj) {
+ return null;
+ }
+
+ return fromJSON(id, obj);
+ },
+
+ /**
+ * @param {!number} id The nuber of the element to retrieve.
+ * @param {!string} listName The name of the portion of the
+ * localstorage hierarchy to search for id.
+ */
+ remById: function(id, listName) {
+ // remove it from the list of ids:
+ var ids = this.get(listName) || [];
+
+ for (var i = 0; i < ids.length; ++i) {
+ if (ids[i] == id) {
+ ids.splice(i, 1);
+ this.set(listName, ids);
+ this.remove(listName + '.' + id);
+ break;
+ }
+ }
+ }
+});
+
+
+/**
+ * @param {!fiveui.Chan} chan
+ * @param {!fiveui.Settings} settings
+ * @return {void}
+ */
+fiveui.Settings.manager = function(chan, settings) {
+
+ var msg = new fiveui.Messenger(chan);
+
+ msg.register('addRuleSet', function(ruleSet,respond){
+ var newRS = settings.addRuleSet(ruleSet);
+ respond(newRS);
+ });
+
+ msg.register('updateRuleSet', function(updatedRS,respond){
+ var newRS = settings.updateRuleSet(updatedRS.id, updatedRS);
+ respond(newRS);
+ });
+
+ msg.register('remRuleSet', function(ruleSetId, respond) {
+ var pats = settings.remRuleSet(ruleSetId);
+ respond({
+ id: ruleSetId,
+ pats: pats,
+ removed: pats.length == 0
+ });
+ });
+
+ msg.register('getRuleSetUrlPats', function(ruleSetId, respond) {
+ var pats = settings.getUrls();
+ var patIds = [];
+
+ _.each(pats, function(patId) {
+ var pat = settings.getUrlPat(patId);
+ if(pat.rule_id == ruleSetId) {
+ patIds.push(patId);
+ }
+ });
+
+ respond(patIds);
+ });
+
+ // Retrieve the manifest, and return the object to the caller. Invokes the
+ // caller with `null` when the manifest fails to load.
+ msg.register('loadRuleSet', function(url, respond) {
+ fiveui.RuleSet.load(url, {
+ success:respond,
+
+ error:function(msg) {
+ respond({ error : msg });
+ },
+ });
+ });
+
+ msg.register('getRuleSet', function(ruleSetId, respond){
+ respond(settings.getRuleSet(ruleSetId));
+ });
+
+ msg.register('getRuleSets', function(unused, respond) {
+ var ruleSets = _.map(settings.getRuleSets(),
+ _.bind(settings.getRuleSet, settings));
+ respond(ruleSets);
+ });
+
+ msg.register('getUrlPats', function(unused, respond){
+ respond(_.map(settings.getUrls(), _.bind(settings.getUrlPat, settings)));
+ });
+
+ msg.register('addUrlPat', function(url, respond){
+ var urlId = settings.addUrl(url.regex, url.rule_id);
+ respond(settings.getUrlPat(urlId));
+ });
+
+ msg.register('updateUrlPat', function(pat, respond) {
+ var obj = settings.getUrlPat(pat.id);
+ settings.updateUrl(pat.id, pat.regex, pat.rule_id);
+ respond(pat);
+ });
+
+ msg.register('getUrlPat', function(urlPatId, respond){
+ respond(settings.getUrlPat(urlPatId));
+ });
+
+ msg.register('remUrlPat', function(urlPatId, respond){
+ settings.remUrlPat(urlPatId);
+ respond(true);
+ });
+
+ msg.register('setDisplayDefault', function(def) {
+ settings.setDisplayDefault(def);
+ });
+
+ msg.register('getDisplayDefault', function(ignored, respond) {
+ respond(settings.getDisplayDefault());
+ });
+
+};
+
+})();
diff --git a/src/js/fiveui/js/state.js b/src/js/fiveui/js/state.js
new file mode 100644
index 0000000..226a439
--- /dev/null
+++ b/src/js/fiveui/js/state.js
@@ -0,0 +1,202 @@
+/*
+ * Module : state.js
+ * Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer :
+ * Stability : Provisional
+ * Portability: Portable
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var fiveui = fiveui || {};
+
+(function() {
+
+/**
+ * @constructor
+ * @param {!number} x The x-offset of the embedded dialog.
+ * @param {!number} y The y-offset the dialog.
+ * @param {!number} width The width of the dalog.
+ * @param {!number} height The width of the dalog.
+ * @param {!boolean} closed True if the window is closed, false if it is open.
+ */
+fiveui.WinState = function(x, y, width, height, closed) {
+ this.x = x;
+ this.y = y;
+ this.width = width;
+ this.height = height;
+ this.closed = closed;
+};
+
+/**
+ * @constructor
+ * @param {string} name The name of the rule that this problem represents.
+ * @param {string} descr Short description of the problem.
+ * @param {string} url The url that the problem occurred at.
+ * @param {number} severity The severity of the problem
+ */
+fiveui.Problem = function(name, descr, url, severity, hash) {
+ this.name = name || '';
+ this.descr = descr || '';
+ this.url = url || '';
+ this.severity = severity || 0;
+ this.hash = hash;
+};
+
+/**
+ * @param {!Object} obj The JSON object to use as a template for a problem.
+ * @return {!fiveui.Problem} The problem that the object represents.
+ */
+fiveui.Problem.fromJSON = function(obj) {
+ return new fiveui.Problem(obj.name, obj.descr, obj.url, obj.severity, obj.hash);
+};
+
+/**
+ * @constructor
+ * @param {fiveui.WinState} winState The location of the FiveUI windown in
+ * the injected page. Null if the window
+ * is hidden.
+ * @param {fiveui.ChromePort} uiPort The port used to communicate with
+ * the corresponding tab.
+ */
+fiveui.TabState = function(tabId, winState, uiPort) {
+ this.tabId = tabId;
+ this.winState = winState;
+ this.uiPort = uiPort;
+ this.computePorts = [];
+ this.problems = [];
+ this.seenProblems = new Set();
+ this.stats = {};
+};
+
+_.extend(fiveui.TabState.prototype, {
+
+ addProblem: function(prob) {
+ if(!this.seenProblems.contains(prob.hash)) {
+ this.problems.push(prob);
+ this.seenProblems.add(prob.hash);
+ return true;
+ }
+ else {
+ return false;
+ }
+ },
+
+ addStats: function (stats) {
+ this.stats = stats;
+ return true;
+ },
+
+ clearProblems: function() {
+ this.problems = [];
+ this.seenProblems = new Set();
+ },
+
+ clearStats: function() {
+ for (var p in fiveui.stats.zero) { this.stats[p] = fiveui.stats.zero[p]; }
+ },
+
+ /*
+ * Returns a copy of only the attributes in a TabState that are needed for
+ * interpage communication.
+ */
+ toEmit: function() {
+ return { winState: this.winState, problems: this.problems, stats: this.stats };
+ }
+
+});
+
+/**
+ * @constructor
+ * @param {!fiveui.Settings} settings The settings object to obtain
+ * defaults from.
+ */
+fiveui.State = function(settings) {
+ this.tabs = {};
+ this.settings = settings;
+};
+
+_.extend(fiveui.State.prototype, {
+
+ /**
+ * @param {!number} tabId The id of the tab to retrieve state for.
+ *
+ * @return {?fiveui.TabState} The stored state of the tab, or null, if
+ * no state exists for the requested tab.
+ */
+ getTabState: function(tabId) {
+ return this.tabs[tabId] || null;
+ },
+
+ /**
+ * Like getTabState, but creates an initial tab state if none exists.
+ *
+ * @param {!number} tabId The id of the tab to retrieve state for.
+ * @param {!fiveui.ChromePort} port The port to use for communication
+ * with the corresponding tab.
+ * @return {!fiveui.TabState} Either an initial state if none existed, or the
+ * state that exists already.
+ */
+ acquireTabState: function(tabId, port) {
+ var ts = this.getTabState(tabId);
+
+ if(null == ts) {
+ var closed = ! this.settings.getDisplayDefault();
+
+ // in the future, get these defaults from the settings instance.
+ var ws = new fiveui.WinState(10, 10, 300, 300, closed);
+ ts = new fiveui.TabState(tabId, ws, port);
+ this.setTabState(ts);
+ }
+
+ return ts;
+ },
+
+ /**
+ * @param {!fiveui.TabState} ts The state to store.
+ * @return {void}
+ */
+ setTabState: function(ts) {
+ this.tabs[ts.tabId] = ts;
+ },
+
+ /**
+ * Update the state of a tab, if and only if the tabId exists in the state.
+ *
+ * @param {!number} tabId The id of the tab to store state for.
+ * @param {function(fiveui.TabState): fiveui.TabState} fn A function
+ * that modifies the tab state.
+ *
+ * @return {void}
+ */
+ adjust: function(tabId, fn) {
+ var tState = this.getTabState(tabId);
+ if (tState) {
+ this.tabs[tabId] = fn(tState);
+ }
+ },
+
+ /**
+ * Remove the state of a tab.
+ *
+ * @param {!number} tabId The id of the tab to remove the state of.
+ * @return {void}
+ */
+ removeTabState: function(tabId) {
+ delete this.tabs[tabId];
+ }
+
+});
+
+})();
diff --git a/src/js/fiveui/js/update-manager.js b/src/js/fiveui/js/update-manager.js
new file mode 100644
index 0000000..8013d2f
--- /dev/null
+++ b/src/js/fiveui/js/update-manager.js
@@ -0,0 +1,51 @@
+/*
+ * Module : update-manager.js
+ * Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer :
+ * Stability : Provisional
+ * Portability: Portable
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function() {
+
+
+fiveui.UpdateManager = function(msg) {
+ var manager = this;
+
+ // fired when the rule set gets updated
+ msg.register('updateRuleSet', function(newRuleSet) {
+ manager.trigger('updateRuleSet.' + newRuleSet.id, false, newRuleSet);
+
+ // update the associated url patterns
+ msg.send('getRuleSetPatIds', null, function(patIds) {
+ _.each(patIds, function(patId) {
+ manager.trigger('updateUrlPat.' + patId);
+ });
+ });
+ });
+
+ // fired when the url pat gets removed
+ msg.register('remUrlPat', function(id) {
+ var evt = 'remUrlPat.' + id;
+ manager.trigger(evt);
+ manager.off(evt);
+ });
+
+};
+
+_.extend(fiveui.UpdateManager.prototype, Backbone.Events);
+
+})();
diff --git a/src/js/fiveui/js/url-pat.js b/src/js/fiveui/js/url-pat.js
new file mode 100644
index 0000000..d723021
--- /dev/null
+++ b/src/js/fiveui/js/url-pat.js
@@ -0,0 +1,140 @@
+
+(function() {
+
+/**
+ * Create a new Url Pattern to map urls to Rule Sets.
+ *
+ * @constructor
+ * @param {!number} id New id for this UrlPat.
+ * @param {!string} regex The pattern that is used to match Urls.
+ * @param {!number} rule_id Unique id of the RuleSet to use for matching URLs.
+ */
+fiveui.UrlPat = function(id, regex, rule_id) {
+ this.id = id;
+ this.regex = regex;
+ this.rule_id = rule_id;
+};
+
+/**
+ * Create a Url Pattern from a JSON object.
+ *
+ * @param {!number} id The id to use for the restored object.
+ * @param {!Object} obj The object to take settings from.
+ * @return {!fiveui.UrlPat} A populated UrlPat object.
+ */
+fiveui.UrlPat.fromJSON = function(id, obj) {
+ return new fiveui.UrlPat(id, obj.regex, obj.rule_id);
+};
+
+/**
+ * Create a regular expression from a globbed pattern.
+ *
+ * @param {!string} str The globbed url.
+ * @return {!RegExp} A compiled regular expression.
+ */
+fiveui.UrlPat.compile = function(str) {
+ var regex = str.replace(/\./g, '\.')
+ .replace(/\*/g, '.*');
+ return new RegExp(regex);
+};
+
+/**
+ * Test a string Url against the regular expression held in a Url Pattern.
+ *
+ * @param {!string} url The Url the string to test.
+ * @return {!boolean} If the Url matched the regular expression.
+ */
+fiveui.UrlPat.prototype.match = function(url) {
+ var pat = fiveui.UrlPat.compile(this.regex);
+ return pat.test(url);
+};
+
+
+
+fiveui.UrlPatModel = Backbone.Model.extend({
+
+ defaults: {
+ id: null,
+ regex: '',
+ rule_id: null,
+ },
+
+ sync:function(method, model, options) {
+ _.defaults(options, {
+ success:function() {},
+ error:function() {}
+ });
+
+ var msg = model.url;
+ var id = model.get('id');
+
+ switch(method) {
+ case 'read':
+ msg.send('getUrlPat', id, function(pat) {
+ model.set(pat);
+ options.success();
+ });
+ break;
+
+ case 'update':
+ msg.send('updateUrlPat', _.clone(model.attributes), options.success);
+ break;
+
+ case 'create':
+ msg.send('addUrlPat', _.clone(model.attributes), options.success);
+ break;
+
+ case 'delete':
+ msg.send('remUrlPat', id, function(res) {
+ if(res) {
+ options.success({});
+ } else {
+ options.error({});
+ }
+ });
+ break;
+ }
+ }
+
+}, {
+
+ fromUrlPat: function(pat, msg) {
+ return new fiveui.UrlPatModel({
+ id: pat.id,
+ regex: pat.regex,
+ rule_id: pat.rule_id
+ }, { url : msg });
+ }
+
+});
+
+
+fiveui.UrlPats = Backbone.Collection.extend({
+
+ model: fiveui.UrlPatModel,
+
+ sync:function(method, collection, options) {
+
+ _.defaults(options, {
+ success:function() {},
+ error:function() {}
+ });
+
+ var msg = this.url;
+
+ switch(method) {
+
+ case 'read':
+ msg.send('getUrlPats', null, function(pats) {
+ options.success(_.map(pats, function(pat) {
+ return fiveui.UrlPatModel.fromUrlPat(pat, msg);
+ }));
+ });
+ break;
+ }
+
+ }
+
+});
+
+})();
diff --git a/src/js/fiveui/js/utils.js b/src/js/fiveui/js/utils.js
new file mode 100644
index 0000000..0ab3128
--- /dev/null
+++ b/src/js/fiveui/js/utils.js
@@ -0,0 +1,145 @@
+/*
+ * Module : utils.js
+ * Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer :
+ * Stability : Provisional
+ * Portability: Portable
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var fiveui = fiveui || {};
+
+(function() {
+
+fiveui.utils = fiveui.utils || {};
+
+/**
+ * Chooses a function based on the current browser.
+ */
+fiveui.utils.pick = function (mozFn, chrFn) {
+ if (typeof chrome != 'undefined' ) {
+ return chrFn;
+ } else {
+ return mozFn;
+ }
+};
+
+/**
+ * Create a browser-independent settings object. This uses different
+ * backing storage depending on the browser:
+ *
+ * - Firefox: simple-storage
+ * - Chrome: localStorage
+ *
+ * @return {!fiveui.Settings} A settings object that will persist data
+ * between browser invocations.
+ */
+// fiveui.utils.getSettings = fiveui.utils.pick(
+// function() {
+// // var ss = require('simple-storage');
+// var ss = {storage: {}}; // XXX hack, obviously.
+
+// var storageWrapper = new fiveui.utils.StorageWrapper(ss.storage);
+// return new fiveui.Settings(storageWrapper);
+// },
+// function() {
+// return new fiveui.Settings(localStorage);
+// });
+
+/**
+ * Get an ID by adding one to the max id in current use.
+ *
+ * @param {Array.<number>} list the list of ids that the new id must
+ * not conflict with.
+ * @return {!number} A unique id for UrlPats.
+ */
+fiveui.utils.getNewId = function(list) {
+ // make sure we have a non-null, non-empty list:
+ if (list === null || list.length == 0) {
+ return 0;
+ } else {
+ return 1 + Math.max.apply(Math, list);
+ }
+};
+
+
+/**
+ * Remove c-style comments
+ *
+ * There's probably a faster way to do this.
+ */
+var removeComments = function(data) {
+
+ var state = 0;
+ var toEOL = 1;
+ var toEOC = 2;
+
+ var sanitized = '';
+ var len = data.length;
+ var s = 0, e = 0;
+
+ for(; e < len; ++e) {
+ switch(state) {
+ case toEOL:
+ if(data[e] == '\n') {
+ state = 0;
+ s = e + 1;
+ }
+ break;
+
+ case toEOC:
+ if(data[e] == '*' && data[e+1] == '/') {
+ state = 0;
+ s = e + 2;
+ e = e + 1;
+ }
+ break;
+
+ default:
+ if(data[e] == '/') {
+ if(data[e+1] == '/') {
+ sanitized = sanitized + data.substring(s,e);
+ state = toEOL;
+ e = e + 1;
+ } else if(data[e+1] == '*') {
+ sanitized = sanitized + data.substring(s,e);
+ state = toEOC;
+ e = e + 1;
+ }
+ }
+ break;
+ }
+ }
+
+ if(state == 0 && s < e) {
+ sanitized = sanitized + data.substring(s,e);
+ }
+
+ return sanitized;
+};
+
+
+/**
+ * Filter out comments, and other things that aren't appropriate in JSON.
+ */
+fiveui.utils.filterJSON = function(data, type) {
+ if(type == 'json') {
+ return removeComments(data);
+ } else {
+ return data;
+ }
+};
+
+})();