Initial commit

This commit is contained in:
2026-10-20 11:51:52 -05:00
commit b3ae1e5afe
17 changed files with 5651 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
.build/
.swiftpm/
+43
View File
@@ -0,0 +1,43 @@
# mdview
A macOS command-line tool, `mdview file.md`, that opens a native window showing the rendered markdown and live-reloads it whenever the file changes. Written in Swift with AppKit and WKWebView, built with SwiftPM. There's no Xcode project and no `.app` bundle: the output is one self-contained binary.
## Build / run
```
make build # swift build -c release -> .build/release/mdview
make install [PREFIX=~/.local] # default PREFIX is /usr/local
.build/release/mdview -f file.md # -f/--foreground: stay attached to the terminal (use when debugging)
```
There are no automated tests. To verify changes, run the binary against a sample markdown file that covers tables, task lists, code fences, footnotes, relative images and links.
## Layout
- `Sources/mdview/main.swift`: parses arguments, validates the file, then **detaches**. It re-launches itself via `posix_spawn` with `--foreground`, in a new session (`POSIX_SPAWN_SETSID`), with stdio sent to `/dev/null`, and the parent exits so the shell gets its prompt back. Only the `--foreground` process starts `NSApplication`. File errors must be caught *before* the detach, or the user never sees them.
- `AppDelegate.swift`: builds the main menu in code (there are no nibs), creates the single viewer, and quits when the last window closes. The activation policy `.regular` is set in `main.swift`; without it a bare binary gets no Dock icon or focus.
- `ViewerController.swift`: owns the `NSWindow` and `WKWebView`.
- The full page is written to `$TMPDIR/mdview-<pid>.html` and loaded with `loadFileURL(_, allowingReadAccessTo: "/")`. `loadHTMLString` can't read local images, which is why the temp file exists. The file is deleted on window close and on quit.
- **Live reload** doesn't reload the page. It calls the JS function `mdviewUpdate(html)` to swap the article's `innerHTML`, which preserves scroll position. A full page load happens only on first open or after navigating to another `.md` file.
- Link handling: links to `.md`/`.markdown` files (and similar extensions) open in the same window; every other link goes to `NSWorkspace.open`. Links with `target=_blank` arrive through the `WKUIDelegate` `createWebViewWith` method.
- `FileWatcher.swift`: a `DispatchSource` vnode watcher with a 150 ms debounce. On `.delete`/`.rename` it re-opens the path, because editors like vim and VS Code save by writing a new file and renaming it over the old one. If the file is missing, it retries every 0.5 s.
- `Renderer.swift`: markdown to an HTML fragment via cmark-gfm (`swiftlang/swift-cmark`, products `cmark-gfm` and `cmark-gfm-extensions`).
- Extensions: table, strikethrough, autolink, tagfilter, tasklist.
- Options: `UNSAFE` (raw HTML passes through, as on GitHub; tagfilter still neutralizes script/iframe/style) and `FOOTNOTES`.
- `Template.swift`: the HTML page shell, extra CSS and in-page JS.
- `<base href>` points at the markdown file's directory so relative paths resolve. Because of that, `#anchor` links would navigate away, so a JS click handler intercepts them and calls `scrollIntoView` instead.
- The JS `decorate()` function adds GitHub-style heading ids (cmark doesn't emit them), adds the `task-list-item` classes that github-markdown-css expects, and runs highlight.js only on `code[class*="language-"]`, so there's no auto-detection on unlabeled blocks.
- Swift multi-line string: backslashes in JS regexes must be doubled (`\\s`).
- `Assets.swift`: **generated, do not edit.** It holds the files in `assets/` embedded as `##"""` raw strings. To update or add an asset, change `assets/` and `scripts/gen-assets.sh`, then run `make assets`. The assets are embedded rather than shipped as SwiftPM resources so the binary stays a single file.
## Assets
- `github-markdown.css` (v5.8.1): its light and dark themes switch automatically with `prefers-color-scheme`, which follows the macOS appearance.
- highlight.js 11.11.1 (the common-languages build), with the `github` theme and the `github-dark` theme. The dark theme is wrapped in `@media (prefers-color-scheme: dark)`.
## Conventions / gotchas
- Swift 6 toolchain, but `swiftLanguageModes: [.v5]` in `Package.swift`, which avoids strict-concurrency friction with the DispatchSource and WebKit callbacks. Everything runs on the main queue.
- Minimum macOS version is 13.
- Killing the viewer with a signal skips cleanup and leaves the temp HTML file behind. That's harmless.
- No Mermaid or math support yet. Either would mean adding the JS to `assets/` (about 1 to 3 MB each) and calling it from `decorate()`.
+21
View File
@@ -0,0 +1,21 @@
PREFIX ?= /usr/local
BIN := .build/release/mdview
.PHONY: build install uninstall assets clean
build:
swift build -c release
install: build
install -d $(PREFIX)/bin
install -m 755 $(BIN) $(PREFIX)/bin/mdview
uninstall:
rm -f $(PREFIX)/bin/mdview
# Re-embed assets/*.css and assets/*.js into Sources/mdview/Assets.swift
assets:
scripts/gen-assets.sh
clean:
swift package clean
+15
View File
@@ -0,0 +1,15 @@
{
"originHash" : "0e85d0eda1891a2db813d4ba5c8ec92e62a2753814d8b0a9e636b8dc9696b3d1",
"pins" : [
{
"identity" : "swift-cmark",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-cmark.git",
"state" : {
"revision" : "08ddb528923cc1a6527e02b7a1aee9e516ca749a",
"version" : "0.9.0"
}
}
],
"version" : 3
}
+20
View File
@@ -0,0 +1,20 @@
// swift-tools-version:6.0
import PackageDescription
let package = Package(
name: "mdview",
platforms: [.macOS(.v13)],
dependencies: [
.package(url: "https://github.com/swiftlang/swift-cmark.git", from: "0.8.0"),
],
targets: [
.executableTarget(
name: "mdview",
dependencies: [
.product(name: "cmark-gfm", package: "swift-cmark"),
.product(name: "cmark-gfm-extensions", package: "swift-cmark"),
]
),
],
swiftLanguageModes: [.v5]
)
+79
View File
@@ -0,0 +1,79 @@
import AppKit
final class AppDelegate: NSObject, NSApplicationDelegate {
private let fileURL: URL
private var viewer: ViewerController?
init(fileURL: URL) {
self.fileURL = fileURL
}
func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.mainMenu = buildMainMenu()
let viewer = ViewerController(fileURL: fileURL)
self.viewer = viewer
viewer.window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
true
}
func applicationWillTerminate(_ notification: Notification) {
viewer?.cleanUp()
}
@objc private func reload(_ sender: Any?) {
viewer?.reload()
}
@objc private func zoomIn(_ sender: Any?) { viewer?.zoomIn(sender) }
@objc private func zoomOut(_ sender: Any?) { viewer?.zoomOut(sender) }
@objc private func actualSize(_ sender: Any?) { viewer?.actualSize(sender) }
private func buildMainMenu() -> NSMenu {
let main = NSMenu()
let appMenu = NSMenu()
appMenu.addItem(withTitle: "Hide mdview", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h")
let hideOthers = appMenu.addItem(
withTitle: "Hide Others",
action: #selector(NSApplication.hideOtherApplications(_:)),
keyEquivalent: "h"
)
hideOthers.keyEquivalentModifierMask = [.command, .option]
appMenu.addItem(.separator())
appMenu.addItem(withTitle: "Quit mdview", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
addSubmenu(appMenu, titled: "mdview", to: main)
let fileMenu = NSMenu(title: "File")
fileMenu.addItem(withTitle: "Close", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w")
fileMenu.addItem(withTitle: "Reload", action: #selector(reload(_:)), keyEquivalent: "r").target = self
addSubmenu(fileMenu, titled: "File", to: main)
let editMenu = NSMenu(title: "Edit")
editMenu.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")
editMenu.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
addSubmenu(editMenu, titled: "Edit", to: main)
let viewMenu = NSMenu(title: "View")
viewMenu.addItem(withTitle: "Actual Size", action: #selector(actualSize(_:)), keyEquivalent: "0").target = self
viewMenu.addItem(withTitle: "Zoom In", action: #selector(zoomIn(_:)), keyEquivalent: "=").target = self
viewMenu.addItem(withTitle: "Zoom Out", action: #selector(zoomOut(_:)), keyEquivalent: "-").target = self
addSubmenu(viewMenu, titled: "View", to: main)
let windowMenu = NSMenu(title: "Window")
windowMenu.addItem(withTitle: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent: "m")
addSubmenu(windowMenu, titled: "Window", to: main)
NSApp.windowsMenu = windowMenu
return main
}
private func addSubmenu(_ submenu: NSMenu, titled title: String, to menu: NSMenu) {
let item = NSMenuItem(title: title, action: nil, keyEquivalent: "")
item.submenu = submenu
menu.addItem(item)
}
}
File diff suppressed because one or more lines are too long
+65
View File
@@ -0,0 +1,65 @@
import Foundation
/// Watches a single file and calls `onChange` (debounced, on the main queue)
/// whenever it is modified. Editors that save by writing a new file and
/// renaming it over the old one are handled by re-opening the path.
final class FileWatcher {
private let url: URL
private let onChange: () -> Void
private var source: DispatchSourceFileSystemObject?
private var pending: DispatchWorkItem?
private var stopped = false
init(url: URL, onChange: @escaping () -> Void) {
self.url = url
self.onChange = onChange
}
deinit {
stop()
}
func start() {
guard !stopped, source == nil else { return }
let fd = open(url.path, O_EVTONLY)
if fd < 0 {
// The file may be mid-replacement or temporarily gone; keep trying.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in self?.start() }
return
}
let src = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: fd,
eventMask: [.write, .extend, .delete, .rename, .attrib],
queue: .main
)
src.setEventHandler { [weak self, unowned src] in
guard let self else { return }
let events = src.data
if events.contains(.delete) || events.contains(.rename) {
self.source?.cancel()
self.source = nil
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in self?.start() }
}
self.scheduleChange()
}
src.setCancelHandler { close(fd) }
source = src
src.resume()
}
func stop() {
stopped = true
pending?.cancel()
source?.cancel()
source = nil
}
private func scheduleChange() {
pending?.cancel()
let item = DispatchWorkItem { [weak self] in self?.onChange() }
pending = item
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15, execute: item)
}
}
+42
View File
@@ -0,0 +1,42 @@
import Foundation
import cmark_gfm
import cmark_gfm_extensions
/// Converts GitHub Flavored Markdown to an HTML fragment using cmark-gfm.
enum Renderer {
private static let extensionNames = ["table", "strikethrough", "autolink", "tagfilter", "tasklist"]
// Raw HTML is passed through (as on GitHub); the tagfilter extension
// still neutralizes <script>, <iframe>, <style> and friends.
private static let options = CMARK_OPT_DEFAULT | CMARK_OPT_UNSAFE | CMARK_OPT_FOOTNOTES
static func html(fromMarkdown markdown: String) -> String {
cmark_gfm_core_extensions_ensure_registered()
guard let parser = cmark_parser_new(options) else { return "" }
defer { cmark_parser_free(parser) }
for name in extensionNames {
if let ext = cmark_find_syntax_extension(name) {
cmark_parser_attach_syntax_extension(parser, ext)
}
}
cmark_parser_feed(parser, markdown, markdown.utf8.count)
guard let doc = cmark_parser_finish(parser) else { return "" }
defer { cmark_node_free(doc) }
guard let cString = cmark_render_html(doc, options, cmark_parser_get_syntax_extensions(parser)) else {
return ""
}
defer { free(cString) }
return String(cString: cString)
}
static func escape(_ text: String) -> String {
text.replacingOccurrences(of: "&", with: "&amp;")
.replacingOccurrences(of: "<", with: "&lt;")
.replacingOccurrences(of: ">", with: "&gt;")
.replacingOccurrences(of: "\"", with: "&quot;")
}
}
+89
View File
@@ -0,0 +1,89 @@
import Foundation
/// Wraps rendered markdown in a complete HTML page with styles and scripts.
enum Template {
/// - Parameters:
/// - body: The rendered HTML fragment.
/// - baseURL: Directory of the markdown file, so relative links and images resolve.
static func page(title: String, body: String, baseURL: URL) -> String {
"""
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<base href="\(Renderer.escape(baseURL.absoluteString))">
<title>\(Renderer.escape(title))</title>
<style>\(Assets.markdownCSS)</style>
<style>\(Assets.highlightLightCSS)</style>
<style>@media (prefers-color-scheme: dark) { \(Assets.highlightDarkCSS) }</style>
<style>\(css)</style>
<script>\(Assets.highlightJS)</script>
</head>
<body>
<article id="content" class="markdown-body">\(body)</article>
<script>\(script)</script>
</body>
</html>
"""
}
private static let css = """
:root { color-scheme: light dark; }
html, body { margin: 0; background: #ffffff; }
@media (prefers-color-scheme: dark) { html, body { background: #0d1117; } }
.markdown-body { box-sizing: border-box; max-width: 980px; margin: 0 auto; padding: 32px 45px; }
.markdown-body pre code.hljs { background: transparent; padding: 0; }
.mdview-error { color: #d1242f; }
"""
private static let script = """
(function () {
function slugify(text) {
return text.trim().toLowerCase()
.replace(/[^\\p{L}\\p{N}\\s_-]/gu, '')
.replace(/\\s/g, '-');
}
// Give headings GitHub-style ids and highlight fenced code blocks.
function decorate(root) {
var seen = {};
root.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(function (h) {
var slug = slugify(h.textContent);
if (slug in seen) { seen[slug] += 1; slug += '-' + seen[slug]; } else { seen[slug] = 0; }
if (!h.id) h.id = slug;
});
root.querySelectorAll('li > input[type="checkbox"]:first-child').forEach(function (box) {
box.classList.add('task-list-item-checkbox');
box.parentElement.classList.add('task-list-item');
box.parentElement.parentElement.classList.add('contains-task-list');
});
root.querySelectorAll('pre code[class*="language-"]').forEach(function (el) {
hljs.highlightElement(el);
});
}
// The <base> tag points at the markdown file's directory, so in-page
// anchors would otherwise navigate away. Scroll to them instead.
document.addEventListener('click', function (e) {
var a = e.target.closest('a[href^="#"]');
if (!a) return;
e.preventDefault();
var id = decodeURIComponent(a.getAttribute('href').slice(1));
var target = document.getElementById(id) || document.getElementsByName(id)[0];
if (target) target.scrollIntoView();
});
hljs.configure({ ignoreUnescapedHTML: true });
// Called from Swift on live reload; swapping the content in place
// keeps the scroll position.
window.mdviewUpdate = function (html) {
var content = document.getElementById('content');
content.innerHTML = html;
decorate(content);
};
decorate(document.getElementById('content'));
})();
"""
}
+164
View File
@@ -0,0 +1,164 @@
import AppKit
import WebKit
/// Owns the window and web view for one markdown file and keeps it live-updated.
final class ViewerController: NSObject, WKNavigationDelegate, WKUIDelegate, NSWindowDelegate {
private static let markdownExtensions: Set<String> = ["md", "markdown", "mdown", "mkd", "mkdn"]
let window: NSWindow
private let webView: WKWebView
private let pageURL: URL
private var fileURL: URL
private var watcher: FileWatcher?
private var pageLoaded = false
init(fileURL: URL) {
self.fileURL = fileURL
pageURL = FileManager.default.temporaryDirectory
.appendingPathComponent("mdview-\(getpid()).html")
webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration())
webView.allowsMagnification = true
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 900, height: 1000),
styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered,
defer: false
)
window.contentView = webView
window.center()
window.setFrameAutosaveName("mdview")
window.isReleasedWhenClosed = false
super.init()
webView.navigationDelegate = self
webView.uiDelegate = self
window.delegate = self
open(fileURL)
}
/// Displays `url` in this window and starts watching it for changes.
func open(_ url: URL) {
fileURL = url
window.title = url.lastPathComponent
window.representedURL = url
watcher?.stop()
watcher = FileWatcher(url: url) { [weak self] in self?.reload() }
watcher?.start()
loadFullPage()
}
/// Re-renders the current file. If the page is already showing, only the
/// content is swapped so the scroll position is preserved.
@objc func reload() {
guard pageLoaded else {
loadFullPage()
return
}
webView.callAsyncJavaScript(
"mdviewUpdate(html)",
arguments: ["html": renderBody()],
in: nil,
in: .page,
completionHandler: nil
)
}
func cleanUp() {
watcher?.stop()
try? FileManager.default.removeItem(at: pageURL)
}
private func renderBody() -> String {
do {
let markdown = try String(contentsOf: fileURL, encoding: .utf8)
return Renderer.html(fromMarkdown: markdown)
} catch {
return "<p class=\"mdview-error\">Could not read \(Renderer.escape(fileURL.path)): "
+ "\(Renderer.escape(error.localizedDescription))</p>"
}
}
// The page is written to a temp file and loaded with read access to the
// whole filesystem so that relative images next to the markdown work.
private func loadFullPage() {
pageLoaded = false
let html = Template.page(
title: fileURL.lastPathComponent,
body: renderBody(),
baseURL: fileURL.deletingLastPathComponent()
)
do {
try html.write(to: pageURL, atomically: true, encoding: .utf8)
webView.loadFileURL(pageURL, allowingReadAccessTo: URL(fileURLWithPath: "/"))
} catch {
webView.loadHTMLString(html, baseURL: fileURL.deletingLastPathComponent())
}
}
// MARK: - Zoom
@objc func zoomIn(_ sender: Any?) { webView.pageZoom = min(webView.pageZoom * 1.1, 5) }
@objc func zoomOut(_ sender: Any?) { webView.pageZoom = max(webView.pageZoom / 1.1, 0.25) }
@objc func actualSize(_ sender: Any?) { webView.pageZoom = 1 }
// MARK: - WKNavigationDelegate
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
pageLoaded = true
}
func webView(
_ webView: WKWebView,
decidePolicyFor action: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
guard let url = action.request.url else {
decisionHandler(.cancel)
return
}
// Our own page loads, and anything inside iframes.
if action.navigationType != .linkActivated || action.targetFrame?.isMainFrame == false {
decisionHandler(.allow)
return
}
decisionHandler(.cancel)
followLink(url)
}
// MARK: - WKUIDelegate
// Links with target="_blank" arrive here instead of the navigation delegate.
func webView(
_ webView: WKWebView,
createWebViewWith configuration: WKWebViewConfiguration,
for action: WKNavigationAction,
windowFeatures: WKWindowFeatures
) -> WKWebView? {
if let url = action.request.url {
followLink(url)
}
return nil
}
/// Markdown files open in this window; everything else goes to the default app.
private func followLink(_ url: URL) {
if url.isFileURL && Self.markdownExtensions.contains(url.pathExtension.lowercased()) {
open(url.standardizedFileURL)
} else {
NSWorkspace.shared.open(url)
}
}
// MARK: - NSWindowDelegate
func windowWillClose(_ notification: Notification) {
cleanUp()
}
}
+78
View File
@@ -0,0 +1,78 @@
import AppKit
let usage = """
usage: mdview [-f|--foreground] <file.md>
Opens a window showing the rendered markdown, reloading it whenever the
file changes. Returns to the shell immediately unless --foreground is given.
"""
func fail(_ message: String) -> Never {
FileHandle.standardError.write(Data("mdview: \(message)\n".utf8))
exit(1)
}
/// Re-launches this executable in the foreground mode, in its own session and
/// with stdio pointed at /dev/null, so the shell gets its prompt back.
func detach(path: String) -> Never {
guard let executable = Bundle.main.executablePath else { fail("cannot locate own executable") }
var attr = posix_spawnattr_t(nil as OpaquePointer?)
posix_spawnattr_init(&attr)
defer { posix_spawnattr_destroy(&attr) }
posix_spawnattr_setflags(&attr, Int16(POSIX_SPAWN_SETSID))
var actions = posix_spawn_file_actions_t(nil as OpaquePointer?)
posix_spawn_file_actions_init(&actions)
defer { posix_spawn_file_actions_destroy(&actions) }
posix_spawn_file_actions_addopen(&actions, 0, "/dev/null", O_RDONLY, 0)
posix_spawn_file_actions_addopen(&actions, 1, "/dev/null", O_WRONLY, 0)
posix_spawn_file_actions_addopen(&actions, 2, "/dev/null", O_WRONLY, 0)
let args = [executable, "--foreground", path]
var cArgs = args.map { strdup($0) } + [nil]
defer { cArgs.forEach { free($0) } }
var pid: pid_t = 0
let status = posix_spawn(&pid, executable, &actions, &attr, &cArgs, environ)
if status != 0 {
fail("could not start viewer: \(String(cString: strerror(status)))")
}
exit(0)
}
var foreground = false
var paths: [String] = []
for arg in CommandLine.arguments.dropFirst() {
switch arg {
case "-f", "--foreground":
foreground = true
case "-h", "--help":
print(usage)
exit(0)
case _ where arg.hasPrefix("-") && arg != "-":
fail("unknown option \(arg)\n\n\(usage)")
default:
paths.append(arg)
}
}
guard paths.count == 1 else { fail("expected exactly one file\n\n\(usage)") }
let fileURL = URL(fileURLWithPath: paths[0]).standardizedFileURL
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else {
fail("\(paths[0]): no such file")
}
guard !isDirectory.boolValue else { fail("\(paths[0]): is a directory") }
guard FileManager.default.isReadableFile(atPath: fileURL.path) else { fail("\(paths[0]): permission denied") }
if !foreground {
detach(path: fileURL.path)
}
let app = NSApplication.shared
let delegate = AppDelegate(fileURL: fileURL)
app.delegate = delegate
app.setActivationPolicy(.regular)
app.run()
File diff suppressed because it is too large Load Diff
+1244
View File
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: GitHub Dark
Description: Dark theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Outdated base version: https://github.com/primer/github-syntax-dark
Current colors taken from GitHub's CSS
*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}
+10
View File
@@ -0,0 +1,10 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: GitHub
Description: Light theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Outdated base version: https://github.com/primer/github-syntax-light
Current colors taken from GitHub's CSS
*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}
+32
View File
@@ -0,0 +1,32 @@
#!/bin/sh
# Regenerates Sources/mdview/Assets.swift from the files in assets/, embedding
# them as Swift raw string literals so the binary is fully self-contained.
set -eu
cd "$(dirname "$0")/.."
out=Sources/mdview/Assets.swift
emit() {
name=$1
file=$2
if grep -q '"""##' "$file"; then
echo "error: $file contains the raw string delimiter" >&2
exit 1
fi
printf ' static let %s = ##"""\n' "$name"
cat "$file"
printf '\n"""##\n\n'
}
{
echo '// Generated by scripts/gen-assets.sh -- do not edit by hand.'
echo
echo 'enum Assets {'
emit markdownCSS assets/github-markdown.css
emit highlightLightCSS assets/hljs-github.css
emit highlightDarkCSS assets/hljs-github-dark.css
emit highlightJS assets/highlight.min.js
echo '}'
} > "$out"
echo "wrote $out"