You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@annotator.apache.org by ra...@apache.org on 2019/07/01 00:17:34 UTC

[incubator-annotator] branch master updated (64c22d6 -> a96b01c)

This is an automated email from the ASF dual-hosted git repository.

randall pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-annotator.git.


    from 64c22d6  Small copy-edits to demo
     new 717e928  Clean up demo code
     new c94ccda  Refactor text quote affix disambiguation
     new 4a303af  Fix demo race condition
     new a96b01c  Remove demo error display

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 demo/index.html                |  7 ++--
 demo/index.js                  | 81 +++++++++++++++++++++---------------------
 packages/dom/src/text-quote.js | 75 +++++++++++++++++++-------------------
 3 files changed, 79 insertions(+), 84 deletions(-)


[incubator-annotator] 01/04: Clean up demo code

Posted by ra...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

randall pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-annotator.git

commit 717e9287a67a7f99f3105bdd2e446841e1f340a0
Author: Randall Leeds <ra...@apache.org>
AuthorDate: Sun Jun 30 12:07:46 2019 -0700

    Clean up demo code
---
 demo/index.js | 67 ++++++++++++++++++++++++++++++-----------------------------
 1 file changed, 34 insertions(+), 33 deletions(-)

diff --git a/demo/index.js b/demo/index.js
index 0c13c4a..47fbe1d 100644
--- a/demo/index.js
+++ b/demo/index.js
@@ -15,63 +15,64 @@
 
 /* global corpus, debug, module, selectable */
 
-import * as fragment from '@annotator/fragment-identifier';
+import {
+  parse as parseFragment,
+  stringify as stringifyFragment,
+  SyntaxError as FragmentSyntaxError,
+} from '@annotator/fragment-identifier';
 import { describeTextQuoteByRange as describeRange } from '@annotator/dom';
 
 import { mark } from './mark.js';
 import { search } from './search.js';
 
