feat: Node type done and working, which is used for styling and rendering documentation strings
This commit is contained in:
@@ -27,7 +27,10 @@ extension CommandConfiguration {
|
||||
abstract: abstract,
|
||||
discussion: [.note(label: "Most options are not required if you have a configuration file setup.")]
|
||||
+ examples.nodes(parentCommand)
|
||||
+ [.seeAlso(label: "Ansible playbook options.", command: "ansible-playbook")]
|
||||
+ [
|
||||
.seeAlso(label: "Ansible playbook options.", command: "ansible-playbook"),
|
||||
.important(label: Constants.importantExtraArgsNote)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -37,7 +40,7 @@ func createAbstract(_ string: String) -> String {
|
||||
}
|
||||
|
||||
struct Discussion {
|
||||
var nodes: [Node]
|
||||
let nodes: [Node]
|
||||
|
||||
init(usesExtraArgs: Bool = true, _ nodes: Node...) {
|
||||
self.init(nodes: nodes, usesExtraArgs: usesExtraArgs)
|
||||
@@ -52,57 +55,188 @@ struct Discussion {
|
||||
|
||||
if usesExtraArgs {
|
||||
if let lastExampleIndex = nodes.lastIndex(where: \.isExampleNode) {
|
||||
let last = nodes[lastExampleIndex]
|
||||
if case let .example(_, example, parent) = last {
|
||||
nodes.insert(
|
||||
.example(label: "Passing extra args.", example: example, parentCommand: parent),
|
||||
at: nodes.index(after: lastExampleIndex)
|
||||
)
|
||||
guard case let .example(.container(exampleNodes, separator)) = nodes[lastExampleIndex] else {
|
||||
// this should never happen.
|
||||
print(nodes[lastExampleIndex])
|
||||
fatalError()
|
||||
}
|
||||
|
||||
// Get the last element of the example container, which will be the
|
||||
// text for the example.
|
||||
guard let exampleNode = exampleNodes.last else {
|
||||
print(exampleNodes)
|
||||
fatalError()
|
||||
}
|
||||
|
||||
// replace the first element, which is the header so that we can
|
||||
// replace it with a new header.
|
||||
nodes.insert(
|
||||
.example(
|
||||
.container(
|
||||
[
|
||||
.exampleLabel("Passing extra args."),
|
||||
.container([exampleNode, .text("-- --vault-id \"myId@$SCRIPTS/vault-gopass-client\"")], separator: " ")
|
||||
],
|
||||
separator: separator
|
||||
)),
|
||||
at: nodes.index(after: lastExampleIndex)
|
||||
)
|
||||
}
|
||||
}
|
||||
self.nodes = nodes
|
||||
}
|
||||
|
||||
func render() -> String {
|
||||
nodes.map { $0.render() }.joined(separator: "\n")
|
||||
}
|
||||
func render() -> String { nodes.render() }
|
||||
}
|
||||
|
||||
enum Node: Equatable {
|
||||
case note(header: String = "NOTE:", label: String, appendNewLine: Bool = true)
|
||||
case example(label: String, example: String, parentCommand: String?)
|
||||
case seeAlso(label: String, command: String, appendHelpToCommand: Bool = true)
|
||||
enum Node {
|
||||
|
||||
static var exampleHeading: Self { .note(header: "Examples:", label: "Some common examples.", appendNewLine: false) }
|
||||
/// A container that holds onto other nodes to be rendered.
|
||||
indirect case container(_ nodes: [Node], separator: String = "\n\n")
|
||||
|
||||
/// A container to identify example nodes, so some other nodes can get injected
|
||||
/// when this is rendered.
|
||||
///
|
||||
/// NOTE: Things blow up if this is not used correctly, at least when using the `Discussion`
|
||||
/// to render the nodes, so this is not safe to create aside from the static methods that
|
||||
/// create an example node. If not using in the context of a `Discussion` then it's fine to
|
||||
/// use, as it doesn't do anything except stand as an type identifier.
|
||||
indirect case example(_ node: Node)
|
||||
|
||||
/// A root node that renders the given string without modification.
|
||||
case text(_ text: String)
|
||||
|
||||
static func styledText(_ text: String, color: NamedColor? = nil, styles: [Style]? = nil) -> Self {
|
||||
var string = text
|
||||
if let color {
|
||||
string = string.applyingColor(color)
|
||||
}
|
||||
if let styles {
|
||||
for style in styles {
|
||||
string = string.applyingStyle(style)
|
||||
}
|
||||
}
|
||||
return .text(string)
|
||||
}
|
||||
|
||||
static func styledText(_ text: String, color: NamedColor? = nil, style: Style) -> Self {
|
||||
.styledText(text, color: color, styles: [style])
|
||||
}
|
||||
|
||||
static func header(_ text: String) -> Self {
|
||||
.styledText(text, color: .yellow, styles: [.bold])
|
||||
}
|
||||
|
||||
static func section(header: Node, content: Node, inline: Bool = false) -> Self {
|
||||
.container([header, content], separator: inline ? " " : "\n")
|
||||
}
|
||||
|
||||
static func section(header: String, label: Node, inline: Bool = false) -> Self {
|
||||
.section(header: .header(header), content: label, inline: inline)
|
||||
}
|
||||
|
||||
static func important(label: String) -> Self {
|
||||
.section(
|
||||
header: .text("IMPORTANT NOTE:".red.bold.underline),
|
||||
content: .text(label.red.italic),
|
||||
inline: true
|
||||
)
|
||||
}
|
||||
|
||||
static func note(label: String, labelInline: Bool = true) -> Self {
|
||||
.section(header: "NOTE:", label: .text(label.italic), inline: labelInline)
|
||||
}
|
||||
|
||||
static func container(separator: String = "\n\n", _ nodes: Node...) -> Self {
|
||||
.container(nodes, separator: separator)
|
||||
}
|
||||
|
||||
static func shellCommand(_ text: String, symbol: String = " $") -> Self {
|
||||
.text("\(symbol) \(text)")
|
||||
}
|
||||
|
||||
static func seeAlso(label: String, command: String, appendHelpToCommand: Bool = true) -> Self {
|
||||
.container(
|
||||
.container([.header("See Also:"), .text(label.italic)], separator: " "),
|
||||
.shellCommand("\(appendHelpToCommand ? command + " --help" : command)")
|
||||
)
|
||||
}
|
||||
|
||||
static var exampleHeading: Self {
|
||||
.section(header: "Examples:", label: .text("Some common examples."), inline: true)
|
||||
}
|
||||
|
||||
var isExampleNode: Bool {
|
||||
if case .example = self { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
/// Renders an example usage of the command.
|
||||
static func example(label: String, example: String, parentCommand: String?) -> Self {
|
||||
let string: String
|
||||
if let parent = parentCommand {
|
||||
string = "\(Constants.appName) \(parent) \(example)"
|
||||
} else {
|
||||
string = "\(Constants.appName) \(example)"
|
||||
}
|
||||
return .example(.section(
|
||||
header: .exampleLabel(label),
|
||||
content: .shellCommand(string, symbol: " $")
|
||||
))
|
||||
}
|
||||
|
||||
static func exampleLabel(_ label: String) -> Node {
|
||||
.text(" \(label.green.italic)")
|
||||
}
|
||||
|
||||
func render() -> String {
|
||||
switch self {
|
||||
case let .note(header, note, appendNewLine):
|
||||
return "\(header.yellow) \(note.italic)\(appendNewLine ? "\n" : "")"
|
||||
case let .example(label: label, example: example, parentCommand: parent):
|
||||
return """
|
||||
\(label.green.italic)
|
||||
$ \(Constants.appName) \(parent ?? "")\(parent != nil ? " \(example)" : example)
|
||||
|
||||
"""
|
||||
case let .seeAlso(label: label, command: command, appendHelpToCommand: appendHelp):
|
||||
return """
|
||||
\("See Also:".yellow) \(label.italic)
|
||||
$ \(command)\(appendHelp ? " --help" : "")
|
||||
|
||||
"""
|
||||
case let .container(nodes, separator):
|
||||
return nodes.render(separator: separator)
|
||||
case let .text(text):
|
||||
return text
|
||||
case let .example(node):
|
||||
return node.render()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Node: ExpressibleByStringLiteral {
|
||||
|
||||
typealias StringLiteralType = String
|
||||
|
||||
init(stringLiteral value: String) {
|
||||
self = .text(value)
|
||||
}
|
||||
}
|
||||
|
||||
extension Node: ExpressibleByArrayLiteral {
|
||||
|
||||
typealias ArrayLiteralElement = Node
|
||||
|
||||
init(arrayLiteral elements: Node...) {
|
||||
self = .container(elements)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Array where Element == (label: String, example: String) {
|
||||
func nodes(_ parentCommand: String?) -> [Node] {
|
||||
map { .example(label: $0.label, example: $0.example, parentCommand: parentCommand) }
|
||||
}
|
||||
}
|
||||
|
||||
private extension Array where Element == Node {
|
||||
|
||||
func render(separator: String = "\n\n") -> String {
|
||||
map {
|
||||
// Strip of any new-line characters from the last section of the rendered string
|
||||
// of the node. This allows us to have a consistent single new-line between each
|
||||
// rendered node.
|
||||
var string = $0.render()[...]
|
||||
while string.hasSuffix("\n") {
|
||||
string = string.dropLast()
|
||||
}
|
||||
return String(string)
|
||||
}.joined(separator: separator)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,8 @@ enum Constants {
|
||||
static let playbookFileName = "main.yml"
|
||||
static let inventoryFileName = "inventory.ini"
|
||||
static let importantExtraArgsNote = """
|
||||
|
||||
\("IMPORTANT NOTE".bold.underline): \("""
|
||||
Any extra arguments to pass to the underlying command invocation have to
|
||||
be at the end with `--` before any arguments otherwise there will
|
||||
be an "Unkown option" error. See examples above.
|
||||
""".italic
|
||||
)
|
||||
""".red
|
||||
Any extra arguments to pass to the underlying command invocation have to
|
||||
be at the end with `--` before any arguments otherwise there will
|
||||
be an "Unkown option" error. See examples above.
|
||||
"""
|
||||
}
|
||||
|
||||
@@ -7,6 +7,12 @@ struct VaultCommand: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: commandName,
|
||||
abstract: createAbstract("Vault commands."),
|
||||
discussion: Discussion(
|
||||
.text("""
|
||||
Allows you to run `ansible-vault` commands on your project or project-template.
|
||||
"""),
|
||||
.seeAlso(label: "Ansible Vault", command: "ansible-vault")
|
||||
).render(),
|
||||
subcommands: [
|
||||
EncryptCommand.self, DecryptCommand.self
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user