Skip to content

Right-to-left Markdown (Persian, Arabic, Hebrew) renders left-to-right: no dir is emitted #236

Description

@su6i

Quick Look previews of Markdown files written in a right-to-left script — Persian, Arabic, Hebrew, Urdu — render left-to-right. The glyphs are shaped correctly, but the reading order of each paragraph is wrong, so the text has to be read from the wrong end.

Reproduce

Save this as rtl.md and press Space on it in Finder:

# گزارش هفتگی

Gemini web app رو Google سرو می‌کند (تو نمی‌تونی local app رو modify کنی).

- مورد اول
- مورد دوم

## סיכום

הטקסט הזה אמור להיקרא מימין לשמאל.

Expected: each paragraph, heading and list item is laid out right-to-left, and the trailing . and the parenthesised clause sit on the left edge.

Actual: everything is laid out left-to-right. The parenthesis in the second paragraph lands on the wrong side and the clause order visually scrambles; list bullets sit on the left of right-to-left text.

Rendering the same file in GitHub, VS Code's preview or Typora is not much better — this is a gap the whole Markdown ecosystem shares — but Quick Look is where a lot of us actually read .md files on macOS, and QLMarkdown is the tool that makes that possible.

Why the generated HTML is the right place to fix it

The output HTML carries no dir attribute, so every block inherits dir from <html>, which is ltr.

Two things that look like fixes but are not:

  • text-align: right in a custom CSS theme. Right-aligned is not right-to-left. Alignment moves the text box; direction decides reading order, bracket and punctuation mirroring, caret movement and selection. A right-aligned LTR paragraph is still scrambled, just against the other margin.
  • unicode-bidi: plaintext. This applies the Unicode first-strong rule: a paragraph takes its direction from its first strongly directional character. That breaks the most common shape of a technical sentence in Persian or Arabic — one that opens with an English identifier, exactly like the second paragraph above. First-strong sees the G in Gemini and lays the whole line out LTR.

What actually works is deciding direction per block, from the ratio of letters per Unicode script in that block, and emitting it as a real dir attribute on the block element:

<p dir="rtl">Gemini web app رو Google سرو می‌کند (تو نمی‌تونی local app رو modify کنی).</p>
<h2 dir="rtl">סיכום</h2>

A Persian sentence carrying English identifiers stays RTL; an English sentence containing one Persian word stays LTR. pre, code, kbd and samp are always left-to-right, including inline code inside an RTL sentence. A dir that the author wrote by hand in inline HTML is never overwritten.

Since this is a property of a block, the natural place is wherever cmark-gfm's block-level nodes are turned into HTML — paragraph, heading, list item, blockquote, table cell — rather than in CSS.

Two smaller items that ride along once dir is correct:

  1. The generated <html> element could take dir="auto" so the scrollbar and the overall layout follow the document's dominant direction.
  2. A font fallback that covers Arabic and Hebrew blocks would help; the system fonts are fine on recent macOS, so this may already be a non-issue.

Context, and a disclosure

I maintain parsi-rtl, an MIT-licensed browser extension that solves exactly this problem for web pages — the script-ratio heuristic, the pre/code exclusions and the "never overwrite an explicit dir" rule are all implemented and tested there, and docs/ARCHITECTURE.md writes down why each decision was made and what the failure modes are. It is MIT, so the logic can be reused here without a licence problem in this direction.

I am opening this as an issue rather than an unsolicited pull request because the port to C/Swift touches the HTML generation path and I would rather agree on the approach first. If you are open to it, I am happy to do the work.

Happy to add screenshots or test more cases if that helps.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions