You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In Scient's PDF reader, selecting Hebrew text that contains an English word can skip that word, jump unexpectedly between lines, or copy text in the wrong order. The visible selection and the intended reading range can diverge.
Affected surface: PDF text selection and copy/paste. This issue does not concern the Markdown editor or Markdown rendering.
Track the bug independently of any implementation: a custom selection controller is an investigation candidate, not an accepted or validated fix.
Reproduction and expected behavior
Open the synthetic hebrew_rtl_selection_test.pdf in Scient's PDF reader. Its complete HTML source is preserved below for regeneration using Chromium's Print to PDF.
On page 1, use T-01. Begin selecting inside the Hebrew sentence beginning, extend across the embedded PDF token, and continue into the second Hebrew line. Repeat in the reverse direction.
Copy and paste into a plain-text editor, comparing the selected characters with the source. Capture the exact clipboard string and selection endpoints when qualifying a fix.
Repeat with T-02 (Chromium, HTML, PDF, Unicode, pdf.js) and T-11 (English code embedded in Hebrew).
Expected: selection follows the intended sentence order across direction changes and line wraps. The highlighted characters and copied text correspond to the same range, with English tokens intact and no skipped text, wrong-column text or unintended line jumps.
Reported: selection jumps around embedded English and line boundaries; copying does not reliably preserve the intended order. The user's screenshot shows the T-01 selection. The original screenshot's temporary file is no longer available and is not attached here.
The opening source text is:
מסמך זה נבנה כדי לבחון את שכבת הבחירה של קורא ה‑PDF בטקסט עברי.
כל מקטע ממוספר, וכל שורה נכתבה כך שניתן לסמן אותה, להעתיק אותה ולהשוות את
התוצאה למה שהוצג על המסך. שבירת השורה היא המקום שבו סדר הגליפים בקובץ נפרד
מן הסדר הלוגי של התווים.
Source newlines above are HTML whitespace; distinguish visual line wraps from paragraph boundaries when evaluating copied text.
Original synthetic PDF: four pages, tagged, document language he.
Original PDF SHA-256: c3abe272b100aca4b81a9790463c8a4e9006a4ef3a2d89ed8e19900fd6bff61d.
Extraction used the actual text-layer options: includeMarkedContent: true, disableNormalization: true.
The original PDF is retained locally under its fixture filename; it is not attached to this issue. Regenerating the embedded HTML preserves the test source but may change pagination, text-item indices and PDF bytes depending on Chromium/fonts/print settings. Treat the hash and exact indices below as evidence for the original PDF, not as properties guaranteed for every regeneration.
Confirmed mechanism
PDF.js positions text spans visually but appends them in extracted item order. It assigns each span its own dir. Its copy path takes document.getSelection().toString() and applies its existing normalization when copying is permitted. Selection painting also derives from the native DOM range.
For the original T-01 paragraph (p2R_mc18), extraction order is:
Text-content index, including marked-content entries
Content
Intended order
128
Hebrew ending of first line
After PDF
131
PDF
In the middle of first line
134
Hyphen
Before PDF in logical order
136
Hebrew beginning of first line
First
140
Hebrew second line
After the complete first line
144
Hebrew third line
Last
A native range beginning inside item 136 and ending inside item 140 cannot include item 131. Visually positioning these elements does not change their DOM relationship. This establishes the ordering mismatch; exact pointer transitions and clipboard output were not re-recorded during the source investigation.
Paragraph tags identify boundaries but do not resolve this paragraph's internal mixed-direction order. TextItem.dir alone also does not determine how adjacent English fragments and neutral punctuation belong together.
Related: #274 corrected stray blue selection painting at the page edge. It did not introduce or fix this reading-order defect. Preserve that paint fix.
A disposable experiment sorted rows top-to-bottom and each row's text items right-to-left, preserving each item's string. It restored T-01's sentence order but caused these failures:
Case
Correct token/expression
Naive RTL sort
T-02
pdf.js
js.pdf
T-04
(parseInt)
)parseInt(
T-11
const dir = "rtl";
"rtl"; = dir const
Do not reverse characters or globally sort the page by coordinates. Those approaches can damage English runs, punctuation, Hebrew character clusters and column order. Moving the existing PDF.js DOM nodes also needs particular caution: search, autolinking and accessibility share item/node mappings.
Candidate approach — not yet implemented or validated
Explore a bounded logical reading-order model and selection adapter:
Preserve PDF.js's original text items, DOM, source offsets and rendering responsibilities.
Map qualified paragraph/line/run reading order back to original character segments and geometry.
Use the same segment list for visible selection and copied text. A clipboard-only correction would leave the jumping selection.
Preserve LTR runs, numbers, neutral punctuation and grapheme boundaries; explicitly decline ambiguous cases instead of guessing.
Preserve native behavior for unsupported regions, with tested handoffs so a partially supported selection cannot silently omit text.
A custom controller adds input/accessibility/lifecycle complexity. Prototype and qualify it before choosing this approach or enabling it broadly. The issue remains valid if this candidate is abandoned or the eventual solution is upstream in PDF.js.
Acceptance criteria
T-01/T-02/T-11: forward and reverse drags, partial English words, boundaries before/inside/after English runs, and selections across line wraps preserve the intended text.
Highlight rectangles and copied text represent the same source segments, without skipped words or extraneous lines/columns.
Mouse, Shift-click, keyboard extension, Select All, context-menu copy, cancellation and focus changes behave coherently.
English-only, Hebrew-only, Hebrew inside English, numbers, decimals, percentages, URLs, code, nested brackets and Hebrew combining marks show no new regressions against recorded baselines.
Tables, columns and an actual cross-page paragraph are tested. The original fixture's T-14 is entirely on page 4 despite its label; it is not a cross-page test.
Search and link geometry, forms, existing annotations, accessibility, SyncTeX, PDF copy permissions and copy from unrelated controls are preserved.
Zoom, fit, pinch, rotation, resize, page virtualization, source renewal, file switches and multiple readers do not invalidate selection identity or leak listeners/caches.
Automated real-browser interaction/clipboard checks and targeted native Electron visual review pass on the exact candidate, followed by applicable repository checks and user visual acceptance.
Verification boundary and related finding
The existing focused PDF suite passed: 15 files / 100 tests. Those tests are baseline coverage and do not prove mixed-direction selection correctness. All four PDF pages were rendered and inspected as documents. The probe below independently verifies the ordering mismatch and rejected-sort counterexamples; it is not a browser interaction test. No product fix was implemented or enabled by this investigation.
A separate extraction-fidelity finding concerns page-stream /ActualText replacements for hyphens, brackets and Hebrew marks that the pinned text-content path does not expose. In T-01, a source U+2011 is extracted as U+002D. Track that separately; fixing selection order alone must not be described as restoring all source characters. Annotation creation was disabled in the examined checkout and is not added by this issue.
Save as hebrew_rtl_selection_test.html and print to PDF with Chromium. The fixture's descriptions and expected-output notes are test data; validate the resulting layout and text rather than assuming the labels prove coverage.
<!DOCTYPE html><htmllang="he" dir="rtl"><head><metacharset="utf-8"><title>גיליון בדיקה — עברית, כיווניות מעורבת ושכבת הבחירה ב‑PDF</title><style>@page { size: A4; margin:18mm16mm16mm; }
:root {
--surface-1:#fcfcfb; --page-plane:#f9f9f7;
--text-primary:#0b0b0b; --text-secondary:#52514e; --text-muted:#898781;
--grid:#e1e0d9; --axis:#c3c2b7; --hairline:rgba(11,11,11,0.10);
--accent:#2a78d6; --accent-soft:#eef4fb; --warn:#b03030;
--he:"Arial Hebrew","Noto Sans Hebrew","Times New Roman", sans-serif;
--he-scholar:"Arial Hebrew Scholar","Times New Roman", serif;
--mono:"SF Mono", Menlo, Consolas, monospace;
}
* { box-sizing: border-box; }
html { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
body {
margin:0; font-family:var(--he); font-size:10.5pt; line-height:1.62;
color:var(--text-primary);
}
.masthead { border-bottom:2px solid var(--accent); padding-bottom:9px; margin-bottom:14px; }
.eyebrow { font-size:8pt; letter-spacing:.06em; color:var(--accent); font-weight:700; margin:005px; }
h1 { font-size:19pt; line-height:1.28; margin:007px; font-weight:700; }
.byline { font-size:9pt; color:var(--text-secondary); margin:0; }
.notice { background:var(--accent-soft); border-inline-start:3px solid var(--accent);
padding:8px11px; font-size:9pt; color:var(--text-secondary);
margin:0015px; break-inside: avoid; }
.noticestrong { color:var(--text-primary); }
h2 { font-size:12.5pt; margin:19px08px; padding-bottom:4px;
border-bottom:1px solid var(--hairline); font-weight:700; break-after: avoid; }
p { margin:008px; orphans:3; widows:3; }
/* Test-case blocks */
.case { border:1px solid var(--hairline); border-radius:5px; background:var(--surface-1);
padding:9px12px10px; margin:0011px; break-inside: avoid; }
.case>h3 { display: flex; align-items: baseline; gap:8px; font-size:9.4pt;
margin:006px; font-weight:700; color:var(--text-primary); }
.cid { font-family:var(--mono); font-size:8pt; font-weight:700; color:var(--accent);
background:var(--accent-soft); border-radius:3px; padding:1px5px;
unicode-bidi: isolate; direction: ltr; }
.what { font-size:8.4pt; font-weight:400; color:var(--text-muted); }
.sample { font-size:11.5pt; line-height:1.7; margin:0; }
.sample.big { font-size:14pt; line-height:1.75; }
.scholar { font-family:var(--he-scholar); }
.expect { font-family:var(--mono); font-size:8.2pt; color:var(--text-secondary);
direction: ltr; unicode-bidi: isolate; text-align: left;
margin:6px00; padding-top:5px; border-top:1px dashed var(--grid);
white-space: pre-wrap; word-break: break-word; }
.ltr { direction: ltr; unicode-bidi: isolate; }
code { font-family:var(--mono); font-size:9pt; direction: ltr; unicode-bidi: isolate;
background:var(--page-plane); border:1px solid var(--grid); border-radius:3px;
padding:04px; }
a { color:var(--accent); }
ol,ul { margin:008px; padding-inline-start:22px; }
li { margin-bottom:3px; }
table { width:100%; border-collapse: collapse; font-size:9pt; margin:4px013px;
font-variant-numeric: tabular-nums; }
caption { text-align: start; font-size:9.6pt; font-weight:700; padding-bottom:5px; }
theadth { text-align: start; font-size:8.2pt; letter-spacing:.02em; color:var(--text-muted);
border-bottom:1px solid var(--axis); padding:5px7px; font-weight:700; }
theadth.num,tbodytd.num { text-align: end; }
tbodyth { font-weight:500; text-align: start; padding:4px7px; }
tbodytd { padding:4px7px; color:var(--text-secondary); }
tbodytr+trth,tbodytr+trtd { border-top:1px solid var(--grid); }
thead { display: table-header-group; } tr { break-inside: avoid; }
.twocol { column-count:2; column-gap:9mm; column-rule:1px solid var(--grid);
font-size:10pt; margin-bottom:10px; }
.twocolp { margin:007px; }
.endnotes { margin-top:18px; padding-top:9px; border-top:1px solid var(--hairline);
font-size:8.4pt; color:var(--text-muted); break-inside: avoid; }
.endnotesol { padding-inline-start:18px; margin:0; }
.pagebreak { break-before: page; }
</style></head><body><headerclass="masthead"><pclass="eyebrow">גיליון בדיקה פנימי · RTL-TEST-01 · 10 בספטמבר 2026</p><h1>עברית, כיווניות מעורבת ושכבת הבחירה בקובץ PDF</h1><pclass="byline">מסמך בדיקה סינתטי — נבנה מ‑HTML ונבנה ל‑PDF במנוע Chromium בהדפסה</p></header><pclass="notice"><strong>מה נבדק כאן.</strong> כל מקטע מציג טקסט עברי בתנאים שונים, ולידו
המחרוזת שאמורה להתקבל בהעתקה. סמנו את הדוגמה, העתיקו אותה, והשוו לשורה המסומנת
<spanclass="ltr">expected</span>. אי‑התאמה מעידה על תקלה בשכבת הבחירה, לא בטקסט עצמו.
כל התוכן במסמך מומצא לצורך הבדיקה בלבד.</p><h2>א. זרימת טקסט בסיסית</h2><divclass="case"><h3><spanclass="cid">T-01</span> עברית בלבד <spanclass="what">— זרימה ימין‑לשמאל, שורה נשברת</span></h3><pclass="sample">מסמך זה נבנה כדי לבחון את שכבת הבחירה של קורא ה‑PDF בטקסט עברי.
כל מקטע ממוספר, וכל שורה נכתבה כך שניתן לסמן אותה, להעתיק אותה ולהשוות את
התוצאה למה שהוצג על המסך. שבירת השורה היא המקום שבו סדר הגליפים בקובץ נפרד
מן הסדר הלוגי של התווים.</p></div><divclass="case"><h3><spanclass="cid">T-02</span> עברית עם אנגלית משולבת <spanclass="what">— מעברי כיוון בתוך שורה</span></h3><pclass="sample">מנוע הרינדור Chromium ממיר את ה‑HTML לקובץ PDF, ולכן סדר הגליפים
בקובץ אינו זהה לסדר הלוגי של התווים בתקן Unicode. ספריית pdf.js בונה את שכבת
הטקסט מחדש, ובדיוק שם נשברת ההעתקה.</p><pclass="expect">expected: …the words Chromium, HTML, PDF, Unicode and pdf.js must each copy intact, in logical order.</p></div><divclass="case"><h3><spanclass="cid">T-03</span> מספרים, אחוזים ויחידות <spanclass="what">— ספרות אירופיות בתוך RTL</span></h3><pclass="sample">בשנת 2026 נסרקו 1,240 עמודים ב‑12 מסמכים; שיעור השגיאות היה 3.5%,
ירידה של 0.8 נקודת אחוז לעומת 2025. גודל הקובץ הגדול ביותר היה 706 ק״ב.</p><pclass="expect">expected: 2026 · 1,240 · 12 · 3.5% · 0.8 · 2025 · 706 — digits in that order, decimal points and commas attached.</p></div><divclass="case"><h3><spanclass="cid">T-04</span> סוגריים ומירכאות <spanclass="what">— שיקוף תווים דו‑כיווניים</span></h3><pclass="sample">הפונקציה (parseInt) מקבלת מחרוזת [string] ומחזירה מספר שלם;
אם הקלט ריק היא מחזירה <code>NaN</code>. השאלה הפתוחה — האם ״הסוגר נפתח בצד הנכון?״ —
נבדקת כאן במפורש (וגם בתוך סוגריים מקוננים [כמו כאן]).</p><pclass="expect">expected: parentheses and brackets keep their logical pairing after copy; the mirrored glyph order on the page is not the copy order.</p></div><h2>ב. ניקוד, טעמים ופיסוק עברי</h2><divclass="case"><h3><spanclass="cid">T-05</span> ניקוד מלא <spanclass="what">— סימנים משולבים על אותיות</span></h3><pclass="sample big">שָׁלוֹם, כֵּיצַד נִרְאֶה הַנִּקּוּד בַּקּוֹבֶץ הַזֶּה?</p><pclass="sample big scholar">שָׁלוֹם, כֵּיצַד נִרְאֶה הַנִּקּוּד בַּקּוֹבֶץ הַזֶּה?
<spanclass="what">(Arial Hebrew Scholar)</span></p><pclass="expect">expected: each base letter copies with its own points; no marks dropped, reordered, or emitted as standalone characters.</p></div><divclass="case"><h3><spanclass="cid">T-06</span> טעמי המקרא <spanclass="what">— בדיקת כיסוי גליפים U+0591–U+05AF</span></h3><pclass="sample big scholar">בְּרֵאשִׁ֖ית בָּרָ֣א אֱלֹהִ֑ים אֵ֥ת הַשָּׁמַ֖יִם וְאֵ֥ת הָאָֽרֶץ׃</p><pclass="expect">expected: no tofu boxes. Cantillation coverage is font-dependent — this line is the coverage check.</p></div><divclass="case"><h3><spanclass="cid">T-07</span> פיסוק עברי <spanclass="what">— מקף, גרש, גרשיים, סוף פסוק</span></h3><pclass="sample big">עַל־כֵּן ר׳ יוֹסֵי · תשפ״ו · כ״ג בֶּאֱלוּל · דו״ח מ‑ג׳ בטבת ׀ סוף פסוק׃</p><pclass="expect">expected: U+05BE maqaf, U+05F3 geresh, U+05F4 gershayim, U+05C3 sof pasuq, U+05C0 paseq — each copies as itself, not as ASCII - ' " : |</p></div><divclass="case"><h3><spanclass="cid">T-08</span> אותיות סופיות וליגטורה <spanclass="what">— צורות תצוגה</span></h3><pclass="sample big">מלך · אדם · בן · כסף · ארץ · ﭏ (ligature U+FB4F) · שׂ שׁ אַ אָ וּ</p><pclass="expect">expected: final forms ך ם ן ף ץ copy as final forms; U+FB4F does not decompose into aleph+lamed.</p></div><divclass="case pagebreak"><h3><spanclass="cid">T-09</span> הדגשה ונטייה בעברית <spanclass="what">— משקל ואלכסון סינתטי</span></h3><pclass="sample big">רגיל · <strong>מודגש</strong> · <em>נטוי</em> · <strong><em>מודגש ונטוי</em></strong></p><pclass="expect">expected: weight/slant changes do not split words into separate copyable runs. Hebrew italic here is a synthesized oblique, not a true cursive face.</p></div><h2>ג. גבולות בין כיוונים</h2><divclass="case"><h3><spanclass="cid">T-10</span> כתובת URL בפסקה עברית <spanclass="what">— קישור לחיץ בתוך RTL</span></h3><pclass="sample">התיעוד המלא זמין בכתובת
<aclass="ltr" href="https://example.org/docs/rtl?lang=he#selection">https://example.org/docs/rtl?lang=he#selection</a>
ובעמוד הפנימי <aclass="ltr" href="#endnotes">הערות סיום</a> שבסוף המסמך.</p><pclass="expect">expected: the URL copies as one unbroken LTR string; both anchors are real clickable link annotations in the PDF.</p></div><divclass="case"><h3><spanclass="cid">T-11</span> מקטע קוד בתוך RTL <spanclass="what">— בלוק LTR מבודד</span></h3><pclass="sample">ההגדרה נעשית בשורה אחת: <code>const dir = "rtl";</code> ולאחריה
<code>element.setAttribute("dir", dir)</code> — שתי המחרוזות חייבות להישאר בסדר LTR.</p></div><divclass="case"><h3><spanclass="cid">T-12</span> ראשי תיבות לטיניים מודבקים <spanclass="what">— מקף מחבר בין כיוונים</span></h3><pclass="sample big">מערכת ה‑PDF · קובץ ה‑HTML · תקן ה‑Unicode · ממשק ה‑API · שכבת ה‑DOM</p><pclass="expect">expected: U+2011/U+2010-style maqaf-joined pairs copy as ה‑PDF etc., with the Latin token intact.</p></div><h2>ד. מבנים מרובי־שורות</h2><p>המקטעים הבאים בודקים את סדר ההעתקה כשהטקסט חוצה תאים, פריטי רשימה, עמודות ועמודים —
המקרים שבהם שכבת בחירה שגויה מחזירה טקסט תקין למראה אך בסדר הפוך.</p><olclass="sample"><li>פריט ראשון ברשימה ממוספרת — המספר נמצא בצד ימין.</li><li>פריט שני, ובתוכו טוקן לטיני יחיד: <code>selectionchange</code>.</li><li>פריט שלישי, ובתוכו מספר: 3.5% מן המסמכים.</li></ol><ulclass="sample"><li>תבליט עם ניקוד: הַבְחִירָה נִשְׁמֶרֶת.</li><li>תבליט עם מספר וסימן: 12 ק״ב.</li></ul><table><caption>טבלה 1 — מקטעי הבדיקה, ספירת תווים וכיוון בסיס</caption><thead><tr><th>מקטע</th><th>מה נבדק</th><thclass="num">תווים</th><thclass="num">סימנים משולבים</th><th>כיוון בסיס</th></tr></thead><tbody><tr><th><spanclass="ltr">T-01</span></th><td>עברית בלבד</td><tdclass="num">248</td><tdclass="num">0</td><td>RTL</td></tr><tr><th><spanclass="ltr">T-02</span></th><td>עברית ואנגלית</td><tdclass="num">196</td><tdclass="num">0</td><td>RTL</td></tr><tr><th><spanclass="ltr">T-05</span></th><td>ניקוד מלא</td><tdclass="num">41</td><tdclass="num">19</td><td>RTL</td></tr><tr><th><spanclass="ltr">T-06</span></th><td>טעמי המקרא</td><tdclass="num">58</td><tdclass="num">31</td><td>RTL</td></tr><tr><th><spanclass="ltr">T-11</span></th><td>מקטע קוד</td><tdclass="num">112</td><tdclass="num">0</td><td>LTR בתוך RTL</td></tr><tr><th><spanclass="ltr">T-13</span></th><td>שתי עמודות</td><tdclass="num">421</td><tdclass="num">4</td><td>RTL</td></tr></tbody></table><divclass="case"><h3><spanclass="cid">T-13</span> שתי עמודות <spanclass="what">— סדר ההעתקה בין עמודה ימנית לשמאלית</span></h3><divclass="twocol"><p>העמודה הימנית נקראת ראשונה. אם שכבת הבחירה בנויה לפי מיקום גיאומטרי בלבד,
היא תתחיל דווקא מן העמודה השמאלית, וההעתקה תשלב את שתי העמודות שורה בשורה.</p><p>זו הבדיקה הקשה ביותר בגיליון: הפלט הנכון הוא כל העמודה הימנית, ואחריה
כל העמודה השמאלית — ולא ערבוב של השתיים.</p><p>העמודה השמאלית נקראת שנייה. היא מכילה מספר, 42, וטוקן לטיני,
<code>column-count</code>, כדי שניתן יהיה לזהות בקלות היכן היא מתחילה בפלט
ההעתקה.</p><p>אם המילה ״שנייה״ מופיעה לפני המילה ״ראשונה״ בהדבקה, סדר הקריאה של הקובץ
שגוי.</p></div></div><divclass="case"><h3><spanclass="cid">T-14</span> פסקה החוצה עמוד <spanclass="what">— בחירה רציפה בין עמודים</span></h3><pclass="sample">הפסקה הזאת נכתבה כדי להתחיל בעמוד אחד ולהסתיים בעמוד הבא, ולכן היא
ארוכה מן הדרוש. סימון רציף שמתחיל כאן ונגמר אחרי שבירת העמוד חייב להחזיר טקסט אחד
ורצוף, בלי לחזור לתחילת העמוד ובלי לדלג על השורות שנותרו בתחתית העמוד הקודם.
בקוראים רבים דווקא כאן נוצרת התקלה: הבחירה נראית נכונה על המסך, אך ההדבקה
מחזירה את העמוד השני לפני העמוד הראשון, או משמיטה את השורה האחרונה שלפני
השבירה. כדי שהבדיקה תהיה חד‑משמעית, שתי המילים הבאות מסמנות את הקצוות:
המילה ״התחלה־כאן״ נמצאת בעמוד הראשון, והמילה ״סיום־שם״ נמצאת בעמוד השני.
אם ההדבקה מחזירה את שתיהן בסדר הזה, ורק פעם אחת כל אחת, הבדיקה עברה.
הטקסט שביניהן הוא מלל מילוי שנועד לדחוף את סוף הפסקה מעבר לגבול העמוד:
שכבת טקסט תקינה שומרת על סדר לוגי גם כשהפריסה הפיזית מפוצלת, וזה בדיוק ההבדל
בין קובץ שניתן לחפש בו ובין קובץ שנראה נכון אך אינו קריא במכונה.</p></div><h2>ה. סימני כיוון בלתי נראים</h2><divclass="case"><h3><spanclass="cid">T-15</span> RLM ו‑LRM <spanclass="what">— תווי בקרה חסרי רוחב</span></h3><pclass="sample">בשורה הזאת שולב תו RLM (U+200F) לפני ואחרי הטוקן הלטיני, ובשורה
הבאה תו LRM (U+200E) אחרי המספר 7 כדי לקבע את הכיוון.</p><pclass="expect">expected: invisible marks may be preserved or stripped on copy, but must not reorder the visible text around them.</p></div><divclass="endnotes" id="endnotes"><p><strong>הערות סיום</strong></p><ol><li>המסמך אינו נושא מספרי עמודים: תיבות השוליים של <spanclass="ltr">@page</span> אינן נתמכות במנוע ההדפסה שבו נבנה הקובץ, ולכן לא הוצהר מה שלא ניתן לאמת.</li><li>כיסוי טעמי המקרא במקטע <spanclass="ltr">T-06</span> תלוי בגופן המותקן במכונה ואינו תכונה של הקובץ.</li><li>כל הנתונים בטבלה 1 הם ספירות מומצאות לצורך הבדיקה ואינם נמדדים מן הקובץ עצמו.</li><li>מקור המסמך: <spanclass="ltr">hebrew_rtl_selection_test.html</span> · נבנה ל‑<spanclass="ltr">hebrew_rtl_selection_test.pdf</span>.</li></ol></div></body></html>
Read-only diagnostic probe
Save as probe.mjs. With the investigated checkout's locked dependencies installed and Node 24, run:
The probe targets the original fixture's content and rejects known-bad sorting. It is not a production resolver. Its exact-index evidence depends on the original PDF bytes.
// Read-only fixture investigation. This is not a proposed production algorithm.// Usage: node probe.mjs <scient-desktop checkout> <fixture.pdf>importassertfrom"node:assert/strict";importfsfrom"node:fs";importpathfrom"node:path";import{pathToFileURL}from"node:url";import{createHash}from"node:crypto";const[checkout,pdf]=process.argv.slice(2);assert(checkout&&pdf,"Provide checkout and PDF paths");const{ getDocument, version }=awaitimport(pathToFileURL(path.join(checkout,"apps/web/node_modules/pdfjs-dist/legacy/build/pdf.mjs")));constbytes=fs.readFileSync(pdf);consttask=getDocument({data: newUint8Array(bytes),useSystemFonts: true});constcontentIds=node=>(node.children??[]).flatMap(child=>child.type==="content" ? [child.id] : contentIds(child));try{constdoc=awaittask.promise;constblocks=[];for(letpageNumber=1;pageNumber<=doc.numPages;pageNumber++){constpage=awaitdoc.getPage(pageNumber);// Match TextLayerBuilder's extraction options, including marked content.constcontent=awaitpage.getTextContent({includeMarkedContent: true,disableNormalization: true});conststack=[],items=[];for(const[index,item]ofcontent.items.entries()){if(item.type==="beginMarkedContent"||item.type==="beginMarkedContentProps")stack.push(item.id??null);elseif(item.type==="endMarkedContent")stack.pop();elseitems.push({ ...item, index,ids: stack.filter(Boolean)});}functionwalk(node){if(!node)return;if(node.role==="P"){constids=newSet(contentIds(node));blocks.push({page: pageNumber,ids: [...ids],items: items.filter(item=>item.str&&item.ids.some(id=>ids.has(id)))});}else(node.children??[]).forEach(walk);}walk(awaitpage.getStructTree());}constblockFor=text=>{constmatches=blocks.filter(block=>block.items.some(item=>item.str.includes(text)));assert.equal(matches.length,1,`Expected one paragraph for ${text}`);returnmatches[0];};// Deliberately naive comparator, evaluated only to establish counterexamples.// The fixed tolerance is appropriate to this fixture probe, not general PDFs.constnaiveRTL=block=>{constrows=[];for(constitemofblock.items){letrow=rows.find(row=>Math.abs(row.y-item.transform[5])<0.5);if(!row)rows.push(row={y: item.transform[5],items: []});row.items.push(item);}returnrows.sort((a,b)=>b.y-a.y).map(row=>row.items.slice().sort((a,b)=>b.transform[4]-a.transform[4]).map(item=>item.str).join("")).join("\n");};constt01=blockFor("מסמך זה נבנה");constfirst=t01.items.find(item=>item.str.startsWith("מסמך זה נבנה"));constenglish=t01.items.find(item=>item.str==="PDF");constsecond=t01.items.find(item=>item.str.startsWith("נכתבה כך"));assert(english.index<first.index&&first.index<second.index,"Cross-line native DOM range excludes the earlier English text item");assert(naiveRTL(t01).startsWith("מסמך זה נבנה כדי לבחון את שכבת הבחירה של קורא ה-PDF בטקסט עברי."));constt02=blockFor("מנוע הרינדור");assert(t02.items.map(item=>item.str).join("").includes("pdf.js"));assert(naiveRTL(t02).includes("js.pdf"));constt04=blockFor("הפונקציה");assert(naiveRTL(t04).includes(")parseInt("));constt11=blockFor("ההגדרה נעשית");assert(t11.items.map(item=>item.str).join("").includes('const dir = "rtl";'));assert(naiveRTL(t11).includes('"rtl"; = dir const'));constt14=blockFor("הפסקה הזאת נכתבה");assert(t14.items.some(item=>item.str.includes("קובץ שניתן לחפש")));console.log(JSON.stringify({
pdf,sha256: createHash("sha256").update(bytes).digest("hex"),pdfjs: version,result: "pass",t01: {page: t01.page,items: t01.items.map(item=>({index: item.index,text: item.str,dir: item.dir}))},counterexamples: [t02,t04,t11].map(block=>({page: block.page,ids: block.ids,naiveRTL: naiveRTL(block)})),t14: {page: t14.page,note: "Entire paragraph occurs on this page; not a cross-page fixture"},limitation: "Extraction and ordering assertions only. No native pointer, keyboard, clipboard, or geometry test."},null,2));}finally{awaittask.destroy();}
Problem
In Scient's PDF reader, selecting Hebrew text that contains an English word can skip that word, jump unexpectedly between lines, or copy text in the wrong order. The visible selection and the intended reading range can diverge.
Affected surface: PDF text selection and copy/paste. This issue does not concern the Markdown editor or Markdown rendering.
Track the bug independently of any implementation: a custom selection controller is an investigation candidate, not an accepted or validated fix.
Reproduction and expected behavior
hebrew_rtl_selection_test.pdfin Scient's PDF reader. Its complete HTML source is preserved below for regeneration using Chromium's Print to PDF.PDFtoken, and continue into the second Hebrew line. Repeat in the reverse direction.Chromium,HTML,PDF,Unicode,pdf.js) and T-11 (English code embedded in Hebrew).Expected: selection follows the intended sentence order across direction changes and line wraps. The highlighted characters and copied text correspond to the same range, with English tokens intact and no skipped text, wrong-column text or unintended line jumps.
Reported: selection jumps around embedded English and line boundaries; copying does not reliably preserve the intended order. The user's screenshot shows the T-01 selection. The original screenshot's temporary file is no longer available and is not attached here.
The opening source text is:
Source newlines above are HTML whitespace; distinguish visual line wraps from paragraph boundaries when evaluating copied text.
Investigated baseline
415347ae2667d2579ed1f1d2d16aca521584e960.6.2.108, legacy viewer/build.he.c3abe272b100aca4b81a9790463c8a4e9006a4ef3a2d89ed8e19900fd6bff61d.includeMarkedContent: true,disableNormalization: true.The original PDF is retained locally under its fixture filename; it is not attached to this issue. Regenerating the embedded HTML preserves the test source but may change pagination, text-item indices and PDF bytes depending on Chromium/fonts/print settings. Treat the hash and exact indices below as evidence for the original PDF, not as properties guaranteed for every regeneration.
Confirmed mechanism
PDF.js positions text spans visually but appends them in extracted item order. It assigns each span its own
dir. Its copy path takesdocument.getSelection().toString()and applies its existing normalization when copying is permitted. Selection painting also derives from the native DOM range.For the original T-01 paragraph (
p2R_mc18), extraction order is:PDFPDFPDFin logical orderA native range beginning inside item 136 and ending inside item 140 cannot include item 131. Visually positioning these elements does not change their DOM relationship. This establishes the ordering mismatch; exact pointer transitions and clipboard output were not re-recorded during the source investigation.
Paragraph tags identify boundaries but do not resolve this paragraph's internal mixed-direction order.
TextItem.diralone also does not determine how adjacent English fragments and neutral punctuation belong together.Related: #274 corrected stray blue selection painting at the page edge. It did not introduce or fix this reading-order defect. Preserve that paint fix.
Source at the investigated commit:
Pinned dependency inspection:
TextLayerBuildercopy handler and extraction options,TextLayeritem append/direction,TextHighlighteritem-offset mapping,Autolinkernative-range construction, andDrawLayerselection rectangles.Rejected shortcut and regression evidence
A disposable experiment sorted rows top-to-bottom and each row's text items right-to-left, preserving each item's string. It restored T-01's sentence order but caused these failures:
pdf.jsjs.pdf(parseInt))parseInt(const dir = "rtl";"rtl"; = dir constDo not reverse characters or globally sort the page by coordinates. Those approaches can damage English runs, punctuation, Hebrew character clusters and column order. Moving the existing PDF.js DOM nodes also needs particular caution: search, autolinking and accessibility share item/node mappings.
Candidate approach — not yet implemented or validated
Explore a bounded logical reading-order model and selection adapter:
A custom controller adds input/accessibility/lifecycle complexity. Prototype and qualify it before choosing this approach or enabling it broadly. The issue remains valid if this candidate is abandoned or the eventual solution is upstream in PDF.js.
Acceptance criteria
Verification boundary and related finding
The existing focused PDF suite passed: 15 files / 100 tests. Those tests are baseline coverage and do not prove mixed-direction selection correctness. All four PDF pages were rendered and inspected as documents. The probe below independently verifies the ordering mismatch and rejected-sort counterexamples; it is not a browser interaction test. No product fix was implemented or enabled by this investigation.
A separate extraction-fidelity finding concerns page-stream
/ActualTextreplacements for hyphens, brackets and Hebrew marks that the pinned text-content path does not expose. In T-01, a source U+2011 is extracted as U+002D. Track that separately; fixing selection order alone must not be described as restoring all source characters. Annotation creation was disabled in the examined checkout and is not added by this issue.References: Unicode UAX #9, PDF.js ordering report #17191, PDF.js Hebrew extraction report #20336. These upstream reports corroborate the problem family; they do not establish a working fix or upgrade.
Complete synthetic HTML reproduction source
Save as
hebrew_rtl_selection_test.htmland print to PDF with Chromium. The fixture's descriptions and expected-output notes are test data; validate the resulting layout and text rather than assuming the labels prove coverage.Read-only diagnostic probe
Save as
probe.mjs. With the investigated checkout's locked dependencies installed and Node 24, run:The probe targets the original fixture's content and rejects known-bad sorting. It is not a production resolver. Its exact-index evidence depends on the original PDF bytes.