Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions OPTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,16 @@ These keys are also stored in `QuickLook.config`.
- Example:
- `<AlwaysUnblockProtectedView>True</AlwaysUnblockProtectedView>`

## QuickLook.Plugin.MarkdownViewer.config options

### `<ToggleTocKey>`
- Default: `Ctrl+Shift+L`
- Type: `String`
- Description: Keyboard shortcut to toggle the Table of Contents (TOC) visibility in the Markdown viewer (with a smooth animation). Specify modifier keys (`Ctrl`, `Shift`, `Alt`) and a key name, separated by `+`. The key name is matched case-insensitively against the browser's `KeyboardEvent.key` value (e.g., `L`, `T`, `F1`, `Escape`). This shortcut matches Typora's default TOC shortcut.
- Example:
- `<ToggleTocKey>Ctrl+Shift+L</ToggleTocKey>`
- `<ToggleTocKey>Ctrl+Shift+T</ToggleTocKey>`

## QuickLook.Plugin.TextViewer.config options

### `<UseFormatDetector>`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,11 @@ protected string GenerateMarkdownHtml(string path)
isRtl = true;
}

var tocShortcut = SettingHelper.Get("ToggleTocKey", "Ctrl+Shift+L", "QuickLook.Plugin.MarkdownViewer");

var html = template.Replace("{{content}}", content)
.Replace("{{rtl}}", isRtl ? "rtl" : "ltr");
.Replace("{{rtl}}", isRtl ? "rtl" : "ltr")
Comment on lines +97 to +100
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 issue (security): Safely serialize tocShortcut for insertion into the HTML/JS template.

tocShortcut is currently injected directly into a JS string literal in the template. If a user sets it to a value containing quotes or special characters, it can break the HTML/JS or allow script injection. Please ensure it’s properly escaped before Replace("{{tocShortcut}}", ...), for example by serializing it with JsonSerializer.Serialize(tocShortcut) and then emitting const TOC_TOGGLE_SHORTCUT = ${jsShortcut}; in the template.

.Replace("{{tocShortcut}}", tocShortcut);
Comment on lines +97 to +101

return html;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

<script>
// Load the saved width immediately
// Load the saved TOC width and visibility immediately to prevent layout shift
const savedWidth = localStorage.getItem("tocWidth") || "178px";
document.documentElement.style.setProperty("--toc-width", savedWidth);
const tocCollapsed = localStorage.getItem("tocVisible") === "false";
document.documentElement.style.setProperty("--toc-width", tocCollapsed ? "0px" : savedWidth);
if (tocCollapsed) {
document.documentElement.style.setProperty("--toc-min-width", "0px");
document.documentElement.style.setProperty("--toc-overflow-y", "hidden");
}
</script>

<link rel="stylesheet" href="highlight.js/styles/github.min.css" />
Expand Down Expand Up @@ -61,17 +66,19 @@

width: var(--toc-width);

min-width: 4px;
min-width: var(--toc-min-width, 4px);
max-width: 80%;

height: 100vh;
background: var(--bgColor-muted);

overflow-y: auto;
overflow-y: var(--toc-overflow-y, auto);
overflow-x: hidden;

max-height: 100vh;
box-sizing: border-box;

transition: width 0.3s ease; /* Duration must match TOC_TRANSITION_MS in the resize script */
}

#toc a {
Expand Down Expand Up @@ -397,6 +404,60 @@ <h2>Contents</h2>
let lastDownX = 0;
let overlay = null;
const RESIZE_HANDLE_WIDTH = 8; // Width of the resize handle area in pixels
const TOC_TRANSITION_MS = 300; // Must match the CSS transition duration for #toc
let tocIsVisible = localStorage.getItem("tocVisible") !== "false";

// Restore initial overflow state when TOC was collapsed before page load
if (!tocIsVisible && toc.style.display !== "none") {
toc.style.overflowY = "hidden";
toc.style.minWidth = "0px";
}

// Toggle TOC visibility with smooth animation
function toggleToc() {
if (count < 2) return; // TOC is not shown when there are fewer than 2 headings

if (tocIsVisible) {
// Collapse TOC
toc.style.width = "0px";
toc.style.minWidth = "0px";
toc.style.overflowY = "hidden";
tocIsVisible = false;
localStorage.setItem("tocVisible", "false");
} else {
// Expand TOC
const width = localStorage.getItem("tocWidth") || "178px";
toc.style.width = width;
toc.style.minWidth = "4px";
tocIsVisible = true;
localStorage.setItem("tocVisible", "true");
// Restore overflow after the transition completes
// Use "auto" explicitly to override any lingering --toc-overflow-y CSS variable
setTimeout(function () {
if (tocIsVisible) {
toc.style.overflowY = "auto";
}
}, TOC_TRANSITION_MS);
}
}

// Expose toggleToc globally so it can be invoked from the host application (C#) via ExecuteScriptAsync
window.toggleToc = toggleToc;

// Helper: check if a KeyboardEvent matches a shortcut string like "Ctrl+Shift+L"
// Supported modifiers: Ctrl, Shift, Alt. Key name is case-insensitive and matches e.key.
function matchesShortcut(e, shortcut) {
if (!shortcut) return false;
var parts = shortcut.toLowerCase().split("+").map(function (p) { return p.trim(); }).filter(Boolean);
if (parts.length === 0) return false;
var key = parts[parts.length - 1];
return e.key.toLowerCase() === key
&& e.ctrlKey === parts.includes("ctrl")
&& e.shiftKey === parts.includes("shift")
&& e.altKey === parts.includes("alt");
}

const TOC_TOGGLE_SHORTCUT = "{{tocShortcut}}";

Comment on lines +447 to 461
// Create overlay function
function createOverlay() {
Expand All @@ -417,6 +478,7 @@ <h2>Contents</h2>

// Check if cursor is in resize area
function isInResizeArea(clientX, rect) {
if (!tocIsVisible) return false; // Disable resize when collapsed
const toc = document.getElementById("toc");
const hasScrollbar = toc.scrollHeight > toc.clientHeight;
const scrollbarWidth = hasScrollbar ? 17 : 0; // Standard scrollbar width in most browsers
Expand All @@ -441,6 +503,8 @@ <h2>Contents</h2>
if (isInResizeArea(e.clientX, rect)) {
isResizing = true;
lastDownX = e.clientX;
// Disable CSS transition during drag so width follows the cursor without delay
toc.style.transition = "none";
createOverlay();
}
});
Expand All @@ -464,6 +528,8 @@ <h2>Contents</h2>
const newWidth = toc.style.width;
localStorage.setItem("tocWidth", newWidth);
document.documentElement.style.setProperty("--toc-width", newWidth);
// Restore CSS transition after drag ends
toc.style.transition = "";
}
});

Expand All @@ -479,7 +545,14 @@ <h2>Contents</h2>
if (e.code === "Space") {
e.preventDefault();
}


// Toggle TOC with configurable shortcut (default: Ctrl+Shift+L, matching Typora)
if (matchesShortcut(e, TOC_TOGGLE_SHORTCUT)) {
toggleToc();
e.preventDefault();
return;
}

// Support keyboard shortcuts for RTL and LTR text direction
// RTL: Ctrl + RShift
// LTR: Ctrl + LShift
Expand Down
Loading