A pure Swift package for importing and exporting OPML (Outline Processor Markup Language) documents. Built with strict Swift 6 concurrency, zero third-party dependencies.
- iOS 18+ / macOS 15+ / tvOS 18+ / watchOS 11+ / visionOS 2+
- Swift 6.2+
- No external dependencies (Foundation and XMLParser only)
Add as a local package dependency in Xcode, or reference it in your Package.swift:
.package(url: "https://github.com/Haven-Apps/HavenOPML")import HavenOPML
let document = try OPMLImporter.importOPML(from: data)
print(document.title) // "My Subscriptions"
print(document.format) // .v1_0 or .v2_0
for outline in document.outlines {
print(outline.text, outline.xmlUrl)
}let document = try OPMLImporter.importOPML(from: fileURL)let data = try OPMLExporter.exportOPML(document)let data = try OPMLExporter.exportOPML(
outlines: myOutlines,
title: "My Subscriptions"
)OPMLService is an actor that wraps the importer and exporter for use in concurrent contexts:
let service = OPMLService()
let document = try await service.importOPML(from: data)
let exported = try await service.exportOPML(document)
let isValid = try await service.validateRoundTrip(document)// All feed outlines (depth-first)
let feeds = document.allFeeds
// Total feed count
let count = document.feedCount
// Top-level folders only
let folders = document.folders// Depth-first traversal with depth tracking
OutlineTraversal.depthFirst(document.outlines) { outline, depth in
let indent = String(repeating: " ", count: depth)
print("\(indent)\(outline.displayName)")
}
// Flatten all outlines
let all = OutlineTraversal.flatten(document.outlines)Sources/HavenOPML/
├── Models/ Sendable, Codable value types (OPMLDocument, OutlineItem, OPMLCategory, OPMLFormat)
├── Parsers/ OPMLImporter (static API), OPMLParserDelegate (SAX-style XMLParser)
├── Exporters/ OPMLExporter (static API, generates indented OPML/XML)
├── Services/ Actor-isolated OPMLService (unified import/export/validation API)
├── Utilities/ OPMLDateFormatter, OutlineTraversal, XMLEscaping
└── Errors/ OPMLError enum
| Type | Description |
|---|---|
OPMLService |
Actor-isolated public API — imports, exports, and validates OPML documents |
OPMLDocument |
Parsed document with title, dates, owner metadata, and outline tree |
OutlineItem |
Single outline element — feed entry or folder with nested children |
OPMLCategory |
Category value parsed from comma-separated OPML category strings |
OPMLFormat |
Enum: .v1_0, .v2_0 |
OPMLError |
Typed errors for parsing, export, and I/O failures |
OPMLImporter |
Stateless OPML parser using Foundation's XMLParser |
OPMLExporter |
Stateless XML generator with proper character escaping |
OPMLDateFormatter |
Parses and formats RFC 822 and ISO 8601 dates |
OutlineTraversal |
Depth-first traversal, flattening, and counting utilities |
XMLEscaping |
Escapes the five predefined XML entities for safe export |
OutlineItem supports both feed entries and folder/grouping outlines. Folders are identified by having non-empty children and no xmlUrl. Custom or namespace-specific attributes are preserved in customAttributes for round-trip fidelity.
outline.isFolder // true if it has children and no xmlUrl
outline.isFeed // true if xmlUrl is present
outline.displayName // prefers title over textThe importer auto-detects the OPML version from the version attribute on the <opml> element. Unknown versions are treated as 2.0 for forward compatibility.
- External XML entity resolution is disabled (
shouldResolveExternalEntities = false) to prevent XXE attacks. - Input size is capped at 10 MB to mitigate denial-of-service via large payloads.
- Outline nesting depth is limited to 128 levels on both import and export.
- Only file URLs are accepted by the URL-based import method; remote URLs are rejected.
OPMLImporter and OPMLExporter are stateless enums with static methods, safe to call from any context. OPMLService is an actor that provides a convenient async API for use in concurrent code.
BSD 3-Clause — see LICENSE.md.