diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js
index 3781355..4c29c8f 100644
--- a/desktop-app/resources/js/script.js
+++ b/desktop-app/resources/js/script.js
@@ -9,7 +9,7 @@ document.addEventListener("DOMContentLoaded", function () {
// View Mode State - Story 1.1
let currentViewMode = 'split'; // 'editor', 'split', or 'preview'
- const APP_VERSION = '3.5.3';
+ const APP_VERSION = '3.5.4';
let activeModal = null;
let lastFocusedElement = null;
let isFindModalOpen = false;
@@ -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,32 @@ document.addEventListener("DOMContentLoaded", function () {
? ''
: '';
+ function updateDirectionToggleUI(direction) {
+ const isRtl = direction === "rtl";
+ const toggleLabel = isRtl ? "Switch to LTR" : "Switch to RTL";
+ if (directionToggle) {
+ directionToggle.textContent = isRtl ? "R" : "L";
+ 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";
+ function applyDirectionToContent(direction) {
+ if (markdownEditor) markdownEditor.setAttribute("dir", direction);
+ if (markdownPreview) markdownPreview.setAttribute("dir", direction);
+ }
+ applyDirectionToContent(initialDirection);
+ updateDirectionToggleUI(initialDirection);
+
const initMermaid = () => {
const currentTheme = document.documentElement.getAttribute("data-theme");
const mermaidTheme = currentTheme === "dark" ? "dark" : "default";
@@ -247,6 +275,150 @@ document.addEventListener("DOMContentLoaded", function () {
const renderer = new marked.Renderer();
const BLOCK_MATH_MARKER_PATTERN = /^\$\$/m;
const BLOCK_MATH_PATTERN = /^\$\$[ \t]*\n?([\s\S]*?)\n?\$\$[ \t]*(?:\n|$)/;
+ const DEFINITION_LIST_ITEM_PATTERN = /^:[ \t]+(.*)$/;
+ const SUPERSCRIPT_PATTERN = /^\^(?!\s)([^^\n]*?\S)\^(?!\^)/;
+ const SUBSCRIPT_PATTERN = /^~(?!~)(?!\s)([^~\n]*?\S)~(?!~)/;
+ const HIGHLIGHT_PATTERN = /^==(?=\S)([\s\S]*?\S)==/;
+ const MARKDOWN_LIST_MARKER_PATTERN = /^(\s*)(?:[-*+]\s+|\d+\.\s+|>\s+)/;
+ const EMPTY_LINE_PATTERN = /^\s*$/;
+ const footnoteDefinitions = new Map();
+ const footnoteOrder = [];
+ const footnoteRefCounts = new Map();
+ const footnoteFirstRefId = new Map();
+ let anonymousFootnoteCounter = 0;
+
+ function resetExtendedMarkdownState() {
+ footnoteDefinitions.clear();
+ footnoteOrder.length = 0;
+ footnoteRefCounts.clear();
+ footnoteFirstRefId.clear();
+ anonymousFootnoteCounter = 0;
+ }
+
+ function normalizeFootnoteId(id) {
+ const normalized = String(id || "")
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9_-]+/g, "-")
+ .replace(/^-+|-+$/g, "");
+ if (normalized) {
+ return normalized;
+ }
+
+ anonymousFootnoteCounter += 1;
+ return `footnote-${anonymousFootnoteCounter}`;
+ }
+
+ function escapeHtmlAttribute(value) {
+ return String(value)
+ .replace(/&/g, "&")
+ .replace(/"/g, """)
+ .replace(/'/g, "'")
+ .replace(//g, ">");
+ }
+
+ function renderDefinitionContent(content) {
+ return content
+ .split(/\n{2,}/)
+ .map((paragraph) => paragraph.trim())
+ .filter(Boolean)
+ .map((paragraph) => {
+ const renderedParagraph = marked.parseInline(paragraph);
+ const safeParagraph = typeof DOMPurify !== "undefined"
+ ? DOMPurify.sanitize(renderedParagraph)
+ : renderedParagraph;
+ return `
${safeParagraph}
`;
+ })
+ .join("");
+ }
+
+ function extractFootnoteDefinitions(markdown) {
+ const lines = markdown.split("\n");
+ const preservedLines = [];
+ let index = 0;
+
+ while (index < lines.length) {
+ const match = /^\[\^([^\]\n]+)\]:[ \t]*(.*)$/.exec(lines[index]);
+ if (!match) {
+ preservedLines.push(lines[index]);
+ index += 1;
+ continue;
+ }
+
+ const id = match[1].trim();
+ const definitionLines = [match[2] || ""];
+ index += 1;
+
+ while (index < lines.length) {
+ const line = lines[index];
+ const indentedMatch = /^(?: {4}|\t)(.*)$/.exec(line);
+ if (indentedMatch) {
+ definitionLines.push(indentedMatch[1]);
+ index += 1;
+ continue;
+ }
+
+ if (line === "" && /^(?: {4}|\t)/.test(lines[index + 1] || "")) {
+ definitionLines.push("");
+ index += 1;
+ continue;
+ }
+
+ break;
+ }
+
+ footnoteDefinitions.set(id, definitionLines.join("\n").trim());
+ }
+
+ return preservedLines.join("\n");
+ }
+
+ function applyFootnotes(markdown) {
+ const markdownWithReferences = markdown.replace(/\[\^([^\]\n]+)\]/g, function(match, idText) {
+ const id = idText.trim();
+ if (!id) {
+ return match;
+ }
+
+ if (!footnoteOrder.includes(id)) {
+ footnoteOrder.push(id);
+ }
+
+ const refCount = (footnoteRefCounts.get(id) || 0) + 1;
+ footnoteRefCounts.set(id, refCount);
+
+ const normalizedId = normalizeFootnoteId(id);
+ const refId = `fnref-${normalizedId}${refCount > 1 ? `-${refCount}` : ""}`;
+ if (!footnoteFirstRefId.has(id)) {
+ footnoteFirstRefId.set(id, refId);
+ }
+
+ const noteNumber = footnoteOrder.indexOf(id) + 1;
+ const safeRefId = escapeHtmlAttribute(refId);
+ const safeNormalizedId = escapeHtmlAttribute(normalizedId);
+ return ``;
+ });
+
+ const footnotesHtml = footnoteOrder
+ .filter((id) => footnoteDefinitions.has(id))
+ .map((id) => {
+ const normalizedId = normalizeFootnoteId(id);
+ const noteHtml = renderDefinitionContent(footnoteDefinitions.get(id) || "");
+ const backRefId = footnoteFirstRefId.get(id) || `fnref-${normalizedId}`;
+ const safeNormalizedId = escapeHtmlAttribute(normalizedId);
+ const safeBackRefId = escapeHtmlAttribute(backRefId);
+ return `${noteHtml}`;
+ })
+ .join("");
+
+ if (!footnotesHtml) {
+ return markdownWithReferences;
+ }
+
+ return `${markdownWithReferences}\n\n`;
+ }
+
const blockMathExtension = {
name: 'blockMath',
level: 'block',
@@ -272,6 +444,163 @@ document.addEventListener("DOMContentLoaded", function () {
return `$$\n${token.text}\n$$
\n`;
}
};
+ const definitionListExtension = {
+ name: "definitionList",
+ level: "block",
+ start(src) {
+ const match = src.match(/\n:[ \t]+/);
+ if (!match) {
+ return undefined;
+ }
+ return match.index + 1;
+ },
+ tokenizer(src) {
+ const lines = src.split("\n");
+ if (lines.length < 2) {
+ return undefined;
+ }
+
+ const term = lines[0];
+ if (EMPTY_LINE_PATTERN.test(term) || MARKDOWN_LIST_MARKER_PATTERN.test(term)) {
+ return undefined;
+ }
+
+ if (!DEFINITION_LIST_ITEM_PATTERN.test(lines[1])) {
+ return undefined;
+ }
+
+ const definitions = [];
+ const rawLines = [term];
+ let index = 1;
+ while (index < lines.length) {
+ const itemMatch = DEFINITION_LIST_ITEM_PATTERN.exec(lines[index]);
+ if (!itemMatch) {
+ break;
+ }
+
+ rawLines.push(lines[index]);
+ const definitionLines = [itemMatch[1]];
+ index += 1;
+
+ while (index < lines.length) {
+ const line = lines[index];
+ if (DEFINITION_LIST_ITEM_PATTERN.test(line)) {
+ break;
+ }
+ if (EMPTY_LINE_PATTERN.test(line)) {
+ const nextLine = lines[index + 1] || "";
+ if (/^(?: {2,}|\t)/.test(nextLine)) {
+ rawLines.push(line);
+ definitionLines.push("");
+ index += 1;
+ continue;
+ }
+ break;
+ }
+ const continuationMatch = /^(?: {2,}|\t)(.*)$/.exec(line);
+ if (!continuationMatch) {
+ break;
+ }
+
+ rawLines.push(line);
+ definitionLines.push(continuationMatch[1]);
+ index += 1;
+ }
+
+ definitions.push(definitionLines.join("\n").trim());
+ }
+
+ if (definitions.length === 0) {
+ return undefined;
+ }
+
+ let raw = rawLines.join("\n");
+ if (src.startsWith(raw + "\n")) {
+ raw += "\n";
+ }
+
+ return {
+ type: "definitionList",
+ raw: raw,
+ term: term.trim(),
+ definitions: definitions,
+ };
+ },
+ renderer(token) {
+ const termHtml = marked.parseInline(token.term);
+ const definitionHtml = token.definitions
+ .map((definition) => `${renderDefinitionContent(definition)}`)
+ .join("");
+ return `- ${termHtml}
${definitionHtml}
\n`;
+ },
+ };
+ const superscriptExtension = {
+ name: "superscript",
+ level: "inline",
+ start(src) {
+ const index = src.indexOf("^");
+ return index >= 0 ? index : undefined;
+ },
+ tokenizer(src) {
+ const match = SUPERSCRIPT_PATTERN.exec(src);
+ if (!match) {
+ return undefined;
+ }
+ return {
+ type: "superscript",
+ raw: match[0],
+ text: match[1],
+ };
+ },
+ renderer(token) {
+ return `${marked.parseInline(token.text)}`;
+ },
+ };
+ const subscriptExtension = {
+ name: "subscript",
+ level: "inline",
+ start(src) {
+ const index = src.indexOf("~");
+ return index >= 0 ? index : undefined;
+ },
+ tokenizer(src) {
+ const match = SUBSCRIPT_PATTERN.exec(src);
+ if (!match) {
+ return undefined;
+ }
+ return {
+ type: "subscript",
+ raw: match[0],
+ text: match[1],
+ };
+ },
+ renderer(token) {
+ return `${marked.parseInline(token.text)}`;
+ },
+ };
+ const highlightExtension = {
+ name: "highlight",
+ level: "inline",
+ start(src) {
+ const index = src.indexOf("==");
+ return index >= 0 ? index : undefined;
+ },
+ tokenizer(src) {
+ const match = HIGHLIGHT_PATTERN.exec(src);
+ if (!match) {
+ return undefined;
+ }
+ return {
+ type: "highlight",
+ raw: match[0],
+ text: match[1],
+ };
+ },
+ renderer(token) {
+ return `${marked.parseInline(token.text)}`;
+ },
+ };
+
renderer.code = function (code, language) {
if (language === 'mermaid') {
const uniqueId = 'mermaid-diagram-' + Math.random().toString(36).substr(2, 9);
@@ -286,7 +615,19 @@ document.addEventListener("DOMContentLoaded", function () {
};
marked.use({
- extensions: [blockMathExtension]
+ extensions: [
+ blockMathExtension,
+ definitionListExtension,
+ superscriptExtension,
+ subscriptExtension,
+ highlightExtension,
+ ],
+ hooks: {
+ preprocess(markdown) {
+ resetExtendedMarkdownState();
+ return applyFootnotes(extractFootnoteDefinitions(markdown));
+ },
+ },
});
marked.setOptions({
@@ -1106,8 +1447,8 @@ This is a fully client-side application. Your content never leaves your browser
const referenceData = extractReferenceDefinitions(body);
const html = tableHtml + marked.parse(referenceData.cleanedMarkdown);
const sanitizedHtml = DOMPurify.sanitize(html, {
- ADD_TAGS: ['mjx-container'],
- ADD_ATTR: ['id', 'class', 'style', 'align'],
+ ADD_TAGS: ['mjx-container', 'input'],
+ ADD_ATTR: ['id', 'class', 'style', 'align', 'type', 'checked', 'disabled'],
ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel|blob):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
});
markdownPreview.innerHTML = sanitizedHtml;
@@ -3588,6 +3929,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 currentDir = markdownEditor ? markdownEditor.getAttribute("dir") : "ltr";
+ const direction = currentDir === "rtl" ? "ltr" : "rtl";
+ applyDirectionToContent(direction);
+ saveGlobalState({ direction });
+ updateDirectionToggleUI(direction);
+ }
+ });
+ }
mobileThemeToggle.addEventListener("click", () => {
themeToggle.click();
mobileThemeToggle.innerHTML = themeToggle.innerHTML + " Toggle Dark Mode";
@@ -3693,6 +4047,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 currentDir = markdownEditor ? markdownEditor.getAttribute("dir") : "ltr";
+ const direction = currentDir === "rtl" ? "ltr" : "rtl";
+ applyDirectionToContent(direction);
+ saveGlobalState({ direction });
+ updateDirectionToggleUI(direction);
+ });
+ }
themeToggle.addEventListener("click", function () {
const theme =
document.documentElement.getAttribute("data-theme") === "dark"
@@ -3777,8 +4140,8 @@ This is a fully client-side application. Your content never leaves your browser
const markdown = markdownEditor.value;
const html = marked.parse(markdown);
const sanitizedHtml = DOMPurify.sanitize(html, {
- ADD_TAGS: ['mjx-container'],
- ADD_ATTR: ['id', 'class', 'style', 'align']
+ ADD_TAGS: ['mjx-container', 'input'],
+ ADD_ATTR: ['id', 'class', 'style', 'align', 'type', 'checked', 'disabled']
});
const tempContainer = document.createElement("div");
tempContainer.innerHTML = sanitizedHtml;
@@ -4395,8 +4758,8 @@ This is a fully client-side application. Your content never leaves your browser
const markdown = markdownEditor.value;
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', 'align', 'viewBox', 'd', 'fill', 'stroke', 'transform', 'marker-end', 'marker-start']
+ ADD_TAGS: ['mjx-container', 'svg', 'path', 'g', 'marker', 'defs', 'pattern', 'clipPath', 'input'],
+ ADD_ATTR: ['id', 'class', 'style', 'align', 'viewBox', 'd', 'fill', 'stroke', 'transform', 'marker-end', 'marker-start', 'type', 'checked', 'disabled']
});
const tempElement = document.createElement("div");
diff --git a/desktop-app/resources/styles.css b/desktop-app/resources/styles.css
index efa662b..ade2bde 100644
--- a/desktop-app/resources/styles.css
+++ b/desktop-app/resources/styles.css
@@ -192,6 +192,65 @@ body {
vertical-align: -0.1em;
}
+.markdown-body ul.contains-task-list,
+.markdown-body li.task-list-item {
+ list-style: none;
+}
+
+.markdown-body ul.contains-task-list {
+ padding-left: 0;
+}
+
+.markdown-body li.task-list-item input[type="checkbox"] {
+ margin: 0 0.5em 0.2em 0;
+ vertical-align: middle;
+ pointer-events: none;
+}
+
+.markdown-body li.task-list-item::marker {
+ content: "";
+}
+
+.markdown-body li:has(> input[type="checkbox"]) {
+ list-style: none;
+}
+
+.markdown-body li:has(> input[type="checkbox"])::marker {
+ content: "";
+}
+
+.markdown-body ul:has(> li > input[type="checkbox"]) {
+ list-style: none;
+ padding-left: 0;
+}
+
+.markdown-body .footnotes {
+ margin-top: 1.5rem;
+ font-size: 0.9em;
+}
+
+.markdown-body .footnotes ol {
+ padding-left: 1.5em;
+}
+
+.markdown-body .footnotes ol > li::marker {
+ content: "[" counter(list-item) "] ";
+ font-weight: 600;
+}
+
+.markdown-body .footnotes li > p {
+ margin: 0.2em 0;
+}
+
+.markdown-body .footnote-ref a,
+.markdown-body .footnote-backref {
+ text-decoration: none;
+}
+
+.markdown-body .footnote-backref {
+ margin-left: 0.4em;
+}
+
.markdown-body .markdown-alert {
padding: 0.5rem 1rem;
margin-bottom: 16px;
@@ -2547,3 +2606,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;
+}
diff --git a/script.js b/script.js
index e3f22be..4c29c8f 100644
--- a/script.js
+++ b/script.js
@@ -275,6 +275,150 @@ document.addEventListener("DOMContentLoaded", function () {
const renderer = new marked.Renderer();
const BLOCK_MATH_MARKER_PATTERN = /^\$\$/m;
const BLOCK_MATH_PATTERN = /^\$\$[ \t]*\n?([\s\S]*?)\n?\$\$[ \t]*(?:\n|$)/;
+ const DEFINITION_LIST_ITEM_PATTERN = /^:[ \t]+(.*)$/;
+ const SUPERSCRIPT_PATTERN = /^\^(?!\s)([^^\n]*?\S)\^(?!\^)/;
+ const SUBSCRIPT_PATTERN = /^~(?!~)(?!\s)([^~\n]*?\S)~(?!~)/;
+ const HIGHLIGHT_PATTERN = /^==(?=\S)([\s\S]*?\S)==/;
+ const MARKDOWN_LIST_MARKER_PATTERN = /^(\s*)(?:[-*+]\s+|\d+\.\s+|>\s+)/;
+ const EMPTY_LINE_PATTERN = /^\s*$/;
+ const footnoteDefinitions = new Map();
+ const footnoteOrder = [];
+ const footnoteRefCounts = new Map();
+ const footnoteFirstRefId = new Map();
+ let anonymousFootnoteCounter = 0;
+
+ function resetExtendedMarkdownState() {
+ footnoteDefinitions.clear();
+ footnoteOrder.length = 0;
+ footnoteRefCounts.clear();
+ footnoteFirstRefId.clear();
+ anonymousFootnoteCounter = 0;
+ }
+
+ function normalizeFootnoteId(id) {
+ const normalized = String(id || "")
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9_-]+/g, "-")
+ .replace(/^-+|-+$/g, "");
+ if (normalized) {
+ return normalized;
+ }
+
+ anonymousFootnoteCounter += 1;
+ return `footnote-${anonymousFootnoteCounter}`;
+ }
+
+ function escapeHtmlAttribute(value) {
+ return String(value)
+ .replace(/&/g, "&")
+ .replace(/"/g, """)
+ .replace(/'/g, "'")
+ .replace(//g, ">");
+ }
+
+ function renderDefinitionContent(content) {
+ return content
+ .split(/\n{2,}/)
+ .map((paragraph) => paragraph.trim())
+ .filter(Boolean)
+ .map((paragraph) => {
+ const renderedParagraph = marked.parseInline(paragraph);
+ const safeParagraph = typeof DOMPurify !== "undefined"
+ ? DOMPurify.sanitize(renderedParagraph)
+ : renderedParagraph;
+ return `${safeParagraph}
`;
+ })
+ .join("");
+ }
+
+ function extractFootnoteDefinitions(markdown) {
+ const lines = markdown.split("\n");
+ const preservedLines = [];
+ let index = 0;
+
+ while (index < lines.length) {
+ const match = /^\[\^([^\]\n]+)\]:[ \t]*(.*)$/.exec(lines[index]);
+ if (!match) {
+ preservedLines.push(lines[index]);
+ index += 1;
+ continue;
+ }
+
+ const id = match[1].trim();
+ const definitionLines = [match[2] || ""];
+ index += 1;
+
+ while (index < lines.length) {
+ const line = lines[index];
+ const indentedMatch = /^(?: {4}|\t)(.*)$/.exec(line);
+ if (indentedMatch) {
+ definitionLines.push(indentedMatch[1]);
+ index += 1;
+ continue;
+ }
+
+ if (line === "" && /^(?: {4}|\t)/.test(lines[index + 1] || "")) {
+ definitionLines.push("");
+ index += 1;
+ continue;
+ }
+
+ break;
+ }
+
+ footnoteDefinitions.set(id, definitionLines.join("\n").trim());
+ }
+
+ return preservedLines.join("\n");
+ }
+
+ function applyFootnotes(markdown) {
+ const markdownWithReferences = markdown.replace(/\[\^([^\]\n]+)\]/g, function(match, idText) {
+ const id = idText.trim();
+ if (!id) {
+ return match;
+ }
+
+ if (!footnoteOrder.includes(id)) {
+ footnoteOrder.push(id);
+ }
+
+ const refCount = (footnoteRefCounts.get(id) || 0) + 1;
+ footnoteRefCounts.set(id, refCount);
+
+ const normalizedId = normalizeFootnoteId(id);
+ const refId = `fnref-${normalizedId}${refCount > 1 ? `-${refCount}` : ""}`;
+ if (!footnoteFirstRefId.has(id)) {
+ footnoteFirstRefId.set(id, refId);
+ }
+
+ const noteNumber = footnoteOrder.indexOf(id) + 1;
+ const safeRefId = escapeHtmlAttribute(refId);
+ const safeNormalizedId = escapeHtmlAttribute(normalizedId);
+ return ``;
+ });
+
+ const footnotesHtml = footnoteOrder
+ .filter((id) => footnoteDefinitions.has(id))
+ .map((id) => {
+ const normalizedId = normalizeFootnoteId(id);
+ const noteHtml = renderDefinitionContent(footnoteDefinitions.get(id) || "");
+ const backRefId = footnoteFirstRefId.get(id) || `fnref-${normalizedId}`;
+ const safeNormalizedId = escapeHtmlAttribute(normalizedId);
+ const safeBackRefId = escapeHtmlAttribute(backRefId);
+ return `${noteHtml}`;
+ })
+ .join("");
+
+ if (!footnotesHtml) {
+ return markdownWithReferences;
+ }
+
+ return `${markdownWithReferences}\n\n`;
+ }
+
const blockMathExtension = {
name: 'blockMath',
level: 'block',
@@ -300,6 +444,163 @@ document.addEventListener("DOMContentLoaded", function () {
return `$$\n${token.text}\n$$
\n`;
}
};
+ const definitionListExtension = {
+ name: "definitionList",
+ level: "block",
+ start(src) {
+ const match = src.match(/\n:[ \t]+/);
+ if (!match) {
+ return undefined;
+ }
+ return match.index + 1;
+ },
+ tokenizer(src) {
+ const lines = src.split("\n");
+ if (lines.length < 2) {
+ return undefined;
+ }
+
+ const term = lines[0];
+ if (EMPTY_LINE_PATTERN.test(term) || MARKDOWN_LIST_MARKER_PATTERN.test(term)) {
+ return undefined;
+ }
+
+ if (!DEFINITION_LIST_ITEM_PATTERN.test(lines[1])) {
+ return undefined;
+ }
+
+ const definitions = [];
+ const rawLines = [term];
+ let index = 1;
+ while (index < lines.length) {
+ const itemMatch = DEFINITION_LIST_ITEM_PATTERN.exec(lines[index]);
+ if (!itemMatch) {
+ break;
+ }
+
+ rawLines.push(lines[index]);
+ const definitionLines = [itemMatch[1]];
+ index += 1;
+
+ while (index < lines.length) {
+ const line = lines[index];
+ if (DEFINITION_LIST_ITEM_PATTERN.test(line)) {
+ break;
+ }
+ if (EMPTY_LINE_PATTERN.test(line)) {
+ const nextLine = lines[index + 1] || "";
+ if (/^(?: {2,}|\t)/.test(nextLine)) {
+ rawLines.push(line);
+ definitionLines.push("");
+ index += 1;
+ continue;
+ }
+ break;
+ }
+ const continuationMatch = /^(?: {2,}|\t)(.*)$/.exec(line);
+ if (!continuationMatch) {
+ break;
+ }
+
+ rawLines.push(line);
+ definitionLines.push(continuationMatch[1]);
+ index += 1;
+ }
+
+ definitions.push(definitionLines.join("\n").trim());
+ }
+
+ if (definitions.length === 0) {
+ return undefined;
+ }
+
+ let raw = rawLines.join("\n");
+ if (src.startsWith(raw + "\n")) {
+ raw += "\n";
+ }
+
+ return {
+ type: "definitionList",
+ raw: raw,
+ term: term.trim(),
+ definitions: definitions,
+ };
+ },
+ renderer(token) {
+ const termHtml = marked.parseInline(token.term);
+ const definitionHtml = token.definitions
+ .map((definition) => `${renderDefinitionContent(definition)}`)
+ .join("");
+ return `- ${termHtml}
${definitionHtml}
\n`;
+ },
+ };
+ const superscriptExtension = {
+ name: "superscript",
+ level: "inline",
+ start(src) {
+ const index = src.indexOf("^");
+ return index >= 0 ? index : undefined;
+ },
+ tokenizer(src) {
+ const match = SUPERSCRIPT_PATTERN.exec(src);
+ if (!match) {
+ return undefined;
+ }
+ return {
+ type: "superscript",
+ raw: match[0],
+ text: match[1],
+ };
+ },
+ renderer(token) {
+ return `${marked.parseInline(token.text)}`;
+ },
+ };
+ const subscriptExtension = {
+ name: "subscript",
+ level: "inline",
+ start(src) {
+ const index = src.indexOf("~");
+ return index >= 0 ? index : undefined;
+ },
+ tokenizer(src) {
+ const match = SUBSCRIPT_PATTERN.exec(src);
+ if (!match) {
+ return undefined;
+ }
+ return {
+ type: "subscript",
+ raw: match[0],
+ text: match[1],
+ };
+ },
+ renderer(token) {
+ return `${marked.parseInline(token.text)}`;
+ },
+ };
+ const highlightExtension = {
+ name: "highlight",
+ level: "inline",
+ start(src) {
+ const index = src.indexOf("==");
+ return index >= 0 ? index : undefined;
+ },
+ tokenizer(src) {
+ const match = HIGHLIGHT_PATTERN.exec(src);
+ if (!match) {
+ return undefined;
+ }
+ return {
+ type: "highlight",
+ raw: match[0],
+ text: match[1],
+ };
+ },
+ renderer(token) {
+ return `${marked.parseInline(token.text)}`;
+ },
+ };
+
renderer.code = function (code, language) {
if (language === 'mermaid') {
const uniqueId = 'mermaid-diagram-' + Math.random().toString(36).substr(2, 9);
@@ -314,7 +615,19 @@ document.addEventListener("DOMContentLoaded", function () {
};
marked.use({
- extensions: [blockMathExtension]
+ extensions: [
+ blockMathExtension,
+ definitionListExtension,
+ superscriptExtension,
+ subscriptExtension,
+ highlightExtension,
+ ],
+ hooks: {
+ preprocess(markdown) {
+ resetExtendedMarkdownState();
+ return applyFootnotes(extractFootnoteDefinitions(markdown));
+ },
+ },
});
marked.setOptions({
@@ -1134,8 +1447,8 @@ This is a fully client-side application. Your content never leaves your browser
const referenceData = extractReferenceDefinitions(body);
const html = tableHtml + marked.parse(referenceData.cleanedMarkdown);
const sanitizedHtml = DOMPurify.sanitize(html, {
- ADD_TAGS: ['mjx-container'],
- ADD_ATTR: ['id', 'class', 'style', 'align'],
+ ADD_TAGS: ['mjx-container', 'input'],
+ ADD_ATTR: ['id', 'class', 'style', 'align', 'type', 'checked', 'disabled'],
ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel|blob):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
});
markdownPreview.innerHTML = sanitizedHtml;
@@ -3827,8 +4140,8 @@ This is a fully client-side application. Your content never leaves your browser
const markdown = markdownEditor.value;
const html = marked.parse(markdown);
const sanitizedHtml = DOMPurify.sanitize(html, {
- ADD_TAGS: ['mjx-container'],
- ADD_ATTR: ['id', 'class', 'style', 'align']
+ ADD_TAGS: ['mjx-container', 'input'],
+ ADD_ATTR: ['id', 'class', 'style', 'align', 'type', 'checked', 'disabled']
});
const tempContainer = document.createElement("div");
tempContainer.innerHTML = sanitizedHtml;
@@ -4445,8 +4758,8 @@ This is a fully client-side application. Your content never leaves your browser
const markdown = markdownEditor.value;
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', 'align', 'viewBox', 'd', 'fill', 'stroke', 'transform', 'marker-end', 'marker-start']
+ ADD_TAGS: ['mjx-container', 'svg', 'path', 'g', 'marker', 'defs', 'pattern', 'clipPath', 'input'],
+ ADD_ATTR: ['id', 'class', 'style', 'align', 'viewBox', 'd', 'fill', 'stroke', 'transform', 'marker-end', 'marker-start', 'type', 'checked', 'disabled']
});
const tempElement = document.createElement("div");
diff --git a/styles.css b/styles.css
index ce26589..ade2bde 100644
--- a/styles.css
+++ b/styles.css
@@ -192,6 +192,65 @@ body {
vertical-align: -0.1em;
}
+.markdown-body ul.contains-task-list,
+.markdown-body li.task-list-item {
+ list-style: none;
+}
+
+.markdown-body ul.contains-task-list {
+ padding-left: 0;
+}
+
+.markdown-body li.task-list-item input[type="checkbox"] {
+ margin: 0 0.5em 0.2em 0;
+ vertical-align: middle;
+ pointer-events: none;
+}
+
+.markdown-body li.task-list-item::marker {
+ content: "";
+}
+
+.markdown-body li:has(> input[type="checkbox"]) {
+ list-style: none;
+}
+
+.markdown-body li:has(> input[type="checkbox"])::marker {
+ content: "";
+}
+
+.markdown-body ul:has(> li > input[type="checkbox"]) {
+ list-style: none;
+ padding-left: 0;
+}
+
+.markdown-body .footnotes {
+ margin-top: 1.5rem;
+ font-size: 0.9em;
+}
+
+.markdown-body .footnotes ol {
+ padding-left: 1.5em;
+}
+
+.markdown-body .footnotes ol > li::marker {
+ content: "[" counter(list-item) "] ";
+ font-weight: 600;
+}
+
+.markdown-body .footnotes li > p {
+ margin: 0.2em 0;
+}
+
+.markdown-body .footnote-ref a,
+.markdown-body .footnote-backref {
+ text-decoration: none;
+}
+
+.markdown-body .footnote-backref {
+ margin-left: 0.4em;
+}
+
.markdown-body .markdown-alert {
padding: 0.5rem 1rem;
margin-bottom: 16px;