Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Checksum selection links #25

Merged
merged 1 commit into from Jul 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
112 changes: 75 additions & 37 deletions static/linkSelections.js
Expand Up @@ -3,9 +3,11 @@ var URL64Code =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
var selectionLink;
var article;
var currentRange;
var currentEncodedRange;

document.addEventListener("selectionchange", renderLinkedSelection);
window.addEventListener("resize", renderLinkedSelection);
document.addEventListener("selectionchange", handleSelectionChange);
window.addEventListener("resize", renderCurrentRange);
window.addEventListener("hashchange", scrollToWindowLocation);
window.addEventListener("load", scrollToWindowLocation);

Expand All @@ -23,45 +25,60 @@ function scrollToSelectionHash(url) {
if (!match) {
return;
}
var selection = document.getSelection();
var range = decodeRange(match[1]);
var rect = range.getBoundingClientRect();
currentEncodedRange = match[1];
currentRange = decodeRange(currentEncodedRange);
var rect = currentRange.getBoundingClientRect();
var topOffset = Math.max(
20,
Math.floor((window.innerHeight - rect.height) * 0.4)
);
window.scrollTo(0, window.scrollY + rect.y - topOffset);
var selection = document.getSelection();
selection.empty();
selection.addRange(range);
selection.addRange(currentRange);
renderCurrentRange();
}

