Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
466a1e655c
|
||
|
|
be68c9f1bb | ||
|
|
77b9274434 | ||
|
|
ab3731e0df | ||
|
|
4536db3f09 | ||
|
|
2bfdf91f77 | ||
|
|
6e25ec7963
|
||
|
|
1ab16bf4ed | ||
|
|
0d1133aafd | ||
|
|
fbb313ccb9 | ||
|
|
53a6ae7896 | ||
|
|
76ed62618c |
No files matched your search
@@ -0,0 +1,16 @@
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
tags: ['v*']
|
||||
jobs:
|
||||
macos:
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/macos.yaml@v1
|
||||
secrets: inherit
|
||||
with:
|
||||
# Build only: the app and the test bundle are compiled unsigned, the tests are not run. They are
|
||||
# app-hosted XCTests and the grogu runner has no GUI session for testmanagerd; run `just test`
|
||||
# locally before merging (README, Building from Source). Package.swift has no test target.
|
||||
commands: |
|
||||
xcodegen generate
|
||||
xcodebuild -quiet -project markdownViewr.xcodeproj -scheme markdownViewr -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO build-for-testing
|
||||
@@ -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.
|
||||
|
||||
[](https://github.com/darinkelkhoff/markdownViewr/releases/latest/download/markdownViewr.dmg)
|
||||
@@ -59,3 +61,12 @@ just run
|
||||
```
|
||||
|
||||
`just --list` shows all available recipes.
|
||||
|
||||
### Tests
|
||||
|
||||
The tests are app-hosted XCTests (`markdownViewrTests`). CI builds the app and the test bundle unsigned
|
||||
but does not run them (the CI macOS runner has no GUI session), so run them locally before merging:
|
||||
|
||||
```bash
|
||||
just test # xcodegen generate first if the project is stale
|
||||
```
|
||||
+2
-1
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,7 +275,7 @@ struct MarkdownDocument: FileDocument {
|
||||
private struct HTMLConverter: MarkupWalker {
|
||||
private var result = ""
|
||||
|
||||
mutating func visit(_ document: Document) -> String {
|
||||
mutating func visit(_ document: Markdown.Document) -> String {
|
||||
result = ""
|
||||
for child in document.children {
|
||||
visitChild(child)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user