Repository: apple/swift-markdown (swift)

Issue:
BlockDirective range issue
For a multi-line block directive, it can parse properly.
eg.
```swift
import Markdown
let source = """
@Image(source: a.png, 
        alt: "hello")
"""
Document(parsing: source, options: [.parseBlockDirectives])
.debugDescription(options: .printSourceLocations)
```

```
Document @1:1-2:22 Root #1 #0
└─ BlockDirective @1:1-2:22 #1 name: "Image"
   ├─ Argument text segments:
   |    @1:8-1:23: "source: a.png, "
   |    @2:1-2:21: "        alt: \"hello\""
```

However, the single-line form behavior is not.

eg.
```swift
import Markdown
let source = """
@Image(source: a.png, alt: "hello")
"""
Document(parsing: source, options: [.parseBlockDirectives])
.debugDescription(options: .printSourceLocations)
```

## Actually behavior

```
Document @1:1-1:8 Root #1 #0
└─ BlockDirective @1:1-1:8 #1 name: "Image"
   ├─ Argument text segments:
   |    @1:8-1:35: "source: a.png, alt: \"hello\""
```

## Expected behavior

```
Document @1:1-1:36 Root #1 #0
└─ BlockDirective @1:1-1:36 #1 name: "Image"
   ├─ Argument text segments:
   |    @1:8-1:35: "source: a.png, alt: \"hello\""
```

## Other

This is effecting https://github.com/apple/swift-docc/pull/278

And a temporary workaround on swift-docc is 👇
```swift
// FIXME: - Bug on swift-markdown #49
guard let blockRange = blockDirective.range else { return }
let bound = blockDirective.argumentText.parseNameValueArguments().compactMap {
    $0.valueRange?.upperBound
}.max() ?? SourceLocation(line: 0, column: 0, source: blockRange.upperBound.source)
let upperBound = SourceLocation(line: bound.line, column: blockRange.upperBound.column + bound.column, source: bound.source)
let range = blockRange.lowerBound ..< upperBound
// let range = blockRange
```
> The workaround is buggy and will not work if the last argument's value is surrounded by quotes.

Pull request:
Bug/issue #, if applicable: 

## Summary

Close #49 

## Checklist

Make sure you check off the following items. If they cannot be completed, provide a reason.

- [x] Added tests
- [x] Ran the `./bin/test` script and it succeeded
- [x] Updated documentation if necessary

