Skip to content

Commit 8d87440

Browse files
author
Jesse Haigh
committed
change parsing to handle values after = and arrays
1 parent 04319de commit 8d87440

File tree

6 files changed

+136
-63
lines changed

6 files changed

+136
-63
lines changed

Sources/SwiftDocC/Checker/Checkers/InvalidCodeBlockOption.swift

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,33 +31,29 @@ public struct InvalidCodeBlockOption: Checker {
3131
}
3232

3333
public mutating func visitCodeBlock(_ codeBlock: CodeBlock) {
34-
let info = codeBlock.language?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
35-
guard !info.isEmpty else { return }
34+
let (lang, tokens) = tokenizeLanguageString(codeBlock.language)
3635

37-
// TODO this will also fail on parsing highlight values with commas inside the array
38-
let tokens = info
39-
.split(separator: ",")
40-
.map { $0.trimmingCharacters(in: .whitespaces) }
41-
.filter { !$0.isEmpty }
36+
func matches(token: RenderBlockContent.CodeListing.OptionName, value: String?) {
37+
guard token == .unknown, let value = value else { return }
4238

43-
guard !tokens.isEmpty else { return }
44-
45-
for token in tokens {
46-
// if the token is an exact match, we don't need to do anything
47-
guard !knownOptions.contains(token) else { continue }
48-
49-
let matches = NearMiss.bestMatches(for: knownOptions, against: token)
39+
let matches = NearMiss.bestMatches(for: knownOptions, against: value)
5040

5141
if !matches.isEmpty {
52-
let diagnostic = Diagnostic(source: sourceFile, severity: .warning, range: codeBlock.range, identifier: "org.swift.docc.InvalidCodeBlockOption", summary: "Unknown option \(token.singleQuoted) in code block.")
42+
let diagnostic = Diagnostic(source: sourceFile, severity: .warning, range: codeBlock.range, identifier: "org.swift.docc.InvalidCodeBlockOption", summary: "Unknown option \(value.singleQuoted) in code block.")
5343
let possibleSolutions = matches.map { candidate in
5444
Solution(
55-
summary: "Replace \(token.singleQuoted) with \(candidate.singleQuoted).",
45+
summary: "Replace \(value.singleQuoted) with \(candidate.singleQuoted).",
5646
replacements: []
5747
)
5848
}
5949
problems.append(Problem(diagnostic: diagnostic, possibleSolutions: possibleSolutions))
6050
}
6151
}
52+
53+
for (token, value) in tokens {
54+
matches(token: token, value: value)
55+
}
56+
// check if first token (lang) might be a typo
57+
matches(token: .unknown, value: lang)
6258
}
6359
}

