Skip to content

Commit e1ac76a

Browse files
author
Jesse Haigh
committed
PR feedback
1 parent 9026302 commit e1ac76a

File tree

10 files changed

+215
-183
lines changed

10 files changed

+215
-183
lines changed

Sources/SwiftDocC/Checker/Checkers/InvalidCodeBlockOption.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public struct InvalidCodeBlockOption: Checker {
1919
public var problems = [Problem]()
2020

2121
/// Parsing options for code blocks
22-
private let knownOptions = RenderBlockContent.CodeListing.knownOptions
22+
private let knownOptions = RenderBlockContent.CodeBlockOptions.knownOptions
2323

2424
private var sourceFile: URL?
2525

@@ -31,9 +31,9 @@ public struct InvalidCodeBlockOption: Checker {
3131
}
3232

3333
public mutating func visitCodeBlock(_ codeBlock: CodeBlock) {
34-
let (lang, tokens) = tokenizeLanguageString(codeBlock.language)
34+
let (lang, tokens) = RenderBlockContent.CodeBlockOptions.tokenizeLanguageString(codeBlock.language)
3535

36-
func matches(token: RenderBlockContent.CodeListing.OptionName, value: String?) {
36+
func matches(token: RenderBlockContent.CodeBlockOptions.OptionName, value: String?) {
3737
guard token == .unknown, let value = value else { return }
3838

3939
let matches = NearMiss.bestMatches(for: knownOptions, against: value)
@@ -58,12 +58,12 @@ public struct InvalidCodeBlockOption: Checker {
5858
}
5959
}
6060

61-
func validateArrayIndices(token: RenderBlockContent.CodeListing.OptionName, value: String?) {
61+
func validateArrayIndices(token: RenderBlockContent.CodeBlockOptions.OptionName, value: String?) {
6262
guard token == .highlight || token == .strikeout, let value = value else { return }
6363
// code property ends in a newline. this gives us a bogus extra line.
6464
let lineCount: Int = codeBlock.code.split(omittingEmptySubsequences: false, whereSeparator: { $0.isNewline }).count - 1
6565

66-
guard let indices = parseCodeBlockOptionArray(value) else {
66+
guard let indices = RenderBlockContent.CodeBlockOptions.parseCodeBlockOptionsArray(value) else {
6767
let diagnostic = Diagnostic(source: sourceFile, severity: .warning, range: codeBlock.range, identifier: "org.swift.docc.InvalidCodeBlockOption", summary: "Could not parse \(token.rawValue.singleQuoted) indices from \(value.singleQuoted). Expected an integer (e.g. 3) or an array (e.g. [1, 3, 5])")
6868
problems.append(Problem(diagnostic: diagnostic, possibleSolutions: []))
6969
return

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

Lines changed: 164 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -124,21 +124,36 @@ public enum RenderBlockContent: Equatable {
124124
public var code: [String]
125125
/// Additional metadata for this code block.
126126
public var metadata: RenderContentMetadata?
127+
/// Annotations for code blocks
128+
public var options: CodeBlockOptions?
129+
130+
/// Make a new `CodeListing` with the given data.
131+
public init(syntax: String?, code: [String], metadata: RenderContentMetadata?, options: CodeBlockOptions?) {
132+
self.syntax = syntax
133+
self.code = code
134+
self.metadata = metadata
135+
self.options = options
136+
}
137+
}
138+
139+
public struct CodeBlockOptions: Equatable {
140+
public var language: String?
127141
public var copyToClipboard: Bool = true
128-
public var wrap: Int = 100
142+
public var wrap: Int = 0
129143
public var highlight: [Int] = [Int]()
130144
public var showLineNumbers: Bool = false
131145
public var strikeout: [Int] = [Int]()
132146

133147
public enum OptionName: String, CaseIterable {
148+
case _nonFrozenEnum_useDefaultCase
134149
case nocopy
135150
case wrap
136151
case highlight
137152
case showLineNumbers
138153
case strikeout
139154
case unknown
140155

141-
init?<S: StringProtocol>(caseInsensitive raw: S) {
156+
init?(caseInsensitive raw: some StringProtocol) {
142157
self.init(rawValue: raw.lowercased())
143158
}
144159
}
@@ -147,17 +162,141 @@ public enum RenderBlockContent: Equatable {
147162
Set(OptionName.allCases.map(\.rawValue))
148163
}
149164

150-
/// Make a new `CodeListing` with the given data.
151-
public init(syntax: String?, code: [String], metadata: RenderContentMetadata?, copyToClipboard: Bool, wrap: Int, highlight: [Int], strikeout: [Int], showLineNumbers: Bool) {
152-
self.syntax = syntax
153-
self.code = code
154-
self.metadata = metadata
165+
// empty initializer with default values
166+
public init() {
167+
self.language = ""
168+
self.copyToClipboard = true
169+
self.wrap = 0
170+
self.highlight = [Int]()
171+
self.showLineNumbers = false
172+
self.strikeout = [Int]()
173+
}
174+
175+
public init(parsingLanguageString language: String?) {
176+
let (lang, tokens) = Self.tokenizeLanguageString(language)
177+
178+
self.language = lang
179+
self.copyToClipboard = !tokens.contains { $0.name == .nocopy }
180+
self.showLineNumbers = tokens.contains { $0.name == .showLineNumbers }
181+
182+
if let wrapString = tokens.first(where: { $0.name == .wrap })?.value,
183+
let wrapValue = Int(wrapString) {
184+
self.wrap = wrapValue
185+
} else {
186+
self.wrap = 0
187+
}
188+
189+
if let highlightString = tokens.first(where: { $0.name == .highlight })?.value,
190+
let highlightValue = Self.parseCodeBlockOptionsArray(highlightString) {
191+
self.highlight = highlightValue
192+
} else {
193+
self.highlight = []
194+
}
195+
196+
if let strikeoutString = tokens.first(where: { $0.name == .strikeout })?.value,
197+
let strikeoutValue = Self.parseCodeBlockOptionsArray(strikeoutString) {
198+
self.strikeout = strikeoutValue
199+
} else {
200+
self.strikeout = []
201+
}
202+
}
203+
204+
public init(copyToClipboard: Bool, wrap: Int, highlight: [Int], strikeout: [Int], showLineNumbers: Bool) {
155205
self.copyToClipboard = copyToClipboard
156206
self.wrap = wrap
157207
self.highlight = highlight
158208
self.showLineNumbers = showLineNumbers
159209
self.strikeout = strikeout
160210
}
211+
212+
/// A function that parses array values on code block options from the language line string
213+
static internal func parseCodeBlockOptionsArray(_ value: String?) -> [Int]? {
214+
guard var s = value?.trimmingCharacters(in: .whitespaces), !s.isEmpty else { return [] }
215+
216+
if s.hasPrefix("[") && s.hasSuffix("]") {
217+
s.removeFirst()
218+
s.removeLast()
219+
}
220+
221+
return s.split(separator: ",").compactMap { Int($0.trimmingCharacters(in: .whitespaces)) }
222+
}
223+
224+
/// A function that parses the language line options on code blocks, returning the language and tokens, an array of OptionName and option values
225+
static internal func tokenizeLanguageString(_ input: String?) -> (lang: String?, tokens: [(name: OptionName, value: String?)]) {
226+
guard let input else { return (lang: nil, tokens: []) }
227+
228+
let parts = parseLanguageString(input)
229+
var tokens: [(OptionName, String?)] = []
230+
var lang: String? = nil
231+
232+
for (index, part) in parts.enumerated() {
233+
if let eq = part.firstIndex(of: "=") {
234+
let key = part[..<eq].trimmingCharacters(in: .whitespaces).lowercased()
235+
let value = part[part.index(after: eq)...].trimmingCharacters(in: .whitespaces)
236+
if key == "wrap" {
237+
tokens.append((.wrap, value))
238+
} else if key == "highlight" {
239+
tokens.append((.highlight, value))
240+
} else if key == "strikeout" {
241+
tokens.append((.strikeout, value))
242+
} else {
243+
tokens.append((.unknown, key))
244+
}
245+
} else {
246+
let key = part.trimmingCharacters(in: .whitespaces).lowercased()
247+
if key == "nocopy" {
248+
tokens.append((.nocopy, nil as String?))
249+
} else if key == "showlinenumbers" {
250+
tokens.append((.showLineNumbers, nil as String?))
251+
} else if key == "wrap" {
252+
tokens.append((.wrap, nil as String?))
253+
} else if key == "highlight" {
254+
tokens.append((.highlight, nil as String?))
255+
} else if key == "strikeout" {
256+
tokens.append((.strikeout, nil as String?))
257+
} else if index == 0 && !key.contains("[") && !key.contains("]") {
258+
lang = key
259+
} else {
260+
tokens.append((.unknown, key))
261+
}
262+
}
263+
}
264+
return (lang, tokens)
265+
}
266+
267+
// helper function for tokenizeLanguageString to parse the language line
268+
static func parseLanguageString(_ input: String?) -> [Substring] {
269+
270+
guard let input else { return [] }
271+
var parts: [Substring] = []
272+
var start = input.startIndex
273+
var i = input.startIndex
274+
275+
var bracketDepth = 0
276+
277+
while i < input.endIndex {
278+
let c = input[i]
279+
280+
if c == "[" { bracketDepth += 1 }
281+
else if c == "]" { bracketDepth = max(0, bracketDepth - 1) }
282+
else if c == "," && bracketDepth == 0 {
283+
let seq = input[start..<i]
284+
if !seq.isEmpty {
285+
parts.append(seq)
286+
}
287+
input.formIndex(after: &i)
288+
start = i
289+
continue
290+
}
291+
input.formIndex(after: &i)
292+
}
293+
let tail = input[start..<input.endIndex]
294+
if !tail.isEmpty {
295+
parts.append(tail)
296+
}
297+
298+
return parts
299+
}
161300
}
162301

163302
/// A heading with the given level.
@@ -747,15 +886,23 @@ extension RenderBlockContent: Codable {
747886
self = try .aside(.init(style: style, content: container.decode([RenderBlockContent].self, forKey: .content)))
748887
case .codeListing:
749888
let copy = FeatureFlags.current.isExperimentalCodeBlockEnabled
889+
let options: CodeBlockOptions?
890+
if !Set(container.allKeys).isDisjoint(with: [.copyToClipboard, .wrap, .highlight, .strikeout, .showLineNumbers]) {
891+
options = try CodeBlockOptions(
892+
copyToClipboard: container.decodeIfPresent(Bool.self, forKey: .copyToClipboard) ?? copy,
893+
wrap: container.decodeIfPresent(Int.self, forKey: .wrap) ?? 0,
894+
highlight: container.decodeIfPresent([Int].self, forKey: .highlight) ?? [],
895+
strikeout: container.decodeIfPresent([Int].self, forKey: .strikeout) ?? [],
896+
showLineNumbers: container.decodeIfPresent(Bool.self, forKey: .showLineNumbers) ?? false
897+
)
898+
} else {
899+
options = nil
900+
}
750901
self = try .codeListing(.init(
751902
syntax: container.decodeIfPresent(String.self, forKey: .syntax),
752903
code: container.decode([String].self, forKey: .code),
753904
metadata: container.decodeIfPresent(RenderContentMetadata.self, forKey: .metadata),
754-
copyToClipboard: container.decodeIfPresent(Bool.self, forKey: .copyToClipboard) ?? copy,
755-
wrap: container.decodeIfPresent(Int.self, forKey: .wrap) ?? 0,
756-
highlight: container.decodeIfPresent([Int].self, forKey: .highlight) ?? [Int](),
757-
strikeout: container.decodeIfPresent([Int].self, forKey: .strikeout) ?? [Int](),
758-
showLineNumbers: container.decodeIfPresent(Bool.self, forKey: .showLineNumbers) ?? false
905+
options: options
759906
))
760907
case .heading:
761908
self = try .heading(.init(level: container.decode(Int.self, forKey: .level), text: container.decode(String.self, forKey: .text), anchor: container.decodeIfPresent(String.self, forKey: .anchor)))
@@ -859,11 +1006,11 @@ extension RenderBlockContent: Codable {
8591006
try container.encode(l.syntax, forKey: .syntax)
8601007
try container.encode(l.code, forKey: .code)
8611008
try container.encodeIfPresent(l.metadata, forKey: .metadata)
862-
try container.encode(l.copyToClipboard, forKey: .copyToClipboard)
863-
try container.encode(l.wrap, forKey: .wrap)
864-
try container.encode(l.highlight, forKey: .highlight)
865-
try container.encode(l.strikeout, forKey: .strikeout)
866-
try container.encode(l.showLineNumbers, forKey: .showLineNumbers)
1009+
try container.encodeIfPresent(l.options?.copyToClipboard, forKey: .copyToClipboard)
1010+
try container.encodeIfPresent(l.options?.wrap, forKey: .wrap)
1011+
try container.encodeIfPresent(l.options?.highlight, forKey: .highlight)
1012+
try container.encodeIfPresent(l.options?.strikeout, forKey: .strikeout)
1013+
try container.encodeIfPresent(l.options?.showLineNumbers, forKey: .showLineNumbers)
8671014
case .heading(let h):
8681015
try container.encode(h.level, forKey: .level)
8691016
try container.encode(h.text, forKey: .text)

Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -49,45 +49,18 @@ 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-
let (lang, tokens) = tokenizeLanguageString(codeBlock.language)
53-
54-
var listing = RenderBlockContent.CodeListing(
55-
syntax: lang ?? bundle.info.defaultCodeListingLanguage,
52+
let codeBlockOptions = RenderBlockContent.CodeBlockOptions(parsingLanguageString: codeBlock.language)
53+
let listing = RenderBlockContent.CodeListing(
54+
syntax: codeBlockOptions.language ?? bundle.info.defaultCodeListingLanguage,
5655
code: codeBlock.code.splitByNewlines,
5756
metadata: nil,
58-
copyToClipboard: true, // default value
59-
wrap: 0, // default value
60-
highlight: [Int](), // default value
61-
strikeout: [Int](), // default value
62-
showLineNumbers: false // default value
57+
options: codeBlockOptions
6358
)
6459

65-
// apply code block options
66-
for (option, value) in tokens {
67-
switch option {
68-
case .nocopy:
69-
listing.copyToClipboard = false
70-
case .wrap:
71-
if let value, let intValue = Int(value) {
72-
listing.wrap = intValue
73-
} else {
74-
listing.wrap = 0
75-
}
76-
case .highlight:
77-
listing.highlight = parseCodeBlockOptionArray(value) ?? []
78-
case .strikeout:
79-
listing.strikeout = parseCodeBlockOptionArray(value) ?? []
80-
case .showLineNumbers:
81-
listing.showLineNumbers = true
82-
case .unknown:
83-
break
84-
}
85-
}
86-
8760
return [RenderBlockContent.codeListing(listing)]
8861

8962
} else {
90-
return [RenderBlockContent.codeListing(.init(syntax: codeBlock.language ?? bundle.info.defaultCodeListingLanguage, code: codeBlock.code.splitByNewlines, metadata: nil, copyToClipboard: false, wrap: 0, highlight: [Int](), strikeout: [Int](), showLineNumbers: false))]
63+
return [RenderBlockContent.codeListing(.init(syntax: codeBlock.language ?? bundle.info.defaultCodeListingLanguage, code: codeBlock.code.splitByNewlines, metadata: nil, options: nil))]
9164
}
9265
}
9366

Sources/SwiftDocC/Semantics/Snippets/Snippet.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,23 @@ extension Snippet: RenderableDirectiveConvertible {
8383
let lines = snippetMixin.lines[lineRange]
8484
let minimumIndentation = lines.map { $0.prefix { $0.isWhitespace }.count }.min() ?? 0
8585
let trimmedLines = lines.map { String($0.dropFirst(minimumIndentation)) }
86-
let copy = FeatureFlags.current.isExperimentalCodeBlockEnabled
87-
return [RenderBlockContent.codeListing(.init(syntax: snippetMixin.language, code: trimmedLines, metadata: nil, copyToClipboard: copy, wrap: 0, highlight: [Int](), strikeout: [Int](), showLineNumbers: false))]
86+
let codeBlockOptions: RenderBlockContent.CodeBlockOptions?
87+
if FeatureFlags.current.isExperimentalCodeBlockEnabled {
88+
codeBlockOptions = .init()
89+
} else {
90+
codeBlockOptions = nil
91+
}
92+
return [RenderBlockContent.codeListing(.init(syntax: snippetMixin.language, code: trimmedLines, metadata: nil, options: codeBlockOptions))]
8893
} else {
8994
// Render the whole snippet with its explanation content.
9095
let docCommentContent = snippetEntity.markup.children.flatMap { contentCompiler.visit($0) }
91-
let copy = FeatureFlags.current.isExperimentalCodeBlockEnabled
92-
let code = RenderBlockContent.codeListing(.init(syntax: snippetMixin.language, code: snippetMixin.lines, metadata: nil, copyToClipboard: copy, wrap: 0, highlight: [Int](), strikeout: [Int](), showLineNumbers: false))
96+
let codeBlockOptions: RenderBlockContent.CodeBlockOptions?
97+
if FeatureFlags.current.isExperimentalCodeBlockEnabled {
98+
codeBlockOptions = .init()
99+
} else {
100+
codeBlockOptions = nil
101+
}
102+
let code = RenderBlockContent.codeListing(.init(syntax: snippetMixin.language, code: snippetMixin.lines, metadata: nil, options: codeBlockOptions))
93103
return docCommentContent + [code]
94104
}
95105
}

0 commit comments

Comments
 (0)