--- /dev/null
+/**
+ * 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
+ * https://github.com/timdown/rangy
+ *
+ * Depends on Rangy core.
+ *
+ * Copyright 2015, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.3.0
+ * Build date: 10 May 2015
+ */
+(function(factory, root) {
+ if (typeof define == "function" && define.amd) {
+ // AMD. Register as an anonymous module with a dependency on Rangy.
+ define(["./rangy-core"], factory);
+ } else if (typeof module != "undefined" && typeof exports == "object") {
+ // Node/CommonJS style
+ module.exports = factory( require("rangy") );
+ } else {
+ // No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
+ factory(root.rangy);
+ }
+})(function(rangy) {
+ rangy.createModule("Serializer", ["WrappedSelection"], function(api, module) {
+ var UNDEF = "undefined";
+ var util = api.util;
+
+ // encodeURIComponent and decodeURIComponent are required for cookie handling
+ if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {
+ module.fail("encodeURIComponent and/or decodeURIComponent method is missing");
+ }
+
+ // 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, "<").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 = "<!--" + 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 pathParts = [], n = node;
+ rootNode = rootNode || dom.getDocument(node).documentElement;
+ while (n && n != rootNode) {
+ pathParts.push(dom.getNodeIndex(n, true));
+ n = n.parentNode;
+ }
+ return pathParts.join("/") + ":" + offset;
+ }
+
+ function deserializePosition(serialized, rootNode, doc) {
+ if (!rootNode) {
+ rootNode = (doc || document).documentElement;
+ }
+ var parts = serialized.split(":");
+ var node = rootNode;
+ var nodeIndices = parts[0] ? parts[0].split("/") : [], i = nodeIndices.length, nodeIndex;
+
+ while (i--) {
+ nodeIndex = parseInt(nodeIndices[i], 10);
+ if (nodeIndex < node.childNodes.length) {
+ node = node.childNodes[nodeIndex];
+ } else {
+ throw module.createError("deserializePosition() failed: node " + dom.inspectNode(node) +
+ " has no child with index " + nodeIndex + ", " + i);
+ }
+ }
+
+ return new dom.DomPosition(node, parseInt(parts[1], 10));
+ }
+
+ function serializeRange(range, omitChecksum, rootNode) {
+ rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
+ if (!dom.isOrIsAncestorOf(rootNode, range.commonAncestorContainer)) {
+ throw module.createError("serializeRange(): range " + range.inspect() +
+ " is not wholly contained within specified root node " + dom.inspectNode(rootNode));
+ }
+ var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
+ serializePosition(range.endContainer, range.endOffset, rootNode);
+ if (!omitChecksum) {
+ serialized += "{" + getElementChecksum(rootNode) + "}";
+ }
+ return serialized;
+ }
+
+ var deserializeRegex = /^([^,]+),([^,\{]+)(\{([^}]+)\})?$/;
+
+ function deserializeRange(serialized, rootNode, doc) {
+ if (rootNode) {
+ doc = doc || dom.getDocument(rootNode);
+ } else {
+ doc = doc || document;
+ rootNode = doc.documentElement;
+ }
+ var result = deserializeRegex.exec(serialized);
+ var checksum = result[4];
+ if (checksum) {
+ var rootNodeChecksum = getElementChecksum(rootNode);
+ if (checksum !== rootNodeChecksum) {
+ throw module.createError("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.setStartAndEnd(start.node, start.offset, end.node, end.offset);
+ return range;
+ }
+
+ function canDeserializeRange(serialized, rootNode, doc) {
+ if (!rootNode) {
+ rootNode = (doc || document).documentElement;
+ }
+ var result = deserializeRegex.exec(serialized);
+ var checksum = result[3];
+ return !checksum || checksum === getElementChecksum(rootNode);
+ }
+
+ function serializeSelection(selection, omitChecksum, rootNode) {
+ selection = api.getSelection(selection);
+ 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;
+ }
+
+ util.extend(api, {
+ serializePosition: serializePosition,
+ deserializePosition: deserializePosition,
+ serializeRange: serializeRange,
+ deserializeRange: deserializeRange,
+ canDeserializeRange: canDeserializeRange,
+ serializeSelection: serializeSelection,
+ deserializeSelection: deserializeSelection,
+ canDeserializeSelection: canDeserializeSelection,
+ restoreSelectionFromCookie: restoreSelectionFromCookie,
+ saveSelectionCookie: saveSelectionCookie,
+ getElementChecksum: getElementChecksum,
+ nodeToInfoString: nodeToInfoString
+ });
+
+ util.crc32 = crc32;
+ });
+
+ return rangy;
+}, this);
\ No newline at end of file