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
9 changes: 9 additions & 0 deletions _headers
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'none'; script-src 'self' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh 'sha256-DgMFO4QE+qqf2xNgeNb5gMKG6BtiiQFniYj21c88yME=' 'sha256-NnbK2LG1LUwYyZ1xgJN7k3oK4oTvHY4o9852VxwNA/o='; script-src-attr 'none'; worker-src 'self'; connect-src 'self' https://api.github.com https://raw.githubusercontent.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://kroki.io https://www.plantuml.com https://mermaid.ink https://paulrosen.github.io wss://markdownviewer.pages.dev; img-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; style-src-attr 'unsafe-inline'; font-src 'self' data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; media-src 'self' blob: data:; manifest-src 'self'; frame-src 'none'; upgrade-insecure-requests
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), browsing-topics=(), camera=(), clipboard-read=(self), clipboard-write=(self), display-capture=(), encrypted-media=(), fullscreen=(self), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), publickey-credentials-get=(), speaker-selection=(), usb=(), xr-spatial-tracking=()
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-site
4 changes: 4 additions & 0 deletions _redirects
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/.env /404 404
/.env.* /404 404
/_headers /404 404
/*.map /404 404
8 changes: 7 additions & 1 deletion desktop-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,13 @@ Build output is written under `desktop-app/dist/`.
| Token security | One-time |
| Logging | Disabled |

Native APIs are intentionally allowlisted: app exit, open/save dialogs, message boxes, external URL open, tray setup, command execution, file read/write, and storage get/set.
Native APIs are intentionally allowlisted: app exit, open/save dialogs, message boxes, external URL open, tray setup, file read/write, and storage get/set. Command execution is not part of the default allowlist.

## Local Renderer Security

Markdown content is treated as untrusted input. The desktop app does not allow Markdown preview rendering to run local shell commands by default. Native file open/save remains available through Neutralino dialogs and filesystem APIs.

Remote-safe diagram rendering continues to work where supported. Local command renderers such as D2 or PlantUML should only be enabled in a custom build after the user has explicitly accepted that local renderer programs process document content on their machine, and only fixed commands with stdin input should be allowed.

## Docker Build

Expand Down
1 change: 0 additions & 1 deletion desktop-app/neutralino.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
"os.showMessageBox",
"os.open",
"os.setTray",
"os.execCommand",
"filesystem.readFile",
"filesystem.writeFile",
"storage.setData",
Expand Down
39 changes: 26 additions & 13 deletions desktop-app/resources/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; base-uri 'self'; object-src 'none'; script-src 'self' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh 'sha256-DgMFO4QE+qqf2xNgeNb5gMKG6BtiiQFniYj21c88yME='; worker-src 'self'; connect-src 'self' ws: wss: https://api.github.com https://raw.githubusercontent.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://kroki.io https://www.plantuml.com https://mermaid.ink https://paulrosen.github.io; img-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; font-src 'self' data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; media-src 'self' blob: data:; manifest-src 'self'; upgrade-insecure-requests">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; base-uri 'self'; object-src 'none'; form-action 'none'; script-src 'self' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh 'sha256-DgMFO4QE+qqf2xNgeNb5gMKG6BtiiQFniYj21c88yME=' 'sha256-NnbK2LG1LUwYyZ1xgJN7k3oK4oTvHY4o9852VxwNA/o='; script-src-attr 'none'; worker-src 'self'; connect-src 'self' https://api.github.com https://raw.githubusercontent.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://kroki.io https://www.plantuml.com https://mermaid.ink https://paulrosen.github.io wss://markdownviewer.pages.dev ws://localhost:* ws://127.0.0.1:*; img-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; style-src-attr 'unsafe-inline'; font-src 'self' data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; media-src 'self' blob: data:; manifest-src 'self'; frame-src 'none'; upgrade-insecure-requests">
<!-- PERF-015: Preload critical-path resources for faster discovery -->
<link rel="preload" href="/styles.css" as="style">
<link rel="preload" href="/js/script.js" as="script">
Expand Down Expand Up @@ -60,13 +60,13 @@ <h1 class="h4 mb-0 me-2">Markdown Viewer<span class="visually-hidden"> - Online
<!-- Right section: Toolbar -->
<div class="toolbar d-none d-md-flex header-right">
<div class="toolbar-group view-toolbar" role="group" aria-label="View mode">
<button class="tool-button view-toggle-btn" data-view-mode="editor" aria-pressed="false" title="Editor only" aria-label="Editor only">
<button class="tool-button view-toggle-btn" data-view-mode="editor" aria-pressed="false" title="Markdown editor only" aria-label="Markdown editor only">
<i class="bi bi-file-text"></i>
</button>
<button class="tool-button view-toggle-btn is-active" data-view-mode="split" aria-pressed="true" title="Split view" aria-label="Split view">
<button class="tool-button view-toggle-btn is-active" data-view-mode="split" aria-pressed="true" title="Split editor and live preview" aria-label="Split editor and live preview">
<i class="bi bi-layout-split"></i>
</button>
<button class="tool-button view-toggle-btn" data-view-mode="preview" aria-pressed="false" title="Preview only" aria-label="Preview only">
<button class="tool-button view-toggle-btn" data-view-mode="preview" aria-pressed="false" title="Markdown preview only" aria-label="Markdown preview only">
<i class="bi bi-eye"></i>
</button>
</div>
Expand Down Expand Up @@ -169,15 +169,15 @@ <h2 class="h5 m-0">Menu</h2>

<!-- Story 1.4: Mobile View Mode Buttons -->
<div class="mobile-view-mode-group mb-3" role="group" aria-label="View mode">
<button class="mobile-view-mode-btn" data-mode="editor" aria-pressed="false" title="Editor only">
<button class="mobile-view-mode-btn" data-mode="editor" aria-pressed="false" title="Markdown editor only">
<i class="bi bi-file-text"></i>
<span>Editor</span>
</button>
<button class="mobile-view-mode-btn active" data-mode="split" aria-pressed="true" title="Split view">
<button class="mobile-view-mode-btn active" data-mode="split" aria-pressed="true" title="Split editor and live preview">
<i class="bi bi-layout-split"></i>
<span>Split</span>
</button>
<button class="mobile-view-mode-btn" data-mode="preview" aria-pressed="false" title="Preview only">
<button class="mobile-view-mode-btn" data-mode="preview" aria-pressed="false" title="Markdown preview only">
<i class="bi bi-eye"></i>
<span>Preview</span>
</button>
Expand Down Expand Up @@ -281,7 +281,7 @@ <h2 class="h5 m-0">Menu</h2>
</div>

<!-- Markdown Formatting Toolbar -->
<div class="markdown-format-toolbar" id="markdown-format-toolbar" role="toolbar" aria-label="Markdown formatting toolbar">
<div class="markdown-format-toolbar" id="markdown-format-toolbar" role="toolbar" aria-label="Markdown formatting toolbar for plain-text editing">
<div class="markdown-toolbar-group">
<button type="button" class="markdown-tool-btn" data-md-action="undo" title="Undo" aria-label="Undo"><i class="bi bi-arrow-counterclockwise"></i></button>
<button type="button" class="markdown-tool-btn" data-md-action="redo" title="Redo" aria-label="Redo"><i class="bi bi-arrow-clockwise"></i></button>
Expand Down Expand Up @@ -616,6 +616,14 @@ <h3 class="modal-section-title">Technology stack</h3>
<li>Marked, DOMPurify, Highlight.js, Mermaid, MathJax, and Emoji Toolkit for rendering.</li>
</ul>
</div>
<div class="modal-section">
<h3 class="modal-section-title">Storage privacy</h3>
<p class="modal-subtext">By default, document tabs are saved on this device for convenience. Private mode stops saving document content until it is turned off.</p>
<div class="modal-action-row">
<button type="button" class="reset-modal-btn reset-modal-cancel" id="private-mode-toggle">Private mode off</button>
<button type="button" class="reset-modal-btn reset-modal-danger" id="clear-local-data-btn">Clear local data</button>
</div>
</div>
<div class="modal-section">
<h3 class="modal-section-title">Open-source credits</h3>
<p class="modal-subtext">Markdown Viewer is built on top of open-source libraries maintained by the community. Thank you to every contributor and maintainer.</p>
Expand All @@ -637,15 +645,15 @@ <h3 class="modal-section-title">Open-source credits</h3>
</button>
</div>
<div class="modal-body">
<p class="share-modal-description">Small documents stay encoded in the URL. Large documents use temporary Cloudflare KV for 90 days. Shared tabs are not saved locally on the recipient's device.</p>
<p class="share-modal-description">Small documents stay encoded in the URL. Large documents use temporary Cloudflare KV for up to 90 days. Shared tabs are not saved locally on the recipient's device.</p>
<div class="live-share-section-title">Access</div>
<div class="share-mode-cards">
<label class="share-mode-card" id="share-card-view" for="share-mode-view">
<input type="radio" id="share-mode-view" name="share-mode" value="view" checked />
<span class="share-card-icon"><i class="bi bi-eye"></i></span>
<span class="share-card-body">
<span class="share-card-title">View only</span>
<span class="share-card-desc">Opens in preview mode. The editor is hidden.</span>
<span class="share-card-desc">Opens in preview mode and hides editing controls. Recipients can still inspect content delivered to their browser.</span>
</span>
<span class="share-card-check"><i class="bi bi-check-lg"></i></span>
</label>
Expand All @@ -670,6 +678,10 @@ <h3 class="modal-section-title">Open-source credits</h3>
<i class="bi bi-clipboard"></i>
<span>Copy</span>
</button>
<button class="reset-modal-btn share-copy-btn live-share-copy-btn" id="share-delete-btn" title="Delete stored snapshot" hidden>
<i class="bi bi-trash"></i>
<span>Delete</span>
</button>
</div>
</div>
</div>
Expand Down Expand Up @@ -1037,7 +1049,7 @@ <h3 class="modal-section-title">Open-source credits</h3>
<div class="skeleton-placeholder skeleton-line skeleton-w92"></div>
<div class="skeleton-placeholder skeleton-line skeleton-w45"></div>
</div>
<textarea id="markdown-editor" placeholder="Type or paste your Markdown here..."></textarea>
<textarea id="markdown-editor" placeholder="Type, paste, or import Markdown here..." aria-label="Markdown editor input with live preview"></textarea>
<div class="drop-hint" aria-hidden="true">
<i class="bi bi-cloud-arrow-up me-1"></i>Drop .md files anywhere to open them
</div>
Expand All @@ -1046,7 +1058,7 @@ <h3 class="modal-section-title">Open-source credits</h3>
<div class="resize-divider" role="separator" aria-orientation="vertical" aria-label="Resize panes" tabindex="0">
<div class="resize-divider-handle"></div>
</div>
<div class="preview-pane">
<div class="preview-pane" role="region" aria-label="Rendered Markdown live preview">
<div id="markdown-preview" class="markdown-body">
<!-- Initial dynamic article skeleton loader -->
<div class="skeleton-preview-container" id="markdown-preview-skeleton" aria-hidden="true">
Expand Down Expand Up @@ -1153,11 +1165,12 @@ <h3 class="modal-section-title">Open-source credits</h3>

## ✨ Key Features
- **Split-Screen Live Markdown Preview** with GitHub-Flavored Markdown and sync scroll
- **Open Local .md and .markdown Files** by picker or drag and drop
- **Smart Import/Export** (Markdown, HTML, PDF, PNG)
- **Insert Diagram & More** for Mermaid, PlantUML, Graphviz, D2, Vega-Lite, WaveDrom, Markmap, maps, STL, and ABC notation
- **LaTeX Math Support** for scientific notation
- **Share Snapshot and Live Share** for quick Markdown sharing workflows
- **No-login client-side defaults** for normal editing, preview, autosave, local file import, settings, and most exports
- **No login required** for editing, preview, autosave, local file import, settings, and most exports
- **Emoji Support** 😄 👍 🎉

## 💻 Code with Syntax Highlighting
Expand Down
Loading
Loading