Contents

haven-apps/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.

Package Metadata

Repository: haven-apps/havenopml

Default branch: main

README: README.md