diff --git a/README.md b/README.md index 32ec481..fa2db52 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,8 @@ To define metadata values within a Markdown document, use the following syntax: ``` --- keyA: valueA -keyB: valueB +keyB: "a value must be wrapped in quotes to have a : in it" +keyC: valueC --- Markdown text... diff --git a/Sources/Ink/Internal/Metadata.swift b/Sources/Ink/Internal/Metadata.swift index 4935f63..79ac5f7 100644 --- a/Sources/Ink/Internal/Metadata.swift +++ b/Sources/Ink/Internal/Metadata.swift @@ -13,28 +13,78 @@ internal struct Metadata: Readable { var metadata = Metadata() var lastKey: String? + enum Quotes { + case no, single, double + } + var valueInQuotes = Quotes.no while !reader.didReachEnd { reader.discardWhitespacesAndNewlines() - - guard reader.currentCharacter != "-" else { - try require(reader.readCount(of: "-") == 3) + + //Check if we are in a value marked by quotes. + guard valueInQuotes == .no + else { + + var value = trim(reader.readUntilEndOfLine()) + + if (value.last == "\"" && valueInQuotes == .double) || + (value.last == "'" && valueInQuotes == .single){ + valueInQuotes = .no + value.removeLast() + } + + if let lastKey = lastKey { + metadata.values[lastKey]?.append(" " + value) + } + continue + } + + //Check for end of metadata. + let currentIndex = reader.currentIndex + guard reader.readCount(of: "-") != 3 + else{ return metadata } + reader.moveToIndex(currentIndex) + //Read until the possible end of a key marked by a colon. This reads until the end of the line if there is no colon. let key = try trim(reader.read(until: ":", required: false)) - - guard reader.previousCharacter == ":" else { + + //If there is no colon, then this must be an orphan metadata line. Merge it into previous value. + guard reader.previousCharacter == ":" + else { if let lastKey = lastKey { metadata.values[lastKey]?.append(" " + key) } continue } + + reader.discardWhitespaces() + + //Check if we are starting a value wrapped in quotes and flag accordingly + if reader.currentCharacter == "\"" + { + reader.advanceIndex() + valueInQuotes = .double + } + if reader.currentCharacter == "'" + { + reader.advanceIndex() + valueInQuotes = .single + } + - let value = trim(reader.readUntilEndOfLine()) + var value = trim(reader.readUntilEndOfLine()) if !value.isEmpty { + + if (value.last == "\"" && valueInQuotes == .double) || + (value.last == "'" && valueInQuotes == .single){ + valueInQuotes = .no + value.removeLast() + } + metadata.values[key] = value lastKey = key } diff --git a/Tests/InkTests/MarkdownTests.swift b/Tests/InkTests/MarkdownTests.swift index b9fccbf..c7363fa 100644 --- a/Tests/InkTests/MarkdownTests.swift +++ b/Tests/InkTests/MarkdownTests.swift @@ -66,6 +66,71 @@ final class MarkdownTests: XCTestCase { XCTAssertEqual(markdown.metadata, [:]) XCTAssertEqual(markdown.html, "

Title

") } + + func testMultilineQuotedMetadataWithColonInValue(){ + let markdown = MarkdownParser().parse(""" + --- + a: "1 + b:2" + --- + # Title + """) + + XCTAssertEqual(markdown.metadata, ["a": "1 b:2"]) + XCTAssertEqual(markdown.html, "

Title

") + } + + func testMultilineQuotedMetadataWithEscapedQuotesInValue(){ + let markdown = MarkdownParser().parse(""" + --- + a: "1 + \"b\"2" + --- + # Title + """) + + XCTAssertEqual(markdown.metadata, ["a": "1 \"b\"2"]) + XCTAssertEqual(markdown.html, "

Title

") + } + + func testMultilineSingleQuotedMetadataWithDoubleQuotesInValue(){ + let markdown = MarkdownParser().parse(""" + --- + a: '1 + "b"2' + --- + # Title + """) + + XCTAssertEqual(markdown.metadata, ["a": "1 \"b\"2"]) + XCTAssertEqual(markdown.html, "

Title

") + } + + func testSingleLineValueWithQuotes(){ + let markdown = MarkdownParser().parse(""" + --- + a: "1" + b: '2' + --- + # Title + """) + + XCTAssertEqual(markdown.metadata, ["a": "1", "b": "2"]) + XCTAssertEqual(markdown.html, "

Title

") + } + + func testOrphanMetadataValueWithDashAtBeginning(){ + let markdown = MarkdownParser().parse(""" + --- + a: 1 + -2 + --- + # Title + """) + + XCTAssertEqual(markdown.metadata, ["a": "1 -2"]) + XCTAssertEqual(markdown.html, "

Title

") + } func testMetadataModifiers() { let parser = MarkdownParser(modifiers: [ @@ -138,6 +203,10 @@ extension MarkdownTests { ("testDiscardingEmptyMetadataValues", testDiscardingEmptyMetadataValues), ("testMergingOrphanMetadataValueIntoPreviousOne", testMergingOrphanMetadataValueIntoPreviousOne), ("testMissingMetadata", testMissingMetadata), + ("testMultilineQuotedMetadataWithColonInValue", testMultilineQuotedMetadataWithColonInValue), + ("testMultilineQuotedMetadataWithEscapedQuotesInValue", testMultilineQuotedMetadataWithEscapedQuotesInValue),("testMultilineSingleQuotedMetadataWithDoubleQuotesInValue", testMultilineSingleQuotedMetadataWithDoubleQuotesInValue), + ("testSingleLineValueWithQuotes", testSingleLineValueWithQuotes), + ("testOrphanMetadataValueWithDashAtBeginning", testOrphanMetadataValueWithDashAtBeginning), ("testMetadataModifiers", testMetadataModifiers), ("testPlainTextTitle", testPlainTextTitle), ("testRemovingTrailingMarkersFromTitle", testRemovingTrailingMarkersFromTitle),