Compare commits

...

28 commits
v0.2.0 ... main

Author SHA1 Message Date
28a901e6ae
Merge pull request #4 from 5sw/figma
Import colors from Figma library
2022-10-08 13:14:23 +02:00
fa9da22633 Remove setup-xcode, should not be needed any more 2022-10-08 13:09:40 +02:00
028095f00e Use macos-12 image 2022-10-08 13:08:03 +02:00
2271198f47 Select Xcode 14 2022-10-08 13:04:59 +02:00
fe95a6fd35 Need Xcode 14 to build 2022-10-08 13:00:03 +02:00
4a489ccd20 Update .swift-version 2022-10-08 12:59:18 +02:00
ac708f4ae2 Let importer determine output file name. 2022-10-08 12:50:55 +02:00
2b1f21dbab Quick and dirty figma importer 2022-10-08 12:41:02 +02:00
956c1f5d27 Make importer async 2022-10-08 11:48:33 +02:00
3adefbf70e Update argument parser 2022-10-08 11:47:32 +02:00
0bb53ea949 Remove library target 2022-10-08 11:46:52 +02:00
012203a4f3 Lowercase importer names 2022-10-08 11:42:42 +02:00
d7d34812cf Format 2022-10-08 11:42:02 +02:00
dc00d87a32 Add option to select importer. 2022-10-08 11:20:00 +02:00
02d2350b29 Fix warnings 2022-10-08 11:04:24 +02:00
e28bee804f Make importer protocol. 2022-10-08 11:03:28 +02:00
e6f44f5be2 Fix lint warnings 2021-01-02 22:43:54 +01:00
ba7c0c04fa
Update README.md 2021-01-02 22:35:45 +01:00
e8a02b0180
Add info on HSV colors to README 2021-01-02 22:33:52 +01:00
78fcdef196 Update asset catalog image 2021-01-02 22:22:35 +01:00
404566c36b Add example file 2021-01-02 22:20:55 +01:00
1c31c11f49 Create unit test for color JSON 2021-01-02 22:03:41 +01:00
be2a655c40 Match formatting used by Xcode. Turns out Xcode requires two digits for hex colors values. Single digits get repeated. 2021-01-02 21:17:41 +01:00
8262760bc7 Limit percent value from 0 to 100. 2021-01-01 15:18:16 +01:00
04943d8f63 Use simplified argument list parser 2021-01-01 15:17:50 +01:00
75a3bc265d Parse hsv colors 2021-01-01 13:49:44 +01:00
0b94872101 Add HSV conversion 2021-01-01 13:33:11 +01:00
e65f684654 Support giving color values as percentage 2021-01-01 12:52:20 +01:00
26 changed files with 559 additions and 82 deletions

View file

@ -24,7 +24,7 @@ jobs:
head "https://github.com/${{ github.repository }}.git"
license "MIT"
depends_on :xcode => ["12.0", :build]
depends_on :xcode => ["14.0", :build]
def install
system "make", "install", "prefix=#{prefix}"

View file

@ -5,7 +5,7 @@ on:
jobs:
BuildAndTest:
runs-on: macos-latest
runs-on: macos-12
steps:
- uses: actions/checkout@v1

View file

@ -1 +1 @@
5.3
5.7

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

2
Example/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
Example.*
!Example.txt

10
Example/Example.txt Normal file
View file

@ -0,0 +1,10 @@
Base/Green #8fd151
Base/PaleGreen #d0f9a9
Base/Red rgb(249, 39, 7)
TransparentRed rgba(255, 0, 0, 128)
Base/Yellow #ff0
Error @Base/Red
Warning @Base/Yellow
Good @Base/Green

View file

@ -1,16 +1,23 @@
{
"object": {
"pins" : [
{
"package": "swift-argument-parser",
"repositoryURL": "https://github.com/apple/swift-argument-parser",
"identity" : "rbbjson",
"kind" : "remoteSourceControl",
"location" : "https://github.com/robb/RBBJSON",
"state" : {
"branch": null,
"revision": "92646c0cdbaca076c8d3d0207891785b3379cbff",
"version": "0.3.1"
"branch" : "main",
"revision" : "102c970283e105d7c5be2e29630db29c808c20eb"
}
}
]
},
"version": 1
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
"revision" : "9f39744e025c7d377987f30b03770805dcb0bcd1",
"version" : "1.1.4"
}
}
],
"version" : 2
}