// If there is currently a selection on the page, render a link encoding it.
function renderLinkedSelection() {
function handleSelectionChange(event) {
var selection = document.getSelection();
if (selection.isCollapsed) {
if (selectionLink) {
selectionLink.parentNode.removeChild(selectionLink);
selectionLink = null;
}
return;
} else {
var range = selection.getRangeAt(0);
if (
!currentRange ||
range.compareBoundaryPoints(Range.START_TO_START, currentRange) !== 0 ||
range.compareBoundaryPoints(Range.END_TO_END, currentRange) !== 0
) {
currentRange = range;
currentEncodedRange = encodeRange(currentRange);
renderCurrentRange();
}
}
var range = selection.getRangeAt(0);
var rect = range.getBoundingClientRect();
var encoded = encodeRange(range);
}

function renderCurrentRange() {
if (!article) {
article = document.getElementsByTagName("article")[0];
}
if (!selectionLink) {
selectionLink = document.createElement("a");
selectionLink.className = "selection-link";
selectionLink.innerText = "\u201F";
document.body.appendChild(selectionLink);
}
var left = article.getBoundingClientRect().x;
selectionLink.href = "#sel-" + encoded;
selectionLink.href = "#sel-" + currentEncodedRange;
selectionLink.onclick = onClickHash;
selectionLink.className = currentRange.isOutdated
? "outdated-selection-link"
: "selection-link";
selectionLink.innerText = currentRange.isOutdated ? "!" : "\u201F";
var left = article.getBoundingClientRect().x;
var top = currentRange.getBoundingClientRect().y;
selectionLink.style.left = Math.floor(left + window.scrollX - 37) + "px";
selectionLink.style.top = Math.floor(rect.y + window.scrollY - 3) + "px";
selectionLink.style.top = Math.floor(top + window.scrollY - 3) + "px";
}

// Encodes the range of a selection on the page as a string. The string is a
Expand All @@ -74,12 +91,13 @@ function renderLinkedSelection() {
// common node to the end container and the end text index.
function encodeRange(range) {
var encoded = "";
var startPath = encodeRangePoint(range.startContainer, range.startOffset);
var endPath = encodeRangePoint(range.endContainer, range.endOffset);
var startPath = encodeNodePath(range.startContainer);
var endPath = encodeNodePath(range.endContainer);
var commonPath = getCommonPath(startPath, endPath);
writeList(commonPath);
writeList(startPath.slice(commonPath.length));
writeList(endPath.slice(commonPath.length));
writeList(startPath.slice(commonPath.length).concat(range.startOffset));
writeList(endPath.slice(commonPath.length).concat(range.endOffset));
writeInt(getFNVChecksum(range.toString()));
return encoded;

// Unsigned ints are represented in a go-style varint encoding. Each hexad
Expand Down Expand Up @@ -109,17 +127,25 @@ function decodeRange(encoded) {
}
var offset = 0;
var commonPath = readList();
var startPoint = decodeRangePoint(commonPath.concat(readList()));
var endPoint = decodeRangePoint(commonPath.concat(readList()));
var startPath = readList();
var endPath = readList();
var expectedChecksum = readInt();
var startOffset = startPath.pop();
var startNode = decodeNodePath(commonPath.concat(startPath));
var endOffset = endPath.pop();
var endNode = decodeNodePath(commonPath.concat(endPath));
var range = document.createRange();
range.setStart(startPoint[0], startPoint[1]);
range.setEnd(endPoint[0], endPoint[1]);
range.setStart(startNode, startOffset);
range.setEnd(endNode, endOffset);
range.isOutdated =
expectedChecksum !== undefined &&
expectedChecksum !== getFNVChecksum(range.toString());
return range;

function readInt() {
var number = 0;
var sign = 0;
while (true) {
while (offset < encoded.length) {
var byte = URL64Decode[encoded.charCodeAt(offset++)];
number |= (byte & 0x1f) << sign;
sign += 5;
Expand All @@ -131,19 +157,20 @@ function decodeRange(encoded) {

function readList() {
var length = readInt();
var list = new Array(length);
for (var i = 0; i < length; i++) {
list[i] = readInt();
if (length != undefined) {
var list = new Array(length);
for (var i = 0; i < length; i++) {
list[i] = readInt();
}
return list;
}
return list;
}
}

// A range point is a tuple of a node, and offset into that node. We encode it
// as a list of integers representing the tree traversal path from the document
// body, followed by the text offset.
function encodeRangePoint(node, offset) {
var path = [offset];
// A node's identity is encoded as a list of integers representing the tree
// traversal path from the document body.
function encodeNodePath(node) {
var path = [];
while (node != document.body) {
var parentNode = node.parentNode;
path.push(Array.prototype.indexOf.call(parentNode.childNodes, node));
Expand All @@ -152,12 +179,12 @@ function encodeRangePoint(node, offset) {
return path.reverse();
}

function decodeRangePoint(path) {
function decodeNodePath(path) {
var node = document.body;
for (var i = 0; i < path.length - 1 && node; i++) {
for (var i = 0; i < path.length && node; i++) {
node = node.childNodes[path[i]];
}
return [node, path[path.length - 1]];
return node;
}

// Given two arrays of integers, returns the common prefix of the two.
Expand All @@ -168,3 +195,14 @@ function getCommonPath(p1, p2) {
}
return p1.slice(0, i);
}

// Given a string, produce a 15-bit unsigned int checksum.
// Later used to catch if a range may have changed since the link was created.
function getFNVChecksum(str) {
var sum = 0x811c9dc5;
for (var i = 0; i < str.length; ++i) {
sum ^= str.charCodeAt(i);
sum += (sum << 1) + (sum << 4) + (sum << 7) + (sum << 8) + (sum << 24);
}
return ((sum >> 15) ^ sum) & 0x7fff;
}
30 changes: 30 additions & 0 deletions static/spec.css
Expand Up @@ -7,6 +7,7 @@ body {

/* Selections */

.outdated-selection-link,
.selection-link {
position: absolute;
display: block;
Expand All @@ -25,6 +26,12 @@ body {
-ms-user-select: none;
}

.outdated-selection-link:hover,
.selection-link:hover {
text-decoration: none;
}

.outdated-selection-link:before,
.selection-link:before {
border: 5px solid transparent;
border-left-color: #cacee0;
Expand All @@ -47,6 +54,29 @@ body {
border-left-color: #3b5998;
}

.outdated-selection-link {
background: #f0babe;
font-size: 21px;
font-weight: 800;
line-height: 27px;
}

.outdated-selection-link:before {
border-left-color: #f0babe;
}

.outdated-selection-link:hover:after {
content: "This selection content has changed since this link was created.";
font: 9pt/11pt Cambria, "Palatino Linotype", Palatino, "Liberation Serif", serif;
position: absolute;
display: block;
white-space: nowrap;
padding: 2px 5px 1px;
top: -20px;
background: black;
color: white;
}

/* Links */

a {
Expand Down
32 changes: 31 additions & 1 deletion test/readme/output.html
Expand Up @@ -12,6 +12,7 @@

/* Selections */

.outdated-selection-link,
.selection-link {
position: absolute;
display: block;
Expand All @@ -30,6 +31,12 @@
-ms-user-select: none;
}

.outdated-selection-link:hover,
.selection-link:hover {
text-decoration: none;
}

.outdated-selection-link:before,
.selection-link:before {
border: 5px solid transparent;
border-left-color: #cacee0;
Expand All @@ -52,6 +59,29 @@
border-left-color: #3b5998;
}

.outdated-selection-link {
background: #f0babe;
font-size: 21px;
font-weight: 800;
line-height: 27px;
}

.outdated-selection-link:before {
border-left-color: #f0babe;
}

.outdated-selection-link:hover:after {
content: "This selection content has changed since this link was created.";
font: 9pt/11pt Cambria, "Palatino Linotype", Palatino, "Liberation Serif", serif;
position: absolute;
display: block;
white-space: nowrap;
padding: 2px 5px 1px;
top: -20px;
background: black;
color: white;
}

/* Links */

a {
Expand Down Expand Up @@ -836,7 +866,7 @@
}
</style>
<script>(function(){var e,t=document.getElementsByTagName("style")[0].sheet;function n(){e&&(t.deleteRule(e),e=void 0)}document.documentElement.addEventListener("mouseover",function(a){var u,d=a.target.attributes["data-name"];d&&(u=d.value,n(),e=t.insertRule('*[data-name="'+u+'"] { background: #FBF8D0; }',t.cssRules.length))}),document.documentElement.addEventListener("mouseout",n);})()</script>
<script>(function(){var e,n,t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";function o(e){a(new URL(e.target.href))}function r(){a(window.location)}function a(e){var n=e.hash.match(/^#sel-([A-Za-z0-9-_]+)$/);if(n){var o=document.getSelection(),r=function(e){for(var n=new Array(64),o=0;o<64;o++)n[t.charCodeAt(o)]=o;var r=0,a=f(),i=l(a.concat(f())),c=l(a.concat(f())),d=document.createRange();return d.setStart(i[0],i[1]),d.setEnd(c[0],c[1]),d;function s(){for(var t=0,o=0;;){var a=n[e.charCodeAt(r++)];if(t|=(31&a)<<o,o+=5,a<32)return t}}function f(){for(var e=s(),n=new Array(e),t=0;t<e;t++)n[t]=s();return n}}(n[1]),a=r.getBoundingClientRect(),i=Math.max(20,Math.floor(.4*(window.innerHeight-a.height)));window.scrollTo(0,window.scrollY+a.y-i),o.empty(),o.addRange(r)}}function i(){var r=document.getSelection();if(r.isCollapsed)e&&(e.parentNode.removeChild(e),e=null);else{var a=r.getRangeAt(0),i=a.getBoundingClientRect(),l=function(e){var n="",o=c(e.startContainer,e.startOffset),r=c(e.endContainer,e.endOffset),a=function(e,n){var t=0;for(;t<e.length&&t<n.length&&e[t]===n[t];)t++;return e.slice(0,t)}(o,r);return l(a),l(o.slice(a.length)),l(r.slice(a.length)),n;function i(e){do{n+=t[31&e|(e>31?32:0)],e>>=5}while(e>0)}function l(e){i(e.length);for(var n=0;n<e.length;n++)i(e[n])}}(a);n||(n=document.getElementsByTagName("article")[0]),e||((e=document.createElement("a")).className="selection-link",e.innerText="‟",document.body.appendChild(e));var d=n.getBoundingClientRect().x;e.href="#sel-"+l,e.onclick=o,e.style.left=Math.floor(d+window.scrollX-37)+"px",e.style.top=Math.floor(i.y+window.scrollY-3)+"px"}}function c(e,n){for(var t=[n];e!=document.body;){var o=e.parentNode;t.push(Array.prototype.indexOf.call(o.childNodes,e)),e=o}return t.reverse()}function l(e){for(var n=document.body,t=0;t<e.length-1&&n;t++)n=n.childNodes[e[t]];return[n,e[e.length-1]]}document.addEventListener("selectionchange",i),window.addEventListener("resize",i),window.addEventListener("hashchange",r),window.addEventListener("load",r);})()</script>
<script>(function(){var e,n,t,o,r="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";function a(e){c(new URL(e.target.href))}function i(){c(window.location)}function c(e){var n=e.hash.match(/^#sel-([A-Za-z0-9-_]+)$/);if(n){o=n[1];var a=(t=function(e){for(var n=new Array(64),t=0;t<64;t++)n[r.charCodeAt(t)]=t;var o=0,a=m(),i=m(),c=m(),d=w(),l=i.pop(),f=u(a.concat(i)),h=c.pop(),g=u(a.concat(c)),v=document.createRange();return v.setStart(f,l),v.setEnd(g,h),v.isOutdated=void 0!==d&&d!==s(v.toString()),v;function w(){for(var t=0,r=0;o<e.length;){var a=n[e.charCodeAt(o++)];if(t|=(31&a)<<r,r+=5,a<32)return t}}function m(){var e=w();if(null!=e){for(var n=new Array(e),t=0;t<e;t++)n[t]=w();return n}}}(o)).getBoundingClientRect(),i=Math.max(20,Math.floor(.4*(window.innerHeight-a.height)));window.scrollTo(0,window.scrollY+a.y-i);var c=document.getSelection();c.empty(),c.addRange(t),d()}}function d(){n||(n=document.getElementsByTagName("article")[0]),e||(e=document.createElement("a"),document.body.appendChild(e)),e.href="#sel-"+o,e.onclick=a,e.className=t.isOutdated?"outdated-selection-link":"selection-link",e.innerText=t.isOutdated?"!":"‟";var r=n.getBoundingClientRect().x,i=t.getBoundingClientRect().y;e.style.left=Math.floor(r+window.scrollX-37)+"px",e.style.top=Math.floor(i+window.scrollY-3)+"px"}function l(e){for(var n=[];e!=document.body;){var t=e.parentNode;n.push(Array.prototype.indexOf.call(t.childNodes,e)),e=t}return n.reverse()}function u(e){for(var n=document.body,t=0;t<e.length&&n;t++)n=n.childNodes[e[t]];return n}function s(e){for(var n=2166136261,t=0;t<e.length;++t)n^=e.charCodeAt(t),n+=(n<<1)+(n<<4)+(n<<7)+(n<<8)+(n<<24);return 32767&(n>>15^n)}document.addEventListener("selectionchange",function(n){var a=document.getSelection();if(a.isCollapsed)e&&(e.parentNode.removeChild(e),e=null);else{var i=a.getRangeAt(0);t&&0===i.compareBoundaryPoints(Range.START_TO_START,t)&&0===i.compareBoundaryPoints(Range.END_TO_END,t)||(o=function(e){var n="",t=l(e.startContainer),o=l(e.endContainer),a=function(e,n){var t=0;for(;t<e.length&&t<n.length&&e[t]===n[t];)t++;return e.slice(0,t)}(t,o);return c(a),c(t.slice(a.length).concat(e.startOffset)),c(o.slice(a.length).concat(e.endOffset)),i(s(e.toString())),n;function i(e){do{n+=r[31&e|(e>31?32:0)],e>>=5}while(e>0)}function c(e){i(e.length);for(var n=0;n<e.length;n++)i(e[n])}}(t=i),d())}}),window.addEventListener("resize",d),window.addEventListener("hashchange",i),window.addEventListener("load",i);})()</script>
</head>
<body><article>
<header>
Expand Down
32 changes: 31 additions & 1 deletion test/simple-header/output.html
Expand Up @@ -12,6 +12,7 @@

/* Selections */

.outdated-selection-link,
.selection-link {
position: absolute;
display: block;
Expand All @@ -30,6 +31,12 @@
-ms-user-select: none;
}

.outdated-selection-link:hover,
.selection-link:hover {
text-decoration: none;
}

.outdated-selection-link:before,
.selection-link:before {
border: 5px solid transparent;
border-left-color: #cacee0;
Expand All @@ -52,6 +59,29 @@
border-left-color: #3b5998;
}

.outdated-selection-link {
background: #f0babe;
font-size: 21px;
font-weight: 800;
line-height: 27px;
}

.outdated-selection-link:before {
border-left-color: #f0babe;
}

.outdated-selection-link:hover:after {
content: "This selection content has changed since this link was created.";
font: 9pt/11pt Cambria, "Palatino Linotype", Palatino, "Liberation Serif", serif;
position: absolute;
display: block;
white-space: nowrap;
padding: 2px 5px 1px;
top: -20px;
background: black;
color: white;
}

/* Links */

a {
Expand Down Expand Up @@ -836,7 +866,7 @@
}
</style>
<script>(function(){var e,t=document.getElementsByTagName("style")[0].sheet;function n(){e&&(t.deleteRule(e),e=void 0)}document.documentElement.addEventListener("mouseover",function(a){var u,d=a.target.attributes["data-name"];d&&(u=d.value,n(),e=t.insertRule('*[data-name="'+u+'"] { background: #FBF8D0; }',t.cssRules.length))}),document.documentElement.addEventListener("mouseout",n);})()</script>
<script>(function(){var e,n,t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";function o(e){a(new URL(e.target.href))}function r(){a(window.location)}function a(e){var n=e.hash.match(/^#sel-([A-Za-z0-9-_]+)$/);if(n){var o=document.getSelection(),r=function(e){for(var n=new Array(64),o=0;o<64;o++)n[t.charCodeAt(o)]=o;var r=0,a=f(),i=l(a.concat(f())),c=l(a.concat(f())),d=document.createRange();return d.setStart(i[0],i[1]),d.setEnd(c[0],c[1]),d;function s(){for(var t=0,o=0;;){var a=n[e.charCodeAt(r++)];if(t|=(31&a)<<o,o+=5,a<32)return t}}function f(){for(var e=s(),n=new Array(e),t=0;t<e;t++)n[t]=s();return n}}(n[1]),a=r.getBoundingClientRect(),i=Math.max(20,Math.floor(.4*(window.innerHeight-a.height)));window.scrollTo(0,window.scrollY+a.y-i),o.empty(),o.addRange(r)}}function i(){var r=document.getSelection();if(r.isCollapsed)e&&(e.parentNode.removeChild(e),e=null);else{var a=r.getRangeAt(0),i=a.getBoundingClientRect(),l=function(e){var n="",o=c(e.startContainer,e.startOffset),r=c(e.endContainer,e.endOffset),a=function(e,n){var t=0;for(;t<e.length&&t<n.length&&e[t]===n[t];)t++;return e.slice(0,t)}(o,r);return l(a),l(o.slice(a.length)),l(r.slice(a.length)),n;function i(e){do{n+=t[31&e|(e>31?32:0)],e>>=5}while(e>0)}function l(e){i(e.length);for(var n=0;n<e.length;n++)i(e[n])}}(a);n||(n=document.getElementsByTagName("article")[0]),e||((e=document.createElement("a")).className="selection-link",e.innerText="‟",document.body.appendChild(e));var d=n.getBoundingClientRect().x;e.href="#sel-"+l,e.onclick=o,e.style.left=Math.floor(d+window.scrollX-37)+"px",e.style.top=Math.floor(i.y+window.scrollY-3)+"px"}}function c(e,n){for(var t=[n];e!=document.body;){var o=e.parentNode;t.push(Array.prototype.indexOf.call(o.childNodes,e)),e=o}return t.reverse()}function l(e){for(var n=document.body,t=0;t<e.length-1&&n;t++)n=n.childNodes[e[t]];return[n,e[e.length-1]]}document.addEventListener("selectionchange",i),window.addEventListener("resize",i),window.addEventListener("hashchange",r),window.addEventListener("load",r);})()</script>
<script>(function(){var e,n,t,o,r="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";function a(e){c(new URL(e.target.href))}function i(){c(window.location)}function c(e){var n=e.hash.match(/^#sel-([A-Za-z0-9-_]+)$/);if(n){o=n[1];var a=(t=function(e){for(var n=new Array(64),t=0;t<64;t++)n[r.charCodeAt(t)]=t;var o=0,a=m(),i=m(),c=m(),d=w(),l=i.pop(),f=u(a.concat(i)),h=c.pop(),g=u(a.concat(c)),v=document.createRange();return v.setStart(f,l),v.setEnd(g,h),v.isOutdated=void 0!==d&&d!==s(v.toString()),v;function w(){for(var t=0,r=0;o<e.length;){var a=n[e.charCodeAt(o++)];if(t|=(31&a)<<r,r+=5,a<32)return t}}function m(){var e=w();if(null!=e){for(var n=new Array(e),t=0;t<e;t++)n[t]=w();return n}}}(o)).getBoundingClientRect(),i=Math.max(20,Math.floor(.4*(window.innerHeight-a.height)));window.scrollTo(0,window.scrollY+a.y-i);var c=document.getSelection();c.empty(),c.addRange(t),d()}}function d(){n||(n=document.getElementsByTagName("article")[0]),e||(e=document.createElement("a"),document.body.appendChild(e)),e.href="#sel-"+o,e.onclick=a,e.className=t.isOutdated?"outdated-selection-link":"selection-link",e.innerText=t.isOutdated?"!":"‟";var r=n.getBoundingClientRect().x,i=t.getBoundingClientRect().y;e.style.left=Math.floor(r+window.scrollX-37)+"px",e.style.top=Math.floor(i+window.scrollY-3)+"px"}function l(e){for(var n=[];e!=document.body;){var t=e.parentNode;n.push(Array.prototype.indexOf.call(t.childNodes,e)),e=t}return n.reverse()}function u(e){for(var n=document.body,t=0;t<e.length&&n;t++)n=n.childNodes[e[t]];return n}function s(e){for(var n=2166136261,t=0;t<e.length;++t)n^=e.charCodeAt(t),n+=(n<<1)+(n<<4)+(n<<7)+(n<<8)+(n<<24);return 32767&(n>>15^n)}document.addEventListener("selectionchange",function(n){var a=document.getSelection();if(a.isCollapsed)e&&(e.parentNode.removeChild(e),e=null);else{var i=a.getRangeAt(0);t&&0===i.compareBoundaryPoints(Range.START_TO_START,t)&&0===i.compareBoundaryPoints(Range.END_TO_END,t)||(o=function(e){var n="",t=l(e.startContainer),o=l(e.endContainer),a=function(e,n){var t=0;for(;t<e.length&&t<n.length&&e[t]===n[t];)t++;return e.slice(0,t)}(t,o);return c(a),c(t.slice(a.length).concat(e.startOffset)),c(o.slice(a.length).concat(e.endOffset)),i(s(e.toString())),n;function i(e){do{n+=r[31&e|(e>31?32:0)],e>>=5}while(e>0)}function c(e){i(e.length);for(var n=0;n<e.length;n++)i(e[n])}}(t=i),d())}}),window.addEventListener("resize",d),window.addEventListener("hashchange",i),window.addEventListener("load",i);})()</script>
</head>
<body><article>
<header>
Expand Down