diff --git a/Sources/CLFormat/Arguments.swift b/Sources/CLFormat/Arguments.swift index 17facff..9e7453e 100644 --- a/Sources/CLFormat/Arguments.swift +++ b/Sources/CLFormat/Arguments.swift @@ -26,7 +26,7 @@ import Foundation /// the control string. `Argument` objects cannot be reused. They can only be used /// once to format a string. /// -public class Arguments: CustomStringConvertible { +open class Arguments: CustomStringConvertible { /// The locale to be used (for locale-specific directives; typically enabled /// via the `+` modifier. @@ -53,11 +53,11 @@ public class Arguments: CustomStringConvertible { private var firstArg: Int /// Constructor. - public init(locale: Locale? = nil, - tabsize: Int = 8, - linewidth: Int = 72, - args: [Any?], - numArgumentsLeft: Int? = nil) { + public required init(locale: Locale? = nil, + tabsize: Int = 8, + linewidth: Int = 80, + args: [Any?], + numArgumentsLeft: Int? = nil) { self.locale = locale self.tabsize = tabsize self.linewidth = linewidth @@ -105,7 +105,7 @@ public class Arguments: CustomStringConvertible { /// Returns the next argument to consume; this is, in fact, a lookahead as /// the next argument is not being consumed by `current`. - public func current() throws -> Any? { + open func current() throws -> Any? { if self.index < self.args.count { return self.args[self.index] } else { @@ -114,7 +114,7 @@ public class Arguments: CustomStringConvertible { } /// Returns and consumes the next argument. - public func next() throws -> Any? { + open func next() throws -> Any? { if self.index < self.args.count { let arg = self.args[self.index] self.index += 1 @@ -128,7 +128,7 @@ public class Arguments: CustomStringConvertible { /// exception if the next argument is not a number. public func nextAsNumber() throws -> Number { if let arg = try self.next() { - if let num = Number(arg) { + if let num = self.coerceToNumber(arg) { return num } else { throw CLFormatError.expectedNumberArgument(self.index - 1, arg) @@ -142,13 +142,7 @@ public class Arguments: CustomStringConvertible { /// exception if the next argument cannot be exactly represented as an `Int`. public func nextAsInt() throws -> Int { if let arg = try self.next() { - if let num = arg as? Int { - return num - } else if let n = arg as? Int64, let num = Int(exactly: n) { - return num - } else if let n = arg as? UInt, let num = Int(exactly: n) { - return num - } else if let n = arg as? UInt64, let num = Int(exactly: n) { + if let num = self.coerceToInt(arg) { return num } else { throw CLFormatError.expectedNumberArgument(self.index - 1, arg) @@ -162,7 +156,7 @@ public class Arguments: CustomStringConvertible { /// exception if the next argument is not a character. public func nextAsCharacter() throws -> Character { if let arg = try self.next() { - if let ch = arg as? Character { + if let ch = self.coerceToCharacter(arg) { return ch } else if let str = arg as? String, str.count == 1 { return str.first! @@ -178,12 +172,8 @@ public class Arguments: CustomStringConvertible { /// exception if the next argument is not a string. public func nextAsString() throws -> String { if let arg = try self.next() { - if let str = arg as? String { + if let str = self.coerceToString(arg) { return str - } else if let str = arg as? NSString { - return str as String - } else if let str = arg as? NSMutableString { - return str as String } else { throw CLFormatError.expectedNumberArgument(self.index - 1, arg) } @@ -195,19 +185,10 @@ public class Arguments: CustomStringConvertible { /// Returns and consumes the next argument as an `Arguments` object. The function throws /// an exception if the next argument is not a sequence (which is being converted into an /// `Arguments` object). - public func nextAsArguments(maxArgs: Int = Int.max) throws -> Arguments { + public func nextAsArguments(maxArgs: Int = Int.max) throws -> Self { if let arg = try self.next() { - if let seq = arg as? any Sequence { - var newargs = [Any?]() - var itercap = maxArgs - var iterator = seq.makeIterator() as (any IteratorProtocol) - while itercap > 0, let next = iterator.next() { - newargs.append(unwrapAny(next)) - itercap -= 1 - } - return Arguments(locale: self.locale, - tabsize: self.tabsize, - args: newargs) + if let arr = self.coerceToArray(arg, capAt: maxArgs) { + return Self(locale: self.locale, tabsize: self.tabsize, args: arr) } else { throw CLFormatError.expectedSequenceArgument(self.index - 1, arg) } @@ -220,18 +201,8 @@ public class Arguments: CustomStringConvertible { /// exception if the next argument cannot be represented as a parameter. public func nextAsParameter() throws -> Parameter { if let arg = try self.next() { - if let num = arg as? Int { - return .number(num) - } else if let n = arg as? Int64, let num = Int(exactly: n) { - return .number(num) - } else if let n = arg as? UInt, let num = Int(exactly: n) { - return .number(num) - } else if let n = arg as? UInt64, let num = Int(exactly: n) { - return .number(num) - } else if let char = arg as? Character { - return .character(char) - } else if let str = arg as? String, str.count < 2 { - return str.count == 0 ? .none : .character(str.first!) + if let param = self.coerceToParameter(arg) { + return param } else { throw CLFormatError.cannotUseArgumentAsParameter(self.index - 1, arg) } @@ -263,4 +234,77 @@ public class Arguments: CustomStringConvertible { } return res + "]>" } + + /// Coerce object to a Number value, if possible. + open func coerceToNumber(_ obj: Any) -> Number? { + return Number(obj) + } + + /// Coerce object to an integer, if possible. + open func coerceToInt(_ obj: Any) -> Int? { + if let num = obj as? Int { + return num + } else if let n = obj as? Int64, let num = Int(exactly: n) { + return num + } else if let n = obj as? UInt, let num = Int(exactly: n) { + return num + } else if let n = obj as? UInt64, let num = Int(exactly: n) { + return num + } else { + return nil + } + } + + /// Coerce object to a character, if possible. + open func coerceToCharacter(_ obj: Any) -> Character? { + if let ch = obj as? Character { + return ch + } else if let str = obj as? String, str.count == 1 { + return str.first! + } else { + return nil + } + } + + /// Coerce object to a string, if possible. + open func coerceToString(_ obj: Any) -> String? { + if let str = obj as? String { + return str + } else if let str = obj as? NSString { + return str as String + } else if let str = obj as? NSMutableString { + return str as String + } else { + return nil + } + } + + /// Coerce object to an array, if possible. + open func coerceToArray(_ obj: Any, capAt: Int) -> [Any?]? { + if let seq = obj as? any Sequence { + var itercap = capAt + var newargs = [Any?]() + var iterator = seq.makeIterator() as (any IteratorProtocol) + while itercap > 0, let next = iterator.next() { + newargs.append(unwrapAny(next)) + itercap -= 1 + } + return newargs + } else { + return nil + } + } + + /// Coerce object to a parameter, if possible. + open func coerceToParameter(_ obj: Any) -> Parameter? { + if let num = self.coerceToInt(obj) { + return .number(num) + } else if let char = self.coerceToCharacter(obj) { + return .character(char) + } else if let str = self.coerceToString(obj), str.count < 2 { + return str.count == 0 ? Parameter.none : Parameter.character(str.first!) + } else { + return nil + } + } } diff --git a/Sources/CLFormat/CLControl.swift b/Sources/CLFormat/CLControl.swift index fe8f196..073d595 100644 --- a/Sources/CLFormat/CLControl.swift +++ b/Sources/CLFormat/CLControl.swift @@ -78,10 +78,10 @@ public struct CLControl: CustomStringConvertible { tabsize: Int = 4, linewidth: Int = 80, args: Any?...) throws -> String { - return try self.format(with: Arguments(locale: locale, - tabsize: tabsize, - linewidth: linewidth, - args: args), + return try self.format(with: self.config.makeArguments(locale: locale, + tabsize: tabsize, + linewidth: linewidth, + args: args), in: .root(self.config)).string } @@ -91,10 +91,10 @@ public struct CLControl: CustomStringConvertible { tabsize: Int = 4, linewidth: Int = 80, arguments: [Any?]) throws -> String { - return try self.format(with: Arguments(locale: locale, - tabsize: tabsize, - linewidth: linewidth, - args: arguments), + return try self.format(with: self.config.makeArguments(locale: locale, + tabsize: tabsize, + linewidth: linewidth, + args: arguments), in: .root(self.config)).string } diff --git a/Sources/CLFormat/CLControlParserConfig.swift b/Sources/CLFormat/CLControlParserConfig.swift index 990d420..159fa53 100644 --- a/Sources/CLFormat/CLControlParserConfig.swift +++ b/Sources/CLFormat/CLControlParserConfig.swift @@ -29,7 +29,23 @@ import Foundation /// logic. /// public struct CLControlParserConfig { - private var directiveParsers: [Character : DirectiveParser] = [:] + private let makeArguments: (Locale?, Int, Int, [Any?], Int?) -> Arguments + private var directiveParsers: [Character : DirectiveParser] + + public init(makeArguments: @escaping (Locale?, Int, Int, [Any?], Int?) -> Arguments = + Arguments.init) { + self.makeArguments = makeArguments + self.directiveParsers = [:] + } + + /// Return a new arguments object. + public func makeArguments(locale: Locale? = nil, + tabsize: Int = 8, + linewidth: Int = 80, + args: [Any?], + numArgumentsLeft: Int? = nil) -> Arguments { + return self.makeArguments(locale, tabsize, linewidth, args, numArgumentsLeft) + } /// Add a new directive parser to this control parser configuration for the given /// array of characters. diff --git a/Sources/CLFormat/StandardDirectiveSpecifier.swift b/Sources/CLFormat/StandardDirectiveSpecifier.swift index 92639e1..904b46f 100644 --- a/Sources/CLFormat/StandardDirectiveSpecifier.swift +++ b/Sources/CLFormat/StandardDirectiveSpecifier.swift @@ -632,19 +632,14 @@ public enum StandardDirectiveSpecifier: DirectiveSpecifier { force = false itercap -= 1 let arg = try iterargs.next() - var subitercap = try parameters.number(1) ?? Int.max - if let seq = arg as? any Sequence { - var newargs = [Any?]() - var iterator = seq.makeIterator() as (any IteratorProtocol) - while subitercap > 0, let next = iterator.next() { - newargs.append(next) - subitercap -= 1 - } - let formatted = try control.format(with: Arguments(locale: arguments.locale, - tabsize: arguments.tabsize, - args: newargs, - numArgumentsLeft: iterargs.left), - in: .frame(res, context)) + let subitercap = try parameters.number(1) ?? Int.max + if let obj = arg, let arr = arguments.coerceToArray(obj, capAt: subitercap) { + let formatted = try control.format(with: + context.parserConfig.makeArguments( + locale: arguments.locale, + tabsize: arguments.tabsize, + args: arr, + numArgumentsLeft: iterargs.left), in: .frame(res, context)) res += formatted.string if case .break = formatted { break @@ -652,11 +647,12 @@ public enum StandardDirectiveSpecifier: DirectiveSpecifier { } else if subitercap > 0 { var newargs = [Any?]() newargs.append(arg) - let formatted = try control.format(with: Arguments(locale: arguments.locale, - tabsize: arguments.tabsize, - args: newargs, - numArgumentsLeft: iterargs.left), - in: .frame(res, context)) + let formatted = try control.format(with: + context.parserConfig.makeArguments( + locale: arguments.locale, + tabsize: arguments.tabsize, + args: newargs, + numArgumentsLeft: iterargs.left), in: .frame(res, context)) res += formatted.string if case .break = formatted { break @@ -744,7 +740,7 @@ public enum StandardDirectiveSpecifier: DirectiveSpecifier { } case .indirection: let control = try CLControl(string: try arguments.nextAsString(), - config: context.parserConfig) + config: context.parserConfig) if modifiers.contains(.at) { return .append(try control.format(with: arguments, in: context).string) } else {