Skip to content

Commit 3ae43b1

Browse files
author
Jesse Haigh
committed
add feature flag 'enable-experimental-code-block' for copy-to-clipboard and other code block annotations
1 parent 1f4894d commit 3ae43b1

File tree

13 files changed

+88
-33
lines changed

13 files changed

+88
-33
lines changed

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2650,7 +2650,6 @@ public class DocumentationContext {
26502650
}
26512651
}
26522652
}
2653-
26542653
/// A closure type getting the information about a reference in a context and returns any possible problems with it.
26552654
public typealias ReferenceCheck = (DocumentationContext, ResolvedTopicReference) -> [Problem]
26562655

Sources/SwiftDocC/Infrastructure/Workspace/FeatureFlags+Info.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,20 @@ extension DocumentationBundle.Info {
3737
self.unknownFeatureFlags = []
3838
}
3939

40+
/// This feature flag corresponds to ``FeatureFlags/isExperimentalCodeBlockEnabled``.
41+
public var experimentalCodeBlock: Bool?
42+
43+
public init(experimentalCodeBlock: Bool? = nil) {
44+
self.experimentalCodeBlock = experimentalCodeBlock
45+
self.unknownFeatureFlags = []
46+
}
47+
4048
/// A list of decoded feature flag keys that didn't match a known feature flag.
4149
public let unknownFeatureFlags: [String]
4250

4351
enum CodingKeys: String, CodingKey, CaseIterable {
4452
case experimentalOverloadedSymbolPresentation = "ExperimentalOverloadedSymbolPresentation"
53+
case experimentalCodeBlock = "ExperimentalCodeBlock"
4554
}
4655

