From 17458297dc7391d562ea6d5a75f8c4b654405f0c Mon Sep 17 00:00:00 2001 From: Josiah Ivey Date: Mon, 3 Jan 2022 16:12:17 -0800 Subject: [PATCH 1/8] send the node path of the highlighted content --- package.json | 2 +- .../XpathRangeSelector/index.ts | 11 +++++- .../XpathRangeSelector/xpath.spec.js | 33 ++++++++++++++++++ .../XpathRangeSelector/xpath.ts | 34 +++++++++++++++++++ yarn.lock | 10 +++--- 5 files changed, 83 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 6097252..13f6243 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "ts-loader": "^5.2.1", "tslint": "^5.11.0", "tslint-loader": "^3.6.0", - "typescript": "^3.1.1", + "typescript": "3.5", "typescript-babel-jest": "^1.0.5" }, "resolutions": { diff --git a/src/serializationStrategies/XpathRangeSelector/index.ts b/src/serializationStrategies/XpathRangeSelector/index.ts index 1e9e652..0930bef 100644 --- a/src/serializationStrategies/XpathRangeSelector/index.ts +++ b/src/serializationStrategies/XpathRangeSelector/index.ts @@ -1,5 +1,5 @@ import Highlighter from '../../Highlighter'; -import { getFirstByXPath, getXPathForElement } from './xpath'; +import { getFirstByXPath, getXPathForElement, getNodePath } from './xpath'; export const discriminator = 'XpathRangeSelector'; @@ -10,6 +10,7 @@ export interface IData { startOffset: number; endContainer: string; endOffset: number; + nodePath?: number[]; } export function serialize(range: Range, referenceElement: HTMLElement): IData { @@ -17,12 +18,20 @@ export function serialize(range: Range, referenceElement: HTMLElement): IData { const [endContainer, endOffset] = getXPathForElement(range.endContainer, range.endOffset, referenceElement); const [startContainer, startOffset] = getXPathForElement(range.startContainer, range.startOffset, referenceElement); + let nodePath: number[] = []; + const element = getFirstByXPath(startContainer, range.startOffset, referenceElement); + + if (element[0]) { + nodePath = getNodePath(element[0], referenceElement); + } + return { endContainer, endOffset, referenceElementId: referenceElement.id, startContainer, startOffset, + nodePath, type: discriminator, }; } diff --git a/src/serializationStrategies/XpathRangeSelector/xpath.spec.js b/src/serializationStrategies/XpathRangeSelector/xpath.spec.js index a87a285..899594d 100644 --- a/src/serializationStrategies/XpathRangeSelector/xpath.spec.js +++ b/src/serializationStrategies/XpathRangeSelector/xpath.spec.js @@ -3,6 +3,39 @@ import * as xpath from './xpath'; const screenReaderNode = ``; +describe('getNodePath', () => { + it('creates a path of indexes', () => { + document.body.innerHTML = ` +
+

Header text

+

Section text

+
+ `; + + const container = document.getElementById('1'); + const element = document.getElementById('2'); + const result = xpath.getNodePath(element, container); + expect(result).toEqual([1, 0]); + }); + + it('works with text nodes', () => { + document.body.innerHTML = ` +
+
+ Some text before the link + + Text after the link +
+
+ `; + + const container = document.getElementById('1'); + const element = xpath.getFirstByXPath('./text()[2]', 0, container); + const result = xpath.getNodePath(element[0], container); + expect(result).toEqual([2]); + }); +}); + describe('getXPathForElement', () => { it('creates path to self', () => { document.body.innerHTML = ` diff --git a/src/serializationStrategies/XpathRangeSelector/xpath.ts b/src/serializationStrategies/XpathRangeSelector/xpath.ts index ec794fb..b4d79f2 100644 --- a/src/serializationStrategies/XpathRangeSelector/xpath.ts +++ b/src/serializationStrategies/XpathRangeSelector/xpath.ts @@ -104,6 +104,40 @@ const resolveToPreviousElementOffsetIfPossible = (element: Node, offset: number) return [element, offset]; }; +export function getNodePath(element: HTMLElement, container: HTMLElement): number[] { + let currentParent: HTMLElement | null = element; + const nodePath: number[] = []; + + // Go up the stack, capturing the index of each node to create a path + while (currentParent != container) { + const currentChild = currentParent; + + if (currentParent.parentElement) { + currentParent = currentParent.parentElement; + let filteredNodes = Array.from(currentParent.childNodes).filter(n => !isTextHighlightOrScreenReaderNode(n)); + + filteredNodes = filteredNodes.filter((current, i) => { + if (current == currentChild) { + // Always include the node with the content to get the index + return true; + } + + const adjacent = filteredNodes[i + 1]; + const isCollapsible = (adjacent && isText(adjacent) && isText(current)); + + // Remove adjacent text nodes + return !isCollapsible; + }); + + const index = filteredNodes.indexOf(currentChild); + nodePath.unshift(index); + } + + } + + return nodePath.length > 0 ? nodePath : [0]; +} + // kinda copied from https://developer.mozilla.org/en-US/docs/Web/XPath/Snippets#getXPathForElement export function getXPathForElement(targetElement: Node, offset: number, reference: HTMLElement): [string, number] { [targetElement, offset] = floatThroughText(targetElement, offset, reference); diff --git a/yarn.lock b/yarn.lock index f9835e5..d766b61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5195,16 +5195,16 @@ typescript-babel-jest@^1.0.5: babel-jest "20.0.3" typescript "^2.4.1" +typescript@3.5: + version "3.5.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" + integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== + typescript@^2.4.1: version "2.9.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c" integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w== -typescript@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.1.1.tgz#3362ba9dd1e482ebb2355b02dfe8bcd19a2c7c96" - integrity sha512-Veu0w4dTc/9wlWNf2jeRInNodKlcdLgemvPsrNpfu5Pq39sgfFjvIIgTsvUHCoLBnMhPoUA+tFxsXjU6VexVRQ== - uglify-js@^3.1.4: version "3.13.5" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.5.tgz#5d71d6dbba64cf441f32929b1efce7365bb4f113" From d1ea50d51f1c4807143564bcf1570e428cbd5dab Mon Sep 17 00:00:00 2001 From: Josiah Ivey Date: Fri, 14 Jan 2022 15:11:23 -0800 Subject: [PATCH 2/8] move node path to separate content path field --- src/SerializedHighlight.ts | 11 +++- src/contentPath.test.ts | 41 +++++++++++++++ src/contentPath.ts | 52 +++++++++++++++++++ .../XpathRangeSelector/index.ts | 11 +--- .../XpathRangeSelector/xpath.spec.js | 33 ------------ .../XpathRangeSelector/xpath.ts | 40 ++------------ 6 files changed, 106 insertions(+), 82 deletions(-) create mode 100644 src/contentPath.test.ts create mode 100644 src/contentPath.ts diff --git a/src/SerializedHighlight.ts b/src/SerializedHighlight.ts index 63d12eb..22f88ec 100644 --- a/src/SerializedHighlight.ts +++ b/src/SerializedHighlight.ts @@ -3,7 +3,9 @@ import {Highlight as ApiHighlight, NewHighlight as NewApiHighlight, styleIsColor import Highlight, {IHighlightData} from './Highlight'; import Highlighter from './Highlighter'; import {getDeserializer, IDeserializer, ISerializationData} from './serializationStrategies'; -import {serialize as defaultSerializer} from './serializationStrategies/XpathRangeSelector'; +import { serialize as defaultSerializer, discriminator as xPathDiscriminator } from './serializationStrategies/XpathRangeSelector'; +import { getContentPath } from './contentPath'; +import { XpathRangeSelector } from '@openstax/highlights-client'; const mapKeys = (transform: (key: string) => string, obj: {[key: string]: any}) => Object.keys(obj).reduce((result, key) => ({ ...result, [transform(key)]: obj[key], @@ -83,6 +85,11 @@ export default class SerializedHighlight { const prevHighlight = highlighter.getHighlightBefore(highlight); const nextHighlight = highlighter.getHighlightAfter(highlight); + let contentPath: number[] | undefined; + + if (serializationData.type == xPathDiscriminator) { + contentPath = getContentPath({ referenceElementId, ...serializationData }, highlighter, highlight); + } if (!style) { throw new Error('a style is requred to create an api payload'); } @@ -99,7 +106,7 @@ export default class SerializedHighlight { locationStrategies: [mapKeys(snakeCase, serializationData)], nextHighlightId: nextHighlight && nextHighlight.id, prevHighlightId: prevHighlight && prevHighlight.id, - + contentPath, }; } diff --git a/src/contentPath.test.ts b/src/contentPath.test.ts new file mode 100644 index 0000000..d2db97d --- /dev/null +++ b/src/contentPath.test.ts @@ -0,0 +1,41 @@ +import { getContentPath } from "./contentPath"; +import Highlighter from "./Highlighter"; +import Highlight from "./Highlight"; +import { IData } from "./serializationStrategies/XpathRangeSelector"; +import { adjacentTextSections } from "./injectHighlightWrappers.spec.data"; + +describe('getContentPath', () => { + it('creates a path of indexes', () => { + document.body.innerHTML = `
${adjacentTextSections}
`; + + const container = document.getElementById('page')!; + const element = document.getElementById('test-p')!; + + const highlighter = new Highlighter(container, { formatMessage: jest.fn() }); + const highlightData = { id: 'some-highlight', content: 'asd', style: 'yellow' }; + + const range: any = { + collapse: false, + commonAncestorContainer: container, + setEndAfter: jest.fn(), + setStartBefore: jest.fn(), + endContainer: element.childNodes[0], + endOffset: 10, + startContainer: element.childNodes[0], + startOffset: 4, + }; + + const highlight = new Highlight(range, highlightData, { formatMessage: jest.fn() }); + + const result = getContentPath( + { + referenceElementId: 'test-container', + startContainer: "./*[name()='section'][1]/*[name()='div'][1]/*[name()='p'][1]/text()[1]" + } as IData, + highlighter, + highlight + ); + + expect(result).toEqual([3, 1, 1, 0, 4]); + }); +}); diff --git a/src/contentPath.ts b/src/contentPath.ts new file mode 100644 index 0000000..27cac48 --- /dev/null +++ b/src/contentPath.ts @@ -0,0 +1,52 @@ +import { isText, isTextHighlightOrScreenReaderNode, getFirstByXPath } from './serializationStrategies/XpathRangeSelector/xpath'; +import Highlight from './Highlight'; +import Highlighter from './Highlighter'; +import { IData } from './serializationStrategies/XpathRangeSelector'; + +export function getContentPath(serializationData: IData, highlighter: Highlighter, highlight: Highlight) { + + const referenceElement = highlighter.getReferenceElement(serializationData.referenceElementId); + if (!referenceElement) { return; } + + const element = getFirstByXPath(serializationData.startContainer, highlight.range.startOffset, referenceElement); + if (!element[0]) { return; } + + const nodePath = getNodePath(element[0], referenceElement); + nodePath.push(highlight.range.startOffset); + + return nodePath; +} + +function getNodePath(element: HTMLElement, container: HTMLElement): number[] { + let currentParent: HTMLElement | null = element; + const nodePath: number[] = []; + + // Go up the stack, capturing the index of each node to create a path + while (currentParent != container) { + const currentChild = currentParent; + + if (currentParent.parentElement) { + currentParent = currentParent.parentElement; + let filteredNodes = Array.from(currentParent.childNodes).filter(n => !isTextHighlightOrScreenReaderNode(n)); + + filteredNodes = filteredNodes.filter((current, i) => { + if (current == currentChild) { + // Always include the node with the content to get the index + return true; + } + + const adjacent = filteredNodes[i + 1]; + const isCollapsible = (adjacent && isText(adjacent) && isText(current)); + + // Remove adjacent text nodes + return !isCollapsible; + }); + + const index = filteredNodes.indexOf(currentChild); + nodePath.unshift(index); + } + + } + + return nodePath.length > 0 ? nodePath : [0]; +} diff --git a/src/serializationStrategies/XpathRangeSelector/index.ts b/src/serializationStrategies/XpathRangeSelector/index.ts index 0930bef..1e9e652 100644 --- a/src/serializationStrategies/XpathRangeSelector/index.ts +++ b/src/serializationStrategies/XpathRangeSelector/index.ts @@ -1,5 +1,5 @@ import Highlighter from '../../Highlighter'; -import { getFirstByXPath, getXPathForElement, getNodePath } from './xpath'; +import { getFirstByXPath, getXPathForElement } from './xpath'; export const discriminator = 'XpathRangeSelector'; @@ -10,7 +10,6 @@ export interface IData { startOffset: number; endContainer: string; endOffset: number; - nodePath?: number[]; } export function serialize(range: Range, referenceElement: HTMLElement): IData { @@ -18,20 +17,12 @@ export function serialize(range: Range, referenceElement: HTMLElement): IData { const [endContainer, endOffset] = getXPathForElement(range.endContainer, range.endOffset, referenceElement); const [startContainer, startOffset] = getXPathForElement(range.startContainer, range.startOffset, referenceElement); - let nodePath: number[] = []; - const element = getFirstByXPath(startContainer, range.startOffset, referenceElement); - - if (element[0]) { - nodePath = getNodePath(element[0], referenceElement); - } - return { endContainer, endOffset, referenceElementId: referenceElement.id, startContainer, startOffset, - nodePath, type: discriminator, }; } diff --git a/src/serializationStrategies/XpathRangeSelector/xpath.spec.js b/src/serializationStrategies/XpathRangeSelector/xpath.spec.js index 899594d..a87a285 100644 --- a/src/serializationStrategies/XpathRangeSelector/xpath.spec.js +++ b/src/serializationStrategies/XpathRangeSelector/xpath.spec.js @@ -3,39 +3,6 @@ import * as xpath from './xpath'; const screenReaderNode = ``; -describe('getNodePath', () => { - it('creates a path of indexes', () => { - document.body.innerHTML = ` -
-

Header text

-

Section text

-
- `; - - const container = document.getElementById('1'); - const element = document.getElementById('2'); - const result = xpath.getNodePath(element, container); - expect(result).toEqual([1, 0]); - }); - - it('works with text nodes', () => { - document.body.innerHTML = ` -
-
- Some text before the link - - Text after the link -
-
- `; - - const container = document.getElementById('1'); - const element = xpath.getFirstByXPath('./text()[2]', 0, container); - const result = xpath.getNodePath(element[0], container); - expect(result).toEqual([2]); - }); -}); - describe('getXPathForElement', () => { it('creates path to self', () => { document.body.innerHTML = ` diff --git a/src/serializationStrategies/XpathRangeSelector/xpath.ts b/src/serializationStrategies/XpathRangeSelector/xpath.ts index b4d79f2..1603386 100644 --- a/src/serializationStrategies/XpathRangeSelector/xpath.ts +++ b/src/serializationStrategies/XpathRangeSelector/xpath.ts @@ -9,9 +9,9 @@ const findNonTextChild = (node: Node) => Array.prototype.find.call(node.childNod const isHighlight = (node: Nullable): node is HTMLElement => !!node && (node as Element).getAttribute && (node as Element).getAttribute(DATA_ATTR) !== null; const isHighlightOrScreenReaderNode = (node: Nullable) => isHighlight(node) || isScreenReaderNode(node); const isTextHighlight = (node: Nullable): node is HTMLElement => isHighlight(node) && !findNonTextChild(node); -const isTextHighlightOrScreenReaderNode = (node: Nullable): node is HTMLElement => (isHighlight(node) || isScreenReaderNode(node)) && !findNonTextChild(node); -const isText = (node: Nullable): node is Text => !!node && node.nodeType === 3; -const isTextOrTextHighlight = (node: Nullable): node is Text | HTMLElement => isText(node) || isTextHighlight(node); +export const isTextHighlightOrScreenReaderNode = (node: Nullable): node is HTMLElement => (isHighlight(node) || isScreenReaderNode(node)) && !findNonTextChild(node); +export const isText = (node: Nullable): node is Text => !!node && node.nodeType === 3; +const isTextOrTextHighlight = (node: Nullable): node is Text | HTMLElement => isText(node) || isTextHighlight(node); const isTextOrTextHighlightOrScreenReaderNode = (node: Nullable) => isText(node) || isTextHighlightOrScreenReaderNode(node) || isScreenReaderNode(node); const isElement = (node: Node): node is HTMLElement => node && node.nodeType === 1; const isElementNotHighlight = (node: Node) => isElement(node) && !isHighlight(node); @@ -104,40 +104,6 @@ const resolveToPreviousElementOffsetIfPossible = (element: Node, offset: number) return [element, offset]; }; -export function getNodePath(element: HTMLElement, container: HTMLElement): number[] { - let currentParent: HTMLElement | null = element; - const nodePath: number[] = []; - - // Go up the stack, capturing the index of each node to create a path - while (currentParent != container) { - const currentChild = currentParent; - - if (currentParent.parentElement) { - currentParent = currentParent.parentElement; - let filteredNodes = Array.from(currentParent.childNodes).filter(n => !isTextHighlightOrScreenReaderNode(n)); - - filteredNodes = filteredNodes.filter((current, i) => { - if (current == currentChild) { - // Always include the node with the content to get the index - return true; - } - - const adjacent = filteredNodes[i + 1]; - const isCollapsible = (adjacent && isText(adjacent) && isText(current)); - - // Remove adjacent text nodes - return !isCollapsible; - }); - - const index = filteredNodes.indexOf(currentChild); - nodePath.unshift(index); - } - - } - - return nodePath.length > 0 ? nodePath : [0]; -} - // kinda copied from https://developer.mozilla.org/en-US/docs/Web/XPath/Snippets#getXPathForElement export function getXPathForElement(targetElement: Node, offset: number, reference: HTMLElement): [string, number] { [targetElement, offset] = floatThroughText(targetElement, offset, reference); From 9ebb47ac6978b98e62b8866d4db0d50ba620ddf4 Mon Sep 17 00:00:00 2001 From: Josiah Ivey Date: Wed, 19 Jan 2022 07:16:36 -0800 Subject: [PATCH 3/8] improve contentPath var names --- src/contentPath.ts | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/contentPath.ts b/src/contentPath.ts index 27cac48..8c50bb3 100644 --- a/src/contentPath.ts +++ b/src/contentPath.ts @@ -18,34 +18,31 @@ export function getContentPath(serializationData: IData, highlighter: Highlighte } function getNodePath(element: HTMLElement, container: HTMLElement): number[] { - let currentParent: HTMLElement | null = element; + let currentNode: HTMLElement | null = element; const nodePath: number[] = []; // Go up the stack, capturing the index of each node to create a path - while (currentParent != container) { - const currentChild = currentParent; + while (currentNode != container) { + if (currentNode && currentNode.parentNode) { + let filteredNodes = Array.from(currentNode.parentNode.childNodes).filter(n => !isTextHighlightOrScreenReaderNode(n)); - if (currentParent.parentElement) { - currentParent = currentParent.parentElement; - let filteredNodes = Array.from(currentParent.childNodes).filter(n => !isTextHighlightOrScreenReaderNode(n)); - - filteredNodes = filteredNodes.filter((current, i) => { - if (current == currentChild) { + filteredNodes = filteredNodes.filter((node, i) => { + if (node == currentNode) { // Always include the node with the content to get the index return true; } - const adjacent = filteredNodes[i + 1]; - const isCollapsible = (adjacent && isText(adjacent) && isText(current)); + const nextNode = filteredNodes[i + 1]; + const isCollapsible = (nextNode && isText(nextNode) && isText(node)); // Remove adjacent text nodes return !isCollapsible; }); - const index = filteredNodes.indexOf(currentChild); + const index = filteredNodes.indexOf(currentNode); nodePath.unshift(index); + currentNode = currentNode.parentElement; } - } return nodePath.length > 0 ? nodePath : [0]; From 9a8149425ad28c46fc60dae61ee2f76e81784293 Mon Sep 17 00:00:00 2001 From: Josiah Ivey Date: Fri, 11 Mar 2022 15:15:52 -0800 Subject: [PATCH 4/8] fix getting text offset for contentPath --- src/SerializedHighlight.ts | 20 +++++++------- src/contentPath.test.ts | 55 +++++++++++++++++++++++++------------- src/contentPath.ts | 19 +++++++------ 3 files changed, 58 insertions(+), 36 deletions(-) diff --git a/src/SerializedHighlight.ts b/src/SerializedHighlight.ts index 22f88ec..c2800ff 100644 --- a/src/SerializedHighlight.ts +++ b/src/SerializedHighlight.ts @@ -1,11 +1,10 @@ -import {camelCase, snakeCase} from 'change-case'; -import {Highlight as ApiHighlight, NewHighlight as NewApiHighlight, styleIsColor } from './api'; -import Highlight, {IHighlightData} from './Highlight'; -import Highlighter from './Highlighter'; -import {getDeserializer, IDeserializer, ISerializationData} from './serializationStrategies'; -import { serialize as defaultSerializer, discriminator as xPathDiscriminator } from './serializationStrategies/XpathRangeSelector'; +import { camelCase, snakeCase } from 'change-case'; +import { Highlight as ApiHighlight, NewHighlight as NewApiHighlight, styleIsColor } from './api'; import { getContentPath } from './contentPath'; -import { XpathRangeSelector } from '@openstax/highlights-client'; +import Highlight, { IHighlightData } from './Highlight'; +import Highlighter from './Highlighter'; +import { getDeserializer, IDeserializer, ISerializationData } from './serializationStrategies'; +import { discriminator as xPathDiscriminator, serialize as defaultSerializer } from './serializationStrategies/XpathRangeSelector'; const mapKeys = (transform: (key: string) => string, obj: {[key: string]: any}) => Object.keys(obj).reduce((result, key) => ({ ...result, [transform(key)]: obj[key], @@ -87,9 +86,10 @@ export default class SerializedHighlight { let contentPath: number[] | undefined; - if (serializationData.type == xPathDiscriminator) { - contentPath = getContentPath({ referenceElementId, ...serializationData }, highlighter, highlight); + if (serializationData.type === xPathDiscriminator) { + contentPath = getContentPath({ referenceElementId, ...serializationData }, highlighter); } + if (!style) { throw new Error('a style is requred to create an api payload'); } @@ -101,12 +101,12 @@ export default class SerializedHighlight { anchor: referenceElementId, annotation, color: style, + contentPath, highlightedContent: content, id, locationStrategies: [mapKeys(snakeCase, serializationData)], nextHighlightId: nextHighlight && nextHighlight.id, prevHighlightId: prevHighlight && prevHighlight.id, - contentPath, }; } diff --git a/src/contentPath.test.ts b/src/contentPath.test.ts index d2db97d..ae85155 100644 --- a/src/contentPath.test.ts +++ b/src/contentPath.test.ts @@ -1,41 +1,60 @@ -import { getContentPath } from "./contentPath"; -import Highlighter from "./Highlighter"; -import Highlight from "./Highlight"; -import { IData } from "./serializationStrategies/XpathRangeSelector"; -import { adjacentTextSections } from "./injectHighlightWrappers.spec.data"; +import { getContentPath } from './contentPath'; +import Highlighter from './Highlighter'; +import { adjacentTextSections } from './injectHighlightWrappers.spec.data'; +import { IData } from './serializationStrategies/XpathRangeSelector'; describe('getContentPath', () => { it('creates a path of indexes', () => { document.body.innerHTML = `
${adjacentTextSections}
`; const container = document.getElementById('page')!; - const element = document.getElementById('test-p')!; + const highlighter = new Highlighter(container, { formatMessage: jest.fn() }); + + const result = getContentPath( + { + referenceElementId: 'test-container', + // tslint:disable-next-line quotemark + startContainer: "./*[name()='section'][1]/*[name()='div'][1]/*[name()='p'][1]/text()[1]", + startOffset: 4, + } as IData, + highlighter + ); + expect(result).toEqual([3, 1, 1, 0, 4]); + }); + + it('handles adjacent text highlights', () => { + document.body.innerHTML = `
+
+

+ A shortcut called FOIL is sometimes used to find the product of two binomials. +

+
+
`; + + const container = document.getElementById('test-container')!; const highlighter = new Highlighter(container, { formatMessage: jest.fn() }); - const highlightData = { id: 'some-highlight', content: 'asd', style: 'yellow' }; const range: any = { collapse: false, commonAncestorContainer: container, + endContainer: './text()[1]', + endOffset: 31, setEndAfter: jest.fn(), setStartBefore: jest.fn(), - endContainer: element.childNodes[0], - endOffset: 10, - startContainer: element.childNodes[0], - startOffset: 4, + startContainer: './text()[1]', + startOffset: 27, }; - const highlight = new Highlight(range, highlightData, { formatMessage: jest.fn() }); - const result = getContentPath( { - referenceElementId: 'test-container', - startContainer: "./*[name()='section'][1]/*[name()='div'][1]/*[name()='p'][1]/text()[1]" + referenceElementId: 'test-p', + startContainer: range.startContainer, + startOffset: range.startOffset, } as IData, - highlighter, - highlight + highlighter ); - expect(result).toEqual([3, 1, 1, 0, 4]); + expect(result).toEqual([0, 27]); }); }); diff --git a/src/contentPath.ts b/src/contentPath.ts index 8c50bb3..314255c 100644 --- a/src/contentPath.ts +++ b/src/contentPath.ts @@ -1,18 +1,21 @@ -import { isText, isTextHighlightOrScreenReaderNode, getFirstByXPath } from './serializationStrategies/XpathRangeSelector/xpath'; -import Highlight from './Highlight'; import Highlighter from './Highlighter'; import { IData } from './serializationStrategies/XpathRangeSelector'; +import { getFirstByXPath, isText, isTextHighlightOrScreenReaderNode } from './serializationStrategies/XpathRangeSelector/xpath'; -export function getContentPath(serializationData: IData, highlighter: Highlighter, highlight: Highlight) { +export function getContentPath(serializationData: IData, highlighter: Highlighter) { const referenceElement = highlighter.getReferenceElement(serializationData.referenceElementId); if (!referenceElement) { return; } - const element = getFirstByXPath(serializationData.startContainer, highlight.range.startOffset, referenceElement); + const element = getFirstByXPath( + serializationData.startContainer, + serializationData.startOffset, + referenceElement + ); if (!element[0]) { return; } const nodePath = getNodePath(element[0], referenceElement); - nodePath.push(highlight.range.startOffset); + nodePath.push(serializationData.startOffset); return nodePath; } @@ -22,12 +25,12 @@ function getNodePath(element: HTMLElement, container: HTMLElement): number[] { const nodePath: number[] = []; // Go up the stack, capturing the index of each node to create a path - while (currentNode != container) { + while (currentNode !== container) { if (currentNode && currentNode.parentNode) { - let filteredNodes = Array.from(currentNode.parentNode.childNodes).filter(n => !isTextHighlightOrScreenReaderNode(n)); + let filteredNodes = Array.from(currentNode.parentNode.childNodes).filter((n) => !isTextHighlightOrScreenReaderNode(n)); filteredNodes = filteredNodes.filter((node, i) => { - if (node == currentNode) { + if (node === currentNode) { // Always include the node with the content to get the index return true; } From 7ebe33df73ebb22bb4c88b6ad3c8b00dabfb912c Mon Sep 17 00:00:00 2001 From: Josiah Ivey Date: Tue, 15 Mar 2022 15:49:23 -0700 Subject: [PATCH 5/8] fix type errors --- src/serializationStrategies/XpathRangeSelector/xpath.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/serializationStrategies/XpathRangeSelector/xpath.ts b/src/serializationStrategies/XpathRangeSelector/xpath.ts index 1603386..5d8a50e 100644 --- a/src/serializationStrategies/XpathRangeSelector/xpath.ts +++ b/src/serializationStrategies/XpathRangeSelector/xpath.ts @@ -87,7 +87,7 @@ const floatThroughText = (element: Node, offset: number, container: Node): [Node } }; -const resolveToNextElementOffsetIfPossible = (element: Node, offset: number) => { +const resolveToNextElementOffsetIfPossible = (element: Node, offset: number): [Node, number] => { if (isTextOrTextHighlightOrScreenReaderNode(element) && element.parentNode && offset === getMaxOffset(element) && (!element.nextSibling || !isHighlightOrScreenReaderNode(element.nextSibling))) { return [element.parentNode, nodeIndex(element.parentNode.childNodes, element) + 1]; } @@ -95,7 +95,7 @@ const resolveToNextElementOffsetIfPossible = (element: Node, offset: number) => return [element, offset]; }; -const resolveToPreviousElementOffsetIfPossible = (element: Node, offset: number) => { +const resolveToPreviousElementOffsetIfPossible = (element: Node, offset: number): [Node, number] => { if (isTextOrTextHighlightOrScreenReaderNode(element) && element.parentNode && offset === 0 && (!element.previousSibling || !isHighlightOrScreenReaderNode(element.previousSibling))) { return [element.parentNode, nodeIndex(element.parentNode.childNodes, element)]; @@ -234,7 +234,7 @@ function followPart(node: Node, part: string) { const findFirst = (nodeList: NodeList, predicate: (node: Node) => boolean) => Array.prototype.find.call(nodeList, (node: Node) => predicate(node)); const findFirstAfter = (nodeList: NodeList, afterThis: Node, predicate: (node: Node) => boolean) => findFirst( - Array.prototype.slice.call(nodeList, Array.prototype.indexOf.call(nodeList, afterThis) + 1), + Array.prototype.slice.call(nodeList, Array.prototype.indexOf.call(nodeList, afterThis) + 1) as unknown as NodeList, predicate ); From 6cf0fd36aef1d883872143101c4257ab8569f076 Mon Sep 17 00:00:00 2001 From: Josiah Ivey Date: Tue, 16 Aug 2022 20:49:15 -0700 Subject: [PATCH 6/8] add text strategy --- src/SerializedHighlight.ts | 6 +- src/contentPath.test.ts | 128 +++++++++++++++++++++++-------------- src/contentPath.ts | 8 ++- 3 files changed, 88 insertions(+), 54 deletions(-) diff --git a/src/SerializedHighlight.ts b/src/SerializedHighlight.ts index c2800ff..fd6abed 100644 --- a/src/SerializedHighlight.ts +++ b/src/SerializedHighlight.ts @@ -4,7 +4,7 @@ import { getContentPath } from './contentPath'; import Highlight, { IHighlightData } from './Highlight'; import Highlighter from './Highlighter'; import { getDeserializer, IDeserializer, ISerializationData } from './serializationStrategies'; -import { discriminator as xPathDiscriminator, serialize as defaultSerializer } from './serializationStrategies/XpathRangeSelector'; +import { serialize as defaultSerializer } from './serializationStrategies/XpathRangeSelector'; const mapKeys = (transform: (key: string) => string, obj: {[key: string]: any}) => Object.keys(obj).reduce((result, key) => ({ ...result, [transform(key)]: obj[key], @@ -86,9 +86,7 @@ export default class SerializedHighlight { let contentPath: number[] | undefined; - if (serializationData.type === xPathDiscriminator) { - contentPath = getContentPath({ referenceElementId, ...serializationData }, highlighter); - } + contentPath = getContentPath({ referenceElementId, ...serializationData }, highlighter); if (!style) { throw new Error('a style is requred to create an api payload'); diff --git a/src/contentPath.test.ts b/src/contentPath.test.ts index ae85155..b746cba 100644 --- a/src/contentPath.test.ts +++ b/src/contentPath.test.ts @@ -1,60 +1,90 @@ import { getContentPath } from './contentPath'; import Highlighter from './Highlighter'; import { adjacentTextSections } from './injectHighlightWrappers.spec.data'; -import { IData } from './serializationStrategies/XpathRangeSelector'; +import { IData as TextPositionData } from './serializationStrategies/TextPositionSelector'; +import { IData as XpathRangeData } from './serializationStrategies/XpathRangeSelector'; describe('getContentPath', () => { - it('creates a path of indexes', () => { - document.body.innerHTML = `
${adjacentTextSections}
`; - - const container = document.getElementById('page')!; - const highlighter = new Highlighter(container, { formatMessage: jest.fn() }); - - const result = getContentPath( - { - referenceElementId: 'test-container', - // tslint:disable-next-line quotemark - startContainer: "./*[name()='section'][1]/*[name()='div'][1]/*[name()='p'][1]/text()[1]", - startOffset: 4, - } as IData, - highlighter - ); - - expect(result).toEqual([3, 1, 1, 0, 4]); - }); + describe('with XpathRangeSelector type', () => { + it('creates a path of indexes', () => { + document.body.innerHTML = `
${adjacentTextSections}
`; + + const container = document.getElementById('page')!; + const highlighter = new Highlighter(container, { formatMessage: jest.fn() }); + + const result = getContentPath( + { + referenceElementId: 'test-container', + // tslint:disable-next-line quotemark + startContainer: "./*[name()='section'][1]/*[name()='div'][1]/*[name()='p'][1]/text()[1]", + startOffset: 4, + type: 'XpathRangeSelector', + } as XpathRangeData, + highlighter + ); + + expect(result).toEqual([3, 1, 1, 0, 4]); + }); - it('handles adjacent text highlights', () => { - document.body.innerHTML = `
+ it('handles adjacent text highlights', () => { + document.body.innerHTML = `
-

- A shortcut called FOIL is sometimes used to find the product of two binomials. +

+ A shortcut called FOIL is sometimes used to find the product of two binomials.

-
`; - - const container = document.getElementById('test-container')!; - const highlighter = new Highlighter(container, { formatMessage: jest.fn() }); - - const range: any = { - collapse: false, - commonAncestorContainer: container, - endContainer: './text()[1]', - endOffset: 31, - setEndAfter: jest.fn(), - setStartBefore: jest.fn(), - startContainer: './text()[1]', - startOffset: 27, - }; - - const result = getContentPath( - { - referenceElementId: 'test-p', - startContainer: range.startContainer, - startOffset: range.startOffset, - } as IData, - highlighter - ); - - expect(result).toEqual([0, 27]); +
`; + + const container = document.getElementById('test-container')!; + const highlighter = new Highlighter(container, { formatMessage: jest.fn() }); + + const range: any = { + collapse: false, + commonAncestorContainer: container, + endContainer: './text()[1]', + endOffset: 31, + setEndAfter: jest.fn(), + setStartBefore: jest.fn(), + startContainer: './text()[1]', + startOffset: 27, + }; + + const result = getContentPath( + { + referenceElementId: 'test-p', + startContainer: range.startContainer, + startOffset: range.startOffset, + type: 'XpathRangeSelector', + } as XpathRangeData, + highlighter + ); + + expect(result).toEqual([0, 27]); + }); + }); + + describe('with TextPositionSelector type', () => { + it('returns a simplified path and offset', () => { + document.body.innerHTML = `
+
+ A shortcut called FOIL is sometimes used to find the product of two binomials. +
+
`; + + const container = document.getElementById('page')!; + const highlighter = new Highlighter(container, { formatMessage: jest.fn() }); + + const result = getContentPath( + { + end: 11, + referenceElementId: 'test-container', + start: 3, + type: 'TextPositionSelector', + } as TextPositionData, + highlighter + ); + + expect(result).toEqual([0, 3]); + }); }); }); diff --git a/src/contentPath.ts b/src/contentPath.ts index 314255c..7741bd4 100644 --- a/src/contentPath.ts +++ b/src/contentPath.ts @@ -1,8 +1,14 @@ import Highlighter from './Highlighter'; -import { IData } from './serializationStrategies/XpathRangeSelector'; +import { IData as TextPositionData } from './serializationStrategies/TextPositionSelector'; +import { IData as XpathData } from './serializationStrategies/XpathRangeSelector'; import { getFirstByXPath, isText, isTextHighlightOrScreenReaderNode } from './serializationStrategies/XpathRangeSelector/xpath'; +type IData = XpathData | TextPositionData; + export function getContentPath(serializationData: IData, highlighter: Highlighter) { + if (serializationData.type === 'TextPositionSelector') { + return [0, serializationData.start]; + } const referenceElement = highlighter.getReferenceElement(serializationData.referenceElementId); if (!referenceElement) { return; } From f459f699c0cc3a876ee7bbb3f08250697d188dfa Mon Sep 17 00:00:00 2001 From: Josiah Ivey Date: Wed, 17 Aug 2022 11:00:43 -0700 Subject: [PATCH 7/8] fix test --- src/contentPath.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/contentPath.test.ts b/src/contentPath.test.ts index b746cba..ffe724c 100644 --- a/src/contentPath.test.ts +++ b/src/contentPath.test.ts @@ -29,11 +29,11 @@ describe('getContentPath', () => { it('handles adjacent text highlights', () => { document.body.innerHTML = `
-

- A shortcut called FOIL is sometimes used to find the product of two binomials. +

+ A shortcut called FOIL is sometimes used to find the product of two binomials.

-
`; + `; const container = document.getElementById('test-container')!; const highlighter = new Highlighter(container, { formatMessage: jest.fn() }); From 04b88e028f11329fe0d1e3a43044d0abeab9e3ef Mon Sep 17 00:00:00 2001 From: Josiah Ivey Date: Wed, 17 Aug 2022 11:01:13 -0700 Subject: [PATCH 8/8] don't switch typescript version yet --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 6b9acee..208f9f9 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "ts-loader": "^5.2.1", "tslint": "^5.11.0", "tslint-loader": "^3.6.0", - "typescript": "3.5", + "typescript": "^3.1.1", "typescript-babel-jest": "^1.0.5" }, "resolutions": { diff --git a/yarn.lock b/yarn.lock index d766b61..996f33f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5195,16 +5195,16 @@ typescript-babel-jest@^1.0.5: babel-jest "20.0.3" typescript "^2.4.1" -typescript@3.5: - version "3.5.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" - integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== - typescript@^2.4.1: version "2.9.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c" integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w== +typescript@^3.1.1: + version "3.9.10" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" + integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== + uglify-js@^3.1.4: version "3.13.5" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.5.tgz#5d71d6dbba64cf441f32929b1efce7365bb4f113"