From eff3381399a2d162cf424d86d5ab510b67dc3e14 Mon Sep 17 00:00:00 2001 From: Baivab Sarkar Date: Sun, 9 Aug 2026 19:54:50 +0530 Subject: [PATCH 1/2] feat(editor): add bold and italic shortcuts --- index.html | 6 ++- script.js | 110 +++++++++++++++++++++++++++++++++++++++++++- wiki/Features.md | 2 + wiki/Usage-Guide.md | 2 + 4 files changed, 116 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index 040f3773..50c2b903 100644 --- a/index.html +++ b/index.html @@ -487,8 +487,8 @@

Explorer

- - + +
@@ -908,6 +908,8 @@ diff --git a/script.js b/script.js index b2ae2e6e..4b28f9bc 100644 --- a/script.js +++ b/script.js @@ -13912,6 +13912,109 @@ ${selector} .arrowheadPath { replaceEditorRange(start, end, replacement, selectionStart, selectionEnd); } + function countRepeatedMarkerBefore(value, index, markerCharacter) { + let count = 0; + for (let cursor = index - 1; cursor >= 0 && value[cursor] === markerCharacter; cursor -= 1) { + count += 1; + } + return count; + } + + function countRepeatedMarkerAfter(value, index, markerCharacter) { + let count = 0; + for (let cursor = index; cursor < value.length && value[cursor] === markerCharacter; cursor += 1) { + count += 1; + } + return count; + } + + function markerRunIncludesFormat(runLength, markerLength) { + return markerLength === 1 ? runLength % 2 === 1 : runLength >= markerLength; + } + + function selectionIncludesMarkdownMarkers(selected, prefix, suffix) { + if (!selected || selected.length < prefix.length + suffix.length) return false; + const repeatedMarker = prefix === suffix && prefix.split('').every(function(character) { + return character === prefix[0]; + }); + if (!repeatedMarker) return selected.startsWith(prefix) && selected.endsWith(suffix); + + const leadingRun = countRepeatedMarkerAfter(selected, 0, prefix[0]); + const trailingRun = countRepeatedMarkerBefore(selected, selected.length, prefix[0]); + return markerRunIncludesFormat(leadingRun, prefix.length) + && markerRunIncludesFormat(trailingRun, suffix.length); + } + + function selectionIsSurroundedByMarkdownMarkers(value, start, end, prefix, suffix) { + const repeatedMarker = prefix === suffix && prefix.split('').every(function(character) { + return character === prefix[0]; + }); + if (!repeatedMarker) { + return value.slice(Math.max(0, start - prefix.length), start) === prefix + && value.slice(end, end + suffix.length) === suffix; + } + + const leadingRun = countRepeatedMarkerBefore(value, start, prefix[0]); + const trailingRun = countRepeatedMarkerAfter(value, end, suffix[0]); + return markerRunIncludesFormat(leadingRun, prefix.length) + && markerRunIncludesFormat(trailingRun, suffix.length); + } + + function replaceMarkdownEditorRange(editor, start, end, replacement, selectionStart, selectionEnd) { + if (editor === markdownEditor) { + replaceEditorRange(start, end, replacement, selectionStart, selectionEnd); + return; + } + editor.focus(); + editor.setRangeText(replacement, start, end, 'end'); + editor.setSelectionRange(selectionStart, selectionEnd); + editor.dispatchEvent(new Event('input', { bubbles: true })); + } + + function toggleMarkdownSelection(editor, prefix, suffix, placeholder) { + const value = editor.value; + const start = editor.selectionStart; + const end = editor.selectionEnd; + const selected = value.slice(start, end); + + if (selectionIncludesMarkdownMarkers(selected, prefix, suffix)) { + const replacement = selected.slice(prefix.length, selected.length - suffix.length); + replaceMarkdownEditorRange(editor, start, end, replacement, start, start + replacement.length); + return; + } + + if (selectionIsSurroundedByMarkdownMarkers(value, start, end, prefix, suffix)) { + const replacementStart = start - prefix.length; + const replacementEnd = end + suffix.length; + replaceMarkdownEditorRange(editor, replacementStart, replacementEnd, selected, replacementStart, replacementStart + selected.length); + return; + } + + const content = selected || placeholder; + const replacement = prefix + content + suffix; + const selectionStart = start + prefix.length; + replaceMarkdownEditorRange(editor, start, end, replacement, selectionStart, selectionStart + content.length); + } + + function handleMarkdownFormatShortcut(event) { + if (event.defaultPrevented || event.isComposing || event.altKey || event.shiftKey || !(event.ctrlKey || event.metaKey)) return; + const key = event.key.toLowerCase(); + if (key !== 'b' && key !== 'i') return; + + event.preventDefault(); + const editor = event.currentTarget; + const isEditable = editor === markdownEditor + ? canMutateEditor() + : editor === documentSplitEditor && Boolean(secondarySplitTabId) && !editor.readOnly; + if (!isEditable) { + announceToScreenReader(getEditorReadOnlyMessage()); + return; + } + + if (key === 'b') toggleMarkdownSelection(editor, '**', '**', 'bold text'); + else toggleMarkdownSelection(editor, '*', '*', 'italic text'); + } + function getCurrentLineRange() { const value = markdownEditor.value; const start = markdownEditor.selectionStart; @@ -18333,9 +18436,9 @@ ${selector} .arrowheadPath { return; } - if (action === 'bold') wrapEditorSelection('**', '**', 'bold text'); + if (action === 'bold') toggleMarkdownSelection(markdownEditor, '**', '**', 'bold text'); else if (action === 'strike') wrapEditorSelection('~~', '~~', 'struck text'); - else if (action === 'italic') wrapEditorSelection('*', '*', 'italic text'); + else if (action === 'italic') toggleMarkdownSelection(markdownEditor, '*', '*', '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'); @@ -19174,6 +19277,9 @@ ${selector} .arrowheadPath { headerAboutButton.addEventListener('click', openAboutModal); } + markdownEditor.addEventListener('keydown', handleMarkdownFormatShortcut); + if (documentSplitEditor) documentSplitEditor.addEventListener('keydown', handleMarkdownFormatShortcut); + // Editor key handlers for list continuation and indentation markdownEditor.addEventListener("keydown", function(e) { if (!canMutateEditor()) return; diff --git a/wiki/Features.md b/wiki/Features.md index e427a0d8..32c55422 100644 --- a/wiki/Features.md +++ b/wiki/Features.md @@ -445,6 +445,8 @@ Common shortcuts: | Save/export Markdown | `Ctrl+S` / `Cmd+S` | | Find | `Ctrl+F` / `Cmd+F` | | Replace | `Ctrl+H` / `Cmd+H` | +| Toggle bold | `Ctrl+B` / `Cmd+B` | +| Toggle italic | `Ctrl+I` / `Cmd+I` | | Toggle scroll sync | `Ctrl+Shift+S` / `Cmd+Shift+S` in Split view | | Undo | `Ctrl+Z` / `Cmd+Z` | | Redo | `Ctrl+Shift+Z`, `Cmd+Shift+Z`, `Ctrl+Y`, or `Cmd+Y` | diff --git a/wiki/Usage-Guide.md b/wiki/Usage-Guide.md index 8753f745..9927e06d 100644 --- a/wiki/Usage-Guide.md +++ b/wiki/Usage-Guide.md @@ -194,6 +194,8 @@ Mermaid, Markmap, maps, STL, ABC, and MathJax use client-side libraries. PlantUM | Toggle scroll sync in Split view | `Ctrl+Shift+S` / `Cmd+Shift+S` | | Find | `Ctrl+F` / `Cmd+F` | | Replace | `Ctrl+H` / `Cmd+H` | +| Toggle bold | `Ctrl+B` / `Cmd+B` | +| Toggle italic | `Ctrl+I` / `Cmd+I` | | Undo | `Ctrl+Z` / `Cmd+Z` | | Redo | `Ctrl+Shift+Z`, `Cmd+Shift+Z`, `Ctrl+Y`, or `Cmd+Y` | | New tab | Desktop: `Ctrl+T` / `Cmd+T`; web and desktop: `Alt+Shift+T` | From 40889c7fc8d26c11db0374a69e075d7ea84734b4 Mon Sep 17 00:00:00 2001 From: Baivab Sarkar Date: Sun, 9 Aug 2026 22:16:08 +0530 Subject: [PATCH 2/2] feat(editor): expand productivity shortcuts --- index.html | 64 ++++---- script.js | 369 +++++++++++++++++++++++++++++++++----------- wiki/Features.md | 31 ++-- wiki/Usage-Guide.md | 50 ++++-- 4 files changed, 374 insertions(+), 140 deletions(-) diff --git a/index.html b/index.html index 50c2b903..373d0fda 100644 --- a/index.html +++ b/index.html @@ -188,7 +188,7 @@

Markdown Viewer - Online Markd