From 4c3783fd457ccbbd45137aa3cc0377e757b0210a Mon Sep 17 00:00:00 2001 From: Junwei Zhao Date: Sat, 16 Aug 2025 18:05:45 +1000 Subject: [PATCH] template works --- .../UserInterfaceState.xcuserstate | Bin 48658 -> 50008 bytes .../Resources/en.lproj/Localizable.strings | 2 + .../zh-Hans.lproj/Localizable.strings | 2 + mobile/Heygo/Utils/URLTemplateProcessor.swift | 21 ++- mobile/Heygo/Views/LinkFormView.swift | 102 +++++++++++++-- mobile/TEMPLATE_ENHANCEMENT_SUMMARY.md | 80 ++++++++++++ mobile/template_test.swift | 123 ++++++++++++++++++ 7 files changed, 316 insertions(+), 14 deletions(-) create mode 100644 mobile/TEMPLATE_ENHANCEMENT_SUMMARY.md create mode 100644 mobile/template_test.swift 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 677581be80ccbef7cd1edb40f4f897de71108c75..10bd4af8f9932d369384f82616bca2e5c9570f84 100644 GIT binary patch delta 21542 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 II&kg(0WV5>+W-In 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)") + } +}