|
| 1 | +/* |
| 2 | + This source file is part of the Swift.org open source project |
| 3 | + |
| 4 | + Copyright (c) 2022 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 | +import Foundation |
| 12 | +import Markdown |
| 13 | + |
| 14 | +/// A directive that adds a prominent button or link to a page's header. |
| 15 | +/// |
| 16 | +/// A "Call to Action" has two main components: a link or file path, and the link text to display. |
| 17 | +/// |
| 18 | +/// The link path can be specified in one of two ways: |
| 19 | +/// - The `url` parameter specifies a URL that will be used verbatim. Use this when you're linking |
| 20 | +/// to an external page or externally-hosted file. |
| 21 | +/// - The `path` parameter specifies the path to a file hosted within your documentation catalog. |
| 22 | +/// Use this if you're linking to a downloadable file that you're managing alongside your |
| 23 | +/// articles and tutorials. |
| 24 | +/// |
| 25 | +/// The link text can also be specified in one of two ways: |
| 26 | +/// - The `purpose` parameter can be used to use a default button label. There are two valid values: |
| 27 | +/// - `download` indicates that the link is to a downloadable file. The button will be labeled "Download". |
| 28 | +/// - `link` indicates that the link is to an external webpage. The button will be labeled "Visit". |
| 29 | +/// - The `label` parameter specifies the literal text to use as the button label. |
| 30 | +/// |
| 31 | +/// `@CallToAction` requires one of `url` or `path`, and one of `purpose` or `label`. Specifying both |
| 32 | +/// `purpose` and `label` is allowed, but the `label` will override the default label provided by |
| 33 | +/// `purpose`. |
| 34 | +/// |
| 35 | +/// This directive is only valid within a ``Metadata`` directive: |
| 36 | +/// |
| 37 | +/// ```markdown |
| 38 | +/// @Metadata { |
| 39 | +/// @CallToAction(url: "https://example.com/sample.zip", purpose: download) |
| 40 | +/// } |
| 41 | +/// ``` |
| 42 | +public final class CallToAction: Semantic, AutomaticDirectiveConvertible { |
| 43 | + /// The kind of action the link is referencing. |
| 44 | + public enum Purpose: String, CaseIterable, DirectiveArgumentValueConvertible { |
| 45 | + /// References a link to download an associated asset, like a sample project. |
| 46 | + case download |
| 47 | + |
| 48 | + /// References a link to view external content, like a source code repository. |
| 49 | + case link |
| 50 | + } |
| 51 | + |
| 52 | + /// The location of the associated link, as a fixed URL. |
| 53 | + @DirectiveArgumentWrapped |
| 54 | + public var url: URL? = nil |
| 55 | + |
| 56 | + /// The location of the associated link, as a reference to a file in this documentation bundle. |
| 57 | + @DirectiveArgumentWrapped( |
| 58 | + parseArgument: { bundle, argumentValue in |
| 59 | + ResourceReference(bundleIdentifier: bundle.identifier, path: argumentValue) |
| 60 | + } |
| 61 | + ) |
| 62 | + public var file: ResourceReference? = nil |
| 63 | + |
| 64 | + /// The purpose of this Call to Action, which provides a default button label. |
| 65 | + @DirectiveArgumentWrapped |
| 66 | + public var purpose: Purpose? = nil |
| 67 | + |
| 68 | + /// Text to use as the button label, which may override ``purpose-swift.property``. |
| 69 | + @DirectiveArgumentWrapped |
| 70 | + public var label: String? = nil |
| 71 | + |
| 72 | + static var keyPaths: [String : AnyKeyPath] = [ |
| 73 | + "url" : \CallToAction._url, |
| 74 | + "file" : \CallToAction._file, |
| 75 | + "purpose" : \CallToAction._purpose, |
| 76 | + "label" : \CallToAction._label, |
| 77 | + ] |
| 78 | + |
| 79 | + /// The computed label for this Call to Action, whether provided directly via ``label`` or |
| 80 | + /// indirectly via ``purpose-swift.property``. |
| 81 | + public var buttonLabel: String { |
| 82 | + if let label = label { |
| 83 | + return label |
| 84 | + } else if let purpose = purpose { |
| 85 | + return purpose.defaultLabel |
| 86 | + } else { |
| 87 | + // The `validate()` method ensures that this type should never be constructed without |
| 88 | + // one of the above. |
| 89 | + fatalError("A valid CallToAction should have either a purpose or label") |
| 90 | + } |
| 91 | + } |
| 92 | + |
| 93 | + func validate( |
| 94 | + source: URL?, |
| 95 | + for bundle: DocumentationBundle, |
| 96 | + in context: DocumentationContext, |
| 97 | + problems: inout [Problem] |
| 98 | + ) -> Bool { |
| 99 | + var isValid = true |
| 100 | + |
| 101 | + if self.url == nil && self.file == nil { |
| 102 | + problems.append(.init(diagnostic: .init( |
| 103 | + source: source, |
| 104 | + severity: .warning, |
| 105 | + range: originalMarkup.range, |
| 106 | + identifier: "org.swift.docc.\(CallToAction.self).missingLink", |
| 107 | + summary: "\(CallToAction.directiveName.singleQuoted) directive requires `url` or `file` argument", |
| 108 | + explanation: "The Call to Action requires a link to direct the user to." |
| 109 | + ))) |
| 110 | + |
| 111 | + isValid = false |
| 112 | + } else if self.url != nil && self.file != nil { |
| 113 | + problems.append(.init(diagnostic: .init( |
| 114 | + source: source, |
| 115 | + severity: .warning, |
| 116 | + range: originalMarkup.range, |
| 117 | + identifier: "org.swift.docc.\(CallToAction.self).tooManyLinks", |
| 118 | + summary: "\(CallToAction.directiveName.singleQuoted) directive requires only one of `url` or `file`", |
| 119 | + explanation: "Both the `url` and `file` arguments specify the link in the heading; specifying both of them creates ambiguity in where the call should link." |
| 120 | + ))) |
| 121 | + |
| 122 | + isValid = false |
| 123 | + } |
| 124 | + |
| 125 | + if self.purpose == nil && self.label == nil { |
| 126 | + problems.append(.init(diagnostic: .init( |
| 127 | + source: source, |
| 128 | + severity: .warning, |
| 129 | + range: originalMarkup.range, |
| 130 | + identifier: "org.swift.docc.\(CallToAction.self).missingLabel", |
| 131 | + summary: "\(CallToAction.directiveName.singleQuoted) directive requires `purpose` or `label` argument", |
| 132 | + explanation: "Without a `purpose` or `label`, the Call to Action has no label to apply to the link." |
| 133 | + ))) |
| 134 | + |
| 135 | + isValid = false |
| 136 | + } |
| 137 | + |
| 138 | + if let file = self.file { |
| 139 | + if context.resolveAsset(named: file.url.lastPathComponent, in: bundle.rootReference) == nil { |
| 140 | + problems.append(.init( |
| 141 | + diagnostic: Diagnostic( |
| 142 | + source: url, |
| 143 | + severity: .warning, |
| 144 | + range: originalMarkup.range, |
| 145 | + identifier: "org.swift.docc.Project.ProjectFilesNotFound", |
| 146 | + summary: "\(file.path) file reference not found in \(CallToAction.directiveName.singleQuoted) directive"), |
| 147 | + possibleSolutions: [ |
| 148 | + Solution(summary: "Copy the referenced file into the documentation bundle directory", replacements: []) |
| 149 | + ] |
| 150 | + )) |
| 151 | + } else { |
| 152 | + self.file = ResourceReference(bundleIdentifier: file.bundleIdentifier, path: file.url.lastPathComponent) |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + return isValid |
| 157 | + } |
| 158 | + |
| 159 | + public let originalMarkup: Markdown.BlockDirective |
| 160 | + |
| 161 | + @available(*, deprecated, message: "Do not call directly. Required for 'AutomaticDirectiveConvertible'.") |
| 162 | + init(originalMarkup: Markdown.BlockDirective) { |
| 163 | + self.originalMarkup = originalMarkup |
| 164 | + } |
| 165 | +} |
| 166 | + |
| 167 | +extension CallToAction.Purpose { |
| 168 | + /// The label that will be applied to a Call to Action with this purpose if it doesn't provide |
| 169 | + /// a separate label. |
| 170 | + public var defaultLabel: String { |
| 171 | + switch self { |
| 172 | + case .download: |
| 173 | + return "Download" |
| 174 | + case .link: |
| 175 | + return "Visit" |
| 176 | + } |
| 177 | + } |
| 178 | +} |
0 commit comments