Skip to content

Instantly share code, notes, and snippets.

@aceontech
Last active August 1, 2020 19:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aceontech/07929e0f6b5a880cad8f08fefa48917a to your computer and use it in GitHub Desktop.
Save aceontech/07929e0f6b5a880cad8f08fefa48917a to your computer and use it in GitHub Desktop.
Prototype implementation of a .strings file parser written in Swift, using Parser Combinators, as explained by PointFree.co. See https://www.pointfree.co/collections/parsing/parser-combinators for all videos on this topic.
public protocol FileParser {
func parse(string: String) -> [Entry]
}
public struct FileParserFactory {
public static func unordered() -> FileParser {
UnorderedFileParser()
}
public static func ordered() -> FileParser {
LineOrderedFileParser(unorderedParser: unordered())
}
}
struct UnorderedFileParser: FileParser {
func parse(string: String) -> [Entry] {
stringsFile.run(string).match ?? []
}
}
struct LineOrderedFileParser: FileParser {
let unorderedParser: FileParser
func parse(string: String) -> [Entry] {
unorderedParser
.parse(string: string)
.enumerated()
.map { index, entry in
entry.map(withOrder: index)
}
}
}
// MARK: Parser helpers
private let quote = literal("\"")
private let newLine = Character("\n")
private let stringsFile: Parser<[Entry]> = zeroOrMore(
oneOf([
entry,
singleLineSlashComment.map { Entry(comment: $0) },
singleLineSlashAsteriskComment.map { Entry(comment: $0) }
]),
separatedBy: zeroOrMore(char: newLine)
)
private let entry = zip(
operand,
assignOperator,
operand,
semicolon
).map { key, _, value, _ in
Entry(
key: String(key),
value: String(value),
order: 0
)
}
private let operand = zip(quote, anyString(until: quote)).map { $1 }
private let assignOperator = zip(
zeroOrMoreSpaces,
literal("="),
zeroOrMoreSpaces
).map { _ in () }
private let semicolon = literal(";")
private let singleLineSlashComment = zip(
zeroOrMoreSpaces,
literal("//"),
zeroOrMoreSpaces,
prefix(while: { $0 != newLine })
).map { _, _, _, commentText in
String(commentText)
}
private let singleLineSlashAsteriskComment = zip(
zeroOrMoreSpaces,
literal("/*"),
anyString(until: zip(
literal("*/"),
zeroOrMore(char: newLine)
))
).map { _, _, commentText in
commentText.trimmingCharacters(in: .whitespaces)
}
import XCTest
import Data
class StringsFileParserTests: XCTestCase {
func testSeparatedByNewlines() throws {
let entries = FileParserFactory.ordered().parse(string:
"""
"editor.button.save" = "Save";
"editor.button.open" = "Open";
"""
)
XCTAssertEqual(entries.count, 2)
XCTAssertEqual(entries[0].content, .key("editor.button.save", value: "Save"))
XCTAssertEqual(entries[0].order, 0)
XCTAssertEqual(entries[1].content, .key("editor.button.open", value: "Open"))
XCTAssertEqual(entries[1].order, 1)
}
func testSeparatedBySemicolonsOnly() throws {
let entries = FileParserFactory.ordered().parse(string:
"""
"editor.button.save" = "Save";"editor.button.open" = "Open";
"""
)
XCTAssertEqual(entries.count, 2)
XCTAssertEqual(entries[0].content, .key("editor.button.save", value: "Save"))
XCTAssertEqual(entries[0].order, 0)
XCTAssertEqual(entries[1].content, .key("editor.button.open", value: "Open"))
XCTAssertEqual(entries[1].order, 1)
}
func testLineOrder() throws {
let entries = FileParserFactory.ordered().parse(string:
"""
"line.0" = "Line 0";
"line.1" = "Line 1";
"line.2" = "Line 2";
"line.3" = "Line 3";
"line.4" = "Line 4";
"""
)
XCTAssertEqual(entries.count, 5)
XCTAssertEqual(entries, [
Entry(key: "line.0", value: "Line 0", order: 0),
Entry(key: "line.1", value: "Line 1", order: 1),
Entry(key: "line.2", value: "Line 2", order: 2),
Entry(key: "line.3", value: "Line 3", order: 3),
Entry(key: "line.4", value: "Line 4", order: 4)
])
}
func testSingleLineSlashComment() throws {
var entries: [Entry]
// In the middle
entries = FileParserFactory.unordered().parse(string:
"""
"line.0" = "Line 0";
// A single line comment
"line.1" = "Line 1";
"""
)
XCTAssertEqual(entries.count, 3)
XCTAssertEqual(entries, [
Entry(key: "line.0", value: "Line 0"),
Entry(comment: "A single line comment"),
Entry(key: "line.1", value: "Line 1")
])
// As first line
entries = FileParserFactory.unordered().parse(string:
"""
// A single line comment
"line.0" = "Line 0";
"line.1" = "Line 1";
"""
)
XCTAssertEqual(entries.count, 3)
XCTAssertEqual(entries, [
Entry(comment: "A single line comment"),
Entry(key: "line.0", value: "Line 0"),
Entry(key: "line.1", value: "Line 1")
])
// As last line
entries = FileParserFactory.unordered().parse(string:
"""
"line.0" = "Line 0";
"line.1" = "Line 1";
// A single line comment
"""
)
XCTAssertEqual(entries.count, 3)
XCTAssertEqual(entries, [
Entry(key: "line.0", value: "Line 0"),
Entry(key: "line.1", value: "Line 1"),
Entry(comment: "A single line comment")
])
// Leading and trailing whitespace handling
entries = FileParserFactory.unordered().parse(string:
"""
"line.0" = "Line 0";
// A single line comment
"line.1" = "Line 1";
"""
)
XCTAssertEqual(entries.count, 3)
XCTAssertEqual(entries, [
Entry(key: "line.0", value: "Line 0"),
Entry(comment: "A single line comment"),
Entry(key: "line.1", value: "Line 1")
])
// Only comment
entries = FileParserFactory.unordered().parse(string:
"""
// A single line comment
"""
)
XCTAssertEqual(entries.count, 1)
XCTAssertEqual(entries, [
Entry(comment: "A single line comment")
])
}
func testSingleLineSlashAsteriskComment() throws {
var entries: [Entry]
// In the middle
entries = FileParserFactory.unordered().parse(string:
"""
"line.0" = "Line 0";
/* A single line comment */
"line.1" = "Line 1";
"""
)
XCTAssertEqual(entries.count, 3)
XCTAssertEqual(entries, [
Entry(key: "line.0", value: "Line 0"),
Entry(comment: "A single line comment"),
Entry(key: "line.1", value: "Line 1")
])
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment