In multi-line PDF selections, pdf.js renders each text run as its own <span> and inserts <br role="presentation"> at line endings. getTextFromRange only walked text nodes, so <br>s were dropped and adjacent line-final/line-initial words glued together (e.g. "lastfirst") in highlights, notes, and AI inputs. Walk elements alongside text nodes and emit "\n" for <br>, mirroring how Selection.toString() handles line breaks.
This commit is contained in:
@@ -340,6 +340,43 @@ describe('sel utilities', () => {
|
||||
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
it('should insert a newline for <br> 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 <span>, and line endings are <br role="presentation">.
|
||||
const container = document.createElement('div');
|
||||
container.className = 'textLayer';
|
||||
container.innerHTML =
|
||||
'<span role="presentation">last word of line 1</span>' +
|
||||
'<br role="presentation">' +
|
||||
'<span role="presentation">first word of line 2</span>';
|
||||
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 <br> in HTML content', async () => {
|
||||
const { getTextFromRange } = await import('@/utils/sel');
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = 'first<br>second<br>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', () => {
|
||||
|
||||
@@ -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 <br role="presentation"> 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 <br> 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;
|
||||
|
||||
Reference in New Issue
Block a user