Files

132 lines
5.2 KiB
Swift

//
// SwiftGenKit
// Copyright © 2022 SwiftGen
// MIT Licence
//
import Foundation
import PathKit
public enum Strings {
public enum ParserError: Error, CustomStringConvertible {
case duplicateTableFile(path: Path, existing: Path)
case failureOnLoading(path: Path)
case invalidFormat(reason: String)
case invalidPlaceholder(previous: Strings.PlaceholderType, new: Strings.PlaceholderType)
case invalidPluralFormat(missingVariableKey: String, pluralKey: String)
case invalidVariableRuleValueType(variableName: String, valueType: String)
case unsupportedFileType(path: Path, supported: [String])
public var description: String {
switch self {
case .duplicateTableFile(let path, let existing):
return """
Duplicate file of the same type for table "\(path.lastComponentWithoutExtension)":
Trying to load \(path) while we've already loaded \(existing)
"""
case .failureOnLoading(let path):
return "Failed to load a file at \"\(path)\""
case .invalidFormat(let reason):
return "Invalid format. \(reason)"
case .invalidPlaceholder(let previous, let new):
return "Invalid placeholder type \(new) (previous: \(previous))"
case .invalidPluralFormat(let missingVariableKey, let pluralKey):
return """
The variable \"\(missingVariableKey)\" referenced in the NSStringLocalizedFormatKey is not \
defined in the variables of the plural key \"\(pluralKey)\"
"""
case .invalidVariableRuleValueType(let variableName, let valueType):
return """
The variable \"\(variableName)\" has specified \"\(valueType)\" as its NSStringFormatValueTypeKey. \
The Stringsdict file format only supports format specifiers for a number.
"""
case .unsupportedFileType(let path, let supported):
return """
Unsupported file type for \(path). \
The supported file types are: \(supported.joined(separator: ", "))
"""
}
}
}
public enum Option {
static let separator = ParserOption(
key: "separator",
defaultValue: ".",
help: "Separator used to split keys into components"
)
}
public final class Parser: SwiftGenKit.Parser {
private typealias File = (path: Path, entries: [Entry])
private let options: ParserOptionValues
private var tableFiles = [String: [File]]()
public var warningHandler: Parser.MessageHandler?
/// This list must be ordered by priority, higher priority first
private static let subParsers: [StringsFileTypeParser.Type] = [
StringsDictFileParser.self,
StringsFileParser.self
]
public init(options: [String: Any] = [:], warningHandler: Parser.MessageHandler? = nil) throws {
self.options = try ParserOptionValues(options: options, available: Self.allOptions)
self.warningHandler = warningHandler
}
public static let allOptions: ParserOptionList = [Option.separator]
public static var defaultFilter: String {
let extensions = Self.subParsers.flatMap { $0.extensions }.sorted()
return filterRegex(forExtensions: extensions)
}
public func parse(path: Path, relativeTo parent: Path) throws {
guard let parserType = Self.subParsers.first(where: { $0.supports(path: path) }) else {
throw ParserError.unsupportedFileType(path: path, supported: Self.subParsers.flatMap { $0.extensions })
}
let name = path.lastComponentWithoutExtension
let parser = parserType.init(options: options)
var files = tableFiles[name] ?? []
let previouslyParsedFile = files.first { file in
file.path.lastComponent.compare(path.lastComponent, options: .caseInsensitive) == .orderedSame
}
if let existing = previouslyParsedFile {
// 2nd time we try to parse a file with that name, e.g. same filename from 2 different .lprojs
throw ParserError.duplicateTableFile(path: path, existing: existing.path)
} else {
let entries = try parser.parseFile(at: path)
files.append((path: path, entries: entries))
}
tableFiles[name] = files
}
var tables: [String: [Entry]] {
tableFiles.mapValues(collectEntries(in:))
}
/// Collect all entries spread over multiple files for a table into one collection. Entries from parsers with
/// lower index in the subParsers array (i.e. closer to the start) will be chosen over other entries
/// (from parsers listed later in the the array) with the same key.
private func collectEntries(in files: [File]) -> [Entry] {
// sort files by parser priority (higher priority first)
let files = files.sorted { lhs, rhs in
let lhsPriority = Self.subParsers.firstIndex { $0.supports(path: lhs.path) } ?? 0
let rhsPriority = Self.subParsers.firstIndex { $0.supports(path: rhs.path) } ?? 0
return lhsPriority < rhsPriority
}
// reduce lists of entries into one list, ignoring duplicate keys of later lists
let entries = files.reduce(into: [String: Entry]()) { result, file in
for entry in file.entries where result[entry.key] == nil {
result[entry.key] = entry
}
}
return Array(entries.values)
}
}
}