import Foundation /// Modifies the header section of a `social.org` file. /// /// All methods are pure string transformations — they return an updated copy /// of the feed content without performing any network operations. /// /// ```swift /// var content = try await FeedFetcher().fetch(from: myFeedURL) /// content = ProfileWriter().addFollow(url: friendURL, nick: "friend", to: content) /// try await VFileUploader(tokenURL: tokenURL).upload(content: content) /// ``` public struct ProfileWriter: Sendable { public init() {} // MARK: - Single-value keyword /// Sets or replaces a single-value header keyword (e.g. `NICK`, `DESCRIPTION`, `AVATAR`). /// /// - If the keyword already exists, its line is replaced in-place. /// - If it does not exist, a new line is inserted before `* Posts`. public func setKeyword(_ keyword: String, value: String, in content: String) -> String { var lines = content.components(separatedBy: "\n") let kwUpper = keyword.uppercased() let prefix = "#+\(kwUpper):" // Replace existing line var found = false for i in lines.indices { if lines[i].trimmingCharacters(in: .whitespaces).uppercased().hasPrefix(prefix) { lines[i] = "#+\(kwUpper): \(value)" found = true break } } if found { return lines.joined(separator: "\n") } // Insert before * Posts if let postsIdx = postsHeadingIndex(in: lines) { lines.insert("#+\(kwUpper): \(value)", at: postsIdx) return lines.joined(separator: "\n") } return content + "\n#+\(kwUpper): \(value)" } /// Removes a single-value header keyword line if present. public func removeKeyword(_ keyword: String, from content: String) -> String { let kwUpper = keyword.uppercased() let prefix = "#+\(kwUpper):" return content.components(separatedBy: "\n") .filter { !$0.trimmingCharacters(in: .whitespaces).uppercased().hasPrefix(prefix) } .joined(separator: "\n") } // MARK: - Multi-value keywords (LINK, CONTACT) /// Replaces all occurrences of a repeatable keyword with a new set of values. /// Pass an empty array to remove all lines for that keyword. public func setMultiKeyword(_ keyword: String, values: [String], in content: String) -> String { let kwUpper = keyword.uppercased() let prefix = "#+\(kwUpper):" var lines = content.components(separatedBy: "\n") // Remove all existing lines for this keyword let firstIdx = lines.firstIndex { $0.trimmingCharacters(in: .whitespaces).uppercased().hasPrefix(prefix) } lines.removeAll { $0.trimmingCharacters(in: .whitespaces).uppercased().hasPrefix(prefix) } let newLines = values.map { "#+\(kwUpper): \($0)" } if newLines.isEmpty { return lines.joined(separator: "\n") } let insertIdx = firstIdx ?? postsHeadingIndex(in: lines) ?? lines.count lines.insert(contentsOf: newLines, at: min(insertIdx, lines.count)) return lines.joined(separator: "\n") } // MARK: - Follow management /// Adds a `#+FOLLOW:` entry for `url` with an optional nick. /// /// The new entry is inserted after the last existing `#+FOLLOW:` line, /// or before `* Posts` if none exist. Does nothing if already following. @discardableResult public func addFollow(url: URL, nick: String? = nil, to content: String) -> String { guard !isFollowing(url: url, in: content) else { return content } let entry: String if let nick, !nick.isEmpty { entry = "#+FOLLOW: \(nick) \(url.absoluteString)" } else { entry = "#+FOLLOW: \(url.absoluteString)" } var lines = content.components(separatedBy: "\n") if let lastFollowIdx = lines.lastIndex(where: { $0.trimmingCharacters(in: .whitespaces).uppercased().hasPrefix("#+FOLLOW:") }) { lines.insert(entry, at: lastFollowIdx + 1) return lines.joined(separator: "\n") } if let postsIdx = postsHeadingIndex(in: lines) { lines.insert(entry, at: postsIdx) return lines.joined(separator: "\n") } return content + "\n" + entry } /// Removes all `#+FOLLOW:` lines containing `url`. @discardableResult public func removeFollow(url: URL, from content: String) -> String { let urlStr = url.absoluteString return content.components(separatedBy: "\n") .filter { line in let t = line.trimmingCharacters(in: .whitespaces) guard t.uppercased().hasPrefix("#+FOLLOW:") else { return true } return !t.contains(urlStr) } .joined(separator: "\n") } /// Returns `true` if the content has a `#+FOLLOW:` line containing `url`. public func isFollowing(url: URL, in content: String) -> Bool { let urlStr = url.absoluteString return content.components(separatedBy: "\n").contains { line in let t = line.trimmingCharacters(in: .whitespaces) return t.uppercased().hasPrefix("#+FOLLOW:") && t.contains(urlStr) } } // MARK: - Helpers private func postsHeadingIndex(in lines: [String]) -> Int? { lines.firstIndex { $0.trimmingCharacters(in: .whitespaces) == "* Posts" } } }