Files
oai-swift/oAI/Utilities/Extensions/String+Extensions.swift
T
runeandClaude Sonnet 5 cf3f4ebfe4 Relicense oAI from AGPL-3.0-or-later to PolyForm Noncommercial 1.0.0
Switches the project from AGPL to a source-available license that
restricts commercial use — selling oAI or any part of it, standalone
or bundled into another product/service, now requires a separate
commercial license from the copyright holder. Noncommercial use,
study, modification, and sharing remain fully permitted.

Updates: LICENSE (canonical PolyForm Noncommercial 1.0.0 text +
commercial licensing contact note), SPDX headers and file-header
boilerplate across all Swift source files, the in-app About dialog's
license link (+ its localization catalog entry), README.md and
DEVELOPMENT.md license sections.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:16:09 +02:00

111 lines
3.6 KiB
Swift

//
// String+Extensions.swift
// oAI
//
// String utility extensions
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
//
// This file is part of oAI.
//
// oAI is licensed under the PolyForm Noncommercial License 1.0.0.
// You may use, study, modify, and share it for any noncommercial
// purpose. Commercial use — including selling oAI or any part of
// it, standalone or bundled into another product or service —
// requires a separate commercial license from the copyright holder.
//
// See the LICENSE file or
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
// the full license text. For commercial licensing, contact Rune
// Olsen via <https://mac.oai.pm>.
import Foundation
extension String {
// MARK: - Command Parsing
var isSlashCommand: Bool {
hasPrefix("/")
}
func parseCommand() -> (command: String, args: [String])? {
guard isSlashCommand else { return nil }
let parts = self.split(separator: " ", omittingEmptySubsequences: true)
.map(String.init)
guard let command = parts.first else { return nil }
let args = Array(parts.dropFirst())
return (command, args)
}
// MARK: - File Attachment Parsing
func parseFileAttachments() -> (cleanText: String, filePaths: [String]) {
var cleanText = self
var filePaths: [String] = []
// Pattern 1: @<filepath>
let anglePattern = #"@<([^>]+)>"#
if let regex = try? NSRegularExpression(pattern: anglePattern) {
let matches = regex.matches(in: self, range: NSRange(self.startIndex..., in: self))
for match in matches.reversed() {
if let range = Range(match.range(at: 1), in: self) {
let path = String(self[range])
filePaths.insert(path, at: 0)
}
if let fullRange = Range(match.range, in: self) {
cleanText.removeSubrange(fullRange)
}
}
}
// Pattern 2: @filepath (starting with /, ~, ., or drive letter)
let directPattern = #"@([~/.][\S]+|[A-Za-z]:[\\\/][\S]+)"#
if let regex = try? NSRegularExpression(pattern: directPattern) {
let matches = regex.matches(in: cleanText, range: NSRange(cleanText.startIndex..., in: cleanText))
for match in matches.reversed() {
if let range = Range(match.range(at: 1), in: cleanText) {
let path = String(cleanText[range])
if !filePaths.contains(path) {
filePaths.insert(path, at: 0)
}
}
if let fullRange = Range(match.range, in: cleanText) {
cleanText.removeSubrange(fullRange)
}
}
}
return (cleanText.trimmingCharacters(in: .whitespaces), filePaths)
}
// MARK: - Token Estimation
func estimateTokens() -> Int {
// Rough estimation: ~4 characters per token
// This is approximate; Phase 2 will use proper tokenizer
return max(1, count / 4)
}
// MARK: - Nil coalescing helpers
/// Returns nil if the string is empty, otherwise self.
var nonEmptyOrNil: String? {
isEmpty ? nil : self
}
// MARK: - Truncation
func truncated(to length: Int, trailing: String = "...") -> String {
if count <= length {
return self
}
let endIndex = index(startIndex, offsetBy: length - trailing.count)
return String(self[..<endIndex]) + trailing
}
}