You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@annotator.apache.org by ge...@apache.org on 2020/11/12 22:08:08 UTC

[incubator-annotator] branch import-dom-seek updated (00679b1 -> fb72414)

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

gerben pushed a change to branch import-dom-seek
in repository https://gitbox.apache.org/repos/asf/incubator-annotator.git.


 discard 00679b1  WIP further steps on describe text quote
 discard 0f13c97  Change approach, (re)implement normalizeRange
 discard bffbeaf  WIP make describe quote work
     new 0dd6dc4  WIP make describe quote work
     new 8360828  Change approach, (re)implement normalizeRange
     new 657daab  Make describeTextQuote work, mostly.
     new fb72414  Add babel class-properties plugin

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (00679b1)
            \
             N -- N -- N   refs/heads/import-dom-seek (fb72414)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

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:
 babel.config.js                         |  1 +
 package.json                            |  1 +
 packages/dom/src/chunker.ts             |  6 +--
 packages/dom/src/normalize-range.ts     |  4 ++
 packages/dom/src/seek.ts                | 39 ++++++++------
 packages/dom/src/text-quote/describe.ts | 85 +++++++++++++++++++++++--------
 packages/dom/src/text-quote/match.ts    | 90 ++++++++++++++++++++++-----------
 yarn.lock                               | 79 +++++++++++++++++++++++++++--
 8 files changed, 232 insertions(+), 73 deletions(-)


[incubator-annotator] 03/04: Make describeTextQuote work, mostly.

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

gerben pushed a commit to branch import-dom-seek
in repository https://gitbox.apache.org/repos/asf/incubator-annotator.git

commit 657daab660a91ce7057136e5417326b7ae42f239
Author: Gerben <ge...@treora.com>
AuthorDate: Thu Nov 12 12:14:24 2020 +0100

    Make describeTextQuote work, mostly.
    
    Tests pass, still some issues with empty quotes at chunk edges
---
 packages/dom/src/chunker.ts             |  4 +-
 packages/dom/src/normalize-range.ts     |  4 ++
 packages/dom/src/seek.ts                | 19 ++++---
 packages/dom/src/text-quote/describe.ts | 84 +++++++++++++++++++++++-------
 packages/dom/src/text-quote/match.ts    | 90 ++++++++++++++++++++++-----------
 5 files changed, 145 insertions(+), 56 deletions(-)

