//===----------------------------------------------------------*- 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 StringProtocol where SubSequence == Substring { 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.. 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 "" } 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 Swift.max(rows, columns) } // Trim common prefix and suffix var selfStartTrim = self.startIndex var targetStartTrim = target.startIndex while selfStartTrim < self.endIndex && targetStartTrim < target.endIndex && self[selfStartTrim] == target[targetStartTrim] { self.formIndex(after: &selfStartTrim) target.formIndex(after: &targetStartTrim) } var selfEndTrim = self.endIndex var targetEndTrim = target.endIndex while selfEndTrim > selfStartTrim && targetEndTrim > targetStartTrim { let selfIdx = self.index(before: selfEndTrim) let targetIdx = target.index(before: targetEndTrim) guard self[selfIdx] == target[targetIdx] else { break } selfEndTrim = selfIdx targetEndTrim = targetIdx } // Equal strings guard !(selfStartTrim == self.endIndex && targetStartTrim == target.endIndex) else { return 0 } // After trimming common prefix and suffix, self is empty. guard selfStartTrim < selfEndTrim else { return target.distance(from: targetStartTrim, to: targetEndTrim) } // After trimming common prefix and suffix, target is empty. guard targetStartTrim < targetEndTrim else { return distance(from: selfStartTrim, to: selfEndTrim) } let newSelf = self[selfStartTrim.. String { let lines = self.split(separator: "\n", omittingEmptySubsequences: false) let spacer = String(repeating: " ", count: n) return lines.map { $0.isEmpty ? $0 : spacer + $0 }.joined(separator: "\n") } func hangingIndentingEachLine(by n: Int) -> String { let lines = self.split( separator: "\n", maxSplits: 1, omittingEmptySubsequences: false) guard lines.count == 2 else { return lines.joined(separator: "") } return "\(lines[0])\n\(lines[1].indentingEachLine(by: n))" } var nonEmpty: Self? { isEmpty ? nil : self } } extension Substring { fileprivate mutating func eat() -> Character? { self.popFirst() } fileprivate mutating func eat(_ ch: Character) -> Character? { guard self.first == ch else { return nil } return self.popFirst() } } func readTokens( from source: () -> String? ) -> [String] { var result: [String] = [] var quoteDelimiter: Character? = nil var currentSubstring = "" GetLine: while let str = source() { var slice = str[...] // Starting a new line mid-quoted section should include the newline in the token if quoteDelimiter != nil { currentSubstring.append("\n") } while let ch = slice.eat() { switch (ch, quoteDelimiter) { case ("\\", nil): // When NOT in quoted section, escape all quotes, newlines, spaces, and backslashes. guard let nextCh = slice.eat() else { // Note: An escaped newline is not included in a token continue GetLine } switch nextCh { case " ", "\\", "\"", "'": break default: currentSubstring.append(ch) } currentSubstring.append(nextCh) case ("\\", let quoteDelimiter): // When IN quoted section, escape closing quotes and backslashes. guard let nextCh = slice.eat() else { currentSubstring.append(ch) continue GetLine } switch nextCh { case quoteDelimiter, "\\": break default: currentSubstring.append(ch) } currentSubstring.append(nextCh) case (quoteDelimiter, _): // End of quoted section. quoteDelimiter = nil case (" ", nil), ("\t", nil): // Unescaped whitespace - skip empty substrings. if !currentSubstring.isEmpty { result.append(currentSubstring) currentSubstring = "" } case ("\"", nil), ("'", nil): // Beginning of quoted section. quoteDelimiter = ch default: currentSubstring.append(ch) } } // If we've reached the end of a line and we aren't in a quoted section, // break out of the parsing. if quoteDelimiter == nil { break } } if !currentSubstring.isEmpty { result.append(currentSubstring) } return result }