fix: set dir="auto" after the linkifier so code-only documents stay linear (release review)

`_renderMarkdown` wrote dir="auto" into its string, before the code pins and
the linkifier. An element carrying dir="auto" re-resolves its direction on
every insertion under it, and a block whose letters all sit in `dir` children
walks its whole subtree each time, so a code-only or path-only document went
quadratic, English ones included.

Measured in real Chromium through the real File Viewer, before and after:
9000 inline-code items 8.7 s to 0.35 s, 9000 bare paths 5.6 s to 0.23 s, a
209 KB out-of-workspace list 41.9 s to 1.4 s. All three now match master.

The pass is now `_applyBlockDirection()`, run last on the DOM by both
consumers. The direction results are unchanged. Tests pin that the rendered
string holds no dir="auto" and that the pass runs after the linkifier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-06 04:49:02 +01:00
1 parent f37f101137
commit 4413240c5f
4 files changed
+89 -27

No files matched your search

+31 -18
View File
@@ -2845,26 +2845,11 @@ class CodemanApp {
a.setAttribute('target', '_blank');
a.setAttribute('rel', 'noopener noreferrer');
});
// Right-to-left text (issue #715). With no `dir`, every block was laid
// out left to right, so a Persian, Arabic or Hebrew answer was
// left-aligned, its punctuation landed on the wrong end and its lists
// and quotes hung off the left edge. Each OUTERMOST prose block now
// takes its direction from its own first letter (`dir="auto"`, what
// GitHub does for rendered markdown), so an RTL paragraph beside an
// English one reads correctly in both; nested blocks inherit, which
// keeps a list's markers and indent on one side. A `dir` the agent
// wrote (the sanitizer allows it) is kept, and so is what it covers.
//
// Code and preformatted text are always left to right. `dir="ltr"`
// also isolates them, and `dir="auto"` skips any descendant that
// carries a `dir`, so `npm install` at the head of a Persian sentence
// no longer turns the whole sentence left to right.
tmpl.content
.querySelectorAll('p, h1, h2, h3, h4, h5, h6, ul, ol, dl, blockquote, .rv-table-wrap')
.forEach((el) => {
if (el.parentElement?.closest('[dir]') || el.hasAttribute('dir')) return;
el.setAttribute('dir', 'auto');
});
// no longer turns the whole sentence left to right (issue #715). The
// `dir="auto"` itself is `_applyBlockDirection`'s, never this string's.
tmpl.content.querySelectorAll('pre, code').forEach((el) => {
if (!el.hasAttribute('dir')) el.setAttribute('dir', 'ltr');
});
@@ -3025,10 +3010,38 @@ class CodemanApp {
renderedText.className = 'rv-text';
renderedText.innerHTML = this._renderMarkdown(text);
this._linkifyFilePaths(renderedText);
this._applyBlockDirection(renderedText);
div.appendChild(renderedText);
return div;
}
/**
* Right-to-left text (issue #715). With no `dir`, every block was laid out
* left to right, so a Persian, Arabic or Hebrew answer was left-aligned, its
* punctuation landed on the wrong end and its lists and quotes hung off the
* left edge. Each OUTERMOST prose block now takes its direction from its own
* first letter (`dir="auto"`, what GitHub does for rendered markdown), so an
* RTL paragraph beside an English one reads correctly in both; nested blocks
* inherit, which keeps a list's markers and indent on one side. A `dir` the
* agent wrote (the sanitizer allows it) is kept, and so is what it covers.
*
* ⚠️ Call this LAST, on the DOM, once nothing else inserts under `root`: an
* element that already carries `dir="auto"` re-resolves its direction on every
* insertion beneath it, and a block whose letters all sit in `dir` children
* (a code-only or path-only list) walks its whole subtree each time. Set
* during `_renderMarkdown` or before the linkifier, that made rendering
* quadratic, for English documents too: a 200 KB list of inline code froze
* the tab for 40 s. Not part of `_linkifyFilePaths`, which returns early when
* a stale constants.js has no `findFilePathLinks`.
*/
_applyBlockDirection(root) {
if (!root) return;
for (const el of root.querySelectorAll('p, h1, h2, h3, h4, h5, h6, ul, ol, dl, blockquote, .rv-table-wrap')) {
if (el.parentElement?.closest('[dir]') || el.hasAttribute('dir')) continue;
el.setAttribute('dir', 'auto');
}
}
/**
* Make file paths in a rendered message clickable: absolute, `~/` and
* relative ones (`findFilePathLinks`, constants.js).
@@ -3130,7 +3143,7 @@ class CodemanApp {
link.dataset.linkified = '1';
if (folder) link.dataset.folder = '1';
// A path reads left to right, and in a right-to-left sentence it must
// not vote on the sentence's direction (`_renderMarkdown`, issue #715).
// not vote on the sentence's direction (`_applyBlockDirection`, issue #715).
link.dir = 'ltr';
link.title = path;
link.textContent = path;
+1
View File
@@ -5071,6 +5071,7 @@ Object.assign(CodemanApp.prototype, {
const doc = tmpl.content.firstElementChild;
this._rebaseFilePreviewMarkdownRefs(doc, info);
this._linkifyFilePaths(doc);
this._applyBlockDirection(doc);
// The linkifier's absolute paths name no session; give them the
// preview's, like the rebased links, or they open in the active tab's.
for (const a of doc.querySelectorAll('a.rv-path:not([data-session-id])')) a.dataset.sessionId = info.sessionId;