ingenta-lens
Version:
A novel way of seeing content.
218 lines (182 loc) • 5.18 kB
JavaScript
var _ = require("underscore");
var util = require("../../substance/util");
// Creates an index for the document applying a given node filter function
// and grouping using a given key function
// --------
//
// - document: a document instance
// - filter: a function that takes a node and returns true if the node should be indexed
// - key: a function that provides a path for scoped indexing (default: returns empty path)
//
var Index = function(graph, options) {
options = options || {};
this.graph = graph;
this.nodes = {};
this.scopes = {};
if (options.filter) {
this.filter = options.filter;
} else if (options.types) {
this.filter = Index.typeFilter(graph.schema, options.types);
}
if (options.property) {
this.property = options.property;
}
this.createIndex();
};
Index.Prototype = function() {
// Resolves a sub-hierarchy of the index via a given path
// --------
//
var _resolve = function(path) {
var index = this;
if (path !== null) {
for (var i = 0; i < path.length; i++) {
var id = path[i];
index.scopes[id] = index.scopes[id] || { nodes: {}, scopes: {} };
index = index.scopes[id];
}
}
return index;
};
var _getKey = function(node) {
if (!this.property) return null;
var key = node[this.property] ? node[this.property] : null;
if (_.isString(key)) key = [key];
return key;
};
// Accumulates all indexed children of the given (sub-)index
var _collect = function(index) {
var result = _.extend({}, index.nodes);
_.each(index.scopes, function(child, name) {
if (name !== "nodes") {
_.extend(result, _collect(child));
}
});
return result;
};
// Keeps the index up-to-date when the graph changes.
// --------
//
this.onGraphChange = function(op) {
this.applyOp(op);
};
this._add = function(node) {
if (!this.filter || this.filter(node)) {
var key = _getKey.call(this, node);
var index = _resolve.call(this, key);
index.nodes[node.id] = node.id;
}
};
this._remove = function(node) {
if (!this.filter || this.filter(node)) {
var key = _getKey.call(this, node);
var index = _resolve.call(this, key);
delete index.nodes[node.id];
}
};
this._update = function(node, property, newValue, oldValue) {
if ((this.property === property) && (!this.filter || this.filter(node))) {
var key = oldValue;
if (_.isString(key)) key = [key];
var index = _resolve.call(this, key);
delete index.nodes[node.id];
key = newValue;
index.nodes[node.id] = node.id;
}
};
this.applyOp = function(op) {
if (op.type === "create") {
this._add(op.val);
}
else if (op.type === "delete") {
this._remove(op.val);
}
// type = 'update' or 'set'
else {
var prop = this.graph.resolve(this, op.path);
var value = prop.get();
var oldValue;
if (value === undefined) {
return;
}
if (op.type === "set") {
oldValue = op.original;
} else {
console.error("Operational updates are not supported in this implementation");
}
this._update(prop.node, prop.key, value, oldValue);
}
};
// Initializes the index
// --------
//
this.createIndex = function() {
this.reset();
var nodes = this.graph.nodes;
_.each(nodes, function(node) {
if (!this.filter || this.filter(node)) {
var key = _getKey.call(this, node);
var index = _resolve.call(this, key);
index.nodes[node.id] = node.id;
}
}, this);
};
// Collects all indexed nodes using a given path for scoping
// --------
//
this.get = function(path) {
if (arguments.length === 0) {
path = null;
} else if (_.isString(path)) {
path = [path];
}
var index = _resolve.call(this, path);
// EXPERIMENTAL: do we need the ability to retrieve indexed elements non-recursively
// for now...
// if so... we would need an paramater to prevent recursion
// E.g.:
// if (shallow) {
// result = index.nodes;
// }
var collected = _collect(index);
var result = new Index.Result();
_.each(collected, function(id) {
result[id] = this.graph.get(id);
}, this);
return result;
};
this.reset = function() {
this.nodes = {};
this.scopes = {};
};
this.dispose = function() {
this.stopListening();
};
this.rebuild = function() {
this.reset();
this.createIndex();
};
};
Index.prototype = _.extend(new Index.Prototype(), util.Events.Listener);
Index.typeFilter = function(schema, types) {
return function(node) {
var typeChain = schema.typeChain(node.type);
for (var i = 0; i < types.length; i++) {
if (typeChain.indexOf(types[i]) >= 0) {
return true;
}
}
return false;
};
};
Index.Result = function() {};
Index.Result.prototype.asList = function() {
var list = [];
for (var key in this) {
list.push(this[key]);
}
};
Index.Result.prototype.getLength = function() {
return Object.keys(this).length;
};
module.exports = Index;