Skip to content

Commit 0b59ad6

Browse files
Kyle-Yed-ronnqvist
andauthored
Fix Directive argument name and value ranges on non-first line (#79)
* Remove unnecessary optional check * Fix and add test case for single line block parse * Add test for various combinations of directive argument location * Initialize LineSegment with the text of only one line Also, update test to verify emoji ranges * Update documentation for `LineSegment.untrimmedText` to reflect its use Also deprecate `LineSegment.lineStartIndex` Also remove `lineStartIndex` parameter from internal `LineSegment` init * Remove custom `==(_:_:)` that's same as synthesized implementation * Update LICENSE year and code format --------- Co-authored-by: David Rönnqvist <[email protected]>
1 parent 6f5d869 commit 0b59ad6

File tree

4 files changed

+215
-40
lines changed

4 files changed

+215
-40
lines changed

Sources/Markdown/Base/DirectiveArgument.swift

Lines changed: 12 additions & 21 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-2022 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2023 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
@@ -43,11 +43,14 @@ public struct DirectiveArgumentText: Equatable {
4343

4444
/// A segment of a line of argument text.
4545
public struct LineSegment: Equatable {
46-
/// The segment's untrimmed text from which arguments can be parsed.
46+
/// The original untrimmed text of the line, from which arguments can be parsed.
4747
public var untrimmedText: String
4848

49-
/// The index in ``untrimmedText`` where the line started.
50-
public var lineStartIndex: String.Index
49+
@available(*, deprecated, renamed: "untrimmedText.startIndex")
50+
public var lineStartIndex: String.Index {
51+
get { untrimmedText.startIndex }
52+
set { }
53+
}
5154

5255
/// The index from which parsing should start.
5356
public var parseIndex: String.Index
@@ -64,28 +67,15 @@ public struct DirectiveArgumentText: Equatable {
6467
/// Create an argument line segment.
6568
/// - Parameters:
6669
/// - untrimmedText: the segment's untrimmed text from which arguments can be parsed.
67-
/// - lineStartIndex: the index in ``text`` where the line started.
68-
/// - parseIndex: index from which parsing should start.
70+
/// - parseIndex: The index from which parsing should start.
6971
/// - range: The range from which a segment was extracted from a line
7072
/// of source, or `nil` if the argument text was provided by other means.
71-
init(untrimmedText: String, lineStartIndex: String.Index, parseIndex: String.Index? = nil, range: SourceRange? = nil) {
73+
init(untrimmedText: String, parseIndex: String.Index? = nil, range: SourceRange? = nil) {
7274
self.untrimmedText = untrimmedText
73-
self.lineStartIndex = lineStartIndex
7475
self.parseIndex = parseIndex ?? untrimmedText.startIndex
7576
self.range = range
7677
}
7778

78-
/// Returns a Boolean value indicating whether two line segments are equal.
79-
/// - Parameter lhs: a line segment to compare
80-
/// - Parameter rhs: another line segment to compare
81-
/// - Returns: `true` if the two segments are equal.
82-
public static func ==(lhs: LineSegment, rhs: LineSegment) -> Bool {
83-
return lhs.untrimmedText == rhs.untrimmedText &&
84-
lhs.lineStartIndex == rhs.lineStartIndex &&
85-
lhs.parseIndex == rhs.parseIndex &&
86-
lhs.range == rhs.range
87-
}
88-
8979
/// Parse a quoted literal.
9080
///
9181
/// ```
@@ -201,7 +191,8 @@ public struct DirectiveArgumentText: Equatable {
201191
var line = TrimmedLine(untrimmedText[...],
202192
source: range?.lowerBound.source,
203193
lineNumber: range?.lowerBound.line,
204-
parseIndex: parseIndex)
194+
parseIndex: parseIndex
195+
)
205196
line.lexWhitespace()
206197
while !line.isEmptyOrAllWhitespace {
207198
let name: TrimmedLine.Lex?
@@ -283,7 +274,7 @@ public struct DirectiveArgumentText: Equatable {
283274
/// from a string.
284275
public init<S: StringProtocol>(_ string: S) {
285276
let text = String(string)
286-
self.segments = [LineSegment(untrimmedText: text, lineStartIndex: text.startIndex, range: nil)]
277+
self.segments = [LineSegment(untrimmedText: text, range: nil)]
287278
}
288279

289280
/// Create a body of argument text from a sequence of ``LineSegment`` elements.

Sources/Markdown/Block Nodes/Block Container Blocks/BlockDirective.swift

Lines changed: 1 addition & 2 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-2023 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
@@ -95,7 +95,6 @@ public extension BlockDirective {
9595
omittingEmptySubsequences: false).map { lineText -> DirectiveArgumentText.LineSegment in
9696
let untrimmedText = String(lineText)
9797
return DirectiveArgumentText.LineSegment(untrimmedText: untrimmedText,
98-
lineStartIndex: untrimmedText.startIndex,
9998
range: nil)
10099
} ?? []
101100
try! self.init(.blockDirective(name: name,

Sources/Markdown/Parser/BlockDirectiveParser.swift

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,6 @@ struct TrimmedLine {
282282
/// The original untrimmed text of the line.
283283
let untrimmedText: Substring
284284

285-
/// The starting parse index.
286-
let startParseIndex: Substring.Index
287-
288285
/// The current index a parser is looking at on a line.
289286
var parseIndex: Substring.Index
290287

@@ -303,13 +300,16 @@ struct TrimmedLine {
303300
}
304301
}
305302

306-
/// - parameter untrimmedText: ``untrimmedText``
303+
/// - Parameters:
304+
/// - untrimmedText: The original untrimmed text of the line.
305+
/// - source: The source file or resource from which the line came, or `nil` if no such file or resource can be identified.
306+
/// - lineNumber: The line number of this line in the source if known, starting with `0`.
307+
/// - parseIndex: The current index a parser is looking at on a line, or `nil` if a parser is looking at the start of the untrimmed text.
307308
init(_ untrimmedText: Substring, source: URL?, lineNumber: Int?, parseIndex: Substring.Index? = nil) {
308309
self.untrimmedText = untrimmedText
309310
self.source = source
310311
self.parseIndex = parseIndex ?? untrimmedText.startIndex
311312
self.lineNumber = lineNumber
312-
self.startParseIndex = self.parseIndex
313313
}
314314

315315
/// Return the UTF-8 source location of the parse index if the line
@@ -318,10 +318,7 @@ struct TrimmedLine {
318318
guard let lineNumber = lineNumber else {
319319
return nil
320320
}
321-
let startIndex = (self.lineNumber ?? 1) == 1
322-
? untrimmedText.startIndex
323-
: startParseIndex
324-
let alreadyParsedPrefix = untrimmedText[startIndex..<parseIndex]
321+
let alreadyParsedPrefix = untrimmedText[..<parseIndex]
325322
return .init(line: lineNumber, column: alreadyParsedPrefix.utf8.count + 1, source: source)
326323
}
327324

@@ -718,14 +715,30 @@ private enum ParseContainer: CustomStringConvertible {
718715
let children = children.flatMap {
719716
$0.convertToRawMarkup(ranges: &ranges, parent: self, options: options)
720717
}
721-
return [.blockDirective(name: String(pendingBlockDirective.name),
722-
nameLocation: pendingBlockDirective.atLocation,
723-
argumentText: DirectiveArgumentText(segments: pendingBlockDirective.argumentsText.map {
724-
let untrimmedText = String($0.text.base[$0.text.base.startIndex..<$0.text.endIndex])
725-
return DirectiveArgumentText.LineSegment(untrimmedText: untrimmedText, lineStartIndex: untrimmedText.startIndex, parseIndex: $0.text.startIndex, range: $0.range)
726-
}),
727-
parsedRange: pendingBlockDirective.atLocation..<pendingBlockDirective.endLocation,
728-
children)]
718+
return [
719+
.blockDirective(
720+
name: String(pendingBlockDirective.name),
721+
nameLocation: pendingBlockDirective.atLocation,
722+
argumentText: DirectiveArgumentText(segments: pendingBlockDirective.argumentsText.map {
723+
let base = $0.text.base
724+
let lineStartIndex: String.Index
725+
if let argumentRange = $0.range {
726+
// If the argument has a known source range, offset the column (number of UTF8 bytes) to find the start of the line.
727+
lineStartIndex = base.utf8.index($0.text.startIndex, offsetBy: 1 - argumentRange.lowerBound.column)
728+
} else if let newLineIndex = base[..<$0.text.startIndex].lastIndex(where: \.isNewline) {
729+
// Iterate backwards from the argument start index to find the the start of the line.
730+
lineStartIndex = base.utf8.index(after: newLineIndex)
731+
} else {
732+
lineStartIndex = base.startIndex
733+
}
734+
let parseIndex = base.utf8.index($0.text.startIndex, offsetBy: -base.utf8.distance(from: base.startIndex, to: lineStartIndex))
735+
let untrimmedLine = String(base[lineStartIndex ..< $0.text.endIndex])
736+
return DirectiveArgumentText.LineSegment(untrimmedText: untrimmedLine, parseIndex: parseIndex, range: $0.range)
737+
}),
738+
parsedRange: pendingBlockDirective.atLocation ..< pendingBlockDirective.endLocation,
739+
children
740+
),
741+
]
729742
case let .doxygenCommand(pendingDoxygenCommand, lines):
730743
let range = pendingDoxygenCommand.atLocation..<pendingDoxygenCommand.endLocation
731744
ranges.add(range)

Tests/MarkdownTests/Parsing/BlockDirectiveParserTests.swift

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,178 @@ class BlockDirectiveArgumentParserTests: XCTestCase {
10431043
"""
10441044
XCTAssertEqual(expected, documentation.debugDescription())
10451045
}
1046+
1047+
func testParsingDirectiveArgumentsWithWhitespaceBeforeDirective() throws {
1048+
struct ExpectedArgumentInfo {
1049+
var line: Int
1050+
let name: String
1051+
var nameRange: Range<Int>
1052+
let value: String
1053+
var valueRange: Range<Int>
1054+
}
1055+
1056+
func assertDirectiveArguments(
1057+
_ expectedArguments: ExpectedArgumentInfo...,
1058+
parsing content: String,
1059+
file: StaticString = #file,
1060+
line: UInt = #line
1061+
) throws {
1062+
func substring(with range: SourceRange) -> String {
1063+
let line = content.split(omittingEmptySubsequences: false, whereSeparator: \.isNewline)[range.lowerBound.line - 1]
1064+
let startIndex = line.utf8.index(line.utf8.startIndex, offsetBy: range.lowerBound.column - 1)
1065+
let endIndex = line.utf8.index(line.utf8.startIndex, offsetBy: range.upperBound.column - 1)
1066+
return String(line[startIndex ..< endIndex])
1067+
}
1068+
1069+
let source = URL(fileURLWithPath: "/test-file-location")
1070+
let document = Document(parsing: content, source: source, options: .parseBlockDirectives)
1071+
let directive = try XCTUnwrap(document.children.compactMap({ $0 as? BlockDirective }).first, file: file, line: line)
1072+
let arguments = directive.argumentText.parseNameValueArguments()
1073+
XCTAssertEqual(arguments.count, expectedArguments.count, file: file, line: line)
1074+
for expectedArgument in expectedArguments {
1075+
let argument = try XCTUnwrap(arguments[expectedArgument.name], file: file, line: line)
1076+
1077+
XCTAssertEqual(expectedArgument.name, argument.name, file: file, line: line)
1078+
XCTAssertEqual(
1079+
argument.nameRange,
1080+
SourceLocation(line: expectedArgument.line, column: expectedArgument.nameRange.lowerBound, source: source) ..< SourceLocation(line: expectedArgument.line, column: expectedArgument.nameRange.upperBound, source: source),
1081+
file: file,
1082+
line: line
1083+
)
1084+
XCTAssertEqual(expectedArgument.name, argument.nameRange.map(substring(with:)), file: file, line: line)
1085+
1086+
XCTAssertEqual(expectedArgument.value, argument.value, file: file, line: line)
1087+
XCTAssertEqual(
1088+
argument.valueRange,
1089+
SourceLocation(line: expectedArgument.line, column: expectedArgument.valueRange.lowerBound, source: source) ..< SourceLocation(line: expectedArgument.line, column: expectedArgument.valueRange.upperBound, source: source),
1090+
file: file,
1091+
line: line
1092+
)
1093+
XCTAssertEqual(expectedArgument.value, argument.valueRange.map(substring(with:)), file: file, line: line)
1094+
}
1095+
}
1096+
1097+
// One argument
1098+
1099+
try assertDirectiveArguments(
1100+
ExpectedArgumentInfo(line: 1, name: "firstArgument", nameRange: 16 ..< 29, value: "firstValue", valueRange: 31 ..< 41),
1101+
parsing: "@DirectiveName(firstArgument: firstValue)"
1102+
)
1103+
1104+
try assertDirectiveArguments(
1105+
ExpectedArgumentInfo(line: 2, name: "firstArgument", nameRange: 16 ..< 29, value: "firstValue", valueRange: 31 ..< 41),
1106+
parsing: """
1107+
1108+
@DirectiveName(firstArgument: firstValue)
1109+
"""
1110+
)
1111+
1112+
// Argument on single line
1113+
1114+
try assertDirectiveArguments(
1115+
ExpectedArgumentInfo(line: 1, name: "firstArgument", nameRange: 17 ..< 30, value: "firstValue", valueRange: 31 ..< 41),
1116+
ExpectedArgumentInfo(line: 1, name: "secondArgument", nameRange: 44 ..< 58, value: "secondValue", valueRange: 62 ..< 73),
1117+
parsing: "@DirectiveName( firstArgument:firstValue ,\tsecondArgument: \t secondValue)"
1118+
)
1119+
1120+
try assertDirectiveArguments(
1121+
ExpectedArgumentInfo(line: 2, name: "firstArgument", nameRange: 17 ..< 30, value: "firstValue", valueRange: 31 ..< 41),
1122+
ExpectedArgumentInfo(line: 2, name: "secondArgument", nameRange: 44 ..< 58, value: "secondValue", valueRange: 62 ..< 73),
1123+
parsing: """
1124+
1125+
@DirectiveName( firstArgument:firstValue ,\tsecondArgument: \t secondValue)
1126+
"""
1127+
)
1128+
1129+
try assertDirectiveArguments(
1130+
ExpectedArgumentInfo(line: 2, name: "firstArgument", nameRange: 19 ..< 32, value: "firstValue", valueRange: 33 ..< 43),
1131+
ExpectedArgumentInfo(line: 2, name: "secondArgument", nameRange: 46 ..< 60, value: "secondValue", valueRange: 64 ..< 75),
1132+
parsing: """
1133+
1134+
@DirectiveName( firstArgument:firstValue ,\tsecondArgument: \t secondValue)
1135+
"""
1136+
)
1137+
1138+
// Second argument on new line
1139+
1140+
try assertDirectiveArguments(
1141+
ExpectedArgumentInfo(line: 1, name: "firstArgument", nameRange: 17 ..< 30, value: "firstValue", valueRange: 31 ..< 41),
1142+
ExpectedArgumentInfo(line: 2, name: "secondArgument", nameRange: 16 ..< 30, value: "secondValue", valueRange: 34 ..< 45),
1143+
parsing: """
1144+
@DirectiveName( firstArgument:firstValue ,
1145+
secondArgument: \t secondValue)
1146+
"""
1147+
)
1148+
1149+
try assertDirectiveArguments(
1150+
ExpectedArgumentInfo(line: 2, name: "firstArgument", nameRange: 17 ..< 30, value: "firstValue", valueRange: 31 ..< 41),
1151+
ExpectedArgumentInfo(line: 3, name: "secondArgument", nameRange: 16 ..< 30, value: "secondValue", valueRange: 34 ..< 45),
1152+
parsing: """
1153+
1154+
@DirectiveName( firstArgument:firstValue ,
1155+
secondArgument: \t secondValue)
1156+
"""
1157+
)
1158+
1159+
try assertDirectiveArguments(
1160+
ExpectedArgumentInfo(line: 2, name: "firstArgument", nameRange: 19 ..< 32, value: "firstValue", valueRange: 33 ..< 43),
1161+
ExpectedArgumentInfo(line: 3, name: "secondArgument", nameRange: 18 ..< 32, value: "secondValue", valueRange: 36 ..< 47),
1162+
parsing: """
1163+
1164+
@DirectiveName( firstArgument:firstValue ,
1165+
secondArgument: \t secondValue)
1166+
"""
1167+
)
1168+
1169+
// Arguments on separate lines
1170+
1171+
try assertDirectiveArguments(
1172+
ExpectedArgumentInfo(line: 2, name: "firstArgument", nameRange: 3 ..< 16, value: "firstValue", valueRange: 17 ..< 27),
1173+
ExpectedArgumentInfo(line: 3, name: "secondArgument", nameRange: 2 ..< 16, value: "secondValue", valueRange: 20 ..< 31),
1174+
parsing: """
1175+
@DirectiveName(
1176+
firstArgument:firstValue ,
1177+
\tsecondArgument: \t secondValue
1178+
)
1179+
"""
1180+
)
1181+
1182+
try assertDirectiveArguments(
1183+
ExpectedArgumentInfo(line: 3, name: "firstArgument", nameRange: 3 ..< 16, value: "firstValue", valueRange: 17 ..< 27),
1184+
ExpectedArgumentInfo(line: 4, name: "secondArgument", nameRange: 2 ..< 16, value: "secondValue", valueRange: 20 ..< 31),
1185+
parsing: """
1186+
1187+
@DirectiveName(
1188+
firstArgument:firstValue ,
1189+
\tsecondArgument: \t secondValue
1190+
)
1191+
"""
1192+
)
1193+
1194+
try assertDirectiveArguments(
1195+
ExpectedArgumentInfo(line: 3, name: "firstArgument", nameRange: 5 ..< 18, value: "firstValue", valueRange: 19 ..< 29),
1196+
ExpectedArgumentInfo(line: 4, name: "secondArgument", nameRange: 4 ..< 18, value: "secondValue", valueRange: 22 ..< 33),
1197+
parsing: """
1198+
1199+
@DirectiveName(
1200+
firstArgument:firstValue ,
1201+
\tsecondArgument: \t secondValue
1202+
)
1203+
"""
1204+
)
1205+
1206+
// Content and directives with emoji
1207+
1208+
try assertDirectiveArguments(
1209+
ExpectedArgumentInfo(line: 3, name: "firstArgument", nameRange: 20 ..< 33, value: "first💻Value", valueRange: 35 ..< 49),
1210+
ExpectedArgumentInfo(line: 3, name: "secondArgument", nameRange: 51 ..< 65, value: "secondValue", valueRange: 67 ..< 78),
1211+
parsing: """
1212+
Paragraph before with emoji: 💻
1213+
1214+
@Directive💻Name(firstArgument: first💻Value, secondArgument: secondValue)
1215+
"""
1216+
)
1217+
}
10461218

10471219
// FIXME: swift-testing macro for specifying the relationship between a bug and a test
10481220
// Uncomment the following code when we integrate swift-testing

0 commit comments

Comments
 (0)