A Swift package for parsing and processing dynamic URL templates with Raycast-compatible placeholder syntax.
QuickLinkParser processes URL templates containing dynamic placeholders for user input, clipboard content, selected text, and date/time values. It supports Raycast's template syntax including modifier chains for transforming values.
- π― Full Raycast Compatibility - Supports Raycast's template syntax
- π System Integration - Access clipboard and selected text (with permissions)
- π Date/Time Support - Custom formats and offsets
- π§ Modifier Chains - Transform values with trim, encode, case conversion
- ποΈ Template Analysis - Extract requirements before processing
- β Validation - Check template syntax with detailed errors
- π Cross-Platform - Works on iOS and macOS
Add QuickLinkParser to your project in Xcode:
- File β Add Package Dependencies
- Enter the repository URL:
https://github.com/arraypress/swift-quicklink-parser
- Choose the version and add to your target
Or add it to your Package.swift
:
dependencies: [
.package(url: "https://github.com/arraypress/swift-quicklink-parser", from: "1.0.0")
]
import QuickLinkParser
// Simple example
let template = "https://google.com/search?q={selection | percent-encode}"
let result = QuickLinkParser.process(
template,
selection: "Swift programming"
)
if result.success {
print(result.url) // "https://google.com/search?q=Swift%20programming"
}
// With system access (auto-detects clipboard/selection)
let result = QuickLinkParser.processWithSystemAccess(template)
{argument name="query"} // Required argument
{argument name="query" default="search"} // Optional with default
{argument name="lang" options="en, es, fr"} // Simple dropdown options
// Label|value syntax for user-friendly options
{argument name="filter" options="Videos|EgIQAQ%3D, Channels|EgIQAg%3D"}
The options
attribute supports two formats:
- Simple:
"option1, option2, option3"
- Label and value are the same - Label|Value:
"Display Label|actual_value"
- Different label and value
This is particularly useful for:
- Encoded values (YouTube filters, API tokens)
- Technical IDs with friendly names
- URLs with descriptive labels
{clipboard} // Current clipboard content
{selection} // Currently selected text (macOS only)
{date} // Current date (system format)
{time} // Current time
{datetime} // Date and time combined
{date format="yyyy-MM-dd"} // Custom format
{date format="MMM d" offset="+7d"} // 7 days from now
{date offset="-1M"} // 1 month ago
{clipboard | percent-encode} // URL encode
{selection | trim} // Remove whitespace
{argument name="text" | lowercase} // Convert to lowercase
{clipboard | trim | lowercase | percent-encode} // Chain multiple
Available modifiers:
percent-encode
- URL encoding for safe URLstrim
- Remove leading/trailing whitespaceuppercase
- Convert to UPPERCASElowercase
- Convert to lowercasejson-stringify
- Escape for JSON strings
// Manual values - you provide everything
let result = QuickLinkParser.process(
template,
arguments: ["query": "test", "lang": "en"],
clipboard: "clipboard text",
selection: "selected text",
date: Date()
)
// With system access - automatically gets clipboard/selection
let result = QuickLinkParser.processWithSystemAccess(
template,
arguments: ["query": "test"]
)
// Check the result
if result.success {
// Use result.url
if let url = URL(string: result.url) {
UIApplication.shared.open(url) // iOS
NSWorkspace.shared.open(url) // macOS
}
} else {
// Handle missing arguments or errors
print("Missing: \(result.missingArguments)")
print("Errors: \(result.errors)")
}
// Get template requirements before processing
let info = QuickLinkParser.analyze(template)
// Build your UI based on requirements
for arg in info.arguments {
print("Argument: \(arg.name)")
print("Required: \(arg.required)")
print("Default: \(arg.defaultValue ?? "none")")
print("Options: \(arg.options ?? [])")
}
// Check what system features are needed
if info.usesSelection {
// May need accessibility permissions on macOS
}
if info.usesClipboard {
// Will need clipboard access
}
// Simple validation
if QuickLinkParser.validate(template) {
// Template syntax is valid
}
// Detailed validation with error messages
let validation = QuickLinkParser.validateWithErrors(template)
if !validation.isValid {
for error in validation.errors {
print("Syntax error: \(error)")
}
}
// Get clipboard content
if let clipboard = QuickLinkParser.getClipboard() {
print("Clipboard: \(clipboard)")
}
// Set clipboard content
SystemAccessHelper.setClipboard("New clipboard text")
// Check accessibility permissions (macOS only)
if !QuickLinkParser.hasAccessibilityPermission() {
// Show explanation to user first
showPermissionExplanation()
// Then request permission
SystemAccessHelper.requestAccessibilityPermission()
// Note: App may need restart after granting
}
// Get selected text (macOS only, requires permission)
if let selection = QuickLinkParser.getSelectedText() {
print("Selected: \(selection)")
}
- β Full clipboard support
- β Selected text with accessibility permissions
β οΈ Selection requires user approval in System Settings
Important: For selected text access, your app must:
- Add to
Info.plist
:
<key>NSAccessibilityUsageDescription</key>
<string>This app needs accessibility access to read selected text for QuickLinks</string>
- Handle permissions in your app:
// Check and request if needed
if !SystemAccessHelper.hasAccessibilityPermission() {
// Show your custom UI explaining why
showAccessibilityPermissionDialog()
// Open System Settings
SystemAccessHelper.openAccessibilitySettings()
// Note: App restart may be required
}
- β Full clipboard support
- β Selected text not available (iOS limitation)
- β No special permissions required
On iOS, {selection}
placeholders will return nil
. Design your templates accordingly or provide clipboard as fallback.
let template = "https://google.com/search?q={selection | percent-encode}"
let result = QuickLinkParser.process(template, selection: "Swift tutorials")
// Result: "https://google.com/search?q=Swift%20tutorials"
let template = "https://github.com/{argument name=\"owner\"}/{argument name=\"repo\"}"
let result = QuickLinkParser.process(
template,
arguments: ["owner": "apple", "repo": "swift"]
)
// Result: "https://github.com/apple/swift"
let template = """
https://translate.google.com/
?sl={argument name="from" default="auto"}
&tl={argument name="to" default="en"}
&text={selection | percent-encode}
"""
let result = QuickLinkParser.process(
template,
arguments: ["from": "es", "to": "en"],
selection: "Hola mundo"
)
// Result: "https://translate.google.com/?sl=es&tl=en&text=Hola%20mundo"
let template = """
https://youtube.com/results?search_query={argument name="query" | percent-encode}
&sp={argument name="filter" options="Any|, Videos|EgIQAQ%253D%253D, Channels|EgIQAg%253D%253D,
Playlists|EgIQAw%253D%253D, This Week|CAISBAgCEAE, This Month|CAISBAgDEAE" default=""}
"""
let info = QuickLinkParser.analyze(template)
// info.arguments[1].options contains:
// - ArgumentOption(label: "Any", value: "")
// - ArgumentOption(label: "Videos", value: "EgIQAQ%253D%253D")
// - ArgumentOption(label: "Channels", value: "EgIQAg%253D%253D")
// etc.
// In your UI, show the labels:
Picker("Filter", selection: $selectedFilter) {
ForEach(info.arguments[1].options ?? [], id: \.value) { option in
Text(option.label).tag(option.value)
}
}
// Process with the value:
let result = QuickLinkParser.process(
template,
arguments: ["query": "Swift tutorials", "filter": "EgIQAQ%253D%253D"]
)
// Result: "https://youtube.com/results?search_query=Swift%20tutorials&sp=EgIQAQ%253D%253D"
let template = """
https://calendar.google.com/calendar/render
?action=TEMPLATE
&text={argument name="title" | percent-encode}
&dates={date format="yyyyMMdd'T'HHmmss"}/{date format="yyyyMMdd'T'HHmmss" offset="+1h"}
"""
let result = QuickLinkParser.process(
template,
arguments: ["title": "Team Meeting"]
)
// Creates a 1-hour calendar event starting now
let template = "https://amazon.com/s?k={clipboard | trim | percent-encode}"
let result = QuickLinkParser.processWithSystemAccess(template)
// Uses current clipboard content
Here's how to build a dynamic UI based on template requirements:
import SwiftUI
import QuickLinkParser
struct QuickLinkView: View {
let template: String
@State private var arguments: [String: String] = [:]
@State private var error: String?
var templateInfo: TemplateInfo {
QuickLinkParser.analyze(template)
}
var body: some View {
Form {
// Generate input fields for each argument
ForEach(templateInfo.arguments, id: \.name) { arg in
Section(arg.name) {
if let options = arg.options {
// Dropdown for options
Picker(arg.name, selection: binding(for: arg)) {
ForEach(options, id: \.self) { option in
Text(option).tag(option)
}
}
} else {
// Text field for free input
TextField(
arg.defaultValue ?? "Enter \(arg.name)",
text: binding(for: arg)
)
}
if arg.required {
Text("Required").font(.caption).foregroundColor(.red)
}
}
}
// System requirements
if templateInfo.usesClipboard {
Text("π Will use clipboard content").font(.caption)
}
if templateInfo.usesSelection {
Text("βοΈ Will use selected text").font(.caption)
}
Button("Open Link") {
openQuickLink()
}
if let error = error {
Text(error).foregroundColor(.red)
}
}
}
func binding(for arg: ArgumentInfo) -> Binding<String> {
Binding(
get: { arguments[arg.name] ?? arg.defaultValue ?? "" },
set: { arguments[arg.name] = $0 }
)
}
func openQuickLink() {
let result = QuickLinkParser.processWithSystemAccess(
template,
arguments: arguments
)
if result.success {
if let url = URL(string: result.url) {
#if os(iOS)
UIApplication.shared.open(url)
#elseif os(macOS)
NSWorkspace.shared.open(url)
#endif
}
} else {
error = "Missing: \(result.missingArguments.joined(separator: ", "))"
}
}
}
struct ProcessResult {
let url: String // Processed URL ready to use
let missingArguments: [String] // Required args not provided
let success: Bool // true if all requirements met
let errors: [String] // Any parsing errors
}
struct TemplateInfo {
let arguments: [ArgumentInfo] // All arguments found
let usesClipboard: Bool // Uses {clipboard}
let usesSelection: Bool // Uses {selection}
let usesDate: Bool // Uses date/time placeholders
let dateFormats: [String] // Custom date formats found
var requiredArguments: [ArgumentInfo] // Args without defaults
var optionalArguments: [ArgumentInfo] // Args with defaults
}
struct ArgumentInfo {
let name: String // Argument identifier
let defaultValue: String? // Default if specified
let options: [ArgumentOption]? // Dropdown options if specified
let required: Bool // true if no default value
var hasOptions: Bool // Has dropdown options
var hasDefault: Bool // Has a default value
}
struct ArgumentOption {
let label: String // Display label ("Videos")
let value: String // Actual value ("EgIQAQ%253D%253D")
}
// ISO 8601
{date format="yyyy-MM-dd'T'HH:mm:ssZ"}
// Human readable
{date format="EEEE, MMMM d, yyyy 'at' h:mm a"}
// File naming
{date format="yyyyMMdd_HHmmss"}
// Process in order: trim β lowercase β encode
{clipboard | trim | lowercase | percent-encode}
// Each modifier transforms the previous result
" HELLO WORLD " β "HELLO WORLD" β "hello world" β "hello%20world"
// In your app logic
let result = QuickLinkParser.process(
template,
selection: getSelectedText(),
clipboard: getSelectedText() ?? getClipboard() // Fallback
)
QuickLinkParser is optimized for speed:
- β‘ ~0.1ms for simple templates
- β‘ ~0.5ms for complex templates with multiple placeholders
- β‘ Efficient regex-based parsing
- β‘ Minimal memory allocation
Suitable for real-time processing in response to hotkeys or user actions.
The package includes comprehensive tests:
# Run all tests
swift test
# Run with coverage
swift test --enable-code-coverage
# Run specific tests
swift test --filter QuickLinkParserTests.testGoogleSearchTemplate
QuickLinkParser/
βββ Sources/
β βββ QuickLinkParser/
β βββ QuickLinkParser.swift # Main API
β βββ Models.swift # Data types
β βββ SystemAccessHelper.swift # Platform-specific
βββ Tests/
βββ QuickLinkParserTests/
βββ QuickLinkParserTests.swift # Test suite
- Swift 5.9+
- iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+
- Xcode 14.0+ (for development)
- iOS: Selected text access not available (platform limitation)
- macOS: Selection requires accessibility permissions
- Clipboard: Only supports current clipboard (no history)
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
QuickLinkParser is available under the MIT license. See LICENSE file for details.
- Syntax compatible with Raycast QuickLinks
- Inspired by URL template systems and text expansion tools
Made with β€οΈ for the Swift community