diff --git a/mobile/Heygo.xcodeproj/project.xcworkspace/xcuserdata/junv.xcuserdatad/UserInterfaceState.xcuserstate b/mobile/Heygo.xcodeproj/project.xcworkspace/xcuserdata/junv.xcuserdatad/UserInterfaceState.xcuserstate index 677581b..10bd4af 100644 Binary files a/mobile/Heygo.xcodeproj/project.xcworkspace/xcuserdata/junv.xcuserdatad/UserInterfaceState.xcuserstate and b/mobile/Heygo.xcodeproj/project.xcworkspace/xcuserdata/junv.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/mobile/Heygo/Resources/en.lproj/Localizable.strings b/mobile/Heygo/Resources/en.lproj/Localizable.strings index ae29bfa..3598173 100644 --- a/mobile/Heygo/Resources/en.lproj/Localizable.strings +++ b/mobile/Heygo/Resources/en.lproj/Localizable.strings @@ -29,6 +29,8 @@ "link_type" = "Link Type"; "markdown_content" = "Markdown Content"; "template_help" = "Use {parameter} or {parameter,default=value} for templates"; +"invalid_template_format" = "Invalid template format. Use {parameter} or {parameter,default=value}"; +"invalid_base_url_format" = "Invalid base URL format"; /* Tags */ "tags" = "Tags"; diff --git a/mobile/Heygo/Resources/zh-Hans.lproj/Localizable.strings b/mobile/Heygo/Resources/zh-Hans.lproj/Localizable.strings index 71deffd..6da33ff 100644 --- a/mobile/Heygo/Resources/zh-Hans.lproj/Localizable.strings +++ b/mobile/Heygo/Resources/zh-Hans.lproj/Localizable.strings @@ -29,6 +29,8 @@ "link_type" = "链接类型"; "markdown_content" = "Markdown 内容"; "template_help" = "使用 {参数} 或 {参数,default=值} 创建模板"; +"invalid_template_format" = "无效的模板格式。请使用 {参数} 或 {参数,default=值}"; +"invalid_base_url_format" = "无效的基础网址格式"; /* Tags */ "tags" = "标签"; diff --git a/mobile/Heygo/Utils/URLTemplateProcessor.swift b/mobile/Heygo/Utils/URLTemplateProcessor.swift index f6bf1b6..6a87aa1 100644 --- a/mobile/Heygo/Utils/URLTemplateProcessor.swift +++ b/mobile/Heygo/Utils/URLTemplateProcessor.swift @@ -6,9 +6,19 @@ struct URLTemplateProcessor { let extractedParams = extractParameters(from: template) for param in extractedParams { - let placeholder = "{\(param.name)" + (param.defaultValue != nil ? ",default=\(param.defaultValue!)" : "") + "}" + // Create patterns to match both formats: {name} and {name, default=value} + let patterns = [ + "\\{\(NSRegularExpression.escapedPattern(for: param.name))\\}", + "\\{\(NSRegularExpression.escapedPattern(for: param.name))\\s*,\\s*default\\s*=\\s*\(NSRegularExpression.escapedPattern(for: param.defaultValue ?? ""))\\}" + ] + let value = parameters[param.name] ?? param.defaultValue ?? "" - processedURL = processedURL.replacingOccurrences(of: placeholder, with: value) + + for pattern in patterns { + if let regex = try? NSRegularExpression(pattern: pattern, options: []) { + processedURL = regex.stringByReplacingMatches(in: processedURL, options: [], range: NSRange(location: 0, length: processedURL.utf16.count), withTemplate: value) + } + } } return processedURL @@ -16,7 +26,8 @@ struct URLTemplateProcessor { static func extractParameters(from template: String) -> [TemplateParameter] { var parameters: [TemplateParameter] = [] - let pattern = "\\{([^,}]+)(?:,default=([^}]+))?\\}" + // Updated pattern to handle optional spaces around comma and default= + let pattern = "\\{([^,}]+)(?:\\s*,\\s*default\\s*=\\s*([^}]+))?\\}" guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return parameters @@ -29,12 +40,12 @@ struct URLTemplateProcessor { let defaultRange = match.range(at: 2) if let nameSwiftRange = Range(nameRange, in: template) { - let name = String(template[nameSwiftRange]) + let name = String(template[nameSwiftRange]).trimmingCharacters(in: .whitespaces) var defaultValue: String? = nil if defaultRange.location != NSNotFound, let defaultSwiftRange = Range(defaultRange, in: template) { - defaultValue = String(template[defaultSwiftRange]) + defaultValue = String(template[defaultSwiftRange]).trimmingCharacters(in: .whitespaces) } parameters.append(TemplateParameter(name: name, defaultValue: defaultValue)) diff --git a/mobile/Heygo/Views/LinkFormView.swift b/mobile/Heygo/Views/LinkFormView.swift index 440a07e..36bc1ef 100644 --- a/mobile/Heygo/Views/LinkFormView.swift +++ b/mobile/Heygo/Views/LinkFormView.swift @@ -45,6 +45,44 @@ struct LinkFormView: View { urlValidationMessage.isEmpty } + var templateValidationView: some View { + Group { + if selectedLinkType == .template && !originalURL.isEmpty { + let isValidTemplate = URLTemplateProcessor.validateTemplate(originalURL) + let hasParameters = URLTemplateProcessor.hasParameters(originalURL) + + if isValidTemplate && hasParameters { + // Valid template with parameters - show success with iOS blue + HStack { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.blue) + Text("Valid template with parameters") + .font(.caption) + .foregroundColor(.blue) + } + } else if !hasParameters { + // No parameters detected - show warning + HStack { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.orange) + Text("Template should contain parameters like {query} or {query,default=value}") + .font(.caption) + .foregroundColor(.orange) + } + } else { + // Invalid template - show error + HStack { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.red) + Text("Invalid template syntax. Use {parameter} or {parameter,default=value}") + .font(.caption) + .foregroundColor(.red) + } + } + } + } + } + var body: some View { NavigationView { Form { @@ -99,7 +137,7 @@ struct LinkFormView: View { .textFieldStyle(RoundedBorderTextFieldStyle()) .overlay( RoundedRectangle(cornerRadius: 8) - .stroke(!urlValidationMessage.isEmpty ? Color.orange : Color.clear, lineWidth: 2) + .stroke(getBorderColor(), lineWidth: 2) ) .onChange(of: originalURL) { _, newValue in validateURL(newValue) @@ -116,9 +154,27 @@ struct LinkFormView: View { } if selectedLinkType == .template { - Text("template_help") - .font(.caption) - .foregroundColor(.secondary) + VStack(alignment: .leading, spacing: 8) { + // Template help text + VStack(alignment: .leading, spacing: 4) { + Text("Template type allows you to create dynamic URLs with parameters. Use {query, default=value} syntax in the URL.") + .font(.caption) + .foregroundColor(.secondary) + + Text("Example: https://google.com/search?q={query, default=hello}") + .font(.caption) + .foregroundColor(.secondary) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.gray.opacity(0.1)) + .cornerRadius(4) + } + + // Template validation status + if !originalURL.isEmpty { + templateValidationView + } + } } } } @@ -263,12 +319,21 @@ struct LinkFormView: View { return } - // For templates, allow URLs with placeholders like {query} + // For templates, use URLTemplateProcessor for validation if selectedLinkType == .template { - // Check if it's a valid URL pattern with or without placeholders - let urlWithoutPlaceholders = url.replacingOccurrences(of: #"\{[^}]+\}"#, with: "placeholder", options: .regularExpression) - if isValidURL(urlWithoutPlaceholders) { - urlValidationMessage = "" + // Check if it's a valid template + if URLTemplateProcessor.validateTemplate(url) { + // Check if the base URL (without parameters) is valid + let urlWithoutPlaceholders = url.replacingOccurrences(of: #"\{[^}]+\}"#, with: "placeholder", options: .regularExpression) + if isValidURL(urlWithoutPlaceholders) { + urlValidationMessage = "" + return + } else { + urlValidationMessage = NSLocalizedString("invalid_base_url_format", comment: "Invalid base URL format") + return + } + } else { + urlValidationMessage = NSLocalizedString("invalid_template_format", comment: "Invalid template format") return } } @@ -312,6 +377,25 @@ struct LinkFormView: View { return true } + private func getBorderColor() -> Color { + // Show error state if there's a validation message + if !urlValidationMessage.isEmpty { + return .orange + } + + // For template links, show blue if valid template with parameters + if selectedLinkType == .template && !originalURL.isEmpty { + let isValidTemplate = URLTemplateProcessor.validateTemplate(originalURL) + let hasParameters = URLTemplateProcessor.hasParameters(originalURL) + + if isValidTemplate && hasParameters { + return .blue + } + } + + return .clear + } + private func toggleTag(_ tag: Tag) { if selectedTags.contains(where: { $0.id == tag.id }) { selectedTags.removeAll { $0.id == tag.id } diff --git a/mobile/TEMPLATE_ENHANCEMENT_SUMMARY.md b/mobile/TEMPLATE_ENHANCEMENT_SUMMARY.md new file mode 100644 index 0000000..ab561f4 --- /dev/null +++ b/mobile/TEMPLATE_ENHANCEMENT_SUMMARY.md @@ -0,0 +1,80 @@ +# Template Link Enhancement Implementation + +This document summarizes the enhancements made to the iOS app's template link functionality. + +## Features Implemented + +### 1. Enhanced Template Help Text +- **Location**: LinkFormView.swift (when template type is selected) +- **Content**: Added detailed explanation with example + ``` + Template type allows you to create dynamic URLs with parameters. Use {query, default=value} syntax in the URL. + + Example: https://google.com/search?q={query, default=hello} + ``` + +### 2. Real-time Template Validation +- **Visual feedback with iOS blue highlighting** when template is valid +- **Warning messages** when template has no parameters +- **Error messages** when template syntax is invalid + +### 3. Border Color Indicators +- **Blue border**: Valid template with parameters +- **Orange border**: Invalid URL or template syntax error +- **Clear border**: Normal state + +### 4. Template Validation States +- ✅ **Valid with parameters**: Shows blue checkmark with "Valid template with parameters" +- ⚠️ **No parameters**: Shows orange warning "Template should contain parameters like {query} or {query,default=value}" +- ❌ **Invalid syntax**: Shows red error "Invalid template syntax. Use {parameter} or {parameter,default=value}" + +## Technical Implementation + +### URLTemplateProcessor Enhancements +- **Updated regex pattern** to handle flexible spacing: `{query, default=value}` vs `{query,default=value}` +- **Improved parameter extraction** with whitespace trimming +- **Enhanced validation** using existing processor methods + +### LinkFormView Changes +- **New computed property**: `templateValidationView` for real-time feedback +- **Enhanced border styling**: `getBorderColor()` method for dynamic colors +- **Improved URL validation**: Better template-specific validation logic + +### Localization Updates +- **English**: Added `invalid_template_format` and `invalid_base_url_format` +- **Chinese**: Added corresponding translations + +## Supported Template Formats + +The implementation supports both syntax formats: +- `{parameter}` - Required parameter +- `{parameter, default=value}` - Parameter with default value (flexible spacing) +- `{parameter,default=value}` - Parameter with default value (no spacing) + +## Example Templates + +✅ **Valid Templates:** +- `https://google.com/search?q={query}` +- `https://google.com/search?q={query, default=hello}` +- `https://github.com/{user}/{repo}` +- `https://maps.apple.com/?q={location,default=New York}` + +⚠️ **Templates needing parameters:** +- `https://example.com/no-parameters` + +❌ **Invalid syntax examples:** +- URLs with malformed parameter syntax +- Non-URL strings when template type is selected + +## User Experience + +1. **Select Template type** in link form +2. **See helpful tips** with syntax explanation and example +3. **Enter URL with parameters** - real-time validation appears +4. **Visual feedback**: + - Blue border and checkmark for valid templates + - Orange warning for templates without parameters + - Red error for invalid syntax +5. **Clear error messages** to guide correction + +The implementation provides a smooth, intuitive experience for creating dynamic URL templates with immediate feedback. diff --git a/mobile/template_test.swift b/mobile/template_test.swift new file mode 100644 index 0000000..77cc948 --- /dev/null +++ b/mobile/template_test.swift @@ -0,0 +1,123 @@ +#!/usr/bin/env swift + +import Foundation + +// Copy the URLTemplateProcessor code for testing +struct URLTemplateProcessor { + static func processTemplate(_ template: String, with parameters: [String: String]) -> String { + var processedURL = template + let extractedParams = extractParameters(from: template) + + for param in extractedParams { + let placeholder = "{\(param.name)" + (param.defaultValue != nil ? ",default=\(param.defaultValue!)" : "") + "}" + let value = parameters[param.name] ?? param.defaultValue ?? "" + processedURL = processedURL.replacingOccurrences(of: placeholder, with: value) + } + + return processedURL + } + + static func extractParameters(from template: String) -> [TemplateParameter] { + var parameters: [TemplateParameter] = [] + // Updated pattern to handle optional spaces around comma and default= + let pattern = "\\{([^,}]+)(?:\\s*,\\s*default\\s*=\\s*([^}]+))?\\}" + + guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { + return parameters + } + + let matches = regex.matches(in: template, options: [], range: NSRange(location: 0, length: template.utf16.count)) + + for match in matches { + let nameRange = match.range(at: 1) + let defaultRange = match.range(at: 2) + + if let nameSwiftRange = Range(nameRange, in: template) { + let name = String(template[nameSwiftRange]).trimmingCharacters(in: .whitespaces) + var defaultValue: String? = nil + + if defaultRange.location != NSNotFound, + let defaultSwiftRange = Range(defaultRange, in: template) { + defaultValue = String(template[defaultSwiftRange]).trimmingCharacters(in: .whitespaces) + } + + parameters.append(TemplateParameter(name: name, defaultValue: defaultValue)) + } + } + + return parameters + } + + static func validateTemplate(_ template: String) -> Bool { + let extractedParams = extractParameters(from: template) + + // Check if all placeholders are valid + let pattern = "\\{[^{}]*\\}" + guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { + return false + } + + let matches = regex.matches(in: template, options: [], range: NSRange(location: 0, length: template.utf16.count)) + + // Each match should correspond to a valid parameter + for match in matches { + if let matchRange = Range(match.range, in: template) { + let matchString = String(template[matchRange]) + let isValid = extractedParams.contains { param in + matchString.contains(param.name) + } + if !isValid { + return false + } + } + } + + return true + } + + static func hasParameters(_ template: String) -> Bool { + return template.contains("{") && template.contains("}") + } +} + +struct TemplateParameter { + let name: String + let defaultValue: String? + + var isRequired: Bool { + return defaultValue == nil + } +} + +// Test cases +print("Testing URLTemplateProcessor...") + +let testCases = [ + "https://google.com/search?q={query, default=hello}", + "https://google.com/search?q={query}", + "https://github.com/{user}/{repo}", + "https://youtube.com/watch?v={videoId}", + "https://maps.apple.com/?q={location,default=New York}", + "https://example.com/{invalid syntax}", + "https://example.com/no-parameters", + "https://example.com/{query,default=test}/{page,default=1}" +] + +for testCase in testCases { + let isValid = URLTemplateProcessor.validateTemplate(testCase) + let hasParams = URLTemplateProcessor.hasParameters(testCase) + let parameters = URLTemplateProcessor.extractParameters(from: testCase) + + print("\nTest: \(testCase)") + print(" Valid: \(isValid)") + print(" Has Parameters: \(hasParams)") + print(" Parameters: \(parameters.map { "\($0.name)" + (($0.defaultValue != nil) ? ",default=\($0.defaultValue!)" : "") })") + + if isValid && hasParams { + print(" ✅ Would show blue border") + } else if !hasParams { + print(" ⚠️ Would show warning (no parameters)") + } else { + print(" ❌ Would show error (invalid)") + } +}