-
Notifications
You must be signed in to change notification settings - Fork 83
Features
This document details the features of Markdown Viewer, focusing on their architectural execution, performance strategies, and code-level configurations for version v3.7.4.
- Off-Thread Web Worker Parser
- Segmented DOM Patching Engine
- Ratio-Based Proportional Scroll Synchronization
- LaTeX Mathematical Typesetting
- Interactive Mermaid Diagrams with Drag-to-Pan
- Cascade PDF Layout Pagination Sandbox
- Multi-Document Session Persistence & Drag Reordering
- Binary Safety Gutter & Multi-File Importer
- Serverless Sharing via Compressed Hash Fragments
- Performance, Security, and UI Variables
To prevent typing lag and main-thread blocks, Markdown Viewer offloads compilation to a background Web Worker (preview-worker.js).
The main thread throttles render requests based on the character length of the active document to conserve CPU cycles:
- Small Documents (<10 KB): 80ms render debounce.
- Medium Documents (10 KB - 50 KB): 150ms render debounce.
- Large Documents (>50 KB): 300ms render debounce.
-
Block Splitting: The worker splits incoming markdown strings by double-newlines (
\n\n) while respecting boundary exclusions (like block math$$and code fences```). -
FNV-1a Alphanumeric Hashing: For each block, the worker computes a 32-bit FNV-1a hash. This is a non-cryptographic hash function designed for speed:
$$H_i = (H_{i-1} \oplus d_i) \times p$$ where
$p = 16777619$ (FNV prime) and$H_0 = 2166136261$ (offset basis). -
Selective Compilation: If the document doesn't use complex global footnotes or reference-style declarations, the worker compiles only changed blocks using
marked.jsandhighlight.js. It returns an array of compiled HTML strings paired with their FNV-1a hashes.
Updating the entire preview pane using element.innerHTML causes layout repaints, resets scrollbar offsets, wipes focus states, and collapses open toggle elements (like <details>). Markdown Viewer employs a custom patching controller:
- Hash Comparison: The main thread compares the hashes of the incoming HTML block array against the child nodes of the preview pane.
-
Targeted Replacement:
- If a node's hash matches, it is skipped.
- If a node's hash differs, the script replaces the corresponding child node in place using
replaceWith(). - If the new array has more elements, new nodes are appended.
- Extra trailing nodes are pruned.
-
Layout Containment: Every block is wrapped in a
<section>container configured with modern CSS rules:This instructs the browser's layout engine to bypass formatting and rendering of off-screen markdown sections, reducing repaint and reflow overhead.content-visibility: auto; contain-intrinsic-size: auto 220px;
When editing in Split View, scrolling the editor textarea scrolls the HTML preview pane proportionally, and vice versa.
Proportional scrolling is mapped using the scroll ratio:
The target container's scroll position is then calculated as:
Since scrolling the target pane triggers its own scroll events, this can create an infinite update loop. To prevent this:
- The application implements state locks:
isEditorScrolling = trueandisPreviewScrolling = true. - Scroll coordinates are updated within
requestAnimationFrame(). - A 50ms timeout releases the locks after scrolling stops.
LaTeX parsing uses MathJax loaded dynamically from a CDN.
- The controller scans inputs using a regex test:
/\$\$|\$[^$]|\\\(|\\\[/. - If math markers are detected, the MathJax libraries are fetched.
- Once loaded, equations are rendered by calling:
MathJax.typesetPromise([previewElement]);
By default, MathJax appends assistive MathML containers (<mjx-assistive-mml>) with tabindex="0". This interrupts keyboard tab order. A post-processing script runs after typesetting:
- It queries all
<mjx-assistive-mml>tags in the preview. - It removes the
tabindexattribute from each element. - These assistive nodes are hidden or stripped prior to PDF canvas capture to prevent overlapping text.
Mermaid code blocks are rendered as SVG diagrams with custom interactive features.
Every rendered Mermaid diagram is wrapped in a container that appends a floating toolbar with four actions:
- Zoom modal: Opens the diagram in a full-screen interactive modal.
- Download PNG: Captures the SVG, draws it to a canvas element, and downloads it.
- Download SVG: Serializes the SVG XML nodes and triggers a browser download.
-
Copy Image: Renders the diagram as a PNG blob and copies it to the system clipboard using the asynchronous Clipboard API:
navigator.clipboard.write([ new ClipboardItem({ "image/png": pngBlob }) ]);
Inside the zoom modal, the SVG transform matrix is updated during mouse events:
-
Scale: Computed using mouse-wheel offsets:
$$\text{scale} = \max(0.1, \min(\text{scale} + \text{delta}, 10))$$ -
Panning: Tracks the difference between starting coordinates and current pointer coordinates:
$$X_{\text{pan}} = X_{\text{current}} - X_{\text{dragStart}}$$ $$Y_{\text{pan}} = Y_{\text{current}} - Y_{\text{dragStart}}$$ -
CSS Transform: The updates are applied using hardware-accelerated CSS properties:
svg.style.transform = `translate(${modalPanX}px, ${modalPanY}px) scale(${modalZoomScale})`;
Exporting long, complex Markdown previews to PDF often leads to sliced text lines and cut-off images. Markdown Viewer uses a custom pagination engine:
-
Sandbox Cloning: The preview DOM is cloned into an off-screen sandbox element (
.pdf-exportclass) set to A4 width (210mm). -
SVG to Raster Conversion: Because
html2canvasstruggles to render inline SVGs, all Mermaid diagrams are converted to Base64-encoded PNG image elements inside the sandbox. -
Cascade Pagination Loop: The pagination engine executes up to 10 passes:
-
Keep-with-Next Headings: Headings within 70px of a page break are shifted down via margin-top spacers (
.pdf-page-break-spacer). -
Table Splitting: Split rows are shifted, and the table header (
<thead>) is duplicated onto the subsequent page. - Text Alignment: Lines are shifted downward to align page cuts cleanly between font heights, avoiding sliced characters.
- Oversized Graphics: Images exceeding page boundaries are downscaled (minimum scale 0.5) to fit.
-
Keep-with-Next Headings: Headings within 70px of a page break are shifted down via margin-top spacers (
-
Compilation: The stabilized sandbox is captured page-by-page using
html2canvas, and the resulting canvases are compiled into a PDF viajsPDF.
Markdown Viewer supports working with multiple documents simultaneously via a tabbed workspace.
The workspace is managed in a global tabs array:
{
id: "tab_" + Date.now() + "_" + Math.random().toString(36).substring(2, 8),
title: "Document Title",
content: "# Markdown Content...",
scrollPos: 0,
viewMode: "split", // split | editor | preview
createdAt: 1718042710000
}- Debounced Save: Document changes trigger an auto-save that is debounced by 500ms using a window timer to prevent blocking the UI thread with constant serialization.
-
Beforeunload Flush: To ensure changes are saved when navigating away or closing the page, the state is flushed immediately during
beforeunloadandvisibilitychangeevents (when the page is hidden).
- Tab elements in the DOM are configured with
draggable="true". - Drag events track the moving tab (
draggedTabId). - Releasing a tab over another swaps their indices in the state array, saves the updated array to
localStorage, and updates the tab bar.
To prevent importing corrupted binary files, the file reader scans the first 8 KB of any imported file:
- If a null byte (
\x00) is found, the import is aborted, and an error is displayed.
Users can import documents directly from public GitHub repositories:
- URL Parsing: The importer resolves repo, branch, folder, or file paths from a pasted URL.
-
API Requests: It queries public GitHub APIs (
api.github.com/repos/.../contents/...) to fetch file trees. - File Browser Modal: Users can preview the file tree in a modal, select the files they want, and import them all at once. Selected files are loaded into separate document tabs.
Markdown Viewer lets you share documents via links that contain the entire compressed document content, eliminating the need for a database.
- The markdown text is encoded to bytes and compressed using
Pako.js(DEFLATE). - The compressed bytes are converted to a Base64 string.
- The string is made URL-safe by replacing
+with-,/with_, and removing trailing=padding. - The hash is appended to the URL as
#share=<encoded_string>. - A warning is displayed if the generated link exceeds 32,000 characters.
- Theme changes are managed using CSS variables.
- Transition animations are scoped to specific properties (
background-color,border-color) rather than usingtransition: all. - The background transition on the
<body>element was removed to prevent repainting the entire viewport during theme shifts.
- The divider between the editor and preview panes can be dragged horizontally.
- It updates the CSS grid layout dynamically using percentage variables.
- Drag limits prevent either pane from being scaled below 20% of the viewport width.
- All compiled HTML is sanitized on the main thread using DOMPurify before being rendered:
const cleanHtml = DOMPurify.sanitize(rawHtml, { USE_PROFILES: { html: true }, ADD_ATTR: ['target', 'draggable', 'contenteditable'] });
- This strips inline script handlers (e.g.
onload,onclick) and<script>elements to prevent Cross-Site Scripting (XSS) when importing or loading external markdown files.
Markdown Viewer Repository · Apache-2.0 License
Developed and maintained by ThisIs-Developer
📖 Overview & Reference
🚀 Getting Started & Usage
🐳 Deployment & Wrappers
🛠️ Development & Journey