Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be5995bb55 | ||
|
|
3a3884155f | ||
|
|
97f398276a | ||
|
|
f60bb0aa53 | ||
|
|
654a134753 | ||
|
|
e8ad868277 | ||
|
|
3fbbe57c6f | ||
|
|
6e71826787 | ||
|
|
e2a6aeafb2 | ||
|
|
2ed08c1e86 | ||
|
|
e09b6713cc | ||
|
|
a793a26977 |
No files matched your search
+10
-2
@@ -5,13 +5,21 @@
|
||||
<link>https://github.com/darinkelkhoff/markdownViewr</link>
|
||||
<description>markdownViewr releases</description>
|
||||
<language>en</language>
|
||||
<item>
|
||||
<title>1.1.3</title>
|
||||
<pubDate>Mon, 08 Jun 2026 10:07:17 -0500</pubDate>
|
||||
<sparkle:version>3</sparkle:version>
|
||||
<sparkle:shortVersionString>1.1.3</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>13.0</sparkle:minimumSystemVersion>
|
||||
<enclosure url="https://github.com/darinkelkhoff/markdownViewr/releases/download/v1.1.4/markdownViewr-1.1.4.zip" length="4202362" type="application/octet-stream" sparkle:edSignature="R5jaamgXCpnoMHHRU4H+WsT+VNfEhRZEMIdXchNKXgzPZyCEMV4lRbEr7TJN/TvxxmiSEJorOXeJjhjH5tMzCA=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>1.0</title>
|
||||
<pubDate>Mon, 08 Jun 2026 08:18:43 -0500</pubDate>
|
||||
<sparkle:version>1</sparkle:version>
|
||||
<sparkle:shortVersionString>1.0</sparkle:shortVersionString>
|
||||
<sparkle:shortVersionString>1.1.2</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>13.0</sparkle:minimumSystemVersion>
|
||||
<enclosure url="https://github.com/darinkelkhoff/markdownViewr/releases/download/v1.1.1/markdownViewr-1.1.1.zip" length="4202330" type="application/octet-stream" sparkle:edSignature="gkQkzhSGNIPUHLIR4p8m3Z9GERq/2hfNwvFrqCfOml0mRXRhdeiAE9OAm5iPmrS78c6d24ouFwN1TVfCtVyjBw=="/>
|
||||
<enclosure url="https://github.com/darinkelkhoff/markdownViewr/releases/download/v1.1.2/markdownViewr-1.1.2.zip" length="4202333" type="application/octet-stream" sparkle:edSignature="FZ19CRV6Ib9Dbpt7Nr5PK4tCcaLJ8rHPlBOr2eGLjV1GnJxBETSnCp0FCEiOsvgWqpgsOlJc/eEQHDbIK/8UBw=="/>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
Whitespace-only changes.
Binary file not shown.
|
After Width: | Height: | Size: 1010 KiB |
+167
@@ -0,0 +1,167 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>markdownViewr</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: #f5f5f7;
|
||||
color: #1d1d1f;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 24px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
margin-bottom: 24px;
|
||||
border-radius: 22%;
|
||||
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.screenshot-wrap {
|
||||
margin-top: 48px;
|
||||
width: 100%;
|
||||
max-width: 780px;
|
||||
}
|
||||
|
||||
.screenshot-wrap img {
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 40px rgba(0,0,0,0.18);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.4rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-size: 1.15rem;
|
||||
color: #555;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: #0071e3;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
padding: 14px 28px;
|
||||
border-radius: 980px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.download-btn:hover { background: #0077ed; }
|
||||
|
||||
.download-btn svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.req {
|
||||
margin-top: 12px;
|
||||
font-size: 0.82rem;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 40px;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.feature {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 10px 16px;
|
||||
font-size: 0.88rem;
|
||||
color: #444;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 56px;
|
||||
font-size: 0.8rem;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
footer a { color: #aaa; }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body { background: #1c1c1e; color: #f5f5f7; }
|
||||
.tagline { color: #aaa; }
|
||||
.feature { background: #2c2c2e; color: #ccc; box-shadow: none; }
|
||||
.req { color: #666; }
|
||||
footer, footer a { color: #555; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="hero">
|
||||
<img class="icon" src="icon.png" alt="markdownViewr icon">
|
||||
|
||||
<h1>markdownViewr</h1>
|
||||
|
||||
<p class="tagline">
|
||||
A fast, beautiful markdown viewer for macOS.<br>
|
||||
Open any markdown file. Live-reloads as you edit.
|
||||
</p>
|
||||
|
||||
<a class="download-btn"
|
||||
href="https://github.com/darinkelkhoff/markdownViewr/releases/latest/download/markdownViewr.dmg">
|
||||
<svg viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v9.586l2.293-2.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L9 12.586V3a1 1 0 011-1zM3 16a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"/></svg>
|
||||
Download for Mac
|
||||
</a>
|
||||
|
||||
<p class="req">macOS 13 Ventura or later · Free</p>
|
||||
|
||||
<div class="features">
|
||||
<span class="feature">GitHub Flavored Markdown</span>
|
||||
<span class="feature">Syntax-highlighted code blocks</span>
|
||||
<span class="feature">Fully customizable themes</span>
|
||||
<span class="feature">Live file watching</span>
|
||||
<span class="feature">Vim navigation keys</span>
|
||||
<span class="feature">Find in document</span>
|
||||
<span class="feature">YAML frontmatter support</span>
|
||||
<span class="feature">Open in external editor</span>
|
||||
<span class="feature">Automatic updates</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="screenshot-wrap">
|
||||
<img src="screenshot.png" alt="markdownViewr showing a markdown document">
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<a href="https://github.com/darinkelkhoff/markdownViewr">View on GitHub</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 521 KiB |
@@ -0,0 +1,732 @@
|
||||
# Markdown Extensions Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add four toggleable inline markdown extensions (highlight, superscript, subscript, underline) with a Markdown settings tab, a theme-aware highlight color, and a post-processing HTML pass.
|
||||
|
||||
**Architecture:** A new `MarkdownExtensions` struct holds four booleans stored in UserDefaults via `ThemeManager`. `MarkdownDocument.convertToHTML` runs a post-processing pass (skipping `<pre>` blocks) after the swift-markdown AST walk. `ThemeColors` gains `highlightBackground`/`highlightText` fields (with a custom decoder for backward compatibility) that feed CSS variables used by `mark {}` in the HTML template.
|
||||
|
||||
**Tech Stack:** SwiftUI, AppKit (NSWindow), swift-markdown 0.7.3, NSRegularExpression, UserDefaults, XcodeGen (sources glob — new Swift files auto-included, no regeneration needed).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: MarkdownExtensions struct + ThemeColors highlight fields
|
||||
|
||||
**Files:**
|
||||
- Create: `markdownViewr/MarkdownExtensions.swift`
|
||||
- Modify: `markdownViewr/Theme.swift`
|
||||
|
||||
- [ ] **Step 1: Create `markdownViewr/MarkdownExtensions.swift`**
|
||||
|
||||
```swift
|
||||
import Foundation
|
||||
|
||||
struct MarkdownExtensions: Codable {
|
||||
var highlight: Bool = true
|
||||
var superscript: Bool = true
|
||||
var subscript_: Bool = true // trailing underscore: `subscript` is a Swift keyword
|
||||
var underline: Bool = true
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `highlightBackground` and `highlightText` to `ThemeColors` in `Theme.swift`**
|
||||
|
||||
Add the two new properties after `blockquoteBackground`:
|
||||
|
||||
```swift
|
||||
var blockquoteBackground: String
|
||||
var highlightBackground: String
|
||||
var highlightText: String
|
||||
```
|
||||
|
||||
Update the `init(...)` signature and body to include them after `blockquoteBackground`:
|
||||
|
||||
```swift
|
||||
init(
|
||||
background: String = "#1e1e2e",
|
||||
text: String = "#cdd6f4",
|
||||
heading1: String = "#cba6f7",
|
||||
heading2: String = "#89b4fa",
|
||||
heading3: String = "#89b4fa",
|
||||
heading4: String = "#74c7ec",
|
||||
heading5: String = "#74c7ec",
|
||||
heading6: String = "#74c7ec",
|
||||
link: String = "#89dceb",
|
||||
codeBackground: String = "#181825",
|
||||
codeText: String = "#a6e3a1",
|
||||
blockquoteBorder: String = "#cba6f7",
|
||||
blockquoteBackground: String = "#252535",
|
||||
highlightBackground: String = "#ffd700",
|
||||
highlightText: String = "#1a1a1a"
|
||||
) {
|
||||
self.background = background
|
||||
self.text = text
|
||||
self.heading1 = heading1
|
||||
self.heading2 = heading2
|
||||
self.heading3 = heading3
|
||||
self.heading4 = heading4
|
||||
self.heading5 = heading5
|
||||
self.heading6 = heading6
|
||||
self.link = link
|
||||
self.codeBackground = codeBackground
|
||||
self.codeText = codeText
|
||||
self.blockquoteBorder = blockquoteBorder
|
||||
self.blockquoteBackground = blockquoteBackground
|
||||
self.highlightBackground = highlightBackground
|
||||
self.highlightText = highlightText
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add explicit `CodingKeys` and custom `init(from:)` to `ThemeColors` for backward compatibility**
|
||||
|
||||
Existing user theme JSONs don't have these fields. Swift's auto-synthesized decoder throws on missing keys, so add the following inside `struct ThemeColors`:
|
||||
|
||||
```swift
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case background, text
|
||||
case heading1, heading2, heading3, heading4, heading5, heading6
|
||||
case link, codeBackground, codeText
|
||||
case blockquoteBorder, blockquoteBackground
|
||||
case highlightBackground, highlightText
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
background = try c.decode(String.self, forKey: .background)
|
||||
text = try c.decode(String.self, forKey: .text)
|
||||
heading1 = try c.decode(String.self, forKey: .heading1)
|
||||
heading2 = try c.decode(String.self, forKey: .heading2)
|
||||
heading3 = try c.decode(String.self, forKey: .heading3)
|
||||
heading4 = try c.decode(String.self, forKey: .heading4)
|
||||
heading5 = try c.decode(String.self, forKey: .heading5)
|
||||
heading6 = try c.decode(String.self, forKey: .heading6)
|
||||
link = try c.decode(String.self, forKey: .link)
|
||||
codeBackground = try c.decode(String.self, forKey: .codeBackground)
|
||||
codeText = try c.decode(String.self, forKey: .codeText)
|
||||
blockquoteBorder = try c.decode(String.self, forKey: .blockquoteBorder)
|
||||
blockquoteBackground = try c.decode(String.self, forKey: .blockquoteBackground)
|
||||
highlightBackground = try c.decodeIfPresent(String.self, forKey: .highlightBackground) ?? "#ffd700"
|
||||
highlightText = try c.decodeIfPresent(String.self, forKey: .highlightText) ?? "#1a1a1a"
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Build to verify**
|
||||
|
||||
```bash
|
||||
just build
|
||||
```
|
||||
|
||||
Expected: build succeeds with no errors.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add markdownViewr/MarkdownExtensions.swift markdownViewr/Theme.swift
|
||||
git commit -m "add MarkdownExtensions struct and highlight color fields to ThemeColors"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: ThemeManager — extensions property and CSS variables
|
||||
|
||||
**Files:**
|
||||
- Modify: `markdownViewr/ThemeManager.swift`
|
||||
|
||||
- [ ] **Step 1: Add `markdownExtensions` as the last `@Published` property in `ThemeManager`**
|
||||
|
||||
Add after the `frontmatterMode` property block (around line 21–26):
|
||||
|
||||
```swift
|
||||
@Published var markdownExtensions: MarkdownExtensions {
|
||||
didSet {
|
||||
if let data = try? JSONEncoder().encode(markdownExtensions) {
|
||||
UserDefaults.standard.set(data, forKey: "markdownExtensions")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Initialize `markdownExtensions` in `ThemeManager.init()`**
|
||||
|
||||
In the `init()` body, after the `frontmatterMode` initialization, add:
|
||||
|
||||
```swift
|
||||
if let data = UserDefaults.standard.data(forKey: "markdownExtensions"),
|
||||
let ext = try? JSONDecoder().decode(MarkdownExtensions.self, from: data) {
|
||||
self.markdownExtensions = ext
|
||||
} else {
|
||||
self.markdownExtensions = MarkdownExtensions()
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add `--highlight-bg` and `--highlight-text` to `generateCSS`**
|
||||
|
||||
In `generateCSS(for:)`, inside the `:root {` block, add these two lines after `--blockquote-bg`:
|
||||
|
||||
```swift
|
||||
--highlight-bg: \(theme.colors.highlightBackground);
|
||||
--highlight-text: \(theme.colors.highlightText);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Build to verify**
|
||||
|
||||
```bash
|
||||
just build
|
||||
```
|
||||
|
||||
Expected: build succeeds.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: HTML template — `mark` CSS rule
|
||||
|
||||
**Files:**
|
||||
- Modify: `markdownViewr/Resources/template.html`
|
||||
|
||||
- [ ] **Step 1: Add `mark` rule to `base-css` in `template.html`**
|
||||
|
||||
After the `del { opacity: 0.6; }` rule (around line 223–225), add:
|
||||
|
||||
```css
|
||||
mark {
|
||||
background-color: var(--highlight-bg);
|
||||
color: var(--highlight-text);
|
||||
border-radius: 2px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
```
|
||||
|
||||
Note: find marks created by the in-page find feature use inline styles that override this rule, so there is no conflict.
|
||||
|
||||
- [ ] **Step 2: Build to verify**
|
||||
|
||||
```bash
|
||||
just build
|
||||
```
|
||||
|
||||
Expected: build succeeds.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Update built-in theme JSON files
|
||||
|
||||
**Files:**
|
||||
- Modify: all 20 files in `markdownViewr/Resources/themes/`
|
||||
|
||||
Each file needs `"highlightBackground"` and `"highlightText"` added inside the `"colors"` object, after `"blockquoteBackground"`.
|
||||
|
||||
- [ ] **Step 1: Update `catppuccin-mocha.json`** (dark)
|
||||
|
||||
Add after `"blockquoteBackground"`:
|
||||
```json
|
||||
"highlightBackground": "#f9e2af",
|
||||
"highlightText": "#1e1e2e"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update `catppuccin-latte.json`** (light)
|
||||
|
||||
```json
|
||||
"highlightBackground": "#f9e2af",
|
||||
"highlightText": "#4c4f69"
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update `dracula.json`** (dark)
|
||||
|
||||
```json
|
||||
"highlightBackground": "#f1fa8c",
|
||||
"highlightText": "#282a36"
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Update `everforest-dark.json`** (dark)
|
||||
|
||||
```json
|
||||
"highlightBackground": "#dbbc7f",
|
||||
"highlightText": "#2d353b"
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Update `github-dark.json`** (dark)
|
||||
|
||||
```json
|
||||
"highlightBackground": "#e3b341",
|
||||
"highlightText": "#0d1117"
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Update `github-light.json`** (light)
|
||||
|
||||
```json
|
||||
"highlightBackground": "#fff8c5",
|
||||
"highlightText": "#1f2328"
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Update `gruvbox-dark.json`** (dark)
|
||||
|
||||
```json
|
||||
"highlightBackground": "#fabd2f",
|
||||
"highlightText": "#282828"
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Update `gruvbox-light.json`** (light)
|
||||
|
||||
```json
|
||||
"highlightBackground": "#fabd2f",
|
||||
"highlightText": "#282828"
|
||||
```
|
||||
|
||||
- [ ] **Step 9: Update `kanagawa.json`** (dark)
|
||||
|
||||
```json
|
||||
"highlightBackground": "#ff9e3b",
|
||||
"highlightText": "#1f1f28"
|
||||
```
|
||||
|
||||
- [ ] **Step 10: Update `monokai.json`** (dark)
|
||||
|
||||
```json
|
||||
"highlightBackground": "#e6db74",
|
||||
"highlightText": "#272822"
|
||||
```
|
||||
|
||||
- [ ] **Step 11: Update `nord.json`** (dark)
|
||||
|
||||
```json
|
||||
"highlightBackground": "#ebcb8b",
|
||||
"highlightText": "#2e3440"
|
||||
```
|
||||
|
||||
- [ ] **Step 12: Update `one-dark.json`** (dark)
|
||||
|
||||
```json
|
||||
"highlightBackground": "#e5c07b",
|
||||
"highlightText": "#282c34"
|
||||
```
|
||||
|
||||
- [ ] **Step 13: Update `one-light.json`** (light)
|
||||
|
||||
```json
|
||||
"highlightBackground": "#ffd700",
|
||||
"highlightText": "#383a42"
|
||||
```
|
||||
|
||||
- [ ] **Step 14: Update `rose-pine.json`** (dark)
|
||||
|
||||
```json
|
||||
"highlightBackground": "#f6c177",
|
||||
"highlightText": "#191724"
|
||||
```
|
||||
|
||||
- [ ] **Step 15: Update `rose-pine-dawn.json`** (light)
|
||||
|
||||
```json
|
||||
"highlightBackground": "#f6c177",
|
||||
"highlightText": "#575279"
|
||||
```
|
||||
|
||||
- [ ] **Step 16: Update `solarized-dark.json`** (dark)
|
||||
|
||||
```json
|
||||
"highlightBackground": "#b58900",
|
||||
"highlightText": "#002b36"
|
||||
```
|
||||
|
||||
- [ ] **Step 17: Update `solarized-light.json`** (light)
|
||||
|
||||
```json
|
||||
"highlightBackground": "#b58900",
|
||||
"highlightText": "#fdf6e3"
|
||||
```
|
||||
|
||||
- [ ] **Step 18: Update `tokyo-night.json`** (dark)
|
||||
|
||||
```json
|
||||
"highlightBackground": "#e0af68",
|
||||
"highlightText": "#1a1b26"
|
||||
```
|
||||
|
||||
- [ ] **Step 19: Update `tokyo-night-light.json`** (light)
|
||||
|
||||
```json
|
||||
"highlightBackground": "#f9c859",
|
||||
"highlightText": "#343b58"
|
||||
```
|
||||
|
||||
- [ ] **Step 20: Update `zenburn.json`** (dark)
|
||||
|
||||
```json
|
||||
"highlightBackground": "#f0dfaf",
|
||||
"highlightText": "#3f3f3f"
|
||||
```
|
||||
|
||||
- [ ] **Step 21: Build to verify**
|
||||
|
||||
```bash
|
||||
just build
|
||||
```
|
||||
|
||||
Expected: build succeeds.
|
||||
|
||||
- [ ] **Step 22: Commit**
|
||||
|
||||
```bash
|
||||
git add markdownViewr/ThemeManager.swift markdownViewr/Resources/template.html markdownViewr/Resources/themes/
|
||||
git commit -m "add highlight CSS variable, mark rule, and highlight colors to all built-in themes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: MarkdownDocument — post-processing pass
|
||||
|
||||
**Files:**
|
||||
- Modify: `markdownViewr/MarkdownDocument.swift`
|
||||
|
||||
- [ ] **Step 1: Update `convertToHTML` signature to accept `extensions`**
|
||||
|
||||
Change the function signature from:
|
||||
|
||||
```swift
|
||||
static func convertToHTML(_ markdown: String, frontmatterMode: FrontmatterMode = .hide) -> String {
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```swift
|
||||
static func convertToHTML(_ markdown: String, frontmatterMode: FrontmatterMode = .hide, extensions: MarkdownExtensions = MarkdownExtensions()) -> String {
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Call `applyExtensions` as the last step in `convertToHTML`**
|
||||
|
||||
At the end of `convertToHTML`, change the final two lines from:
|
||||
|
||||
```swift
|
||||
var htmlVisitor = HTMLConverter()
|
||||
html += htmlVisitor.visit(document)
|
||||
return html
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```swift
|
||||
var htmlVisitor = HTMLConverter()
|
||||
html += htmlVisitor.visit(document)
|
||||
return applyExtensions(html, extensions: extensions)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add the three private static helpers at the bottom of `MarkdownDocument`** (before the closing brace, after `escapeHTML`)
|
||||
|
||||
```swift
|
||||
private static func applyExtensions(_ html: String, extensions: MarkdownExtensions) -> String {
|
||||
// Split on <pre>...</pre> blocks so code blocks are never transformed.
|
||||
var segments = html.components(separatedBy: "<pre>")
|
||||
for i in 0..<segments.count {
|
||||
if i == 0 {
|
||||
segments[i] = applyExtensionRegexes(segments[i], extensions: extensions)
|
||||
} else {
|
||||
// Each segment begins with the pre block content up to </pre>,
|
||||
// followed by regular content.
|
||||
if let endRange = segments[i].range(of: "</pre>") {
|
||||
let preContent = String(segments[i][..<endRange.upperBound])
|
||||
let afterPre = applyExtensionRegexes(
|
||||
String(segments[i][endRange.upperBound...]),
|
||||
extensions: extensions
|
||||
)
|
||||
segments[i] = preContent + afterPre
|
||||
}
|
||||
}
|
||||
}
|
||||
return segments.joined(separator: "<pre>")
|
||||
}
|
||||
|
||||
private static func applyExtensionRegexes(_ text: String, extensions: MarkdownExtensions) -> String {
|
||||
var result = text
|
||||
if extensions.highlight {
|
||||
result = applyRegex(result, pattern: "==(.+?)==", template: "<mark>$1</mark>")
|
||||
}
|
||||
if extensions.superscript {
|
||||
result = applyRegex(result, pattern: "\\^(.+?)\\^", template: "<sup>$1</sup>")
|
||||
}
|
||||
if extensions.subscript_ {
|
||||
result = applyRegex(result, pattern: "~(.+?)~", template: "<sub>$1</sub>")
|
||||
}
|
||||
if extensions.underline {
|
||||
result = applyRegex(result, pattern: "\\+\\+(.+?)\\+\\+", template: "<ins>$1</ins>")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private static func applyRegex(_ text: String, pattern: String, template: String) -> String {
|
||||
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return text }
|
||||
let range = NSRange(text.startIndex..., in: text)
|
||||
return regex.stringByReplacingMatches(in: text, range: range, withTemplate: template)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Build to verify**
|
||||
|
||||
```bash
|
||||
just build
|
||||
```
|
||||
|
||||
Expected: build succeeds.
|
||||
|
||||
---
|
||||
|
||||
### Task 6: ContentView — wire extensions into rerender
|
||||
|
||||
**Files:**
|
||||
- Modify: `markdownViewr/ContentView.swift`
|
||||
|
||||
- [ ] **Step 1: Update `rerender()` to pass `extensions`**
|
||||
|
||||
Change:
|
||||
|
||||
```swift
|
||||
private func rerender() {
|
||||
renderedHTML = MarkdownDocument.convertToHTML(currentMarkdown, frontmatterMode: themeManager.frontmatterMode)
|
||||
}
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```swift
|
||||
private func rerender() {
|
||||
renderedHTML = MarkdownDocument.convertToHTML(
|
||||
currentMarkdown,
|
||||
frontmatterMode: themeManager.frontmatterMode,
|
||||
extensions: themeManager.markdownExtensions
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `onReceive` for `markdownExtensions`**
|
||||
|
||||
After the existing `.onReceive(themeManager.$frontmatterMode)` block, add:
|
||||
|
||||
```swift
|
||||
.onReceive(themeManager.$markdownExtensions) { _ in
|
||||
DispatchQueue.main.async { rerender() }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Build to verify**
|
||||
|
||||
```bash
|
||||
just build
|
||||
```
|
||||
|
||||
Expected: build succeeds.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add markdownViewr/MarkdownDocument.swift markdownViewr/ContentView.swift
|
||||
git commit -m "add post-processing pass for markdown extensions (highlight, superscript, subscript, underline)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Settings UI — Markdown tab
|
||||
|
||||
**Files:**
|
||||
- Modify: `markdownViewr/SettingsView.swift`
|
||||
|
||||
- [ ] **Step 1: Add `MarkdownSettingsView` struct to `SettingsView.swift`**
|
||||
|
||||
Add this new struct before the `ThemeEditorItem` struct (before line 265):
|
||||
|
||||
```swift
|
||||
struct MarkdownSettingsView: View {
|
||||
@EnvironmentObject var themeManager: ThemeManager
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text("Markdown Extensions")
|
||||
.font(.headline)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text("Enable or disable inline syntax extensions applied during rendering.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 16)
|
||||
|
||||
GroupBox {
|
||||
VStack(spacing: 0) {
|
||||
extensionRow(
|
||||
name: "Highlight",
|
||||
example: "==text==",
|
||||
isOn: Binding(
|
||||
get: { themeManager.markdownExtensions.highlight },
|
||||
set: { themeManager.markdownExtensions.highlight = $0 }
|
||||
)
|
||||
)
|
||||
Divider().padding(.vertical, 6)
|
||||
extensionRow(
|
||||
name: "Superscript",
|
||||
example: "^text^",
|
||||
isOn: Binding(
|
||||
get: { themeManager.markdownExtensions.superscript },
|
||||
set: { themeManager.markdownExtensions.superscript = $0 }
|
||||
)
|
||||
)
|
||||
Divider().padding(.vertical, 6)
|
||||
extensionRow(
|
||||
name: "Subscript",
|
||||
example: "~text~",
|
||||
isOn: Binding(
|
||||
get: { themeManager.markdownExtensions.subscript_ },
|
||||
set: { themeManager.markdownExtensions.subscript_ = $0 }
|
||||
)
|
||||
)
|
||||
Divider().padding(.vertical, 6)
|
||||
extensionRow(
|
||||
name: "Underline",
|
||||
example: "++text++",
|
||||
isOn: Binding(
|
||||
get: { themeManager.markdownExtensions.underline },
|
||||
set: { themeManager.markdownExtensions.underline = $0 }
|
||||
)
|
||||
)
|
||||
}
|
||||
.padding(4)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
|
||||
private func extensionRow(name: String, example: String, isOn: Binding<Bool>) -> some View {
|
||||
HStack {
|
||||
Text(name)
|
||||
.frame(width: 100, alignment: .leading)
|
||||
Text(example)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
Toggle("", isOn: isOn)
|
||||
.toggleStyle(.checkbox)
|
||||
.labelsHidden()
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the Markdown tab to `SettingsView`**
|
||||
|
||||
In `SettingsView.body`, add the new tab after the Themes tab:
|
||||
|
||||
```swift
|
||||
MarkdownSettingsView()
|
||||
.tabItem {
|
||||
Label("Markdown", systemImage: "doc.plaintext")
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Build to verify**
|
||||
|
||||
```bash
|
||||
just build
|
||||
```
|
||||
|
||||
Expected: build succeeds.
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Theme Editor — highlight color pickers
|
||||
|
||||
**Files:**
|
||||
- Modify: `markdownViewr/ThemeEditorView.swift`
|
||||
- Modify: `markdownViewr/SampleMarkdown.swift`
|
||||
|
||||
- [ ] **Step 1: Add highlight color rows to `colorsSection` in `ThemeEditorView.swift`**
|
||||
|
||||
In the `LazyVGrid` inside `colorsSection`, after `colorRow("Quote BG", $colors.blockquoteBackground)`, add:
|
||||
|
||||
```swift
|
||||
colorRow("Highlight BG", $colors.highlightBackground)
|
||||
colorRow("Highlight Text", $colors.highlightText)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add highlight/extension examples to `sampleMarkdown` in `SampleMarkdown.swift`**
|
||||
|
||||
Find the sample markdown string and add a new section so the Theme Editor preview shows the highlight color in action. Add this block to the sample (a sensible place is near the inline formatting examples):
|
||||
|
||||
```swift
|
||||
## Extensions
|
||||
|
||||
This sentence uses ==highlighted text==, H~2~O for subscript, E=mc^2^ for superscript, and ++underlined text++.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Build to verify**
|
||||
|
||||
```bash
|
||||
just build
|
||||
```
|
||||
|
||||
Expected: build succeeds.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add markdownViewr/SettingsView.swift markdownViewr/ThemeEditorView.swift markdownViewr/SampleMarkdown.swift
|
||||
git commit -m "add Markdown settings tab and highlight color pickers to theme editor"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Manual test
|
||||
|
||||
- [ ] **Step 1: Launch the app**
|
||||
|
||||
```bash
|
||||
just run
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Open a test file with all four extension types**
|
||||
|
||||
Create `/tmp/extensions-test.md` with the following content:
|
||||
|
||||
```markdown
|
||||
# Extension Test
|
||||
|
||||
Regular text with ==highlighted phrase== inline.
|
||||
|
||||
E=mc^2^ uses superscript.
|
||||
|
||||
H~2~O uses subscript.
|
||||
|
||||
This is ++underlined text++ using plus markers.
|
||||
|
||||
Code blocks must NOT be transformed:
|
||||
|
||||
==not highlighted==
|
||||
^not superscript^
|
||||
~not subscript~
|
||||
++not underlined++
|
||||
|
||||
Fenced code blocks must also be skipped:
|
||||
|
||||
```text
|
||||
==still not highlighted==
|
||||
```
|
||||
|
||||
Open this file in the app.
|
||||
|
||||
- [ ] **Step 3: Verify each extension renders correctly**
|
||||
|
||||
- `==highlighted phrase==` → yellow/amber background highlight (matching the active theme's highlight color)
|
||||
- `mc^2^` → `2` as superscript
|
||||
- `H~2~O` → `2` as subscript
|
||||
- `++underlined text++` → underline styling
|
||||
- Content inside code blocks: no transformation, literal syntax visible
|
||||
|
||||
- [ ] **Step 4: Verify toggles work**
|
||||
|
||||
Open Settings → Markdown tab. Toggle Highlight off. Verify the highlighted phrase no longer has a background and shows `==highlighted phrase==` literally. Toggle it back on and verify it re-renders immediately.
|
||||
|
||||
- [ ] **Step 5: Verify highlight color in Theme Editor**
|
||||
|
||||
Open Settings → Themes → select a theme → Edit. Verify "Highlight BG" and "Highlight Text" color pickers appear in the Colors section. Change the highlight color and verify the preview updates immediately.
|
||||
|
||||
- [ ] **Step 6: Verify theme switching**
|
||||
|
||||
Switch between a dark theme and a light theme in the toolbar. Verify the highlight background color changes appropriately with the theme.
|
||||
@@ -0,0 +1,134 @@
|
||||
# Markdown Extensions Design
|
||||
|
||||
**Date:** 2026-06-04
|
||||
**Status:** Approved
|
||||
|
||||
## Overview
|
||||
|
||||
Add a Markdown tab to the Settings window that lets users toggle four inline
|
||||
syntax extensions. Enabled extensions are applied via a post-processing pass
|
||||
on the generated HTML.
|
||||
|
||||
## Extensions
|
||||
|
||||
| Syntax | Name | HTML output | Default |
|
||||
|---|---|---|---|
|
||||
| `==text==` | Highlight | `<mark>text</mark>` | on |
|
||||
| `^text^` | Superscript | `<sup>text</sup>` | on |
|
||||
| `~text~` | Subscript | `<sub>text</sub>` | on |
|
||||
| `++text++` | Underline | `<ins>text</ins>` | on |
|
||||
|
||||
Strikethrough (`~~text~~`) is already handled at the parser level (swift-markdown
|
||||
GFM extension) and is not part of this feature.
|
||||
|
||||
## Data Model
|
||||
|
||||
New `MarkdownExtensions` struct in its own file (`MarkdownExtensions.swift`):
|
||||
|
||||
```swift
|
||||
struct MarkdownExtensions: Codable {
|
||||
var highlight: Bool = true
|
||||
var superscript: Bool = true
|
||||
var subscript_: Bool = true // trailing underscore: `subscript` is a Swift keyword
|
||||
var underline: Bool = true
|
||||
}
|
||||
```
|
||||
|
||||
`ThemeManager` gets one new `@Published var markdownExtensions: MarkdownExtensions`
|
||||
property. On `didSet` it encodes to JSON and writes to UserDefaults key
|
||||
`markdownExtensions`. On init it reads and decodes from UserDefaults, falling
|
||||
back to `MarkdownExtensions()` (all on) if the key is absent or malformed.
|
||||
|
||||
## Post-Processing
|
||||
|
||||
`MarkdownDocument` gains a private static `applyExtensions(_:extensions:) -> String`
|
||||
helper. It is called as the final step of `convertToHTML`, after `HTMLConverter`
|
||||
produces its output.
|
||||
|
||||
The helper splits the HTML on `<pre>` / `</pre>` boundaries. Segments inside
|
||||
`<pre>` blocks are passed through unchanged. Segments outside have each enabled
|
||||
extension's regex applied using `NSRegularExpression`.
|
||||
|
||||
Patterns (applied in the order listed):
|
||||
|
||||
| Extension | Regex | Replacement |
|
||||
|---|---|---|
|
||||
| Highlight | `==(.+?)==` | `<mark>$1</mark>` |
|
||||
| Superscript | `\^(.+?)\^` | `<sup>$1</sup>` |
|
||||
| Subscript | `~(.+?)~` | `<sub>$1</sub>` |
|
||||
| Underline | `\+\+(.+?)\+\+` | `<ins>$1</ins>` |
|
||||
|
||||
`convertToHTML` signature gains one new parameter:
|
||||
|
||||
```swift
|
||||
static func convertToHTML(
|
||||
_ markdown: String,
|
||||
frontmatterMode: FrontmatterMode = .hide,
|
||||
extensions: MarkdownExtensions = MarkdownExtensions()
|
||||
) -> String
|
||||
```
|
||||
|
||||
All existing callers that omit `extensions` get the all-on defaults.
|
||||
|
||||
## Settings UI
|
||||
|
||||
A fourth tab is added to the `TabView` in `SettingsView`:
|
||||
|
||||
- Label: "Markdown", system image: `doc.plaintext`
|
||||
- New struct `MarkdownSettingsView` in `SettingsView.swift`
|
||||
- One `GroupBox` containing a `VStack` of four rows
|
||||
- Each row: extension name (left), example syntax in secondary monospaced text
|
||||
(center), `Toggle` with `.checkbox` style (right)
|
||||
- Toggling writes through `themeManager.markdownExtensions` immediately
|
||||
|
||||
## Re-render Trigger
|
||||
|
||||
`ContentView` already observes `themeManager` via `@EnvironmentObject` and
|
||||
re-renders on published changes. Adding `onReceive(themeManager.$markdownExtensions)`
|
||||
(same pattern as `frontmatterMode`) triggers a re-render when any toggle changes.
|
||||
No restart required.
|
||||
|
||||
## Theme Color: Highlight
|
||||
|
||||
`<mark>` has a browser-default yellow background that looks wrong on dark themes.
|
||||
`ThemeColors` gets two new fields:
|
||||
|
||||
```swift
|
||||
var highlightBackground: String // default: "#ffd700" (gold — readable on dark)
|
||||
var highlightText: String // default: "#1e1e2e" (dark — pairs with gold bg)
|
||||
```
|
||||
|
||||
`ThemeManager.generateCSS` emits `--highlight-bg` and `--highlight-text` CSS
|
||||
variables. The HTML template adds:
|
||||
|
||||
```css
|
||||
mark {
|
||||
background-color: var(--highlight-bg);
|
||||
color: var(--highlight-text);
|
||||
border-radius: 2px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
```
|
||||
|
||||
All 20 built-in theme JSON files get the two new color fields with
|
||||
theme-appropriate values. The Theme Editor color list gains a "Highlight"
|
||||
section (background + text), matching the existing "Code" section layout.
|
||||
|
||||
User themes that predate this change omit these fields in their JSON. Swift's
|
||||
auto-synthesized `Decodable` throws on missing keys, so `ThemeColors` needs a
|
||||
custom `init(from decoder:)` that uses `decodeIfPresent` for the two new fields
|
||||
and falls back to the `ThemeColors.init` defaults when absent.
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `MarkdownExtensions.swift` | New — struct definition |
|
||||
| `Theme.swift` | Add `highlightBackground`, `highlightText` to `ThemeColors` |
|
||||
| `ThemeManager.swift` | Add `markdownExtensions` published property; emit `--highlight-bg`/`--highlight-text` in `generateCSS` |
|
||||
| `MarkdownDocument.swift` | Add `applyExtensions` helper, update `convertToHTML` signature |
|
||||
| `SettingsView.swift` | Add `MarkdownSettingsView`, add Markdown tab |
|
||||
| `ContentView.swift` | Add `onReceive` for `markdownExtensions` |
|
||||
| `ThemeEditorView.swift` | Add Highlight color pickers |
|
||||
| `Resources/template.html` | Add `mark { … }` CSS rule |
|
||||
| `Resources/themes/*.json` (×20) | Add `highlightBackground` + `highlightText` per theme |
|
||||
@@ -30,17 +30,27 @@ release: kill
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
VERSION=$(grep 'MARKETING_VERSION' project.yml | head -1 | sed 's/.*"\(.*\)".*/\1/')
|
||||
MAJOR=$(echo "$VERSION" | cut -d. -f1)
|
||||
MINOR=$(echo "$VERSION" | cut -d. -f2)
|
||||
PATCH=$(echo "$VERSION" | cut -d. -f3)
|
||||
BUILD_NUMBER=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
|
||||
LATEST=$(gh release list --repo darinkelkhoff/markdownViewr --limit 1 --json tagName --jq '.[0].tagName' 2>/dev/null || echo "none")
|
||||
if [[ "$LATEST" == "v$VERSION" ]]; then
|
||||
echo "Error: v$VERSION is already released. Bump MARKETING_VERSION in project.yml first."
|
||||
exit 1
|
||||
fi
|
||||
ARCHIVE="/tmp/markdownViewr.xcarchive"
|
||||
EXPORT="/tmp/markdownViewr-export"
|
||||
ZIP="/tmp/markdownViewr-$VERSION.zip"
|
||||
echo "==> Archiving v$VERSION..."
|
||||
echo "==> Archiving v$VERSION (build $BUILD_NUMBER)..."
|
||||
rm -rf "$ARCHIVE" "$EXPORT" "$ZIP"
|
||||
xcodebuild archive \
|
||||
-project markdownViewr.xcodeproj \
|
||||
-scheme markdownViewr \
|
||||
-archivePath "$ARCHIVE" \
|
||||
-destination 'generic/platform=macOS' \
|
||||
ENABLE_HARDENED_RUNTIME=YES | xcpretty || true
|
||||
ENABLE_HARDENED_RUNTIME=YES \
|
||||
CURRENT_PROJECT_VERSION="$BUILD_NUMBER" | xcpretty || true
|
||||
echo "==> Exporting with Developer ID signing..."
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "$ARCHIVE" \
|
||||
@@ -58,8 +68,24 @@ release: kill
|
||||
xcrun stapler staple "$APP"
|
||||
echo "==> Packaging final release..."
|
||||
ditto -c -k --keepParent "$APP" "$ZIP"
|
||||
echo "==> Creating DMG..."
|
||||
DMG="/tmp/markdownViewr.dmg"
|
||||
DMG_STAGING="/tmp/markdownViewr-dmg-staging"
|
||||
rm -f "$DMG"
|
||||
rm -rf "$DMG_STAGING"
|
||||
mkdir "$DMG_STAGING"
|
||||
cp -R "$APP" "$DMG_STAGING/"
|
||||
create-dmg \
|
||||
--volname "markdownViewr" \
|
||||
--window-size 660 400 \
|
||||
--icon-size 128 \
|
||||
--icon "markdownViewr.app" 180 185 \
|
||||
--hide-extension "markdownViewr.app" \
|
||||
--app-drop-link 480 185 \
|
||||
"$DMG" \
|
||||
"$DMG_STAGING/"
|
||||
echo "==> Creating GitHub release v$VERSION..."
|
||||
gh release create "v$VERSION" "$ZIP" \
|
||||
gh release create "v$VERSION" "$DMG" "$ZIP" \
|
||||
--repo darinkelkhoff/markdownViewr \
|
||||
--title "markdownViewr v$VERSION" \
|
||||
--generate-notes
|
||||
|
||||
+2
-2
@@ -31,8 +31,8 @@ targets:
|
||||
base:
|
||||
INFOPLIST_FILE: markdownViewr/Info.plist
|
||||
PRODUCT_BUNDLE_IDENTIFIER: com.dkelkhoff.markdownViewr
|
||||
MARKETING_VERSION: "1.1.2"
|
||||
CURRENT_PROJECT_VERSION: "1"
|
||||
MARKETING_VERSION: "1.1.5"
|
||||
CURRENT_PROJECT_VERSION: "3"
|
||||
MACOSX_DEPLOYMENT_TARGET: "13.0"
|
||||
ENABLE_APP_SANDBOX: "NO"
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||
|
||||
Reference in new issue
Block a user