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