Files
swift-argument-parser/Sources/ArgumentParser/Utilities/StringExtensions.swift
T
Rauhul Varma 00a86771ca Clean up help generation (#385)
- Adds check to ensure that `wrapped(to:wrappingIndent:)` doesn't
  attempt to retrieve a negative prefix.
- The Usage struct was composed of an array of strings which always
  contained exact one string at runtime. This struct has been removed
  and replaced with a single usage string.
- Removes HelpGenerator._screenWidthOverride in favor of explicitly
  setting the screen width in generateHelp calls.
2022-01-10 16:02:50 -06:00

182 lines
6.3 KiB
Swift

//===----------------------------------------------------------*- swift -*-===//
//
// This source file is part of the Swift Argument Parser open source project
//
// Copyright (c) 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//
extension String {
func wrapped(to columns: Int, wrappingIndent: Int = 0) -> String {
let columns = columns - wrappingIndent
guard columns > 0 else {
// Skip wrapping logic if the number of columns is less than 1 in release
// builds and assert in debug builds.
assertionFailure("`columns - wrappingIndent` should be always be greater than 0.")
return ""
}
var result: [Substring] = []
var currentIndex = startIndex
while true {
let nextChunk = self[currentIndex...].prefix(columns)
if let lastLineBreak = nextChunk.lastIndex(of: "\n") {
result.append(contentsOf: self[currentIndex..<lastLineBreak].split(separator: "\n", omittingEmptySubsequences: false))
currentIndex = index(after: lastLineBreak)
} else if nextChunk.endIndex == self.endIndex {
result.append(self[currentIndex...])
break
} else if let lastSpace = nextChunk.lastIndex(of: " ") {
result.append(self[currentIndex..<lastSpace])
currentIndex = index(after: lastSpace)
} else if let nextSpace = self[currentIndex...].firstIndex(of: " ") {
result.append(self[currentIndex..<nextSpace])
currentIndex = index(after: nextSpace)
} else {
result.append(self[currentIndex...])
break
}
}
return result
.map { $0.isEmpty ? $0 : String(repeating: " ", count: wrappingIndent) + $0 }
.joined(separator: "\n")
}
/// Returns this string prefixed using a camel-case style.
///
/// Example:
///
/// "hello".addingIntercappedPrefix("my")
/// // myHello
func addingIntercappedPrefix(_ prefix: String) -> String {
guard let firstChar = first else { return prefix }
return "\(prefix)\(firstChar.uppercased())\(self.dropFirst())"
}
/// Returns this string prefixed using kebab-, snake-, or camel-case style
/// depending on what can be detected from the string.
///
/// Examples:
///
/// "hello".addingPrefixWithAutodetectedStyle("my")
/// // my-hello
/// "hello_there".addingPrefixWithAutodetectedStyle("my")
/// // my_hello_there
/// "hello-there".addingPrefixWithAutodetectedStyle("my")
/// // my-hello-there
/// "helloThere".addingPrefixWithAutodetectedStyle("my")
/// // myHelloThere
func addingPrefixWithAutodetectedStyle(_ prefix: String) -> String {
if contains("-") {
return "\(prefix)-\(self)"
} else if contains("_") {
return "\(prefix)_\(self)"
} else if first?.isLowercase == true && contains(where: { $0.isUppercase }) {
return addingIntercappedPrefix(prefix)
} else {
return "\(prefix)-\(self)"
}
}
/// Returns a new string with the camel-case-based words of this string
/// split by the specified separator.
///
/// Examples:
///
/// "myProperty".convertedToSnakeCase()
/// // my_property
/// "myURLProperty".convertedToSnakeCase()
/// // my_url_property
/// "myURLProperty".convertedToSnakeCase(separator: "-")
/// // my-url-property
func convertedToSnakeCase(separator: Character = "_") -> String {
guard !isEmpty else { return self }
var result = ""
// Whether we should append a separator when we see a uppercase character.
var separateOnUppercase = true
for index in indices {
let nextIndex = self.index(after: index)
let character = self[index]
if character.isUppercase {
if separateOnUppercase && !result.isEmpty {
// Append the separator.
result += "\(separator)"
}
// If the next character is uppercase and the next-next character is lowercase, like "L" in "URLSession", we should separate words.
separateOnUppercase = nextIndex < endIndex && self[nextIndex].isUppercase && self.index(after: nextIndex) < endIndex && self[self.index(after: nextIndex)].isLowercase
} else {
// If the character is `separator`, we do not want to append another separator when we see the next uppercase character.
separateOnUppercase = character != separator
}
// Append the lowercased character.
result += character.lowercased()
}
return result
}
/// Returns the edit distance between this string and the provided target string.
///
/// Uses the Levenshtein distance algorithm internally.
///
/// See: https://en.wikipedia.org/wiki/Levenshtein_distance
///
/// Examples:
///
/// "kitten".editDistance(to: "sitting")
/// // 3
/// "bar".editDistance(to: "baz")
/// // 1
func editDistance(to target: String) -> Int {
let rows = self.count
let columns = target.count
if rows <= 0 || columns <= 0 {
return max(rows, columns)
}
var matrix = Array(repeating: Array(repeating: 0, count: columns + 1), count: rows + 1)
for row in 1...rows {
matrix[row][0] = row
}
for column in 1...columns {
matrix[0][column] = column
}
for row in 1...rows {
for column in 1...columns {
let source = self[self.index(self.startIndex, offsetBy: row - 1)]
let target = target[target.index(target.startIndex, offsetBy: column - 1)]
let cost = source == target ? 0 : 1
matrix[row][column] = Swift.min(
matrix[row - 1][column] + 1,
matrix[row][column - 1] + 1,
matrix[row - 1][column - 1] + cost
)
}
}
return matrix.last!.last!
}
func indentingEachLine(by n: Int) -> String {
let hasTrailingNewline = self.last == "\n"
let lines = self.split(separator: "\n", omittingEmptySubsequences: false)
if hasTrailingNewline && lines.last == "" {
return lines.dropLast().map { String(repeating: " ", count: n) + $0 }
.joined(separator: "\n") + "\n"
} else {
return lines.map { String(repeating: " ", count: n) + $0 }
.joined(separator: "\n")
}
}
}