mediawiki-extensions-Visual.../modules/rangy/rangy-serializer.js
2012-02-08 00:02:08 +00:00

301 lines
11 KiB
JavaScript

/**
* @license Serializer module for Rangy.
* Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a
* cookie or local storage and restore it on the user's next visit to the same page.
*
* Part of Rangy, a cross-browser JavaScript range and selection library
* http://code.google.com/p/rangy/
*
* Depends on Rangy core.
*
* Copyright 2011, Tim Down
* Licensed under the MIT license.
* Version: 1.2.2
* Build date: 13 November 2011
*/
rangy.createModule("Serializer", function(api, module) {
api.requireModules( ["WrappedSelection", "WrappedRange"] );
var UNDEF = "undefined";
// encodeURIComponent and decodeURIComponent are required for cookie handling
if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {
module.fail("Global object is missing encodeURIComponent and/or decodeURIComponent method");
}
// Checksum for checking whether range can be serialized
var crc32 = (function() {
function utf8encode(str) {
var utf8CharCodes = [];
for (var i = 0, len = str.length, c; i < len; ++i) {
c = str.charCodeAt(i);
if (c < 128) {
utf8CharCodes.push(c);
} else if (c < 2048) {
utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128);
} else {
utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128);
}
}
return utf8CharCodes;
}
var cachedCrcTable = null;
function buildCRCTable() {
var table = [];
for (var i = 0, j, crc; i < 256; ++i) {
crc = i;
j = 8;
while (j--) {
if ((crc & 1) == 1) {
crc = (crc >>> 1) ^ 0xEDB88320;
} else {
crc >>>= 1;
}
}
table[i] = crc >>> 0;
}
return table;
}
function getCrcTable() {
if (!cachedCrcTable) {
cachedCrcTable = buildCRCTable();
}
return cachedCrcTable;
}
return function(str) {
var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable();
for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) {
y = (crc ^ utf8CharCodes[i]) & 0xFF;
crc = (crc >>> 8) ^ crcTable[y];
}
return (crc ^ -1) >>> 0;
};
})();
var dom = api.dom;
function escapeTextForHtml(str) {
return str.replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
function nodeToInfoString(node, infoParts) {
infoParts = infoParts || [];
var nodeType = node.nodeType, children = node.childNodes, childCount = children.length;
var nodeInfo = [nodeType, node.nodeName, childCount].join(":");
var start = "", end = "";
switch (nodeType) {
case 3: // Text node
start = escapeTextForHtml(node.nodeValue);
break;
case 8: // Comment
start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->";
break;
default:
start = "<" + nodeInfo + ">";
end = "</>";
break;
}
if (start) {
infoParts.push(start);
}
for (var i = 0; i < childCount; ++i) {
nodeToInfoString(children[i], infoParts);
}
if (end) {
infoParts.push(end);
}
return infoParts;
}
// Creates a string representation of the specified element's contents that is similar to innerHTML but omits all
// attributes and comments and includes child node counts. This is done instead of using innerHTML to work around
// IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's
// innerHTML whenever the user changes an input within the element.
function getElementChecksum(el) {
var info = nodeToInfoString(el).join("");
return crc32(info).toString(16);
}
function serializePosition(node, offset, rootNode) {
var pathBits = [], n = node;
rootNode = rootNode || dom.getDocument(node).documentElement;
while (n && n != rootNode) {
pathBits.push(dom.getNodeIndex(n, true));
n = n.parentNode;
}
return pathBits.join("/") + ":" + offset;
}
function deserializePosition(serialized, rootNode, doc) {
if (rootNode) {
doc = doc || dom.getDocument(rootNode);
} else {
doc = doc || document;
rootNode = doc.documentElement;
}
var bits = serialized.split(":");
var node = rootNode;
var nodeIndices = bits[0] ? bits[0].split("/") : [], i = nodeIndices.length, nodeIndex;
while (i--) {
nodeIndex = parseInt(nodeIndices[i], 10);
if (nodeIndex < node.childNodes.length) {
node = node.childNodes[parseInt(nodeIndices[i], 10)];
} else {
throw module.createError("deserializePosition failed: node " + dom.inspectNode(node) +
" has no child with index " + nodeIndex + ", " + i);
}
}
return new dom.DomPosition(node, parseInt(bits[1], 10));
}
function serializeRange(range, omitChecksum, rootNode) {
rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
if (!dom.isAncestorOf(rootNode, range.commonAncestorContainer, true)) {
throw new Error("serializeRange: range is not wholly contained within specified root node");
}
var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
serializePosition(range.endContainer, range.endOffset, rootNode);
if (!omitChecksum) {
serialized += "{" + getElementChecksum(rootNode) + "}";
}
return serialized;
}
function deserializeRange(serialized, rootNode, doc) {
if (rootNode) {
doc = doc || dom.getDocument(rootNode);
} else {
doc = doc || document;
rootNode = doc.documentElement;
}
var result = /^([^,]+),([^,\{]+)({([^}]+)})?$/.exec(serialized);
var checksum = result[4], rootNodeChecksum = getElementChecksum(rootNode);
if (checksum && checksum !== getElementChecksum(rootNode)) {
throw new Error("deserializeRange: checksums of serialized range root node (" + checksum +
") and target root node (" + rootNodeChecksum + ") do not match");
}
var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc);
var range = api.createRange(doc);
range.setStart(start.node, start.offset);
range.setEnd(end.node, end.offset);
return range;
}
function canDeserializeRange(serialized, rootNode, doc) {
if (rootNode) {
doc = doc || dom.getDocument(rootNode);
} else {
doc = doc || document;
rootNode = doc.documentElement;
}
var result = /^([^,]+),([^,]+)({([^}]+)})?$/.exec(serialized);
var checksum = result[3];
return !checksum || checksum === getElementChecksum(rootNode);
}
function serializeSelection(selection, omitChecksum, rootNode) {
selection = selection || api.getSelection();
var ranges = selection.getAllRanges(), serializedRanges = [];
for (var i = 0, len = ranges.length; i < len; ++i) {
serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode);
}
return serializedRanges.join("|");
}
function deserializeSelection(serialized, rootNode, win) {
if (rootNode) {
win = win || dom.getWindow(rootNode);
} else {
win = win || window;
rootNode = win.document.documentElement;
}
var serializedRanges = serialized.split("|");
var sel = api.getSelection(win);
var ranges = [];
for (var i = 0, len = serializedRanges.length; i < len; ++i) {
ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document);
}
sel.setRanges(ranges);
return sel;
}
function canDeserializeSelection(serialized, rootNode, win) {
var doc;
if (rootNode) {
doc = win ? win.document : dom.getDocument(rootNode);
} else {
win = win || window;
rootNode = win.document.documentElement;
}
var serializedRanges = serialized.split("|");
for (var i = 0, len = serializedRanges.length; i < len; ++i) {
if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) {
return false;
}
}
return true;
}
var cookieName = "rangySerializedSelection";
function getSerializedSelectionFromCookie(cookie) {
var parts = cookie.split(/[;,]/);
for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) {
nameVal = parts[i].split("=");
if (nameVal[0].replace(/^\s+/, "") == cookieName) {
val = nameVal[1];
if (val) {
return decodeURIComponent(val.replace(/\s+$/, ""));
}
}
}
return null;
}
function restoreSelectionFromCookie(win) {
win = win || window;
var serialized = getSerializedSelectionFromCookie(win.document.cookie);
if (serialized) {
deserializeSelection(serialized, win.doc)
}
}
function saveSelectionCookie(win, props) {
win = win || window;
props = (typeof props == "object") ? props : {};
var expires = props.expires ? ";expires=" + props.expires.toUTCString() : "";
var path = props.path ? ";path=" + props.path : "";
var domain = props.domain ? ";domain=" + props.domain : "";
var secure = props.secure ? ";secure" : "";
var serialized = serializeSelection(api.getSelection(win));
win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure;
}
api.serializePosition = serializePosition;
api.deserializePosition = deserializePosition;
api.serializeRange = serializeRange;
api.deserializeRange = deserializeRange;
api.canDeserializeRange = canDeserializeRange;
api.serializeSelection = serializeSelection;
api.deserializeSelection = deserializeSelection;
api.canDeserializeSelection = canDeserializeSelection;
api.restoreSelectionFromCookie = restoreSelectionFromCookie;
api.saveSelectionCookie = saveSelectionCookie;
api.getElementChecksum = getElementChecksum;
});