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/04/17 17:47:32 UTC
[incubator-annotator] 02/02: Convert code to typscript
This is an automated email from the ASF dual-hosted git repository.
gerben pushed a commit to branch typescript
in repository https://gitbox.apache.org/repos/asf/incubator-annotator.git
commit 18a7158f6c88375e0fa407c1443bdb85b22a02e6
Author: Gerben <ge...@treora.com>
AuthorDate: Fri Apr 17 18:38:30 2020 +0200
Convert code to typscript
Adopt the word ‘matcher’ to distinguish the selector functions from
selector objects. We could choose another word like ‘locator’ or
whatever if desired.
---
@types/cartesian/index.d.ts | 3 +
@types/dom-node-iterator/index.d.ts | 4 +
@types/dom-seek/index.d.ts | 3 +
babel.config.js | 2 +-
packages/dom/src/{cartesian.js => cartesian.ts} | 20 +++--
packages/dom/src/{css.js => css.ts} | 6 +-
.../src/{highlight-range.js => highlight-range.ts} | 38 +++++----
packages/dom/src/{index.js => index.ts} | 0
packages/dom/src/{range.js => range.ts} | 26 ++++---
packages/dom/src/{scope.js => scope.ts} | 13 ++--
packages/dom/src/{text-quote.js => text-quote.ts} | 90 +++++++++++++---------
packages/dom/src/types.ts | 5 ++
packages/dom/test/cartesian.js | 2 +-
packages/selector/src/{index.js => index.ts} | 30 ++++++--
packages/selector/src/types.ts | 25 ++++++
web/demo/index.js | 18 ++---
16 files changed, 186 insertions(+), 99 deletions(-)
diff --git a/@types/cartesian/index.d.ts b/@types/cartesian/index.d.ts
new file mode 100644
index 0000000..9fc47e3
--- /dev/null
+++ b/@types/cartesian/index.d.ts
@@ -0,0 +1,3 @@
+declare module 'cartesian' {
+ export default function cartesian<T>(list: Array<Array<T>> | { [k: string]: Array<T> }): Array<Array<T>>;
+}
diff --git a/@types/dom-node-iterator/index.d.ts b/@types/dom-node-iterator/index.d.ts
new file mode 100644
index 0000000..0e10887
--- /dev/null
+++ b/@types/dom-node-iterator/index.d.ts
@@ -0,0 +1,4 @@
+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
new file mode 100644
index 0000000..0ba0753
--- /dev/null
+++ b/@types/dom-seek/index.d.ts
@@ -0,0 +1,3 @@
+declare module 'dom-seek' {
+ export default function seek(iter: NodeIterator, where: number | Node): number;
+}
diff --git a/babel.config.js b/babel.config.js
index dcd45ea..0c052cc 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -36,7 +36,7 @@ module.exports = api => {
// Used for resolving source files during development.
let resolverOptions = {
alias: {
- '^@annotator/(.+)$': '@annotator/\\1/src/index.js',
+ '^@annotator/(.+)$': '@annotator/\\1/src/index.ts',
},
};
diff --git a/packages/dom/src/cartesian.js b/packages/dom/src/cartesian.ts
similarity index 80%
rename from packages/dom/src/cartesian.js
rename to packages/dom/src/cartesian.ts
index e800c2b..d2b44b7 100644
--- a/packages/dom/src/cartesian.js
+++ b/packages/dom/src/cartesian.ts
@@ -20,7 +20,7 @@
import cartesianArrays from 'cartesian';
-export async function* product(...iterables) {
+export async function* product<T>(...iterables: AsyncGenerator<T>[]): AsyncGenerator<Array<T>, void, undefined> {
// We listen to all iterators in parallel, while logging all the values they
// produce. Whenever an iterator produces a value, we produce and yield all
// combinations of that value with the logged values from other iterators.
@@ -28,28 +28,27 @@ export async function* product(...iterables) {
const iterators = iterables.map(iterable => iterable[Symbol.asyncIterator]());
// Initialise an empty log for each iterable.
- const logs = iterables.map(() => []);
+ const logs: T[][] = iterables.map(() => []);
const nextValuePromises = iterators.map((iterator, iterableNr) =>
iterator
.next()
- .then(async ({ value, done }) => ({ value: await value, done }))
.then(
// Label the result with iterableNr, to know which iterable produced
// this value after Promise.race below.
- ({ value, done }) => ({ value, done, iterableNr }),
+ nextResult => ({ nextResult, iterableNr }),
),
);
// Keep listening as long as any of the iterables is not yet exhausted.
while (nextValuePromises.some(p => p !== null)) {
// Wait until any of the active iterators has produced a new value.
- const { value, done, iterableNr } = await Promise.race(
+ const { nextResult, iterableNr } = await Promise.race(
nextValuePromises.filter(p => p !== null),
);
// If this iterable was exhausted, stop listening to it and move on.
- if (done) {
+ if (nextResult.done === true) {
nextValuePromises[iterableNr] = null;
continue;
}
@@ -57,17 +56,16 @@ export async function* product(...iterables) {
// Produce all combinations of the received value with the logged values
// from the other iterables.
const arrays = [...logs];
- arrays[iterableNr] = [value];
- const combinations = cartesianArrays(arrays);
+ arrays[iterableNr] = [nextResult.value];
+ const combinations: T[][] = cartesianArrays(arrays);
// Append the received value to the right log.
- logs[iterableNr] = [...logs[iterableNr], value];
+ logs[iterableNr] = [...logs[iterableNr], nextResult.value];
// Start listening for the next value of this iterable.
nextValuePromises[iterableNr] = iterators[iterableNr]
.next()
- .then(async ({ value, done }) => ({ value: await value, done }))
- .then(({ value, done }) => ({ value, done, iterableNr }));
+ .then(nextResult => ({ nextResult, iterableNr }));
// Yield each of the produced combinations separately.
yield* combinations;
diff --git a/packages/dom/src/css.js b/packages/dom/src/css.ts
similarity index 80%
rename from packages/dom/src/css.js
rename to packages/dom/src/css.ts
index 3a7c6c1..004158c 100644
--- a/packages/dom/src/css.js
+++ b/packages/dom/src/css.ts
@@ -18,8 +18,10 @@
* under the License.
*/
-export function createCssSelector(selector) {
- return async function* matchAll(scope) {
+import { CssSelector, Matcher } from "../../selector/src";
+
+export function createCssSelectorMatcher(selector: CssSelector): Matcher<Document, Element> {
+ return async function* matchAll(scope: Document) {
yield* scope.querySelectorAll(selector.value);
};
}
diff --git a/packages/dom/src/highlight-range.js b/packages/dom/src/highlight-range.ts
similarity index 84%
rename from packages/dom/src/highlight-range.js
rename to packages/dom/src/highlight-range.ts
index 57f76ee..d46cead 100644
--- a/packages/dom/src/highlight-range.js
+++ b/packages/dom/src/highlight-range.ts
@@ -28,14 +28,18 @@
// unusable afterwards
// - tagName: the element used to wrap text nodes. Defaults to 'mark'.
// - attributes: an Object defining any attributes to be set on the wrapper elements.
-export function highlightRange(range, tagName = 'mark', attributes = {}) {
+export function highlightRange(
+ range: Range,
+ tagName: string = 'mark',
+ attributes: Record<string, string> = {}
+): () => void {
if (range.collapsed) return;
// First put all nodes in an array (splits start and end nodes if needed)
const nodes = textNodesInRange(range);
// Highlight each node
- const highlightElements = [];
+ const highlightElements: HTMLElement[] = [];
for (const node of nodes) {
const highlightElement = wrapNodeInHighlight(node, tagName, attributes);
highlightElements.push(highlightElement);
@@ -52,10 +56,10 @@ export function highlightRange(range, tagName = 'mark', attributes = {}) {
}
// Return an array of the text nodes in the range. Split the start and end nodes if required.
-function textNodesInRange(range) {
+function textNodesInRange(range: Range): Text[] {
// If the start or end node is a text node and only partly in the range, split it.
if (
- range.startContainer.nodeType === Node.TEXT_NODE &&
+ isTextNode(range.startContainer) &&
range.startOffset > 0
) {
const endOffset = range.endOffset; // (this may get lost when the splitting the node)
@@ -67,7 +71,7 @@ function textNodesInRange(range) {
range.setStart(createdNode, 0);
}
if (
- range.endContainer.nodeType === Node.TEXT_NODE &&
+ isTextNode(range.endContainer) &&
range.endOffset < range.endContainer.length
) {
range.endContainer.splitText(range.endOffset);
@@ -77,10 +81,12 @@ function textNodesInRange(range) {
const walker = range.startContainer.ownerDocument.createTreeWalker(
range.commonAncestorContainer,
NodeFilter.SHOW_TEXT,
- node =>
- range.intersectsNode(node)
- ? NodeFilter.FILTER_ACCEPT
- : NodeFilter.FILTER_REJECT,
+ {
+ acceptNode: node =>
+ range.intersectsNode(node)
+ ? NodeFilter.FILTER_ACCEPT
+ : NodeFilter.FILTER_REJECT
+ },
);
walker.currentNode = range.startContainer;
@@ -98,16 +104,16 @@ function textNodesInRange(range) {
// }
// }
- const nodes = [];
- if (walker.currentNode.nodeType === Node.TEXT_NODE)
+ const nodes: Text[] = [];
+ if (isTextNode(walker.currentNode))
nodes.push(walker.currentNode);
while (walker.nextNode() && range.comparePoint(walker.currentNode, 0) !== 1)
- nodes.push(walker.currentNode);
+ nodes.push(walker.currentNode as Text);
return nodes;
}
// Replace [node] with <tagName ...attributes>[node]</tagName>
-function wrapNodeInHighlight(node, tagName, attributes) {
+function wrapNodeInHighlight(node: Node, tagName: string, attributes: Record<string, string>): HTMLElement {
const highlightElement = node.ownerDocument.createElement(tagName);
Object.keys(attributes).forEach(key => {
highlightElement.setAttribute(key, attributes[key]);
@@ -119,7 +125,7 @@ function wrapNodeInHighlight(node, tagName, attributes) {
}
// Remove a highlight element created with wrapNodeInHighlight.
-function removeHighlight(highlightElement) {
+function removeHighlight(highlightElement: HTMLElement) {
// If it has somehow been removed already, there is nothing to be done.
if (!highlightElement.parentNode) return;
if (highlightElement.childNodes.length === 1) {
@@ -138,3 +144,7 @@ function removeHighlight(highlightElement) {
highlightElement.remove();
}
}
+
+function isTextNode(node: Node): node is Text {
+ return node.nodeType === Node.TEXT_NODE
+}
diff --git a/packages/dom/src/index.js b/packages/dom/src/index.ts
similarity index 100%
rename from packages/dom/src/index.js
rename to packages/dom/src/index.ts
diff --git a/packages/dom/src/range.js b/packages/dom/src/range.ts
similarity index 63%
rename from packages/dom/src/range.js
rename to packages/dom/src/range.ts
index 51b1483..e465386 100644
--- a/packages/dom/src/range.js
+++ b/packages/dom/src/range.ts
@@ -18,19 +18,23 @@
* under the License.
*/
-import { ownerDocument } from './scope.js';
-import { product } from './cartesian.js';
-
-export function createRangeSelectorCreator(createSelector) {
- return function createRangeSelector(selector) {
- const startSelector = createSelector(selector.startSelector);
- const endSelector = createSelector(selector.endSelector);
-
- return async function* matchAll(scope) {
+import { ownerDocument } from './scope';
+import { product } from './cartesian';
+import { RangeSelector, Selector } from '../../selector/src/types';
+import { DomMatcher, DomScope } from './types';
+
+export function makeCreateRangeSelectorMatcher(
+ createMatcher: <T extends Selector>(selector: T) => DomMatcher
+): (selector: RangeSelector) => DomMatcher {
+ return function createRangeSelectorMatcher(selector: RangeSelector) {
+ const startMatcher = createMatcher(selector.startSelector);
+ const endMatcher = createMatcher(selector.endSelector);
+
+ return async function* matchAll(scope: DomScope) {
const document = ownerDocument(scope);
- const startMatches = startSelector(scope);
- const endMatches = endSelector(scope);
+ const startMatches = startMatcher(scope);
+ const endMatches = endMatcher(scope);
const pairs = product(startMatches, endMatches);
diff --git a/packages/dom/src/scope.js b/packages/dom/src/scope.ts
similarity index 81%
rename from packages/dom/src/scope.js
rename to packages/dom/src/scope.ts
index 007618d..2b112ff 100644
--- a/packages/dom/src/scope.js
+++ b/packages/dom/src/scope.ts
@@ -18,15 +18,16 @@
* under the License.
*/
-export function ownerDocument(scope) {
- if ('commonAncestorContainer' in scope) {
- return scope.commonAncestorContainer.ownerDocument;
- }
+import { DomScope } from './types';
- return scope.ownerDocument;
+export function ownerDocument(scope: DomScope): Document {
+ if ('commonAncestorContainer' in scope)
+ return scope.commonAncestorContainer.ownerDocument;
+ else
+ return scope.ownerDocument;
}
-export function rangeFromScope(scope) {
+export function rangeFromScope(scope: DomScope | null): Range {
if ('commonAncestorContainer' in scope) {
return scope;
}
diff --git a/packages/dom/src/text-quote.js b/packages/dom/src/text-quote.ts
similarity index 71%
rename from packages/dom/src/text-quote.js
rename to packages/dom/src/text-quote.ts
index 21a530e..01a3989 100644
--- a/packages/dom/src/text-quote.js
+++ b/packages/dom/src/text-quote.ts
@@ -21,26 +21,22 @@
import createNodeIterator from 'dom-node-iterator';
import seek from 'dom-seek';
-import { ownerDocument, rangeFromScope } from './scope.js';
+import { TextQuoteSelector } from '../../selector/src';
+import { DomScope, DomMatcher } from './types';
+import { ownerDocument, rangeFromScope } from './scope';
-// Node constants
-const TEXT_NODE = 3;
-
-// NodeFilter constants
-const SHOW_TEXT = 4;
-
-function firstTextNodeInRange(range) {
+function firstTextNodeInRange(range: Range): Text {
const { startContainer } = range;
- if (startContainer.nodeType === TEXT_NODE) return startContainer;
+ if (isTextNode(startContainer)) return startContainer;
const root = range.commonAncestorContainer;
- const iter = createNodeIterator(root, SHOW_TEXT);
- return iter.nextNode();
+ const iter = createNodeIterator(root, NodeFilter.SHOW_TEXT);
+ return iter.nextNode() as Text;
}
-export function createTextQuoteSelector(selector) {
- return async function* matchAll(scope) {
+export function createTextQuoteSelectorMatcher(selector: TextQuoteSelector): DomMatcher {
+ return async function* matchAll(scope: DomScope) {
const document = ownerDocument(scope);
const range = rangeFromScope(scope);
const root = range.commonAncestorContainer;
@@ -51,12 +47,12 @@ export function createTextQuoteSelector(selector) {
const suffix = selector.suffix || '';
const pattern = prefix + exact + suffix;
- const iter = createNodeIterator(root, SHOW_TEXT);
+ const iter = createNodeIterator(root, NodeFilter.SHOW_TEXT);
let fromIndex = 0;
let referenceNodeIndex = 0;
- if (range.startContainer.nodeType === TEXT_NODE) {
+ if (isTextNode(range.startContainer)) {
referenceNodeIndex -= range.startOffset;
}
@@ -122,32 +118,49 @@ export function createTextQuoteSelector(selector) {
};
}
-export async function describeTextQuote(range, scope = null) {
- scope = rangeFromScope(scope || ownerDocument(range).documentElement);
+export async function describeTextQuote(
+ range: Range,
+ scope: DomScope = null
+): Promise<TextQuoteSelector> {
+ const exact = range.toString();
+
+ const result: TextQuoteSelector = { type: 'TextQuoteSelector', exact };
- const root = scope.commonAncestorContainer;
- const text = scope.toString();
+ const { prefix, suffix } = await calculateContextForDisambiguation(range, result, scope);
+ result.prefix = prefix;
+ result.suffix = suffix;
- const exact = range.toString();
- const selector = createTextQuoteSelector({ exact });
+ return result
+}
- const iter = createNodeIterator(root, SHOW_TEXT);
+async function calculateContextForDisambiguation(
+ range: Range,
+ selector: TextQuoteSelector,
+ scope: DomScope
+): Promise<{ prefix?: string, suffix?: string }> {
+ const scopeAsRange = rangeFromScope(scope || ownerDocument(range).documentElement);
+ const root = scopeAsRange.commonAncestorContainer;
+ const text = scopeAsRange.toString();
+
+ const matcher = createTextQuoteSelectorMatcher(selector);
+
+ const iter = createNodeIterator(root, NodeFilter.SHOW_TEXT);
const startNode = firstTextNodeInRange(range);
const startIndex =
- range.startContainer.nodeType === TEXT_NODE
+ isTextNode(range.startContainer)
? seek(iter, startNode) + range.startOffset
: seek(iter, startNode);
- const endIndex = startIndex + exact.length;
+ const endIndex = startIndex + selector.exact.length;
- const affixLengthPairs = [];
+ const affixLengthPairs: Array<[number, number]> = [];
- for await (const match of selector(scope)) {
- const matchIter = createNodeIterator(root, SHOW_TEXT);
+ for await (const match of matcher(scopeAsRange)) {
+ const matchIter = createNodeIterator(root, NodeFilter.SHOW_TEXT);
const matchStartNode = firstTextNodeInRange(match);
const matchStartIndex =
- match.startContainer.nodeType === TEXT_NODE
+ isTextNode(match.startContainer)
? seek(matchIter, matchStartNode) + match.startOffset
: seek(matchIter, matchStartNode);
const matchEndIndex = matchStartIndex + match.toString().length;
@@ -174,24 +187,23 @@ export async function describeTextQuote(range, scope = null) {
}
// Construct and return an unambiguous selector.
- const result = { type: 'TextQuoteSelector', exact };
-
+ let prefix, suffix;
if (affixLengthPairs.length) {
const [prefixLength, suffixLength] = minimalSolution(affixLengthPairs);
if (prefixLength > 0 && startIndex > 0) {
- result.prefix = text.substring(startIndex - prefixLength, startIndex);
+ prefix = text.substring(startIndex - prefixLength, startIndex);
}
if (suffixLength > 0 && endIndex < text.length) {
- result.suffix = text.substring(endIndex, endIndex + suffixLength);
+ suffix = text.substring(endIndex, endIndex + suffixLength);
}
}
- return result;
+ return { prefix, suffix };
}
-function overlap(text1, text2) {
+function overlap(text1: string, text2: string) {
let count = 0;
while (count < text1.length && count < text2.length) {
@@ -204,7 +216,7 @@ function overlap(text1, text2) {
return count;
}
-function overlapRight(text1, text2) {
+function overlapRight(text1: string, text2: string) {
let count = 0;
while (count < text1.length && count < text2.length) {
@@ -217,9 +229,9 @@ function overlapRight(text1, text2) {
return count;
}
-function minimalSolution(requirements) {
+function minimalSolution(requirements: Array<[number, number]>): [number, number] {
// Build all the pairs and order them by their sums.
- const pairs = requirements.flatMap(l => requirements.map(r => [l[0], r[1]]));
+ 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]));
// Find the first pair that satisfies every requirement.
@@ -233,3 +245,7 @@ function minimalSolution(requirements) {
// Return the largest pairing (unreachable).
return pairs[pairs.length - 1];
}
+
+function isTextNode(node: Node): node is Text {
+ return node.nodeType === Node.TEXT_NODE
+}
diff --git a/packages/dom/src/types.ts b/packages/dom/src/types.ts
new file mode 100644
index 0000000..db6d018
--- /dev/null
+++ b/packages/dom/src/types.ts
@@ -0,0 +1,5 @@
+import { Matcher } from '../../selector/src';
+
+export type DomScope = Node | Range
+
+export type DomMatcher = Matcher<DomScope, Range>
diff --git a/packages/dom/test/cartesian.js b/packages/dom/test/cartesian.js
index a56f9c8..c5cfd23 100644
--- a/packages/dom/test/cartesian.js
+++ b/packages/dom/test/cartesian.js
@@ -18,7 +18,7 @@
* under the License.
*/
-import { product } from '../src/cartesian.js';
+import { product } from '../src/cartesian';
async function* gen1() {
yield 1;
diff --git a/packages/selector/src/index.js b/packages/selector/src/index.ts
similarity index 50%
rename from packages/selector/src/index.js
rename to packages/selector/src/index.ts
index c9c100d..8135f6e 100644
--- a/packages/selector/src/index.js
+++ b/packages/selector/src/index.ts
@@ -18,20 +18,36 @@
* under the License.
*/
-export function makeRefinable(selectorCreator) {
- return function createSelector(source) {
- const selector = selectorCreator(source);
+import { Selector, Matcher } from './types';
- if (source.refinedBy) {
- const refiningSelector = createSelector(source.refinedBy);
+export * from './types';
+
+export function makeRefinable<
+ // Any subtype of Selector can be made refinable; but note we limit the value
+ // of refinedBy because it must also be accepted by matcherCreator.
+ TSelector extends (Selector & { refinedBy: TSelector }),
+ TScope,
+ // To enable refinement, the implementation’s Match object must be usable as a
+ // Scope object itself.
+ TMatch extends TScope,
+>(
+ matcherCreator: (selector: TSelector) => Matcher<TScope, TMatch>,
+): (selector: TSelector) => Matcher<TScope, TMatch> {
+ return function createMatcherWithRefinement(
+ sourceSelector: TSelector
+ ): Matcher<TScope, TMatch> {
+ const matcher = matcherCreator(sourceSelector);
+
+ if (sourceSelector.refinedBy) {
+ const refiningSelector = createMatcherWithRefinement(sourceSelector.refinedBy);
return async function* matchAll(scope) {
- for await (const match of selector(scope)) {
+ for await (const match of matcher(scope)) {
yield* refiningSelector(match);
}
};
}
- return selector;
+ return matcher;
};
}
diff --git a/packages/selector/src/types.ts b/packages/selector/src/types.ts
new file mode 100644
index 0000000..4f620a8
--- /dev/null
+++ b/packages/selector/src/types.ts
@@ -0,0 +1,25 @@
+export interface Selector {
+ refinedBy?: Selector,
+}
+
+export interface CssSelector extends Selector {
+ type: 'CssSelector',
+ value: string,
+}
+
+export interface TextQuoteSelector extends Selector {
+ type: 'TextQuoteSelector',
+ exact: string,
+ prefix?: string,
+ suffix?: string,
+}
+
+export interface RangeSelector extends Selector {
+ type: 'RangeSelector',
+ startSelector: Selector,
+ endSelector: Selector,
+}
+
+export interface Matcher<TScope, TMatch> {
+ (scope: TScope): AsyncGenerator<TMatch, void, void>,
+}
diff --git a/web/demo/index.js b/web/demo/index.js
index 47f7f30..b9e2533 100644
--- a/web/demo/index.js
+++ b/web/demo/index.js
@@ -21,8 +21,8 @@
/* global info, module, source, target */
import {
- createRangeSelectorCreator,
- createTextQuoteSelector,
+ makeCreateRangeSelectorMatcher,
+ createTextQuoteSelectorMatcher,
describeTextQuote,
highlightRange,
} from '@annotator/dom';
@@ -91,21 +91,21 @@ function cleanup() {
target.normalize();
}
-const createSelector = makeRefinable(selector => {
- const selectorCreator = {
- TextQuoteSelector: createTextQuoteSelector,
- RangeSelector: createRangeSelectorCreator(createSelector),
+const createMatcher = makeRefinable(selector => {
+ const innerCreateMatcher = {
+ TextQuoteSelector: createTextQuoteSelectorMatcher,
+ RangeSelector: makeCreateRangeSelectorMatcher(createMatcher),
}[selector.type];
- if (selectorCreator == null) {
+ if (!innerCreateMatcher) {
throw new Error(`Unsupported selector type: ${selector.type}`);
}
- return selectorCreator(selector);
+ return innerCreateMatcher(selector);
});
async function anchor(selector) {
- const matchAll = createSelector(selector);
+ const matchAll = createMatcher(selector);
const ranges = [];
for await (const range of matchAll(target)) {