This is an automated email from the ASF dual-hosted git repository. gerben pushed a commit to branch simpler-matcher-creation in repository https://gitbox.apache.org/repos/asf/incubator-annotator.git
commit 92531b495cb28b6ad2f8ba8f09ca6e0324eaee57 Merge: d896d01 cc99947 Author: Gerben <[email protected]> AuthorDate: Sun Jun 20 17:02:53 2021 +0200 Merge branch 'master' into simpler-matcher-creation .eslintignore | 1 + .eslintrc.js | 82 +- .gitignore | 5 + .mocharc.js | 7 +- .travis.yml | 23 +- Makefile | 1 + README.md | 116 +- babel-register.js | 5 +- babel.config.js | 33 +- lerna.json | 8 +- package.json | 44 +- packages/apache-annotator/.npmignore | 5 + packages/apache-annotator/package.json | 28 + .../src/types.ts => apache-annotator/src/dom.ts} | 15 +- .../types.ts => apache-annotator/src/selector.ts} | 16 +- packages/{dom => apache-annotator}/tsconfig.json | 1 + packages/dom/.npmignore | 7 +- packages/dom/@types/optimal-select/index.d.ts | 9 + packages/dom/README.md | 3 - packages/dom/package.json | 23 +- packages/dom/src/css.ts | 83 +- packages/dom/src/highlight-range.ts | 37 +- packages/dom/src/index.ts | 1 + packages/dom/src/normalize-range.ts | 165 ++ packages/dom/src/{scope.ts => owner-document.ts} | 27 +- packages/dom/src/range/cartesian.ts | 113 +- packages/dom/src/range/match.ts | 105 +- packages/dom/src/text-node-chunker.ts | 170 ++ packages/dom/src/text-position/describe.ts | 72 + .../dom-seek.d.ts => text-position/index.ts} | 8 +- packages/dom/src/text-position/match.ts | 68 + packages/dom/src/text-quote/describe.ts | 221 +- packages/dom/src/text-quote/match.ts | 117 +- .../dom/src/{types/cartesian.d.ts => to-range.ts} | 28 +- packages/dom/test/css/describe.test.ts | 58 + packages/dom/test/css/match-cases.ts | 56 + packages/dom/test/css/match.test.ts | 59 + .../test/highlight-range/highlight-range.test.ts | 8 +- packages/dom/test/range/cartesian.test.ts | 66 +- packages/dom/test/text-position/describe.test.ts | 56 + packages/dom/test/text-position/match-cases.ts | 143 ++ .../{text-quote => text-position}/match.test.ts | 134 +- packages/dom/test/text-quote/describe-cases.ts | 316 ++- packages/dom/test/text-quote/describe.test.ts | 79 +- packages/dom/test/text-quote/match-cases.ts | 58 +- packages/dom/test/text-quote/match.test.ts | 50 +- packages/dom/tsconfig.json | 2 +- packages/selector/.npmignore | 7 +- packages/selector/README.md | 3 - packages/selector/package.json | 18 +- packages/selector/src/index.ts | 10 +- packages/selector/src/text/chunker.ts | 157 ++ packages/selector/src/text/code-point-seeker.ts | 196 ++ .../selector/src/text/describe-text-position.ts | 61 + packages/selector/src/text/describe-text-quote.ts | 298 +++ .../src/types.ts => selector/src/text/index.ts} | 10 +- packages/selector/src/text/match-text-position.ts | 76 + packages/selector/src/text/match-text-quote.ts | 208 ++ packages/selector/src/text/seeker.ts | 415 ++++ packages/selector/src/types.ts | 61 + test/data-model.test.ts | 5 +- tsconfig.base.json | 7 +- tsconfig.json | 15 +- tsconfig.test.json | 8 + typedoc.json | 5 + web/demo/index.html | 99 - web/index.html | 79 +- web/{demo => }/index.js | 59 +- web/test/index.html | 24 - web/webpack.config.js | 20 +- yarn.lock | 2328 ++++++++++---------- 71 files changed, 4846 insertions(+), 2055 deletions(-) diff --cc packages/dom/src/range/match.ts index 87f54b4,d6891c6..d0b6943 --- a/packages/dom/src/range/match.ts +++ b/packages/dom/src/range/match.ts @@@ -18,17 -18,86 +18,86 @@@ * under the License. */ - import type { RangeSelector, Selector, MatcherCreator, Plugin } from '@annotator/selector'; - - import { ownerDocument } from '../scope'; - import type { DomMatcher, DomScope } from '../types'; - - import { product } from './cartesian'; + import type { + Matcher, ++ Plugin, + RangeSelector, + Selector, ++ MatcherCreator, + } from '@apache-annotator/selector'; + import { ownerDocument } from '../owner-document'; + import { toRange } from '../to-range'; + import { cartesian } from './cartesian'; + /** + * Find the range(s) corresponding to the given {@link RangeSelector}. + * + * As a RangeSelector itself nests two further selectors, one needs to pass a + * `createMatcher` function that will be used to process those nested selectors. + * + * The function is curried, taking first the `createMatcher` function, then the + * selector, and then the scope. + * + * As there may be multiple matches for the start & end selectors, the resulting + * matcher will return an (async) iterable, that produces a match for each + * possible pair of matches of the nested selectors (except those where its end + * would precede its start). *(Note that this behaviour is a rather free + * interpretation of the Web Annotation Data Model spec, which is silent about + * the possibility of multiple matches for RangeSelectors)* + * + * @example + * By using a matcher for {@link TextQuoteSelector}s, one + * could create a matcher for text quotes with ellipsis to select a phrase + * “ipsum … amet,”: + * ``` + * const selector = { + * type: 'RangeSelector', + * startSelector: { + * type: 'TextQuoteSelector', + * exact: 'ipsum ', + * }, + * endSelector: { + * type: 'TextQuoteSelector', + * // Because the end of a RangeSelector is *exclusive*, we will present the + * // latter part of the quote as the *prefix* so it will be part of the + * // match. + * exact: '', + * prefix: ' amet,', + * } + * }; + * const createRangeSelectorMatcher = + * makeCreateRangeSelectorMatcher(createTextQuoteMatcher); + * const match = createRangeSelectorMatcher(selector)(document.body); + * console.log(match) + * // ⇒ Range { startContainer: #text, startOffset: 6, endContainer: #text, + * // endOffset: 27, … } + * ``` + * + * @example + * To support RangeSelectors that might themselves contain RangeSelectors, + * recursion can be created by supplying the resulting matcher creator function + * as the `createMatcher` parameter: + * ``` + * const createWhicheverMatcher = (selector) => { + * const innerCreateMatcher = { + * TextQuoteSelector: createTextQuoteSelectorMatcher, + * TextPositionSelector: createTextPositionSelectorMatcher, + * RangeSelector: makeCreateRangeSelectorMatcher(createWhicheverMatcher), + * }[selector.type]; + * return innerCreateMatcher(selector); + * }); + * ``` + * + * @param createMatcher - The function used to process nested selectors. + * @returns A function that, given a RangeSelector `selector`, creates a {@link + * Matcher} function that can apply it to a given `scope`. + * + * @public + */ export function makeCreateRangeSelectorMatcher( - createMatcher: <T extends Selector>(selector: T) => DomMatcher, - ): (selector: RangeSelector) => DomMatcher { - return function createRangeSelectorMatcher(selector: RangeSelector) { - createMatcher: <T extends Selector, TMatch extends Node | Range>( - selector: T, - ) => Matcher<Node | Range, TMatch>, ++ createMatcher: MatcherCreator<Node | Range, Node | Range>, + ): (selector: RangeSelector) => Matcher<Node | Range, Range> { + return function createRangeSelectorMatcher(selector) { const startMatcher = createMatcher(selector.startSelector); const endMatcher = createMatcher(selector.endSelector); @@@ -51,17 -122,3 +122,17 @@@ }; }; } + - export const supportRangeSelector: Plugin<DomScope, Range> = function supportRangeSelectorPlugin( ++export const supportRangeSelector: Plugin<Node | Range, Node | Range> = function supportRangeSelectorPlugin( + next, + recurse, +) { + const createRangeSelectorMatcher = makeCreateRangeSelectorMatcher(recurse); + return function (selector: Selector) { + if (selector.type === 'RangeSelector') { + return createRangeSelectorMatcher(selector as RangeSelector); + } else { + return next(selector); + } + }; +}; diff --cc packages/selector/src/index.ts index 2214711,e0a5e48..7c294ae --- a/packages/selector/src/index.ts +++ b/packages/selector/src/index.ts @@@ -18,69 -18,47 +18,77 @@@ * under the License. */ -import type { Matcher, Selector } from './types'; +import type { Matcher, Selector, SelectorType, MatcherCreator, Plugin } from './types'; -export type { Matcher, Selector } from './types'; -export type { - CssSelector, - RangeSelector, - TextPositionSelector, - TextQuoteSelector, -} from './types'; + export * from './text'; +export * from './types'; + +interface TypeToMatcherCreatorMap<TScope, TMatch> { + // [K: SelectorType]: MatcherCreator<TScope, TMatch>; // Gives errors further down. TypeScript’s fault? + [K: string]: MatcherCreator<TScope, TMatch> | undefined; +} + +export function composeMatcherCreator<TScope, TMatch extends TScope>( + ...plugins: Array<Plugin<TScope, TMatch>> +): MatcherCreator<TScope, TMatch> { + function innerMatcherCreator(selector: Selector): Matcher<TScope, TMatch> { + throw new TypeError(`Unhandled selector. Selector type: ${selector.type}`); + } + + function outerMatcherCreator(selector: Selector): Matcher<TScope, TMatch> { + return composedMatcherCreator(selector); + } + + const composedMatcherCreator = plugins.reduceRight( + ( + matcherCreator: MatcherCreator<TScope, TMatch>, + plugin: Plugin<TScope, TMatch> + ) => plugin(matcherCreator, outerMatcherCreator), + innerMatcherCreator, + ); + + return outerMatcherCreator; +} + +// A plugin with parameters (i.e. a function that returns a plugin) +// Invokes the matcher implementation corresponding to the selector’s type. +export function mapSelectorTypes<TScope, TMatch extends TScope>( + typeToMatcherCreator: TypeToMatcherCreatorMap<TScope, TMatch>, +): Plugin<TScope, TMatch> { + return function mapSelectorTypesPlugin(next, recurse): MatcherCreator<TScope, TMatch> { + return function(selector: Selector): Matcher<TScope, TMatch> { + const type = selector.type; + if (type !== undefined) { + const matcherCreator = typeToMatcherCreator[type]; + if (matcherCreator !== undefined) + return matcherCreator(selector); + } + // Not a know selector type; continue down the plugin chain. + return next(selector); + } + } +} - // A plugin to support the Selector’s refinedBy field . + /** - * Wrap a matcher creation function so that it supports refinement of selection. ++ * A plugin to support the Selector’s refinedBy field. + * + * See {@link https://www.w3.org/TR/2017/REC-annotation-model-20170223/#refinement-of-selection + * | §4.2.9 Refinement of Selection} in the Web Annotation Data Model. + * - * @param matcherCreator - The function to wrap; it will be executed both for - * {@link Selector}s passed to the returned wrapper function, and for any - * refining Selector those might contain (and any refinement of that, etc.). - * + * @public + */ -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> { +export const supportRefinement: Plugin<any, any> = + function supportRefinementPlugin<TScope, TMatch extends TScope>( + next: MatcherCreator<TScope, TMatch>, + recurse: MatcherCreator<TScope, TMatch>, + ) { return function createMatcherWithRefinement( - sourceSelector: TSelector, + sourceSelector: Selector, ): Matcher<TScope, TMatch> { - const matcher = matcherCreator(sourceSelector); + const matcher = next(sourceSelector); if (sourceSelector.refinedBy) { - const refiningSelector = createMatcherWithRefinement( + const refiningSelector = recurse( sourceSelector.refinedBy, ); diff --cc packages/selector/src/types.ts index 2cf0929,fd59dfb..fff5199 --- a/packages/selector/src/types.ts +++ b/packages/selector/src/types.ts @@@ -18,13 -18,34 +18,38 @@@ * under the License. */ + /** + * A {@link https://www.w3.org/TR/2017/REC-annotation-model-20170223/#selectors + * | Selector} object of the Web Annotation Data Model. + * + * Corresponds to RDF class {@link http://www.w3.org/ns/oa#Selector} + * + * @public + */ export interface Selector { + /** + * A Selector can be refined by another Selector. + * + * See {@link https://www.w3.org/TR/2017/REC-annotation-model-20170223/#refinement-of-selection + * | §4.2.9 Refinement of Selection} in the Web Annotation Data Model. + * + * Corresponds to RDF property {@link http://www.w3.org/ns/oa#refinedBy} + */ - refinedBy?: Selector; + refinedBy?: this; ++ + type?: SelectorType; } +export type SelectorType = string; // not enumerating known options: we allow extensibility. + + /** + * The {@link https://www.w3.org/TR/2017/REC-annotation-model-20170223/#css-selector + * | CssSelector} of the Web Annotation Data Model. + * + * Corresponds to RDF class {@link http://www.w3.org/ns/oa#CssSelector} + * + * @public + */ export interface CssSelector extends Selector { type: 'CssSelector'; value: string; diff --cc web/index.js index fd5050e,abb9a8b..d0c33f8 --- a/web/index.js +++ b/web/index.js @@@ -18,23 -18,17 +18,21 @@@ * under the License. */ - /* global info, module, source, target */ - // declare const module; // TODO type? - // declare const info: HTMLElement; - // declare const source: HTMLElement; - // declare const target: HTMLElement; + /* global info, module, source, target, form */ import { - makeCreateRangeSelectorMatcher, createTextQuoteSelectorMatcher, describeTextQuote, + supportRangeSelector, + createTextPositionSelectorMatcher, + describeTextPosition, highlightRange, - } from '@annotator/dom'; + } from '@apache-annotator/dom'; -import { makeRefinable } from '@apache-annotator/selector'; +import { + composeMatcherCreator, + mapSelectorTypes, + supportRefinement, - } from '@annotator/selector'; ++} from '@apache-annotator/selector'; const EXAMPLE_SELECTORS = [ { @@@ -97,15 -93,22 +97,17 @@@ function cleanup() removeHighlight(); } target.normalize(); + info.innerText = ''; } -const createMatcher = makeRefinable((selector) => { - const innerCreateMatcher = { +const createMatcher = composeMatcherCreator( + supportRefinement, // this plugin must come first: it needs to access the result of the ones below. + supportRangeSelector, + mapSelectorTypes({ TextQuoteSelector: createTextQuoteSelectorMatcher, + TextPositionSelector: createTextPositionSelectorMatcher, - RangeSelector: makeCreateRangeSelectorMatcher(createMatcher), - }[selector.type]; - - if (!innerCreateMatcher) { - throw new Error(`Unsupported selector type: ${selector.type}`); - } - - return innerCreateMatcher(selector); -}); + }), +); async function anchor(selector) { const matchAll = createMatcher(selector);
