Skip to content

Commit caafc56

Browse files
authored
Add support for an initial directive argument without a name (#27)
* Add support for a leading directive argument without a name * Update the supported years in check-source * Add missing copyright information to Markdown.md * Document changes to name-value argument syntax
1 parent 4f0c76f commit caafc56

File tree

5 files changed

+147
-20
lines changed

5 files changed

+147
-20
lines changed

Sources/Markdown/Base/DirectiveArgument.swift

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2021 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2022 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -178,8 +178,11 @@ public struct DirectiveArgumentText: Equatable {
178178
/// Parse the line segment as name-value argument pairs separated by commas.
179179
///
180180
/// ```
181-
/// name-value-arguments -> name-value-argument name-value-arguments-rest
181+
/// arguments -> first-argument name-value-arguments-rest
182+
/// first-argument -> value-only-argument | name-value-argument
183+
/// value-only-argument -> literal
182184
/// name-value-argument -> literal : literal
185+
/// name-value-arguments -> name-value-argument name-value-arguments-rest
183186
/// name-value-arguments-rest -> , name-value-arguments | ε
184187
/// ```
185188
///
@@ -188,6 +191,8 @@ public struct DirectiveArgumentText: Equatable {
188191
/// - An argument-name pair is only recognized within a single line or line segment;
189192
/// that is, an argument cannot span multiple lines.
190193
/// - A comma is expected between name-value pairs.
194+
/// - The first argument can be unnamed. An unnamed argument will have an empty ``DirectiveArgument/name`` with no ``DirectiveArgument/nameRange``.
195+
///
191196
/// - Parameter diagnosticEngine: the diagnostic engine to use for emitting diagnostics.
192197
/// - Returns: an array of successfully parsed ``DirectiveArgument`` values.
193198
public func parseNameValueArguments(parseErrors: inout [ParseError]) -> [DirectiveArgument] {
@@ -199,7 +204,10 @@ public struct DirectiveArgumentText: Equatable {
199204
parseIndex: parseIndex)
200205
line.lexWhitespace()
201206
while !line.isEmptyOrAllWhitespace {
202-
guard let name = parseLiteral(from: &line, parseErrors: &parseErrors) else {
207+
let name: TrimmedLine.Lex?
208+
let value: TrimmedLine.Lex
209+
210+
guard let firstLiteral = parseLiteral(from: &line, parseErrors: &parseErrors) else {
203211
while parseCharacter(",", from: &line, required: true, allowEscape: false, diagnoseIfNotFound: false, parseErrors: &parseErrors) {
204212
if let location = line.location {
205213
parseErrors.append(.unexpectedCharacter(",", location: location))
@@ -208,22 +216,33 @@ public struct DirectiveArgumentText: Equatable {
208216
_ = line.lex(untilCharacter: ",")
209217
continue
210218
}
211-
_ = parseCharacter(":", from: &line, required: true, allowEscape: false, diagnoseIfNotFound: true,
212-
parseErrors: &parseErrors)
213-
guard let value = parseLiteral(from: &line, parseErrors: &parseErrors) else {
214-
while parseCharacter(",", from: &line, required: true, allowEscape: false, diagnoseIfNotFound: false, parseErrors: &parseErrors) {
215-
if let location = line.location {
216-
parseErrors.append(.unexpectedCharacter(",", location: location))
219+
220+
// The first argument can be without a name.
221+
// An argument without a name must be followed by a "," or be the only argument. Otherwise the argument will be parsed as a named argument.
222+
if arguments.isEmpty && (line.isEmptyOrAllWhitespace || line.text.first == ",") {
223+
name = nil
224+
value = firstLiteral
225+
} else {
226+
_ = parseCharacter(":", from: &line, required: true, allowEscape: false, diagnoseIfNotFound: true, parseErrors: &parseErrors)
227+
228+
guard let secondLiteral = parseLiteral(from: &line, parseErrors: &parseErrors) else {
229+
while parseCharacter(",", from: &line, required: true, allowEscape: false, diagnoseIfNotFound: false, parseErrors: &parseErrors) {
230+
if let location = line.location {
231+
parseErrors.append(.unexpectedCharacter(",", location: location))
232+
}
217233
}
234+
_ = line.lex(untilCharacter: ",")
235+
continue
218236
}
219-
_ = line.lex(untilCharacter: ",")
220-
continue
237+
name = firstLiteral
238+
value = secondLiteral
221239
}
240+
222241
let nameRange: SourceRange?
223242
let valueRange: SourceRange?
224243

225244
if let lineLocation = line.location,
226-
let range = name.range {
245+
let range = name?.range {
227246
nameRange = SourceLocation(line: lineLocation.line, column: range.lowerBound.column, source: range.lowerBound.source)..<SourceLocation(line: lineLocation.line, column: range.upperBound.column, source: range.upperBound.source)
228247
} else {
229248
nameRange = nil
@@ -243,7 +262,7 @@ public struct DirectiveArgumentText: Equatable {
243262
diagnoseIfNotFound: false,
244263
parseErrors: &parseErrors)
245264

246-
let argument = DirectiveArgument(name: String(name.text),
265+
let argument = DirectiveArgument(name: String(name?.text ?? ""),
247266
nameRange: nameRange,
248267
value: String(value.text),
249268
valueRange: valueRange,
@@ -284,8 +303,11 @@ public struct DirectiveArgumentText: Equatable {
284303
/// Parse the line segments as name-value argument pairs separated by commas.
285304
///
286305
/// ```
287-
/// name-value-arguments -> name-value-argument name-value-arguments-rest
306+
/// arguments -> first-argument name-value-arguments-rest
307+
/// first-argument -> value-only-argument | name-value-argument
308+
/// value-only-argument -> literal
288309
/// name-value-argument -> literal : literal
310+
/// name-value-arguments -> name-value-argument name-value-arguments-rest
289311
/// name-value-arguments-rest -> , name-value-arguments | ε
290312
/// ```
291313
///
@@ -294,6 +316,8 @@ public struct DirectiveArgumentText: Equatable {
294316
/// - An argument-name pair is only recognized within a single line or line segment;
295317
/// that is, an argument cannot span multiple lines.
296318
/// - A comma is expected between name-value pairs.
319+
/// - The first argument can be unnamed. An unnamed argument will have an empty ``DirectiveArgument/name`` with no ``DirectiveArgument/nameRange``.
320+
///
297321
/// - Parameter parseErrors: an array to collect errors while parsing arguments.
298322
/// - Returns: an array of successfully parsed ``DirectiveArgument`` values.
299323
public func parseNameValueArguments(parseErrors: inout [ParseError]) -> [DirectiveArgument] {
@@ -326,8 +350,11 @@ public struct DirectiveArgumentText: Equatable {
326350
/// Parse the line segments as name-value argument pairs separated by commas.
327351
///
328352
/// ```
329-
/// name-value-arguments -> name-value-argument name-value-arguments-rest
353+
/// arguments -> first-argument name-value-arguments-rest
354+
/// first-argument -> value-only-argument | name-value-argument
355+
/// value-only-argument -> literal
330356
/// name-value-argument -> literal : literal
357+
/// name-value-arguments -> name-value-argument name-value-arguments-rest
331358
/// name-value-arguments-rest -> , name-value-arguments | ε
332359
/// ```
333360
///
@@ -336,6 +363,8 @@ public struct DirectiveArgumentText: Equatable {
336363
/// - An argument-name pair is only recognized within a single line or line segment;
337364
/// that is, an argument cannot span multiple lines.
338365
/// - A comma is expected between name-value pairs.
366+
/// - The first argument can be unnamed. An unnamed argument will have an empty ``DirectiveArgument/name`` with no ``DirectiveArgument/nameRange``.
367+
///
339368
/// - Returns: an array of successfully parsed ``DirectiveArgument`` values.
340369
///
341370
/// This overload discards parse errors.

Sources/Markdown/Markdown.docc/Markdown.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,5 @@ The markup tree provided by this package is comprised of immutable/persistent, t
3636

3737
- <doc:VisitMarkup>
3838
- <doc:FormatterAndOptions>
39+
40+
<!-- Copyright (c) 2021-2022 Apple Inc and the Swift Project authors. All Rights Reserved. -->

Sources/Markdown/Markdown.docc/Markdown/BlockDirectives.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,18 @@ You can parse argument text segments however you like. Swift Markdown also inclu
7373

7474
When using the name-value argument parser, this results in arguments `x` with value `1` and `y` with value `2`. Names and values are both strings; it's up to you to decide how to convert them into something more specific.
7575

76+
When using the name-value argument parser, the first argument can be unnamed for when the directive name also describes the purpose of the first argument. This parsed name-value pair will have an empty name. All other arguments have both names and values.
77+
7678
Here is the grammar of name-value argument syntax:
7779

7880
```
79-
Arguments -> Argument ArgumentsRest?
80-
ArgumentsRest -> , Arguments
81+
Arguments -> FirstArgument ArgumentsRest?
82+
ArgumentsRest -> , NamedArguments
83+
NamedArguments -> Argument ArgumentsRest?
84+
FirstArgument -> UnnamedArgument
85+
| Argument
8186
Argument -> Literal : Literal
87+
UnnamedArgument -> Literal
8288
Literal -> QuotedLiteral
8389
| UnquotedLiteral
8490
QuotedLiteral -> " QuotedLiteralContent "
@@ -171,4 +177,4 @@ for diagnostic in collector.diagnostics {
171177
}
172178
```
173179

174-
<!-- Copyright (c) 2021 Apple Inc and the Swift Project authors. All Rights Reserved. -->
180+
<!-- Copyright (c) 2021-2022 Apple Inc and the Swift Project authors. All Rights Reserved. -->

Tests/MarkdownTests/Parsing/BlockDirectiveParserTests.swift

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2021 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2022 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -394,7 +394,97 @@ class BlockDirectiveArgumentParserTests: XCTestCase {
394394
XCTAssertEqual(SourceLocation(line: 1, column: 11, source: nil)..<SourceLocation(line: 1, column: 16, source: nil), x.valueRange)
395395
}
396396
}
397+
398+
func testUnlabeledOnlyArgument() {
399+
let source = "@Outer(unlabeledArgumentValue)"
400+
401+
let document = Document(parsing: source, options: .parseBlockDirectives)
402+
403+
let directive = document.child(at: 0) as! BlockDirective
404+
XCTAssertEqual(1, directive.argumentText.segments.count)
405+
406+
var parseErrors = [DirectiveArgumentText.ParseError]()
407+
let arguments = directive.argumentText.parseNameValueArguments(parseErrors: &parseErrors)
408+
XCTAssertTrue(parseErrors.isEmpty)
409+
410+
XCTAssertEqual(1, arguments.count)
411+
412+
arguments[""].map { x in
413+
XCTAssertEqual("", x.name)
414+
XCTAssertEqual(nil, x.nameRange)
415+
XCTAssertEqual("unlabeledArgumentValue", x.value)
416+
XCTAssertEqual(SourceLocation(line: 1, column: 8, source: nil)..<SourceLocation(line: 1, column: 30, source: nil), x.valueRange)
417+
}
418+
}
419+
420+
func testUnlabeledQuotedOnlyArgument(){
421+
let source = "@Outer(\"Unlabeled argument value\")"
422+
423+
let document = Document(parsing: source, options: .parseBlockDirectives)
424+
425+
let directive = document.child(at: 0) as! BlockDirective
426+
XCTAssertEqual(1, directive.argumentText.segments.count)
397427

428+
var parseErrors = [DirectiveArgumentText.ParseError]()
429+
let arguments = directive.argumentText.parseNameValueArguments(parseErrors: &parseErrors)
430+
XCTAssertTrue(parseErrors.isEmpty)
431+
432+
XCTAssertEqual(1, arguments.count)
433+
434+
arguments[""].map { x in
435+
XCTAssertEqual("", x.name)
436+
XCTAssertEqual(nil, x.nameRange)
437+
XCTAssertEqual("Unlabeled argument value", x.value)
438+
XCTAssertEqual(SourceLocation(line: 1, column: 9, source: nil)..<SourceLocation(line: 1, column: 33, source: nil), x.valueRange)
439+
}
440+
}
441+
442+
func testFirstArgumentWithoutName() {
443+
let source = "@Outer(unlabeledArgumentValue, label: value)"
444+
445+
let document = Document(parsing: source, options: .parseBlockDirectives)
446+
447+
let directive = document.child(at: 0) as! BlockDirective
448+
XCTAssertEqual(1, directive.argumentText.segments.count)
449+
450+
var parseErrors = [DirectiveArgumentText.ParseError]()
451+
let arguments = directive.argumentText.parseNameValueArguments(parseErrors: &parseErrors)
452+
XCTAssertTrue(parseErrors.isEmpty)
453+
454+
XCTAssertEqual(2, arguments.count)
455+
456+
arguments[""].map { x in
457+
XCTAssertEqual("", x.name)
458+
XCTAssertEqual(nil, x.nameRange)
459+
XCTAssertEqual("unlabeledArgumentValue", x.value)
460+
XCTAssertEqual(SourceLocation(line: 1, column: 8, source: nil)..<SourceLocation(line: 1, column: 30, source: nil), x.valueRange)
461+
}
462+
}
463+
464+
func testSecondArgumentWithoutName() throws {
465+
let source = "@Outer(label: value, unlabeledArgumentValue)"
466+
467+
let document = Document(parsing: source, options: .parseBlockDirectives)
468+
469+
let directive = document.child(at: 0) as! BlockDirective
470+
XCTAssertEqual(1, directive.argumentText.segments.count)
471+
472+
var parseErrors = [DirectiveArgumentText.ParseError]()
473+
let arguments = directive.argumentText.parseNameValueArguments(parseErrors: &parseErrors)
474+
XCTAssertEqual(parseErrors.count, 1)
475+
476+
XCTAssertEqual([.missingExpectedCharacter(":", location: .init(line: 1, column: 44, source: nil))], parseErrors)
477+
478+
XCTAssertEqual(1, arguments.count)
479+
480+
arguments["label"].map { x in
481+
XCTAssertEqual("label", x.name)
482+
XCTAssertEqual(SourceLocation(line: 1, column: 8, source: nil)..<SourceLocation(line: 1, column: 13, source: nil), x.nameRange)
483+
XCTAssertEqual("value", x.value)
484+
XCTAssertEqual(SourceLocation(line: 1, column: 15, source: nil)..<SourceLocation(line: 1, column: 20, source: nil), x.valueRange)
485+
}
486+
}
487+
398488
func testRangeAdjustment() {
399489
let source = """
400490
@Outer {

bin/check-source

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
1818

1919
function replace_acceptable_years() {
2020
# this needs to replace all acceptable forms with 'YEARS'
21-
sed -e 's/20[12][78901]-20[12][8901]/YEARS/' -e 's/20[12][8901]/YEARS/'
21+
sed -e 's/20[12][78901]-20[12][89012]/YEARS/' -e 's/20[12][89012]/YEARS/'
2222
}
2323

2424
printf "=> Checking for unacceptable language… "

0 commit comments

Comments
 (0)