/** * @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, ">"); } 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 = ""; 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; });