From 7b75019518e30b7eaf8c8c9cef3931f3bf3f8e45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 18:24:28 +0000 Subject: [PATCH 1/7] Initial plan From 272b5e2fc377e8a4af212fa702cc1ba25c40b48d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 18:34:40 +0000 Subject: [PATCH 2/7] fix: support superscript subscript highlight task list footnotes and definition lists Agent-Logs-Url: https://github.com/ThisIs-Developer/Markdown-Viewer/sessions/5057b2b5-2f98-40f3-9b51-ea87cc1274a5 Co-authored-by: ThisIs-Developer <109382325+ThisIs-Developer@users.noreply.github.com> --- desktop-app/resources/js/script.js | 352 ++++++++++++++++++++++++++++- desktop-app/resources/styles.css | 81 +++++++ script.js | 300 +++++++++++++++++++++++- styles.css | 15 ++ 4 files changed, 733 insertions(+), 15 deletions(-) diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js index 3781355..79b067c 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,123 @@ 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 = /^\^([^^\n]+)\^/; + const SUBSCRIPT_PATTERN = /^~(?!~)([^~\n]+)~/; + 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(); + + function resetExtendedMarkdownState() { + footnoteDefinitions.clear(); + footnoteOrder.length = 0; + footnoteRefCounts.clear(); + footnoteFirstRefId.clear(); + } + + function normalizeFootnoteId(id) { + return String(id || "") + .trim() + .toLowerCase() + .replace(/[^a-z0-9_-]+/g, "-") + .replace(/^-+|-+$/g, "") || "footnote"; + } + + function renderDefinitionContent(content) { + return content + .split(/\n{2,}/) + .map((paragraph) => paragraph.trim()) + .filter(Boolean) + .map((paragraph) => `

${marked.parseInline(paragraph)}

