extension String { func htmlEscaped() -> String { replacingOccurrences(of: "&", with: "&") .replacingOccurrences(of: "\"", with: """) .replacingOccurrences(of: "'", with: "'") .replacingOccurrences(of: "<", with: "<") .replacingOccurrences(of: ">", with: ">") } func percentDecoded() -> String { guard let decoded = removingPercentEncoding else { return self } return decoded } var withLeadingSlashRemoved: String { hasPrefix("/") ? String(dropFirst("/".count)) : self } var withLeadingSlash: String { hasPrefix("/") ? self : "/" + self } var withTrailingSlashRemoved: String { hasSuffix("/") ? String(dropLast("/".count)) : self } var withTrailingSlash: String { hasSuffix("/") ? self : self + "/" } var withHttpPrefixRemoved: String { if hasPrefix("https://") { return String(dropFirst("https://".count)) } if hasPrefix("http://") { return String(dropFirst("http://".count)) } return self } var removingSurroundingQuotes: String { if hasPrefix("\"") && hasSuffix("\"") { return dropBeforeFirst("\"").dropAfterLast("\"") } if hasPrefix("'") && hasSuffix("'") { return dropBeforeFirst("'").dropAfterLast("'") } return self } var nonEmpty: String? { isEmpty ? nil : self } var trimmed: String { trimmingCharacters(in: .whitespacesAndNewlines) } func indented(by indentation: String) -> String { components(separatedBy: "\n").joined(separator: "\n" + indentation) } var withoutEmptyLines: String { components(separatedBy: "\n") .filter { !$0.trimmed.isEmpty } .joined(separator: "\n") } /** Remove the part after the last occurence of the separator (including the separator itself). The string is left unchanged, if it does not contain the separator. */ func dropAfterLast(_ separator: String) -> String { guard contains(separator) else { return self } return components(separatedBy: separator).dropLast().joined(separator: separator) } /** Remove everything before the last separator. Also removes the separator itself. If the separator is not contained in the string, then the full string is returned. */ func dropBeforeLast(_ separator: T) -> String where T: StringProtocol { components(separatedBy: separator).last! } func dropBeforeFirst(_ separator: String) -> String { guard contains(separator) else { return self } return components(separatedBy: separator).dropFirst().joined(separator: separator) } func lastComponentAfter(_ separator: String) -> String { components(separatedBy: separator).last! } /** Insert the new content before the last occurence of the specified separator. If the separator does not appear in the string, then the new content is simply appended. */ func insert(_ content: String, beforeLast separator: String) -> String { let parts = components(separatedBy: separator) guard parts.count > 1 else { return self + content } return parts.dropLast().joined(separator: separator) + content + separator + parts.last! } /** Remove everything behind the first separator. Also removes the separator itself. If the separator is not contained in the string, then the full string is returned. */ func dropAfterFirst(_ separator: T) -> String where T: StringProtocol { components(separatedBy: separator).first! } func between(_ start: String, and end: String) -> String { dropBeforeFirst(start).dropAfterFirst(end) } /** Split the string at the first occurence of the separator If the String does not contain the separator, then `before` will contain the whole string, and `after` will be empty */ func splitAtFirst(_ separator: String) -> (before: String, after: String) { let parts = components(separatedBy: separator) return (parts.first!, parts.dropFirst().joined(separator: separator)) } } extension String { func removingPrefix(_ prefix: String) -> String? { guard self.hasPrefix(prefix) else { return nil } return String(self.dropFirst(prefix.count)) } } extension String { var fileNameWithoutExtension: String { dropAfterLast(".") } var fileExtension: String? { let parts = components(separatedBy: ".") guard parts.count > 1 else { return nil } return parts.last } var fileNameAndExtension: (fileName: String, fileExtension: String?) { let parts = components(separatedBy: ".") guard parts.count > 1 else { return (self, nil) } return (fileName: parts.dropLast().joined(separator: "."), fileExtension: parts.last) } } extension Substring { func dropBeforeFirst(_ separator: String) -> String { guard contains(separator) else { return String(self) } return components(separatedBy: separator).dropFirst().joined(separator: separator) } func between(_ start: String, and end: String) -> String { components(separatedBy: start).last! .components(separatedBy: end).first! } func between(first: String, andLast last: String) -> String { dropBeforeFirst(first).dropAfterLast(last) } func last(after: String) -> String { components(separatedBy: after).last! } /** Split the string at the first occurence of the separator */ func splitAtFirst(_ separator: String) -> (String, String) { let parts = components(separatedBy: separator) return (parts.first!, parts.dropFirst().joined(separator: separator)) } }