4756
struct AnyCodingKeys: CodingKey {
@@ -66,6 +75,9 @@ extension DocumentationBundle.Info {
6675
switch codingKey {
6776
case .experimentalOverloadedSymbolPresentation:
6877
self.experimentalOverloadedSymbolPresentation = try values.decode(Bool.self, forKey: flagName)
78+
79+
case .experimentalCodeBlock:
80+
self.experimentalCodeBlock = try values.decode(Bool.self, forKey: flagName)
6981
}
7082
} else {
7183
unknownFeatureFlags.append(flagName.stringValue)
@@ -79,6 +91,7 @@ extension DocumentationBundle.Info {
7991
var container = encoder.container(keyedBy: CodingKeys.self)
8092

8193
try container.encode(experimentalOverloadedSymbolPresentation, forKey: .experimentalOverloadedSymbolPresentation)
94+
try container.encode(experimentalCodeBlock, forKey: .experimentalCodeBlock)
8295
}
8396
}
8497
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -721,12 +721,16 @@ extension RenderBlockContent: Codable {
721721
}
722722
self = try .aside(.init(style: style, content: container.decode([RenderBlockContent].self, forKey: .content)))
723723
case .codeListing:
724+
var copy = false
725+
if FeatureFlags.current.isExperimentalCodeBlockEnabled {
726+
copy = true
727+
}
724728
self = try .codeListing(.init(
725729
syntax: container.decodeIfPresent(String.self, forKey: .syntax),
726730
code: container.decode([String].self, forKey: .code),
727731
metadata: container.decodeIfPresent(RenderContentMetadata.self, forKey: .metadata),
728-
copyToClipboard: container.decodeIfPresent(Bool.self, forKey: .copyToClipboard) ?? true
729-
))
732+
copyToClipboard: container.decodeIfPresent(Bool.self, forKey: .copyToClipboard) ?? copy
733+
))
730734
case .heading:
731735
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)))
732736
case .orderedList:

Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,34 +47,41 @@ struct RenderContentCompiler: MarkupVisitor {
4747

4848
mutating func visitCodeBlock(_ codeBlock: CodeBlock) -> [any RenderContent] {
4949
// Default to the bundle's code listing syntax if one is not explicitly declared in the code block.
50-
struct ParsedOptions {
51-
var lang: String?
52-
var nocopy = false
53-
}
5450

55-
func parseLanguageString(_ input: String?) -> ParsedOptions {
56-
guard let input else { return ParsedOptions() }
51+
if FeatureFlags.current.isExperimentalCodeBlockEnabled {
52+
53+
struct ParsedOptions {
54+
var lang: String?
55+
var nocopy = false
56+
}
57+
58+
func parseLanguageString(_ input: String?) -> ParsedOptions {
59+
guard let input else { return ParsedOptions() }
5760

58-
let parts = input
61+
let parts = input
5962
.split(separator: ",")
6063
.map { $0.trimmingCharacters(in: .whitespaces) }
6164

62-
var options = ParsedOptions()
65+
var options = ParsedOptions()
6366

64-
for part in parts {
65-
let lower = part.lowercased()
66-
if lower == "nocopy" {
67-
options.nocopy = true
68-
} else if options.lang == nil {
69-
options.lang = part
67+
for part in parts {
68+
let lower = part.lowercased()
69+
if lower == "nocopy" {
70+
options.nocopy = true
71+
} else if options.lang == nil {
72+
options.lang = part
73+
}
7074
}
75+
return options
7176
}
72-
return options
73-
}
7477

75-
let options = parseLanguageString(codeBlock.language)
78+
let options = parseLanguageString(codeBlock.language)
79+
80+
return [RenderBlockContent.codeListing(.init(syntax: options.lang ?? bundle.info.defaultCodeListingLanguage, code: codeBlock.code.splitByNewlines, metadata: nil, copyToClipboard: !options.nocopy))]
7681

77-
return [RenderBlockContent.codeListing(.init(syntax: options.lang ?? bundle.info.defaultCodeListingLanguage, code: codeBlock.code.splitByNewlines, metadata: nil, copyToClipboard: !options.nocopy))]
82+
} else {
83+
return [RenderBlockContent.codeListing(.init(syntax: codeBlock.language ?? bundle.info.defaultCodeListingLanguage, code: codeBlock.code.splitByNewlines, metadata: nil, copyToClipboard: false))]
84+
}
7885
}
7986

8087
mutating func visitHeading(_ heading: Heading) -> [any RenderContent] {

Sources/SwiftDocC/Semantics/Snippets/Snippet.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,19 @@ extension Snippet: RenderableDirectiveConvertible {
6868
let lines = snippetMixin.lines[lineRange]
6969
let minimumIndentation = lines.map { $0.prefix { $0.isWhitespace }.count }.min() ?? 0
7070
let trimmedLines = lines.map { String($0.dropFirst(minimumIndentation)) }
71-
return [RenderBlockContent.codeListing(.init(syntax: snippetMixin.language, code: trimmedLines, metadata: nil, copyToClipboard: true))]
71+
var copy = false
72+
if FeatureFlags.current.isExperimentalCodeBlockEnabled {
73+
copy = true
74+
}
75+
return [RenderBlockContent.codeListing(.init(syntax: snippetMixin.language, code: trimmedLines, metadata: nil, copyToClipboard: copy))]
7276
} else {
7377
// Render the whole snippet with its explanation content.
7478
let docCommentContent = snippetEntity.markup.children.flatMap { contentCompiler.visit($0) }
75-
let code = RenderBlockContent.codeListing(.init(syntax: snippetMixin.language, code: snippetMixin.lines, metadata: nil, copyToClipboard: true))
79+
var copy = false
80+
if FeatureFlags.current.isExperimentalCodeBlockEnabled {
81+
copy = true
82+
}
83+
let code = RenderBlockContent.codeListing(.init(syntax: snippetMixin.language, code: snippetMixin.lines, metadata: nil, copyToClipboard: copy))
7684
return docCommentContent + [code]
7785
}
7886
}

Sources/SwiftDocC/Utility/FeatureFlags.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ public struct FeatureFlags: Codable {
1313
/// The current feature flags that Swift-DocC uses to conditionally enable
1414
/// (usually experimental) behavior in Swift-DocC.
1515
public static var current = FeatureFlags()
16-
16+
17+
/// Whether or not experimental annotation of code blocks is enabled.
18+
public var isExperimentalCodeBlockEnabled = false
19+
1720
/// Whether or not experimental support for device frames on images and video is enabled.
1821
public var isExperimentalDeviceFrameSupportEnabled = false
1922

Sources/SwiftDocCUtilities/ArgumentParsing/ActionExtensions/ConvertAction+CommandInitialization.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ extension ConvertAction {
1919
public init(fromConvertCommand convert: Docc.Convert, withFallbackTemplate fallbackTemplateURL: URL? = nil) throws {
2020
var standardError = LogHandle.standardError
2121
let outOfProcessResolver: OutOfProcessReferenceResolver?
22-
22+
FeatureFlags.current.isExperimentalCodeBlockEnabled = convert.enableExperimentalCodeBlock
2323
FeatureFlags.current.isExperimentalDeviceFrameSupportEnabled = convert.enableExperimentalDeviceFrameSupport
2424
FeatureFlags.current.isExperimentalLinkHierarchySerializationEnabled = convert.enableExperimentalLinkHierarchySerialization
2525
FeatureFlags.current.isExperimentalOverloadedSymbolPresentationEnabled = convert.enableExperimentalOverloadedSymbolPresentation

Sources/SwiftDocCUtilities/ArgumentParsing/Subcommands/Convert.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,13 @@ extension Docc {
475475
struct FeatureFlagOptions: ParsableArguments {
476476
@Flag(help: "Allows for custom templates, like `header.html`.")
477477
var experimentalEnableCustomTemplates = false
478-
478+
479+
@Flag(
480+
name: .customLong("enable-experimental-code-block"),
481+
help: "Annotate code blocks with additional metadata to support copy-to-clipboard, highlighting, and wrapping on code blocks."
482+
)
483+
var enableExperimentalCodeBlock = false
484+
479485
@Flag(help: .hidden)
480486
var enableExperimentalDeviceFrameSupport = false
481487

@@ -557,6 +563,14 @@ extension Docc {
557563

558564
}
559565

566+
/// A user-provided value that is true if the user enables experimental support for code block annotation.
567+
///
568+
/// Defaults to false.
569+
public var enableExperimentalCodeBlock: Bool {
570+
get { featureFlags.enableExperimentalCodeBlock }
571+
set { featureFlags.enableExperimentalCodeBlock = newValue}
572+
}
573+
560574
/// A user-provided value that is true if the user enables experimental support for device frames.
561575
///
562576
/// Defaults to false.

Tests/SwiftDocCTests/Model/RenderContentMetadataTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class RenderContentMetadataTests: XCTestCase {
5454
RenderInlineContent.text("Content"),
5555
])
5656

57-
let code = RenderBlockContent.codeListing(.init(syntax: nil, code: [], metadata: metadata, copyToClipboard: true))
57+
let code = RenderBlockContent.codeListing(.init(syntax: nil, code: [], metadata: metadata, copyToClipboard: false))
5858
let data = try JSONEncoder().encode(code)
5959
let roundtrip = try JSONDecoder().decode(RenderBlockContent.self, from: data)
6060

Tests/SwiftDocCTests/Model/RenderNodeSerializationTests.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class RenderNodeSerializationTests: XCTestCase {
4444
.strong(inlineContent: [.text("Project > Run")]),
4545
.text(" menu item, or the following code:"),
4646
])),
47-
.codeListing(.init(syntax: "swift", code: ["xcrun xcodebuild -h", "xcrun xcodebuild build -configuration Debug"], metadata: nil, copyToClipboard: true)),
47+
.codeListing(.init(syntax: "swift", code: ["xcrun xcodebuild -h", "xcrun xcodebuild build -configuration Debug"], metadata: nil, copyToClipboard: false)),
4848
]))
4949
]
5050

@@ -71,16 +71,16 @@ class RenderNodeSerializationTests: XCTestCase {
7171
let assessment1 = TutorialAssessmentsRenderSection.Assessment(title: [.paragraph(.init(inlineContent: [.text("Lorem ipsum dolor sit amet?")]))],
7272
content: nil,
7373
choices: [
74-
.init(content: [.codeListing(.init(syntax: "swift", code: ["override func viewDidLoad() {", "super.viewDidLoad()", "}"], metadata: nil, copyToClipboard: true))], isCorrect: true, justification: [.paragraph(.init(inlineContent: [.text("It's correct because...")]))], reaction: "That's right!"),
75-
.init(content: [.codeListing(.init(syntax: "swift", code: ["sceneView.delegate = self"], metadata: nil, copyToClipboard: true))], isCorrect: false, justification: [.paragraph(.init(inlineContent: [.text("It's incorrect because...")]))], reaction: "Not quite."),
74+
.init(content: [.codeListing(.init(syntax: "swift", code: ["override func viewDidLoad() {", "super.viewDidLoad()", "}"], metadata: nil, copyToClipboard: false))], isCorrect: true, justification: [.paragraph(.init(inlineContent: [.text("It's correct because...")]))], reaction: "That's right!"),
75+
.init(content: [.codeListing(.init(syntax: "swift", code: ["sceneView.delegate = self"], metadata: nil, copyToClipboard: false))], isCorrect: false, justification: [.paragraph(.init(inlineContent: [.text("It's incorrect because...")]))], reaction: "Not quite."),
7676
.init(content: [.paragraph(.init(inlineContent: [.text("None of the above.")]))], isCorrect: false, justification: [.paragraph(.init(inlineContent: [.text("It's incorrect because...")]))], reaction: nil),
7777
])
7878

7979
let assessment2 = TutorialAssessmentsRenderSection.Assessment(title: [.paragraph(.init(inlineContent: [.text("Duis aute irure dolor in reprehenderit?")]))],
8080
content: [.paragraph(.init(inlineContent: [.text("What is the airspeed velocity of an unladen swallow?")]))],
8181
choices: [
82-
.init(content: [.codeListing(.init(syntax: "swift", code: ["super.viewWillAppear()"], metadata: nil, copyToClipboard: true))], isCorrect: true, justification: [.paragraph(.init(inlineContent: [.text("It's correct because...")]))], reaction: "Correct."),
83-
.init(content: [.codeListing(.init(syntax: "swift", code: ["sceneView.delegate = self"], metadata: nil, copyToClipboard: true))], isCorrect: true, justification: [.paragraph(.init(inlineContent: [.text("It's correct because...")]))], reaction: "Yep."),
82+
.init(content: [.codeListing(.init(syntax: "swift", code: ["super.viewWillAppear()"], metadata: nil, copyToClipboard: false))], isCorrect: true, justification: [.paragraph(.init(inlineContent: [.text("It's correct because...")]))], reaction: "Correct."),
83+
.init(content: [.codeListing(.init(syntax: "swift", code: ["sceneView.delegate = self"], metadata: nil, copyToClipboard: false))], isCorrect: true, justification: [.paragraph(.init(inlineContent: [.text("It's correct because...")]))], reaction: "Yep."),
8484
.init(content: [.paragraph(.init(inlineContent: [.text("None of the above.")]))], isCorrect: false, justification: [.paragraph(.init(inlineContent: [.text("It's incorrect because...")]))], reaction: "Close!"),
8585
])
8686

0 commit comments

Comments
 (0)