-
Notifications
You must be signed in to change notification settings - Fork 6.5k
fix(web): read Arabic and Hebrew messages in the right direction #7126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0d66eab
8d90e3a
adcffb4
ca9169c
3e064d8
c430d4b
0129d2c
d8e825a
9481d7c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -195,6 +195,7 @@ const CHAT_MARKDOWN_REMARK_PLUGINS = [ | |||||
| remarkNormalizeListItemIndentation, | ||||||
| remarkPreserveCodeMeta, | ||||||
| remarkTagInlineCode, | ||||||
| remarkTextDirection, | ||||||
| ] satisfies NonNullable<ReactMarkdownOptions["remarkPlugins"]>; | ||||||
|
|
||||||
| const CHAT_MARKDOWN_REMARK_PLUGINS_WITH_BREAKS = [ | ||||||
|
|
@@ -204,6 +205,7 @@ const CHAT_MARKDOWN_REMARK_PLUGINS_WITH_BREAKS = [ | |||||
| remarkBreaks, | ||||||
| remarkPreserveCodeMeta, | ||||||
| remarkTagInlineCode, | ||||||
| remarkTextDirection, | ||||||
| ] satisfies NonNullable<ReactMarkdownOptions["remarkPlugins"]>; | ||||||
|
|
||||||
| const CHAT_MARKDOWN_REHYPE_PLUGINS = [ | ||||||
|
|
@@ -340,6 +342,74 @@ function remarkTagInlineCode() { | |||||
| }; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Message prose belongs to whoever wrote it, so its direction is a property of | ||||||
| * the text and not of the app: `dir="auto"` makes the browser read each block's | ||||||
| * base direction off that block's own first strong character, which is what | ||||||
| * puts an Arabic sentence's trailing punctuation and its list markers on the | ||||||
| * right side without touching the English block above it. | ||||||
| * | ||||||
| * Code and tables opt out and stay LTR. Their shape is not prose — identifiers, | ||||||
| * paths, and column order read the same in every locale, and letting an Arabic | ||||||
| * comment flip a snippet would misreport what the agent actually wrote. | ||||||
| */ | ||||||
| const AUTO_DIRECTION_NODE_TYPES = new Set([ | ||||||
| "blockquote", | ||||||
|
AsimNet marked this conversation as resolved.
|
||||||
| "heading", | ||||||
| "list", | ||||||
| "paragraph", | ||||||
| "tableCell", | ||||||
| ]); | ||||||
| const LTR_DIRECTION_NODE_TYPES = new Set(["code", "inlineCode", "table"]); | ||||||
|
AsimNet marked this conversation as resolved.
|
||||||
|
|
||||||
| function setDirection(node: MarkdownAstNode, dir: "auto" | "ltr") { | ||||||
| node.data = { | ||||||
| ...node.data, | ||||||
| hProperties: { | ||||||
| ...node.data?.hProperties, | ||||||
| dir, | ||||||
| }, | ||||||
| }; | ||||||
| } | ||||||
|
|
||||||
| function remarkTextDirection() { | ||||||
| return (tree: MarkdownAstNode) => { | ||||||
| // `dir="auto"` reads the first strong character of an element's *own* text | ||||||
| // and skips any descendant that carries its own `dir`. So only the outermost | ||||||
| // block of a run gets marked: marking a list and its items both would leave | ||||||
| // the list itself with no text to judge, fall back to LTR, and paint the | ||||||
| // bullets of an RTL item into a gutter that is no longer on that side. | ||||||
| // | ||||||
| // The cost is that one list reads in one direction. A list that mixes an | ||||||
| // Arabic item with an English one takes the direction of its first item, | ||||||
| // which is the trade for markers that stay next to the text they label. | ||||||
| const visit = (node: MarkdownAstNode, insideAutoBlock: boolean) => { | ||||||
| const type = node.type ?? ""; | ||||||
| if (LTR_DIRECTION_NODE_TYPES.has(type)) { | ||||||
| setDirection(node, "ltr"); | ||||||
| // A pinned table is not an `auto` ancestor, so its cells are free to | ||||||
| // pick their own direction while the column order stays put. | ||||||
| node.children?.forEach((child) => visit(child, false)); | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| // A GitHub alert is rendered as a titled callout rather than a quote, and | ||||||
| // its own renderer builds that chrome from scratch. Claiming the block | ||||||
| // here would strand its body: the `dir` never reaches the callout, and the | ||||||
| // paragraphs inside it would have been skipped as already-covered. | ||||||
| const isAlertBlockquote = type === "blockquote" && node.data?.hProperties?.dataAlert != null; | ||||||
| const isAutoBlock = | ||||||
| !insideAutoBlock && !isAlertBlockquote && AUTO_DIRECTION_NODE_TYPES.has(type); | ||||||
| if (isAutoBlock) { | ||||||
| setDirection(node, "auto"); | ||||||
| } | ||||||
| node.children?.forEach((child) => visit(child, insideAutoBlock || isAutoBlock)); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium Mixed-direction lists render every item with the direction inferred from the first item, so later items get incorrect base direction for their punctuation and inline content. Because
Suggested change
🤖 Copy this AI Prompt to have your agent fix this:
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not taking this one, and I want to show my work because the suggested patch doesn't do what it looks like it does. Resetting the flag for So the reset would only reach the paragraph inside each item, and mdast-util-to-hast unwraps that paragraph in tight lists. Rendered output today: The suggestion changes only the loose case. That would make A mixed list therefore takes its first item's direction. The minority item still renders as a correct LTR run inside it; what it loses is its alignment and the side its full stop lands on. I've written that trade-off into the comment above the plugin so the next person doesn't have to rediscover it. If you'd rather have per-item direction, it needs the marker rendered inside the item (custom
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I'm unable to act on this request because you do not have permissions within this repository. |
||||||
| }; | ||||||
|
|
||||||
| visit(tree, false); | ||||||
| }; | ||||||
| } | ||||||
|
|
||||||
| function nodeToPlainText(node: ReactNode): string { | ||||||
| if (typeof node === "string" || typeof node === "number") { | ||||||
| return String(node); | ||||||
|
|
@@ -671,6 +741,9 @@ function MarkdownCodeBlock({ | |||||
|
|
||||||
| return ( | ||||||
| <div | ||||||
| // The fence's own chrome, not just its code: a title bar and copy/wrap | ||||||
| // controls that read left-to-right regardless of the prose around them. | ||||||
| dir="ltr" | ||||||
|
cursor[bot] marked this conversation as resolved.
cursor[bot] marked this conversation as resolved.
Comment on lines
+744
to
+746
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The fence pins its whole wrapper here so the chrome stays LTR, but the other pinned widget only gets the attribute on the inner element: Suggest pinning the container the same way the fence is; the cells keep their own <div
ref={containerRef}
+ dir="ltr"
className="chat-markdown-table-container"Posted via Macroscope — UI Consistency |
||||||
| className="chat-markdown-codeblock my-[0.65rem] overflow-hidden rounded-[var(--radius)] border border-border/70 bg-secondary leading-snug dark:border-transparent dark:bg-input/32" | ||||||
| data-language={language} | ||||||
| data-wrap={wrapped ? "true" : "false"} | ||||||
|
|
@@ -1301,6 +1374,10 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ | |||||
| <TooltipTrigger | ||||||
| render={ | ||||||
| <a | ||||||
| // A path is an identifier, so it reads left-to-right wherever the | ||||||
| // prose around it points. The `code` renderer swaps this chip in for | ||||||
| // the `<code dir="ltr">` it replaces, so the pin has to live here too. | ||||||
| dir="ltr" | ||||||
| href={href} | ||||||
| className={cn(CHAT_FILE_TAG_CHIP_CLASS_NAME, MARKDOWN_FILE_LINK_CLASS_NAME, className)} | ||||||
| data-markdown-copy={copyMarkdown} | ||||||
|
|
@@ -1570,7 +1647,7 @@ function ChatMarkdown({ | |||||
| // Not a <blockquote>: the stylesheet mutes those, and an alert's body is ordinary | ||||||
| // text under a colored title — which is how the host renders it. | ||||||
| return ( | ||||||
| <div role="note" className={cn("my-1 border-l-2 pl-3", alert.borderClassName)}> | ||||||
| <div role="note" className={cn("my-1 border-s-2 ps-3", alert.borderClassName)}> | ||||||
| <p className={cn("flex items-center gap-1.5 font-medium", alert.titleClassName)}> | ||||||
| <alert.Icon aria-hidden className="size-3.5 shrink-0" /> | ||||||
| {alert.label} | ||||||
|
|
||||||
Uh oh!
There was an error while loading. Please reload this page.