import Foundation protocol Template { associatedtype Key where Key: RawRepresentable, Key.RawValue == String, Key: CaseIterable, Key: Hashable static var templateName: String { get } var raw: String { get } var results: GenerationResultsHandler { get } init(raw: String, results: GenerationResultsHandler) } extension Template { init(in folder: URL, results: GenerationResultsHandler) throws { let url = folder.appendingPathComponent(Self.templateName) try self.init(from: url, results: results) } init(from url: URL, results: GenerationResultsHandler) throws { let raw = try String(contentsOf: url) self.init(raw: raw, results: results) } @discardableResult func generate(_ content: [Key : String], to file: String, source: String) -> Bool { let content = generate(content).data(using: .utf8)! return results.writeIfChanged(content, file: file, source: source) } func generate(_ content: [Key : String], shouldIndent: Bool = false) -> String { var result = raw.components(separatedBy: "\n") Key.allCases.forEach { key in let newContent = content[key]?.withoutEmptyLines ?? "" let stringMarker = "" let indices = result.enumerated().filter { $0.element.contains(stringMarker) } .map { $0.offset } guard !indices.isEmpty else { return } for index in indices { let old = result[index].components(separatedBy: stringMarker) // Add indentation to all added lines let indentation = old.first! guard shouldIndent, indentation.trimmingCharacters(in: .whitespaces).isEmpty else { // Prefix is not indentation, so just insert new content result[index] = old.joined(separator: newContent) continue } let indentedReplacements = newContent.indented(by: indentation) result[index] = old.joined(separator: indentedReplacements) } } return result.joined(separator: "\n").withoutEmptyLines } }