Patch:
diff --git a/Sources/Markdown/Parser/BlockDirectiveParser.swift b/Sources/Markdown/Parser/BlockDirectiveParser.swift
index 9426decc..73eb2308 100644
--- a/Sources/Markdown/Parser/BlockDirectiveParser.swift
+++ b/Sources/Markdown/Parser/BlockDirectiveParser.swift
@@ -102,7 +102,6 @@ struct PendingBlockDirective {
             // still consider subsequent lines for argument text, so we'll
             // indicate acceptance either way at this point.
             _ = parseArgumentsText(from: line)
-            endLocation = line.location!
             return true
         } else {
             parseState = .contentsStart
@@ -121,6 +120,7 @@ struct PendingBlockDirective {
             self.argumentsText.append(argumentsText)
             accepted = true
         }
+        endLocation = line.location!
 
         if line.text.starts(with: ")") {
             parseState = .argumentsEnd


Test patch:
diff --git a/Tests/MarkdownTests/Parsing/BlockDirectiveParserTests.swift b/Tests/MarkdownTests/Parsing/BlockDirectiveParserTests.swift
index 0d81d383..07bbebba 100644
--- a/Tests/MarkdownTests/Parsing/BlockDirectiveParserTests.swift
+++ b/Tests/MarkdownTests/Parsing/BlockDirectiveParserTests.swift
@@ -292,7 +292,7 @@ class BlockDirectiveArgumentParserTests: XCTestCase {
 
         let expectedDump = """
         Document @1:1-3:4
-        ├─ BlockDirective @1:1-1:8 name: "Outer"
+        ├─ BlockDirective @1:1-1:13 name: "Outer"
         │  ├─ Argument text segments:
         │  |    @1:8-1:12: "x: 1"
         └─ Paragraph @2:3-3:2
@@ -867,4 +867,92 @@ class BlockDirectiveArgumentParserTests: XCTestCase {
             XCTAssertEqual(x.value, "")
         }
     }
+    
+    
+    func testSingleLineRange() {
+        let source = """
+        @Image(source: 1.png, alt: "hello")
+        """
+        
+        let document = Document(parsing: source, options: .parseBlockDirectives)
+        let directive = document.child(at: 0) as! BlockDirective
+        XCTAssertEqual(1, directive.argumentText.segments.count)
+
+        var parseErrors = [DirectiveArgumentText.ParseError]()
+        let arguments = directive.argumentText.parseNameValueArguments(parseErrors: &parseErrors)
+        XCTAssertTrue(parseErrors.isEmpty)
+        
+        XCTAssertEqual(arguments.count,2)
+        arguments["source"].map { x in
+            XCTAssertEqual(x.name, "source")
+            XCTAssertEqual(x.value, "1.png")
+        }
+        arguments["alt"].map { x in
+            XCTAssertEqual(x.name, "alt")
+            XCTAssertEqual(x.value, "hello")
+        }
+        
+        let expectedDump = #"""
+        Document @1:1-1:36
+        └─ BlockDirective @1:1-1:36 name: "Image"
+           ├─ Argument text segments:
+           |    @1:8-1:35: "source: 1.png, alt: \"hello\""
+        """#
+        XCTAssertEqual(document.debugDescription(options: .printSourceLocations), expectedDump)
+    }
+    
+    func testSingleLineMissingCloseParenthesis() {
+        let source = """
+        @Image(source: 1.png,
+        alt: "hello"
+        @Image(source: 2.png, alt: "hello2")
+        """
+        
+        let document = Document(parsing: source, options: .parseBlockDirectives)
+        let expectedDump = #"""
+        Document @1:1-3:37
+        ├─ BlockDirective @1:1-2:13 name: "Image"
+        │  ├─ Argument text segments:
+        │  |    @1:8-1:22: "source: 1.png,"
+        │  |    @2:1-2:13: "alt: \"hello\""
+        └─ BlockDirective @3:1-3:37 name: "Image"
+           ├─ Argument text segments:
+           |    @3:8-3:36: "source: 2.png, alt: \"hello2\""
+        """#
+        XCTAssertEqual(document.debugDescription(options: .printSourceLocations), expectedDump)
+    }
+    
+    func testSingleLineOnlyOpenParenthesis() {
+        let source = """
+        @Image(
+        @Image(source: 2.png, alt: "hello2")
+        """
+        
+        let document = Document(parsing: source, options: .parseBlockDirectives)
+        let expectedDump = #"""
+        Document @1:1-2:37
+        ├─ BlockDirective @1:1-1:8 name: "Image"
+        └─ BlockDirective @2:1-2:37 name: "Image"
+           ├─ Argument text segments:
+           |    @2:8-2:36: "source: 2.png, alt: \"hello2\""
+        """#
+        XCTAssertEqual(document.debugDescription(options: .printSourceLocations), expectedDump)
+    }
+    
+    func testSingleLineMissingParenthesis() {
+        let source = """
+        @Image
+        @Image(source: 2.png, alt: "hello2")
+        """
+        
+        let document = Document(parsing: source, options: .parseBlockDirectives)
+        let expectedDump = #"""
+        Document @1:1-2:37
+        ├─ BlockDirective @1:1-1:7 name: "Image"
+        └─ BlockDirective @2:1-2:37 name: "Image"
+           ├─ Argument text segments:
+           |    @2:8-2:36: "source: 2.png, alt: \"hello2\""
+        """#
+        XCTAssertEqual(document.debugDescription(options: .printSourceLocations), expectedDump)
+    }
 }