View file

@ -1,4 +1,4 @@
// swift-tools-version:5.3
// swift-tools-version:5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@ -6,27 +6,25 @@ import PackageDescription
let package = Package(
name: "MakeColors",
platforms: [
.macOS("10.15.4"),
.macOS("12.0"),
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "0.3.1")),
.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "1.1.4")),
.package(url: "https://github.com/robb/RBBJSON", branch: "main"),
],
targets: [
.target(
.executableTarget(
name: "MakeColors",
dependencies: [
"LibMakeColors",
]
),
.target(
name: "LibMakeColors",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]
),
.testTarget(
name: "MakeColorsTests",
dependencies: ["LibMakeColors"]
dependencies: [
"MakeColors",
.product(name: "RBBJSON", package: "RBBJSON"),
]
),
]
)

View file

@ -45,6 +45,7 @@ Base/Red rgb(249, 39, 7)
TransparentRed rgba(255, 0, 0, 128)
Base/Yellow #ff0
```
Grayscale colors can be produced with the `white(value)` and `white(value, alpha)` syntax. A value of zero means black while a value of 255 is pure white.
```
@ -53,6 +54,12 @@ MediumGray white(128)
TransparentGray white(128, 128)
```
HSV colors can be specified as `hsv(hue, saturation, value)` or `hsva(hue, saturation, value, alpha)` syntax. Hue is specified as degrees with the `°` or `deg` suffix.
```
HSV/Yellow hsv(60°, 255, 255)
```
Colors can also reference other colors by prefixing them with an `@` sign:
```
@ -102,7 +109,6 @@ The generated HTML looks like this:
## Future work
- Support other color formats (HSV, ...)
- Calculate derived colors (blend, change hue/saturation/brightness/alpha)
- Support for dark/light mode
- Improved error reporting in the parser

View file

