openreader/src/lib/client/epub/epub-range-context.ts

166 lines
4.8 KiB
TypeScript

'use client';
const EPUB_CONTINUATION_CHARS = 5000;
const stepToNextNode = (node: Node | null, root: Node): Node | null => {
if (!node) return null;
if (node.firstChild) return node.firstChild;
let current: Node | null = node;
while (current) {
if (current === root) return null;
if (current.nextSibling) return current.nextSibling;
current = current.parentNode;
}
return null;
};
const stepToPreviousNode = (node: Node | null, root: Node): Node | null => {
if (!node) return null;
if (node.previousSibling) {
let prev: Node | null = node.previousSibling;
while (prev?.lastChild) prev = prev.lastChild;
return prev;
}
let current: Node | null = node.parentNode;
while (current) {
if (current === root) return null;
if (current.previousSibling) {
let prev: Node | null = current.previousSibling;
while (prev?.lastChild) prev = prev.lastChild;
return prev;
}
current = current.parentNode;
}
return null;
};
const getNextTextNode = (node: Node | null, root: Node): Text | null => {
let next = stepToNextNode(node, root);
while (next) {
if (next.nodeType === Node.TEXT_NODE) return next as Text;
next = stepToNextNode(next, root);
}
return null;
};
const getPreviousTextNode = (node: Node | null, root: Node): Text | null => {
let prev = stepToPreviousNode(node, root);
while (prev) {
if (prev.nodeType === Node.TEXT_NODE) return prev as Text;
prev = stepToPreviousNode(prev, root);
}
return null;
};
const getLastTextNodeInSubtree = (node: Node | null): Text | null => {
if (!node) return null;
if (node.nodeType === Node.TEXT_NODE) return node as Text;
let child: Node | null = node.lastChild;
while (child) {
const nested = getLastTextNodeInSubtree(child);
if (nested) return nested;
child = child.previousSibling;
}
return null;
};
export const collectContinuationFromRange = (
range: Range | null | undefined,
limit = EPUB_CONTINUATION_CHARS,
): string => {
if (typeof window === 'undefined' || !range) return '';
const root = range.commonAncestorContainer;
if (!root) return '';
const parts: string[] = [];
let remaining = limit;
const appendFromTextNode = (textNode: Text, offset: number) => {
if (remaining <= 0) return;
const textContent = textNode.textContent || '';
if (offset >= textContent.length) return;
const slice = textContent.slice(offset, offset + remaining);
if (slice) {
parts.push(slice);
remaining -= slice.length;
}
};
if (range.endContainer.nodeType === Node.TEXT_NODE) {
appendFromTextNode(range.endContainer as Text, range.endOffset);
let nextNode = getNextTextNode(range.endContainer, root);
while (nextNode && remaining > 0) {
appendFromTextNode(nextNode, 0);
nextNode = getNextTextNode(nextNode, root);
}
} else {
let nextNode = getNextTextNode(range.endContainer, root);
while (nextNode && remaining > 0) {
appendFromTextNode(nextNode, 0);
nextNode = getNextTextNode(nextNode, root);
}
}
return parts.join(' ').replace(/\s+/g, ' ').trim();
};
export const collectLeadingContextFromRange = (
range: Range | null | undefined,
limit = EPUB_CONTINUATION_CHARS,
): string => {
if (typeof window === 'undefined' || !range) return '';
const root = range.commonAncestorContainer;
if (!root) return '';
const parts: string[] = [];
let remaining = limit;
const prependFromTextNode = (textNode: Text, endOffset: number) => {
if (remaining <= 0) return;
const textContent = textNode.textContent || '';
const safeEnd = Math.max(0, Math.min(endOffset, textContent.length));
if (safeEnd <= 0) return;
const safeStart = Math.max(0, safeEnd - remaining);
const slice = textContent.slice(safeStart, safeEnd);
if (slice) {
parts.unshift(slice);
remaining -= slice.length;
}
};
let cursor: Node | null = null;
if (range.startContainer.nodeType === Node.TEXT_NODE) {
const startText = range.startContainer as Text;
prependFromTextNode(startText, range.startOffset);
cursor = startText;
} else {
const startNode = range.startContainer;
let anchor: Node | null = null;
if (range.startOffset > 0) {
anchor = startNode.childNodes[range.startOffset - 1] ?? null;
}
if (!anchor) {
anchor = stepToPreviousNode(startNode, root);
}
const anchorText = getLastTextNodeInSubtree(anchor);
if (anchorText) {
prependFromTextNode(anchorText, (anchorText.textContent || '').length);
cursor = anchorText;
} else {
cursor = anchor;
}
}
let prevNode = getPreviousTextNode(cursor, root);
while (prevNode && remaining > 0) {
prependFromTextNode(prevNode, (prevNode.textContent || '').length);
prevNode = getPreviousTextNode(prevNode, root);
}
return parts.join(' ').replace(/\s+/g, ' ').trim();
};