diff --git a/apps/readest-app/src/__tests__/utils/sel.test.ts b/apps/readest-app/src/__tests__/utils/sel.test.ts
index e40fac2e..511f4a06 100644
--- a/apps/readest-app/src/__tests__/utils/sel.test.ts
+++ b/apps/readest-app/src/__tests__/utils/sel.test.ts
@@ -340,6 +340,43 @@ describe('sel utilities', () => {
document.body.removeChild(container);
});
+
+ it('should insert a newline for
between adjacent text spans (PDF line wrap)', async () => {
+ const { getTextFromRange } = await import('@/utils/sel');
+ // Mirrors how pdf.js renders the text layer: each text run is its
+ // own , and line endings are
.
+ const container = document.createElement('div');
+ container.className = 'textLayer';
+ container.innerHTML =
+ 'last word of line 1' +
+ '
' +
+ 'first word of line 2';
+ document.body.appendChild(container);
+
+ const range = document.createRange();
+ range.selectNodeContents(container);
+
+ const text = getTextFromRange(range);
+ // Without separating the spans the text becomes
+ // "last word of line 1first word of line 2" — words are glued.
+ expect(text).toBe('last word of line 1\nfirst word of line 2');
+
+ document.body.removeChild(container);
+ });
+
+ it('should insert a newline for explicit
in HTML content', async () => {
+ const { getTextFromRange } = await import('@/utils/sel');
+ const container = document.createElement('div');
+ container.innerHTML = 'first
second
third';
+ document.body.appendChild(container);
+
+ const range = document.createRange();
+ range.selectNodeContents(container);
+
+ expect(getTextFromRange(range)).toBe('first\nsecond\nthird');
+
+ document.body.removeChild(container);
+ });
});
describe('snapRangeToWords', () => {
diff --git a/apps/readest-app/src/utils/sel.ts b/apps/readest-app/src/utils/sel.ts
index 75827f81..76fdf4a8 100644
--- a/apps/readest-app/src/utils/sel.ts
+++ b/apps/readest-app/src/utils/sel.ts
@@ -271,21 +271,33 @@ export const snapRangeToWords = (range: Range): void => {
export const getTextFromRange = (range: Range, rejectTags: string[] = []): string => {
const clonedRange = range.cloneRange();
const fragment = clonedRange.cloneContents();
- const walker = document.createTreeWalker(fragment, NodeFilter.SHOW_TEXT, {
- acceptNode: (node) => {
- const parent = node.parentElement;
- if (rejectTags.includes(parent?.tagName.toLowerCase() || '')) {
- return NodeFilter.FILTER_REJECT;
- }
- return NodeFilter.FILTER_ACCEPT;
+ const walker = document.createTreeWalker(
+ fragment,
+ NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
+ {
+ acceptNode: (node) => {
+ const parent = node.parentElement;
+ if (rejectTags.includes(parent?.tagName.toLowerCase() || '')) {
+ return NodeFilter.FILTER_REJECT;
+ }
+ return NodeFilter.FILTER_ACCEPT;
+ },
},
- });
+ );
+ // pdf.js inserts
between text spans at line endings
+ // (see TextLayer#appendText in pdfjs). Without this, multi-line PDF
+ // selections collapse adjacent line-final and line-initial words into a
+ // single token (e.g. "lastfirst"). Treat
as a newline, matching how
+ // Selection.toString() handles line breaks in the browser.
let text = '';
- let node: Text | null;
-
- while ((node = walker.nextNode() as Text | null)) {
- text += node.nodeValue ?? '';
+ let node: Node | null;
+ while ((node = walker.nextNode())) {
+ if (node.nodeType === Node.TEXT_NODE) {
+ text += (node as Text).nodeValue ?? '';
+ } else if ((node as Element).tagName === 'BR') {
+ text += '\n';
+ }
}
return text;