enum ContentBlock: String, CaseIterable { case audio case swift var processor: BlockProcessor.Type { switch self { case .audio: return AudioBlockProcessor.self case .swift: return SwiftBlockProcessor.self } } } protocol BlockProcessor { static var blockId: ContentBlock { get } var results: PageGenerationResults { get } init(content: Content, results: PageGenerationResults, language: ContentLanguage) func process(_ markdown: Substring) -> String } extension BlockProcessor { func invalid(_ markdown: Substring) { results.invalid(block: Self.blockId, markdown) } } protocol BlockLineProcessor: BlockProcessor { func process(_ lines: [String], markdown: Substring) -> String } extension BlockLineProcessor { func process(_ markdown: Substring) -> String { let lines = markdown .between("```\(Self.blockId.self)", and: "```") .components(separatedBy: "\n") return process(lines, markdown: markdown) } } protocol OrderedKeyBlockProcessor: BlockLineProcessor { associatedtype Key: Hashable, RawRepresentable where Key.RawValue == String func process(_ arguments: [(key: Key, value: String)], markdown: Substring) -> String } extension OrderedKeyBlockProcessor { func process(_ lines: [String], markdown: Substring) -> String { let result: [(key: Key, value: String)] = lines.compactMap { line in guard line.trimmed != "" else { return nil } let (rawKey, rawValue) = line.splitAtFirst(":") guard let key = Key(rawValue: rawKey.trimmed) else { print("Invalid key \(rawKey)") invalid(markdown) return nil } return (key, rawValue.trimmed) } return process(result, markdown: markdown) } } protocol KeyedBlockProcessor: OrderedKeyBlockProcessor { func process(_ arguments: [Key : String], markdown: Substring) -> String } extension KeyedBlockProcessor { func process(_ arguments: [(key: Key, value: String)], markdown: Substring) -> String { let result = arguments.reduce(into: [:]) { $0[$1.key] = $1.value } return process(result, markdown: markdown) } }