Skip to content

Haven-Apps/HavenOPML

HavenOPML

A pure Swift package for importing and exporting OPML (Outline Processor Markup Language) documents. Built with strict Swift 6 concurrency, zero third-party dependencies.

Requirements

  • iOS 18+ / macOS 15+ / tvOS 18+ / watchOS 11+ / visionOS 2+
  • Swift 6.2+
  • No external dependencies (Foundation and XMLParser only)

Installation

Add as a local package dependency in Xcode, or reference it in your Package.swift:

.package(url: "https://github.com/Haven-Apps/HavenOPML")

Usage

Import from Data

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)
}

Import from File URL

let document = try OPMLImporter.importOPML(from: fileURL)

Export to Data

let data = try OPMLExporter.exportOPML(document)

Quick Export from Outlines

let data = try OPMLExporter.exportOPML(
    outlines: myOutlines,
    title: "My Subscriptions"
)

Using OPMLService

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)

Working with the 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

Outline Traversal

// 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)

Architecture

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

Key Types

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

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 text

Version Detection

The importer auto-detects the OPML version from the version attribute on the <opml> element. Unknown versions are treated as 2.0 for forward compatibility.

Security

  • 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.

Concurrency

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.

License

BSD 3-Clause — see LICENSE.md.

About

A pure Swift package for importing and exporting OPML (Outline Processor Markup Language) documents.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Sponsor this project

Packages

 
 
 

Contributors

Languages