diff --git a/script.js b/script.js
index 4bf31fd..bdf5c2f 100644
--- a/script.js
+++ b/script.js
@@ -77,6 +77,7 @@ document.addEventListener("DOMContentLoaded", function () {
// GLOBAL STATE (persisted across reloads)
// ========================================
const GLOBAL_STATE_KEY = 'markdownViewerGlobalState';
+ let referenceCounter = 1;
function loadGlobalState() {
try { return JSON.parse(localStorage.getItem(GLOBAL_STATE_KEY)) || {}; }
@@ -1850,6 +1851,24 @@ This is a fully client-side application. Your content never leaves your browser
return slug || 'section';
}
+ function getUsedReferenceNumbers(text) {
+ const used = new Set();
+ const regex = /\[(\d+)\]/g;
+ let match = regex.exec(text);
+ while (match) {
+ const num = parseInt(match[1], 10);
+ if (!Number.isNaN(num)) used.add(num);
+ match = regex.exec(text);
+ }
+ return used;
+ }
+
+ function getNextAvailableReferenceNumber(used, startNumber) {
+ let next = Math.max(1, startNumber || 1);
+ while (used.has(next)) next += 1;
+ return next;
+ }
+
function insertMarkdownBlock(block) {
const value = markdownEditor.value;
const start = markdownEditor.selectionStart;
@@ -1862,33 +1881,271 @@ This is a fully client-side application. Your content never leaves your browser
}
function insertMarkdownLink() {
+ const modal = document.getElementById('link-modal');
+ const urlInput = document.getElementById('link-modal-url');
+ const textInput = document.getElementById('link-modal-text');
+ const confirmBtn = document.getElementById('link-modal-apply');
+ const cancelBtn = document.getElementById('link-modal-cancel');
+ if (!modal || !urlInput || !textInput || !confirmBtn || !cancelBtn) return;
const start = markdownEditor.selectionStart;
const end = markdownEditor.selectionEnd;
- const selected = markdownEditor.value.slice(start, end) || 'link text';
- const url = prompt('Link URL', 'https://');
- if (url === null) return;
- const replacement = '[' + selected + '](' + (url.trim() || 'https://') + ')';
- replaceEditorRange(start, end, replacement, start + 1, start + 1 + selected.length);
+ const selected = markdownEditor.value.slice(start, end);
+ urlInput.value = 'https://';
+ textInput.value = selected || '';
+ modal.style.display = 'flex';
+
+ function applyLink() {
+ const url = urlInput.value.trim() || 'https://';
+ const linkText = textInput.value.trim() || selected || 'link text';
+ const replacement = '[' + linkText + '](' + url + ')';
+ modal.style.display = 'none';
+ cleanup();
+ replaceEditorRange(start, end, replacement, start + replacement.length, start + replacement.length);
+ }
+
+ function closeModal() {
+ modal.style.display = 'none';
+ cleanup();
+ }
+
+ function onKey(e) {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ applyLink();
+ } else if (e.key === 'Escape') {
+ e.preventDefault();
+ closeModal();
+ }
+ }
+
+ function cleanup() {
+ confirmBtn.removeEventListener('click', applyLink);
+ cancelBtn.removeEventListener('click', closeModal);
+ urlInput.removeEventListener('keydown', onKey);
+ textInput.removeEventListener('keydown', onKey);
+ }
+
+ confirmBtn.addEventListener('click', applyLink);
+ cancelBtn.addEventListener('click', closeModal);
+ urlInput.addEventListener('keydown', onKey);
+ textInput.addEventListener('keydown', onKey);
+
+ requestAnimationFrame(function() {
+ urlInput.focus();
+ urlInput.select();
+ });
}
function insertMarkdownImage() {
+ const modal = document.getElementById('image-modal');
+ const uploadOption = document.getElementById('image-source-upload');
+ const urlOption = document.getElementById('image-source-url');
+ const uploadFields = document.getElementById('image-upload-fields');
+ const urlFields = document.getElementById('image-url-fields');
+ const fileInput = document.getElementById('image-modal-file');
+ const urlInput = document.getElementById('image-modal-url');
+ const altInput = document.getElementById('image-modal-alt');
+ const confirmBtn = document.getElementById('image-modal-insert');
+ const cancelBtn = document.getElementById('image-modal-cancel');
+ if (!modal || !uploadOption || !urlOption || !uploadFields || !urlFields || !fileInput || !urlInput || !altInput || !confirmBtn || !cancelBtn) return;
const start = markdownEditor.selectionStart;
const end = markdownEditor.selectionEnd;
- const selected = markdownEditor.value.slice(start, end) || 'alt text';
- const url = prompt('Image URL', 'https://');
- if (url === null) return;
- const replacement = ' || 'https://') + ')';
- replaceEditorRange(start, end, replacement, start + 2, start + 2 + selected.length);
- }
+ const selected = markdownEditor.value.slice(start, end);
+ urlInput.value = 'https://';
+ altInput.value = selected || '';
+ fileInput.value = '';
+ urlOption.checked = true;
+ uploadOption.checked = false;
+ modal.style.display = 'flex';
+
+ function buildImageMarkdown(url) {
+ const altText = altInput.value.trim() || 'alt text';
+ const titleText = altInput.value.trim();
+ const safeTitle = titleText.replace(/"/g, '\\"');
+ const titlePart = safeTitle ? ' "' + safeTitle + '"' : '';
+ return '';
+ }
- function insertMarkdownAnchor() {
+ function insertImage(url) {
+ const safeUrl = url.trim() || 'https://';
+ const replacement = buildImageMarkdown(safeUrl);
+ modal.style.display = 'none';
+ cleanup();
+ replaceEditorRange(start, end, replacement, start + replacement.length, start + replacement.length);
+ }
+
+ function insertFromFile(file) {
+ const reader = new FileReader();
+ reader.onload = function() {
+ if (typeof reader.result !== 'string') return;
+ insertImage(reader.result);
+ };
+ reader.readAsDataURL(file);
+ }
+
+ function updateMode(shouldFocus) {
+ const isUpload = uploadOption.checked;
+ uploadFields.style.display = isUpload ? 'flex' : 'none';
+ urlFields.style.display = isUpload ? 'none' : 'flex';
+ if (shouldFocus) {
+ requestAnimationFrame(function() {
+ if (isUpload) {
+ fileInput.focus();
+ } else {
+ urlInput.focus();
+ urlInput.select();
+ }
+ });
+ }
+ }
+
+ function onModeChange() {
+ updateMode(true);
+ }
+
+ function onFileChange() {
+ const file = fileInput.files && fileInput.files[0];
+ if (file) {
+ insertFromFile(file);
+ }
+ }
+
+ function onKey(e) {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ if (uploadOption.checked) {
+ const file = fileInput.files && fileInput.files[0];
+ if (file) insertFromFile(file);
+ else fileInput.click();
+ } else {
+ insertImage(urlInput.value);
+ }
+ } else if (e.key === 'Escape') {
+ e.preventDefault();
+ closeModal();
+ }
+ }
+
+ function closeModal() {
+ modal.style.display = 'none';
+ cleanup();
+ }
+
+ function cleanup() {
+ confirmBtn.removeEventListener('click', onConfirm);
+ cancelBtn.removeEventListener('click', closeModal);
+ uploadOption.removeEventListener('change', onModeChange);
+ urlOption.removeEventListener('change', onModeChange);
+ fileInput.removeEventListener('change', onFileChange);
+ fileInput.removeEventListener('keydown', onKey);
+ urlInput.removeEventListener('keydown', onKey);
+ altInput.removeEventListener('keydown', onKey);
+ }
+
+ function onConfirm() {
+ if (uploadOption.checked) {
+ const file = fileInput.files && fileInput.files[0];
+ if (file) insertFromFile(file);
+ else fileInput.click();
+ } else {
+ insertImage(urlInput.value);
+ }
+ }
+
+ confirmBtn.addEventListener('click', onConfirm);
+ cancelBtn.addEventListener('click', closeModal);
+ uploadOption.addEventListener('change', onModeChange);
+ urlOption.addEventListener('change', onModeChange);
+ fileInput.addEventListener('change', onFileChange);
+ fileInput.addEventListener('keydown', onKey);
+ urlInput.addEventListener('keydown', onKey);
+ altInput.addEventListener('keydown', onKey);
+ updateMode(true);
+ }
+
+ function insertMarkdownReference() {
+ const modal = document.getElementById('reference-modal');
+ const numberInput = document.getElementById('reference-modal-number');
+ const urlInput = document.getElementById('reference-modal-url');
+ const titleInput = document.getElementById('reference-modal-title-input');
+ const confirmBtn = document.getElementById('reference-modal-apply');
+ const cancelBtn = document.getElementById('reference-modal-cancel');
+ if (!modal || !numberInput || !urlInput || !titleInput || !confirmBtn || !cancelBtn) return;
const start = markdownEditor.selectionStart;
const end = markdownEditor.selectionEnd;
- const selected = markdownEditor.value.slice(start, end);
- const id = prompt('Anchor ID', toSlug(selected));
- if (id === null) return;
- const replacement = '
';
- replaceEditorRange(start, end, replacement, start + replacement.length, start + replacement.length);
+ const currentValue = markdownEditor.value;
+ const used = getUsedReferenceNumbers(currentValue);
+ const maxUsed = used.size ? Math.max(...used) : 0;
+ referenceCounter = Math.max(1, maxUsed + 1);
+ const suggestedNumber = getNextAvailableReferenceNumber(used, referenceCounter);
+ numberInput.value = '[' + suggestedNumber + ']';
+ urlInput.value = 'https://';
+ titleInput.value = '';
+ modal.style.display = 'flex';
+
+ function insertReference() {
+ const latestValue = markdownEditor.value;
+ const usedNumbers = getUsedReferenceNumbers(latestValue);
+ const parsed = parseInt(numberInput.value.replace(/[^\d]/g, ''), 10);
+ const baseNumber = Number.isNaN(parsed) ? suggestedNumber : parsed;
+ const finalNumber = getNextAvailableReferenceNumber(usedNumbers, baseNumber);
+ const url = urlInput.value.trim() || 'https://';
+ const title = titleInput.value.trim();
+ const safeTitle = title.replace(/"/g, '\\"');
+ const definition = '[' + finalNumber + ']: ' + url + (safeTitle ? ' "' + safeTitle + '"' : '');
+ const selected = latestValue.slice(start, end);
+ const inlineReference = selected + '[' + finalNumber + ']';
+ const baseValue = latestValue.slice(0, start) + inlineReference + latestValue.slice(end);
+ let separator = '';
+ if (baseValue.length) {
+ if (baseValue.endsWith('\n\n')) separator = '';
+ else if (baseValue.endsWith('\n')) separator = '\n';
+ else separator = '\n\n';
+ }
+ const updatedValue = baseValue + separator + definition;
+ markdownEditor.value = updatedValue;
+ markdownEditor.focus();
+ const caret = start + inlineReference.length;
+ markdownEditor.setSelectionRange(caret, caret);
+ markdownEditor.dispatchEvent(new Event('input', { bubbles: true }));
+ referenceCounter = Math.max(referenceCounter, finalNumber + 1);
+ modal.style.display = 'none';
+ cleanup();
+ }
+
+ function closeModal() {
+ modal.style.display = 'none';
+ cleanup();
+ }
+
+ function onKey(e) {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ insertReference();
+ } else if (e.key === 'Escape') {
+ e.preventDefault();
+ closeModal();
+ }
+ }
+
+ function cleanup() {
+ confirmBtn.removeEventListener('click', insertReference);
+ cancelBtn.removeEventListener('click', closeModal);
+ numberInput.removeEventListener('keydown', onKey);
+ urlInput.removeEventListener('keydown', onKey);
+ titleInput.removeEventListener('keydown', onKey);
+ }
+
+ confirmBtn.addEventListener('click', insertReference);
+ cancelBtn.addEventListener('click', closeModal);
+ numberInput.addEventListener('keydown', onKey);
+ urlInput.addEventListener('keydown', onKey);
+ titleInput.addEventListener('keydown', onKey);
+
+ requestAnimationFrame(function() {
+ numberInput.focus();
+ numberInput.select();
+ });
}
function findInMarkdownEditor() {
@@ -1942,7 +2199,7 @@ This is a fully client-side application. Your content never leaves your browser
applyMarkdownList('ordered');
} else if (action === 'horizontal-rule') insertMarkdownBlock('---\n');
else if (action === 'link') insertMarkdownLink();
- else if (action === 'anchor') insertMarkdownAnchor();
+ else if (action === 'reference') insertMarkdownReference();
else if (action === 'image') insertMarkdownImage();
else if (action === 'inline-code') wrapEditorSelection('`', '`', 'code');
else if (action === 'code-block') insertMarkdownBlock('```js\n' + (markdownEditor.value.slice(markdownEditor.selectionStart, markdownEditor.selectionEnd) || 'console.log("Hello, Markdown!");') + '\n```\n');
diff --git a/styles.css b/styles.css
index 6ef070a..b743333 100644
--- a/styles.css
+++ b/styles.css
@@ -1811,6 +1811,47 @@ a:focus {
border-color: #b02a37;
}
+/* ========================================
+ RESET MODAL FORM FIELDS
+ ======================================== */
+
+.reset-modal-field {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ text-align: left;
+}
+
+.reset-modal-field-group {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.reset-modal-label {
+ font-size: 12px;
+ color: var(--text-secondary, #57606a);
+ font-weight: 600;
+}
+
+.reset-modal-toggle-group {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.reset-modal-option {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 13px;
+ color: var(--text-color);
+}
+
+.reset-modal-option input {
+ margin: 0;
+}
+
/* ========================================
RENAME MODAL INPUT
======================================== */
From eba538de43050123d859fe23302be61bcd791bca Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 5 May 2026 17:22:26 +0000
Subject: [PATCH 2/5] Sanitize modal title inputs
Agent-Logs-Url: https://github.com/ThisIs-Developer/Markdown-Viewer/sessions/b2788a61-05c8-4f0f-a5d0-8ecd315db97a
Co-authored-by: ThisIs-Developer <109382325+ThisIs-Developer@users.noreply.github.com>
---
desktop-app/resources/js/script.js | 14 ++++++++++++--
script.js | 14 ++++++++++++--
2 files changed, 24 insertions(+), 4 deletions(-)
diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js
index bdf5c2f..719843d 100644
--- a/desktop-app/resources/js/script.js
+++ b/desktop-app/resources/js/script.js
@@ -1869,6 +1869,16 @@ This is a fully client-side application. Your content never leaves your browser
return next;
}
+ function sanitizeMarkdownTitle(title) {
+ return title
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/'/g, ''')
+ .replace(/\\/g, '\\\\')
+ .replace(/"/g, '\\"');
+ }
+
function insertMarkdownBlock(block) {
const value = markdownEditor.value;
const start = markdownEditor.selectionStart;
@@ -1961,7 +1971,7 @@ This is a fully client-side application. Your content never leaves your browser
function buildImageMarkdown(url) {
const altText = altInput.value.trim() || 'alt text';
const titleText = altInput.value.trim();
- const safeTitle = titleText.replace(/"/g, '\\"');
+ const safeTitle = sanitizeMarkdownTitle(titleText);
const titlePart = safeTitle ? ' "' + safeTitle + '"' : '';
return '';
}
@@ -2091,7 +2101,7 @@ This is a fully client-side application. Your content never leaves your browser
const finalNumber = getNextAvailableReferenceNumber(usedNumbers, baseNumber);
const url = urlInput.value.trim() || 'https://';
const title = titleInput.value.trim();
- const safeTitle = title.replace(/"/g, '\\"');
+ const safeTitle = sanitizeMarkdownTitle(title);
const definition = '[' + finalNumber + ']: ' + url + (safeTitle ? ' "' + safeTitle + '"' : '');
const selected = latestValue.slice(start, end);
const inlineReference = selected + '[' + finalNumber + ']';
diff --git a/script.js b/script.js
index bdf5c2f..719843d 100644
--- a/script.js
+++ b/script.js
@@ -1869,6 +1869,16 @@ This is a fully client-side application. Your content never leaves your browser
return next;
}
+ function sanitizeMarkdownTitle(title) {
+ return title
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/'/g, ''')
+ .replace(/\\/g, '\\\\')
+ .replace(/"/g, '\\"');
+ }
+
function insertMarkdownBlock(block) {
const value = markdownEditor.value;
const start = markdownEditor.selectionStart;
@@ -1961,7 +1971,7 @@ This is a fully client-side application. Your content never leaves your browser
function buildImageMarkdown(url) {
const altText = altInput.value.trim() || 'alt text';
const titleText = altInput.value.trim();
- const safeTitle = titleText.replace(/"/g, '\\"');
+ const safeTitle = sanitizeMarkdownTitle(titleText);
const titlePart = safeTitle ? ' "' + safeTitle + '"' : '';
return '';
}
@@ -2091,7 +2101,7 @@ This is a fully client-side application. Your content never leaves your browser
const finalNumber = getNextAvailableReferenceNumber(usedNumbers, baseNumber);
const url = urlInput.value.trim() || 'https://';
const title = titleInput.value.trim();
- const safeTitle = title.replace(/"/g, '\\"');
+ const safeTitle = sanitizeMarkdownTitle(title);
const definition = '[' + finalNumber + ']: ' + url + (safeTitle ? ' "' + safeTitle + '"' : '');
const selected = latestValue.slice(start, end);
const inlineReference = selected + '[' + finalNumber + ']';
From a67552b795884845483cbf0de8eb15d5c4e1a35c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 5 May 2026 17:24:36 +0000
Subject: [PATCH 3/5] Refine reference detection and labels
Agent-Logs-Url: https://github.com/ThisIs-Developer/Markdown-Viewer/sessions/b2788a61-05c8-4f0f-a5d0-8ecd315db97a
Co-authored-by: ThisIs-Developer <109382325+ThisIs-Developer@users.noreply.github.com>
---
desktop-app/resources/index.html | 4 ++--
desktop-app/resources/js/script.js | 8 ++------
index.html | 4 ++--
script.js | 8 ++------
4 files changed, 8 insertions(+), 16 deletions(-)
diff --git a/desktop-app/resources/index.html b/desktop-app/resources/index.html
index 9ee192c..f70b07e 100644
--- a/desktop-app/resources/index.html
+++ b/desktop-app/resources/index.html
@@ -341,7 +341,7 @@
Menu