15 Commits
Author SHA1 Message Date
Darin Kelkhoff 823b8f5ecc release 1.1.6 (fix version in about dialog) 2026-06-08 11:59:56 -05:00
Darin KelkhoffandClaude Sonnet 4.6 a0f1f88a4f release: run xcodegen generate before archiving
Ensures the .xcodeproj reflects the current project.yml (MARKETING_VERSION,
etc.) before the archive step, so the built app's version strings match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 11:59:02 -05:00
Darin Kelkhoff ca644efc4e release: update appcast for v1.1.5 2026-06-08 11:56:45 -05:00
Darin Kelkhoff be5995bb55 release 1.1.5 (adding dmg) 2026-06-08 11:54:51 -05:00
Darin KelkhoffandClaude Sonnet 4.6 3a3884155f release: derive CURRENT_PROJECT_VERSION from MARKETING_VERSION, fix DMG staging
Build number is computed as major*10000+minor*100+patch so Sparkle always
sees a strictly-increasing CFBundleVersion without a separate manual bump.

DMG is now staged from a clean temp directory containing only the .app,
so xcodebuild export artifacts (DistributionSummary.plist, etc.) no longer
appear in the mounted volume.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 11:54:27 -05:00
Darin Kelkhoff 97f398276a release: update appcast for v1.1.4 2026-06-08 11:38:07 -05:00
Darin Kelkhoff f60bb0aa53 release 1.1.4 2026-06-08 11:29:14 -05:00
Darin Kelkhoff 654a134753 widen hero and features max-width to 1200px 2026-06-08 11:28:28 -05:00
Darin Kelkhoff e8ad868277 add .nojekyll to prevent Jekyll from interfering with static assets 2026-06-08 11:27:12 -05:00
Darin KelkhoffandClaude Sonnet 4.6 3fbbe57c6f add app icon and screenshot to landing page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 11:25:41 -05:00
Darin KelkhoffandClaude Sonnet 4.6 6e71826787 add DMG creation to release recipe and GitHub Pages landing page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 11:23:47 -05:00
Darin KelkhoffandClaude Sonnet 4.6 e2a6aeafb2 guard release recipe against re-releasing an already-published version
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 10:10:39 -05:00
Darin Kelkhoff 2ed08c1e86 release: update appcast for v1.1.3 2026-06-08 10:07:18 -05:00
Darin KelkhoffandClaude Sonnet 4.6 e09b6713cc bump to 1.1.3; set CURRENT_PROJECT_VERSION=3 so Sparkle detects updates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 09:54:00 -05:00
Darin Kelkhoff a793a26977 release: update appcast for v1.1.2 2026-06-08 09:50:34 -05:00
9 changed files with 1084 additions and 7 deletions

No files matched your search

+18 -2
View File
@@ -5,13 +5,29 @@
<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 11:56:45 -0500</pubDate>
<sparkle:version>10105</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.5/markdownViewr-1.1.5.zip" length="4202371" type="application/octet-stream" sparkle:edSignature="8sD2QkODU6bzR134PtCSIQrmrEL7ihXcRelFWqZhnrB6RpdvrMmXs44uggcDxZDaNXERtJGP4JbISXFCLFedCg=="/>
</item>
<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>
View File
Whitespace-only changes.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1010 KiB

+167
View File
@@ -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 &nbsp;·&nbsp; 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 |
+31 -3
View File
@@ -30,17 +30,29 @@ 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 ))
echo "==> Regenerating Xcode project..."
xcodegen generate
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 +70,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
View File
@@ -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.6"
CURRENT_PROJECT_VERSION: "3"
MACOSX_DEPLOYMENT_TARGET: "13.0"
ENABLE_APP_SANDBOX: "NO"
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon