diff --git a/desktop-app/resources/index.html b/desktop-app/resources/index.html
index 9bfbf0c..fccf41e 100644
--- a/desktop-app/resources/index.html
+++ b/desktop-app/resources/index.html
@@ -269,6 +269,11 @@
Menu
+
+
+
+
+
diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js
index 1644be0..aeb5974 100644
--- a/desktop-app/resources/js/script.js
+++ b/desktop-app/resources/js/script.js
@@ -290,7 +290,7 @@ document.addEventListener("DOMContentLoaded", function () {
path: "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z",
},
};
- const GITHUB_ALERT_MARKER_REGEX = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\](?:\s+|$)/i;
+ const GITHUB_ALERT_MARKER_REGEX = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\](?:(?:\s| |
)+|$)/i;
function enhanceGitHubAlerts(container) {
if (!container) return;
@@ -306,8 +306,8 @@ document.addEventListener("DOMContentLoaded", function () {
}
if (!firstParagraph) return;
- const firstParagraphHtml = firstParagraph.innerHTML.trim();
- const markerMatch = firstParagraphHtml.match(GITHUB_ALERT_MARKER_REGEX);
+ const firstParagraphHtml = firstParagraph.innerHTML.trim();
+ const markerMatch = firstParagraphHtml.match(GITHUB_ALERT_MARKER_REGEX);
if (!markerMatch) return;
const alertType = markerMatch[1].toLowerCase();
@@ -336,9 +336,9 @@ document.addEventListener("DOMContentLoaded", function () {
blockquote.insertBefore(title, blockquote.firstChild);
- const remainingHtml = firstParagraphHtml
- .replace(GITHUB_ALERT_MARKER_REGEX, "")
- .trim();
+ const remainingHtml = firstParagraphHtml
+ .replace(GITHUB_ALERT_MARKER_REGEX, "")
+ .trim();
if (remainingHtml) {
firstParagraph.innerHTML = remainingHtml;
} else {
@@ -1076,7 +1076,7 @@ This is a fully client-side application. Your content never leaves your browser
const html = tableHtml + marked.parse(referenceData.cleanedMarkdown);
const sanitizedHtml = DOMPurify.sanitize(html, {
ADD_TAGS: ['mjx-container'],
- ADD_ATTR: ['id', 'class', 'style'],
+ ADD_ATTR: ['id', 'class', 'style', 'align'],
ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel|blob):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
});
markdownPreview.innerHTML = sanitizedHtml;
@@ -2168,6 +2168,30 @@ This is a fully client-side application. Your content never leaves your browser
});
}
+ function insertAlignmentBlock(align) {
+ const allowedAlignments = new Set(['left', 'center', 'right']);
+ const isAllowed = allowedAlignments.has(align);
+ if (!isAllowed) {
+ console.warn('Unsupported alignment:', align);
+ return;
+ }
+ const safeAlign = align;
+ const value = markdownEditor.value;
+ const start = markdownEditor.selectionStart;
+ const end = markdownEditor.selectionEnd;
+ const selected = value.slice(start, end);
+ const hasSelection = start !== end;
+ const blockStart = `
\n`;
+ const blockEnd = `\n
`;
+ const block = `${blockStart}${hasSelection ? selected : ''}${blockEnd}`;
+ const needsLeadingBreak = start > 0 && value[start - 1] !== '\n';
+ const needsTrailingBreak = end < value.length && value[end] !== '\n';
+ const replacement = (needsLeadingBreak ? '\n' : '') + block + (needsTrailingBreak ? '\n' : '');
+ const contentStart = start + (needsLeadingBreak ? 1 : 0) + blockStart.length;
+ const contentEnd = contentStart + (hasSelection ? selected.length : 0);
+ replaceEditorRange(start, end, replacement, contentStart, hasSelection ? contentEnd : contentStart);
+ }
+
function insertMarkdownBlock(block, startOverride, endOverride) {
const value = markdownEditor.value;
const start = typeof startOverride === 'number' ? startOverride : markdownEditor.selectionStart;
@@ -3348,6 +3372,9 @@ This is a fully client-side application. Your content never leaves your browser
else if (action === 'strike') wrapEditorSelection('~~', '~~', 'struck text');
else if (action === 'italic') wrapEditorSelection('*', '*', 'italic text');
else if (action === 'quote') transformEditorLines(function(line) { return line ? '> ' + line.replace(/^>\s?/, '') : '>'; });
+ else if (action === 'align-left') insertAlignmentBlock('left');
+ else if (action === 'align-center') insertAlignmentBlock('center');
+ else if (action === 'align-right') insertAlignmentBlock('right');
else if (action === 'title-case') transformSelectionOrCurrentLine(toTitleCase);
else if (action === 'uppercase') transformSelectionOrCurrentLine(function(text) { return text.toUpperCase(); });
else if (action === 'lowercase') transformSelectionOrCurrentLine(function(text) { return text.toLowerCase(); });
@@ -3720,7 +3747,7 @@ This is a fully client-side application. Your content never leaves your browser
const html = marked.parse(markdown);
const sanitizedHtml = DOMPurify.sanitize(html, {
ADD_TAGS: ['mjx-container'],
- ADD_ATTR: ['id', 'class', 'style']
+ ADD_ATTR: ['id', 'class', 'style', 'align']
});
const tempContainer = document.createElement("div");
tempContainer.innerHTML = sanitizedHtml;
@@ -4338,7 +4365,7 @@ This is a fully client-side application. Your content never leaves your browser
const html = marked.parse(markdown);
const sanitizedHtml = DOMPurify.sanitize(html, {
ADD_TAGS: ['mjx-container', 'svg', 'path', 'g', 'marker', 'defs', 'pattern', 'clipPath'],
- ADD_ATTR: ['id', 'class', 'style', 'viewBox', 'd', 'fill', 'stroke', 'transform', 'marker-end', 'marker-start']
+ ADD_ATTR: ['id', 'class', 'style', 'align', 'viewBox', 'd', 'fill', 'stroke', 'transform', 'marker-end', 'marker-start']
});
const tempElement = document.createElement("div");
diff --git a/index.html b/index.html
index 0f0fddf..939caab 100644
--- a/index.html
+++ b/index.html
@@ -135,6 +135,9 @@
Markdown Viewer
Share
+
@@ -231,6 +234,9 @@
Menu
Share
+
@@ -266,6 +272,11 @@
Menu
+
+
+
+
+
diff --git a/script.js b/script.js
index 1644be0..2d8ca04 100644
--- a/script.js
+++ b/script.js
@@ -21,6 +21,7 @@ document.addEventListener("DOMContentLoaded", function () {
const markdownPreview = document.getElementById("markdown-preview");
const markdownFormatToolbar = document.getElementById("markdown-format-toolbar");
const themeToggle = document.getElementById("theme-toggle");
+ const directionToggle = document.getElementById("direction-toggle");
const importFromFileButton = document.getElementById("import-from-file");
const importFromGithubButton = document.getElementById("import-from-github");
const fileInput = document.getElementById("file-input");
@@ -66,6 +67,7 @@ document.addEventListener("DOMContentLoaded", function () {
const mobileExportPdf = document.getElementById("mobile-export-pdf");
const mobileCopyMarkdown = document.getElementById("mobile-copy-markdown");
const mobileThemeToggle = document.getElementById("mobile-theme-toggle");
+ const mobileDirectionToggle = document.getElementById("mobile-direction-toggle");
const shareButton = document.getElementById("share-button");
const mobileShareButton = document.getElementById("mobile-share-button");
const githubImportModal = document.getElementById("github-import-modal");
@@ -209,6 +211,30 @@ document.addEventListener("DOMContentLoaded", function () {
? '
'
: '
';
+ function updateDirectionToggleUI(direction) {
+ const isRtl = direction === "rtl";
+ const toggleLabel = isRtl ? "Switch to LTR" : "Switch to RTL";
+ if (directionToggle) {
+ directionToggle.innerHTML = isRtl
+ ? '
'
+ : '
';
+ directionToggle.setAttribute("title", toggleLabel);
+ directionToggle.setAttribute("aria-label", toggleLabel);
+ directionToggle.setAttribute("aria-pressed", isRtl.toString());
+ }
+ if (mobileDirectionToggle) {
+ const icon = isRtl
+ ? '
'
+ : '
';
+ mobileDirectionToggle.innerHTML = `${icon} ${toggleLabel}`;
+ }
+ }
+
+ const savedDirection = loadGlobalState().direction;
+ const initialDirection = savedDirection === "rtl" ? "rtl" : "ltr";
+ document.documentElement.setAttribute("dir", initialDirection);
+ updateDirectionToggleUI(initialDirection);
+
const initMermaid = () => {
const currentTheme = document.documentElement.getAttribute("data-theme");
const mermaidTheme = currentTheme === "dark" ? "dark" : "default";
@@ -290,7 +316,7 @@ document.addEventListener("DOMContentLoaded", function () {
path: "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z",
},
};
- const GITHUB_ALERT_MARKER_REGEX = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\](?:\s+|$)/i;
+ const GITHUB_ALERT_MARKER_REGEX = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\](?:(?:\s| |
)+|$)/i;
function enhanceGitHubAlerts(container) {
if (!container) return;
@@ -306,8 +332,8 @@ document.addEventListener("DOMContentLoaded", function () {
}
if (!firstParagraph) return;
- const firstParagraphHtml = firstParagraph.innerHTML.trim();
- const markerMatch = firstParagraphHtml.match(GITHUB_ALERT_MARKER_REGEX);
+ const firstParagraphHtml = firstParagraph.innerHTML.trim();
+ const markerMatch = firstParagraphHtml.match(GITHUB_ALERT_MARKER_REGEX);
if (!markerMatch) return;
const alertType = markerMatch[1].toLowerCase();
@@ -336,9 +362,9 @@ document.addEventListener("DOMContentLoaded", function () {
blockquote.insertBefore(title, blockquote.firstChild);
- const remainingHtml = firstParagraphHtml
- .replace(GITHUB_ALERT_MARKER_REGEX, "")
- .trim();
+ const remainingHtml = firstParagraphHtml
+ .replace(GITHUB_ALERT_MARKER_REGEX, "")
+ .trim();
if (remainingHtml) {
firstParagraph.innerHTML = remainingHtml;
} else {
@@ -1076,7 +1102,7 @@ This is a fully client-side application. Your content never leaves your browser
const html = tableHtml + marked.parse(referenceData.cleanedMarkdown);
const sanitizedHtml = DOMPurify.sanitize(html, {
ADD_TAGS: ['mjx-container'],
- ADD_ATTR: ['id', 'class', 'style'],
+ ADD_ATTR: ['id', 'class', 'style', 'align'],
ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel|blob):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
});
markdownPreview.innerHTML = sanitizedHtml;
@@ -2168,6 +2194,30 @@ This is a fully client-side application. Your content never leaves your browser
});
}
+ function insertAlignmentBlock(align) {
+ const allowedAlignments = new Set(['left', 'center', 'right']);
+ const isAllowed = allowedAlignments.has(align);
+ if (!isAllowed) {
+ console.warn('Unsupported alignment:', align);
+ return;
+ }
+ const safeAlign = align;
+ const value = markdownEditor.value;
+ const start = markdownEditor.selectionStart;
+ const end = markdownEditor.selectionEnd;
+ const selected = value.slice(start, end);
+ const hasSelection = start !== end;
+ const blockStart = `
\n`;
+ const blockEnd = `\n
`;
+ const block = `${blockStart}${hasSelection ? selected : ''}${blockEnd}`;
+ const needsLeadingBreak = start > 0 && value[start - 1] !== '\n';
+ const needsTrailingBreak = end < value.length && value[end] !== '\n';
+ const replacement = (needsLeadingBreak ? '\n' : '') + block + (needsTrailingBreak ? '\n' : '');
+ const contentStart = start + (needsLeadingBreak ? 1 : 0) + blockStart.length;
+ const contentEnd = contentStart + (hasSelection ? selected.length : 0);
+ replaceEditorRange(start, end, replacement, contentStart, hasSelection ? contentEnd : contentStart);
+ }
+
function insertMarkdownBlock(block, startOverride, endOverride) {
const value = markdownEditor.value;
const start = typeof startOverride === 'number' ? startOverride : markdownEditor.selectionStart;
@@ -3348,6 +3398,9 @@ This is a fully client-side application. Your content never leaves your browser
else if (action === 'strike') wrapEditorSelection('~~', '~~', 'struck text');
else if (action === 'italic') wrapEditorSelection('*', '*', 'italic text');
else if (action === 'quote') transformEditorLines(function(line) { return line ? '> ' + line.replace(/^>\s?/, '') : '>'; });
+ else if (action === 'align-left') insertAlignmentBlock('left');
+ else if (action === 'align-center') insertAlignmentBlock('center');
+ else if (action === 'align-right') insertAlignmentBlock('right');
else if (action === 'title-case') transformSelectionOrCurrentLine(toTitleCase);
else if (action === 'uppercase') transformSelectionOrCurrentLine(function(text) { return text.toUpperCase(); });
else if (action === 'lowercase') transformSelectionOrCurrentLine(function(text) { return text.toLowerCase(); });
@@ -3530,6 +3583,19 @@ This is a fully client-side application. Your content never leaves your browser
mobileExportHtml.addEventListener("click", () => exportHtml.click());
mobileExportPdf.addEventListener("click", () => exportPdf.click());
mobileCopyMarkdown.addEventListener("click", () => copyMarkdownButton.click());
+ if (mobileDirectionToggle) {
+ mobileDirectionToggle.addEventListener("click", () => {
+ if (directionToggle) {
+ directionToggle.click();
+ } else {
+ const direction =
+ document.documentElement.getAttribute("dir") === "rtl" ? "ltr" : "rtl";
+ document.documentElement.setAttribute("dir", direction);
+ saveGlobalState({ direction });
+ updateDirectionToggleUI(direction);
+ }
+ });
+ }
mobileThemeToggle.addEventListener("click", () => {
themeToggle.click();
mobileThemeToggle.innerHTML = themeToggle.innerHTML + " Toggle Dark Mode";
@@ -3635,6 +3701,15 @@ This is a fully client-side application. Your content never leaves your browser
});
previewPane.addEventListener("scroll", syncPreviewToEditor);
toggleSyncButton.addEventListener("click", toggleSyncScrolling);
+ if (directionToggle) {
+ directionToggle.addEventListener("click", function () {
+ const direction =
+ document.documentElement.getAttribute("dir") === "rtl" ? "ltr" : "rtl";
+ document.documentElement.setAttribute("dir", direction);
+ saveGlobalState({ direction });
+ updateDirectionToggleUI(direction);
+ });
+ }
themeToggle.addEventListener("click", function () {
const theme =
document.documentElement.getAttribute("data-theme") === "dark"
@@ -3720,7 +3795,7 @@ This is a fully client-side application. Your content never leaves your browser
const html = marked.parse(markdown);
const sanitizedHtml = DOMPurify.sanitize(html, {
ADD_TAGS: ['mjx-container'],
- ADD_ATTR: ['id', 'class', 'style']
+ ADD_ATTR: ['id', 'class', 'style', 'align']
});
const tempContainer = document.createElement("div");
tempContainer.innerHTML = sanitizedHtml;
@@ -4338,7 +4413,7 @@ This is a fully client-side application. Your content never leaves your browser
const html = marked.parse(markdown);
const sanitizedHtml = DOMPurify.sanitize(html, {
ADD_TAGS: ['mjx-container', 'svg', 'path', 'g', 'marker', 'defs', 'pattern', 'clipPath'],
- ADD_ATTR: ['id', 'class', 'style', 'viewBox', 'd', 'fill', 'stroke', 'transform', 'marker-end', 'marker-start']
+ ADD_ATTR: ['id', 'class', 'style', 'align', 'viewBox', 'd', 'fill', 'stroke', 'transform', 'marker-end', 'marker-start']
});
const tempElement = document.createElement("div");
diff --git a/styles.css b/styles.css
index efa662b..ce26589 100644
--- a/styles.css
+++ b/styles.css
@@ -2547,3 +2547,69 @@ a:focus {
background-color: var(--button-bg);
white-space: nowrap;
}
+
+/* ========================================
+ RTL SUPPORT
+ ======================================== */
+
+[dir="rtl"] body {
+ direction: rtl;
+}
+
+[dir="rtl"] #markdown-editor,
+[dir="rtl"] .markdown-body {
+ direction: rtl;
+ text-align: right;
+}
+
+[dir="rtl"] .markdown-body pre,
+[dir="rtl"] .markdown-body code,
+[dir="rtl"] .fm-complex {
+ direction: ltr;
+ text-align: left;
+}
+
+[dir="rtl"] .line-numbers {
+ left: auto;
+ right: 20px;
+ padding: 10px 0 10px 8px;
+ text-align: left;
+ border-right: none;
+ border-left: 1px solid var(--border-color);
+}
+
+[dir="rtl"] #markdown-editor {
+ padding-left: 10px;
+ padding-right: calc(10px + var(--line-number-gutter));
+}
+
+[dir="rtl"] .editor-highlight-layer {
+ inset: 20px calc(20px + var(--line-number-gutter)) 20px 0;
+}
+
+[dir="rtl"] .mobile-menu-item,
+[dir="rtl"] .tab-menu-item,
+[dir="rtl"] .modal-header .reset-modal-message,
+[dir="rtl"] .reset-modal-field,
+[dir="rtl"] .alert-option,
+[dir="rtl"] .github-import-error,
+[dir="rtl"] #github-import-modal .reset-modal-message,
+[dir="rtl"] .github-tree-file-btn,
+[dir="rtl"] .frontmatter-table td {
+ text-align: right;
+}
+
+[dir="rtl"] .github-import-tree ul {
+ padding-left: 0;
+ padding-right: 18px;
+}
+
+[dir="rtl"] .github-import-tree > ul {
+ padding-right: 4px;
+}
+
+[dir="rtl"] .markdown-body .markdown-alert,
+[dir="rtl"] .alert-preview .markdown-alert {
+ border-left: 0;
+ border-right: 0.25em solid currentColor;
+}