forked from akai/readest
@@ -88,7 +88,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
|
||||
expect(originalCfi).toEqual(convertedCfi);
|
||||
expect(xpointer).toEqual({
|
||||
xpointer: '/body/DocFragment[2]/body/div/p[0]',
|
||||
xpointer: '/body/DocFragment[2]/body/div/p[1]',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,7 +99,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
|
||||
expect(originalCfi).toEqual(convertedCfi);
|
||||
expect(xpointer).toEqual({
|
||||
xpointer: '/body/DocFragment[2]/body/div/p[1]',
|
||||
xpointer: '/body/DocFragment[2]/body/div/p[2]',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -110,7 +110,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
|
||||
expect(originalCfi).toEqual(convertedCfi);
|
||||
expect(xpointer).toEqual({
|
||||
xpointer: '/body/DocFragment[2]/body/div/p[2]',
|
||||
xpointer: '/body/DocFragment[2]/body/div/p[3]',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -126,9 +126,9 @@ describe('CFIToXPointerConverter', () => {
|
||||
const convertedCfi = converter.xPointerToCFI(xpointer.pos0!, xpointer.pos1!);
|
||||
|
||||
expect(originalCfi).toEqual(convertedCfi);
|
||||
expect(xpointer.xpointer).toEqual('/body/DocFragment[3]/body/div/p[0]/text().6');
|
||||
expect(xpointer.pos0).toEqual('/body/DocFragment[3]/body/div/p[0]/text().6');
|
||||
expect(xpointer.pos1).toEqual('/body/DocFragment[3]/body/div/p[1]/text().16');
|
||||
expect(xpointer.xpointer).toEqual('/body/DocFragment[3]/body/div/p[1]/text().6');
|
||||
expect(xpointer.pos0).toEqual('/body/DocFragment[3]/body/div/p[1]/text().6');
|
||||
expect(xpointer.pos1).toEqual('/body/DocFragment[3]/body/div/p[2]/text().16');
|
||||
});
|
||||
|
||||
it('should convert range CFI within same element', () => {
|
||||
@@ -147,8 +147,8 @@ describe('CFIToXPointerConverter', () => {
|
||||
const convertedCfi = converter.xPointerToCFI(xpointer.pos0!, xpointer.pos1!);
|
||||
|
||||
expect(originalCfi).toEqual(convertedCfi);
|
||||
expect(xpointer.pos0).toMatch(/p\[0\]/);
|
||||
expect(xpointer.pos1).toMatch(/p\[2\]/);
|
||||
expect(xpointer.pos0).toMatch(/p\[1\]/);
|
||||
expect(xpointer.pos1).toMatch(/p\[3\]/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -163,7 +163,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
const convertedCfi = converter.xPointerToCFI(xpointer.xpointer);
|
||||
|
||||
expect(originalCfi).toEqual(convertedCfi);
|
||||
expect(xpointer.xpointer).toMatch(/\/body\/section\[0\]/);
|
||||
expect(xpointer.xpointer).toMatch(/\/body\/section\[1\]/);
|
||||
});
|
||||
|
||||
it('should handle elements with IDs', () => {
|
||||
@@ -172,7 +172,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
const convertedCfi = converter.xPointerToCFI(xpointer.xpointer);
|
||||
|
||||
expect(originalCfi).toEqual(convertedCfi);
|
||||
expect(xpointer.xpointer).toMatch(/\/body\/section\[1\]\/p\[0\]/);
|
||||
expect(xpointer.xpointer).toMatch(/\/body\/section\[2\]\/p\[1\]/);
|
||||
});
|
||||
|
||||
it('should handle inline elements', () => {
|
||||
@@ -181,7 +181,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
const convertedCfi = converter.xPointerToCFI(xpointer.xpointer);
|
||||
|
||||
expect(originalCfi).toEqual(convertedCfi);
|
||||
expect(xpointer.xpointer).toMatch(/\/body\/section\[1\]\/p\[1\]/);
|
||||
expect(xpointer.xpointer).toMatch(/\/body\/section\[2\]\/p\[2\]/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -212,7 +212,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
});
|
||||
|
||||
it('should convert XPointer to CFI for first element', () => {
|
||||
const xpointer = '/body/DocFragment[2]/body/div/p[0]';
|
||||
const xpointer = '/body/DocFragment[2]/body/div/p[1]';
|
||||
const cfi = converter.xPointerToCFI(xpointer);
|
||||
|
||||
// Verify by converting back to XPointer
|
||||
@@ -221,7 +221,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
});
|
||||
|
||||
it('should convert XPointer to CFI for second element', () => {
|
||||
const xpointer = '/body/DocFragment[2]/body/div/p[1]';
|
||||
const xpointer = '/body/DocFragment[2]/body/div/p[2]';
|
||||
const cfi = converter.xPointerToCFI(xpointer);
|
||||
|
||||
const backToXPointer = converter.cfiToXPointer(cfi);
|
||||
@@ -229,14 +229,14 @@ describe('CFIToXPointerConverter', () => {
|
||||
});
|
||||
|
||||
it('should convert XPointer with text offset to CFI', () => {
|
||||
const xpointer = '/body/DocFragment[2]/body/div[0]/p[0]/text().6';
|
||||
const xpointer = '/body/DocFragment[2]/body/div[0]/p[1]/text().6';
|
||||
const cfi = converter.xPointerToCFI(xpointer);
|
||||
expect(cfi).toBe('epubcfi(/6/4!/4/2/2/1:6)');
|
||||
});
|
||||
|
||||
it('should convert range XPointer to CFI', () => {
|
||||
const pos0 = '/body/DocFragment[2]/body/div/p[0]/text().6';
|
||||
const pos1 = '/body/DocFragment[2]/body/div/p[1]/text().16';
|
||||
const pos0 = '/body/DocFragment[2]/body/div/p[1]/text().6';
|
||||
const pos1 = '/body/DocFragment[2]/body/div/p[2]/text().16';
|
||||
const cfi = converter.xPointerToCFI(pos0, pos1);
|
||||
const xpointer = converter.cfiToXPointer(cfi);
|
||||
|
||||
@@ -269,7 +269,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
it('should handle CFI without spine step prefix', () => {
|
||||
// Test the adjustSpineIndex method handles CFIs that don't start with /6/n!
|
||||
const converter = new XCFI(simpleDoc, 3); // Use different spine index
|
||||
const xpointer = '/body/DocFragment[4]/body/div/p[0]';
|
||||
const xpointer = '/body/DocFragment[4]/body/div/p[1]';
|
||||
const cfi = converter.xPointerToCFI(xpointer);
|
||||
|
||||
// Verify the spine step is correctly added/adjusted
|
||||
@@ -331,7 +331,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
const cfi = 'epubcfi(/6/6!/4/2/2)'; // Empty p element
|
||||
const result = converter.cfiToXPointer(cfi);
|
||||
|
||||
expect(result.xpointer).toBe('/body/DocFragment[3]/body/div/p[0]');
|
||||
expect(result.xpointer).toBe('/body/DocFragment[3]/body/div/p[1]');
|
||||
});
|
||||
|
||||
it('should handle whitespace-only text nodes', () => {
|
||||
@@ -353,7 +353,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
const cfi = 'epubcfi(/6/6!/4/2/4)'; // Second p element
|
||||
const result = converter.cfiToXPointer(cfi);
|
||||
|
||||
expect(result.xpointer).toBe('/body/DocFragment[3]/body/div/p[1]');
|
||||
expect(result.xpointer).toBe('/body/DocFragment[3]/body/div/p[2]');
|
||||
});
|
||||
|
||||
it('should handle deeply nested elements', () => {
|
||||
@@ -379,7 +379,7 @@ describe('CFIToXPointerConverter', () => {
|
||||
const cfi = 'epubcfi(/6/6!/4/2/2/2/2)'; // Deeply nested p
|
||||
const result = converter.cfiToXPointer(cfi);
|
||||
|
||||
expect(result.xpointer).toBe('/body/DocFragment[3]/body/div/section/article/p[0]');
|
||||
expect(result.xpointer).toBe('/body/DocFragment[3]/body/div/section/article/p[1]');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,12 +96,8 @@ export const useKOSync = (bookKey: string) => {
|
||||
if (!remote.progress?.startsWith('/body')) return;
|
||||
try {
|
||||
const content = view?.renderer.getContents()[0];
|
||||
const cfi = await getCFIFromXPointer(
|
||||
remote.progress!,
|
||||
content?.doc,
|
||||
content?.index,
|
||||
bookDoc,
|
||||
);
|
||||
const koProgress = normalizeProgressXPointer(remote.progress);
|
||||
const cfi = await getCFIFromXPointer(koProgress, content?.doc, content?.index, bookDoc);
|
||||
view?.goTo(cfi);
|
||||
} catch (error) {
|
||||
console.error('Failed to convert XPointer to CFI', error);
|
||||
@@ -242,6 +238,7 @@ export const useKOSync = (bookKey: string) => {
|
||||
const handlePushProgress = (event: CustomEvent) => {
|
||||
if (event.detail.bookKey !== bookKey) return;
|
||||
pushProgress();
|
||||
pushProgress.flush();
|
||||
};
|
||||
const handleFlush = (event: CustomEvent) => {
|
||||
if (event.detail.bookKey !== bookKey) return;
|
||||
|
||||
@@ -116,8 +116,9 @@ export const useProgressSync = (bookKey: string) => {
|
||||
const view = getView(bookKey);
|
||||
if (xPointer && view && bookData && bookData.bookDoc) {
|
||||
const content = view.renderer.getContents()[0];
|
||||
const koProgress = normalizeProgressXPointer(xPointer);
|
||||
const candidateCFI = await getCFIFromXPointer(
|
||||
xPointer,
|
||||
koProgress,
|
||||
content?.doc,
|
||||
content?.index,
|
||||
bookData.bookDoc,
|
||||
|
||||
@@ -35,6 +35,8 @@ export class XCFI {
|
||||
// Convert to 0-based: (step - 2) / 2 = 0, 1, 2, 3, ...
|
||||
return Math.floor((spineStep - 2) / 2);
|
||||
} else if (cfiOrXPath.startsWith('/body/DocFragment[')) {
|
||||
// Note that all indices in XPointer/XPath are 1-based
|
||||
// but the text() offsets are 0-based
|
||||
const match = cfiOrXPath.match(/DocFragment\[(\d+)\]/);
|
||||
if (match) {
|
||||
return parseInt(match[1]!, 10) - 1;
|
||||
@@ -224,10 +226,10 @@ export class XCFI {
|
||||
let index: number;
|
||||
|
||||
if (segmentWithIndexMatch) {
|
||||
// Format: tag[index]
|
||||
// Format: tag[index] (1-based index)
|
||||
const [, tag, indexStr] = segmentWithIndexMatch;
|
||||
tagName = tag!;
|
||||
index = parseInt(indexStr!, 10);
|
||||
index = Math.max(0, parseInt(indexStr!, 10) - 1);
|
||||
} else if (segmentWithoutIndexMatch) {
|
||||
// Format: tag (implicit index 0)
|
||||
const [, tag] = segmentWithoutIndexMatch;
|
||||
@@ -379,7 +381,7 @@ export class XCFI {
|
||||
if (totalSameTagSiblings === 1) {
|
||||
pathParts.unshift(tagName);
|
||||
} else {
|
||||
pathParts.unshift(`${tagName}[${siblingIndex}]`);
|
||||
pathParts.unshift(`${tagName}[${siblingIndex + 1}]`); // Convert to 1-based index for XPointer
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
@@ -545,7 +547,7 @@ export const getXPointerFromCFI = async (
|
||||
// This has neglectable effect on position accuracy as the XPointer still point to the correct element
|
||||
// while offset within the text node is usually ignored by pagination.
|
||||
export const normalizeProgressXPointer = (xpointer: string): string => {
|
||||
const tailingTextRange = /\/text\(\)\.\d+$/;
|
||||
const tailingTextRange = /\/text\(\).*$/;
|
||||
if (xpointer.match(tailingTextRange)) {
|
||||
xpointer = xpointer.replace(tailingTextRange, '');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user