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.')