If you need a markdown editor (uses a gtk webview, requires an internet for marked.js+katex from a cdn)
Simple GTK Markdown Editor
A gtk+adwaita markdown editor in ~600 lines that uses a webview with marked.js and katex for rendering markdown. This is made as a self contained uv script to be very portable
| #!/usr/bin/env python3 | |
| # /// script | |
| # requires-python = ">=3.14" | |
| # dependencies = [ | |
| # "pygobject>=3.56.3", | |
| # "pygobject-stubs>=2.17.0", | |
| # ] | |
| # /// | |
| """ | |
| markdown_renderer.py — Markdown Editor & Renderer Prototype Component for PDF Atlas. | |
| Features: | |
| - Disables WebKit compositing mode (WEBKIT_DISABLE_COMPOSITING_MODE=1) at launch. | |
| - Centered "Source", "Rendered", and "Side by Side" tabs in the Adw.HeaderBar. | |
| - Open File (Ctrl+O) and Save File (Ctrl+S) buttons in top-left HeaderBar. | |
| - Monospace code editor in the "Source" tab. | |
| - WebKit.WebView preview in the "Rendered" tab with Marked.js & KaTeX math rendering. | |
| - Live preview updates at 32ms with GTK dark/light theme support. | |
| - CLI argparse with optional file positional arg and --lorem-ipsum showcase flag. | |
| Usage: | |
| uv run scripts/markdown_renderer.py | |
| uv run scripts/markdown_renderer.py --lorem-ipsum | |
| uv run scripts/markdown_renderer.py note.md | |
| """ | |
| import argparse | |
| import os | |
| import sys | |
| # Must be set before initializing GTK and WebKit | |
| os.environ["WEBKIT_DISABLE_COMPOSITING_MODE"] = "1" | |
| import gi | |
| gi.require_version("Gtk", "4.0") | |
| gi.require_version("Adw", "1") | |
| gi.require_version("WebKit", "6.0") | |
| from gi.repository import Adw, Gio, GLib, GObject, Gtk, WebKit | |
| DEFAULT_MARKDOWN_SAMPLE = """# Markdown Note & Preview | |
| Welcome to the **PDF Atlas** Markdown note component prototype. | |
| ## Features | |
| - **Monospace Editor**: Write raw Markdown source with word wrapping. | |
| - **Live Preview**: Render Markdown and KaTeX math equations in real-time. | |
| - **Libadwaita Header Tabs**: Easily switch between *Source*, *Rendered*, and *Side by Side* views. | |
| ## Math Equations | |
| Inline math: $E = mc^2$ or $\\nabla \\cdot \\mathbf{B} = 0$. | |
| Display math: | |
| $\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}$ | |
| ## Code Example | |
| ```python | |
| def calculate_area(radius: float) -> float: | |
| import math | |
| return math.pi * radius ** 2 | |
| ``` | |
| ## Checklist | |
| - [x] WebKit GTK4 integration | |
| - [x] Centered Adw.HeaderBar switcher | |
| - [x] Dark / Light theme support | |
| - [x] Open / Save file dialogs | |
| """ | |
| LOREM_IPSUM_SHOWCASE = """# Lorem Ipsum & Markdown Showcase | |
| > *"Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."* | |
| Welcome to the **Lorem Ipsum Showcase** demonstrating live Markdown formatting and LaTeX mathematical equations in PDF Atlas. | |
| ## 1. Mathematical Physics & Calculus | |
| Inline equation: $f(x) = \\int_{-\\infty}^{x} \\frac{1}{\\sqrt{2\\pi}\\sigma} e^{-\\frac{(t-\\mu)^2}{2\\sigma^2}} dt$ | |
| Display equation (Schrödinger Equation): | |
| $i\\hbar\\frac{\\partial}{\\partial t}\\Psi(\\mathbf{r},t) = \\left[ -\\frac{\\hbar^2}{2m}\\nabla^2 + V(\\mathbf{r},t) \\right]\\Psi(\\mathbf{r},t)$ | |
| Maxwell's Equations in Differential Form: | |
| $\\nabla \\cdot \\mathbf{E} = \\frac{\\rho}{\\varepsilon_0}, \\quad \\nabla \\cdot \\mathbf{B} = 0$ | |
| ## 2. Formatting & Lists | |
| - **Bold Typography**, *Italicized Emphasis*, `Monospace Code Identifiers` | |
| - [x] High-performance WebKit GTK4 renderer | |
| - [x] Live KaTeX LaTeX equation parsing | |
| - [x] Split side-by-side synchronized editing | |
| - [x] Monitor-aware window scaling | |
| ## 3. Code Implementation | |
| ```python | |
| def fibonacci(n: int) -> list[int]: | |
| \"\"\"Generate Fibonacci sequence up to n terms.\"\"\" | |
| a, b = 0, 1 | |
| result: list[int] = [] | |
| for _ in range(n): | |
| result.append(a) | |
| a, b = b, a + b | |
| return result | |
| # Print first 10 terms | |
| print(fibonacci(10)) | |
| ``` | |
| ## 4. Benchmark Performance Matrix | |
| | Component | Library | Update Latency | Feature Support | | |
| | :--- | :--- | :--- | :--- | | |
| | **Parser** | Marked.js | < 5 ms | GFM, Tables, Tasks | | |
| | **Math Engine** | KaTeX | < 15 ms | TeX Math, Symbols | | |
| | **Split Container** | Gtk.Paned | 60 FPS | Double-pane 50/50 | | |
| """ | |
| HTML_PREVIEW_TEMPLATE = """<!DOCTYPE html> | |
| <html> | |
| <head> | |
| Markdown Preview | |
| <!-- Marked.js for Markdown parsing --> | |
| <!-- KaTeX for TeX Math rendering --> | |
| :root { | |
| color-scheme: light dark; | |
| --bg-color: #ffffff; | |
| --text-color: #24292e; | |
| --code-bg: #f6f8fa; | |
| --border-color: #e1e4e8; | |
| --link-color: #0366d6; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; | |
| font-size: 15px; | |
| line-height: 1.6; | |
| background-color: var(--bg-color); | |
| color: var(--text-color); | |
| padding: 24px 32px; | |
| margin: 0; | |
| word-wrap: break-word; | |
| } | |
| h1, h2, h3, h4, h5, h6 { | |
| margin-top: 24px; | |
| margin-bottom: 16px; | |
| font-weight: 600; | |
| line-height: 1.25; | |
| border-bottom: 1px solid var(--border-color); | |
| padding-bottom: 0.3em; | |
| } | |
| h1 { font-size: 2em; } | |
| h2 { font-size: 1.5em; } | |
| h3 { font-size: 1.25em; } | |
| a { | |
| color: var(--link-color); | |
| text-decoration: none; | |
| } | |
| a:hover { | |
| text-decoration: underline; | |
| } | |
| code { | |
| font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; | |
| font-size: 85%; | |
| background-color: var(--code-bg); | |
| padding: 0.2em 0.4em; | |
| border-radius: 6px; | |
| } | |
| pre { | |
| background-color: var(--code-bg); | |
| padding: 16px; | |
| overflow: auto; | |
| font-size: 85%; | |
| line-height: 1.45; | |
| border-radius: 6px; | |
| } | |
| pre code { | |
| background-color: transparent; | |
| padding: 0; | |
| } | |
| blockquote { | |
| margin: 0; | |
| padding: 0 1em; | |
| color: #6a737d; | |
| border-left: 0.25em solid var(--border-color); | |
| } | |
| table { | |
| border-collapse: collapse; | |
| width: 100%; | |
| margin-top: 16px; | |
| margin-bottom: 16px; | |
| } | |
| table th, table td { | |
| padding: 6px 13px; | |
| border: 1px solid var(--border-color); | |
| } | |
| table tr:nth-child(2n) { | |
| background-color: var(--code-bg); | |
| } | |
| .katex-display { | |
| overflow-x: auto; | |
| overflow-y: hidden; | |
| padding: 8px 0; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| function updateContent(markdownText) { | |
| const container = document.getElementById("content"); | |
| if (!container) return; | |
| if (typeof marked !== 'undefined') { | |
| container.innerHTML = marked.parse(markdownText); | |
| } else { | |
| container.textContent = markdownText; | |
| } | |
| if (typeof renderMathInElement !== 'undefined') { | |
| renderMathInElement(container, { | |
| delimiters: [ | |
| {left: '$', right: '$', display: true}, | |
| {left: ' |