`) + .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; + return `${noteNumber}`; + }); + + 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}`; + return `
  • ${noteHtml}
  • `; + }) + .join(""); + + if (!footnotesHtml) { + return markdownWithReferences; + } + + return `${markdownWithReferences}\n\n

      ${footnotesHtml}
    `; + } + const blockMathExtension = { name: 'blockMath', level: 'block', @@ -272,6 +417,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 +588,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 +1420,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 +3902,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 +4020,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 +4113,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 +4731,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..9ab7c44 100644 --- a/desktop-app/resources/styles.css +++ b/desktop-app/resources/styles.css @@ -192,6 +192,21 @@ 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 .markdown-alert { padding: 0.5rem 1rem; margin-bottom: 16px; @@ -2547,3 +2562,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..79b067c 100644 --- a/script.js +++ b/script.js @@ -275,6 +275,123 @@ 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 = /^\^([^^\n]+)\^/; + const SUBSCRIPT_PATTERN = /^~(?!~)([^~\n]+)~/; + 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(); + + function resetExtendedMarkdownState() { + footnoteDefinitions.clear(); + footnoteOrder.length = 0; + footnoteRefCounts.clear(); + footnoteFirstRefId.clear(); + } + + function normalizeFootnoteId(id) { + return String(id || "") + .trim() + .toLowerCase() + .replace(/[^a-z0-9_-]+/g, "-") + .replace(/^-+|-+$/g, "") || "footnote"; + } + + function renderDefinitionContent(content) { + return content + .split(/\n{2,}/) + .map((paragraph) => paragraph.trim()) + .filter(Boolean) + .map((paragraph) => `

    ${marked.parseInline(paragraph)}

    `) + .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; + return `${noteNumber}`; + }); + + 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}`; + return `
  • ${noteHtml}
  • `; + }) + .join(""); + + if (!footnotesHtml) { + return markdownWithReferences; + } + + return `${markdownWithReferences}\n\n

      ${footnotesHtml}
    `; + } + const blockMathExtension = { name: 'blockMath', level: 'block', @@ -300,6 +417,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 +588,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 +1420,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 +4113,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 +4731,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..9ab7c44 100644 --- a/styles.css +++ b/styles.css @@ -192,6 +192,21 @@ 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 .markdown-alert { padding: 0.5rem 1rem; margin-bottom: 16px; From b9f58ec60549f73cbcaab54d67570aed7a1d0eff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 18:36:22 +0000 Subject: [PATCH 3/7] fix: harden footnote rendering attribute handling Agent-Logs-Url: https://github.com/ThisIs-Developer/Markdown-Viewer/sessions/5057b2b5-2f98-40f3-9b51-ea87cc1274a5 Co-authored-by: ThisIs-Developer <109382325+ThisIs-Developer@users.noreply.github.com> --- desktop-app/resources/js/script.js | 25 ++++++++++++++++++++++--- script.js | 25 ++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js index 79b067c..52ec529 100644 --- a/desktop-app/resources/js/script.js +++ b/desktop-app/resources/js/script.js @@ -301,12 +301,27 @@ document.addEventListener("DOMContentLoaded", function () { .replace(/^-+|-+$/g, "") || "footnote"; } + 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) => `

    ${marked.parseInline(paragraph)}

    `) + .map((paragraph) => { + const renderedParagraph = marked.parseInline(paragraph); + const safeParagraph = typeof DOMPurify !== "undefined" + ? DOMPurify.sanitize(renderedParagraph) + : renderedParagraph; + return `

    ${safeParagraph}

    `; + }) .join(""); } @@ -372,7 +387,9 @@ document.addEventListener("DOMContentLoaded", function () { } const noteNumber = footnoteOrder.indexOf(id) + 1; - return `${noteNumber}`; + const safeRefId = escapeHtmlAttribute(refId); + const safeNormalizedId = escapeHtmlAttribute(normalizedId); + return `${noteNumber}`; }); const footnotesHtml = footnoteOrder @@ -381,7 +398,9 @@ document.addEventListener("DOMContentLoaded", function () { const normalizedId = normalizeFootnoteId(id); const noteHtml = renderDefinitionContent(footnoteDefinitions.get(id) || ""); const backRefId = footnoteFirstRefId.get(id) || `fnref-${normalizedId}`; - return `
  • ${noteHtml}
  • `; + const safeNormalizedId = escapeHtmlAttribute(normalizedId); + const safeBackRefId = escapeHtmlAttribute(backRefId); + return `
  • ${noteHtml}
  • `; }) .join(""); diff --git a/script.js b/script.js index 79b067c..52ec529 100644 --- a/script.js +++ b/script.js @@ -301,12 +301,27 @@ document.addEventListener("DOMContentLoaded", function () { .replace(/^-+|-+$/g, "") || "footnote"; } + 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) => `

    ${marked.parseInline(paragraph)}

    `) + .map((paragraph) => { + const renderedParagraph = marked.parseInline(paragraph); + const safeParagraph = typeof DOMPurify !== "undefined" + ? DOMPurify.sanitize(renderedParagraph) + : renderedParagraph; + return `

    ${safeParagraph}

    `; + }) .join(""); } @@ -372,7 +387,9 @@ document.addEventListener("DOMContentLoaded", function () { } const noteNumber = footnoteOrder.indexOf(id) + 1; - return `${noteNumber}`; + const safeRefId = escapeHtmlAttribute(refId); + const safeNormalizedId = escapeHtmlAttribute(normalizedId); + return `${noteNumber}`; }); const footnotesHtml = footnoteOrder @@ -381,7 +398,9 @@ document.addEventListener("DOMContentLoaded", function () { const normalizedId = normalizeFootnoteId(id); const noteHtml = renderDefinitionContent(footnoteDefinitions.get(id) || ""); const backRefId = footnoteFirstRefId.get(id) || `fnref-${normalizedId}`; - return `
  • ${noteHtml}
  • `; + const safeNormalizedId = escapeHtmlAttribute(normalizedId); + const safeBackRefId = escapeHtmlAttribute(backRefId); + return `
  • ${noteHtml}
  • `; }) .join(""); From f9106a4b3430503bb45d3b4f89f7a6ac946622ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 18:37:36 +0000 Subject: [PATCH 4/7] fix: make generated fallback footnote IDs unique Agent-Logs-Url: https://github.com/ThisIs-Developer/Markdown-Viewer/sessions/5057b2b5-2f98-40f3-9b51-ea87cc1274a5 Co-authored-by: ThisIs-Developer <109382325+ThisIs-Developer@users.noreply.github.com> --- desktop-app/resources/js/script.js | 12 ++++++++++-- script.js | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js index 52ec529..5594dd7 100644 --- a/desktop-app/resources/js/script.js +++ b/desktop-app/resources/js/script.js @@ -285,20 +285,28 @@ document.addEventListener("DOMContentLoaded", function () { 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) { - return String(id || "") + const normalized = String(id || "") .trim() .toLowerCase() .replace(/[^a-z0-9_-]+/g, "-") - .replace(/^-+|-+$/g, "") || "footnote"; + .replace(/^-+|-+$/g, ""); + if (normalized) { + return normalized; + } + + anonymousFootnoteCounter += 1; + return `footnote-${anonymousFootnoteCounter}`; } function escapeHtmlAttribute(value) { diff --git a/script.js b/script.js index 52ec529..5594dd7 100644 --- a/script.js +++ b/script.js @@ -285,20 +285,28 @@ document.addEventListener("DOMContentLoaded", function () { 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) { - return String(id || "") + const normalized = String(id || "") .trim() .toLowerCase() .replace(/[^a-z0-9_-]+/g, "-") - .replace(/^-+|-+$/g, "") || "footnote"; + .replace(/^-+|-+$/g, ""); + if (normalized) { + return normalized; + } + + anonymousFootnoteCounter += 1; + return `footnote-${anonymousFootnoteCounter}`; } function escapeHtmlAttribute(value) { From 87a66a1eb8cd67f8dc8c6f4f18a75cbc8918fe09 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 18:38:51 +0000 Subject: [PATCH 5/7] fix: tighten superscript and subscript token matching Agent-Logs-Url: https://github.com/ThisIs-Developer/Markdown-Viewer/sessions/5057b2b5-2f98-40f3-9b51-ea87cc1274a5 Co-authored-by: ThisIs-Developer <109382325+ThisIs-Developer@users.noreply.github.com> --- desktop-app/resources/js/script.js | 4 ++-- script.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js index 5594dd7..40d70ff 100644 --- a/desktop-app/resources/js/script.js +++ b/desktop-app/resources/js/script.js @@ -276,8 +276,8 @@ document.addEventListener("DOMContentLoaded", function () { 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 = /^\^([^^\n]+)\^/; - const SUBSCRIPT_PATTERN = /^~(?!~)([^~\n]+)~/; + 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*$/; diff --git a/script.js b/script.js index 5594dd7..40d70ff 100644 --- a/script.js +++ b/script.js @@ -276,8 +276,8 @@ document.addEventListener("DOMContentLoaded", function () { 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 = /^\^([^^\n]+)\^/; - const SUBSCRIPT_PATTERN = /^~(?!~)([^~\n]+)~/; + 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*$/; From 7bf4a930c8e94060ad82c6effc3d5982394853f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 19:15:21 +0000 Subject: [PATCH 6/7] fix: improve task list and footnote rendering Agent-Logs-Url: https://github.com/ThisIs-Developer/Markdown-Viewer/sessions/94cd9670-b3c4-4df4-955b-5d2cc71ac585 Co-authored-by: ThisIs-Developer <109382325+ThisIs-Developer@users.noreply.github.com> --- desktop-app/resources/index.html | 4 +++ desktop-app/resources/js/script.js | 2 +- desktop-app/resources/styles.css | 44 ++++++++++++++++++++++++++++++ script.js | 2 +- styles.css | 44 ++++++++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 2 deletions(-) diff --git a/desktop-app/resources/index.html b/desktop-app/resources/index.html index fccf41e..d48e524 100644 --- a/desktop-app/resources/index.html +++ b/desktop-app/resources/index.html @@ -234,6 +234,9 @@
    Menu
    Share + @@ -273,6 +276,7 @@
    Menu
    +
    diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js index 40d70ff..4c29c8f 100644 --- a/desktop-app/resources/js/script.js +++ b/desktop-app/resources/js/script.js @@ -397,7 +397,7 @@ document.addEventListener("DOMContentLoaded", function () { const noteNumber = footnoteOrder.indexOf(id) + 1; const safeRefId = escapeHtmlAttribute(refId); const safeNormalizedId = escapeHtmlAttribute(normalizedId); - return `${noteNumber}`; + return `[${noteNumber}]`; }); const footnotesHtml = footnoteOrder diff --git a/desktop-app/resources/styles.css b/desktop-app/resources/styles.css index 9ab7c44..ade2bde 100644 --- a/desktop-app/resources/styles.css +++ b/desktop-app/resources/styles.css @@ -207,6 +207,50 @@ body { 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; diff --git a/script.js b/script.js index 40d70ff..4c29c8f 100644 --- a/script.js +++ b/script.js @@ -397,7 +397,7 @@ document.addEventListener("DOMContentLoaded", function () { const noteNumber = footnoteOrder.indexOf(id) + 1; const safeRefId = escapeHtmlAttribute(refId); const safeNormalizedId = escapeHtmlAttribute(normalizedId); - return `${noteNumber}`; + return `[${noteNumber}]`; }); const footnotesHtml = footnoteOrder diff --git a/styles.css b/styles.css index 9ab7c44..ade2bde 100644 --- a/styles.css +++ b/styles.css @@ -207,6 +207,50 @@ body { 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; From 3354026d2b87f8eb2ad0a1faa959953912e03144 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 19:16:07 +0000 Subject: [PATCH 7/7] chore: revert resource index drift Agent-Logs-Url: https://github.com/ThisIs-Developer/Markdown-Viewer/sessions/94cd9670-b3c4-4df4-955b-5d2cc71ac585 Co-authored-by: ThisIs-Developer <109382325+ThisIs-Developer@users.noreply.github.com> --- desktop-app/resources/index.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/desktop-app/resources/index.html b/desktop-app/resources/index.html index d48e524..fccf41e 100644 --- a/desktop-app/resources/index.html +++ b/desktop-app/resources/index.html @@ -234,9 +234,6 @@
    Menu
    Share - @@ -276,7 +273,6 @@
    Menu
    -