@ -58,7 +58,7 @@ private let infoTag = """
}
"""
private extension Color {
extension Color {
func json() -> String {
"""
{
@ -67,10 +67,10 @@ private extension Color {
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "0x\(String(alpha, radix: 16))",
"blue" : "0x\(String(blue, radix: 16))",
"green" : "0x\(String(green, radix: 16))",
"red" : "0x\(String(red, radix: 16))"
"alpha" : "\(Double(alpha) / 0xFF)",
"blue" : "0x\(blue, radix: 16, width: 2)",
"green" : "0x\(green, radix: 16, width: 2)",
"red" : "0x\(red, radix: 16, width: 2)"
}
},
"idiom" : "universal"
@ -88,6 +88,22 @@ private extension Color {
}
}
extension String.StringInterpolation {
mutating func appendInterpolation<I: BinaryInteger>(
_ value: I,
radix: Int,
width: Int = 0,
uppercase: Bool = true
) {
var string = String(value, radix: radix, uppercase: uppercase)
if width > string.count {
string.insert(contentsOf: String(repeating: "0", count: width - string.count), at: string.startIndex)
}
appendLiteral(string)
}
}
private let group = """
{
"properties" : {

View file

@ -1,6 +1,6 @@
import Foundation
protocol Generator: class {
protocol Generator: AnyObject {
static var defaultExtension: String { get }
static var option: String { get }
@ -9,7 +9,7 @@ protocol Generator: class {
func generate(data: [String: ColorDef]) throws -> FileWrapper
}
protocol Context: class {
protocol Context: AnyObject {
var prefix: String? { get }
}

View file

@ -0,0 +1,151 @@
import Foundation
enum FigmaErrors: Error {
case invalidUrl
case missingToken
case invalidResponse
case missingColor(String)
}
class FigmaImporter: Importer {
let key: String
let token: String
let outputName: String
required init(source: String) throws {
// https://www.figma.com/file/:key/:title
guard
let url = URL(string: source),
url.host == "www.figma.com",
url.pathComponents.count >= 4,
url.pathComponents[1] == "file"
else {
throw FigmaErrors.invalidUrl
}
key = url.pathComponents[2]
outputName = url.pathComponents[3]
guard let token = ProcessInfo.processInfo.environment["FIGMA_TOKEN"] else {
throw FigmaErrors.missingToken
}
self.token = token
}
func read() async throws -> [String: ColorDef] {
let styles = try await request(StylesResponse.self, path: "/v1/files/\(key)/styles").meta.styles
.filter { $0.styleType == "FILL" }
let ids = styles.map(\.nodeId).joined(separator: ",")
let nodes = try await request(
NodesResponse.self,
path: "/v1/files/\(key)/nodes",
query: [URLQueryItem(name: "ids", value: ids)]
)
.nodes
var result: [String: ColorDef] = [:]
result.reserveCapacity(styles.count)
for style in styles {
guard
let node = nodes[style.nodeId],
let fill = node.document.fills.first(where: { $0.type == "SOLID" })
else {
throw FigmaErrors.missingColor(style.name)
}
if node.document.fills.count > 1 {
print("Warning: Multiple fills defined for \(style.name)")
}
if fill.blendMode != "NORMAL" {
print("Warning: Blend mode \(fill.blendMode) used for \(style.name)")
}
guard !result.keys.contains(style.name) else {
throw Errors.duplicateColor(style.name)
}
result[style.name] = .color(Color(fill.color))
}
return result
}
func request<T: Decodable>(_: T.Type = T.self, path: String, query: [URLQueryItem]? = nil) async throws -> T {
var components = URLComponents()
components.scheme = "https"
components.host = "api.figma.com"
components.path = path
components.queryItems = query
guard let url = components.url else {
fatalError("Cannot create url. Components: \(components)")
}
var request = URLRequest(url: url)
request.setValue(token, forHTTPHeaderField: "X-Figma-Token")
let (data, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse else {
fatalError("Non-HTTP-Response received: \(response)")
}
guard response.statusCode == 200 else {
throw FigmaErrors.invalidResponse
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return try decoder.decode(T.self, from: data)
}
}
struct StylesResponse: Decodable {
var meta: Meta
struct Meta: Decodable {
var styles: [Style]
}
struct Style: Decodable {
var nodeId: String
var styleType: String
var name: String
var description: String
}
}
struct NodesResponse: Decodable {
var nodes: [String: Node]
struct Node: Decodable {
var document: Document
}
struct Document: Decodable {
var fills: [Fill]
}
struct Fill: Decodable {
var blendMode: String
var type: String
var color: Color
}
struct Color: Decodable {
var r, g, b, a: Float
}
}
extension Color {
init(_ color: NodesResponse.Color) {
red = UInt8(truncatingIfNeeded: Int(color.r * 0xFF))
green = UInt8(truncatingIfNeeded: Int(color.g * 0xFF))
blue = UInt8(truncatingIfNeeded: Int(color.b * 0xFF))
alpha = UInt8(truncatingIfNeeded: Int(color.a * 0xFF))
}
}

View file

@ -0,0 +1,15 @@
protocol Importer {
init(source: String) throws
func read() async throws -> [String: ColorDef]
var outputName: String { get }
static var option: String { get }
}
extension Importer {
static var option: String {
String(describing: self).droppingSuffix("Importer").lowercased()
}
}

View file

@ -0,0 +1,38 @@
import Foundation
struct ListImporter: Importer {
let input: String
var outputName: String
init(source: String) {
input = source
outputName = URL(fileURLWithPath: source).deletingPathExtension().lastPathComponent
}
func read() throws -> [String: ColorDef] {
let scanner = Scanner(string: try readInput())
scanner.charactersToBeSkipped = .whitespaces
return try scanner.colorList()
}
func readInput() throws -> String {
if input == "-" {
return try readStdin()
}
let url = URL(fileURLWithPath: input)
return try String(contentsOf: url)
}
func readStdin() throws -> String {
guard
let data = try FileHandle.standardInput.readToEnd(),
let input = String(data: data, encoding: .utf8)
else {
throw Errors.cannotReadStdin
}
return input
}
}

View file

@ -19,11 +19,11 @@ extension Scanner {
}
}
if string("rgba"), string("("), let components = commaSeparated(), components.count == 4, string(")") {
if string("rgba"), let components = argumentList(4) {
return Color(components)
}
if string("rgb"), string("("), let components = commaSeparated(), components.count == 3, string(")") {
if string("rgb"), let components = argumentList(3) {
return Color(components)
}
@ -31,9 +31,58 @@ extension Scanner {
return Color(white: arguments)
}
if string("hsva") {
return readHSV(alpha: true)
}
if string("hsv") {
return readHSV(alpha: false)
}
return nil
}
private func readHSV(alpha readAlpha: Bool) -> Color? {
guard
string("("),
let hue = degrees(),
string(","),
let saturation = component(),
string(","),
let value = component()
else { return nil }
let alpha: UInt8
if readAlpha {
guard
string(","),
let value = component()
else { return nil }
alpha = value
} else {
alpha = 0xFF
}
guard string(")") else { return nil }
return Color(hue: hue, saturation: saturation, value: value, alpha: alpha)
}
func degrees() -> Int? {
guard let int = scanInt() else { return nil }
if string("%") {
guard 0...100 ~= int else { return nil }
return (360 * int) / 100
} else if string("°") || string("deg") {
return int
} else {
guard 0...0xFF ~= int else { return nil }
return (360 * int) / 0xFF
}
}
func colorReference() -> String? {
guard string("@") else { return nil }
return name()
@ -61,7 +110,7 @@ extension Scanner {
func colorLine() -> (String, ColorDef)? {
guard
let name = self.name(),
let name = name(),
let def = colorDef(),
endOfLine()
else {
@ -119,13 +168,26 @@ extension Scanner {
func commaSeparated() -> [UInt8]? {
var result: [UInt8] = []
repeat {
guard let int = scanInt(), let component = UInt8(exactly: int) else {
guard let component = component() else {
return nil
}
result.append(component)
} while string(",")
return result
}
func component() -> UInt8? {
guard var int = scanInt() else {
return nil
}
if string("%") {
guard 0...100 ~= int else { return nil }
int = int * 0xFF / 100
}
return UInt8(exactly: int)
}
}
private extension Scanner {

View file

@ -19,6 +19,39 @@ private struct GeneratorOption: EnumerableFlag, CustomStringConvertible {
}
}
private struct ImporterOption: CaseIterable, ExpressibleByArgument, CustomStringConvertible {
static let allCases: [ImporterOption] = [
.list,
.init(type: FigmaImporter.self),
]
static let list = ImporterOption(type: ListImporter.self)
let type: Importer.Type
init(type: Importer.Type) {
self.type = type
}
init?(argument: String) {
guard
let found = Self.allCases
.first(where: { $0.description.caseInsensitiveCompare(argument) == .orderedSame })
else {
return nil
}
self = found
}
var description: String {
type.option
}
static func == (lhs: ImporterOption, rhs: ImporterOption) -> Bool {
lhs.type == rhs.type
}
}
enum Errors: Error {
case syntaxError
case duplicateColor(String)
@ -47,13 +80,17 @@ enum HelpTexts {
)
}
public final class MakeColors: ParsableCommand, Context {
@main
public final class MakeColors: AsyncParsableCommand, Context {
@Argument(help: HelpTexts.input)
var input: String
@Flag(help: "The formatter to use.")
private var formatter = GeneratorOption.allCases[0]
@Option(help: "The importer to use.")
private var importer = ImporterOption.list
@Option(help: "Prefix for color names.")
var prefix: String?
@ -65,11 +102,9 @@ public final class MakeColors: ParsableCommand, Context {
public init() {}
public func run() throws {
let scanner = Scanner(string: try readInput())
scanner.charactersToBeSkipped = .whitespaces
let data = try scanner.colorList()
public func run() async throws {
let importer = try importer.type.init(source: input)
let data = try await importer.read()
if dump {
try dump(data: data)
@ -78,27 +113,7 @@ public final class MakeColors: ParsableCommand, Context {
let generator = formatter.type.init(context: self)
let fileWrapper = try generator.generate(data: data)
try writeOutput(fileWrapper)
}
func readInput() throws -> String {
if input == "-" {
return try readStdin()
}
let url = URL(fileURLWithPath: input)
return try String(contentsOf: url)
}
func readStdin() throws -> String {
guard
let data = try FileHandle.standardInput.readToEnd(),
let input = String(data: data, encoding: .utf8)
else {
throw Errors.cannotReadStdin
}
return input
try writeOutput(fileWrapper, name: output ?? "\(importer.outputName).\(formatter.type.defaultExtension)")
}
func dump(data: [String: ColorDef]) throws {
@ -118,7 +133,7 @@ public final class MakeColors: ParsableCommand, Context {
}
}
func writeOutput(_ wrapper: FileWrapper) throws {
func writeOutput(_ wrapper: FileWrapper, name: String) throws {
if shouldWriteToStdout {
guard wrapper.isRegularFile, let contents = wrapper.regularFileContents else {
throw Errors.cannotWriteWrapperToStdout
@ -126,19 +141,10 @@ public final class MakeColors: ParsableCommand, Context {
FileHandle.standardOutput.write(contents)
} else {
let writeURL = outputURL(extension: formatter.type.defaultExtension)
let writeURL = URL(fileURLWithPath: name)
try wrapper.write(to: writeURL, options: .atomic, originalContentsURL: nil)
}
}
var shouldWriteToStdout: Bool { output == "-" || (input == "-" && output == nil) }
func outputURL(extension: String) -> URL {
if let output = output {
return URL(fileURLWithPath: output)
} else {
let basename = URL(fileURLWithPath: input).deletingPathExtension().lastPathComponent
return URL(fileURLWithPath: basename).appendingPathExtension(`extension`)
}
}
}

View file

@ -0,0 +1,45 @@
extension Color {
init(hue: Int, saturation: UInt8, value: UInt8, alpha: UInt8 = 0xFF) {
let degrees = abs(hue % 360)
let saturation = Double(saturation) / 0xFF
let value = Double(value) / 0xFF
// swiftlint:disable identifier_name - Wish I knew what these actually mean.
let C = saturation * value
let X = C * (1 - abs((Double(degrees) / 60).truncatingRemainder(dividingBy: 2) - 1))
let m = value - C
// swiftlint:enable identifier_name
let result: (r: Double, g: Double, b: Double)
switch degrees {
case 0..<60:
result = (C, X, 0)
case 60..<120:
result = (X, C, 0)
case 120..<180:
result = (0, C, X)
case 180..<240:
result = (0, X, C)
case 240..<300:
result = (X, 0, C)
case 300..<360:
result = (C, 0, X)
default:
fatalError("Degrees out of range")
}
self.init(
red: UInt8(((result.r + m) * 0xFF).rounded()),
green: UInt8(((result.g + m) * 0xFF).rounded()),
blue: UInt8(((result.b + m) * 0xFF).rounded()),
alpha: alpha
)
}
}

View file

@ -1,3 +0,0 @@
import LibMakeColors
MakeColors.main()

View file

@ -0,0 +1,26 @@
@testable import MakeColors
import RBBJSON
import XCTest
class AssetCatalogFormattingTest: XCTestCase {
func testColorProducedValidJSON() throws {
let color = Color(red: 0xFF, green: 0xF0, blue: 0x0F)
let data = Data(color.json().utf8)
let json = try JSONDecoder().decode(RBBJSON.self, from: data)
XCTAssertEqual(json.info.author, .string("de.5sw.MakeColors"))
XCTAssertEqual(json.info.version, .number(1))
XCTAssertEqual(Array(json.colors[any: .child]).count, 1)
XCTAssertEqual(json.colors[0].idiom, .string("universal"))
let jsonColor = json.colors[0].color
XCTAssertEqual(jsonColor["color-space"], .string("srgb"))
XCTAssertEqual(jsonColor.components.alpha, .string("1.0"))
XCTAssertEqual(jsonColor.components.red, .string("0xFF"))
XCTAssertEqual(jsonColor.components.green, .string("0xF0"))
XCTAssertEqual(jsonColor.components.blue, .string("0x0F"))
}
}

View file

@ -0,0 +1,39 @@
@testable import MakeColors
import XCTest
final class ColorHSVTest: XCTestCase {
func testHSV0Degrees() {
let color = Color(hue: 0, saturation: 0xFF, value: 0xFF)
XCTAssertEqual(color, Color(red: 0xFF, green: 0, blue: 0))
}
func testHSV60Degrees() {
let color = Color(hue: 60, saturation: 0xFF, value: 0xFF)
XCTAssertEqual(color, Color(red: 0xFF, green: 0xFF, blue: 0))
}
func testHSV120Degrees() {
let color = Color(hue: 120, saturation: 0xFF, value: 0xFF)
XCTAssertEqual(color, Color(red: 0, green: 0xFF, blue: 0))
}
func testHSV180Degrees() {
let color = Color(hue: 180, saturation: 0xFF, value: 0xFF)
XCTAssertEqual(color, Color(red: 0, green: 0xFF, blue: 0xFF))
}
func testHSV240Degrees() {
let color = Color(hue: 240, saturation: 0xFF, value: 0xFF)
XCTAssertEqual(color, Color(red: 0, green: 0, blue: 0xFF))
}
func testHSV300Degrees() {
let color = Color(hue: 300, saturation: 0xFF, value: 0xFF)
XCTAssertEqual(color, Color(red: 0xFF, green: 0, blue: 0xFF))
}
func testHSV360Degrees() {
let color = Color(hue: 360, saturation: 0xFF, value: 0xFF)
XCTAssertEqual(color, Color(red: 0xFF, green: 0, blue: 0))
}
}

View file

@ -1,4 +1,4 @@
@testable import LibMakeColors
@testable import MakeColors
import XCTest
final class ColorParserTest: XCTestCase {
@ -57,8 +57,67 @@ final class ColorParserTest: XCTestCase {
XCTAssertNil(color)
}
func testScanningColorWithPercentage() throws {
let color = scanColor("rgba(100%, 0, 50%, 100%)")
XCTAssertEqual(color, Color(red: 255, green: 0, blue: 127, alpha: 255))
}
func testReadingComponentAsByte() throws {
let scanner = Scanner(string: "128")
XCTAssertEqual(scanner.component(), 128)
XCTAssertTrue(scanner.isAtEnd)
}
func testReadingComponentAs100Percent() throws {
let scanner = Scanner(string: "100%")
XCTAssertEqual(scanner.component(), 0xFF)
XCTAssertTrue(scanner.isAtEnd)
}
func testReadingComponentAs0Percent() throws {
let scanner = Scanner(string: "0%")
XCTAssertEqual(scanner.component(), 0)
XCTAssertTrue(scanner.isAtEnd)
}
func testReadingComponentAs50PercentRoundsDown() throws {
let scanner = Scanner(string: "50%")
XCTAssertEqual(scanner.component(), 127)
XCTAssertTrue(scanner.isAtEnd)
}
func testScanningDegreesAsByte() throws {
let scanner = Scanner(string: "128")
XCTAssertEqual(scanner.degrees(), 180)
}
func testScanningDegreesAsPercentage() throws {
let scanner = Scanner(string: "50%")
XCTAssertEqual(scanner.degrees(), 180)
}
func testScanningDegrees() throws {
let scanner = Scanner(string: "120°")
XCTAssertEqual(scanner.degrees(), 120)
}
func testScanningDegreesWithDegSuffix() throws {
let scanner = Scanner(string: "120 deg")
XCTAssertEqual(scanner.degrees(), 120)
}
func testScanningHSVColor() throws {
XCTAssertEqual(scanColor("hsv(60°, 255, 100%)"), Color(red: 0xFF, green: 0xFF, blue: 0))
}
func testScanningHSVAColor() throws {
XCTAssertEqual(scanColor("hsva(60°, 50%, 255, 99)"), Color(red: 0xFF, green: 0xFF, blue: 128, alpha: 99))
}
private func scanColor(_ input: String) -> Color? {
let scanner = Scanner(string: input)
return scanner.color()
let result = scanner.color()
XCTAssertTrue(result == nil || scanner.isAtEnd)
return result
}
}