2023-04-25 17:56:57 -07:00
|
|
|
//
|
|
|
|
|
// String.swift
|
|
|
|
|
// Meshtastic
|
|
|
|
|
//
|
|
|
|
|
// Copyright(c) Garth Vander Houwen 4/25/23.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
import UIKit
|
|
|
|
|
|
|
|
|
|
extension String {
|
|
|
|
|
|
|
|
|
|
func base64urlToBase64() -> String {
|
|
|
|
|
var base64 = self
|
|
|
|
|
.replacingOccurrences(of: "-", with: "+")
|
|
|
|
|
.replacingOccurrences(of: "_", with: "/")
|
|
|
|
|
if base64.count % 4 != 0 {
|
|
|
|
|
base64.append(String(repeating: "==", count: 4 - base64.count % 4))
|
|
|
|
|
}
|
|
|
|
|
return base64
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func base64ToBase64url() -> String {
|
|
|
|
|
let base64url = self
|
|
|
|
|
.replacingOccurrences(of: "+", with: "-")
|
|
|
|
|
.replacingOccurrences(of: "/", with: "_")
|
|
|
|
|
.replacingOccurrences(of: "=", with: "")
|
|
|
|
|
return base64url
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-05 09:27:24 -07:00
|
|
|
var localized: String { NSLocalizedString(self, comment: self) }
|
2023-05-06 18:40:11 -07:00
|
|
|
func isEmoji() -> Bool {
|
|
|
|
|
// Emoji are no more than 4 bytes
|
|
|
|
|
if self.count > 4 {
|
|
|
|
|
return false
|
|
|
|
|
} else {
|
|
|
|
|
let characters = Array(self)
|
2023-05-13 15:46:18 -07:00
|
|
|
if characters.count <= 0 {
|
|
|
|
|
return false
|
|
|
|
|
} else {
|
|
|
|
|
return characters[0].isEmoji
|
|
|
|
|
}
|
2023-05-06 18:40:11 -07:00
|
|
|
}
|
|
|
|
|
}
|
2023-04-25 17:56:57 -07:00
|
|
|
func onlyEmojis() -> Bool {
|
|
|
|
|
return count > 0 && !contains { !$0.isEmoji }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func image(fontSize: CGFloat = 40, bgColor: UIColor = UIColor.clear, imageSize: CGSize? = nil) -> UIImage? {
|
|
|
|
|
let font = UIFont.systemFont(ofSize: fontSize)
|
|
|
|
|
let attributes = [NSAttributedString.Key.font: font]
|
|
|
|
|
let imageSize = imageSize ?? self.size(withAttributes: attributes)
|
|
|
|
|
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0)
|
|
|
|
|
bgColor.set()
|
|
|
|
|
let rect = CGRect(origin: .zero, size: imageSize)
|
|
|
|
|
UIRectFill(rect)
|
|
|
|
|
self.draw(in: rect, withAttributes: [.font: font])
|
|
|
|
|
let image = UIGraphicsGetImageFromCurrentImageContext()
|
|
|
|
|
UIGraphicsEndImageContext()
|
|
|
|
|
return image
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func camelCaseToWords() -> String {
|
2024-11-25 13:12:43 -08:00
|
|
|
return self
|
|
|
|
|
.replacingOccurrences(of: "([a-z])([A-Z](?=[A-Z])[a-z]*)", with: "$1 $2", options: .regularExpression)
|
|
|
|
|
.replacingOccurrences(of: "([A-Z])([A-Z][a-z])", with: "$1 $2", options: .regularExpression)
|
|
|
|
|
.replacingOccurrences(of: "([a-z])([A-Z][a-z])", with: "$1 $2", options: .regularExpression)
|
2023-04-25 17:56:57 -07:00
|
|
|
}
|
2024-03-24 22:23:55 -07:00
|
|
|
|
|
|
|
|
var length: Int {
|
|
|
|
|
return count
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
subscript (i: Int) -> String {
|
|
|
|
|
return self[i ..< i + 1]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func substring(fromIndex: Int) -> String {
|
|
|
|
|
return self[min(fromIndex, length) ..< length]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func substring(toIndex: Int) -> String {
|
|
|
|
|
return self[0 ..< max(0, toIndex)]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
subscript (r: Range<Int>) -> String {
|
|
|
|
|
let range = Range(uncheckedBounds: (lower: max(0, min(length, r.lowerBound)),
|
|
|
|
|
upper: min(length, max(0, r.upperBound))))
|
|
|
|
|
let start = index(startIndex, offsetBy: range.lowerBound)
|
|
|
|
|
let end = index(start, offsetBy: range.upperBound - range.lowerBound)
|
|
|
|
|
return String(self[start ..< end])
|
|
|
|
|
}
|
2025-02-28 08:16:56 -05:00
|
|
|
|
|
|
|
|
// Filter out variation selectors from the string
|
|
|
|
|
var withoutVariationSelectors: String {
|
2025-03-15 11:39:46 -04:00
|
|
|
var scalars: [UnicodeScalar] = []
|
|
|
|
|
var previousWasASCII = false
|
|
|
|
|
|
|
|
|
|
for scalar in self.unicodeScalars {
|
|
|
|
|
if scalar.properties.isVariationSelector {
|
|
|
|
|
// Only keep variation selector if the previous character was ASCII
|
|
|
|
|
if previousWasASCII {
|
|
|
|
|
scalars.append(scalar)
|
|
|
|
|
}
|
|
|
|
|
// No need to update previousWasASCII since variation selectors aren't characters
|
|
|
|
|
// Shouldn't have 2 in a row
|
|
|
|
|
} else {
|
|
|
|
|
scalars.append(scalar)
|
|
|
|
|
previousWasASCII = scalar.isASCII
|
2025-02-28 08:16:56 -05:00
|
|
|
}
|
2025-03-15 11:39:46 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return scalars.compactMap { UnicodeScalar($0) }
|
2025-02-28 08:16:56 -05:00
|
|
|
.map { String($0) }
|
|
|
|
|
.joined()
|
|
|
|
|
}
|
2025-03-15 09:47:52 -04:00
|
|
|
|
2025-05-11 16:12:34 -07:00
|
|
|
/// Formats a short name like "P130" to read as "Node P 130" for VoiceOver
|
|
|
|
|
/// This ensures proper pronunciation of alphanumeric node IDs
|
|
|
|
|
func formatNodeNameForVoiceOver() -> String {
|
|
|
|
|
let spaced = self.replacingOccurrences(
|
|
|
|
|
of: #"([A-Za-z])([0-9]+)"#,
|
|
|
|
|
with: "$1 $2",
|
|
|
|
|
options: .regularExpression
|
|
|
|
|
)
|
2025-05-20 21:16:12 -07:00
|
|
|
return "Node".localized + " " + spaced
|
2025-05-11 16:12:34 -07:00
|
|
|
}
|
|
|
|
|
|
2025-03-15 09:47:52 -04:00
|
|
|
// Adds variation selectors to prefer the graphical form of emoji.
|
|
|
|
|
// Looks ahead to make sure that the variation selector is not already applied.
|
|
|
|
|
var addingVariationSelectors: String {
|
|
|
|
|
var result = ""
|
2025-03-15 11:39:46 -04:00
|
|
|
let scalars = self.unicodeScalars
|
2025-03-15 09:47:52 -04:00
|
|
|
var index = scalars.startIndex
|
|
|
|
|
while index < scalars.endIndex {
|
|
|
|
|
let currentScalar = scalars[index]
|
|
|
|
|
result += String(currentScalar)
|
2025-03-15 10:13:19 -04:00
|
|
|
if currentScalar.properties.isEmoji && !currentScalar.properties.isEmojiPresentation && !currentScalar.isASCII {
|
2025-03-15 09:47:52 -04:00
|
|
|
// Check if the next scalar is U+FE0F
|
|
|
|
|
let nextIndex = scalars.index(after: index)
|
|
|
|
|
if nextIndex < scalars.endIndex && scalars[nextIndex].value == 0xFE0F {
|
|
|
|
|
// Already has variation selector; skip the next scalar
|
|
|
|
|
index = nextIndex
|
|
|
|
|
} else {
|
|
|
|
|
// Append variation selector
|
|
|
|
|
result += String(UnicodeScalar(0xFE0F)!)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Move to the next scalar
|
|
|
|
|
index = scalars.index(after: index)
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
2023-04-25 17:56:57 -07:00
|
|
|
}
|