Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions src/SerializedHighlight.ts
Original file line number Diff line number Diff line change
@@ -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],
Expand Down Expand Up @@ -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');
}
Expand All @@ -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,

};
}

Expand Down
90 changes: 90 additions & 0 deletions src/contentPath.test.ts
Original file line number Diff line number Diff line change
@@ -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 = `<div id="page">${adjacentTextSections}</div>`;

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 = `<div id="page">
<div id="test-container">
<p id="test-p">
<span data-highlighted>A shortcut called</span> FOIL is sometimes used to find the product of two binomials.
</p>
</div>
</div>`;

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 = `<div id="page">
<div id="test-container">
A shortcut called FOIL is sometimes used to find the product of two binomials.
</div>
</div>`;

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]);
});
});
});
58 changes: 58 additions & 0 deletions src/contentPath.ts
Original file line number Diff line number Diff line change
@@ -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];
}
12 changes: 6 additions & 6 deletions src/serializationStrategies/XpathRangeSelector/xpath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ const findNonTextChild = (node: Node) => Array.prototype.find.call(node.childNod
const isHighlight = (node: Nullable<Node>): node is HTMLElement => !!node && (node as Element).getAttribute && (node as Element).getAttribute(DATA_ATTR) !== null;
const isHighlightOrScreenReaderNode = (node: Nullable<Node>) => isHighlight(node) || isScreenReaderNode(node);
const isTextHighlight = (node: Nullable<Node>): node is HTMLElement => isHighlight(node) && !findNonTextChild(node);
const isTextHighlightOrScreenReaderNode = (node: Nullable<Node>): node is HTMLElement => (isHighlight(node) || isScreenReaderNode(node)) && !findNonTextChild(node);
const isText = (node: Nullable<Node>): node is Text => !!node && node.nodeType === 3;
const isTextOrTextHighlight = (node: Nullable<Node>): node is Text | HTMLElement => isText(node) || isTextHighlight(node);
export const isTextHighlightOrScreenReaderNode = (node: Nullable<Node>): node is HTMLElement => (isHighlight(node) || isScreenReaderNode(node)) && !findNonTextChild(node);
export const isText = (node: Nullable<Node>): node is Text => !!node && node.nodeType === 3;
const isTextOrTextHighlight = (node: Nullable<Node>): node is Text | HTMLElement => isText(node) || isTextHighlight(node);
const isTextOrTextHighlightOrScreenReaderNode = (node: Nullable<Node | HTMLElement>) => 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);
Expand Down Expand Up @@ -87,15 +87,15 @@ 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];
}

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)];
Expand Down Expand Up @@ -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
);

Expand Down
6 changes: 3 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down