From 973ab0a07c446f6fd0a4ce700e940f559836d4cd Mon Sep 17 00:00:00 2001 From: alinddf Date: Sun, 26 Jul 2026 07:10:44 +0330 Subject: [PATCH] feat: add auto-RTL for mixed LTR/RTL content in auto mode When auto mode is active and the setting is enabled, lines containing RTL scripts (Arabic, Hebrew, Persian, etc.) are treated as RTL regardless of their first directional character. Works in both editor (CodeMirror line decorations) and reading view (MarkdownPostProcessor). Includes a toggle in settings. --- EditorPlugin.ts | 15 +++++++++------ MarkdownPostProcessor.ts | 15 ++++++++++++++- direction.util.ts | 6 ++++++ main.ts | 2 +- settingsTab.ts | 14 +++++++++++++- 5 files changed, 43 insertions(+), 9 deletions(-) diff --git a/EditorPlugin.ts b/EditorPlugin.ts index 3154797..278d38d 100644 --- a/EditorPlugin.ts +++ b/EditorPlugin.ts @@ -10,7 +10,7 @@ import { EditorState, RangeSetBuilder } from "@codemirror/state"; import { syntaxTree } from "@codemirror/language"; import RtlPlugin from './main'; import { editorInfoField, MarkdownView } from 'obsidian'; -import { Direction } from './direction.util'; +import { Direction, hasRtlChar } from './direction.util'; type Region = {from: number; to: number;}; type DecorationRegion = Region & {dec: Decoration}; @@ -110,11 +110,6 @@ export function getEditorPlugin(rtlPlugin: RtlPlugin) { if (!viewport) return builder.finish(); - let decoration = this.emptyDirDec; - if (this.direction != 'auto') { - decoration = this.direction === 'ltr' ? this.ltrDec : this.rtlDec; - } - let pos = viewport.from, preventRTLFrontmatter = this.rtlPlugin.settings.preventRTLFrontmatter; // Move the pos after the frontmatter if the pos touches it. @@ -126,6 +121,14 @@ export function getEditorPlugin(rtlPlugin: RtlPlugin) { while (pos <= viewport.to) { const line = view.state.doc.lineAt(pos); + let decoration: Decoration; + if (this.direction != 'auto') { + decoration = this.direction === 'ltr' ? this.ltrDec : this.rtlDec; + } else if (this.rtlPlugin.settings.autoRTLForMixedContent && hasRtlChar(line.text)) { + decoration = this.rtlDec; + } else { + decoration = this.emptyDirDec; + } builder.add(line.from, line.from, decoration); pos = line.to + 1; } diff --git a/MarkdownPostProcessor.ts b/MarkdownPostProcessor.ts index 4cd814d..53d7293 100644 --- a/MarkdownPostProcessor.ts +++ b/MarkdownPostProcessor.ts @@ -1,5 +1,5 @@ import { MarkdownPostProcessorContext } from 'obsidian'; -import { Direction } from './direction.util'; +import { Direction, hasRtlChar } from './direction.util'; // Try to detect if the postprocessor was asked to render an export, which seems to be the only case on which // the post processor is called with a top-level Markdown Preview View div. @@ -12,6 +12,15 @@ function detectExport(el: HTMLElement, ctx: MarkdownPostProcessorContext, setPre type SetPreviewDirection = (path: string, markdownPreviewElement: HTMLDivElement) => void; +function applyMixedContentRTL(el: HTMLElement) { + const elements = el.querySelectorAll('p, li, h1, h2, h3, h4, h5, h6, td, th, blockquote'); + for (const element of elements) { + if (hasRtlChar(element.textContent || '')) { + (element as HTMLElement).setAttribute('dir', 'rtl'); + } + } +} + /* * This Markdown post-processor handles the Reading view and other rendered components of notes. * It detects the direction for each node individually and adds corresponding CSS classes that are @@ -20,8 +29,12 @@ type SetPreviewDirection = (path: string, markdownPreviewElement: HTMLDivElement export const autoDirectionPostProcessor = ( el: HTMLElement, ctx: MarkdownPostProcessorContext, + autoRTLForMixedContent: boolean, setPreviewDirection: SetPreviewDirection, ) => { detectExport(el, ctx, setPreviewDirection); + if (autoRTLForMixedContent) { + applyMixedContentRTL(el); + } } diff --git a/direction.util.ts b/direction.util.ts index d4c86c6..03a2914 100644 --- a/direction.util.ts +++ b/direction.util.ts @@ -3,3 +3,9 @@ export const LTR_CLASS = 'is-ltr'; export const RTL_CLASS = 'is-rtl'; export const AUTO_CLASS = 'is-auto'; +export const RTL_CHAR_REGEX = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/; + +export function hasRtlChar(text: string): boolean { + return RTL_CHAR_REGEX.test(text); +} + diff --git a/main.ts b/main.ts index 8050d37..9b8d105 100644 --- a/main.ts +++ b/main.ts @@ -33,7 +33,7 @@ export default class RtlPlugin extends Plugin { // extensions and can override the direction that they set. this.registerEditorExtension(Prec.lowest(this.editorPlugin)); this.registerMarkdownPostProcessor((el, ctx) => { - autoDirectionPostProcessor(el, ctx, (path, markdownPreviewElement) => this.setPreviewDirectionByFileSettings(path, markdownPreviewElement)); + autoDirectionPostProcessor(el, ctx, this.settings.autoRTLForMixedContent, (path, markdownPreviewElement) => this.setPreviewDirectionByFileSettings(path, markdownPreviewElement)); }); this.addSettingTab(new RtlSettingsTab(this.app, this)); diff --git a/settingsTab.ts b/settingsTab.ts index d2d50ad..8f2a9ad 100644 --- a/settingsTab.ts +++ b/settingsTab.ts @@ -10,6 +10,7 @@ export type Settings = { setYamlDirection: boolean; preventRTLFrontmatter: boolean; // Frontmatter == Raw YAML statusBar: boolean; + autoRTLForMixedContent: boolean; }; export const DEFAULT_SETTINGS: Settings = { @@ -19,7 +20,8 @@ export const DEFAULT_SETTINGS: Settings = { setNoteTitleDirection: true, setYamlDirection: false, preventRTLFrontmatter: false, - statusBar: true + statusBar: true, + autoRTLForMixedContent: false }; export class RtlSettingsTab extends PluginSettingTab { @@ -94,6 +96,16 @@ export class RtlSettingsTab extends PluginSettingTab { this.plugin.adjustDirectionToActiveView(); })); + new Setting(containerEl) + .setName('Auto-RTL for mixed LTR/RTL content') + .setDesc('When in auto mode, lines containing RTL scripts (Arabic, Hebrew, Persian, etc.) will be treated as RTL regardless of their first directional character.') + .addToggle(toggle => toggle.setValue(this.settings.autoRTLForMixedContent ?? false) + .onChange((value) => { + this.settings.autoRTLForMixedContent = value; + this.plugin.saveSettings(); + this.plugin.adjustDirectionToActiveView(); + })); + new Setting(containerEl) .setName('Show status bar item') .setDesc('Show a clickable status bar item showing the current direction.')