Merge branch 'dom-tests'
diff --git a/.mocharc.js b/.mocharc.js
index a6239f7..8058a94 100644
--- a/.mocharc.js
+++ b/.mocharc.js
@@ -19,5 +19,5 @@
*/
module.exports = {
- require: ['./babel-register.js'],
+ require: ['./babel-register.js', 'global-jsdom/lib/register'],
};
diff --git a/@types/dom-node-iterator/index.d.ts b/@types/dom-node-iterator/index.d.ts
deleted file mode 100644
index 0e10887..0000000
--- a/@types/dom-node-iterator/index.d.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-declare module 'dom-node-iterator' {
- let createNodeIterator: Document['createNodeIterator'];
- export default createNodeIterator;
-}
diff --git a/@types/dom-seek/index.d.ts b/@types/dom-seek/index.d.ts
index 0ba0753..5bc1bc2 100644
--- a/@types/dom-seek/index.d.ts
+++ b/@types/dom-seek/index.d.ts
@@ -1,3 +1,3 @@
declare module 'dom-seek' {
- export default function seek(iter: NodeIterator, where: number | Node): number;
+ export default function seek(iter: NodeIterator, where: number | Text): number;
}
diff --git a/package.json b/package.json
index db5ac9c..190cae1 100644
--- a/package.json
+++ b/package.json
@@ -25,9 +25,9 @@
"prepare": "lerna run prepare",
"prepublishOnly": "yarn run build",
"start": "yarn run web:server",
- "test": "cross-env BABEL_ENV=test nyc mocha packages/*/test/**/*.[jt]s",
+ "test": "cross-env BABEL_ENV=test nyc mocha packages/*/test/**/*.test.[jt]s",
"typecheck": "tsc && tsc -p tsconfig.tests.json",
- "validate": "cross-env BABEL_ENV=test mocha test/**/*.[jt]s",
+ "validate": "cross-env BABEL_ENV=test mocha test/**/*.test.[jt]s",
"web:build": "webpack --config=web/webpack.config.js --mode development",
"web:server": "webpack-dev-server --config=web/webpack.config.js --hot --mode development"
},
@@ -58,7 +58,9 @@
"eslint-plugin-node": "^11.0.0",
"eslint-plugin-prettier": "^3.1.2",
"file-loader": "^6.0.0",
+ "global-jsdom": "^6.1.0",
"husky": "^4.2.1",
+ "jsdom": "^16.2.2",
"lerna": "^3.20.2",
"lint-staged": "^10.0.2",
"mocha": "^7.0.0",
diff --git a/packages/dom/package.json b/packages/dom/package.json
index fb1f5a6..4f0baed 100644
--- a/packages/dom/package.json
+++ b/packages/dom/package.json
@@ -15,8 +15,7 @@
"@babel/runtime-corejs3": "^7.8.7",
"cartesian": "^1.0.1",
"core-js": "^3.6.4",
- "dom-node-iterator": "^3.5.3",
- "dom-seek": "^4.0.3"
+ "dom-seek": "^5.1.0"
},
"engines": {
"node": ">=10.0.0"
diff --git a/packages/dom/src/index.ts b/packages/dom/src/index.ts
index 5b89ef6..6ef53d7 100644
--- a/packages/dom/src/index.ts
+++ b/packages/dom/src/index.ts
@@ -19,6 +19,6 @@
*/
export * from './css';
-export * from './range';
+export * from './range/index';
export * from './text-quote/index';
export * from './highlight-range';
diff --git a/packages/dom/src/cartesian.ts b/packages/dom/src/range/cartesian.ts
similarity index 100%
rename from packages/dom/src/cartesian.ts
rename to packages/dom/src/range/cartesian.ts
diff --git a/packages/dom/test/index.js b/packages/dom/src/range/index.ts
similarity index 96%
rename from packages/dom/test/index.js
rename to packages/dom/src/range/index.ts
index ebd8b07..011e994 100644
--- a/packages/dom/test/index.js
+++ b/packages/dom/src/range/index.ts
@@ -18,4 +18,4 @@
* under the License.
*/
-export {};
+export * from './match';
diff --git a/packages/dom/src/range.ts b/packages/dom/src/range/match.ts
similarity index 90%
rename from packages/dom/src/range.ts
rename to packages/dom/src/range/match.ts
index e465386..e540d5c 100644
--- a/packages/dom/src/range.ts
+++ b/packages/dom/src/range/match.ts
@@ -18,10 +18,10 @@
* under the License.
*/
-import { ownerDocument } from './scope';
+import { ownerDocument } from '../scope';
import { product } from './cartesian';
-import { RangeSelector, Selector } from '../../selector/src/types';
-import { DomMatcher, DomScope } from './types';
+import { RangeSelector, Selector } from '../../../selector/src/types';
+import { DomMatcher, DomScope } from '../types';
export function makeCreateRangeSelectorMatcher(
createMatcher: <T extends Selector>(selector: T) => DomMatcher
diff --git a/packages/dom/src/text-quote/describe.ts b/packages/dom/src/text-quote/describe.ts
index ef7eb53..15749c9 100644
--- a/packages/dom/src/text-quote/describe.ts
+++ b/packages/dom/src/text-quote/describe.ts
@@ -18,136 +18,104 @@
* under the License.
*/
-import createNodeIterator from 'dom-node-iterator';
import seek from 'dom-seek';
import { TextQuoteSelector } from '../../../selector/src';
import { DomScope } from '../types';
import { ownerDocument, rangeFromScope } from '../scope';
-import { createTextQuoteSelectorMatcher } from './match';
-
-function firstTextNodeInRange(range: Range): Text {
- const { startContainer } = range;
-
- if (isTextNode(startContainer)) return startContainer;
-
- const root = range.commonAncestorContainer;
- const iter = createNodeIterator(root, NodeFilter.SHOW_TEXT);
- return iter.nextNode() as Text;
-}
export async function describeTextQuote(
range: Range,
scope: DomScope = ownerDocument(range).documentElement,
): Promise<TextQuoteSelector> {
+ range = range.cloneRange();
+
+ // Take the part of the range that falls within the scope.
+ const scopeAsRange = rangeFromScope(scope);
+ if (!scopeAsRange.isPointInRange(range.startContainer, range.startOffset))
+ range.setStart(scopeAsRange.startContainer, scopeAsRange.startOffset);
+ if (!scopeAsRange.isPointInRange(range.endContainer, range.endOffset))
+ range.setEnd(scopeAsRange.endContainer, scopeAsRange.endOffset);
+
const exact = range.toString();
const result: TextQuoteSelector = { type: 'TextQuoteSelector', exact };
- const { prefix, suffix } = await calculateContextForDisambiguation(range, result, scope);
+ const { prefix, suffix } = calculateContextForDisambiguation(range, result, scope);
result.prefix = prefix;
result.suffix = suffix;
return result
}
-async function calculateContextForDisambiguation(
+function calculateContextForDisambiguation(
range: Range,
selector: TextQuoteSelector,
scope: DomScope,
-): Promise<{ prefix?: string, suffix?: string }> {
- const scopeAsRange = rangeFromScope(scope);
- const root = scopeAsRange.commonAncestorContainer;
- const text = scopeAsRange.toString();
+): { prefix?: string, suffix?: string } {
+ const exactText = selector.exact;
+ const scopeText = rangeFromScope(scope).toString();
+ const targetStartIndex = getRangeTextPosition(range, scope);
+ const targetEndIndex = targetStartIndex + exactText.length;
- const matcher = createTextQuoteSelectorMatcher(selector);
+ // Find all matches of the text in the scope.
+ const stringMatches: number[] = [];
+ let fromIndex = 0;
+ while (fromIndex <= scopeText.length) {
+ const matchIndex = scopeText.indexOf(exactText, fromIndex);
+ if (matchIndex === -1) break;
+ stringMatches.push(matchIndex);
+ fromIndex = matchIndex + 1;
+ }
- const iter = createNodeIterator(root, NodeFilter.SHOW_TEXT);
-
- const startNode = firstTextNodeInRange(range);
- const startIndex =
- isTextNode(range.startContainer)
- ? seek(iter, startNode) + range.startOffset
- : seek(iter, startNode);
- const endIndex = startIndex + selector.exact.length;
-
+ // Count for each undesired match the required prefix and suffix lengths, such that either of them
+ // would have invalidated the match.
const affixLengthPairs: Array<[number, number]> = [];
+ for (const matchStartIndex of stringMatches) {
+ const matchEndIndex = matchStartIndex + exactText.length
- for await (const match of matcher(scopeAsRange)) {
- const matchIter = createNodeIterator(root, NodeFilter.SHOW_TEXT);
-
- const matchStartNode = firstTextNodeInRange(match);
- const matchStartIndex =
- isTextNode(match.startContainer)
- ? seek(matchIter, matchStartNode) + match.startOffset
- : seek(matchIter, matchStartNode);
- const matchEndIndex = matchStartIndex + match.toString().length;
-
- // If the match is the same as the input range, continue.
- if (matchStartIndex === startIndex || matchEndIndex === endIndex) {
+ // Skip the found match if it is the actual target.
+ if (matchStartIndex === targetStartIndex)
continue;
- }
- // Determine how many prefix characters are shared.
- const prefixLength = overlapRight(
- text.substring(0, matchStartIndex),
- text.substring(0, startIndex),
+ // Count how many characters before & after them the false match and target have in common.
+ const sufficientPrefixLength = charactersNeededToBeUnique(
+ scopeText.substring(0, targetStartIndex),
+ scopeText.substring(0, matchStartIndex),
+ true,
);
-
- // Determine how many suffix characters are shared.
- const suffixLength = overlap(
- text.substring(matchEndIndex),
- text.substring(endIndex),
+ const sufficientSuffixLength = charactersNeededToBeUnique(
+ scopeText.substring(targetEndIndex),
+ scopeText.substring(matchEndIndex),
+ false,
);
-
- // Record the affix lengths that would have precluded this match.
- affixLengthPairs.push([prefixLength + 1, suffixLength + 1]);
+ affixLengthPairs.push([sufficientPrefixLength, sufficientSuffixLength]);
}
- // Construct and return an unambiguous selector.
- let prefix, suffix;
- if (affixLengthPairs.length) {
- const [prefixLength, suffixLength] = minimalSolution(affixLengthPairs);
-
- if (prefixLength > 0 && startIndex > 0) {
- prefix = text.substring(startIndex - prefixLength, startIndex);
- }
-
- if (suffixLength > 0 && endIndex < text.length) {
- suffix = text.substring(endIndex, endIndex + suffixLength);
- }
- }
-
+ // Find the prefix and suffix that would invalidate all mismatches, using the minimal characters
+ // for prefix and suffix combined.
+ const [prefixLength, suffixLength] = minimalSolution(affixLengthPairs);
+ const prefix = scopeText.substring(targetStartIndex - prefixLength, targetStartIndex);
+ const suffix = scopeText.substring(targetEndIndex, targetEndIndex + suffixLength);
return { prefix, suffix };
}
-function overlap(text1: string, text2: string) {
- let count = 0;
-
- while (count < text1.length && count < text2.length) {
- const c1 = text1[count];
- const c2 = text2[count];
- if (c1 !== c2) break;
- count++;
- }
-
- return count;
-}
-
-function overlapRight(text1: string, text2: string) {
- let count = 0;
-
- 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++;
- }
-
- return count;
+function charactersNeededToBeUnique(target: string, impostor: string, reverse: boolean = false) {
+ // Count how many characters the two strings have in common.
+ let overlap = 0;
+ const charAt = (s: string, i: number) => reverse ? s[s.length - 1 - i] : s[overlap];
+ while (overlap < target.length && charAt(target, overlap) === charAt(impostor, overlap))
+ overlap++;
+ if (overlap === target.length)
+ return Infinity; // (no substring of target can make it distinguishable from its impostor)
+ else
+ return overlap + 1;
}
function minimalSolution(requirements: Array<[number, number]>): [number, number] {
+ // Ensure we try solutions with an empty prefix or suffix.
+ requirements.push([0, 0]);
+
// Build all the pairs and order them by their sums.
const pairs = requirements.flatMap(l => requirements.map<[number, number]>(r => [l[0], r[1]]));
pairs.sort((a, b) => a[0] + a[1] - (b[0] + b[1]));
@@ -164,6 +132,47 @@
return pairs[pairs.length - 1];
}
+// Get the index of the first character of range within the text of scope.
+function getRangeTextPosition(range: Range, scope: DomScope): number {
+ const scopeAsRange = rangeFromScope(scope);
+ const iter = document.createNodeIterator(
+ scopeAsRange.commonAncestorContainer,
+ NodeFilter.SHOW_TEXT,
+ {
+ acceptNode(node: Text) {
+ // Only reveal nodes within the range
+ return scopeAsRange.intersectsNode(node)
+ ? NodeFilter.FILTER_ACCEPT
+ : NodeFilter.FILTER_REJECT
+ },
+ },
+ );
+ const scopeOffset = isTextNode(scopeAsRange.startContainer) ? scopeAsRange.startOffset : 0;
+ if (isTextNode(range.startContainer))
+ return seek(iter, range.startContainer) + range.startOffset - scopeOffset;
+ else
+ return seek(iter, firstTextNodeInRange(range)) - scopeOffset;
+}
+
+function firstTextNodeInRange(range: Range): Text {
+ // Find the first text node inside the range.
+ const iter = document.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;
+}
+
function isTextNode(node: Node): node is Text {
- return node.nodeType === Node.TEXT_NODE
+ return node.nodeType === Node.TEXT_NODE;
}
diff --git a/packages/dom/src/text-quote/index.ts b/packages/dom/src/text-quote/index.ts
index 9f77e75..bb73732 100644
--- a/packages/dom/src/text-quote/index.ts
+++ b/packages/dom/src/text-quote/index.ts
@@ -18,5 +18,5 @@
* under the License.
*/
- export * from './describe';
+export * from './describe';
export * from './match';
diff --git a/packages/dom/src/text-quote/match.ts b/packages/dom/src/text-quote/match.ts
index 2657a6f..18b077e 100644
--- a/packages/dom/src/text-quote/match.ts
+++ b/packages/dom/src/text-quote/match.ts
@@ -18,7 +18,6 @@
* under the License.
*/
-import createNodeIterator from 'dom-node-iterator';
import seek from 'dom-seek';
import { TextQuoteSelector } from '../../../selector/src';
@@ -28,86 +27,62 @@
export function createTextQuoteSelectorMatcher(selector: TextQuoteSelector): DomMatcher {
return async function* matchAll(scope: DomScope) {
const document = ownerDocument(scope);
- const range = rangeFromScope(scope);
- const root = range.commonAncestorContainer;
- const text = range.toString();
+ const scopeAsRange = rangeFromScope(scope);
+ const scopeText = scopeAsRange.toString();
const exact = selector.exact;
const prefix = selector.prefix || '';
const suffix = selector.suffix || '';
- const pattern = prefix + exact + suffix;
+ const searchPattern = prefix + exact + suffix;
- const iter = createNodeIterator(root, NodeFilter.SHOW_TEXT);
+ const iter = document.createNodeIterator(
+ scopeAsRange.commonAncestorContainer,
+ NodeFilter.SHOW_TEXT,
+ {
+ acceptNode(node: Text) {
+ // Only reveal nodes within the range; and skip any empty text nodes.
+ return scopeAsRange.intersectsNode(node) && node.length > 0
+ ? NodeFilter.FILTER_ACCEPT
+ : NodeFilter.FILTER_REJECT
+ },
+ },
+ );
+
+ // The index of the first character of iter.referenceNode inside the text.
+ let referenceNodeIndex = isTextNode(scopeAsRange.startContainer)
+ ? -scopeAsRange.startOffset
+ : 0;
let fromIndex = 0;
- let referenceNodeIndex = 0;
-
- if (isTextNode(range.startContainer)) {
- referenceNodeIndex -= range.startOffset;
- }
-
- while (fromIndex < text.length) {
- const patternStartIndex = text.indexOf(pattern, fromIndex);
+ while (fromIndex <= scopeText.length) {
+ // Find the quote with its prefix and suffix in the string.
+ const patternStartIndex = scopeText.indexOf(searchPattern, fromIndex);
if (patternStartIndex === -1) return;
- const match = document.createRange();
-
+ // Correct for the prefix and suffix lengths.
const matchStartIndex = patternStartIndex + prefix.length;
const matchEndIndex = matchStartIndex + exact.length;
- // Seek to the start of the match.
+ // Create a range to represent this exact quote in the dom.
+ const match = document.createRange();
+
+ // Seek to the start of the match, make the range start there.
referenceNodeIndex += seek(iter, matchStartIndex - referenceNodeIndex);
-
- // Normalize the reference to the start of the match.
- if (!iter.pointerBeforeReferenceNode) {
- // Peek forward and skip over any empty nodes.
- if (iter.nextNode()) {
- while ((iter.referenceNode.nodeValue as String).length === 0) {
- iter.nextNode();
- }
-
- // The iterator now points to the end of the reference node.
- // Move the iterator back to the start of the reference node.
- iter.previousNode();
- }
- }
-
- // Record the start container and offset.
match.setStart(iter.referenceNode, matchStartIndex - referenceNodeIndex);
- // Seek to the end of the match.
+ // Seek to the end of the match, make the range end there.
referenceNodeIndex += seek(iter, matchEndIndex - referenceNodeIndex);
-
- // Normalize the reference to the end of the match.
- if (!iter.pointerBeforeReferenceNode) {
- // Peek forward and skip over any empty nodes.
- if (iter.nextNode()) {
- while ((iter.referenceNode.nodeValue as String).length === 0) {
- iter.nextNode();
- }
-
- // The iterator now points to the end of the reference node.
- // Move the iterator back to the start of the reference node.
- iter.previousNode();
- }
-
- // Maybe seek backwards to the start of the node.
- referenceNodeIndex += seek(iter, iter.referenceNode);
- }
-
- // Record the end container and offset.
match.setEnd(iter.referenceNode, matchEndIndex - referenceNodeIndex);
// Yield the match.
yield match;
- // Advance the search forward.
+ // Advance the search forward to detect multiple occurrences.
fromIndex = matchStartIndex + 1;
- referenceNodeIndex += seek(iter, fromIndex - referenceNodeIndex);
}
};
}
function isTextNode(node: Node): node is Text {
- return node.nodeType === Node.TEXT_NODE
+ return node.nodeType === Node.TEXT_NODE;
}
diff --git a/packages/dom/test/cartesian.ts b/packages/dom/test/range/cartesian.test.ts
similarity index 96%
rename from packages/dom/test/cartesian.ts
rename to packages/dom/test/range/cartesian.test.ts
index 9ff47eb..5fd854b 100644
--- a/packages/dom/test/cartesian.ts
+++ b/packages/dom/test/range/cartesian.test.ts
@@ -19,7 +19,7 @@
*/
import { assert } from 'chai';
-import { product } from '../src/cartesian';
+import { product } from '../../src/range/cartesian';
async function* gen1() {
yield 1;
diff --git a/packages/dom/test/text-quote/describe-cases.ts b/packages/dom/test/text-quote/describe-cases.ts
new file mode 100644
index 0000000..129804d
--- /dev/null
+++ b/packages/dom/test/text-quote/describe-cases.ts
@@ -0,0 +1,138 @@
+/**
+ * @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 { TextQuoteSelector } from "../../../selector/src";
+import { RangeInfo } from "./utils";
+
+const testCases: {
+ [name: string]: {
+ html: string,
+ range: RangeInfo,
+ expected: TextQuoteSelector,
+ }
+} = {
+ 'simple': {
+ html: '<b>lorem ipsum dolor amet yada yada</b>',
+ range: {
+ startContainerXPath: '//b/text()',
+ startOffset: 12,
+ endContainerXPath: '//b/text()',
+ endOffset: 20,
+ },
+ expected: {
+ type: 'TextQuoteSelector',
+ exact: 'dolor am',
+ prefix: '',
+ suffix: '',
+ },
+ },
+ 'minimal prefix': {
+ html: '<b>To annotate or not to annotate.</b>',
+ range: {
+ startContainerXPath: '//b/text()',
+ startOffset: 22,
+ endContainerXPath: '//b/text()',
+ endOffset: 26,
+ },
+ expected: {
+ type: 'TextQuoteSelector',
+ exact: 'anno',
+ prefix: 'to ',
+ suffix: '',
+ },
+ },
+ 'minimal suffix': {
+ html: '<b>To annotate or not to annotate.</b>',
+ range: {
+ startContainerXPath: '//b/text()',
+ startOffset: 7,
+ endContainerXPath: '//b/text()',
+ endOffset: 11,
+ },
+ expected: {
+ type: 'TextQuoteSelector',
+ exact: 'tate',
+ prefix: '',
+ suffix: ' ',
+ },
+ },
+ 'use suffix for start of text': {
+ html: '<b>to annotate or not to annotate.</b>',
+ range: {
+ startContainerXPath: '//b/text()',
+ startOffset: 0,
+ endContainerXPath: '//b/text()',
+ endOffset: 2,
+ },
+ expected: {
+ type: 'TextQuoteSelector',
+ exact: 'to',
+ prefix: '',
+ suffix: ' annotate ',
+ },
+ },
+ 'use prefix for end of text': {
+ html: '<b>To annotate or not to annotate</b>',
+ range: {
+ startContainerXPath: '//b/text()',
+ startOffset: 26,
+ endContainerXPath: '//b/text()',
+ endOffset: 30,
+ },
+ expected: {
+ type: 'TextQuoteSelector',
+ exact: 'tate',
+ prefix: 'to anno',
+ suffix: '',
+ },
+ },
+ 'empty quote': {
+ html: '<b>To annotate or not to annotate</b>',
+ range: {
+ startContainerXPath: '//b/text()',
+ startOffset: 11,
+ endContainerXPath: '//b/text()',
+ endOffset: 11,
+ },
+ expected: {
+ type: 'TextQuoteSelector',
+ exact: '',
+ prefix: 'e',
+ suffix: ' ',
+ },
+ },
+ 'across elements': {
+ html: '<b>To annotate or <i>not</i> to <u>anno</u>tate</b>',
+ range: {
+ startContainerXPath: '//u/text()',
+ startOffset: 0,
+ endContainerXPath: '//b/text()[3]',
+ endOffset: 2,
+ },
+ expected: {
+ type: 'TextQuoteSelector',
+ exact: 'annota',
+ prefix: 'to ',
+ suffix: '',
+ },
+ },
+};
+
+export default testCases;
diff --git a/packages/dom/test/text-quote/describe.test.ts b/packages/dom/test/text-quote/describe.test.ts
new file mode 100644
index 0000000..f962157
--- /dev/null
+++ b/packages/dom/test/text-quote/describe.test.ts
@@ -0,0 +1,95 @@
+/**
+ * @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 { assert } from 'chai';
+import { describeTextQuote } from '../../src/text-quote/describe';
+import testCases from './describe-cases';
+import testMatchCases from './match-cases';
+import { hydrateRange, evaluateXPath } from './utils';
+
+const domParser = new window.DOMParser();
+
+describe('describeTextQuote', () => {
+ for (const [name, { html, range, expected }] of Object.entries(testCases)) {
+ it(`works for case: ${name}`, async () => {
+ const doc = domParser.parseFromString(html, 'text/html');
+ const result = await describeTextQuote(hydrateRange(range, doc), doc);
+ assert.deepEqual(result, expected);
+ })
+ }
+
+ it('works with custom scope', async () => {
+ const { html, range } = testCases['minimal prefix'];
+ const doc = domParser.parseFromString(html, 'text/html');
+ const scope = document.createRange();
+ scope.setStart(evaluateXPath(doc, '//b/text()'), 15);
+ scope.setEnd(evaluateXPath(doc, '//b/text()'), 30); // "not to annotate"
+ const result = await describeTextQuote(hydrateRange(range, doc), scope);
+ assert.deepEqual(result, {
+ type: 'TextQuoteSelector',
+ exact: 'anno',
+ prefix: '', // no prefix needed in this scope.
+ suffix: '',
+ });
+ });
+
+ it('strips part of the range outside the scope', async () => {
+ const { html, range } = testCases['simple'];
+ const doc = domParser.parseFromString(html, 'text/html');
+ const scope = document.createRange();
+ scope.setStart(evaluateXPath(doc, '//b/text()'), 6);
+ scope.setEnd(evaluateXPath(doc, '//b/text()'), 17); // "ipsum dolor"
+ const result = await describeTextQuote(hydrateRange(range, doc), scope);
+ assert.deepEqual(result, {
+ type: 'TextQuoteSelector',
+ exact: 'dolor',
+ prefix: '',
+ suffix: '',
+ });
+ });
+
+ it('works if the range equals the scope', async () => {
+ const { html, range, expected } = testCases['simple'];
+ const doc = domParser.parseFromString(html, 'text/html');
+ const result = await describeTextQuote(hydrateRange(range, doc), hydrateRange(range, doc));
+ assert.deepEqual(result, expected);
+ });
+
+ describe('inverts test cases of text quote matcher', () => {
+ const applicableTestCases = Object.entries(testMatchCases)
+ .filter(([_, { expected }]) => expected.length > 0);
+
+ for (const [name, { html, selector, expected }] of applicableTestCases) {
+ it(`case: '${name}'`, async () => {
+ const doc = domParser.parseFromString(html, 'text/html');
+ for (const rangeInfo of expected) {
+ const range = hydrateRange(rangeInfo, doc);
+ const result = await describeTextQuote(range, doc);
+ assert.equal(result.exact, selector.exact);
+ // Our result may have a different combination of prefix/suffix; only check for obvious inconsistency.
+ if (selector.prefix && result.prefix)
+ assert(selector.prefix.endsWith(result.prefix.substring(result.prefix.length - selector.prefix.length)), 'Inconsistent prefixes');
+ if (selector.suffix && result.suffix)
+ assert(selector.suffix.startsWith(result.suffix.substring(0, selector.suffix.length)), 'Inconsistent suffixes');
+ }
+ });
+ }
+ });
+});
diff --git a/packages/dom/test/text-quote/match-cases.ts b/packages/dom/test/text-quote/match-cases.ts
new file mode 100644
index 0000000..1748d1d
--- /dev/null
+++ b/packages/dom/test/text-quote/match-cases.ts
@@ -0,0 +1,355 @@
+/**
+ * @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 { TextQuoteSelector } from "../../../selector/src";
+import { RangeInfo } from "./utils";
+
+const testCases: {
+ [name: string]: {
+ html: string,
+ selector: TextQuoteSelector,
+ expected: RangeInfo[],
+ }
+} = {
+ 'simple': {
+ html: '<b>lorem ipsum dolor amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: 'dolor am',
+ },
+ expected: [
+ {
+ startContainerXPath: '//b/text()',
+ startOffset: 12,
+ endContainerXPath: '//b/text()',
+ endOffset: 20,
+ },
+ ],
+ },
+ 'first characters': {
+ html: '<b>lorem ipsum dolor amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: 'lorem ipsum',
+ },
+ expected: [
+ {
+ startContainerXPath: '//b/text()',
+ startOffset: 0,
+ endContainerXPath: '//b/text()',
+ endOffset: 11,
+ },
+ ],
+ },
+ 'last characters': {
+ html: '<b>lorem ipsum dolor amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: 'yada yada',
+ },
+ expected: [
+ {
+ startContainerXPath: '//b/text()',
+ startOffset: 23,
+ endContainerXPath: '//b/text()',
+ endOffset: 32,
+ },
+ ],
+ },
+ 'across elements': {
+ html: '<b>lorem <i>ipsum</i> dolor <u>amet</u> yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: 'dolor am',
+ },
+ expected: [
+ {
+ startContainerXPath: '//b/text()[2]',
+ startOffset: 1,
+ endContainerXPath: '//u/text()',
+ endOffset: 2,
+ },
+ ],
+ },
+ 'exact element contents': {
+ html: '<b>lorem <i>ipsum dolor</i> amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: 'ipsum dolor',
+ },
+ expected: [
+ {
+ startContainerXPath: '//i/text()',
+ startOffset: 0,
+ endContainerXPath: '//b/text()[2]',
+ endOffset: 0,
+ },
+ ],
+ },
+ 'text inside <head>': {
+ html: '<head><title>The title</title></head><b>lorem ipsum dolor amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: 'title',
+ },
+ expected: [
+ {
+ startContainerXPath: '//title/text()',
+ startOffset: 4,
+ endContainerXPath: '//b/text()[1]',
+ endOffset: 0,
+ },
+ ],
+ },
+ 'two matches': {
+ html: '<b>lorem ipsum dolor amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: 'yada',
+ },
+ expected: [
+ {
+ startContainerXPath: '//b/text()',
+ startOffset: 23,
+ endContainerXPath: '//b/text()',
+ endOffset: 27,
+ },
+ {
+ startContainerXPath: '//b/text()',
+ startOffset: 28,
+ endContainerXPath: '//b/text()',
+ endOffset: 32,
+ },
+ ],
+ },
+ 'overlapping matches': {
+ html: '<b>bananas</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: 'ana',
+ },
+ expected: [
+ {
+ startContainerXPath: '//b/text()',
+ startOffset: 1,
+ endContainerXPath: '//b/text()',
+ endOffset: 4,
+ },
+ {
+ startContainerXPath: '//b/text()',
+ startOffset: 3,
+ endContainerXPath: '//b/text()',
+ endOffset: 6,
+ },
+ ],
+ },
+ 'no matches': {
+ html: '<b>lorem ipsum dolor amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: 'holy grail',
+ },
+ expected: [],
+ },
+ 'with prefix': {
+ html: '<b>lorem ipsum dolor amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: 'yada',
+ prefix: 't ',
+ },
+ expected: [
+ {
+ startContainerXPath: '//b/text()',
+ startOffset: 23,
+ endContainerXPath: '//b/text()',
+ endOffset: 27,
+ },
+ ],
+ },
+ 'with suffix': {
+ html: '<b>lorem ipsum dolor amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: 'o',
+ suffix: 'l',
+ },
+ expected: [
+ {
+ startContainerXPath: '//b/text()',
+ startOffset: 13,
+ endContainerXPath: '//b/text()',
+ endOffset: 14,
+ },
+ ],
+ },
+ 'with prefix and suffix': {
+ html: '<b>lorem ipsum dolor amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: 'o',
+ prefix: 'l',
+ suffix: 're',
+ },
+ expected: [
+ {
+ startContainerXPath: '//b/text()',
+ startOffset: 1,
+ endContainerXPath: '//b/text()',
+ endOffset: 2,
+ },
+ ],
+ },
+ 'with prefix and suffix, two matches': {
+ html: '<b>lorem ipsum dolor amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: 'o',
+ prefix: 'l',
+ suffix: 'r',
+ },
+ expected: [
+ {
+ startContainerXPath: '//b/text()',
+ startOffset: 1,
+ endContainerXPath: '//b/text()',
+ endOffset: 2,
+ },
+ {
+ startContainerXPath: '//b/text()',
+ startOffset: 15,
+ endContainerXPath: '//b/text()',
+ endOffset: 16,
+ },
+ ],
+ },
+ 'with prefix, no matches': {
+ html: '<b>lorem ipsum dolor amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: 'dolor',
+ prefix: 'oopsum ',
+ },
+ expected: [],
+ },
+ 'with suffix, no matches': {
+ html: '<b>lorem ipsum dolor amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: 'dolor',
+ suffix: ' amot',
+ },
+ expected: [],
+ },
+ 'with suffix, no matches due to whitespace': {
+ html: '<b>lorem ipsum dolor amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: 'dolor',
+ suffix: 'a',
+ },
+ expected: [],
+ },
+ 'with empty prefix and suffix': {
+ html: '<b>lorem ipsum dolor amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: 'dolor am',
+ prefix: '',
+ suffix: '',
+ },
+ expected: [
+ {
+ startContainerXPath: '//b/text()',
+ startOffset: 12,
+ endContainerXPath: '//b/text()',
+ endOffset: 20,
+ },
+ ],
+ },
+ 'empty quote': {
+ html: '<b>lorem</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: '',
+ },
+ // A five character string contains six spots to find an empty string
+ expected: Array(6).fill(null).map((_, i) => ({
+ startContainerXPath: '//b/text()',
+ startOffset: i,
+ endContainerXPath: '//b/text()',
+ endOffset: i,
+ }))
+ },
+ 'empty quote, with prefix': {
+ html: '<b>lorem ipsum dolor amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: '',
+ prefix: 'dolor',
+ },
+ expected: [{
+ startContainerXPath: '//b/text()',
+ startOffset: 17,
+ endContainerXPath: '//b/text()',
+ endOffset: 17,
+ }]
+ },
+ 'empty quote, with suffix': {
+ html: '<b>lorem ipsum dolor amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: '',
+ suffix: 'i',
+ },
+ expected: [{
+ startContainerXPath: '//b/text()',
+ startOffset: 6,
+ endContainerXPath: '//b/text()',
+ endOffset: 6,
+ }]
+ },
+ 'empty quote, with prefix and suffix': {
+ html: '<b>lorem ipsum dolor amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: '',
+ prefix: 'lorem ',
+ suffix: 'ipsum',
+ },
+ expected: [{
+ startContainerXPath: '//b/text()',
+ startOffset: 6,
+ endContainerXPath: '//b/text()',
+ endOffset: 6,
+ }]
+ },
+ 'empty quote, no matches': {
+ html: '<b>lorem ipsum dolor amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: '',
+ prefix: 'X',
+ },
+ expected: [],
+ }
+};
+
+export default testCases;
diff --git a/packages/dom/test/text-quote/match.test.ts b/packages/dom/test/text-quote/match.test.ts
new file mode 100644
index 0000000..3148a3f
--- /dev/null
+++ b/packages/dom/test/text-quote/match.test.ts
@@ -0,0 +1,214 @@
+/**
+ * @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 { assert } from 'chai';
+import { createTextQuoteSelectorMatcher } from '../../src/text-quote/match';
+import { TextQuoteSelector } from '../../../selector/src/types';
+import { DomScope } from '../../src/types';
+import testCases from './match-cases';
+import { evaluateXPath, RangeInfo } from './utils';
+
+const domParser = new window.DOMParser();
+
+describe('createTextQuoteSelectorMatcher', () => {
+ for (const [name, { html, selector, expected }] of Object.entries(testCases)) {
+ it(`works for case: '${name}'`, async () => {
+ const doc = domParser.parseFromString(html, 'text/html');
+ await testMatcher(doc, doc, selector, expected);
+ });
+ }
+
+ it('handles adjacent text nodes', async () => {
+ const { html, selector } = testCases['simple'];
+ const doc = domParser.parseFromString(html, 'text/html');
+ const textNode = evaluateXPath(doc, '//b/text()') as Text;
+
+ for (let index = textNode.length - 1; index > 0; index--)
+ textNode.splitText(index);
+ // console.log([...textNode.parentNode.childNodes].map(node => node.textContent))
+ // → 'l', 'o', 'r', 'e', 'm', …
+
+ await testMatcher(doc, doc, selector, [
+ {
+ startContainerXPath: '//b/text()[13]',
+ startOffset: 0,
+ endContainerXPath: '//b/text()[21]',
+ endOffset: 0,
+ },
+ ]);
+ });
+
+ it('handles empty text nodes', async () => {
+ const { html, selector } = testCases['simple'];
+ const doc = domParser.parseFromString(html, 'text/html');
+
+ const textNode = evaluateXPath(doc, '//b/text()') as Text;
+ textNode.splitText(textNode.length);
+ textNode.splitText(20);
+ textNode.splitText(20);
+ textNode.splitText(17);
+ textNode.splitText(17);
+ textNode.splitText(12);
+ textNode.splitText(12);
+ textNode.splitText(0);
+ // console.log([...textNode.parentNode.childNodes].map(node => node.textContent))
+ // → '', 'lorem ipsum ', '', 'dolor', '', ' am', '', 'et yada yada', ''
+
+ await testMatcher(doc, doc, selector, [
+ {
+ startContainerXPath: '//b/text()[4]', // "dolor"
+ startOffset: 0,
+ endContainerXPath: '//b/text()[8]', // "et yada yada"
+ endOffset: 0,
+ },
+ ]);
+ });
+
+ it('works with parent of text as scope', async () => {
+ const { html, selector, expected } = testCases['simple'];
+ const doc = domParser.parseFromString(html, 'text/html');
+
+ await testMatcher(doc, evaluateXPath(doc, '//b'), selector, expected);
+ });
+
+ it('works with parent of text as scope, when matching its first characters', async () => {
+ const { html, selector, expected } = testCases['first characters'];
+ const doc = domParser.parseFromString(html, 'text/html');
+
+ await testMatcher(doc, evaluateXPath(doc, '//b'), selector, expected);
+ });
+
+ it('works with parent of text as scope, when matching its first characters, with an empty text node', async () => {
+ const { html, selector } = testCases['first characters'];
+ const doc = domParser.parseFromString(html, 'text/html');
+
+ const textNode = evaluateXPath(doc, '//b/text()') as Text;
+ textNode.splitText(0);
+
+ await testMatcher(doc, evaluateXPath(doc, '//b'), selector, [
+ {
+ startContainerXPath: '//b/text()[2]',
+ startOffset: 0,
+ endContainerXPath: '//b/text()[2]',
+ endOffset: 11,
+ },
+ ]);
+ });
+
+ it('works when scope is a Range within one text node', async () => {
+ const { html, selector, expected } = testCases['simple'];
+ const doc = domParser.parseFromString(html, 'text/html');
+
+ // Use the substring ‘ipsum dolor amet’ as scope.
+ const scope = document.createRange();
+ scope.setStart(evaluateXPath(doc, '//b/text()'), 6);
+ scope.setEnd(evaluateXPath(doc, '//b/text()'), 22);
+ await testMatcher(doc, scope, selector, expected);
+ });
+
+ it('works when scope is a Range with both ends inside text nodes', async () => {
+ const { html, selector, expected } = testCases['across elements'];
+ const doc = domParser.parseFromString(html, 'text/html');
+
+ // Use the substring ‘sum dolor am’ as scope.
+ const scope = document.createRange();
+ scope.setStart(evaluateXPath(doc, '//i/text()'), 2);
+ scope.setEnd(evaluateXPath(doc, '//u/text()'), 2);
+ await testMatcher(doc, scope, selector, expected);
+ });
+
+ it('works when scope is a Range with both ends inside elements', async () => {
+ const { html, selector, expected } = testCases['across elements'];
+ const doc = domParser.parseFromString(html, 'text/html');
+
+ const scope = document.createRange();
+ scope.setStart(evaluateXPath(doc, '//b'), 1); // before the <i>
+ scope.setEnd(evaluateXPath(doc, '//b'), 4); // before the " yada yada"
+ await testMatcher(doc, scope, selector, expected);
+ });
+
+ it('ignores quote when scope is an empty range', async () => {
+ const { html, selector } = testCases['simple'];
+ const doc = domParser.parseFromString(html, 'text/html');
+
+ const scope = document.createRange();
+ await testMatcher(doc, scope, selector, []);
+ });
+
+ it('ignores quote extending just beyond scope', async () => {
+ const { html, selector } = testCases['simple'];
+ const doc = domParser.parseFromString(html, 'text/html');
+
+ const scope = document.createRange();
+ scope.setStart(evaluateXPath(doc, '//b/text()'), 0);
+ scope.setEnd(evaluateXPath(doc, '//b/text()'), 19);
+ await testMatcher(doc, scope, selector, []);
+ });
+
+ it('ignores quote starting just before scope', async () => {
+ const { html, selector } = testCases['simple'];
+ const doc = domParser.parseFromString(html, 'text/html');
+
+ const scope = document.createRange();
+ scope.setStart(evaluateXPath(doc, '//b/text()'), 13);
+ scope.setEnd(evaluateXPath(doc, '//b/text()'), 32);
+ await testMatcher(doc, scope, selector, []);
+ });
+});
+
+async function testMatcher(
+ doc: Document,
+ scope: DomScope,
+ selector: TextQuoteSelector,
+ expected: RangeInfo[],
+) {
+ const matcher = createTextQuoteSelectorMatcher(selector);
+ const matches = [];
+ for await (const value of matcher(scope))
+ matches.push(value);
+ assert.equal(matches.length, expected.length);
+ matches.forEach((match, i) => {
+ const expectedRange = expected[i];
+ const expectedStartContainer = evaluateXPath(doc, expectedRange.startContainerXPath);
+ const expectedEndContainer = evaluateXPath(doc, expectedRange.endContainerXPath);
+ assert(match.startContainer === expectedStartContainer,
+ `unexpected start container: ${prettyNodeName(match.startContainer)}; `
+ + `expected ${prettyNodeName(expectedStartContainer)}`
+ );
+ assert.equal(match.startOffset, expectedRange.startOffset);
+ assert(match.endContainer === evaluateXPath(doc, expectedRange.endContainerXPath),
+ `unexpected end container: ${prettyNodeName(match.endContainer)}; `
+ + `expected ${prettyNodeName(expectedEndContainer)}`
+ );
+ assert.equal(match.endOffset, expectedRange.endOffset);
+ });
+}
+
+function prettyNodeName(node: Node) {
+ switch (node.nodeType) {
+ case Node.TEXT_NODE:
+ const text = (node as Text).nodeValue;
+ return `#text "${text.length > 50 ? text.substring(0, 50) + '…' : text}"`;
+ case Node.ELEMENT_NODE:
+ return `<${(node as Element).tagName.toLowerCase()}>`;
+ default:
+ return node.nodeName.toLowerCase();
+ }
+}
diff --git a/packages/dom/test/text-quote/utils.ts b/packages/dom/test/text-quote/utils.ts
new file mode 100644
index 0000000..a59fb06
--- /dev/null
+++ b/packages/dom/test/text-quote/utils.ts
@@ -0,0 +1,45 @@
+/**
+ * @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 { assert } from "chai";
+
+// RangeInfo serialises a Range’s start and end containers as XPaths.
+export type RangeInfo = {
+ startContainerXPath: string,
+ startOffset: number,
+ endContainerXPath: string,
+ endOffset: number,
+};
+
+export function evaluateXPath(doc: Document, xpath: string): Node {
+ const result = doc.evaluate(xpath, doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
+ const nodes = new Array(result.snapshotLength).fill(undefined).map((_, i) => result.snapshotItem(i));
+ assert.equal(nodes.length, 1,
+ `Test suite contains XPath with ${nodes.length} results instead of 1: '${xpath}'`
+ );
+ return nodes[0];
+}
+
+export function hydrateRange(rangeInfo: RangeInfo, doc: Document): Range {
+ const range = doc.createRange();
+ range.setStart(evaluateXPath(doc, rangeInfo.startContainerXPath), rangeInfo.startOffset);
+ range.setEnd(evaluateXPath(doc, rangeInfo.endContainerXPath), rangeInfo.endOffset);
+ return range;
+}
diff --git a/test/data-model.ts b/test/data-model.test.ts
similarity index 100%
rename from test/data-model.ts
rename to test/data-model.test.ts
diff --git a/web/demo/index.js b/web/demo/index.js
index 9cf64d7..88b9a06 100644
--- a/web/demo/index.js
+++ b/web/demo/index.js
@@ -127,13 +127,7 @@
const range = selection.getRangeAt(0);
if (range.collapsed) return;
- const scope = document.createRange();
- scope.selectNodeContents(source);
-
- if (!scope.isPointInRange(range.startContainer, range.startOffset)) return;
- if (!scope.isPointInRange(range.endContainer, range.endOffset)) return;
-
- return describeTextQuote(range, scope);
+ return describeTextQuote(range, source);
}
async function onSelectionChange() {
diff --git a/web/webpack.config.js b/web/webpack.config.js
index 8da5b59..5dcbfda 100644
--- a/web/webpack.config.js
+++ b/web/webpack.config.js
@@ -30,7 +30,7 @@
demo: ['./demo/index.html', './demo/index.js'],
test: [
'./test/index.html',
- 'mocha-loader!multi-entry-loader?include=./packages/*/test/**/*.[jt]s!',
+ 'mocha-loader!multi-entry-loader?include=./packages/*/test/**/*.test.[jt]s!',
],
},
resolve: {
diff --git a/yarn.lock b/yarn.lock
index 2cb91e5..fcfea75 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2124,6 +2124,11 @@
jsonparse "^1.2.0"
through ">=2.2.7 <3"
+abab@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a"
+ integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==
+
abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
@@ -2137,11 +2142,24 @@
mime-types "~2.1.24"
negotiator "0.6.2"
+acorn-globals@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45"
+ integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==
+ dependencies:
+ acorn "^7.1.1"
+ acorn-walk "^7.1.1"
+
acorn-jsx@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384"
integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==
+acorn-walk@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e"
+ integrity sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==
+
acorn@^6.2.1:
version "6.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e"
@@ -2152,6 +2170,11 @@
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c"
integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==
+acorn@^7.1.1:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.2.0.tgz#17ea7e40d7c8640ff54a694c889c26f31704effe"
+ integrity sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==
+
agent-base@4, agent-base@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
@@ -2220,11 +2243,6 @@
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
-ancestors@0.0.3:
- version "0.0.3"
- resolved "https://registry.yarnpkg.com/ancestors/-/ancestors-0.0.3.tgz#124eb944447d68b302057047d15d077a9da5179d"
- integrity sha1-Ek65RER9aLMCBXBH0V0Hep2lF50=
-
ansi-colors@3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813"
@@ -2721,6 +2739,11 @@
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
+browser-process-hrtime@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
+ integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==
+
browser-stdout@1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
@@ -3625,6 +3648,23 @@
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+cssom@^0.4.4:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
+ integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==
+
+cssom@~0.3.6:
+ version "0.3.8"
+ resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
+ integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
+
+cssstyle@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852"
+ integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==
+ dependencies:
+ cssom "~0.3.6"
+
currently-unhandled@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
@@ -3651,6 +3691,15 @@
dependencies:
assert-plus "^1.0.0"
+data-urls@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b"
+ integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==
+ dependencies:
+ abab "^2.0.3"
+ whatwg-mimetype "^2.3.0"
+ whatwg-url "^8.0.0"
+
date-fns@^1.27.2:
version "1.30.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
@@ -3712,6 +3761,11 @@
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+decimal.js@^10.2.0:
+ version "10.2.0"
+ resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.0.tgz#39466113a9e036111d02f82489b5fd6b0b5ed231"
+ integrity sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw==
+
decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
@@ -3922,24 +3976,23 @@
dependencies:
esutils "^2.0.2"
-dom-node-iterator@^3.5.3:
- version "3.5.3"
- resolved "https://registry.yarnpkg.com/dom-node-iterator/-/dom-node-iterator-3.5.3.tgz#32b68aa440962f1734487029f544a3db704637b7"
- integrity sha1-MraKpECWLxc0SHAp9USj23BGN7c=
-
-dom-seek@^4.0.3:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/dom-seek/-/dom-seek-4.0.3.tgz#f14dddf04b3fb062d901c7b00b0c142a06e0a94b"
- integrity sha1-8U3d8Es/sGLZAcewCwwUKgbgqUs=
- dependencies:
- ancestors "0.0.3"
- index-of "^0.2.0"
+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"
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
+domexception@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304"
+ integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==
+ dependencies:
+ webidl-conversions "^5.0.0"
+
dot-prop@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177"
@@ -4164,6 +4217,18 @@
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+escodegen@^1.14.1:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457"
+ integrity sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==
+ dependencies:
+ esprima "^4.0.1"
+ estraverse "^4.2.0"
+ esutils "^2.0.2"
+ optionator "^0.8.1"
+ optionalDependencies:
+ source-map "~0.6.1"
+
eslint-config-prettier@^6.9.0:
version "6.9.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.9.0.tgz#430d24822e82f7deb1e22a435bfa3999fae4ad64"
@@ -4327,7 +4392,7 @@
acorn-jsx "^5.1.0"
eslint-visitor-keys "^1.1.0"
-esprima@^4.0.0:
+esprima@^4.0.0, esprima@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
@@ -4346,7 +4411,7 @@
dependencies:
estraverse "^4.1.0"
-estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1:
+estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
@@ -5103,6 +5168,11 @@
once "^1.3.0"
path-is-absolute "^1.0.0"
+global-jsdom@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/global-jsdom/-/global-jsdom-6.1.0.tgz#a911ec57c51cf72e93a2ce97925a02a6427aed76"
+ integrity sha512-zaNNWr7hpov5SgF21fVtvnliRcRMYSZGc47nSipUOw5Ktft+2njD4hjzN1OXWQzlBFnRU/W+MBN6OvaPMTawKQ==
+
global-modules@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780"
@@ -5375,6 +5445,13 @@
readable-stream "^2.0.1"
wbuf "^1.1.0"
+html-encoding-sniffer@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3"
+ integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==
+ dependencies:
+ whatwg-encoding "^1.0.5"
+
html-entities@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
@@ -5604,11 +5681,6 @@
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
-index-of@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/index-of/-/index-of-0.2.0.tgz#38c1e2367ea55dffad3b6eb592ec1cc3090d7d65"
- integrity sha1-OMHiNn6lXf+tO261kuwcwwkNfWU=
-
indexes-of@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
@@ -5985,6 +6057,11 @@
dependencies:
isobject "^4.0.0"
+is-potential-custom-element-name@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397"
+ integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c=
+
is-promise@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
@@ -6184,6 +6261,38 @@
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
+jsdom@^16.2.2:
+ version "16.2.2"
+ resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.2.2.tgz#76f2f7541646beb46a938f5dc476b88705bedf2b"
+ integrity sha512-pDFQbcYtKBHxRaP55zGXCJWgFHkDAYbKcsXEK/3Icu9nKYZkutUXfLBwbD+09XDutkYSHcgfQLZ0qvpAAm9mvg==
+ dependencies:
+ abab "^2.0.3"
+ acorn "^7.1.1"
+ acorn-globals "^6.0.0"
+ cssom "^0.4.4"
+ cssstyle "^2.2.0"
+ data-urls "^2.0.0"
+ decimal.js "^10.2.0"
+ domexception "^2.0.1"
+ escodegen "^1.14.1"
+ html-encoding-sniffer "^2.0.1"
+ is-potential-custom-element-name "^1.0.0"
+ nwsapi "^2.2.0"
+ parse5 "5.1.1"
+ request "^2.88.2"
+ request-promise-native "^1.0.8"
+ saxes "^5.0.0"
+ symbol-tree "^3.2.4"
+ tough-cookie "^3.0.1"
+ w3c-hr-time "^1.0.2"
+ w3c-xmlserializer "^2.0.0"
+ webidl-conversions "^6.0.0"
+ whatwg-encoding "^1.0.5"
+ whatwg-mimetype "^2.3.0"
+ whatwg-url "^8.0.0"
+ ws "^7.2.3"
+ xml-name-validator "^3.0.0"
+
jsesc@^2.5.1:
version "2.5.2"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
@@ -7368,6 +7477,11 @@
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
+nwsapi@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
+ integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
+
nyc@^15.0.0:
version "15.0.0"
resolved "https://registry.yarnpkg.com/nyc/-/nyc-15.0.0.tgz#eb32db2c0f29242c2414fe46357f230121cfc162"
@@ -7546,7 +7660,7 @@
minimist "~0.0.1"
wordwrap "~0.0.2"
-optionator@^0.8.3:
+optionator@^0.8.1, optionator@^0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
@@ -7823,6 +7937,11 @@
parse-path "^4.0.0"
protocols "^1.4.0"
+parse5@5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
+ integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
+
parseurl@~1.3.2, parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
@@ -8203,6 +8322,11 @@
resolved "https://registry.yarnpkg.com/psl/-/psl-1.4.0.tgz#5dd26156cdb69fa1fdb8ab1991667d3f80ced7c2"
integrity sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==
+psl@^1.1.28:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
+ integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
+
public-encrypt@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"
@@ -8250,7 +8374,7 @@
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
-punycode@^2.1.0:
+punycode@^2.1.0, punycode@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
@@ -8622,6 +8746,22 @@
dependencies:
is-finite "^1.0.0"
+request-promise-core@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9"
+ integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==
+ dependencies:
+ lodash "^4.17.15"
+
+request-promise-native@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36"
+ integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==
+ dependencies:
+ request-promise-core "1.1.3"
+ stealthy-require "^1.1.1"
+ tough-cookie "^2.3.3"
+
request@^2.87.0:
version "2.88.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
@@ -8648,6 +8788,32 @@
tunnel-agent "^0.6.0"
uuid "^3.3.2"
+request@^2.88.2:
+ version "2.88.2"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
+ integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
+ dependencies:
+ aws-sign2 "~0.7.0"
+ aws4 "^1.8.0"
+ caseless "~0.12.0"
+ combined-stream "~1.0.6"
+ extend "~3.0.2"
+ forever-agent "~0.6.1"
+ form-data "~2.3.2"
+ har-validator "~5.1.3"
+ http-signature "~1.2.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.19"
+ oauth-sign "~0.9.0"
+ performance-now "^2.1.0"
+ qs "~6.5.2"
+ safe-buffer "^5.1.2"
+ tough-cookie "~2.5.0"
+ tunnel-agent "^0.6.0"
+ uuid "^3.3.2"
+
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -8852,6 +9018,13 @@
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+saxes@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d"
+ integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==
+ dependencies:
+ xmlchars "^2.2.0"
+
schema-utils@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
@@ -9320,6 +9493,11 @@
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
+stealthy-require@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
+ integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
+
stream-browserify@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b"
@@ -9579,6 +9757,11 @@
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
+symbol-tree@^3.2.4:
+ version "3.2.4"
+ resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
+ integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
+
table@^5.2.3:
version "5.4.6"
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
@@ -9767,6 +9950,23 @@
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
+tough-cookie@^2.3.3:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
+ integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
+ dependencies:
+ psl "^1.1.28"
+ punycode "^2.1.1"
+
+tough-cookie@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2"
+ integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==
+ dependencies:
+ ip-regex "^2.1.0"
+ psl "^1.1.28"
+ punycode "^2.1.1"
+
tough-cookie@~2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
@@ -9782,6 +9982,13 @@
dependencies:
punycode "^2.1.0"
+tr46@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.0.2.tgz#03273586def1595ae08fedb38d7733cee91d2479"
+ integrity sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==
+ dependencies:
+ punycode "^2.1.1"
+
trim-newlines@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
@@ -10085,9 +10292,23 @@
extsprintf "^1.2.0"
vm-browserify@^1.0.1:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019"
- integrity sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
+ integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
+
+w3c-hr-time@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
+ integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==
+ dependencies:
+ browser-process-hrtime "^1.0.0"
+
+w3c-xmlserializer@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a"
+ integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==
+ dependencies:
+ xml-name-validator "^3.0.0"
watchpack@^1.6.0:
version "1.6.0"
@@ -10121,6 +10342,16 @@
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
+webidl-conversions@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
+ integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==
+
+webidl-conversions@^6.0.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
+ integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
+
webpack-cli@^3.3.10:
version "3.3.10"
resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.10.tgz#17b279267e9b4fb549023fae170da8e6e766da13"
@@ -10247,6 +10478,18 @@
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==
+whatwg-encoding@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
+ integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==
+ dependencies:
+ iconv-lite "0.4.24"
+
+whatwg-mimetype@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
+ integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
+
whatwg-url@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd"
@@ -10256,6 +10499,15 @@
tr46 "^1.0.1"
webidl-conversions "^4.0.2"
+whatwg-url@^8.0.0:
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.1.0.tgz#c628acdcf45b82274ce7281ee31dd3c839791771"
+ integrity sha512-vEIkwNi9Hqt4TV9RdnaBPNt+E2Sgmo3gePebCRgZ1R7g6d23+53zCTnuB0amKI4AXq6VM8jj2DUAa0S1vjJxkw==
+ dependencies:
+ lodash.sortby "^4.7.0"
+ tr46 "^2.0.2"
+ webidl-conversions "^5.0.0"
+
which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
@@ -10415,6 +10667,21 @@
dependencies:
async-limiter "~1.0.0"
+ws@^7.2.3:
+ version "7.3.0"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd"
+ integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==
+
+xml-name-validator@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
+ integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
+
+xmlchars@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
+ integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
+
xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"