infusion
Version:
Infusion is an application framework for developing flexible stuff with JavaScript
474 lines (425 loc) • 17 kB
JavaScript
/*
Copyright 2008-2010 University of Cambridge
Copyright 2008-2009 University of Toronto
Copyright 2010-2011 Lucendo Development Ltd.
Licensed under the Educational Community License (ECL), Version 2.0 or the New
BSD license. You may not use this file except in compliance with one these
Licenses.
You may obtain a copy of the ECL 2.0 License and BSD License at
https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
*/
fluid_2_0_0 = fluid_2_0_0 || {};
(function ($, fluid) {
"use strict";
// unsupported, non-API function
fluid.parseTemplate = function (template, baseURL, scanStart, cutpoints_in, opts) {
opts = opts || {};
if (!template) {
fluid.fail("empty template supplied to fluid.parseTemplate");
}
var t;
var parser;
var tagstack;
var lumpindex = 0;
var nestingdepth = 0;
var justended = false;
var defstart = -1;
var defend = -1;
var debugMode = false;
var cutpoints = []; // list of selector, tree, id
var simpleClassCutpoints = {};
var cutstatus = [];
var XMLLump = function (lumpindex, nestingdepth) {
return {
//rsfID: "",
//text: "",
//downmap: {},
//attributemap: {},
//finallump: {},
nestingdepth: nestingdepth,
lumpindex: lumpindex,
parent: t
};
};
function isSimpleClassCutpoint(tree) {
return tree.length === 1 && tree[0].predList.length === 1 && tree[0].predList[0].clazz;
}
function init(baseURLin, debugModeIn, cutpointsIn) {
t.rootlump = XMLLump(0, -1); // eslint-disable-line new-cap
tagstack = [t.rootlump];
lumpindex = 0;
nestingdepth = 0;
justended = false;
defstart = -1;
defend = -1;
baseURL = baseURLin;
debugMode = debugModeIn;
if (cutpointsIn) {
for (var i = 0; i < cutpointsIn.length; ++i) {
var tree = fluid.parseSelector(cutpointsIn[i].selector, fluid.simpleCSSMatcher);
var clazz = isSimpleClassCutpoint(tree);
if (clazz) {
simpleClassCutpoints[clazz] = cutpointsIn[i].id;
}
else {
cutstatus.push([]);
cutpoints.push($.extend({}, cutpointsIn[i], {tree: tree}));
}
}
}
}
function findTopContainer() {
for (var i = tagstack.length - 1; i >= 0; --i) {
var lump = tagstack[i];
if (lump.rsfID !== undefined) {
return lump;
}
}
return t.rootlump;
}
function newLump() {
var togo = XMLLump(lumpindex, nestingdepth); // eslint-disable-line new-cap
if (debugMode) {
togo.line = parser.getLineNumber();
togo.column = parser.getColumnNumber();
}
//togo.parent = t;
t.lumps[lumpindex] = togo;
++lumpindex;
return togo;
}
function addLump(mmap, ID, lump) {
var list = mmap[ID];
if (!list) {
list = [];
mmap[ID] = list;
}
list[list.length] = lump;
}
function checkContribute(ID, lump) {
if (ID.indexOf("scr=contribute-") !== -1) {
var scr = ID.substring("scr=contribute-".length);
addLump(t.collectmap, scr, lump);
}
}
function debugLump(lump) {
// TODO expand this to agree with the Firebug "self-selector" idiom
return "<" + lump.tagname + ">";
}
function hasCssClass(clazz, totest) {
if (!totest) {
return false;
}
// algorithm from jQuery
return (" " + totest + " ").indexOf(" " + clazz + " ") !== -1;
}
function matchNode(term, headlump, headclazz) {
if (term.predList) {
for (var i = 0; i < term.predList.length; ++i) {
var pred = term.predList[i];
if (pred.id && headlump.attributemap.id !== pred.id) {return false;}
if (pred.clazz && !hasCssClass(pred.clazz, headclazz)) {return false;}
if (pred.tag && headlump.tagname !== pred.tag) {return false;}
}
return true;
}
}
function tagStartCut(headlump) {
var togo;
var headclazz = headlump.attributemap["class"];
var i;
if (headclazz) {
var split = headclazz.split(" ");
for (i = 0; i < split.length; ++i) {
var simpleCut = simpleClassCutpoints[$.trim(split[i])];
if (simpleCut) {
return simpleCut;
}
}
}
for (i = 0; i < cutpoints.length; ++i) {
var cut = cutpoints[i];
var cutstat = cutstatus[i];
var nextterm = cutstat.length; // the next term for this node
if (nextterm < cut.tree.length) {
var term = cut.tree[nextterm];
if (nextterm > 0) {
if (cut.tree[nextterm - 1].child &&
cutstat[nextterm - 1] !== headlump.nestingdepth - 1) {
continue; // it is a failure to match if not at correct nesting depth
}
}
var isMatch = matchNode(term, headlump, headclazz);
if (isMatch) {
cutstat[cutstat.length] = headlump.nestingdepth;
if (cutstat.length === cut.tree.length) {
if (togo !== undefined) {
fluid.fail("Cutpoint specification error - node " +
debugLump(headlump) +
" has already matched with rsf:id of " + togo);
}
if (cut.id === undefined || cut.id === null) {
fluid.fail("Error in cutpoints list - entry at position " + i + " does not have an id set");
}
togo = cut.id;
}
}
}
}
return togo;
}
function tagEndCut() {
if (cutpoints) {
for (var i = 0; i < cutpoints.length; ++i) {
var cutstat = cutstatus[i];
if (cutstat.length > 0 && cutstat[cutstat.length - 1] === nestingdepth) {
cutstat.length--;
}
}
}
}
function processTagEnd() {
tagEndCut();
var endlump = newLump();
--nestingdepth;
endlump.text = "</" + parser.getName() + ">";
var oldtop = tagstack[tagstack.length - 1];
oldtop.close_tag = t.lumps[lumpindex - 1];
tagstack.length--;
justended = true;
}
function processTagStart(isempty) {
++nestingdepth;
if (justended) {
justended = false;
var backlump = newLump();
backlump.nestingdepth--;
}
if (t.firstdocumentindex === -1) {
t.firstdocumentindex = lumpindex;
}
var headlump = newLump();
var stacktop = tagstack[tagstack.length - 1];
headlump.uplump = stacktop;
var tagname = parser.getName();
headlump.tagname = tagname;
// NB - attribute names and values are now NOT DECODED!!
var attrs = headlump.attributemap = parser.m_attributes;
var ID = attrs[fluid.ID_ATTRIBUTE];
if (ID === undefined) {
ID = tagStartCut(headlump);
}
for (var attrname in attrs) {
if (ID === undefined) {
if (/href|src|codebase|action/.test(attrname)) {
ID = "scr=rewrite-url";
}
// port of TPI effect of IDRelationRewriter
else if (ID === undefined && /for|headers/.test(attrname)) {
ID = "scr=null";
}
}
}
if (ID) {
// TODO: ensure this logic is correct on RSF Server
if (ID.charCodeAt(0) === 126) { // "~"
ID = ID.substring(1);
headlump.elide = true;
}
checkContribute(ID, headlump);
headlump.rsfID = ID;
var downreg = findTopContainer();
if (!downreg.downmap) {
downreg.downmap = {};
}
while (downreg) { // TODO: unusual fix for locating branches in parent contexts (applies to repetitive leaves)
if (downreg.downmap) {
addLump(downreg.downmap, ID, headlump);
}
downreg = downreg.uplump;
}
addLump(t.globalmap, ID, headlump);
var colpos = ID.indexOf(":");
if (colpos !== -1) {
var prefix = ID.substring(0, colpos);
if (!stacktop.finallump) {
stacktop.finallump = {};
}
stacktop.finallump[prefix] = headlump;
}
}
// TODO: accelerate this by grabbing original template text (requires parser
// adjustment) as well as dealing with empty tags
headlump.text = "<" + tagname + fluid.dumpAttributes(attrs) + (isempty && !ID ? "/>" : ">");
tagstack[tagstack.length] = headlump;
if (isempty) {
if (ID) {
processTagEnd();
}
else {
--nestingdepth;
tagstack.length--;
}
}
}
function processDefaultTag() {
if (defstart !== -1) {
if (t.firstdocumentindex === -1) {
t.firstdocumentindex = lumpindex;
}
var text = parser.getContent().substr(defstart, defend - defstart);
justended = false;
var newlump = newLump();
newlump.text = text;
defstart = -1;
}
}
/** ACTUAL BODY of fluid.parseTemplate begins here **/
t = fluid.XMLViewTemplate();
init(baseURL, opts.debugMode, cutpoints_in);
var idpos = template.indexOf(fluid.ID_ATTRIBUTE);
if (scanStart) {
var brackpos = template.indexOf(">", idpos);
parser = fluid.XMLP(template.substring(brackpos + 1));
}
else {
parser = fluid.XMLP(template);
}
parseloop: // eslint-disable-line indent
while (true) {
var iEvent = parser.next();
switch (iEvent) {
case fluid.XMLP._ELM_B:
processDefaultTag();
//var text = parser.getContent().substr(parser.getContentBegin(), parser.getContentEnd() - parser.getContentBegin());
processTagStart(false, "");
break;
case fluid.XMLP._ELM_E:
processDefaultTag();
processTagEnd();
break;
case fluid.XMLP._ELM_EMP:
processDefaultTag();
//var text = parser.getContent().substr(parser.getContentBegin(), parser.getContentEnd() - parser.getContentBegin());
processTagStart(true, "");
break;
case fluid.XMLP._PI:
case fluid.XMLP._DTD:
defstart = -1;
continue; // not interested in reproducing these
case fluid.XMLP._TEXT:
case fluid.XMLP._ENTITY:
case fluid.XMLP._CDATA:
case fluid.XMLP._COMMENT:
if (defstart === -1) {
defstart = parser.m_cB;
}
defend = parser.m_cE;
break;
case fluid.XMLP._ERROR:
fluid.setLogging(true);
var message = "Error parsing template: " + parser.m_cAlt + " at line " + parser.getLineNumber();
fluid.log(message);
fluid.log("Just read: " + parser.m_xml.substring(parser.m_iP - 30, parser.m_iP));
fluid.log("Still to read: " + parser.m_xml.substring(parser.m_iP, parser.m_iP + 30));
fluid.fail(message);
break parseloop;
case fluid.XMLP._NONE:
break parseloop;
}
}
processDefaultTag();
var excess = tagstack.length - 1;
if (excess) {
fluid.fail("Error parsing template - unclosed tag(s) of depth " + (excess) +
": " + fluid.transform(tagstack.splice(1, excess), function (lump) {return debugLump(lump);}).join(", "));
}
return t;
};
// unsupported, non-API function
fluid.debugLump = function (lump) {
var togo = lump.text;
togo += " at ";
togo += "lump line " + lump.line + " column " + lump.column + " index " + lump.lumpindex;
togo += lump.parent.href === null ? "" : " in file " + lump.parent.href;
return togo;
};
// Public definitions begin here
fluid.ID_ATTRIBUTE = "rsf:id";
// unsupported, non-API function
fluid.getPrefix = function (id) {
var colpos = id.indexOf(":");
return colpos === -1 ? id : id.substring(0, colpos);
};
// unsupported, non-API function
fluid.SplitID = function (id) {
var that = {};
var colpos = id.indexOf(":");
if (colpos === -1) {
that.prefix = id;
}
else {
that.prefix = id.substring(0, colpos);
that.suffix = id.substring(colpos + 1);
}
return that;
};
// unsupported, non-API function
fluid.XMLViewTemplate = function () {
return {
globalmap: {},
collectmap: {},
lumps: [],
firstdocumentindex: -1
};
};
// TODO: find faster encoder
fluid.XMLEncode = function (text) {
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\"/g, """);
};
// unsupported, non-API function
fluid.dumpAttributes = function (attrcopy) {
var togo = "";
for (var attrname in attrcopy) {
var attrvalue = attrcopy[attrname];
if (attrvalue !== null && attrvalue !== undefined) {
togo += " " + attrname + "=\"" + attrvalue + "\"";
}
}
return togo;
};
// unsupported, non-API function
fluid.aggregateMMap = function (target, source) {
for (var key in source) {
var targhas = target[key];
if (!targhas) {
target[key] = [];
}
target[key] = target[key].concat(source[key]);
}
};
/** Returns a "template structure", with globalmap in the root, and a list
* of entries {href, template, cutpoints} for each parsed template.
*/
fluid.parseTemplates = function (resourceSpec, templateList, opts) {
var togo = [];
opts = opts || {};
togo.globalmap = {};
for (var i = 0; i < templateList.length; ++i) {
var resource = resourceSpec[templateList[i]];
var lastslash = resource.href.lastIndexOf("/");
var baseURL = lastslash === -1 ? "" : resource.href.substring(0, lastslash + 1);
var template = fluid.parseTemplate(resource.resourceText, baseURL,
opts.scanStart && i === 0, resource.cutpoints, opts);
if (i === 0) {
fluid.aggregateMMap(togo.globalmap, template.globalmap);
}
template.href = resource.href;
template.baseURL = baseURL;
template.resourceKey = resource.resourceKey;
togo[i] = template;
fluid.aggregateMMap(togo.globalmap, template.rootlump.downmap);
}
return togo;
};
})(jQuery, fluid_2_0_0);