/// Reads an Atom feed (spec 6.2) into the same shape dated links produce, so a /// site's posts come from one store however they were published. Pure data: no /// Flutter imports, so it can run inside the fetch actor's isolate. library; import 'package:xml/xml.dart'; import 'document.dart'; import 'parser.dart' show MewsParseException; /// The entries of an Atom feed, newest first. Entries without a usable address /// or date are skipped rather than guessed at. /// /// Element names are matched without their namespace, so a feed using the /// default Atom namespace and one using an `atom:` prefix both read the same. List parseAtomEntries({ required String source, required String feedUrl, }) { final XmlDocument doc; try { doc = XmlDocument.parse(source); } on XmlException catch (e) { throw MewsParseException(e.toString()); } final base = Uri.tryParse(feedUrl); final entries = []; for (final element in doc.rootElement.children.whereType()) { if (element.name.local != 'entry') continue; final url = _entryUrl(element, base); final date = _entryDate(element); if (url == null || date == null) continue; entries.add(MewsEntry( url: url, date: date, title: _childText(element, 'title') ?? url, )); } entries.sort((a, b) => b.date.compareTo(a.date)); return entries; } /// An entry's address: its alternate link, or its id when that is a web /// address. The id is a fallback because many feeds use the post's URL for it. String? _entryUrl(XmlElement entry, Uri? base) { for (final child in entry.children.whereType()) { if (child.name.local != 'link') continue; final rel = child.getAttribute('rel'); if (rel != null && rel != 'alternate') continue; final href = child.getAttribute('href'); if (href == null || href.isEmpty) continue; final resolved = base?.resolve(href) ?? Uri.tryParse(href); if (resolved != null) return resolved.toString(); } final id = _childText(entry, 'id'); if (id == null) return null; final asUri = Uri.tryParse(id); return asUri != null && (asUri.scheme == 'http' || asUri.scheme == 'https') ? asUri.toString() : null; } /// When an entry was published. `updated` is required by Atom, so it comes /// first, with `published` as the fallback. DateTime? _entryDate(XmlElement entry) { for (final name in const ['updated', 'published']) { final text = _childText(entry, name); if (text == null) continue; final date = DateTime.tryParse(text); if (date != null) return date; } return null; } String? _childText(XmlElement parent, String name) { for (final child in parent.children.whereType()) { if (child.name.local != name) continue; final text = child.innerText.trim(); if (text.isNotEmpty) return text; } return null; }