From 0ae18a75c97cde61abc37b2932d7e337f8882ea0 Mon Sep 17 00:00:00 2001 From: Gerben Date: Sun, 6 Nov 2022 15:15:58 +0100 Subject: [PATCH 1/4] Tweak types Based on experience in Web Annotation Discovery webextension --- packages/dom/src/range/match.ts | 6 +++--- packages/selector/src/index.ts | 32 ++++++++++++++++++++++++++------ packages/selector/src/types.ts | 6 +++--- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/packages/dom/src/range/match.ts b/packages/dom/src/range/match.ts index 04222f0..27a1ad6 100644 --- a/packages/dom/src/range/match.ts +++ b/packages/dom/src/range/match.ts @@ -95,11 +95,11 @@ import { cartesian } from './cartesian.js'; * * @public */ -export function makeCreateRangeSelectorMatcher( - createMatcher: ( +export function makeCreateRangeSelectorMatcher( + createMatcher: ( selector: T, ) => Matcher, -): (selector: RangeSelector) => Matcher { +): (selector: RangeSelector) => Matcher { return function createRangeSelectorMatcher(selector) { const startMatcher = createMatcher(selector.startSelector); const endMatcher = createMatcher(selector.endSelector); diff --git a/packages/selector/src/index.ts b/packages/selector/src/index.ts index 48183a6..f75f6e1 100644 --- a/packages/selector/src/index.ts +++ b/packages/selector/src/index.ts @@ -32,6 +32,28 @@ export type { } from './types.js'; export * from './text/index.js'; +/** + * A Refinable selector can have the `refinedBy` attribute, whose value must be + * of the same type (possibly again refined, recursively). + * + * 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. + * + * @example + * Example value of type `Refinable`: + * + * { + * type: "CssSelector", + * …, + * refinedBy: { + * type: "TextQuoteSelector", + * …, + * refinedBy: { … }, // again either a CssSelector or TextQuoteSelector + * } + * } + */ +export type Refinable = T & { refinedBy?: Refinable }; + /** * Wrap a matcher creation function so that it supports refinement of selection. * @@ -45,18 +67,16 @@ export * from './text/index.js'; * @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 }, + TSelector extends Selector, TScope, // To enable refinement, the implementation’s Match object must be usable as a // Scope object itself. TMatch extends TScope >( - matcherCreator: (selector: TSelector) => Matcher, -): (selector: TSelector) => Matcher { + matcherCreator: (selector: Refinable) => Matcher, +): (selector: Refinable) => Matcher { return function createMatcherWithRefinement( - sourceSelector: TSelector, + sourceSelector: Refinable, ): Matcher { const matcher = matcherCreator(sourceSelector); diff --git a/packages/selector/src/types.ts b/packages/selector/src/types.ts index d3c227b..a93d168 100644 --- a/packages/selector/src/types.ts +++ b/packages/selector/src/types.ts @@ -91,10 +91,10 @@ export interface TextPositionSelector extends Selector { * * @public */ -export interface RangeSelector extends Selector { +export interface RangeSelector extends Selector { type: 'RangeSelector'; - startSelector: Selector; - endSelector: Selector; + startSelector: T; + endSelector: T; } /** From 5875ed9564b7ff512915b66c443f4e8730111977 Mon Sep 17 00:00:00 2001 From: Gerben Date: Sun, 6 Nov 2022 15:21:04 +0100 Subject: [PATCH 2/4] Move refinedBy code into its own file --- packages/selector/src/index.ts | 70 +----------------------- packages/selector/src/refinable.ts | 88 ++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 67 deletions(-) create mode 100644 packages/selector/src/refinable.ts diff --git a/packages/selector/src/index.ts b/packages/selector/src/index.ts index f75f6e1..2c15f8f 100644 --- a/packages/selector/src/index.ts +++ b/packages/selector/src/index.ts @@ -21,77 +21,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { Matcher, Selector } from './types.js'; - -export type { Matcher, Selector } from './types.js'; export type { + Matcher, + Selector, CssSelector, RangeSelector, TextPositionSelector, TextQuoteSelector, } from './types.js'; export * from './text/index.js'; - -/** - * A Refinable selector can have the `refinedBy` attribute, whose value must be - * of the same type (possibly again refined, recursively). - * - * 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. - * - * @example - * Example value of type `Refinable`: - * - * { - * type: "CssSelector", - * …, - * refinedBy: { - * type: "TextQuoteSelector", - * …, - * refinedBy: { … }, // again either a CssSelector or TextQuoteSelector - * } - * } - */ -export type Refinable = T & { refinedBy?: Refinable }; - -/** - * Wrap a matcher creation function so that it supports refinement of selection. - * - * 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< - TSelector extends Selector, - TScope, - // To enable refinement, the implementation’s Match object must be usable as a - // Scope object itself. - TMatch extends TScope ->( - matcherCreator: (selector: Refinable) => Matcher, -): (selector: Refinable) => Matcher { - return function createMatcherWithRefinement( - sourceSelector: Refinable, - ): Matcher { - const matcher = matcherCreator(sourceSelector); - - if (sourceSelector.refinedBy) { - const refiningSelector = createMatcherWithRefinement( - sourceSelector.refinedBy, - ); - - return async function* matchAll(scope) { - for await (const match of matcher(scope)) { - yield* refiningSelector(match); - } - }; - } - - return matcher; - }; -} +export * from './refinable.js'; diff --git a/packages/selector/src/refinable.ts b/packages/selector/src/refinable.ts new file mode 100644 index 0000000..9bb1c1a --- /dev/null +++ b/packages/selector/src/refinable.ts @@ -0,0 +1,88 @@ +/** + * @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. + * + * SPDX-FileCopyrightText: The Apache Software Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { Matcher, Selector } from './types.js'; + +/** + * A Refinable selector can have the `refinedBy` attribute, whose value must be + * of the same type (possibly again refined, recursively). + * + * 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. + * + * @example + * Example value of type `Refinable`: + * + * { + * type: "CssSelector", + * …, + * refinedBy: { + * type: "TextQuoteSelector", + * …, + * refinedBy: { … }, // again either a CssSelector or TextQuoteSelector + * } + * } + */ +export type Refinable = T & { refinedBy?: Refinable }; + +/** + * Wrap a matcher creation function so that it supports refinement of selection. + * + * 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< + TSelector extends Selector, + TScope, + // To enable refinement, the implementation’s Match object must be usable as a + // Scope object itself. + TMatch extends TScope +>( + matcherCreator: (selector: Refinable) => Matcher, +): (selector: Refinable) => Matcher { + return function createMatcherWithRefinement( + sourceSelector: Refinable, + ): Matcher { + const matcher = matcherCreator(sourceSelector); + + if (sourceSelector.refinedBy) { + const refiningSelector = createMatcherWithRefinement( + sourceSelector.refinedBy, + ); + + return async function* matchAll(scope) { + for await (const match of matcher(scope)) { + yield* refiningSelector(match); + } + }; + } + + return matcher; + }; +} From 3dad393d4745b59d84875aacfc958d7426f96955 Mon Sep 17 00:00:00 2001 From: Gerben Date: Wed, 23 Nov 2022 22:00:40 +0100 Subject: [PATCH 3/4] Create new package @apache-annotator/annotation Types & utility functions for annotations as a whole. With code imported from . --- packages/annotation/package.json | 25 ++ packages/annotation/src/index.ts | 25 ++ packages/annotation/src/multiplicity.ts | 48 ++++ packages/annotation/src/web-annotation.ts | 227 ++++++++++++++++++ .../test/model/multiplicity.test.ts | 39 +++ packages/annotation/tsconfig.json | 8 + packages/apache-annotator/package.json | 1 + packages/apache-annotator/src/annotation.ts | 37 +++ packages/apache-annotator/tsconfig.json | 1 + packages/selector/src/types.ts | 21 +- packages/selector/tsconfig.json | 5 +- tsconfig.json | 1 + tsconfig.test.json | 1 + 13 files changed, 419 insertions(+), 20 deletions(-) create mode 100644 packages/annotation/package.json create mode 100644 packages/annotation/src/index.ts create mode 100644 packages/annotation/src/multiplicity.ts create mode 100644 packages/annotation/src/web-annotation.ts create mode 100644 packages/annotation/test/model/multiplicity.test.ts create mode 100644 packages/annotation/tsconfig.json create mode 100644 packages/apache-annotator/src/annotation.ts diff --git a/packages/annotation/package.json b/packages/annotation/package.json new file mode 100644 index 0000000..a663402 --- /dev/null +++ b/packages/annotation/package.json @@ -0,0 +1,25 @@ +{ + "name": "@apache-annotator/annotation", + "version": "0.3.0", + "description": "Web Annotation types and utilities.", + "homepage": "https://annotator.apache.org", + "repository": { + "type": "git", + "url": "https://github.com/apache/incubator-annotator.git", + "directory": "packages/annotation" + }, + "license": "Apache-2.0", + "author": "Apache Software Foundation", + "type": "module", + "exports": "./lib/index.js", + "main": "./lib/index.js", + "dependencies": { + "@babel/runtime-corejs3": "^7.13.10" + }, + "engines": { + "node": "^14.15 || ^15.4 || >=16" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/annotation/src/index.ts b/packages/annotation/src/index.ts new file mode 100644 index 0000000..1e5f467 --- /dev/null +++ b/packages/annotation/src/index.ts @@ -0,0 +1,25 @@ +/** + * @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. + * + * SPDX-FileCopyrightText: The Apache Software Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './web-annotation.js'; +export * from './multiplicity.js'; diff --git a/packages/annotation/src/multiplicity.ts b/packages/annotation/src/multiplicity.ts new file mode 100644 index 0000000..93a74d4 --- /dev/null +++ b/packages/annotation/src/multiplicity.ts @@ -0,0 +1,48 @@ +/** + * @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. + * + * SPDX-FileCopyrightText: The Apache Software Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +export type OneOrMore = T | T[]; +export type ZeroOrMore = undefined | null | T | T[]; + +export type OneOrMoreIncluding = + | RequiredValue + | [RequiredValue, ...Other[]] + | [...Other[], RequiredValue]; +// | [Other, ...OneOrMoreIncluding]; // FIXME TypeScript complains about the circular reference.. + +/** + * OnlyOne extracts the T from a One/ZeroOrMore type + */ +export type OnlyOne = T extends (infer X)[] ? X : T; + +export function asArray(value: ZeroOrMore): T[] { + if (Array.isArray(value)) return value; + if (value === undefined || value === null) return []; + return [value]; +} + +export function asSingleValue(value: ZeroOrMore): T | undefined { + if (value instanceof Array) return value[0]; + if (value === undefined || value === null) return undefined; + return value; +} diff --git a/packages/annotation/src/web-annotation.ts b/packages/annotation/src/web-annotation.ts new file mode 100644 index 0000000..013abef --- /dev/null +++ b/packages/annotation/src/web-annotation.ts @@ -0,0 +1,227 @@ +/** + * @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. + * + * SPDX-FileCopyrightText: The Apache Software Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { + OneOrMore, + OneOrMoreIncluding, + ZeroOrMore, +} from './multiplicity.js'; + +/** + * A Web Annotation object. + * + * This is an interpretation of the Web Annotation Data Model: + * + * + * TODO Deal more systemically with ‘relations’, i.e. values that could be + * either a nested object or a URI referring to such an object. + */ +export interface WebAnnotation { + '@context': OneOrMoreIncluding; + type: OneOrMoreIncluding; + id: string; + target: OneOrMore; + creator?: ZeroOrMore; + created?: UtcDateTime; + generator?: ZeroOrMore; + generated?: UtcDateTime; + modified?: UtcDateTime; + motivation?: ZeroOrMore; + audience?: ZeroOrMore; + rights?: ZeroOrMore; + canonical?: string; + via?: ZeroOrMore; + body?: BodyChoice | OneOrMore; + bodyValue?: string; +} + +/** + * A slightly stricter type for WebAnnotation, not allowing both a body and bodyValue. + */ +export type WebAnnotationStrict = WebAnnotation & (WithBody | WithBodyValue | WithoutBody); + +interface WithBody { + body: BodyChoice | OneOrMore; + bodyValue?: undefined; +} + +interface WithBodyValue { + body?: undefined; + bodyValue: string; +} + +interface WithoutBody { + body?: undefined; + bodyValue?: undefined; +} + +export type Body = string | BodyObject; + +export type BodyObject = { + creator?: ZeroOrMore; + created?: UtcDateTime; + modified?: UtcDateTime; + purpose?: ZeroOrMore; +} & (TextualBody | SpecificResource | ExternalResource); + +export type Target = string | SpecificResource | ExternalResource; + +export type Agent = + | string + | { + id?: string; + type?: ZeroOrMore<'Person' | 'Organization' | 'Software'>; + name?: ZeroOrMore; + nickname?: ZeroOrMore; + email?: ZeroOrMore; + email_sha1?: ZeroOrMore; + homepage?: ZeroOrMore; + }; + +export type Audience = + | string + | { + id?: string; + type?: string; + }; + +export interface BodyChoice { + type: 'Choice'; + items: Body[]; +} + +export interface TextualBody extends Omit { + id?: string; + type: 'TextualBody'; + value: string; +} + +export interface SpecificResource { + id?: string; + type?: 'SpecificResource'; + source: string; + selector?: string | OneOrMore; + accessibility?: AccessibilityFeatures; + rights?: ZeroOrMore; + canonical?: string; + via?: ZeroOrMore; +} + +/** + * 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 { + type?: string; + + /** + * 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; +} + +export interface ExternalResource { + id: string; + // XXX type’s value SHOULD be one of these, “but MAY come from other vocabularies”. + type?: ZeroOrMore<'Dataset' | 'Image' | 'Video' | 'Sound' | 'Text'>; + format?: ZeroOrMore; + language?: ZeroOrMore; + processingLanguage?: string; + textDirection?: 'ltr' | 'rtl' | 'auto'; + accessibility?: AccessibilityFeatures; + rights?: ZeroOrMore; + canonical?: string; + via?: ZeroOrMore; +} + +export type Motivation = + | 'assessing' + | 'bookmarking' + | 'classifying' + | 'commenting' + | 'describing' + | 'editing' + | 'highlighting' + | 'identifying' + | 'linking' + | 'moderating' + | 'questioning' + | 'replying' + | 'tagging'; + +// “The datetime MUST be a xsd:dateTime with the UTC timezone expressed as "Z".” +type UtcDateTime = `${string}Z`; + +// To help usage, narrow the type of Date.toISOString(); it is guaranteed to end with a 'Z'. +declare global { + interface Date { + toISOString(): UtcDateTime; + } +} + +// From +export type AccessibilityFeatures = + | ZeroOrMore + | 'none' + | ['none']; +export type AccessibilityFeature = + | 'annotations' + | 'ARIA' + | 'bookmarks' + | 'index' + | 'printPageNumbers' + | 'readingOrder' + | 'structuralNavigation' + | 'tableOfContents' + | 'taggedPDF' + | 'alternativeText' + | 'audioDescription' + | 'captions' + | 'describedMath' + | 'longDescription' + | 'rubyAnnotations' + | 'signLanguage' + | 'transcript' + | 'displayTransformability' + | 'synchronizedAudioText' + | 'timingControl' + | 'unlocked' + | 'ChemML' + | 'latex' + | 'MathML' + | 'ttsMarkup' + | 'highContrastAudio' + | 'highContrastDisplay' + | 'largePrint' + | 'braille' + | 'tactileGraphic' + | 'tactileObject'; diff --git a/packages/annotation/test/model/multiplicity.test.ts b/packages/annotation/test/model/multiplicity.test.ts new file mode 100644 index 0000000..6e18e10 --- /dev/null +++ b/packages/annotation/test/model/multiplicity.test.ts @@ -0,0 +1,39 @@ +/** + * @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. + * + * SPDX-FileCopyrightText: The Apache Software Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +import { strict as assert } from 'assert'; +import { asArray, asSingleValue } from '../../src/multiplicity'; +import type { OneOrMore, OnlyOne, ZeroOrMore } from '../../src/multiplicity'; + +describe('asArray', () => { + it('wraps a single value', () => { + const input: OneOrMore = 'blub'; + const output = asArray(input); + assert.strictEqual(output, ['blub']); + }); + it('leaves an array untouched', () => { + const input: OneOrMore = ['blub']; + const output = asArray(input); + assert.strictEqual(output, input); + }); +}); diff --git a/packages/annotation/tsconfig.json b/packages/annotation/tsconfig.json new file mode 100644 index 0000000..653b0a5 --- /dev/null +++ b/packages/annotation/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"], + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + } +} diff --git a/packages/apache-annotator/package.json b/packages/apache-annotator/package.json index ebec725..597dd7e 100644 --- a/packages/apache-annotator/package.json +++ b/packages/apache-annotator/package.json @@ -15,6 +15,7 @@ "./*": "./lib/*.js" }, "dependencies": { + "@apache-annotator/annotation": "^0.3.0", "@apache-annotator/dom": "^0.3.0", "@apache-annotator/selector": "^0.3.0", "@babel/runtime-corejs3": "^7.13.10" diff --git a/packages/apache-annotator/src/annotation.ts b/packages/apache-annotator/src/annotation.ts new file mode 100644 index 0000000..98bc670 --- /dev/null +++ b/packages/apache-annotator/src/annotation.ts @@ -0,0 +1,37 @@ +/** + * @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. + * + * SPDX-FileCopyrightText: The Apache Software Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * This module provides types and utilities for the {@link https://www.w3.org/TR/2017/REC-annotation-model-20170223/ + * | Web Annotation Data Model}. + * + * Besides a type definition, it provides convenience functions for dealing with + * Web Annotations, such as getting the URL(s) of pages an annotation targets, + * or the plain text content the annotation body. It aims to provide some basic + * tools to get started writing interoperable annotation tools without having to + * deal with the intricacies of the data model. + * + * @module + */ + +export * from '@apache-annotator/annotation'; diff --git a/packages/apache-annotator/tsconfig.json b/packages/apache-annotator/tsconfig.json index 0ac1cf1..cabb91b 100644 --- a/packages/apache-annotator/tsconfig.json +++ b/packages/apache-annotator/tsconfig.json @@ -6,6 +6,7 @@ "rootDir": "src" }, "references": [ + { "path": "../annotation" }, { "path": "../dom" }, { "path": "../selector" } ] diff --git a/packages/selector/src/types.ts b/packages/selector/src/types.ts index a93d168..455ccf6 100644 --- a/packages/selector/src/types.ts +++ b/packages/selector/src/types.ts @@ -21,25 +21,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -/** - * 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; -} +import type { Selector } from '@apache-annotator/annotation'; +export type { Selector }; /** * The {@link https://www.w3.org/TR/2017/REC-annotation-model-20170223/#css-selector diff --git a/packages/selector/tsconfig.json b/packages/selector/tsconfig.json index 653b0a5..2281984 100644 --- a/packages/selector/tsconfig.json +++ b/packages/selector/tsconfig.json @@ -4,5 +4,8 @@ "compilerOptions": { "outDir": "lib", "rootDir": "src" - } + }, + "references": [ + { "path": "../annotation" } + ] } diff --git a/tsconfig.json b/tsconfig.json index b9a2c0d..126dcba 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "files": [], "references": [ { "path": "packages/apache-annotator" }, + { "path": "packages/annotation" }, { "path": "packages/dom" }, { "path": "packages/selector" }, { "path": "tsconfig.test.json"} diff --git a/tsconfig.test.json b/tsconfig.test.json index f439fba..0c2f128 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -2,6 +2,7 @@ "extends": "./tsconfig.base.json", "include": ["test", "packages/*/test"], "references": [ + { "path": "packages/annotation" }, { "path": "packages/dom" }, { "path": "packages/selector" } ] From 27650e59ed276e3f270ecdba33596a2565c6106d Mon Sep 17 00:00:00 2001 From: Gerben Date: Wed, 23 Nov 2022 22:06:26 +0100 Subject: [PATCH 4/4] Add convenience matcher functions to dom package - createAnySelectorMatcher mimics the signature of existing matcher creators. - matchSelector makes the common use case easy. --- packages/dom/src/index.ts | 1 + packages/dom/src/match.ts | 101 ++++++++++++++++++++++++++++++++++++++ web/index.js | 28 +---------- 3 files changed, 104 insertions(+), 26 deletions(-) create mode 100644 packages/dom/src/match.ts diff --git a/packages/dom/src/index.ts b/packages/dom/src/index.ts index 6969ea9..5073811 100644 --- a/packages/dom/src/index.ts +++ b/packages/dom/src/index.ts @@ -21,6 +21,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +export * from './match.js'; export * from './css.js'; export * from './range/index.js'; export * from './text-quote/index.js'; diff --git a/packages/dom/src/match.ts b/packages/dom/src/match.ts new file mode 100644 index 0000000..d0038f8 --- /dev/null +++ b/packages/dom/src/match.ts @@ -0,0 +1,101 @@ +/** + * @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. + * + * SPDX-FileCopyrightText: The Apache Software Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +import { asArray, OneOrMore } from "@apache-annotator/annotation"; +import type { + CssSelector, + TextQuoteSelector, + TextPositionSelector, + RangeSelector, + Matcher, + Refinable, +} from "@apache-annotator/selector"; +import { makeRefinable } from "@apache-annotator/selector"; +import { createCssSelectorMatcher } from "./css"; +import { makeCreateRangeSelectorMatcher } from "./range"; +import { createTextPositionSelectorMatcher } from "./text-position"; +import { createTextQuoteSelectorMatcher } from "./text-quote"; + +export const supportedSelectorTypes = [ + "CssSelector", + "TextQuoteSelector", + "TextPositionSelector", + "RangeSelector", +]; + +export type SupportedSelector = Refinable< + | CssSelector + | TextQuoteSelector + | TextPositionSelector + | RangeSelector +>; + +export type DomScope = Node | Range; +export type DomMatch = Element | Range; +export type DomMatcher = Matcher; + +const createMatcher: ( + selector: SupportedSelector +) => DomMatcher = makeRefinable( + (selector: SupportedSelector) => { + if (selector.type === "CssSelector") + return createCssSelectorMatcher(selector); + if (selector.type === "TextQuoteSelector") + return createTextQuoteSelectorMatcher(selector); + if (selector.type === "TextPositionSelector") + return createTextPositionSelectorMatcher(selector); + if (selector.type === "RangeSelector") + return makeCreateRangeSelectorMatcher( + // @ts-ignore (needless type error; bug in TypeScript?) + createMatcher + )(selector); + throw new Error(`Unsupported selector type: ${(selector as any)?.type}`); + } +); + +export function createAnySelectorMatcher( + oneOrMoreSelectors: OneOrMore +): DomMatcher { + const selectors = asArray(oneOrMoreSelectors); + // Use the first selector we understand. (“Multiple Selectors SHOULD select the same content”) + // TODO Take the more precise one; retry with others if the first fails; perhaps combine e.g. Position+Quote for speedup. + const selector = selectors.find( + (selector) => + selector.type && supportedSelectorTypes.includes(selector.type) + ); + if (!selector) throw new Error(`Unsupported selector type: ${asArray(selectors).map(s => s.type)}`); + const matcher = createMatcher(selector as SupportedSelector) + return matcher; +} + +export async function matchSelector( + selectors: OneOrMore, + scope: DomScope = window.document +) { + const matchGenerator = createAnySelectorMatcher(selectors)(scope); + const matches: DomMatch[] = []; + for await (const match of matchGenerator) { + matches.push(match); + } + return matches; +} diff --git a/web/index.js b/web/index.js index 7917798..bdaa31c 100644 --- a/web/index.js +++ b/web/index.js @@ -24,14 +24,11 @@ /* global info, module, source, target, form */ import { - makeCreateRangeSelectorMatcher, - createTextQuoteSelectorMatcher, + matchSelector, describeTextQuote, - createTextPositionSelectorMatcher, describeTextPosition, highlightText, } from '@apache-annotator/dom'; -import { makeRefinable } from '@apache-annotator/selector'; const EXAMPLE_SELECTORS = [ { @@ -99,29 +96,8 @@ function cleanup() { info.innerText = ''; } -const createMatcher = makeRefinable((selector) => { - const innerCreateMatcher = { - 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); - const ranges = []; - - // First collect all matches, and only then highlight them; to avoid - // modifying the DOM while the matcher is running. - for await (const range of matchAll(target)) { - ranges.push(range); - } + const ranges = matchSelector(selector, target); for (const range of ranges) { const removeHighlight = highlightText(range);