diff --git a/src/SerializedHighlight.ts b/src/SerializedHighlight.ts
index 63d12eb..fd6abed 100644
--- a/src/SerializedHighlight.ts
+++ b/src/SerializedHighlight.ts
@@ -1,9 +1,10 @@
-import {camelCase, snakeCase} from 'change-case';
-import {Highlight as ApiHighlight, NewHighlight as NewApiHighlight, styleIsColor } from './api';
-import Highlight, {IHighlightData} from './Highlight';
+import { camelCase, snakeCase } from 'change-case';
+import { Highlight as ApiHighlight, NewHighlight as NewApiHighlight, styleIsColor } from './api';
+import { getContentPath } from './contentPath';
+import Highlight, { IHighlightData } from './Highlight';
import Highlighter from './Highlighter';
-import {getDeserializer, IDeserializer, ISerializationData} from './serializationStrategies';
-import {serialize as defaultSerializer} from './serializationStrategies/XpathRangeSelector';
+import { getDeserializer, IDeserializer, ISerializationData } from './serializationStrategies';
+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],
@@ -83,6 +84,10 @@ export default class SerializedHighlight {
const prevHighlight = highlighter.getHighlightBefore(highlight);
const nextHighlight = highlighter.getHighlightAfter(highlight);
+ let contentPath: number[] | undefined;
+
+ contentPath = getContentPath({ referenceElementId, ...serializationData }, highlighter);
+
if (!style) {
throw new Error('a style is requred to create an api payload');
}
@@ -94,12 +99,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,
-
};
}
diff --git a/src/contentPath.test.ts b/src/contentPath.test.ts
new file mode 100644
index 0000000..ffe724c
--- /dev/null
+++ b/src/contentPath.test.ts
@@ -0,0 +1,90 @@
+import { getContentPath } from './contentPath';
+import Highlighter from './Highlighter';
+import { adjacentTextSections } from './injectHighlightWrappers.spec.data';
+import { IData as TextPositionData } from './serializationStrategies/TextPositionSelector';
+import { IData as XpathRangeData } from './serializationStrategies/XpathRangeSelector';
+
+describe('getContentPath', () => {
+ 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 = `
+
+
+ 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,
+ 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
new file mode 100644
index 0000000..7741bd4
--- /dev/null
+++ b/src/contentPath.ts
@@ -0,0 +1,58 @@
+import Highlighter from './Highlighter';
+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; }
+
+ const element = getFirstByXPath(
+ serializationData.startContainer,
+ serializationData.startOffset,
+ referenceElement
+ );
+ if (!element[0]) { return; }
+
+ const nodePath = getNodePath(element[0], referenceElement);
+ nodePath.push(serializationData.startOffset);
+
+ return nodePath;
+}
+
+function getNodePath(element: HTMLElement, container: HTMLElement): number[] {
+ let currentNode: HTMLElement | null = element;
+ const nodePath: number[] = [];
+
+ // Go up the stack, capturing the index of each node to create a path
+ while (currentNode !== container) {
+ if (currentNode && currentNode.parentNode) {
+ let filteredNodes = Array.from(currentNode.parentNode.childNodes).filter((n) => !isTextHighlightOrScreenReaderNode(n));
+
+ filteredNodes = filteredNodes.filter((node, i) => {
+ if (node === currentNode) {
+ // Always include the node with the content to get the index
+ return true;
+ }
+
+ const nextNode = filteredNodes[i + 1];
+ const isCollapsible = (nextNode && isText(nextNode) && isText(node));
+
+ // Remove adjacent text nodes
+ return !isCollapsible;
+ });
+
+ const index = filteredNodes.indexOf(currentNode);
+ nodePath.unshift(index);
+ currentNode = currentNode.parentElement;
+ }
+ }
+
+ return nodePath.length > 0 ? nodePath : [0];
+}
diff --git a/src/serializationStrategies/XpathRangeSelector/xpath.ts b/src/serializationStrategies/XpathRangeSelector/xpath.ts
index ec794fb..5d8a50e 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);
@@ -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
);
diff --git a/yarn.lock b/yarn.lock
index f9835e5..996f33f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5201,9 +5201,9 @@ typescript@^2.4.1:
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==
+ 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"