Compare commits

Author SHA1 Message Date
james.maesandClaude Opus 5.5 6e25ec7963 docs: mark this repository as a contribution fork of the upstream app
The README was the upstream README word for word, so it read as if
KofTwentyTwo shipped the macOS app. Every download, cask and update link
points at darinkelkhoff/markdownViewr, and this fork publishes no
releases. Say so, and point to the upstream printing PRs (#2 merged,
#3 open) and to the Windows port.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 06:23:23 -05:00
james.maes 1ab16bf4ed feat(printing): add print font scaling options and document-proportional clean print theme sizing 2026-07-24 08:28:58 -05:00
james.maes 0d1133aafd test(printing): add unit test validating every theme option generates non-empty CSS and script injection 2026-07-24 08:26:49 -05:00
james.maes fbb313ccb9 test(printing): expand test assertions for all heading sizes, colors, blockquotes, and highlights 2026-07-24 08:26:15 -05:00
james.maes 53a6ae7896 fix(printing): enforce theme fonts, sizes, and colors in print media queries 2026-07-24 08:23:49 -05:00
Darin Kelkhoff 76ed62618c release: add 1.5.0 release notes link to appcast 2026-07-23 08:35:09 -05:00
7 changed files with 222 additions and 16 deletions

No files matched your search

+2
View File
@@ -1,5 +1,7 @@
# markdownViewr
> **This is KofTwentyTwo's contribution fork of [darinkelkhoff/markdownViewr](https://github.com/darinkelkhoff/markdownViewr).** It publishes no builds. The download, Homebrew cask and Sparkle updates below all come from the upstream project. The fork carries printing work that is offered upstream: native printing merged as [darinkelkhoff/markdownViewr#2](https://github.com/darinkelkhoff/markdownViewr/pull/2), and the print-styling fixes are open as [#3](https://github.com/darinkelkhoff/markdownViewr/pull/3). Report app issues upstream. The Windows port is [KofTwentyTwo/MarkdownViewr.Windows](https://github.com/KofTwentyTwo/MarkdownViewr.Windows).
A fast, beautiful markdown viewer for macOS. Open any markdown file — it live-reloads as you edit in your favorite editor.
[![Download](https://img.shields.io/badge/Download-macOS-blue?style=flat-square&logo=apple)](https://github.com/darinkelkhoff/markdownViewr/releases/latest/download/markdownViewr.dmg)
+2 -1
View File
@@ -10,6 +10,7 @@
<pubDate>Thu, 23 Jul 2026 08:33:55 -0500</pubDate>
<sparkle:version>10500</sparkle:version>
<sparkle:shortVersionString>1.5.0</sparkle:shortVersionString>
<sparkle:releaseNotesLink>https://darinkelkhoff.github.io/markdownViewr/releases/1.5.0.html</sparkle:releaseNotesLink>
<sparkle:minimumSystemVersion>13.0</sparkle:minimumSystemVersion>
<enclosure url="https://github.com/darinkelkhoff/markdownViewr/releases/download/v1.5.0/markdownViewr-1.5.0.zip" length="4426474" type="application/octet-stream" sparkle:edSignature="sIjjKAMzb8Fv9zjr0vkfbpxeriD+lmkdB/L+wcs/Ay0mcvYu+WIr4UkIpeYBLsLevOl2aYfM3+KOhHdTjAnXDA=="/>
</item>
@@ -32,4 +33,4 @@
<enclosure url="https://github.com/darinkelkhoff/markdownViewr/releases/download/v1.3.0/markdownViewr-1.3.0.zip" length="4256339" type="application/octet-stream" sparkle:edSignature="TNbtuUw4oI/YHBwiHqtfy4CFTu/pay+R/fGjF/usHw9DWVfKh+tRvgu8UyHVe0SbR7KFX6PGg1aVzeONMicqDw=="/>
</item>
</channel>
</rss>
</rss>
+24 -2
View File
@@ -272,11 +272,13 @@ struct ContentView: View {
} else if printTheme == "Plain HTML" {
themeToPrint = nil
} else {
themeToPrint = themeManager.themes.first { $0.name == printTheme } ?? Theme.cleanPrinting
themeToPrint = themeManager.allThemes.first { $0.name == printTheme }
?? themeManager.themes.first { $0.name == printTheme }
?? Theme.cleanPrinting
}
// Determine zoom scale based on print settings
let zoom = printZoom == "Match Screen" ? zoomScale : 1.0
let zoom = PrintZoomResolver.zoomScale(for: printZoom, screenZoom: zoomScale)
let printCSS = themeToPrint != nil ? themeManager.generateCSS(for: themeToPrint!, zoomScale: zoom) : ""
let escapedPrintCSS = printCSS.escapedForJavaScriptLiteral()
@@ -1191,6 +1193,7 @@ struct PrintScriptGenerator {
\(fullWidthJS)
\(bordersJS)
\(paddingJS)
void document.body.offsetHeight;
"""
}
@@ -1226,3 +1229,22 @@ fileprivate extension String {
.replacingOccurrences(of: "\n", with: "\\n")
}
}
enum PrintZoomResolver {
static func zoomScale(for printZoom: String, screenZoom: Double) -> Double {
switch printZoom {
case "Compact (75%)":
return 0.75
case "Small (85%)":
return 0.85
case "Standard (100%)":
return 1.0
case "Large (115%)":
return 1.15
case "Match Screen":
return screenZoom
default:
return 1.0
}
}
}
+41 -4
View File
@@ -421,6 +421,40 @@ mark {
display: block !important;
background-color: var(--bg) !important;
color: var(--text) !important;
font-family: var(--body-font) !important;
font-size: var(--base-font-size) !important;
line-height: var(--line-height) !important;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--heading-font) !important;
}
h1 { color: var(--h1) !important; font-size: var(--h1-size) !important; }
h2 { color: var(--h2) !important; font-size: var(--h2-size) !important; }
h3 { color: var(--h3) !important; font-size: var(--h3-size) !important; }
h4 { color: var(--h4) !important; font-size: var(--h4-size) !important; }
h5 { color: var(--h5) !important; font-size: var(--h5-size) !important; }
h6 { color: var(--h6) !important; font-size: var(--h6-size) !important; }
a { color: var(--link) !important; }
pre { background-color: var(--code-bg) !important; }
pre code, code {
font-family: var(--code-font) !important;
font-size: var(--code-font-size) !important;
color: var(--code-text) !important;
}
code { background-color: var(--code-bg) !important; }
blockquote {
border-left-color: var(--blockquote-border) !important;
background-color: var(--blockquote-bg) !important;
}
mark {
background-color: var(--highlight-bg) !important;
color: var(--highlight-text) !important;
}
#content {
@@ -454,22 +488,25 @@ mark {
html.print-no-backgrounds, html.print-no-backgrounds body {
background-color: #ffffff !important;
color: #000000 !important;
}
html.print-no-backgrounds * {
-webkit-print-color-adjust: economy !important;
print-color-adjust: economy !important;
}
html.print-no-backgrounds pre,
html.print-no-backgrounds code,
html.print-no-backgrounds blockquote,
html.print-no-backgrounds mark,
html.print-no-backgrounds .frontmatter {
background: transparent !important;
background-color: transparent !important;
background-image: none !important;
/* Avoid light text rendering on white backgrounds */
color: #000000 !important;
}
/* Maintain link readability in ink-saving mode */
html.print-no-backgrounds a {
color: #0969da !important;
text-decoration: underline !important;
}
+7 -3
View File
@@ -581,17 +581,21 @@ struct PrintingSettingsView: View {
GroupBox(label: Text("Zoom & Scaling")) {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Font Zoom")
Text("Font Scaling")
Spacer()
Picker("Font Zoom", selection: $printZoom) {
Picker("Font Scaling", selection: $printZoom) {
Text("Compact (75%)").tag("Compact (75%)")
Text("Small (85%)").tag("Small (85%)")
Text("Standard (100%)").tag("Standard (100%)")
Text("Large (115%)").tag("Large (115%)")
Divider()
Text("Match Screen").tag("Match Screen")
}
.frame(width: 200)
.labelsHidden()
}
Text("Select whether the printed document inherits the screen zoom scaling.")
Text("Select the scale for printed document fonts, or match current screen zoom.")
.font(.caption)
.foregroundStyle(.secondary)
+11 -1
View File
@@ -194,7 +194,17 @@ extension Theme {
highlightText: "#24292e"
),
fonts: ThemeFonts(body: "System", heading: "System", code: "SF Mono"),
sizes: ThemeSizes(),
sizes: ThemeSizes(
baseFontSize: 11,
h1Size: 22,
h2Size: 18,
h3Size: 15,
h4Size: 13.5,
h5Size: 12,
h6Size: 11,
codeFontSize: 10.5,
lineHeight: 1.5
),
customCSS: "",
isBuiltIn: true
)
+135 -5
View File
@@ -142,14 +142,12 @@ final class PrintingTests: XCTestCase {
XCTAssertTrue(printBlock.contains("-webkit-print-color-adjust: exact !important"))
XCTAssertTrue(printBlock.contains("print-color-adjust: exact !important"))
// Verify print background toggling override style exists
XCTAssertTrue(printBlock.contains("html.print-no-backgrounds *"))
// Verify print background toggling override style exists without wiping theme text colors
XCTAssertTrue(printBlock.contains("html.print-no-backgrounds"))
XCTAssertTrue(printBlock.contains("print-color-adjust: economy !important"))
XCTAssertTrue(printBlock.contains("background: transparent !important"))
XCTAssertTrue(printBlock.contains("color: #000000 !important"))
XCTAssertFalse(printBlock.contains("color: #000000 !important"))
// Verify ink-saving layout overrides for links, rules, headers, and blockquotes
XCTAssertTrue(printBlock.contains("html.print-no-backgrounds a"))
XCTAssertTrue(printBlock.contains("html.print-no-backgrounds hr"))
XCTAssertTrue(printBlock.contains("html.print-no-backgrounds blockquote"))
XCTAssertTrue(printBlock.contains("html.print-no-backgrounds h1"))
@@ -205,6 +203,138 @@ final class PrintingTests: XCTestCase {
XCTAssertTrue(printBlock.contains("white-space: normal"))
}
func testPrintMediaQueriesPreserveThemeColorsInInkSavingMode() throws {
let template = try loadTemplate()
let printBlock = try printMediaQueryBlock(in: template)
// Ensure print-no-backgrounds class does not apply universal color: #000000 !important
// so that selected theme CSS variables (--h1, --h2, --link, --code-text, --text) apply during print.
XCTAssertFalse(printBlock.contains("html.print-no-backgrounds * {\n -webkit-print-color-adjust: economy !important;\n print-color-adjust: economy !important;\n background: transparent !important;\n background-color: transparent !important;\n background-image: none !important;\n /* Avoid light text rendering on white backgrounds */\n color: #000000 !important;\n }"))
XCTAssertFalse(printBlock.contains("html.print-no-backgrounds a {\n color: #0969da !important;"))
}
func testPrintMediaQueriesEnforceThemeFontsSizesAndColors() throws {
let template = try loadTemplate()
let printBlock = try printMediaQueryBlock(in: template)
// Verify fonts are explicitly bound in @media print
XCTAssertTrue(printBlock.contains("var(--body-font) !important"))
XCTAssertTrue(printBlock.contains("var(--base-font-size) !important"))
XCTAssertTrue(printBlock.contains("var(--heading-font) !important"))
XCTAssertTrue(printBlock.contains("var(--code-font) !important"))
XCTAssertTrue(printBlock.contains("var(--code-font-size) !important"))
// Verify heading sizes are explicitly bound in @media print
XCTAssertTrue(printBlock.contains("var(--h1-size) !important"))
XCTAssertTrue(printBlock.contains("var(--h2-size) !important"))
XCTAssertTrue(printBlock.contains("var(--h3-size) !important"))
XCTAssertTrue(printBlock.contains("var(--h4-size) !important"))
XCTAssertTrue(printBlock.contains("var(--h5-size) !important"))
XCTAssertTrue(printBlock.contains("var(--h6-size) !important"))
// Verify colors are explicitly bound in @media print
XCTAssertTrue(printBlock.contains("var(--h1) !important"))
XCTAssertTrue(printBlock.contains("var(--h2) !important"))
XCTAssertTrue(printBlock.contains("var(--h3) !important"))
XCTAssertTrue(printBlock.contains("var(--h4) !important"))
XCTAssertTrue(printBlock.contains("var(--h5) !important"))
XCTAssertTrue(printBlock.contains("var(--h6) !important"))
XCTAssertTrue(printBlock.contains("var(--link) !important"))
XCTAssertTrue(printBlock.contains("var(--code-text) !important"))
XCTAssertTrue(printBlock.contains("var(--code-bg) !important"))
XCTAssertTrue(printBlock.contains("var(--blockquote-border) !important"))
XCTAssertTrue(printBlock.contains("var(--blockquote-bg) !important"))
XCTAssertTrue(printBlock.contains("var(--highlight-bg) !important"))
XCTAssertTrue(printBlock.contains("var(--highlight-text) !important"))
}
func testPrintPrepareScriptForcesLayoutPass() {
let script = PrintScriptGenerator.prepareScript(
escapedCSS: "body { color: red; }",
stripBackgrounds: false,
stripImages: false,
stripWidth: false,
printBorders: false,
pagePadding: "0px"
)
XCTAssertTrue(script.contains("document.body.offsetHeight;"))
}
@MainActor
func testEveryThemeOptionIsRespectedAndGeneratesValidPrintCSS() {
let manager = ThemeManager()
let themesToTest = manager.allThemes
XCTAssertFalse(themesToTest.isEmpty, "ThemeManager should load built-in themes")
for theme in themesToTest {
let css = manager.generateCSS(for: theme, zoomScale: 1.0)
XCTAssertFalse(css.isEmpty, "Theme '\(theme.name)' should generate non-empty CSS")
XCTAssertTrue(css.contains("--bg: \(theme.colors.background)"), "Theme '\(theme.name)' should include background color")
XCTAssertTrue(css.contains("--text: \(theme.colors.text)"), "Theme '\(theme.name)' should include text color")
XCTAssertTrue(css.contains("--h1: \(theme.colors.heading1)"), "Theme '\(theme.name)' should include h1 color")
XCTAssertTrue(css.contains("--link: \(theme.colors.link)"), "Theme '\(theme.name)' should include link color")
XCTAssertTrue(css.contains("--code-bg: \(theme.colors.codeBackground)"), "Theme '\(theme.name)' should include code bg color")
XCTAssertTrue(css.contains("--code-text: \(theme.colors.codeText)"), "Theme '\(theme.name)' should include code text color")
let prepareScript = PrintScriptGenerator.prepareScript(
escapedCSS: css.replacingOccurrences(of: "\n", with: "\\n"),
stripBackgrounds: false,
stripImages: false,
stripWidth: true,
printBorders: false,
pagePadding: "0px"
)
XCTAssertTrue(prepareScript.contains("document.getElementById('theme-css').textContent ="), "Script for '\(theme.name)' should target theme-css element")
XCTAssertTrue(prepareScript.contains(theme.colors.background), "Prepared script for '\(theme.name)' should inject background color")
}
// Verify Clean Printing option
let cleanCSS = manager.generateCSS(for: Theme.cleanPrinting, zoomScale: 1.0)
XCTAssertTrue(cleanCSS.contains("--bg: #ffffff"), "Clean printing should use white background")
XCTAssertTrue(cleanCSS.contains("--text: #24292e"), "Clean printing should use dark text")
// Verify Active Theme option
let activeCSS = manager.generateCSS(for: manager.activeTheme, zoomScale: 1.0)
XCTAssertFalse(activeCSS.isEmpty, "Active theme CSS should generate non-empty CSS")
// Verify Plain HTML option
let plainScript = PrintScriptGenerator.prepareScript(
escapedCSS: "",
stripBackgrounds: true,
stripImages: false,
stripWidth: true,
printBorders: false,
pagePadding: "0px"
)
XCTAssertTrue(plainScript.contains("document.getElementById('theme-css').textContent = ''"))
}
func testPrintZoomResolver() {
XCTAssertEqual(PrintZoomResolver.zoomScale(for: "Compact (75%)", screenZoom: 1.5), 0.75)
XCTAssertEqual(PrintZoomResolver.zoomScale(for: "Small (85%)", screenZoom: 1.5), 0.85)
XCTAssertEqual(PrintZoomResolver.zoomScale(for: "Standard (100%)", screenZoom: 1.5), 1.0)
XCTAssertEqual(PrintZoomResolver.zoomScale(for: "Large (115%)", screenZoom: 1.5), 1.15)
XCTAssertEqual(PrintZoomResolver.zoomScale(for: "Match Screen", screenZoom: 1.5), 1.5)
XCTAssertEqual(PrintZoomResolver.zoomScale(for: "Unknown", screenZoom: 1.5), 1.0)
}
func testCleanPrintingThemeUsesPrintProportionalFontSizes() {
let sizes = Theme.cleanPrinting.sizes
XCTAssertLessThanOrEqual(sizes.baseFontSize, 12.0)
XCTAssertLessThanOrEqual(sizes.h1Size, 22.0)
XCTAssertLessThanOrEqual(sizes.h2Size, 18.0)
XCTAssertLessThanOrEqual(sizes.codeFontSize, 11.0)
}
func testSettingsViewIncludesExpandedFontScalingOptions() throws {
let settingsSource = try loadSourceFile("markdownViewr/SettingsView.swift")
XCTAssertTrue(settingsSource.contains(#"Text("Compact (75%)").tag("Compact (75%)")"#))
XCTAssertTrue(settingsSource.contains(#"Text("Small (85%)").tag("Small (85%)")"#))
XCTAssertTrue(settingsSource.contains(#"Text("Standard (100%)").tag("Standard (100%)")"#))
XCTAssertTrue(settingsSource.contains(#"Text("Large (115%)").tag("Large (115%)")"#))
}
// 4. Functional test for PrintCoordinator script building and memory cycles
@MainActor
func testPrintCoordinatorMemoryManagementAndRestore() {