Demark is a Swift package for converting HTML strings and rendered web pages to Markdown on Apple platforms. It uses Turndown.js in a WKWebView by default and includes an html-to-md engine backed by JavaScriptCore for simpler input.
In Xcode, choose File → Add Package Dependencies and enter:
https://github.com/steipete/Demark.git
For a Package.swift manifest, add the package and product dependency:
dependencies: [
.package(url: "https://github.com/steipete/Demark.git", from: "1.1.0"),
]
.target(
name: "YourTarget",
dependencies: [.product(name: "Demark", package: "Demark")]
)Demark requires Swift 6 and has no external Swift Package dependencies.
Call Demark from an async main-actor context:
import Demark
@MainActor
func convert(_ html: String) async throws -> String {
try await Demark().convertToMarkdown(html)
}
print(try await convert("<h1>Hello</h1><p>This is <strong>bold</strong>.</p>"))Output:
# Hello
This is **bold**.Demark exposes two bundled conversion engines:
| Engine | Runtime | Use it for |
|---|---|---|
.turndown (default) |
WebKit and a browser DOM | Complex or malformed HTML and configurable heading/code styles |
.htmlToMd |
JavaScriptCore on an internal serial queue | Simple, well-formed HTML without a web view |
The public Demark API is main-actor isolated. If Turndown cannot complete a conversion, the default path retries the input with html-to-md. Both engines support custom bullet markers, skipped tags, and ignored tags; some formatting options are engine-specific.
See Using Demark for the complete option table, engine behavior, and error model.
Demark can load an HTTP or HTTPS URL in an ephemeral WKWebView, wait for the rendered document, and convert either the whole page or a selected element:
import Demark
import Foundation
@MainActor
func convertArticle(at url: URL) async throws -> String {
try await Demark().convertToMarkdown(
url: url,
loadingOptions: URLLoadingOptions(contentSelector: "article")
)
}URL conversion is available in Demark 1.2.0 and later. It requires WebKit and network access. Loading options cover timeouts, an additional idle delay, CSS selection, and a custom user agent; the usage guide has the details.
The package manifest declares these deployment targets:
| Platform | Minimum | Conversion availability |
|---|---|---|
| macOS | 14 | WebKit and JavaScriptCore engines |
| iOS | 16 | WebKit and JavaScriptCore engines |
| visionOS | 1 | WebKit and JavaScriptCore engines |
| watchOS | 10 | Package compiles; an unavailable engine throws DemarkError.runtimeUnavailable |
| tvOS | 17 | Package compiles; an unavailable engine throws DemarkError.runtimeUnavailable |
The example app provides a native macOS/iOS interface for comparing both engines and their output.
Issues, feature requests, and pull requests are welcome. Keep changes focused and run the development checks before submitting a pull request.
Build and test the package:
swift build
swift testThe default tests do not access the network. URL-loading integration tests are opt-in with DEMARK_LIVE_TESTS=1 swift test --filter DemarkURLLoading. CI also checks formatting and linting and builds the example app; the release guide lists the full matrix.
Demark bundles Turndown.js by Dom Christie and html-to-md. Thanks also to the Swift community and WebKit team for the language and platform runtimes the package builds on.
Demark is available under the MIT License. Copyright Peter Steinberger.