diff --git a/packages/dom/src/chunker.ts b/packages/dom/src/chunker.ts
index 7209d7a..2becebf 100644
--- a/packages/dom/src/chunker.ts
+++ b/packages/dom/src/chunker.ts
@@ -104,11 +104,13 @@ export class TextNodeChunker implements Chunker<PartialTextNode> {
 
   rangeToChunkRange(range: Range): ChunkRange<PartialTextNode> {
     const textRange = normalizeRange(range);
+    // FIXME: normalizeRange can mess up: a collapsed range at the very end of
+    // the chunker’s scope might move to the next text node outside the scope.
 
     const startChunk = this.nodeToChunk(textRange.startContainer);
     const startIndex = textRange.startOffset - startChunk.startOffset;
     const endChunk = this.nodeToChunk(textRange.endContainer);
-    const endIndex = textRange.endOffset - endChunk.endOffset;
+    const endIndex = textRange.endOffset - endChunk.startOffset;
 
     return { startChunk, startIndex, endChunk, endIndex };
   }
diff --git a/packages/dom/src/normalize-range.ts b/packages/dom/src/normalize-range.ts
index 8616bce..0fece41 100644
--- a/packages/dom/src/normalize-range.ts
+++ b/packages/dom/src/normalize-range.ts
@@ -48,6 +48,10 @@ export interface TextRange extends Range {
 // if there are equivalent text selections it takes the narrowest option (i.e.
 // it prefers the start not to be at the end of a text node, and vice versa).
 //
+// If there is no text between the start and end, they thus collapse onto one a
+// single position; and if there are multiple equivalent positions, it takes the
+// first one.
+//
 // Note that if the given range does not contain non-empty text nodes, it will
 // end up pointing at a text node outside of it (after it if possible, else
 // before). If the document does not contain any text nodes, an error is thrown.
diff --git a/packages/dom/src/seek.ts b/packages/dom/src/seek.ts
index 1c821a9..36914a7 100644
--- a/packages/dom/src/seek.ts
+++ b/packages/dom/src/seek.ts
@@ -111,15 +111,14 @@ export class TextSeeker<TChunk extends Chunk<string>> implements Seeker<string>
 
     if (this.position <= target) {
       while (this.position <= target) { // could be `while (true)`?
-        if (!roundUp && target < this.currentChunkPosition + this.currentChunk.data.length) {
-          // The target is before the end of the current chunk.
+        if (
+          this.currentChunkPosition + this.currentChunk.data.length <= target
+          || (roundUp && this.offsetInChunk !== 0)
+        ) {
+          // The target is beyond the current chunk.
           // (we use < not ≤: if the target is *at* the end of the chunk, possibly
           // because the current chunk is empty, we prefer to take the next chunk)
-          const newOffset = target - this.currentChunkPosition;
-          if (read) result += this.currentChunk.data.substring(this.offsetInChunk, newOffset);
-          this.offsetInChunk = newOffset;
-          break;
-        } else {
+
           // Move to the start of the next chunk, while counting the characters of the current one.
           if (read) result += this.currentChunk.data.substring(this.offsetInChunk);
           const chunkLength = this.currentChunk.data.length;
@@ -140,6 +139,12 @@ export class TextSeeker<TChunk extends Chunk<string>> implements Seeker<string>
             else
               throw new RangeError(E_END);
           }
+        } else {
+          // The target is within the current chunk.
+          const newOffset = target - this.currentChunkPosition;
+          if (read) result += this.currentChunk.data.substring(this.offsetInChunk, newOffset);
+          this.offsetInChunk = newOffset;
+          break;
         }
       }
     } else { // Similar to the if-block, but moving backward in the text.
diff --git a/packages/dom/src/text-quote/describe.ts b/packages/dom/src/text-quote/describe.ts
index cbad0c3..1a7941d 100644
--- a/packages/dom/src/text-quote/describe.ts
+++ b/packages/dom/src/text-quote/describe.ts
@@ -26,17 +26,18 @@ import { TextSeeker, NonEmptyChunker } from '../seek';
 
 export async function describeTextQuote(
   range: Range,
-  scope?: Range,
+  maybeScope?: Range,
 ): Promise<TextQuoteSelector> {
   // Default to search in the whole document.
-  if (scope === undefined) {
+  let scope: Range;
+  if (maybeScope !== undefined) {
+    scope = maybeScope;
+  } else {
     const document = ownerDocument(range);
     scope = document.createRange();
     scope.selectNodeContents(document);
   }
 
-  const chunker = new TextNodeChunker(scope);
-
   // Take the part of the range that falls within the scope.
   range = range.cloneRange();
   if (range.compareBoundaryPoints(Range.START_TO_START, scope) === -1)
@@ -44,17 +45,21 @@ export async function describeTextQuote(
   if (range.compareBoundaryPoints(Range.END_TO_END, scope) === 1)
     range.setEnd(scope.endContainer, scope.endOffset);
 
+  const chunker = new TextNodeChunker(scope);
+
   return await abstractDescribeTextQuote(
     chunker.rangeToChunkRange(range),
-    chunker,
+    () => new TextNodeChunker(scope),
   );
 }
 
 async function abstractDescribeTextQuote<TChunk extends Chunk<string>>(
   target: ChunkRange<TChunk>,
-  scope: Chunker<TChunk>,
+  scope: () => Chunker<TChunk>,
 ): Promise<TextQuoteSelector> {
-  const seeker = new TextSeeker(scope as NonEmptyChunker<TChunk>);
+  const seeker = new TextSeeker(scope() as NonEmptyChunker<TChunk>);
+
+  // Read the target’s exact text.
   seeker.seekToChunk(target.startChunk, target.startIndex);
   const exact = seeker.readToChunk(target.endChunk, target.endIndex);
 
@@ -70,7 +75,8 @@ async function abstractDescribeTextQuote<TChunk extends Chunk<string>>(
       prefix,
       suffix,
     }
-    const matches = abstractTextQuoteSelectorMatcher(tentativeSelector)(scope);
+
+    const matches = abstractTextQuoteSelectorMatcher(tentativeSelector)(scope());
     let nextMatch = await matches.next();
 
     if (!nextMatch.done && chunkRangeEquals(nextMatch.value, target)) {
@@ -81,17 +87,19 @@ async function abstractDescribeTextQuote<TChunk extends Chunk<string>>(
     // If there are no more unintended matches, our selector is unambiguous!
     if (nextMatch.done) return tentativeSelector;
 
-    // TODO either reset chunker to start from the beginning, or rewind the chunker by previous match’s length.
-    // chunker.seekTo(0)  or chunker.seek(-prefix)
+    // A subsequent search could safely skip the part we already processed,
+    // we’d need the matcher to start at the seeker’s position, instead of
+    // searching in the whole current chunk.
+    // seeker.seekBy(-prefix.length + 1);
 
     // We’ll have to add more prefix/suffix to disqualify this unintended match.
     const unintendedMatch = nextMatch.value;
-    const seeker1 = new TextSeeker(scope as NonEmptyChunker<TChunk>);
-    const seeker2 = new TextSeeker(scope as NonEmptyChunker<TChunk>); // TODO must clone scope.
+    const seeker1 = new TextSeeker(scope() as NonEmptyChunker<TChunk>);
+    const seeker2 = new TextSeeker(scope() as NonEmptyChunker<TChunk>);
 
     // Count how many characters we’d need as a prefix to disqualify this match.
-    seeker1.seekToChunk(target.startChunk, target.startIndex);
-    seeker2.seekToChunk(unintendedMatch.startChunk, unintendedMatch.startIndex);
+    seeker1.seekToChunk(target.startChunk, target.startIndex - prefix.length);
+    seeker2.seekToChunk(unintendedMatch.startChunk, unintendedMatch.startIndex - prefix.length);
     let sufficientPrefix: string | undefined = prefix;
     while (true) {
       let previousCharacter: string;
@@ -102,15 +110,53 @@ async function abstractDescribeTextQuote<TChunk extends Chunk<string>>(
         break;
       }
       sufficientPrefix = previousCharacter + sufficientPrefix;
-      if (previousCharacter !== seeker2.read(-1)) break;
+
+      // Break if the newly added character makes the prefix unambiguous.
+      try {
+        const unintendedMatchPreviousCharacter = seeker2.read(-1);
+        if (previousCharacter !== unintendedMatchPreviousCharacter) break;
+      } catch (err) {
+        if (err instanceof RangeError)
+          break;
+        else
+          throw err;
+      }
+    }
+
+    // Count how many characters we’d need as a suffix to disqualify this match.
+    seeker1.seekToChunk(target.endChunk, target.endIndex + suffix.length);
+    seeker2.seekToChunk(unintendedMatch.endChunk, unintendedMatch.endIndex + suffix.length);
+    let sufficientSuffix: string | undefined = suffix;
+    while (true) {
+      let nextCharacter: string;
+      try {
+        nextCharacter = seeker1.read(1);
+      } catch (err) {
+        sufficientSuffix = undefined; // End of text reached.
+        break;
+      }
+      sufficientSuffix += nextCharacter;
+
+      // Break if the newly added character makes the suffix unambiguous.
+      try {
+        const unintendedMatchNextCharacter = seeker2.read(1);
+        if (nextCharacter !== unintendedMatchNextCharacter) break;
+      } catch (err) {
+        if (err instanceof RangeError)
+          break;
+        else
+          throw err;
+      }
     }
 
     // Use either the prefix or suffix, whichever is shortest.
-    if (sufficientPrefix !== undefined && (sufficientSuffix === undefined || sufficientPrefix.length <= sufficientSuffix.length))
-      prefix = sufficientPrefix; // chunker.seek(prefix.length - sufficientPrefix.length)
-    else if (sufficientSuffix !== undefined)
+    if (sufficientPrefix !== undefined && (sufficientSuffix === undefined || sufficientPrefix.length <= sufficientSuffix.length)) {
+      prefix = sufficientPrefix;
+      // seeker.seekBy(sufficientPrefix.length - prefix.length) // Would be required if we’d skip the processed part.
+    } else if (sufficientSuffix !== undefined) {
       suffix = sufficientSuffix;
-    else
+    } else {
       throw new Error('Target cannot be disambiguated; how could that have happened‽');
+    }
   }
 }
diff --git a/packages/dom/src/text-quote/match.ts b/packages/dom/src/text-quote/match.ts
index 112e054..73cd1d7 100644
--- a/packages/dom/src/text-quote/match.ts
+++ b/packages/dom/src/text-quote/match.ts
@@ -53,41 +53,62 @@ export function abstractTextQuoteSelectorMatcher(
     const suffix = selector.suffix || '';
     const searchPattern = prefix + exact + suffix;
 
-    let partialMatches: Array<{
-      startChunk: TChunk;
-      startIndex: number;
+    // The code below runs a loop with three steps:
+    // 1. Continue checking any partial matches from the previous chunk(s).
+    // 2. Try find the whole pattern in the chunk (possibly multiple times).
+    // 3. Check if this chunk ends with a partial match (or even multiple partial matches).
+
+    interface PartialMatch {
+      startChunk?: TChunk;
+      startIndex?: number;
+      endChunk?: TChunk;
+      endIndex?: number;
       charactersMatched: number;
-    }> = [];
+    }
+    let partialMatches: PartialMatch[] = [];
 
     let chunk: TChunk | null;
     while (chunk = textChunks.currentChunk) {
       const chunkValue = chunk.data;
 
-      // Continue checking any partial matches from the previous chunk(s).
+      // 1. Continue checking any partial matches from the previous chunk(s).
       const remainingPartialMatches: typeof partialMatches = [];
-      for (const { startChunk, startIndex, charactersMatched } of partialMatches) {
-        if (searchPattern.length - charactersMatched > chunkValue.length) {
-          if (chunkValue === searchPattern.substring(charactersMatched, charactersMatched + chunkValue.length)) {
-            // The chunk is too short to complete the match; comparison has to be completed in subsequent chunks.
-            remainingPartialMatches.push({
-              startChunk,
-              startIndex,
-              charactersMatched: charactersMatched + chunkValue.length,
-            });
+      for (const partialMatch of partialMatches) {
+        const charactersMatched = partialMatch.charactersMatched;
+
+        // If the current chunk contains the start and/or end of the match, record these.
+        if (partialMatch.endChunk === undefined) {
+          const charactersUntilMatchEnd = prefix.length + exact.length - charactersMatched;
+          if (charactersUntilMatchEnd <= chunkValue.length) {
+            partialMatch.endChunk = chunk;
+            partialMatch.endIndex = charactersUntilMatchEnd;
           }
         }
-        else if (chunkValue.startsWith(searchPattern.substring(charactersMatched))) {
-          yield {
-            startChunk,
-            startIndex,
-            endChunk: chunk,
-            endIndex: searchPattern.length - charactersMatched,
-          };
+        if (partialMatch.startChunk === undefined) {
+          const charactersUntilMatchStart = prefix.length - charactersMatched;
+          if (
+            charactersUntilMatchStart < chunkValue.length
+            || partialMatch.endChunk !== undefined // handles an edge case: an empty quote at the end of a chunk.
+          ) {
+            partialMatch.startChunk = chunk;
+            partialMatch.startIndex = charactersUntilMatchStart;
+          }
+        }
+
+        const charactersUntilSuffixEnd = searchPattern.length - charactersMatched;
+        if (charactersUntilSuffixEnd <= chunkValue.length) {
+          if (chunkValue.startsWith(searchPattern.substring(charactersMatched))) {
+            yield partialMatch as ChunkRange<TChunk>; // all fields are certainly defined now.
+          }
+        } else if (chunkValue === searchPattern.substring(charactersMatched, charactersMatched + chunkValue.length)) {
+          // The chunk is too short to complete the match; comparison has to be completed in subsequent chunks.
+          partialMatch.charactersMatched += chunkValue.length;
+          remainingPartialMatches.push(partialMatch);
         }
       }
       partialMatches = remainingPartialMatches;
 
-      // Try find the whole pattern in the chunk (possibly multiple times).
+      // 2. Try find the whole pattern in the chunk (possibly multiple times).
       if (searchPattern.length <= chunkValue.length) {
         let fromIndex = 0;
         while (fromIndex <= chunkValue.length) {
@@ -106,11 +127,11 @@ export function abstractTextQuoteSelectorMatcher(
           };
 
           // Advance the search forward to detect multiple occurrences within the same chunk.
-          fromIndex = matchStartIndex + 1;
+          fromIndex = patternStartIndex + 1;
         }
       }
 
-      // Check if this chunk ends with a partial match (or even multiple partial matches).
+      // 3. Check if this chunk ends with a partial match (or even multiple partial matches).
       let newPartialMatches: number[] = [];
       const searchStartPoint = Math.max(chunkValue.length - searchPattern.length + 1, 0);
       for (let i = searchStartPoint; i < chunkValue.length; i++) {
@@ -121,11 +142,22 @@ export function abstractTextQuoteSelectorMatcher(
         if (character === searchPattern[0]) newPartialMatches.push(i);
       }
       for (const partialMatchStartIndex of newPartialMatches) {
-        partialMatches.push({
-          startChunk: chunk,
-          startIndex: partialMatchStartIndex,
-          charactersMatched: chunkValue.length - partialMatchStartIndex,
-        });
+        const charactersMatched = chunkValue.length - partialMatchStartIndex;
+        const partialMatch: PartialMatch = {
+          charactersMatched,
+        };
+        if (charactersMatched >= prefix.length + exact.length) {
+          partialMatch.endChunk = chunk;
+          partialMatch.endIndex = partialMatchStartIndex + prefix.length + exact.length;
+        }
+        if (
+          charactersMatched > prefix.length
+          || partialMatch.endChunk !== undefined // handles an edge case: an empty quote at the end of a chunk.
+        ) {
+          partialMatch.startChunk = chunk;
+          partialMatch.startIndex = partialMatchStartIndex + prefix.length;
+        }
+        partialMatches.push(partialMatch);
       }
 
       if (textChunks.nextChunk() === null)


[incubator-annotator] 02/04: Change approach, (re)implement normalizeRange

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

gerben pushed a commit to branch import-dom-seek
in repository https://gitbox.apache.org/repos/asf/incubator-annotator.git

commit 8360828543cac1c42a6ed6942e022c5321e2216a
Author: Gerben <ge...@treora.com>
AuthorDate: Wed Nov 11 16:54:10 2020 +0100

    Change approach, (re)implement normalizeRange
---
 packages/dom/src/chunker.ts             |  25 ++++++
 packages/dom/src/normalize-range.ts     | 135 ++++++++++++++++++++++++++++++++
 packages/dom/src/seek.ts                |  19 +----
 packages/dom/src/text-quote/describe.ts |  20 +----
 4 files changed, 165 insertions(+), 34 deletions(-)

diff --git a/packages/dom/src/chunker.ts b/packages/dom/src/chunker.ts
index c8e3015..7209d7a 100644
--- a/packages/dom/src/chunker.ts
+++ b/packages/dom/src/chunker.ts
@@ -18,6 +18,7 @@
  * under the License.
  */
 
+import { normalizeRange } from "./normalize-range";
 import { ownerDocument } from "./owner-document";
 
 // A Chunk represents a fragment (typically a string) of some document.
@@ -78,6 +79,12 @@ export class TextNodeChunker implements Chunker<PartialTextNode> {
     const node = this.iter.referenceNode;
     if (!isText(node))
       return null;
+    return this.nodeToChunk(node);
+  }
+
+  nodeToChunk(node: Text): PartialTextNode {
+    if (!this.scope.intersectsNode(node))
+      throw new Error('Cannot convert node to chunk, as it falls outside of chunker’s scope.');
     const startOffset = (node === this.scope.startContainer) ? this.scope.startOffset : 0;
     const endOffset = (node === this.scope.endContainer) ? this.scope.endOffset : node.length;
     return {
@@ -85,9 +92,27 @@ export class TextNodeChunker implements Chunker<PartialTextNode> {
       startOffset,
       endOffset,
       data: node.data.substring(startOffset, endOffset),
+      equals(other) {
+        return (
+          other.node === this.node
+          && other.startOffset === this.startOffset
+          && other.endOffset === this.endOffset
+        );
+      },
     }
   }
 
+  rangeToChunkRange(range: Range): ChunkRange<PartialTextNode> {
+    const textRange = normalizeRange(range);
+
+    const startChunk = this.nodeToChunk(textRange.startContainer);
+    const startIndex = textRange.startOffset - startChunk.startOffset;
+    const endChunk = this.nodeToChunk(textRange.endContainer);
+    const endIndex = textRange.endOffset - endChunk.endOffset;
+
+    return { startChunk, startIndex, endChunk, endIndex };
+  }
+
   constructor(private scope: Range) {
     this.iter = ownerDocument(scope).createNodeIterator(
       scope.commonAncestorContainer,
diff --git a/packages/dom/src/normalize-range.ts b/packages/dom/src/normalize-range.ts
new file mode 100644
index 0000000..8616bce
--- /dev/null
+++ b/packages/dom/src/normalize-range.ts
@@ -0,0 +1,135 @@
+/**
+ * @license
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { ownerDocument } from "./owner-document";
+
+// TextRange is a Range that guarantees to always have Text nodes as its start
+// and end nodes. To ensure the type remains correct, it also restricts usage
+// of methods that would modify these nodes (note that a user can simply cast
+// the TextRange back to a Range to remove these restrictions).
+export interface TextRange extends Range {
+  readonly startContainer: Text;
+  readonly endContainer: Text;
+  cloneRange(): TextRange;
+
+  // Allow only Text nodes to be passed to these methods.
+  insertNode(node: Text): void;
+  selectNodeContents(node: Text): void;
+  setEnd(node: Text, offset: number): void;
+  setStart(node: Text, offset: number): void;
+
+  // Do not allow these methods to be used at all.
+  selectNode(node: never): void;
+  setEndAfter(node: never): void;
+  setEndBefore(node: never): void;
+  setStartAfter(node: never): void;
+  setStartBefore(node: never): void;
+  surroundContents(newParent: never): void;
+}
+
+// Normalise a range such that both its start and end are text nodes, and that
+// if there are equivalent text selections it takes the narrowest option (i.e.
+// it prefers the start not to be at the end of a text node, and vice versa).
+//
+// Note that if the given range does not contain non-empty text nodes, it will
+// end up pointing at a text node outside of it (after it if possible, else
+// before). If the document does not contain any text nodes, an error is thrown.
+export function normalizeRange(range: Range): TextRange {
+  const document = ownerDocument(range);
+  const walker = document.createTreeWalker(document, NodeFilter.SHOW_TEXT);
+
+  let [ startContainer, startOffset ] = snapBoundaryPointToTextNode(range.startContainer, range.startOffset);
+
+  // If we point at the end of a text node, move to the start of the next one.
+  // The step is repeated to skip over empty text nodes.
+  walker.currentNode = startContainer;
+  while (startOffset === startContainer.length && walker.nextNode()) {
+    startContainer = walker.currentNode as Text;
+    startOffset = 0;
+  }
+
+  range.setStart(startContainer, startOffset);
+
+  let [ endContainer, endOffset ] = snapBoundaryPointToTextNode(range.endContainer, range.endOffset);
+
+  // If we point at the start of a text node, move to the end of the previous one.
+  // The step is repeated to skip over empty text nodes.
+  walker.currentNode = endContainer;
+  while (endOffset === 0 && walker.previousNode()) {
+    endContainer = walker.currentNode as Text;
+    endOffset = endContainer.length;
+  }
+
+  range.setEnd(endContainer, endOffset);
+
+  return range as TextRange;
+}
+
+// Given an arbitrary boundary point, this returns either:
+// - that same boundary point, if its node is a text node;
+// - otherwise the first boundary point after it whose node is a text node, if any;
+// - otherwise, the last boundary point before it whose node is a text node.
+// If the document has no text nodes, it throws an error.
+function snapBoundaryPointToTextNode(node: Node, offset: number): [Text, number] {
+  if (isText(node))
+    return [node, offset];
+
+  // Find the node at or right after the boundary point.
+  let curNode: Node;
+  if (isCharacterData(node)) {
+    curNode = node;
+  } else if (offset < node.childNodes.length) {
+    curNode = node.childNodes[offset];
+  } else {
+    curNode = node;
+    while (curNode.nextSibling === null) {
+      if (curNode.parentNode === null) // Boundary point is at end of document
+        throw new Error('not implemented'); // TODO
+      curNode = curNode.parentNode;
+    }
+    curNode = curNode.nextSibling;
+  }
+
+  if (isText(curNode))
+    return [curNode, 0];
+
+  // Walk to the next text node, or the last if there is none.
+  const document = node.ownerDocument ?? node as Document;
+  const walker = document.createTreeWalker(document, NodeFilter.SHOW_TEXT);
+  walker.currentNode = curNode;
+  if (walker.nextNode() !== null)
+    return [walker.currentNode as Text, 0];
+  else if (walker.previousNode() !== null)
+    return [walker.currentNode as Text, (walker.currentNode as Text).length];
+  else
+    throw new Error('Document contains no text nodes.');
+}
+
+function isText(node: Node): node is Text {
+  return node.nodeType === Node.TEXT_NODE;
+}
+
+function isCharacterData(node: Node): node is CharacterData {
+  return (
+    node.nodeType === Node.PROCESSING_INSTRUCTION_NODE
+    || node.nodeType === Node.COMMENT_NODE
+    || node.nodeType === Node.TEXT_NODE
+  );
+}
diff --git a/packages/dom/src/seek.ts b/packages/dom/src/seek.ts
index af38386..1c821a9 100644
--- a/packages/dom/src/seek.ts
+++ b/packages/dom/src/seek.ts
@@ -170,10 +170,8 @@ export class TextSeeker<TChunk extends Chunk<string>> implements Seeker<string>
 }
 
 export class DomSeeker extends TextSeeker<PartialTextNode> implements BoundaryPointer<Text> {
-  constructor(chunkerOrScope: Chunker<PartialTextNode> | Range) {
-    const chunker = 'currentChunk' in chunkerOrScope
-      ? chunkerOrScope
-      : new TextNodeChunker(chunkerOrScope);
+  constructor(scope: Range) {
+    const chunker = new TextNodeChunker(scope);
     if (chunker.currentChunk === null)
       throw new RangeError('Range does not contain any Text nodes.');
     super(chunker as NonEmptyChunker<PartialTextNode>);
@@ -186,17 +184,4 @@ export class DomSeeker extends TextSeeker<PartialTextNode> implements BoundaryPo
   get offsetInReferenceNode() {
     return this.offsetInChunk + this.currentChunk.startOffset;
   }
-
-  seekToBoundaryPoint(node: Node, offset: number) {
-    const document = (node.ownerDocument ?? node as Document);
-    const target = document.createRange();
-    target.setStart(node, offset);
-    // target.setEnd(node, offset); // (implied by setting the start)
-
-    // Seek step by step until we are at, or crossed, the target point.
-    const reverse = !!(node.compareDocumentPosition(this.referenceNode) & Node.DOCUMENT_POSITION_PRECEDING);
-    while (target.comparePoint(this.referenceNode, this.offsetInReferenceNode) === (reverse ? 1 : -1)) {
-      this.seekBy(reverse ? -1 : 1);
-    }
-  }
 }
diff --git a/packages/dom/src/text-quote/describe.ts b/packages/dom/src/text-quote/describe.ts
index 8ccf47e..cbad0c3 100644
--- a/packages/dom/src/text-quote/describe.ts
+++ b/packages/dom/src/text-quote/describe.ts
@@ -20,9 +20,9 @@
 
 import type { TextQuoteSelector } from '@annotator/selector';
 import { ownerDocument } from '../owner-document';
-import { Chunk, Chunker, ChunkRange, PartialTextNode, TextNodeChunker, chunkRangeEquals } from '../chunker';
+import { Chunk, Chunker, ChunkRange, TextNodeChunker, chunkRangeEquals } from '../chunker';
 import { abstractTextQuoteSelectorMatcher } from '.';
-import { DomSeeker, TextSeeker, NonEmptyChunker } from '../seek';
+import { TextSeeker, NonEmptyChunker } from '../seek';
 
 export async function describeTextQuote(
   range: Range,
@@ -45,7 +45,7 @@ export async function describeTextQuote(
     range.setEnd(scope.endContainer, scope.endOffset);
 
   return await abstractDescribeTextQuote(
-    convertRangeToChunkRange(chunker, range),
+    chunker.rangeToChunkRange(range),
     chunker,
   );
 }
@@ -114,17 +114,3 @@ async function abstractDescribeTextQuote<TChunk extends Chunk<string>>(
       throw new Error('Target cannot be disambiguated; how could that have happened‽');
   }
 }
-
-function convertRangeToChunkRange(chunker: Chunker<PartialTextNode>, range: Range): ChunkRange<PartialTextNode> {
-  const domSeeker = new DomSeeker(chunker);
-
-  domSeeker.seekToBoundaryPoint(range.startContainer, range.startOffset);
-  const startChunk = domSeeker.currentChunk;
-  const startIndex = domSeeker.offsetInChunk;
-
-  domSeeker.seekToBoundaryPoint(range.endContainer, range.endOffset);
-  const endChunk = domSeeker.currentChunk;
-  const endIndex = domSeeker.offsetInChunk;
-
-  return { startChunk, startIndex, endChunk, endIndex };
-}


[incubator-annotator] 01/04: WIP make describe quote work

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

gerben pushed a commit to branch import-dom-seek
in repository https://gitbox.apache.org/repos/asf/incubator-annotator.git

commit 0dd6dc420d9544d73f3aab39fed4041f49089c0a
Author: Gerben <ge...@treora.com>
AuthorDate: Fri Nov 6 22:20:22 2020 +0100

    WIP make describe quote work
---
 packages/dom/src/seek.ts                |  55 +++++++++--
 packages/dom/src/text-quote/describe.ts | 163 ++++++++++++++------------------
 2 files changed, 119 insertions(+), 99 deletions(-)

diff --git a/packages/dom/src/seek.ts b/packages/dom/src/seek.ts
index c055291..af38386 100644
--- a/packages/dom/src/seek.ts
+++ b/packages/dom/src/seek.ts
@@ -18,14 +18,12 @@
  * under the License.
  */
 
-import { Chunk, TextNodeChunker, PartialTextNode } from "./chunker";
+import { Chunk, Chunker, TextNodeChunker, PartialTextNode, chunkEquals } from "./chunker";
 
 const E_END = 'Iterator exhausted before seek ended.';
 
-interface NonEmptyChunker<TChunk extends Chunk<any>> {
+export interface NonEmptyChunker<TChunk extends Chunk<any>> extends Chunker<TChunk> {
   readonly currentChunk: TChunk;
-  nextChunk(): TChunk | null;
-  previousChunk(): TChunk | null;
 }
 
 export interface BoundaryPointer<T extends any> {
@@ -77,6 +75,35 @@ export class TextSeeker<TChunk extends Chunk<string>> implements Seeker<string>
     this._readOrSeekTo(false, target);
   }
 
+  seekToChunk(target: TChunk, offset: number = 0) {
+    this._readOrSeekToChunk(false, target, offset);
+  }
+
+  readToChunk(target: TChunk, offset: number = 0): string {
+    return this._readOrSeekToChunk(true, target, offset);
+  }
+
+  private _readOrSeekToChunk(read: true, target: TChunk, offset?: number): string
+  private _readOrSeekToChunk(read: false, target: TChunk, offset?: number): void
+  private _readOrSeekToChunk(read: boolean, target: TChunk, offset: number = 0): string {
+    // XXX We have no way of knowing whether a chunk follows or precedes the current chunk; we assume it follows.
+    let result = '';
+    // This will throw a RangeError if we reach the end without encountering the target chunk.
+    while (!chunkEquals(this.currentChunk, target)) {
+      if (read)
+        result += this.read(1, true);
+      else
+        this._readOrSeekTo(false, this.position + 1, true);
+    }
+    if (offset > this.offsetInChunk) {
+      if (read)
+        result += this.read(offset - this.offsetInChunk);
+      else
+        this.seekBy(offset - this.offsetInChunk);
+    }
+    return result;
+  }
+
   private _readOrSeekTo(read: true, target: number, roundUp?: boolean): string
   private _readOrSeekTo(read: false, target: number, roundUp?: boolean): void
   private _readOrSeekTo(read: boolean, target: number, roundUp: boolean = false): string | void {
@@ -142,10 +169,11 @@ export class TextSeeker<TChunk extends Chunk<string>> implements Seeker<string>
   }
 }
 
-
 export class DomSeeker extends TextSeeker<PartialTextNode> implements BoundaryPointer<Text> {
-  constructor(scope: Range) {
-    const chunker = new TextNodeChunker(scope);
+  constructor(chunkerOrScope: Chunker<PartialTextNode> | Range) {
+    const chunker = 'currentChunk' in chunkerOrScope
+      ? chunkerOrScope
+      : new TextNodeChunker(chunkerOrScope);
     if (chunker.currentChunk === null)
       throw new RangeError('Range does not contain any Text nodes.');
     super(chunker as NonEmptyChunker<PartialTextNode>);
@@ -158,4 +186,17 @@ export class DomSeeker extends TextSeeker<PartialTextNode> implements BoundaryPo
   get offsetInReferenceNode() {
     return this.offsetInChunk + this.currentChunk.startOffset;
   }
+
+  seekToBoundaryPoint(node: Node, offset: number) {
+    const document = (node.ownerDocument ?? node as Document);
+    const target = document.createRange();
+    target.setStart(node, offset);
+    // target.setEnd(node, offset); // (implied by setting the start)
+
+    // Seek step by step until we are at, or crossed, the target point.
+    const reverse = !!(node.compareDocumentPosition(this.referenceNode) & Node.DOCUMENT_POSITION_PRECEDING);
+    while (target.comparePoint(this.referenceNode, this.offsetInReferenceNode) === (reverse ? 1 : -1)) {
+      this.seekBy(reverse ? -1 : 1);
+    }
+  }
 }
diff --git a/packages/dom/src/text-quote/describe.ts b/packages/dom/src/text-quote/describe.ts
index e5846bc..8ccf47e 100644
--- a/packages/dom/src/text-quote/describe.ts
+++ b/packages/dom/src/text-quote/describe.ts
@@ -20,7 +20,9 @@
 
 import type { TextQuoteSelector } from '@annotator/selector';
 import { ownerDocument } from '../owner-document';
-import { Seeker } from '../seek';
+import { Chunk, Chunker, ChunkRange, PartialTextNode, TextNodeChunker, chunkRangeEquals } from '../chunker';
+import { abstractTextQuoteSelectorMatcher } from '.';
+import { DomSeeker, TextSeeker, NonEmptyChunker } from '../seek';
 
 export async function describeTextQuote(
   range: Range,
@@ -32,120 +34,97 @@ export async function describeTextQuote(
     scope = document.createRange();
     scope.selectNodeContents(document);
   }
-  range = range.cloneRange();
+
+  const chunker = new TextNodeChunker(scope);
 
   // Take the part of the range that falls within the scope.
+  range = range.cloneRange();
   if (range.compareBoundaryPoints(Range.START_TO_START, scope) === -1)
     range.setStart(scope.startContainer, scope.startOffset);
   if (range.compareBoundaryPoints(Range.END_TO_END, scope) === 1)
     range.setEnd(scope.endContainer, scope.endOffset);
 
-  return {
-    type: 'TextQuoteSelector',
-    exact: range.toString(),
-    ...calculateContextForDisambiguation(range, scope),
-  };
+  return await abstractDescribeTextQuote(
+    convertRangeToChunkRange(chunker, range),
+    chunker,
+  );
 }
 
-function calculateContextForDisambiguation(
-  range: Range,
-  scope: Range,
-): { prefix: string; suffix: string } {
-  const exactText = range.toString();
-  const scopeText = scope.toString();
-  const targetStartIndex = getRangeTextPosition(range, scope);
-  const targetEndIndex = targetStartIndex + exactText.length;
+async function abstractDescribeTextQuote<TChunk extends Chunk<string>>(
+  target: ChunkRange<TChunk>,
+  scope: Chunker<TChunk>,
+): Promise<TextQuoteSelector> {
+  const seeker = new TextSeeker(scope as NonEmptyChunker<TChunk>);
+  seeker.seekToChunk(target.startChunk, target.startIndex);
+  const exact = seeker.readToChunk(target.endChunk, target.endIndex);
 
   // Starting with an empty prefix and suffix, we search for matches. At each unintended match
   // we encounter, we extend the prefix or suffix just enough to ensure it will no longer match.
   let prefix = '';
   let suffix = '';
-  let fromIndex = 0;
-  while (fromIndex <= scopeText.length) {
-    const searchPattern = prefix + exactText + suffix;
-    const patternMatchIndex = scopeText.indexOf(searchPattern, fromIndex);
-    if (patternMatchIndex === -1) break;
-    fromIndex = patternMatchIndex + 1;
 
-    const matchStartIndex = patternMatchIndex + prefix.length;
-    const matchEndIndex = matchStartIndex + exactText.length;
+  while (true) {
+    const tentativeSelector: TextQuoteSelector = {
+      type: 'TextQuoteSelector',
+      exact,
+      prefix,
+      suffix,
+    }
+    const matches = abstractTextQuoteSelectorMatcher(tentativeSelector)(scope);
+    let nextMatch = await matches.next();
+
+    if (!nextMatch.done && chunkRangeEquals(nextMatch.value, target)) {
+      // This match is the intended one, ignore it.
+      nextMatch = await matches.next();
+    }
+
+    // If there are no more unintended matches, our selector is unambiguous!
+    if (nextMatch.done) return tentativeSelector;
+
+    // TODO either reset chunker to start from the beginning, or rewind the chunker by previous match’s length.
+    // chunker.seekTo(0)  or chunker.seek(-prefix)
 
-    // Skip the found match if it is the actual target.
-    if (matchStartIndex === targetStartIndex) continue;
+    // We’ll have to add more prefix/suffix to disqualify this unintended match.
+    const unintendedMatch = nextMatch.value;
+    const seeker1 = new TextSeeker(scope as NonEmptyChunker<TChunk>);
+    const seeker2 = new TextSeeker(scope as NonEmptyChunker<TChunk>); // TODO must clone scope.
 
     // Count how many characters we’d need as a prefix to disqualify this match.
-    let sufficientPrefixLength = prefix.length + 1;
-    const firstChar = (offset: number) =>
-      scopeText[offset - sufficientPrefixLength];
-    while (
-      firstChar(targetStartIndex) &&
-      firstChar(targetStartIndex) === firstChar(matchStartIndex)
-    )
-      sufficientPrefixLength++;
-    if (!firstChar(targetStartIndex))
-      // We reached the start of scopeText; prefix won’t work.
-      sufficientPrefixLength = Infinity;
-
-    // Count how many characters we’d need as a suffix to disqualify this match.
-    let sufficientSuffixLength = suffix.length + 1;
-    const lastChar = (offset: number) =>
-      scopeText[offset + sufficientSuffixLength - 1];
-    while (
-      lastChar(targetEndIndex) &&
-      lastChar(targetEndIndex) === lastChar(matchEndIndex)
-    )
-      sufficientSuffixLength++;
-    if (!lastChar(targetEndIndex))
-      // We reached the end of scopeText; suffix won’t work.
-      sufficientSuffixLength = Infinity;
+    seeker1.seekToChunk(target.startChunk, target.startIndex);
+    seeker2.seekToChunk(unintendedMatch.startChunk, unintendedMatch.startIndex);
+    let sufficientPrefix: string | undefined = prefix;
+    while (true) {
+      let previousCharacter: string;
+      try {
+        previousCharacter = seeker1.read(-1);
+      } catch (err) {
+        sufficientPrefix = undefined; // Start of text reached.
+        break;
+      }
+      sufficientPrefix = previousCharacter + sufficientPrefix;
+      if (previousCharacter !== seeker2.read(-1)) break;
+    }
 
     // Use either the prefix or suffix, whichever is shortest.
-    if (sufficientPrefixLength <= sufficientSuffixLength) {
-      // Compensate our search position for the increase in prefix length.
-      fromIndex -= sufficientPrefixLength - prefix.length;
-      prefix = scopeText.substring(
-        targetStartIndex - sufficientPrefixLength,
-        targetStartIndex,
-      );
-    } else {
-      suffix = scopeText.substring(
-        targetEndIndex,
-        targetEndIndex + sufficientSuffixLength,
-      );
-    }
+    if (sufficientPrefix !== undefined && (sufficientSuffix === undefined || sufficientPrefix.length <= sufficientSuffix.length))
+      prefix = sufficientPrefix; // chunker.seek(prefix.length - sufficientPrefix.length)
+    else if (sufficientSuffix !== undefined)
+      suffix = sufficientSuffix;
+    else
+      throw new Error('Target cannot be disambiguated; how could that have happened‽');
   }
-
-  return { prefix, suffix };
 }
 
-// Get the index of the first character of range within the text of scope.
-function getRangeTextPosition(range: Range, scope: Range): number {
-  const seeker = new Seeker(scope);
-  const scopeOffset = isTextNode(scope.startContainer) ? scope.startOffset : 0;
-  if (isTextNode(range.startContainer))
-    return seeker.seek(range.startContainer) + range.startOffset - scopeOffset;
-  else return seeker.seek(firstTextNodeInRange(range)) - scopeOffset;
-}
+function convertRangeToChunkRange(chunker: Chunker<PartialTextNode>, range: Range): ChunkRange<PartialTextNode> {
+  const domSeeker = new DomSeeker(chunker);
 
-function firstTextNodeInRange(range: Range): Text {
-  // Find the first text node inside the range.
-  const iter = ownerDocument(range).createNodeIterator(
-    range.commonAncestorContainer,
-    NodeFilter.SHOW_TEXT,
-    {
-      acceptNode(node: Text) {
-        // Only reveal nodes within the range; and skip any empty text nodes.
-        return range.intersectsNode(node) && node.length > 0
-          ? NodeFilter.FILTER_ACCEPT
-          : NodeFilter.FILTER_REJECT;
-      },
-    },
-  );
-  const node = iter.nextNode() as Text | null;
-  if (node === null) throw new Error('Range contains no text nodes');
-  return node;
-}
+  domSeeker.seekToBoundaryPoint(range.startContainer, range.startOffset);
+  const startChunk = domSeeker.currentChunk;
+  const startIndex = domSeeker.offsetInChunk;
+
+  domSeeker.seekToBoundaryPoint(range.endContainer, range.endOffset);
+  const endChunk = domSeeker.currentChunk;
+  const endIndex = domSeeker.offsetInChunk;
 
-function isTextNode(node: Node): node is Text {
-  return node.nodeType === Node.TEXT_NODE;
+  return { startChunk, startIndex, endChunk, endIndex };
 }


[incubator-annotator] 04/04: Add babel class-properties plugin

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

gerben pushed a commit to branch import-dom-seek
in repository https://gitbox.apache.org/repos/asf/incubator-annotator.git

commit fb72414826cc691ffb1fddc7319d03951973e29e
Author: Gerben <ge...@treora.com>
AuthorDate: Thu Nov 12 23:04:09 2020 +0100

    Add babel class-properties plugin
---
 babel.config.js |  1 +
 package.json    |  1 +
 yarn.lock       | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++----
 3 files changed, 76 insertions(+), 5 deletions(-)

diff --git a/babel.config.js b/babel.config.js
index 8e5e5a3..71d3912 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -86,6 +86,7 @@ module.exports = (api) => {
 
   return {
     plugins: [
+      '@babel/plugin-proposal-class-properties',
       ['@babel/transform-runtime', runtimeOptions],
       ...(TEST ? ['istanbul'] : []),
       ['add-import-extension', addImportExtensionOptions],
diff --git a/package.json b/package.json
index 8b98e05..602be2b 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
   "devDependencies": {
     "@babel/cli": "^7.11.0",
     "@babel/core": "^7.11.0",
+    "@babel/plugin-proposal-class-properties": "^7.12.1",
     "@babel/plugin-transform-runtime": "^7.11.0",
     "@babel/preset-env": "^7.11.0",
     "@babel/preset-typescript": "^7.10.4",
diff --git a/yarn.lock b/yarn.lock
index 2e9fe82..b969327 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -65,6 +65,15 @@
     jsesc "^2.5.1"
     source-map "^0.5.0"
 
+"@babel/generator@^7.12.5":
+  version "7.12.5"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.5.tgz#a2c50de5c8b6d708ab95be5e6053936c1884a4de"
+  integrity sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==
+  dependencies:
+    "@babel/types" "^7.12.5"
+    jsesc "^2.5.1"
+    source-map "^0.5.0"
+
 "@babel/helper-annotate-as-pure@^7.10.4":
   version "7.10.4"
   resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3"
@@ -103,6 +112,17 @@
     "@babel/helper-replace-supers" "^7.10.4"
     "@babel/helper-split-export-declaration" "^7.10.4"
 
+"@babel/helper-create-class-features-plugin@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz#3c45998f431edd4a9214c5f1d3ad1448a6137f6e"
+  integrity sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w==
+  dependencies:
+    "@babel/helper-function-name" "^7.10.4"
+    "@babel/helper-member-expression-to-functions" "^7.12.1"
+    "@babel/helper-optimise-call-expression" "^7.10.4"
+    "@babel/helper-replace-supers" "^7.12.1"
+    "@babel/helper-split-export-declaration" "^7.10.4"
+
 "@babel/helper-create-regexp-features-plugin@^7.10.4":
   version "7.10.4"
   resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8"
@@ -158,6 +178,13 @@
   dependencies:
     "@babel/types" "^7.11.0"
 
+"@babel/helper-member-expression-to-functions@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.1.tgz#fba0f2fcff3fba00e6ecb664bb5e6e26e2d6165c"
+  integrity sha512-k0CIe3tXUKTRSoEx1LQEPFU9vRQfqHtl+kf8eNnDqb4AUJEy5pz6aIiog+YWtVm2jpggjS1laH68bPsR+KWWPQ==
+  dependencies:
+    "@babel/types" "^7.12.1"
+
 "@babel/helper-module-imports@^7.10.4":
   version "7.10.4"
   resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620"
@@ -217,6 +244,16 @@
     "@babel/traverse" "^7.10.4"
     "@babel/types" "^7.10.4"
 
+"@babel/helper-replace-supers@^7.12.1":
+  version "7.12.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz#f009a17543bbbbce16b06206ae73b63d3fca68d9"
+  integrity sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==
+  dependencies:
+    "@babel/helper-member-expression-to-functions" "^7.12.1"
+    "@babel/helper-optimise-call-expression" "^7.10.4"
+    "@babel/traverse" "^7.12.5"
+    "@babel/types" "^7.12.5"
+
 "@babel/helper-simple-access@^7.10.4":
   version "7.10.4"
   resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461"
@@ -277,6 +314,11 @@
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037"
   integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==
 
+"@babel/parser@^7.12.5":
+  version "7.12.5"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.5.tgz#b4af32ddd473c0bfa643bd7ff0728b8e71b81ea0"
+  integrity sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==
+
 "@babel/plugin-proposal-async-generator-functions@^7.10.4":
   version "7.10.5"
   resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz#3491cabf2f7c179ab820606cec27fed15e0e8558"
@@ -294,6 +336,14 @@
     "@babel/helper-create-class-features-plugin" "^7.10.4"
     "@babel/helper-plugin-utils" "^7.10.4"
 
+"@babel/plugin-proposal-class-properties@^7.12.1":
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz#a082ff541f2a29a4821065b8add9346c0c16e5de"
+  integrity sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w==
+  dependencies:
+    "@babel/helper-create-class-features-plugin" "^7.12.1"
+    "@babel/helper-plugin-utils" "^7.10.4"
+
 "@babel/plugin-proposal-dynamic-import@^7.10.4":
   version "7.10.4"
   resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz#ba57a26cb98b37741e9d5bca1b8b0ddf8291f17e"
@@ -888,6 +938,21 @@
     globals "^11.1.0"
     lodash "^4.17.19"
 
+"@babel/traverse@^7.12.5":
+  version "7.12.5"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.5.tgz#78a0c68c8e8a35e4cacfd31db8bb303d5606f095"
+  integrity sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA==
+  dependencies:
+    "@babel/code-frame" "^7.10.4"
+    "@babel/generator" "^7.12.5"
+    "@babel/helper-function-name" "^7.10.4"
+    "@babel/helper-split-export-declaration" "^7.11.0"
+    "@babel/parser" "^7.12.5"
+    "@babel/types" "^7.12.5"
+    debug "^4.1.0"
+    globals "^11.1.0"
+    lodash "^4.17.19"
+
 "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.4.4":
   version "7.11.5"
   resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d"
@@ -897,6 +962,15 @@
     lodash "^4.17.19"
     to-fast-properties "^2.0.0"
 
+"@babel/types@^7.12.1", "@babel/types@^7.12.5":
+  version "7.12.6"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.6.tgz#ae0e55ef1cce1fbc881cd26f8234eb3e657edc96"
+  integrity sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.10.4"
+    lodash "^4.17.19"
+    to-fast-properties "^2.0.0"
+
 "@evocateur/libnpmaccess@^3.1.2":
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/@evocateur/libnpmaccess/-/libnpmaccess-3.1.2.tgz#ecf7f6ce6b004e9f942b098d92200be4a4b1c845"
@@ -3911,11 +3985,6 @@ doctrine@^3.0.0:
   dependencies:
     esutils "^2.0.2"
 
-dom-seek@^5.1.0:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/dom-seek/-/dom-seek-5.1.1.tgz#4e35bee763b6ba082f372345823ec9665d1fbf26"
-  integrity sha512-1strSwd201Gfhfkfsk77SX9xyJGzu12gqUo5Q0W3Njtj2QxcfQTwCDOynZ6npZ4ASUFRQq0asjYDRlFxYPKwTA==
-
 domain-browser@^1.1.1:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"