| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @fileoverview Interface used to monitor and extract visible text on the page |
| * and pass it on to the annotations manager. |
| */ |
| |
| import type {TextViewportAnnotation} from '//ios/web/annotations/resources/text_annotation_list.js'; |
| import {TextAnnotationList} from '//ios/web/annotations/resources/text_annotation_list.js'; |
| import {TextClick} from '//ios/web/annotations/resources/text_click.js'; |
| import {annotationExternalData, annotationFullText} from '//ios/web/annotations/resources/text_decoration.js'; |
| import {TextDecorator} from '//ios/web/annotations/resources/text_decorator.js'; |
| import {TextDomObserver} from '//ios/web/annotations/resources/text_dom_observer.js'; |
| import type {HTMLElementWithSymbolIndex, NodeWithSymbolIndex} from '//ios/web/annotations/resources/text_dom_utils.js'; |
| import {getMetaContentByHttpEquiv, hasNoIntentDetection, noFormatDetectionTypes, rectFromElement} from '//ios/web/annotations/resources/text_dom_utils.js'; |
| import type {TextChunk} from '//ios/web/annotations/resources/text_extractor.js'; |
| import {TextExtractor} from '//ios/web/annotations/resources/text_extractor.js'; |
| import {TextIntersectionObserver} from '//ios/web/annotations/resources/text_intersection_observer.js'; |
| import {TextStyler} from '//ios/web/annotations/resources/text_styler.js'; |
| import {IdleTaskTracker} from '//ios/web/annotations/resources/text_tasks.js'; |
| import {CrWebApi, gCrWeb} from '//ios/web/public/js_messaging/resources/gcrweb.js'; |
| import {sendWebKitMessage} from '//ios/web/public/js_messaging/resources/utils.js'; |
| |
| // Used to trigger text extraction and decoration when system is idle. |
| let idleTaskTracker: IdleTaskTracker|null; |
| let intersectionObserver: TextIntersectionObserver|null; |
| let mutationObserver: TextDomObserver|null; |
| let styler: TextStyler|null; |
| let decorator: TextDecorator|null; |
| let extractor: TextExtractor|null; |
| let click: TextClick|null; |
| |
| // Mark: Private API |
| |
| let uniqueId = 0; |
| const chunksInFlight = new Map<number, TextChunk>(); |
| |
| // Consumer of text `chunk` that, memorizes them and sends them to the browser |
| // side for intent detection. |
| function textChunkConsumer(chunk: TextChunk): void { |
| chunksInFlight.set(++uniqueId, chunk); |
| if (chunksInFlight.size === 1) { |
| idleTaskTracker?.startActivityListeners(); |
| } |
| const disabledTypes = noFormatDetectionTypes(); |
| sendWebKitMessage('annotations', { |
| command: 'annotations.extractedText', |
| text: chunk.text, |
| seqId: uniqueId, |
| metadata: { |
| htmlLang: document.documentElement.lang, |
| httpContentLanguage: getMetaContentByHttpEquiv('content-language'), |
| wkNoTelephone: disabledTypes.has('telephone'), |
| wkNoEmail: disabledTypes.has('email'), |
| wkNoAddress: disabledTypes.has('address'), |
| wkNoDate: disabledTypes.has('date'), |
| wkNoUnit: disabledTypes.has('unit'), |
| }, |
| }); |
| } |
| |
| // Creates a task to decorate the given `annotations` against the TextChunk with |
| // `seqId`. The text chunk is then deleted. |
| function decorateChunk( |
| annotations: TextViewportAnnotation[], seqId: number): void { |
| // Find and remove the TextChunk. |
| const chunk = chunksInFlight.get(seqId); |
| chunksInFlight.delete(seqId); |
| |
| idleTaskTracker?.schedule(() => { |
| const run = new TextAnnotationList( |
| annotations, chunk?.visibleStart, chunk?.visibleEnd); |
| const count = annotations.length; |
| if (decorator && chunk) { |
| decorator.decorateChunk(chunk, run); |
| } else { |
| run.fail(); |
| } |
| sendWebKitMessage('annotations', { |
| command: 'annotations.decoratingComplete', |
| annotations: count, |
| successes: run.successes, |
| failures: run.failures, |
| cancelled: run.cancelled, |
| }); |
| }); |
| |
| if (chunksInFlight.size === 0) { |
| idleTaskTracker?.stopActivityListeners(); |
| } |
| } |
| |
| // Consumer of taps on annotations. Forwards to the browser side. |
| function tapConsumer( |
| annotation: HTMLElementWithSymbolIndex, cancel: boolean): void { |
| decorator?.highlightAnnotation(annotation); |
| sendWebKitMessage('annotations', { |
| command: 'annotations.onClick', |
| cancel: cancel, |
| data: annotation[annotationExternalData], |
| rect: rectFromElement(annotation), |
| text: annotation[annotationFullText], |
| }); |
| } |
| |
| function decorationNodeRemovedConsumer(node: NodeWithSymbolIndex): void { |
| decorator?.decorationNodeUnexpectedlyRemoved(node); |
| // Clean cache of corrupted annotation on the browser side. |
| sendWebKitMessage('annotations', { |
| command: 'annotations.decoratingComplete', |
| annotations: 0, |
| successes: 0, |
| failures: 1, |
| cancelled: [node[annotationExternalData]], |
| }); |
| } |
| |
| // Mark: Public API |
| |
| // Starts the annotation observer. |
| function start(): void { |
| // Check for already started or for a page request to not detect intent. |
| if (hasNoIntentDetection() || intersectionObserver) { |
| return; |
| } |
| const root = document.documentElement; |
| idleTaskTracker = new IdleTaskTracker(); |
| click = new TextClick(root, tapConsumer, () => decorator?.decorations); |
| extractor = new TextExtractor(textChunkConsumer); |
| styler = new TextStyler(); |
| decorator = new TextDecorator(styler); |
| intersectionObserver = |
| new TextIntersectionObserver(root, extractor, idleTaskTracker); |
| mutationObserver = new TextDomObserver( |
| root, intersectionObserver, decorationNodeRemovedConsumer); |
| intersectionObserver.start(); |
| mutationObserver.start(); |
| click.start(); |
| } |
| |
| // Stops the annotation observer. |
| function stop(): void { |
| click?.stop(); |
| idleTaskTracker?.shutdown(); |
| mutationObserver?.stop(); |
| intersectionObserver?.stop(); |
| decorator?.removeAllDecorations(); |
| styler = null; |
| decorator = null; |
| intersectionObserver = null; |
| mutationObserver = null; |
| extractor = null; |
| idleTaskTracker = null; |
| click = null; |
| } |
| |
| // Triggers `decorator` to apply `annotations` on the cached text chunk for |
| // the given `seqId`. |
| function decorateAnnotations( |
| annotations: TextViewportAnnotation[], seqId: number): void { |
| decorateChunk(annotations, seqId); |
| } |
| |
| function removeDecorations(): void { |
| decorator?.removeAllDecorations(); |
| } |
| |
| function removeDecorationsWithType(type: string): void { |
| decorator?.removeDecorationsOfType(type); |
| } |
| |
| function removeHighlight(): void { |
| decorator?.removeHighlight(); |
| } |
| |
| const annotations = new CrWebApi(); |
| |
| annotations.addFunction('start', start); |
| annotations.addFunction('stop', stop); |
| annotations.addFunction('decorateAnnotations', decorateAnnotations); |
| annotations.addFunction('removeDecorations', removeDecorations); |
| annotations.addFunction('removeDecorationsWithType', removeDecorationsWithType); |
| annotations.addFunction('removeHighlight', removeHighlight); |
| |
| gCrWeb.registerApi('annotations', annotations); |