Sources/SwiftDocC/Model/Rendering/Content/RenderBlockContent.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ public enum RenderBlockContent: Equatable {
132132
case nocopy
133133
case wrap
134134
case highlight
135+
case unknown
135136

136137
init?<S: StringProtocol>(caseInsensitive raw: S) {
137138
self.init(rawValue: raw.lowercased())

Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift

Lines changed: 5 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -49,51 +49,10 @@ struct RenderContentCompiler: MarkupVisitor {
4949
// Default to the bundle's code listing syntax if one is not explicitly declared in the code block.
5050

5151
if FeatureFlags.current.isExperimentalCodeBlockEnabled {
52-
53-
func parseLanguageString(_ input: String?) -> (lang: String? , tokens: [(RenderBlockContent.CodeListing.OptionName, Substring?)]) {
54-
guard let input else { return (lang: nil, tokens: []) }
55-
// TODO this fails on parsing highlight values with commas inside the array
56-
let parts = input
57-
.split(separator: ",")
58-
.map { $0.trimmingCharacters(in: .whitespaces) }
59-
var lang: String? = nil
60-
var options: [(RenderBlockContent.CodeListing.OptionName, Substring?)] = []
61-
62-
for part in parts {
63-
if let eq = part.firstIndex(of: "=") {
64-
let name = part[..<eq].trimmingCharacters(in: .whitespaces)
65-
let value = part[part.index(after: eq)...]
66-
if let option = RenderBlockContent.CodeListing.OptionName(caseInsensitive: name) {
67-
options.append((option, value))
68-
} else if lang == nil {
69-
lang = String(part)
70-
}
71-
} else {
72-
if let option = RenderBlockContent.CodeListing.OptionName(caseInsensitive: part) {
73-
options.append((option, nil))
74-
} else if lang == nil {
75-
lang = String(part)
76-
}
77-
}
78-
}
79-
return (lang, options)
80-
}
81-
82-
func parseHighlight(_ value: Substring?) -> [Int]? {
83-
guard var s = value.map(String.init) else { return nil }
84-
s = s.trimmingCharacters(in: .whitespaces)
85-
if s.hasPrefix("[") && s.hasSuffix("]") {
86-
s.removeFirst()
87-
s.removeLast()
88-
}
89-
let ints = s.split(separator: ",").compactMap{ Int($0.trimmingCharacters(in: .whitespaces)) }
90-
return ints.isEmpty ? nil : ints
91-
}
92-
93-
let options = parseLanguageString(codeBlock.language)
52+
let (lang, tokens) = tokenizeLanguageString(codeBlock.language)
9453

9554
var listing = RenderBlockContent.CodeListing(
96-
syntax: options.lang ?? bundle.info.defaultCodeListingLanguage,
55+
syntax: lang ?? bundle.info.defaultCodeListingLanguage,
9756
code: codeBlock.code.splitByNewlines,
9857
metadata: nil,
9958
copyToClipboard: true, // default value
@@ -102,7 +61,7 @@ struct RenderContentCompiler: MarkupVisitor {
10261
)
10362

10463
// apply code block options
105-
for (option, value) in options.tokens {
64+
for (option, value) in tokens {
10665
switch option {
10766
case .nocopy:
10867
listing.copyToClipboard = false
@@ -114,6 +73,8 @@ struct RenderContentCompiler: MarkupVisitor {
11473
}
11574
case .highlight:
11675
listing.highlight = parseHighlight(value) ?? []
76+
case .unknown:
77+
break
11778
}
11879
}
11980

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2025 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
public func parseHighlight(_ value: String?) -> [Int]? {
12+
guard var s = value?.trimmingCharacters(in: .whitespaces), !s.isEmpty else { return [] }
13+
14+
if s.hasPrefix("[") && s.hasSuffix("]") {
15+
s.removeFirst()
16+
s.removeLast()
17+
}
18+
19+
return s.split(separator: ",").compactMap { Int($0.trimmingCharacters(in: .whitespaces)) }
20+
}
21+
22+
/// A function that parses the language line options on code blocks, returning the language and tokens, an array of OptionName and option values
23+
public func tokenizeLanguageString(_ input: String?) -> (lang: String?, tokens: [(RenderBlockContent.CodeListing.OptionName, String?)]) {
24+
guard let input else { return (lang: nil, tokens: []) }
25+
26+
let parts = parseLanguageString(input)
27+
var tokens: [(RenderBlockContent.CodeListing.OptionName, String?)] = []
28+
var lang: String? = nil
29+
30+
for (index, part) in parts.enumerated() {
31+
if let eq = part.firstIndex(of: "=") {
32+
let key = part[..<eq].trimmingCharacters(in: .whitespaces).lowercased()
33+
let value = part[part.index(after: eq)...].trimmingCharacters(in: .whitespaces)
34+
if key == "wrap" {
35+
tokens.append((.wrap, value))
36+
} else if key == "highlight" {
37+
tokens.append((.highlight, value))
38+
} else {
39+
tokens.append((.unknown, key))
40+
}
41+
} else {
42+
let key = part.trimmingCharacters(in: .whitespaces).lowercased()
43+
if key == "nocopy" {
44+
tokens.append((.nocopy, nil as String?))
45+
} else if key == "wrap" {
46+
tokens.append((.wrap, nil as String?))
47+
} else if key == "highlight" {
48+
tokens.append((.highlight, nil as String?))
49+
} else if index == 0 && !key.contains("[") && !key.contains("]") {
50+
lang = key
51+
} else {
52+
tokens.append((.unknown, key))
53+
}
54+
}
55+
}
56+
return (lang, tokens)
57+
}
58+
59+
func parseLanguageString(_ input: String?) -> [Substring] {
60+
61+
guard let input else { return [] }
62+
var parts: [Substring] = []
63+
var start = input.startIndex
64+
var i = input.startIndex
65+
66+
var bracketDepth = 0
67+
68+
while i < input.endIndex {
69+
let c = input[i]
70+
71+
if c == "[" { bracketDepth += 1 }
72+
else if c == "]" { bracketDepth = max(0, bracketDepth - 1) }
73+
else if c == "," && bracketDepth == 0 {
74+
let seq = input[start..<i]
75+
if !seq.isEmpty {
76+
parts.append(seq)
77+
}
78+
input.formIndex(after: &i)
79+
start = i
80+
continue
81+
}
82+
input.formIndex(after: &i)
83+
}
84+
let tail = input[start..<input.endIndex]
85+
if !tail.isEmpty {
86+
parts.append(tail)
87+
}
88+
89+
return parts
90+
}

Tests/SwiftDocCTests/Checker/Checkers/InvalidCodeBlockOptionTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ let a = 1
2424
var checker = InvalidCodeBlockOption(sourceFile: nil)
2525
checker.visit(document)
2626
XCTAssertTrue(checker.problems.isEmpty)
27-
XCTAssertEqual(RenderBlockContent.CodeListing.knownOptions, ["highlight", "nocopy", "wrap"])
27+
XCTAssertEqual(RenderBlockContent.CodeListing.knownOptions, ["highlight", "nocopy", "unknown", "wrap"])
2828
}
2929

3030
func testOption() {
@@ -67,7 +67,7 @@ let c = 3
6767
let d = 4
6868
```
6969
70-
```unknown, nocpoy
70+
```haskell, nocpoy
7171
let e = 5
7272
```
7373

Tests/SwiftDocCTests/Rendering/RenderContentCompilerTests.swift

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,31 @@ class RenderContentCompilerTests: XCTestCase {
272272
XCTAssertEqual(codeListing.copyToClipboard, false)
273273
}
274274

275+
func testCopyToClipboardNoLang() async throws {
276+
enableFeatureFlag(\.isExperimentalCodeBlockEnabled)
277+
278+
let (bundle, context) = try await testBundleAndContext()
279+
var compiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift))
280+
281+
let source = #"""
282+
```nocopy
283+
let x = 1
284+
```
285+
"""#
286+
let document = Document(parsing: source)
287+
288+
let result = document.children.flatMap { compiler.visit($0) }
289+
290+
let renderCodeBlock = try XCTUnwrap(result[0] as? RenderBlockContent)
291+
guard case let .codeListing(codeListing) = renderCodeBlock else {
292+
XCTFail("Expected RenderBlockContent.codeListing")
293+
return
294+
}
295+
296+
XCTAssertEqual(codeListing.syntax, nil)
297+
XCTAssertEqual(codeListing.copyToClipboard, false)
298+
}
299+
275300
func testCopyToClipboardNoFeatureFlag() async throws {
276301
let (bundle, context) = try await testBundleAndContext()
277302
var compiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift))
@@ -432,6 +457,6 @@ class RenderContentCompilerTests: XCTestCase {
432457
}
433458

434459
XCTAssertEqual(codeListing.syntax, "swift")
435-
//XCTAssertEqual(codeListing.highlight, [1, 2, 3])
460+
XCTAssertEqual(codeListing.highlight, [1, 2, 3])
436461
}
437462
}

0 commit comments

Comments
 (0)