-const refresh = async () => {
+function clear() {
   corpus.innerHTML = selectable.innerHTML;
+}
 
-  debug.classList.remove('error');
+const refresh = async () => {
+  clear();
 
   const identifier = window.location.hash.slice(1);
   if (!identifier) return;
 
   try {
-    const { selector } = fragment.parse(identifier);
+    const { selector } = parseFragment(identifier);
+    for await (const range of search(corpus, selector)) mark(range);
+    debug.classList.remove('error');
     debug.innerText = JSON.stringify(selector, null, 2);
-    const results = search(corpus, selector);
-    const ranges = [];
-    for await (let range of results) {
-      ranges.push(range);
-    }
-    for (let range of ranges) {
-      mark(range);
-    }
   } catch (e) {
     debug.classList.add('error');
     debug.innerText = JSON.stringify(e, null, 2);
-    if (e instanceof fragment.SyntaxError) return;
+    if (e instanceof FragmentSyntaxError) return;
     else throw e;
   }
 };
 
-async function onSelectionChange() {
+async function describeSelection() {
   const selection = document.getSelection();
-  if (selection === null || selection.isCollapsed) {
-    return;
-  }
+  if (selection.isCollapsed) return;
+
   const range = selection.getRangeAt(0);
-  if (!isWithinNode(range, selectable)) {
-    return;
-  }
-  const selectableRange = document.createRange();
-  selectableRange.selectNodeContents(selectable);
-  const descriptor = await describeRange({ range, context: selectableRange });
-  const nextFragment = fragment.stringify(descriptor);
-  window.history.replaceState(descriptor, null, `#${nextFragment}`);
-  refresh();
+  const context = document.createRange();
+  context.selectNodeContents(selectable);
+
+  if (!context.isPointInRange(range.startContainer, range.startOffset)) return;
+  if (!context.isPointInRange(range.endContainer, range.endOffset)) return;
+
+  return describeRange({ range, context });
 }
 
-function isWithinNode(range, node) {
-  const nodeRange = document.createRange();
-  nodeRange.selectNode(node);
-  return (
-    range.compareBoundaryPoints(Range.START_TO_START, nodeRange) >= 0 &&
-    range.compareBoundaryPoints(Range.END_TO_END, nodeRange) <= 0
-  );
+async function onSelectionChange() {
+  const selector = await describeSelection();
+
+  if (selector) {
+    const fragment = stringifyFragment(selector);
+    window.history.replaceState(selector, null, `#${fragment}`);
+  } else {
+    window.history.replaceState(null, null, location.pathname);
+  }
+
+  refresh();
 }
 
 window.addEventListener('popstate', refresh);


[incubator-annotator] 03/04: Fix demo race condition

Posted by ra...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

randall pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-annotator.git

commit 4a303af8ae0d721474fba760cbf779f6589c459f
Author: Randall Leeds <ra...@apache.org>
AuthorDate: Sun Jun 30 17:14:14 2019 -0700

    Fix demo race condition
    
    Concurrently highlighting and searching is not safe because it will
    modify the nodes during iteration.
---
 demo/index.js | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/demo/index.js b/demo/index.js
index 47fbe1d..290ba69 100644
--- a/demo/index.js
+++ b/demo/index.js
@@ -37,7 +37,16 @@ const refresh = async () => {
 
   try {
     const { selector } = parseFragment(identifier);
-    for await (const range of search(corpus, selector)) mark(range);
+    const ranges = [];
+
+    for await (const range of search(corpus, selector)) {
+      ranges.push(range);
+    }
+
+    for (const range of ranges) {
+      mark(range);
+    }
+
     debug.classList.remove('error');
     debug.innerText = JSON.stringify(selector, null, 2);
   } catch (e) {


[incubator-annotator] 02/04: Refactor text quote affix disambiguation

Posted by ra...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

randall pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-annotator.git

commit c94ccda2b90fb34b72d0f387e1314bddbfdff308
Author: Randall Leeds <ra...@apache.org>
AuthorDate: Sun Jun 30 16:45:21 2019 -0700

    Refactor text quote affix disambiguation
---
 packages/dom/src/text-quote.js | 75 ++++++++++++++++++++----------------------
 1 file changed, 36 insertions(+), 39 deletions(-)

diff --git a/packages/dom/src/text-quote.js b/packages/dom/src/text-quote.js
index ddebfbc..bc56fa1 100644
--- a/packages/dom/src/text-quote.js
+++ b/packages/dom/src/text-quote.js
@@ -133,8 +133,7 @@ export async function describeTextQuoteByRange({ range, context }) {
       : seek(iter, startNode);
   const endIndex = startIndex + exact.length;
 
-  const minSuffixes = [];
-  const minPrefixes = [];
+  const affixLengthPairs = [[0, 0]];
 
   for await (const match of selector(context)) {
     const matchIter = createNodeIterator(root, SHOW_TEXT);
@@ -152,34 +151,33 @@ export async function describeTextQuoteByRange({ range, context }) {
     }
 
     // Determine how many prefix characters are shared.
-    const prefixOverlap = overlapRight(
+    const prefixLength = overlapRight(
       text.substring(0, matchStartIndex),
       text.substring(0, startIndex),
     );
 
     // Determine how many suffix characters are shared.
-    const suffixOverlap = overlap(
+    const suffixLength = overlap(
       text.substring(matchEndIndex),
       text.substring(endIndex),
     );
 
-    // Record the prefix or suffix lengths that would not have matched.
-    minPrefixes.push(prefixOverlap + 1);
-    minSuffixes.push(suffixOverlap + 1);
+    // Record the affix lengths that would have precluded this match.
+    affixLengthPairs.push([prefixLength + 1, suffixLength + 1]);
   }
 
   // Construct and return an unambiguous selector.
   const result = { type: 'TextQuoteSelector', exact };
 
-  if (minPrefixes.length > 0 || minSuffixes.length > 0) {
-    const [minPrefix, minSuffix] = minimalSolution(minPrefixes, minSuffixes);
+  if (affixLengthPairs.length) {
+    const [prefixLength, suffixLength] = minimalSolution(affixLengthPairs);
 
-    if (minPrefix > 0) {
-      result.prefix = text.substring(startIndex - minPrefix, startIndex);
+    if (prefixLength > 0) {
+      result.prefix = text.substring(startIndex - prefixLength, startIndex);
     }
 
-    if (minSuffix > 0) {
-      result.suffix = text.substring(endIndex, endIndex + minSuffix);
+    if (suffixLength > 0) {
+      result.suffix = text.substring(endIndex, endIndex + suffixLength);
     }
   }
 
@@ -188,44 +186,43 @@ export async function describeTextQuoteByRange({ range, context }) {
 
 function overlap(text1, text2) {
   let count = 0;
-  while (text1[count] === text2[count]) {
+
+  while (count < text1.length && count < text2.length) {
+    const c1 = text1[count];
+    const c2 = text2[count];
+    if (c1 !== c2) break;
     count++;
-    if (count >= text1.length) {
-      return Infinity;
-    }
   }
+
   return count;
 }
 
 function overlapRight(text1, text2) {
   let count = 0;
-  while (text1[text1.length - 1 - count] === text2[text2.length - 1 - count]) {
+
+  while (count < text1.length && count < text2.length) {
+    const c1 = text1[text1.length - 1 - count];
+    const c2 = text2[text2.length - 1 - count];
+    if (c1 !== c2) break;
     count++;
-    if (count >= text1.length) {
-      return Infinity;
-    }
   }
+
   return count;
 }
 
-function minimalSolution(reqs1, reqs2) {
-  if (reqs1.length !== reqs2.length) {
-    throw new Error('unequal lengths');
-  }
-  // Add 0 as an option to try.
-  reqs1.push(0);
-  reqs2.push(0);
-  let bestResult = [Infinity, Infinity];
-  for (let i = 0; i < reqs1.length; i++) {
-    const req1 = reqs1[i];
-    // The values to satisfy for req2, given the proposed req1.
-    const reqsToSatisfy = reqs1.map((v, i) => (v > req1 ? reqs2[i] : 0));
-    // Take the lowest value that satisfies them all.
-    const req2 = Math.max(...reqsToSatisfy);
-    // If this combination is the best so far, remember it.
-    if (req1 + req2 < bestResult[0] + bestResult[1]) {
-      bestResult = [req1, req2];
+function minimalSolution(requirements) {
+  // Build all the pairs and order them by their sums.
+  const pairs = requirements.flatMap(l => requirements.map(r => [l[0], r[1]]));
+  pairs.sort((a, b) => a[0] + a[1] - (b[0] + b[1]));
+
+  // Find the first pair that satisfies every requirement.
+  for (const pair of pairs) {
+    const [p0, p1] = pair;
+    if (requirements.every(([r0, r1]) => r0 <= p0 || r1 <= p1)) {
+      return pair;
     }
   }
-  return bestResult;
+
+  // Return the largest pairing (unreachable).
+  return pairs[pairs.length - 1];
 }


[incubator-annotator] 04/04: Remove demo error display

Posted by ra...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

randall pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-annotator.git

commit a96b01cd5e058454f444611ef19907a9ad749430
Author: Randall Leeds <ra...@apache.org>
AuthorDate: Sun Jun 30 17:17:08 2019 -0700

    Remove demo error display
---
 demo/index.html |  7 ++-----
 demo/index.js   | 33 ++++++++++++---------------------
 2 files changed, 14 insertions(+), 26 deletions(-)

diff --git a/demo/index.html b/demo/index.html
index a41a949..1b62b4c 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -28,14 +28,11 @@ specific language governing permissions and limitations under the License.
         background-color: rgba(255, 255, 120, 0.5);
         outline: 0.1px solid rgba(255, 100, 0, 0.8);
       }
-      #debug {
+      #selector {
         color: #666;
         background: #f8f8f8;
         padding: 2em;
       }
-      #debug.error {
-        color: red;
-      }
       #selectable, #corpus {
         display: inline-block;
         max-width: 15em;
@@ -73,7 +70,7 @@ specific language governing permissions and limitations under the License.
       <a href="https://www.w3.org/TR/2017/NOTE-selectors-states-20170223/#frags"
         target="_blank">as the fragment identifier</a>.</p>
     <p>Here is the selector in JSON format:</p>
-    <pre id="debug"></pre>
+    <pre id="parsed"></pre>
     <p>Notice how, when the text of your selection appears multiple times, just
       enough characters around it are stored in the selector to find the right
       occurrence again.</p>
diff --git a/demo/index.js b/demo/index.js
index 290ba69..78f0d23 100644
--- a/demo/index.js
+++ b/demo/index.js
@@ -13,12 +13,11 @@
  * the License.
  */
 
-/* global corpus, debug, module, selectable */
+/* global corpus, module, parsed, selectable */
 
 import {
   parse as parseFragment,
   stringify as stringifyFragment,
-  SyntaxError as FragmentSyntaxError,
 } from '@annotator/fragment-identifier';
 import { describeTextQuoteByRange as describeRange } from '@annotator/dom';
 
@@ -35,26 +34,18 @@ const refresh = async () => {
   const identifier = window.location.hash.slice(1);
   if (!identifier) return;
 
-  try {
-    const { selector } = parseFragment(identifier);
-    const ranges = [];
-
-    for await (const range of search(corpus, selector)) {
-      ranges.push(range);
-    }
-
-    for (const range of ranges) {
-      mark(range);
-    }
-
-    debug.classList.remove('error');
-    debug.innerText = JSON.stringify(selector, null, 2);
-  } catch (e) {
-    debug.classList.add('error');
-    debug.innerText = JSON.stringify(e, null, 2);
-    if (e instanceof FragmentSyntaxError) return;
-    else throw e;
+  const { selector } = parseFragment(identifier);
+  const ranges = [];
+
+  for await (const range of search(corpus, selector)) {
+    ranges.push(range);
+  }
+
+  for (const range of ranges) {
+    mark(range);
   }
+
+  parsed.innerText = JSON.stringify(selector, null, 2);
 };
 
 async function describeSelection() {