Showing preview only (439K chars total). Download the full file or copy to clipboard to get everything.
Repository: lexrus/RegExPlus
Branch: master
Commit: f10137f28dff
Files: 164
Total size: 380.0 KB
Directory structure:
gitextract__mufejjy/
├── .github/
│ └── workflow/
│ └── claude.yml
├── .gitignore
├── .swift-version
├── .swiftlint.yml
├── AGENTS.md
├── CLAUDE.md
├── LICENSE
├── README.md
├── RegEx+/
│ ├── About/
│ │ └── AboutView.swift
│ ├── AppDelegate.swift
│ ├── Assets.xcassets/
│ │ ├── AccentColor.colorset/
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset/
│ │ │ └── Contents.json
│ │ ├── AppIconForAboutView.imageset/
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── Base.lproj/
│ │ └── LaunchScreen.storyboard
│ ├── CheatSheet/
│ │ └── CheatSheetView.swift
│ ├── CoreData+CloudKit/
│ │ ├── DataManager.swift
│ │ ├── RegEx.swift
│ │ └── RegExFetch.swift
│ ├── Editor/
│ │ ├── EditorView.swift
│ │ ├── EditorViewModel.swift
│ │ └── RegExFlowView.swift
│ ├── HomeView.swift
│ ├── Info.plist
│ ├── Library/
│ │ ├── LibraryItemView.swift
│ │ ├── LibraryView+Data.swift
│ │ └── LibraryView.swift
│ ├── Localizable.xcstrings
│ ├── Preview Content/
│ │ └── Preview Assets.xcassets/
│ │ └── Contents.json
│ ├── RegEx+.entitlements
│ ├── RegEx.xcdatamodeld/
│ │ ├── .xccurrentversion
│ │ └── RegEx.xcdatamodel/
│ │ └── contents
│ ├── SceneDelegate.swift
│ ├── Views/
│ │ ├── ActivityViewController.swift
│ │ ├── RegExSyntaxView.swift
│ │ ├── RegExTextView/
│ │ │ ├── MatchesTextView.swift
│ │ │ ├── RegExSyntaxHighlighter.swift
│ │ │ ├── RegExTextView.swift
│ │ │ ├── ShortcutKeys.swift
│ │ │ └── String+NSRange.swift
│ │ ├── SafariView.swift
│ │ └── SearchView.swift
│ ├── de.lproj/
│ │ └── CheatSheet.plist
│ ├── en.lproj/
│ │ └── CheatSheet.plist
│ ├── es.lproj/
│ │ └── CheatSheet.plist
│ ├── fr.lproj/
│ │ └── CheatSheet.plist
│ ├── it.lproj/
│ │ └── CheatSheet.plist
│ ├── ja.lproj/
│ │ └── CheatSheet.plist
│ ├── ko.lproj/
│ │ └── CheatSheet.plist
│ ├── nl.lproj/
│ │ └── CheatSheet.plist
│ ├── pl.lproj/
│ │ └── CheatSheet.plist
│ ├── zh-Hans.lproj/
│ │ └── CheatSheet.plist
│ └── zh-Hant.lproj/
│ └── CheatSheet.plist
├── RegEx+.xcodeproj/
│ └── project.pbxproj
├── fastlane/
│ ├── Deliverfile
│ ├── Fastfile
│ └── metadata/
│ ├── copyright.txt
│ ├── de-DE/
│ │ ├── apple_tv_privacy_policy.txt
│ │ ├── description.txt
│ │ ├── keywords.txt
│ │ ├── marketing_url.txt
│ │ ├── name.txt
│ │ ├── privacy_url.txt
│ │ ├── promotional_text.txt
│ │ ├── release_notes.txt
│ │ ├── subtitle.txt
│ │ └── support_url.txt
│ ├── en-US/
│ │ ├── apple_tv_privacy_policy.txt
│ │ ├── description.txt
│ │ ├── keywords.txt
│ │ ├── marketing_url.txt
│ │ ├── name.txt
│ │ ├── privacy_url.txt
│ │ ├── promotional_text.txt
│ │ ├── release_notes.txt
│ │ ├── subtitle.txt
│ │ └── support_url.txt
│ ├── es-ES/
│ │ ├── apple_tv_privacy_policy.txt
│ │ ├── description.txt
│ │ ├── keywords.txt
│ │ ├── marketing_url.txt
│ │ ├── name.txt
│ │ ├── privacy_url.txt
│ │ ├── promotional_text.txt
│ │ ├── release_notes.txt
│ │ ├── subtitle.txt
│ │ └── support_url.txt
│ ├── fr-FR/
│ │ ├── apple_tv_privacy_policy.txt
│ │ ├── description.txt
│ │ ├── keywords.txt
│ │ ├── marketing_url.txt
│ │ ├── name.txt
│ │ ├── privacy_url.txt
│ │ ├── promotional_text.txt
│ │ ├── release_notes.txt
│ │ ├── subtitle.txt
│ │ └── support_url.txt
│ ├── it/
│ │ ├── apple_tv_privacy_policy.txt
│ │ ├── description.txt
│ │ ├── keywords.txt
│ │ ├── marketing_url.txt
│ │ ├── name.txt
│ │ ├── privacy_url.txt
│ │ ├── promotional_text.txt
│ │ ├── release_notes.txt
│ │ ├── subtitle.txt
│ │ └── support_url.txt
│ ├── ja/
│ │ ├── apple_tv_privacy_policy.txt
│ │ ├── description.txt
│ │ ├── keywords.txt
│ │ ├── marketing_url.txt
│ │ ├── name.txt
│ │ ├── privacy_url.txt
│ │ ├── promotional_text.txt
│ │ ├── release_notes.txt
│ │ ├── subtitle.txt
│ │ └── support_url.txt
│ ├── nl-NL/
│ │ ├── apple_tv_privacy_policy.txt
│ │ ├── description.txt
│ │ ├── keywords.txt
│ │ ├── marketing_url.txt
│ │ ├── name.txt
│ │ ├── privacy_url.txt
│ │ ├── promotional_text.txt
│ │ ├── release_notes.txt
│ │ ├── subtitle.txt
│ │ └── support_url.txt
│ ├── pl/
│ │ ├── apple_tv_privacy_policy.txt
│ │ ├── description.txt
│ │ ├── keywords.txt
│ │ ├── marketing_url.txt
│ │ ├── name.txt
│ │ ├── privacy_url.txt
│ │ ├── promotional_text.txt
│ │ ├── release_notes.txt
│ │ ├── subtitle.txt
│ │ └── support_url.txt
│ ├── primary_category.txt
│ ├── primary_first_sub_category.txt
│ ├── primary_second_sub_category.txt
│ ├── secondary_category.txt
│ ├── secondary_first_sub_category.txt
│ ├── secondary_second_sub_category.txt
│ ├── zh-Hans/
│ │ ├── apple_tv_privacy_policy.txt
│ │ ├── description.txt
│ │ ├── keywords.txt
│ │ ├── marketing_url.txt
│ │ ├── name.txt
│ │ ├── privacy_url.txt
│ │ ├── promotional_text.txt
│ │ ├── release_notes.txt
│ │ ├── subtitle.txt
│ │ └── support_url.txt
│ └── zh-Hant/
│ ├── apple_tv_privacy_policy.txt
│ ├── description.txt
│ ├── keywords.txt
│ ├── marketing_url.txt
│ ├── name.txt
│ ├── privacy_url.txt
│ ├── promotional_text.txt
│ ├── release_notes.txt
│ ├── subtitle.txt
│ └── support_url.txt
└── mise.toml
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflow/claude.yml
================================================
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1)
# model: "claude-opus-4-1-20250805"
model: ${{ vars.ANTHROPIC_MODEL }}
# Optional: Customize the trigger phrase (default: @claude)
# trigger_phrase: "/claude"
# Optional: Trigger when specific user is assigned to an issue
# assignee_trigger: "claude-bot"
# Optional: Allow Claude to run specific commands
# allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
# Optional: Add custom instructions for Claude to customize its behavior for your project
# custom_instructions: |
# Follow our coding standards
# Ensure all new code has tests
# Use TypeScript for new files
# Optional: Custom environment variables for Claude
claude_env: |
ANTHROPIC_BASE_URL: ${{ vars.ANTHROPIC_BASE_URL }}
================================================
FILE: .gitignore
================================================
# Created by https://www.gitignore.io/api/xcode,macos,fastlane
# Edit at https://www.gitignore.io/?templates=xcode,macos,fastlane
### fastlane ###
# fastlane - A streamlined workflow tool for Cocoa deployment
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
# fastlane specific
fastlane/report.xml
fastlane/Appfile
fastlane/metadata/**/review_information/
fastlane/keys
# deliver temporary files
fastlane/Preview.html
# snapshot generated screenshots
fastlane/screenshots
# scan temporary files
fastlane/test_output
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Xcode ###
# Xcode
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## User settings
xcuserdata/
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
## Xcode Patch
*.xcodeproj/*
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/
!*.xcworkspace/contents.xcworkspacedata
/*.gcno
### Xcode Patch ###
**/xcshareddata/WorkspaceSettings.xcsettings
# End of https://www.gitignore.io/api/xcode,macos,fastlane
================================================
FILE: .swift-version
================================================
5.9
================================================
FILE: .swiftlint.yml
================================================
disabled_rules:
- todo
- trailing_whitespace
- colon
- identifier_name
- comma
- vertical_whitespace
- type_name
- trailing_comma
- multiple_closures_with_trailing_closure
excluded:
- Carthage
- Pods
line_length:
- 200
- 220
function_body_length:
- 200
- 300
type_body_length:
- 300
- 500
file_length:
- 500
- 700
================================================
FILE: AGENTS.md
================================================
# Repository Guidelines
## Project Structure & Module Organization
`RegEx+/` hosts the SwiftUI app: `HomeView.swift` drives navigation, `Views/`, `Editor/`, and `Library/` contain feature areas, and `CheatSheet/` holds localized regex tips backed by `RegEx.xcdatamodeld`. Assets and previews live in `Assets.xcassets` and `Preview Content/`. Localizations reside in per-language `.lproj` folders alongside `Localizable.xcstrings`. Use the Xcode project at `RegEx+.xcodeproj`; the `Build/` directory is derived output and should stay untracked. `fastlane/` stores App Store metadata flows, and `mise.toml` defines repeatable automation tasks.
## Build, Test, and Development Commands
- `open RegEx+.xcodeproj` — launch the workspace in Xcode for iterative SwiftUI development.
- `xcodebuild -scheme "RegEx+" -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 15' build` — verify the iOS target from the command line.
- `xcodebuild test -scheme "RegEx+" -destination 'platform=macOS'` — run XCTest targets (create or enable them before CI).
- `mise run sc2tc` — refresh Traditional Chinese cheat-sheet resources via OpenCC.
- `mise run pull-metadata` / `mise run push-metadata` — sync App Store metadata with Fastlane.
## Coding Style & Naming Conventions
Follow idiomatic Swift 5.9: four-space indentation, `UpperCamelCase` for types and `lowerCamelCase` for functions, properties, and Core Data entities. Prefer SwiftUI modifiers over imperative UIKit. Strings must be localized by adding keys to `Localizable.xcstrings` and the matching `.lproj` plist. SwiftLint runs as an Xcode build phase; ensure `swiftlint` is installed (e.g., `brew install swiftlint`) before pushing.
## Testing Guidelines
Author tests with XCTest and snapshot SwiftUI previews where appropriate. Group specs by feature (e.g., `LibraryViewTests.swift`) and name methods `test_<scenario>_<expected>()`. Run `xcodebuild test` against both Catalyst and iOS destinations when the change affects shared logic. Aim to cover regex evaluation, Core Data persistence, and localization fallbacks; flag gaps in PRs if coverage is impractical.
## Commit & Pull Request Guidelines
Git history follows conventional prefixes (`feat:`, `fix:`, `chore:`). Keep subject lines under 72 characters and describe what changed and why. For pull requests, include a concise summary, affected platforms, and simulator or macOS screenshots when UI shifts. Link related issues, note localization or metadata follow-up steps, and confirm that SwiftLint and targeted builds/tests succeed locally. Avoid committing Fastlane API keys or other secrets under `fastlane/keys/`; use mocked or encrypted references instead.
================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build Commands
- **Build project**: Use Xcode GUI or `xcodebuild -project RegEx+.xcodeproj -scheme RegEx+ build`
- **Run SwiftLint**: Automatically runs during build via build phase, or manually with `swiftlint` (requires installation via Homebrew)
- **Localization conversion**: `mise run sc2tc` (converts Simplified Chinese to Traditional Chinese using OpenCC)
## Architecture Overview
RegEx+ is a SwiftUI-based Regular Expression tool that supports both iOS and macOS via Mac Catalyst. The app uses Core Data with CloudKit for data persistence and synchronization.
### Core Architecture Components
- **SwiftUI App Structure**: Uses scene-based architecture with `AppDelegate.swift` and `SceneDelegate.swift`
- **Navigation**: Master-detail navigation with `HomeView` as root, `LibraryView` as master, and `EditorView` as detail
- **Data Layer**: Core Data + CloudKit integration via `NSPersistentCloudKitContainer` for automatic sync
- **Custom Text Editing**: Specialized `RegExTextView` with syntax highlighting and live matching
### Key Modules
1. **CoreData+CloudKit**: Data persistence and cloud sync
- `DataManager.swift`: Singleton managing Core Data stack and CloudKit integration
- `RegEx.swift`: Core Data model for regex entries
- `RegExFetch.swift`: Fetch request definitions
2. **Editor**: Main regex editing interface
- `EditorView.swift`: SwiftUI view for regex editing
- `EditorViewModel.swift`: Business logic and state management
3. **Library**: Regex collection management
- `LibraryView.swift`: Master list of saved regexes
- `LibraryItemView.swift`: Individual regex list items
- `LibraryView+Data.swift`: Data manipulation extensions
4. **Views/RegExTextView**: Custom text editing components
- `RegExTextView.swift`: UIKit-based text editor wrapper
- `RegExSyntaxHighlighter.swift`: Syntax highlighting engine
- `MatchesTextView.swift`: Display matching results
- `String+NSRange.swift`: String range utilities
5. **CheatSheet**: Reference documentation
- Localized plist files for regex syntax reference
### Platform Support
- **Multi-platform**: iOS, iPadOS, and macOS (Mac Catalyst)
- **Navigation adaptivity**: Automatically switches between single/double column navigation based on device
- **Internationalization**: English, Simplified Chinese, Traditional Chinese
- **CloudKit**: Automatic data sync across devices
### Data Model
The app stores regex patterns with metadata in Core Data, automatically synced via CloudKit:
- Name, pattern, test string, flags
- Creation/modification dates
- CloudKit integration for cross-device sync
### Development Notes
- Uses `#if targetEnvironment(macCatalyst)` for platform-specific code
- SwiftLint integration via build phase
- Traditional Chinese localization generated from Simplified Chinese via OpenCC
- Custom UIKit text view integration within SwiftUI for advanced text editing features
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright © 2020 Lex Tang, https://lex.sh
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# RegEx+
[](https://swift.org/download/)
[](https://twitter.com/lexrus)
[<img src="https://cloud.githubusercontent.com/assets/219689/5575342/963e0ee8-9013-11e4-8091-7ece67d64729.png" width="135" height="40" alt="AppStore"/>](https://apps.apple.com/us/app/regex/id1511763524)
> A Regular Expression tool built with **SwiftUI**. The App Store version is no longer open source as the project has transitioned to agentic coding workflows.

## Features
- [x] Universal SwiftUI app for macOS and iOS
- [x] Regex editor with live match highlighting
- [x] Substitution template preview with copy-to-clipboard support
- [x] Share a regular expression from the editor
- [x] Built-in regular expression cheat sheet
- [x] CloudKit-backed sync via Core Data
- [x] Localized into:
- [x] English
- [x] Simplified Chinese
- [x] Traditional Chinese
- [x] Spanish
- [x] German
- [x] Japanese
- [x] French
- [x] Italian
- [x] Korean
- [x] Dutch
- [x] Polish
## License
This code is distributed under the terms and conditions of the MIT license.
================================================
FILE: RegEx+/About/AboutView.swift
================================================
//
// AboutView.swift
// RegEx+
//
// Created by Lex on 2020/5/24.
// Copyright © 2020 Lex.sh. All rights reserved.
//
import SwiftUI
import StoreKit
import AppAboutView
struct AboutView: View {
var body: some View {
AppAboutView.fromMainBundle(
appIcon: Image(.appIconForAboutView),
feedbackEmail: "lexrus@gmail.com",
appStoreID: "1511763524",
privacyPolicy: URL(string: "https://lex.sh/regexplus/privacypolicy")!,
copyrightText: "©2026 lex.sh",
appsShowcaseURL: URL(string: "https://lex.sh/apps/apps.json")
)
.background(Color.init(white: 0.5, opacity: 0.1))
.navigationBarTitleDisplayMode(.inline)
.navigationBarTitle("RegEx+")
}
}
#Preview {
AboutView()
}
================================================
FILE: RegEx+/AppDelegate.swift
================================================
//
// AppDelegate.swift
// RegEx+
//
// Created by Lex on 2020/4/21.
// Copyright © 2020 Lex.sh. All rights reserved.
//
import UIKit
import CoreData
import CloudKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
override func buildMenu(with builder: UIMenuBuilder) {
super.buildMenu(with: builder)
// Ensure that the builder is modifying the menu bar system.
guard builder.system == UIMenuSystem.main else { return }
builder.remove(menu: .help)
}
}
================================================
FILE: RegEx+/Assets.xcassets/AccentColor.colorset/Contents.json
================================================
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "0.400",
"red" : "0.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.900",
"green" : "0.300",
"red" : "0.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: RegEx+/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
"images" : [
{
"filename" : "icon_40pt.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "icon_20pt@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "icon_29pt@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "icon_29pt@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "icon_40pt@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "icon_60pt@2x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "icon_60pt@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "icon_60pt@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "icon_20pt.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "icon_40pt.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "icon_29pt.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "icon_29pt@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "icon_40pt.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "icon_40pt@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "icon_76pt.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "icon_76pt@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "icon_83.5@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "Icon.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "mac128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "mac256.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "mac256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "mac512.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "mac512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "mac.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: RegEx+/Assets.xcassets/AppIconForAboutView.imageset/Contents.json
================================================
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "AppIconForAboutView.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: RegEx+/Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: RegEx+/Base.lproj/LaunchScreen.storyboard
================================================
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="RegEx+" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="uh6-UG-ggI">
<rect key="frame" x="150.5" y="427.5" width="113.5" height="41"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle0"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="^(\w+)$" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="mtl-3p-SGW">
<rect key="frame" x="20" y="398" width="374" height="21"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="6Tk-OE-BBY" firstAttribute="trailing" secondItem="mtl-3p-SGW" secondAttribute="trailing" constant="20" id="8mK-0Y-Vwb"/>
<constraint firstItem="uh6-UG-ggI" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="IUg-rq-0FK"/>
<constraint firstItem="mtl-3p-SGW" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" constant="20" id="Qas-mh-Sgd"/>
<constraint firstItem="uh6-UG-ggI" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="W5e-Db-s6l"/>
<constraint firstItem="uh6-UG-ggI" firstAttribute="top" secondItem="mtl-3p-SGW" secondAttribute="bottom" constant="8.5" id="xQz-5B-3gd"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="52.173913043478265" y="375"/>
</scene>
</scenes>
<resources>
<systemColor name="secondaryLabelColor">
<color red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
================================================
FILE: RegEx+/CheatSheet/CheatSheetView.swift
================================================
//
// CheatSheetView.swift
// RegEx+
//
// Created by Lex on 2020/4/21.
// Copyright © 2020 Lex.sh. All rights reserved.
//
import SwiftUI
// Official documentation of NSRegularExpression
private let kNSRegularExpressionDocumentLink = "https://developer.apple.com/documentation/foundation/nsregularexpression"
struct CheatSheetView: View {
@State var showingSafari = false
@State var metacharacters: [CheatSheetPlist.Item] = []
@State var operators: [CheatSheetPlist.Item] = []
var body: some View {
List {
Section(header: Text("Metacharacters")) {
ForEach(metacharacters, id: \.exp) {
RowView(title: $0.exp, content: $0.des)
}
}
Section(header: Text("Operators")) {
ForEach(operators, id: \.exp) {
RowView(title: $0.exp, content: $0.des)
}
}
}
.navigationBarTitle("Cheat Sheet")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
safariButton
}
}
.onAppear(perform: loadPlist)
}
private func loadPlist() {
guard let url = Bundle.main.url(forResource: "CheatSheet", withExtension: "plist") else {
assertionFailure("Missing CheatSheet.plist!")
return
}
let plistDecoder = PropertyListDecoder()
do {
let data = try Data(contentsOf: url)
let dict = try plistDecoder.decode(CheatSheetPlist.self, from: data)
metacharacters = dict.metacharacters
operators = dict.operators
} catch {
print(error.localizedDescription)
}
}
private var safariButton: some View {
let url = URL(string: kNSRegularExpressionDocumentLink)!
return Button(action: {
#if targetEnvironment(macCatalyst)
UIApplication.shared.open(url)
#else
showingSafari.toggle()
#endif
}) {
Image(systemName: "safari")
.imageScale(.large)
.padding(EdgeInsets(top: 8, leading: 24, bottom: 8, trailing: 0))
}
.sheet(isPresented: $showingSafari, content: {
SafariView(url: url)
})
}
}
struct CheatSheetView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
CheatSheetView()
.navigationBarTitle("Cheat Sheet")
}
}
}
private struct RowView: View {
var title: String
var content: String
var body: some View {
VStack(alignment: .leading, spacing: 3) {
Text(title)
.font(.headline)
.foregroundColor(.accentColor)
Text(content)
.font(.subheadline)
}
.padding(.vertical, 6)
}
}
struct CheatSheetPlist: Decodable {
struct Item: Decodable, Hashable {
var exp: String
var des: String
}
var metacharacters: [Item]
var operators: [Item]
}
================================================
FILE: RegEx+/CoreData+CloudKit/DataManager.swift
================================================
//
// DataManager.swift
// RegEx+
//
// Created by Lex on 2020/5/3.
// Copyright © 2020 Lex.sh. All rights reserved.
//
import CoreData
import CloudKit
class DataManager {
static let shared = DataManager()
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "RegEx")
guard let description = container.persistentStoreDescriptions.first else {
fatalError("No Descriptions found")
}
description.setOption(true as NSObject, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump
container.loadPersistentStores(completionHandler: { (_, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
// NotificationCenter.default.addObserver(self, selector: #selector(self.processUpdate), name: .NSPersistentStoreRemoteChange, object: nil)
return container
}()
// MARK: - Initialize CloudKit schema
func initializeCloudKitSchema() {
do {
try persistentContainer.initializeCloudKitSchema(options: NSPersistentCloudKitContainerSchemaInitializationOptions.printSchema)
} catch {
print(error.localizedDescription)
}
}
// MARK: - Core Data Saving support
func saveContext() {
let context = persistentContainer.viewContext
guard context.hasChanges else {
return
}
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
@objc
func processUpdate(notification: NSNotification) {
operationQueue.addOperation {
let context = self.persistentContainer.newBackgroundContext()
context.performAndWait {
let items: [RegEx]
do {
try items = context.fetch(RegEx.fetchAllRegEx())
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
items.forEach {
print("NAME: \($0.name) !!!!")
}
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
}
private lazy var operationQueue: OperationQueue = {
var queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
}
================================================
FILE: RegEx+/CoreData+CloudKit/RegEx.swift
================================================
//
// RegEx+CoreDataClass.swift
// RegEx+
//
// Created by Lex on 2020/5/3.
// Copyright © 2020 Lex.sh. All rights reserved.
//
//
import Foundation
import CoreData
@objc(RegEx)
public class RegEx: NSManagedObject {
@NSManaged public var name: String
@NSManaged public var raw: String
@NSManaged public var sample: String
@NSManaged public var substitution: String
@NSManaged public var createdAt: Date
@NSManaged public var updatedAt: Date
@NSManaged public var allowCommentsAndWhitespace: Bool
@NSManaged public var anchorsMatchLines: Bool
@NSManaged public var caseInsensitive: Bool
@NSManaged public var dotMatchesLineSeparators: Bool
@NSManaged public var ignoreMetacharacters: Bool
@NSManaged public var useUnicodeWordBoundaries: Bool
@NSManaged public var useUnixLineSeparators: Bool
convenience init(name: String = "Untitled") {
self.init()
self.name = name
self.raw = ""
self.sample = ""
self.substitution = ""
self.createdAt = Date()
self.updatedAt = Date()
}
public var regularExpressionOptions: NSRegularExpression.Options {
var options: NSRegularExpression.Options = []
if allowCommentsAndWhitespace {
options.insert(.allowCommentsAndWhitespace)
}
if anchorsMatchLines {
options.insert(.anchorsMatchLines)
}
if caseInsensitive {
options.insert(.caseInsensitive)
}
if dotMatchesLineSeparators {
options.insert(.dotMatchesLineSeparators)
}
if ignoreMetacharacters {
options.insert(.ignoreMetacharacters)
}
if useUnicodeWordBoundaries {
options.insert(.useUnicodeWordBoundaries)
}
if useUnixLineSeparators {
options.insert(.useUnixLineSeparators)
}
return options
}
public var flagOptions: String {
""
+ (caseInsensitive ? "i" : "")
+ (allowCommentsAndWhitespace ? "x" : "")
+ (dotMatchesLineSeparators ? "." : "")
+ (anchorsMatchLines ? "m" : "")
+ (useUnicodeWordBoundaries ? "w" : "")
}
public override var description: String {
"/\(raw)/\(flagOptions)"
}
public func isEqual(to object: RegEx) -> Bool {
name == object.name
&& regularExpressionOptions == object.regularExpressionOptions
&& raw == object.raw
&& sample == object.sample
&& substitution == object.substitution
&& allowCommentsAndWhitespace == object.allowCommentsAndWhitespace
&& anchorsMatchLines == object.anchorsMatchLines
&& caseInsensitive == object.caseInsensitive
&& dotMatchesLineSeparators == object.dotMatchesLineSeparators
&& ignoreMetacharacters == object.ignoreMetacharacters
&& useUnicodeWordBoundaries == object.useUnicodeWordBoundaries
&& useUnixLineSeparators == object.useUnixLineSeparators
}
}
================================================
FILE: RegEx+/CoreData+CloudKit/RegExFetch.swift
================================================
//
// RegExFetch.swift
// RegEx+
//
// Created by Lex on 2020/5/3.
// Copyright © 2020 Lex.sh. All rights reserved.
//
import CoreData
extension RegEx {
@nonobjc public class func fetchRequest() -> NSFetchRequest<RegEx> {
NSFetchRequest<RegEx>(entityName: "RegEx")
}
@nonobjc public class func fetchAllRegEx() -> NSFetchRequest<RegEx> {
let req: NSFetchRequest<RegEx> = RegEx.fetchRequest()
req.sortDescriptors = [
NSSortDescriptor(key: "createdAt", ascending: false),
NSSortDescriptor(key: "updatedAt", ascending: false)
]
return req
}
@nonobjc public class func fetch(byID ID: NSManagedObjectID) -> NSFetchRequest<RegEx> {
let req: NSFetchRequest<RegEx> = RegEx.fetchRequest()
req.predicate = NSPredicate(format: "self.objectID IN %@", ID)
return req
}
}
================================================
FILE: RegEx+/Editor/EditorView.swift
================================================
//
// EditorView.swift
// RegEx+
//
// Created by Lex on 2020/4/21.
// Copyright © 2020 Lex.sh. All rights reserved.
//
import SwiftUI
struct EditorView: View, Equatable {
static func == (lhs: EditorView, rhs: EditorView) -> Bool {
lhs.regEx.objectID == rhs.regEx.objectID
}
let regEx: RegEx
@StateObject private var viewModel = EditorViewModel()
@State private var isSharePresented = false
@State private var copyButtonText = "Copy"
@State private var isFlowViewVisible = false
init(regEx: RegEx) {
self.regEx = regEx
}
var body: some View {
Group {
if let regExBinding = Binding($viewModel.regEx) {
List {
Section(header: Text("Name")) {
TextField("Name", text: regExBinding.name)
.font(.headline)
}
RegExTextViewSection(regEx: regExBinding)
Section(header: FlowViewHeaderView(isVisible: $isFlowViewVisible)) {
if isFlowViewVisible {
RegExFlowView(pattern: regExBinding.wrappedValue.raw)
.frame(minHeight: 80)
}
}
Section(header: SampleHeaderView(count: viewModel.matches.count)) {
MatchesTextView(
"$56.78 $90.12",
text: regExBinding.sample,
matches: $viewModel.matches
)
.equatable()
.padding(kTextFieldPadding)
}
SubstitutionSection(
regExBinding: regExBinding,
substitutionResult: viewModel.substitutionResult,
copyButtonText: copyButtonText,
copyAction: copyToClipboard
)
}
.navigationTitle(regExBinding.name)
.toolbar {
#if !targetEnvironment(macCatalyst)
ToolbarItemGroup(placement: .navigationBarTrailing) {
shareButton.padding()
cheatSheetButton().padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 0))
}
#endif
}
.gesture(dismissKeyboardDesture)
.listStyle(InsetGroupedListStyle())
.onDisappear(perform: {
viewModel.updateLastModified()
DataManager.shared.saveContext()
})
} else {
Text("Loading...")
.navigationTitle("RegEx+")
}
}
.onAppear {
viewModel.configure(with: regEx)
}
}
private func copyToClipboard() {
UIPasteboard.general.string = viewModel.substitutionResult
copyButtonText = "Copied"
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
copyButtonText = "Copy"
}
}
// https://stackoverflow.com/questions/56491386/how-to-hide-keyboard-when-using-swiftui
private var dismissKeyboardDesture: some Gesture {
DragGesture().onChanged { _ in
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
private var shareButton: some View {
Button(action: {
self.isSharePresented = true
}) {
Image(systemName: "square.and.arrow.up")
.imageScale(.large)
}
.accessibilityLabel("Share")
.accessibilityHint("Share this regular expression")
.sheet(isPresented: $isSharePresented) {
if let regEx = viewModel.regEx {
ActivityViewController(activityItems: [regEx.description])
}
}
}
}
private func cheatSheetButton() -> some View {
#if targetEnvironment(macCatalyst)
ZStack {
Image(systemName: "wand.and.stars")
.imageScale(.large)
.foregroundColor(.accentColor)
NavigationLink(destination: CheatSheetView()) {
EmptyView()
}
.opacity(0)
}
.accessibilityLabel("Cheat Sheet")
.accessibilityHint("View regular expression reference guide")
#else
NavigationLink(destination: CheatSheetView()) {
Image(systemName: "wand.and.stars")
.imageScale(.large)
.foregroundColor(.accentColor)
}
.accessibilityLabel("Cheat Sheet")
.accessibilityHint("View regular expression reference guide")
#endif
}
private struct RegExTextViewSection: View {
@Binding var regEx: RegEx
@State private var isOptionsVisible = false
var body: some View {
Section(header: Text("Regular Expression")) {
#if targetEnvironment(macCatalyst)
HStack {
RegExTextView(
"Type RegEx here",
text: $regEx.raw,
showShortcutBar: false,
highlightingMode: .regularExpression(regEx.regularExpressionOptions)
)
.equatable()
.padding(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 5))
cheatSheetButton()
.frame(width: 20)
}
#else
RegExTextView(
"Type RegEx here",
text: $regEx.raw,
showShortcutBar: true,
highlightingMode: .regularExpression(regEx.regularExpressionOptions)
)
.padding(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 5))
#endif
Button(action: {
isOptionsVisible.toggle()
}) {
HStack {
Text("Options")
if !isOptionsVisible {
Spacer()
VStack(alignment: .trailing) {
if regEx.caseInsensitive {
Text("Case Insensitive")
}
if regEx.allowCommentsAndWhitespace {
Text("Allow Comments and Whitespace")
}
if regEx.ignoreMetacharacters {
Text("Ignore Metacharacters")
}
if regEx.anchorsMatchLines {
Text("Anchors Match Lines")
}
if regEx.dotMatchesLineSeparators {
Text("Dot Matches Line Separators")
}
if regEx.useUnixLineSeparators {
Text("Use Unix Line Separators")
}
if regEx.useUnicodeWordBoundaries {
Text("Use Unicode Word Boundaries")
}
}
.font(.footnote)
}
}
.foregroundColor(isOptionsVisible ? .secondary : .accentColor)
}
if isOptionsVisible {
Toggle("Case Insensitive", isOn: $regEx.caseInsensitive)
Toggle("Allow Comments and Whitespace", isOn: $regEx.allowCommentsAndWhitespace)
Toggle("Ignore Metacharacters", isOn: $regEx.ignoreMetacharacters)
Toggle("Anchors Match Lines", isOn: $regEx.anchorsMatchLines)
Toggle("Dot Matches Line Separators", isOn: $regEx.dotMatchesLineSeparators)
Toggle("Use Unix Line Separators", isOn: $regEx.useUnixLineSeparators)
Toggle("Use Unicode Word Boundaries", isOn: $regEx.useUnicodeWordBoundaries)
}
}
}
}
private struct SampleFooterView: View {
var count: Int
private var matchesString: String {
return self.count == 1 ? "1 match" : "\(count) matches"
}
var body: some View {
Text(matchesString)
}
}
private struct SampleHeaderView: View {
var count: Int
var body: some View {
HStack {
Text("Sample Text")
if count > 0 {
Spacer()
Text(count == 1 ? "1 match" : "\(count) matches")
.font(.footnote)
.foregroundColor(Color.secondary)
.padding(EdgeInsets(top: 1, leading: 6, bottom: 1, trailing: 6))
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.secondary, lineWidth: 1)
)
}
}
.frame(minHeight: 20)
}
}
private let kTextFieldPadding = EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 5)
#if DEBUG
struct EditorView_Previews: PreviewProvider {
private static var regEx: RegEx = {
var r: RegEx = RegEx(context: DataManager.shared.persistentContainer.viewContext)
r.name = "Dollars"
r.raw = #"\$?((\d+)\.?(\d\d)?)"#
r.sample = "$100.00 12.50 $10"
r.substitution = "$3"
return r
}()
static var previews: some View {
Group {
NavigationView {
EditorView(regEx: regEx)
}
.environment(\.sizeCategory, .extraLarge)
.previewLayout(.device)
.previewDevice("iPhone 11")
NavigationView {
EditorView(regEx: regEx)
}
.previewDevice("iPhone 11")
.preferredColorScheme(.dark)
.environment(\.sizeCategory, .large)
}
}
}
#endif
private struct SubstitutionSection: View {
@Binding var regExBinding: RegEx
let substitutionResult: String
let copyButtonText: String
let copyAction: () -> Void
var body: some View {
Section(header: Text("Substitution Template")) {
#if targetEnvironment(macCatalyst)
TextField("Price: $$$1\\.$2\\n", text: $regExBinding.substitution)
.padding(kTextFieldPadding)
#else
RegExTextView(
"Price: $$$1\\.$2\\n",
text: $regExBinding.substitution,
showShortcutBar: true,
highlightingMode: .plainText
)
.padding(kTextFieldPadding)
#endif
}
if !regExBinding.substitution.isEmpty {
Section(header: Text("Substitution Result")) {
HStack {
Text(substitutionResult)
.padding(kTextFieldPadding)
if !substitutionResult.isEmpty {
Spacer()
Button(action: copyAction) {
Text("\(copyButtonText)")
.font(.footnote)
.foregroundColor(Color.accentColor)
.padding(EdgeInsets(top: 1, leading: 6, bottom: 1, trailing: 6))
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.accentColor, lineWidth: 1)
)
}
.accessibilityLabel("Copy substitution result")
.accessibilityHint("Copies the substitution result to clipboard")
}
}
}
}
}
}
private struct FlowViewHeaderView: View {
@Binding var isVisible: Bool
var body: some View {
Button(action: {
withAnimation {
isVisible.toggle()
}
}) {
HStack {
Text("Flow Diagram")
Spacer()
Image(systemName: isVisible ? "chevron.down" : "chevron.right")
.font(.caption)
.foregroundColor(.secondary)
}
}
.foregroundColor(isVisible ? .primary : .accentColor)
}
}
================================================
FILE: RegEx+/Editor/EditorViewModel.swift
================================================
//
// RegExEditorViewModel.swift
// RegEx+
//
// Created by Lex on 2020/4/25.
// Copyright © 2020 Lex.sh. All rights reserved.
//
import Foundation
import Combine
class EditorViewModel : ObservableObject, Equatable {
@Published var regEx: RegEx?
@Published var matches = [NSTextCheckingResult]()
@Published var substitutionResult = ""
private var cancellables = Set<AnyCancellable>()
init() {
self.regEx = nil
setupBindings()
}
func configure(with regEx: RegEx) {
self.regEx = regEx
}
private func setupBindings() {
let optionsObservable = $regEx
.compactMap { $0 }
.map(\.regularExpressionOptions)
let regExObservable = $regEx
.compactMap { $0 }
.map(\.raw)
.throttle(for: 0.2, scheduler: RunLoop.main, latest: true)
.removeDuplicates()
.combineLatest(optionsObservable)
.compactMap { (raw, options) in
try? NSRegularExpression(pattern: raw, options: options)
}
let sampleObservable = $regEx
.compactMap { $0 }
.map(\.sample)
.throttle(for: 0.2, scheduler: RunLoop.main, latest: true)
.removeDuplicates()
let substitutionObservalbe = $regEx
.compactMap { $0 }
.map(\.substitution)
.throttle(for: 0.2, scheduler: RunLoop.main, latest: true)
.removeDuplicates()
let subAndSampleObservable = substitutionObservalbe.combineLatest(sampleObservable)
.map { ($0.0, $0.1) }
regExObservable
.combineLatest(sampleObservable)
.sink { [weak self] (reg: NSRegularExpression, sample: String) in
let range = NSRange(location: 0, length: sample.count)
self?.matches = reg.matches(in: sample, options: [], range: range)
}
.store(in: &cancellables)
regExObservable
.combineLatest(subAndSampleObservable)
.map { ($0, $1.0, $1.1) }
.sink { [weak self] (reg: NSRegularExpression, sub: String, sample: String) in
let range = NSRange(location: 0, length: sample.count)
self?.substitutionResult = reg.stringByReplacingMatches(
in: sample,
options: [],
range: range,
withTemplate: sub
)
}
.store(in: &cancellables)
}
// Keep the old initializer for compatibility
convenience init(regEx: RegEx) {
self.init()
self.regEx = regEx
}
func updateLastModified() {
if let regEx, regEx.hasChanges {
regEx.updatedAt = Date()
}
}
static func == (lhs: EditorViewModel, rhs: EditorViewModel) -> Bool {
if let lr = lhs.regEx, let rr = rhs.regEx {
return lr.isEqual(to: rr) == true
&& lhs.substitutionResult == rhs.substitutionResult
&& lhs.matches == rhs.matches
}
return false
}
}
================================================
FILE: RegEx+/Editor/RegExFlowView.swift
================================================
//
// RegExFlowView.swift
// RegEx+
//
// Created by Lex on 2026/4/11.
// Copyright © 2020 Lex.sh. All rights reserved.
//
// swiftlint:disable file_length type_body_length cyclomatic_complexity
import SwiftUI
import _RegexParser
struct RegExFlowView: View {
let pattern: String
private var diagram: FlowComponent {
var builder = FlowDiagramBuilder(source: pattern)
return builder.build()
}
private var breakdown: FlowPatternBreakdown {
FlowPatternBreakdownBuilder(source: pattern).build()
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
if pattern.isEmpty {
Text("Enter a regular expression to see the flow diagram")
.foregroundStyle(.secondary)
.font(.footnote)
.padding()
} else {
ScrollView(.horizontal, showsIndicators: true) {
HStack {
Spacer(minLength: 0)
FlowDiagramView(component: diagram)
.padding(.vertical, 4)
Spacer(minLength: 0)
}
.frame(maxWidth: .infinity)
}
if breakdown.hasItems {
PatternBreakdownView(breakdown: breakdown)
.padding(.top, 4)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 4)
}
}
private struct FlowDiagramBuilder {
let source: String
private var captureGroupIndex = 0
init(source: String) {
self.source = source
}
mutating func build() -> FlowComponent {
let ast = parseWithRecovery(source, .traditional)
var components = [FlowComponent]()
if let globalOptions = ast.globalOptions {
components.append(contentsOf: globalOptions.options.map { option in
.node(
FlowNode(
style: .directive,
label: sourceText(for: option.location) ?? "Options"
)
)
})
}
let root = component(from: ast.root)
switch root {
case .sequence(let children):
components.append(contentsOf: children)
case .empty:
break
default:
components.append(root)
}
return normalizedSequence(components)
}
private mutating func component(from node: AST.Node) -> FlowComponent {
switch node {
case .alternation(let alternation):
var branches = [[FlowComponent]]()
for child in alternation.children {
branches.append(branch(from: child))
}
return .alternation(branches)
case .concatenation(let concatenation):
var children = [FlowComponent]()
for child in concatenation.children {
if let component = semanticComponent(from: child) {
children.append(component)
}
}
return normalizedSequence(children)
case .group(let group):
let groupKind = group.kind.value
return .group(
FlowGroup(
style: style(for: groupKind),
title: title(for: groupKind, captureReference: nextCaptureReference(for: groupKind)),
content: component(from: group.child)
)
)
case .conditional(let conditional):
return .group(
FlowGroup(
style: .assertion,
title: "Conditional \(conditionLabel(for: conditional.condition))",
content: .alternation([
branch(from: conditional.trueBranch),
branch(from: conditional.falseBranch)
])
)
)
case .quantification(let quantification):
return .quantified(
component(from: quantification.child),
quantifier(for: quantification)
)
case .quote(let quote):
return .node(
FlowNode(
style: .literal,
label: sourceText(for: quote.location) ?? quote.literal
)
)
case .trivia:
return .empty
case .interpolation(let interpolation):
return .node(
FlowNode(
style: .directive,
label: sourceText(for: interpolation.location) ?? interpolation.contents
)
)
case .atom(let atom):
return .node(flowNode(for: atom))
case .customCharacterClass(let characterClass):
return .node(
FlowNode(
style: .characterClass,
label: sourceText(for: characterClass.location) ?? "[...]"
)
)
case .absentFunction(let absentFunction):
return .node(
FlowNode(
style: .special,
label: sourceText(for: absentFunction.location) ?? "(?~...)"
)
)
case .empty:
return .empty
}
}
private mutating func semanticComponent(from node: AST.Node) -> FlowComponent? {
let component = component(from: node)
if case .empty = component {
return nil
}
return component
}
private mutating func branch(from node: AST.Node) -> [FlowComponent] {
let resolved = component(from: node)
switch resolved {
case .sequence(let children):
return children.isEmpty ? [.empty] : children
case .empty:
return [.empty]
default:
return [resolved]
}
}
private func normalizedSequence(_ components: [FlowComponent]) -> FlowComponent {
let flattened = components.flatMap { component -> [FlowComponent] in
switch component {
case .sequence(let children):
return children
case .empty:
return []
default:
return [component]
}
}
let compacted = mergeContinuousLiteralNodes(in: flattened)
switch compacted.count {
case 0:
return .empty
case 1:
return compacted[0]
default:
return .sequence(compacted)
}
}
private func mergeContinuousLiteralNodes(in components: [FlowComponent]) -> [FlowComponent] {
var merged = [FlowComponent]()
for component in components {
guard case .node(let node) = component, node.style == .literal else {
merged.append(component)
continue
}
if let last = merged.last, case .node(let previous) = last, previous.style == .literal {
merged.removeLast()
merged.append(
.node(
FlowNode(
style: .literal,
label: previous.label + node.label
)
)
)
} else {
merged.append(component)
}
}
return merged
}
private func quantifier(for quantification: AST.Quantification) -> FlowQuantifier {
let label: String
if let sourceLabel = sourceText(
from: quantification.amount.location.start,
to: quantification.location.end
) {
label = sourceLabel
} else {
let amount: String
switch quantification.amount.value {
case .zeroOrMore:
amount = "*"
case .oneOrMore:
amount = "+"
case .zeroOrOne:
amount = "?"
case .exactly(let number):
amount = "{\(number.value ?? 0)}"
case .nOrMore(let number):
amount = "{\(number.value ?? 0),}"
case .upToN(let number):
amount = "{,\(number.value ?? 0)}"
case .range(let lower, let upper):
amount = "{\(lower.value ?? 0),\(upper.value ?? 0)}"
}
label = amount + quantification.kind.value.rawValue
}
return FlowQuantifier(
label: label,
isOptional: isOptional(quantification.amount.value)
)
}
private func isOptional(_ amount: AST.Quantification.Amount) -> Bool {
switch amount {
case .zeroOrMore, .zeroOrOne, .upToN:
return true
case .range(let lower, _):
return (lower.value ?? 0) == 0
case .exactly, .nOrMore, .oneOrMore:
return false
}
}
private func flowNode(for atom: AST.Atom) -> FlowNode {
let label = sourceText(for: atom.location) ?? fallbackLabel(for: atom)
switch atom.kind {
case .dot:
return FlowNode(style: .wildcard, label: "Any char")
case .caretAnchor:
return FlowNode(style: .anchor, label: "^ Start")
case .dollarAnchor:
return FlowNode(style: .anchor, label: "$ End")
case .property, .escaped:
return FlowNode(style: escapedStyle(for: atom), label: label)
case .backreference, .subpattern:
return FlowNode(style: .special, label: label)
case .callout, .backtrackingDirective, .changeMatchingOptions:
return FlowNode(style: .directive, label: label)
case .invalid:
return FlowNode(style: .invalid, label: label)
case .char, .scalar, .scalarSequence, .keyboardControl,
.keyboardMeta, .keyboardMetaControl, .namedCharacter:
return FlowNode(style: .literal, label: label)
}
}
private func escapedStyle(for atom: AST.Atom) -> FlowNode.Style {
guard case .escaped(let builtin) = atom.kind else {
return .characterClass
}
switch builtin {
case .wordBoundary, .notWordBoundary:
return .assertion
case .startOfSubject, .endOfSubjectBeforeNewline,
.endOfSubject, .firstMatchingPositionInSubject:
return .anchor
case .alarm, .escape, .formfeed, .newline,
.carriageReturn, .tab, .backspace:
return .literal
default:
return .characterClass
}
}
private func fallbackLabel(for atom: AST.Atom) -> String {
switch atom.kind {
case .char(let character):
return String(character)
case .scalar(let scalar):
return String(scalar.value)
case .scalarSequence(let sequence):
return sequence.scalarValues.map(String.init).joined()
case .property:
return "Property"
case .escaped(let builtin):
return "\\\(builtin.character)"
case .keyboardControl(let character):
return "\\C-\(character)"
case .keyboardMeta(let character):
return "\\M-\(character)"
case .keyboardMetaControl(let character):
return "\\M-\\C-\(character)"
case .namedCharacter(let name):
return "\\N{\(name)}"
case .dot:
return "."
case .caretAnchor:
return "^"
case .dollarAnchor:
return "$"
case .backreference:
return "Backreference"
case .subpattern:
return "Subpattern"
case .callout:
return "Callout"
case .backtrackingDirective:
return "Directive"
case .changeMatchingOptions:
return "Options"
case .invalid:
return "Invalid"
}
}
private func style(for kind: AST.Group.Kind) -> FlowNode.Style {
switch kind {
case .capture, .namedCapture, .balancedCapture:
return .capturingGroup
case .lookahead, .negativeLookahead, .nonAtomicLookahead,
.lookbehind, .negativeLookbehind, .nonAtomicLookbehind:
return .assertion
case .changeMatchingOptions:
return .directive
case .scriptRun, .atomicScriptRun:
return .special
case .nonCapture, .nonCaptureReset, .atomicNonCapturing:
return .grouping
}
}
private func title(for kind: AST.Group.Kind, captureReference: String?) -> String {
let suffix = captureReference.map { " \($0)" } ?? ""
switch kind {
case .capture:
return "Group\(suffix)"
case .namedCapture(let name):
return "Group <\(name.value)>\(suffix)"
case .balancedCapture(let balanced):
let current = balanced.name?.value ?? ""
return "Group <\(current)-\(balanced.priorName.value)>\(suffix)"
case .nonCapture:
return "Group"
case .nonCaptureReset:
return "Branch Reset Group"
case .atomicNonCapturing:
return "Atomic Group"
case .lookahead:
return "Lookahead"
case .negativeLookahead:
return "Negative Lookahead"
case .nonAtomicLookahead:
return "Non-atomic Lookahead"
case .lookbehind:
return "Lookbehind"
case .negativeLookbehind:
return "Negative Lookbehind"
case .nonAtomicLookbehind:
return "Non-atomic Lookbehind"
case .scriptRun:
return "Script Run"
case .atomicScriptRun:
return "Atomic Script Run"
case .changeMatchingOptions:
return "Scoped Options"
}
}
private mutating func nextCaptureReference(for kind: AST.Group.Kind) -> String? {
switch kind {
case .capture, .namedCapture, .balancedCapture:
captureGroupIndex += 1
return "$\(captureGroupIndex)"
default:
return nil
}
}
private func conditionLabel(for condition: AST.Conditional.Condition) -> String {
switch condition.kind {
case .groupMatched:
return "if group matched"
case .recursionCheck:
return "if recursion"
case .groupRecursionCheck:
return "if group recursion"
case .defineGroup:
return "define group"
case .pcreVersionCheck:
return "if PCRE version"
case .group:
return "if nested pattern"
}
}
private func sourceText(for location: SourceLocation) -> String? {
sourceText(from: location.start, to: location.end)
}
private func sourceText(from start: String.Index, to end: String.Index) -> String? {
guard start >= source.startIndex,
end <= source.endIndex,
start <= end else {
return nil
}
return String(source[start..<end])
}
}
private struct FlowPatternBreakdownBuilder {
let source: String
func build() -> FlowPatternBreakdown {
let ast = parseWithRecovery(source, .traditional)
let catalog = CheatSheetCatalog.shared
var collector = FlowPatternBreakdownCollector(source: source, catalog: catalog)
if let globalOptions = ast.globalOptions {
globalOptions.options.forEach { option in
collector.collectGlobalOption(option)
}
}
collector.collect(from: ast.root)
return collector.result
}
}
private struct FlowPatternBreakdown {
let metacharacters: [FlowCheatSheetMatch]
let operators: [FlowCheatSheetMatch]
var hasItems: Bool {
!metacharacters.isEmpty || !operators.isEmpty
}
}
private struct FlowCheatSheetMatch: Hashable {
let id: String
let title: String
let description: String
}
private enum FlowCheatSheetKey: String {
case alarm
case startOfInput
case wordBoundary
case backspaceInSet
case notWordBoundary
case controlCharacter
case decimalDigit
case notDecimalDigit
case escapeCharacter
case quoteEnd
case formFeed
case previousMatchEnd
case newline
case namedCharacter
case unicodeProperty
case unicodePropertyInverted
case quoteStart
case carriageReturn
case whitespace
case notWhitespace
case tab
case unicodeScalar4
case unicodeScalar8
case wordCharacter
case notWordCharacter
case hexScalarBraced
case hexScalar2
case graphemeCluster
case endOfInputBeforeNewline
case endOfInput
case backreference
case octalScalar
case customCharacterClass
case wildcard
case lineStart
case lineEnd
case escapedLiteral
case alternation
case zeroOrMore
case oneOrMore
case zeroOrOne
case exactlyN
case nOrMore
case range
case zeroOrMoreReluctant
case oneOrMoreReluctant
case zeroOrOneReluctant
case exactlyNReluctant
case nOrMoreReluctant
case rangeReluctant
case zeroOrMorePossessive
case oneOrMorePossessive
case zeroOrOnePossessive
case exactlyNPossessive
case nOrMorePossessive
case rangePossessive
case capturingGroup
case nonCapturingGroup
case atomicGroup
case commentGroup
case lookahead
case negativeLookahead
case lookbehind
case negativeLookbehind
case scopedOptionChange
case inlineOptionChange
}
private struct CheatSheetCatalog {
static let shared = CheatSheetCatalog.load()
let metacharacters: [FlowCheatSheetKey: CheatSheetPlist.Item]
let operators: [FlowCheatSheetKey: CheatSheetPlist.Item]
func metacharacter(for key: FlowCheatSheetKey) -> CheatSheetPlist.Item? {
metacharacters[key]
}
func operatorItem(for key: FlowCheatSheetKey) -> CheatSheetPlist.Item? {
operators[key]
}
private static func load() -> CheatSheetCatalog {
let plist = CheatSheetPlist.localizedCheatSheet ?? CheatSheetPlist(metacharacters: [], operators: [])
let metacharacters = Dictionary(
uniqueKeysWithValues: plist.metacharacters.compactMap { item -> (FlowCheatSheetKey, CheatSheetPlist.Item)? in
guard let key = metacharacterKey(for: item.exp, description: item.des) else {
return nil
}
return (key, item)
}
)
let operators = Dictionary(
uniqueKeysWithValues: plist.operators.compactMap { item -> (FlowCheatSheetKey, CheatSheetPlist.Item)? in
guard let key = operatorKey(for: item.exp) else {
return nil
}
return (key, item)
}
)
return CheatSheetCatalog(metacharacters: metacharacters, operators: operators)
}
private static func metacharacterKey(for expression: String, description: String) -> FlowCheatSheetKey? {
switch expression {
case "\\a": return .alarm
case "\\A": return .startOfInput
case "\\b, outside of a [Set]": return .wordBoundary
case "\\b, within a [Set]": return .backspaceInSet
case "\\B": return .notWordBoundary
case "\\cX": return .controlCharacter
case "\\d": return .decimalDigit
case "\\D": return .notDecimalDigit
case "\\e": return .escapeCharacter
case "\\E": return .quoteEnd
case "\\f": return .formFeed
case "\\G": return .previousMatchEnd
case "\\n":
return description.contains("Back Reference") ? .backreference : .newline
case "\\N{UNICODE CHARACTER NAME}": return .namedCharacter
case "\\p{UNICODE PROPERTY NAME}": return .unicodeProperty
case "\\P{UNICODE PROPERTY NAME}": return .unicodePropertyInverted
case "\\Q": return .quoteStart
case "\\r": return .carriageReturn
case "\\s": return .whitespace
case "\\S": return .notWhitespace
case "\\t": return .tab
case "\\uhhhh": return .unicodeScalar4
case "\\Uhhhhhhhh": return .unicodeScalar8
case "\\w": return .wordCharacter
case "\\W": return .notWordCharacter
case "\\x{hhhh}": return .hexScalarBraced
case "\\xhh": return .hexScalar2
case "\\X": return .graphemeCluster
case "\\Z": return .endOfInputBeforeNewline
case "\\z": return .endOfInput
case "\\0ooo": return .octalScalar
case "[pattern]": return .customCharacterClass
case ".": return .wildcard
case "^": return .lineStart
case "$": return .lineEnd
case "\\": return .escapedLiteral
default: return nil
}
}
private static func operatorKey(for expression: String) -> FlowCheatSheetKey? {
switch expression {
case "|": return .alternation
case "*": return .zeroOrMore
case "+": return .oneOrMore
case "?": return .zeroOrOne
case "{n}": return .exactlyN
case "{n,}": return .nOrMore
case "{n,m}": return .range
case "*?": return .zeroOrMoreReluctant
case "+?": return .oneOrMoreReluctant
case "??": return .zeroOrOneReluctant
case "{n}?": return .exactlyNReluctant
case "{n,}?": return .nOrMoreReluctant
case "{n,m}?": return .rangeReluctant
case "*+": return .zeroOrMorePossessive
case "++": return .oneOrMorePossessive
case "?+": return .zeroOrOnePossessive
case "{n}+": return .exactlyNPossessive
case "{n,}+": return .nOrMorePossessive
case "{n,m}+": return .rangePossessive
case "(...)": return .capturingGroup
case "(?:...)": return .nonCapturingGroup
case "(?>...)": return .atomicGroup
case "(?# ... )": return .commentGroup
case "(?= ... )": return .lookahead
case "(?! ... )": return .negativeLookahead
case "(?<= ... )": return .lookbehind
case "(?<! ... )": return .negativeLookbehind
case "(?ismwx-ismwx: ... )": return .scopedOptionChange
case "(?ismwx-ismwx)": return .inlineOptionChange
default: return nil
}
}
}
private struct FlowPatternBreakdownCollector {
let source: String
let catalog: CheatSheetCatalog
private(set) var metacharacters = [FlowCheatSheetMatch]()
private(set) var operators = [FlowCheatSheetMatch]()
private var seenMetacharacters = Set<String>()
private var seenOperators = Set<String>()
init(source: String, catalog: CheatSheetCatalog) {
self.source = source
self.catalog = catalog
}
var result: FlowPatternBreakdown {
FlowPatternBreakdown(metacharacters: metacharacters, operators: operators)
}
mutating func collect(from node: AST.Node) {
switch node {
case .alternation(let alternation):
addOperator(.alternation)
alternation.children.forEach { collect(from: $0) }
case .concatenation(let concatenation):
concatenation.children.forEach { collect(from: $0) }
case .group(let group):
collect(group)
case .conditional(let conditional):
collect(condition: conditional.condition)
collect(from: conditional.trueBranch)
collect(from: conditional.falseBranch)
case .quantification(let quantification):
addOperator(quantifierSignature(for: quantification))
collect(from: quantification.child)
case .quote:
addMetacharacter(.quoteStart)
addMetacharacter(.quoteEnd)
case .trivia(let trivia):
if sourceText(for: trivia.location)?.hasPrefix("(?#") == true {
addOperator(.commentGroup)
}
case .interpolation:
break
case .atom(let atom):
collect(atom)
case .customCharacterClass(let characterClass):
addMetacharacter(.customCharacterClass)
characterClass.members.forEach { collect(characterClassMember: $0) }
case .absentFunction, .empty:
break
}
}
mutating func collectGlobalOption(_ option: AST.GlobalMatchingOption) {
addOperator(.inlineOptionChange, display: sourceText(for: option.location))
}
private mutating func collect(_ group: AST.Group) {
switch group.kind.value {
case .capture, .namedCapture, .balancedCapture:
addOperator(.capturingGroup)
case .nonCapture, .nonCaptureReset:
addOperator(.nonCapturingGroup)
case .atomicNonCapturing:
addOperator(.atomicGroup)
case .lookahead, .nonAtomicLookahead:
addOperator(.lookahead)
case .negativeLookahead:
addOperator(.negativeLookahead)
case .lookbehind, .nonAtomicLookbehind:
addOperator(.lookbehind)
case .negativeLookbehind:
addOperator(.negativeLookbehind)
case .changeMatchingOptions:
addOperator(.scopedOptionChange, display: sourceText(for: group.location))
case .scriptRun, .atomicScriptRun:
break
}
collect(from: group.child)
}
private mutating func collect(condition: AST.Conditional.Condition) {
if case .group(let group) = condition.kind {
collect(group)
}
}
private mutating func collect(_ atom: AST.Atom) {
let text = sourceText(for: atom.location)
switch atom.kind {
case .char:
if text?.hasPrefix("\\") == true {
addMetacharacter(.escapedLiteral, display: text)
}
case .scalar:
if let text {
if text.hasPrefix("\\u") {
addMetacharacter(.unicodeScalar4, display: text)
} else if text.hasPrefix("\\U") {
addMetacharacter(.unicodeScalar8, display: text)
} else if text.hasPrefix("\\x{") {
addMetacharacter(.hexScalarBraced, display: text)
} else if text.hasPrefix("\\x") {
addMetacharacter(.hexScalar2, display: text)
} else if text.hasPrefix("\\0") {
addMetacharacter(.octalScalar, display: text)
}
}
case .scalarSequence:
addMetacharacter(.hexScalarBraced, display: text)
case .property(let property):
addMetacharacter(property.isInverted ? .unicodePropertyInverted : .unicodeProperty, display: text)
case .escaped(let builtin):
collectEscapedBuiltin(builtin, display: text)
case .keyboardControl:
addMetacharacter(.controlCharacter, display: text)
case .keyboardMeta, .keyboardMetaControl:
break
case .namedCharacter:
addMetacharacter(.namedCharacter, display: text)
case .dot:
addMetacharacter(.wildcard, display: text)
case .caretAnchor:
addMetacharacter(.lineStart, display: text)
case .dollarAnchor:
addMetacharacter(.lineEnd, display: text)
case .backreference:
addMetacharacter(.backreference, display: text)
case .subpattern:
break
case .callout, .backtrackingDirective:
break
case .changeMatchingOptions:
addOperator(.inlineOptionChange, display: text)
case .invalid:
break
}
}
private mutating func collectEscapedBuiltin(_ builtin: AST.Atom.EscapedBuiltin, display: String?) {
switch builtin {
case .alarm: addMetacharacter(.alarm, display: display)
case .escape: addMetacharacter(.escapeCharacter, display: display)
case .formfeed: addMetacharacter(.formFeed, display: display)
case .newline: addMetacharacter(.newline, display: display)
case .carriageReturn: addMetacharacter(.carriageReturn, display: display)
case .tab: addMetacharacter(.tab, display: display)
case .decimalDigit: addMetacharacter(.decimalDigit, display: display)
case .notDecimalDigit: addMetacharacter(.notDecimalDigit, display: display)
case .whitespace: addMetacharacter(.whitespace, display: display)
case .notWhitespace: addMetacharacter(.notWhitespace, display: display)
case .wordCharacter: addMetacharacter(.wordCharacter, display: display)
case .notWordCharacter: addMetacharacter(.notWordCharacter, display: display)
case .graphemeCluster: addMetacharacter(.graphemeCluster, display: display)
case .wordBoundary: addMetacharacter(.wordBoundary, display: display)
case .notWordBoundary: addMetacharacter(.notWordBoundary, display: display)
case .startOfSubject: addMetacharacter(.startOfInput, display: display)
case .endOfSubjectBeforeNewline: addMetacharacter(.endOfInputBeforeNewline, display: display)
case .endOfSubject: addMetacharacter(.endOfInput, display: display)
case .firstMatchingPositionInSubject: addMetacharacter(.previousMatchEnd, display: display)
case .backspace: addMetacharacter(.backspaceInSet, display: display)
case .singleDataUnit, .horizontalWhitespace, .notHorizontalWhitespace,
.notNewline, .newlineSequence, .verticalTab, .notVerticalTab,
.resetStartOfMatch, .trueAnychar, .textSegment, .notTextSegment:
break
}
}
private mutating func collect(characterClassMember member: AST.CustomCharacterClass.Member) {
switch member {
case .custom(let characterClass):
addMetacharacter(.customCharacterClass)
characterClass.members.forEach { collect(characterClassMember: $0) }
case .range(let range):
collect(range.lhs)
collect(range.rhs)
case .atom(let atom):
collect(atom)
case .quote:
addMetacharacter(.quoteStart)
addMetacharacter(.quoteEnd)
case .trivia:
break
case .setOperation(let lhs, _, let rhs):
lhs.forEach { collect(characterClassMember: $0) }
rhs.forEach { collect(characterClassMember: $0) }
}
}
private func quantifierSignature(for quantification: AST.Quantification) -> FlowCheatSheetKey {
switch (quantification.amount.value, quantification.kind.value) {
case (.zeroOrMore, .eager): return .zeroOrMore
case (.oneOrMore, .eager): return .oneOrMore
case (.zeroOrOne, .eager): return .zeroOrOne
case (.exactly, .eager): return .exactlyN
case (.nOrMore, .eager): return .nOrMore
case (.range, .eager), (.upToN, .eager): return .range
case (.zeroOrMore, .reluctant): return .zeroOrMoreReluctant
case (.oneOrMore, .reluctant): return .oneOrMoreReluctant
case (.zeroOrOne, .reluctant): return .zeroOrOneReluctant
case (.exactly, .reluctant): return .exactlyNReluctant
case (.nOrMore, .reluctant): return .nOrMoreReluctant
case (.range, .reluctant), (.upToN, .reluctant): return .rangeReluctant
case (.zeroOrMore, .possessive): return .zeroOrMorePossessive
case (.oneOrMore, .possessive): return .oneOrMorePossessive
case (.zeroOrOne, .possessive): return .zeroOrOnePossessive
case (.exactly, .possessive): return .exactlyNPossessive
case (.nOrMore, .possessive): return .nOrMorePossessive
case (.range, .possessive), (.upToN, .possessive): return .rangePossessive
}
}
private mutating func addMetacharacter(_ key: FlowCheatSheetKey, display: String? = nil) {
guard let item = catalog.metacharacter(for: key),
seenMetacharacters.insert(key.rawValue).inserted else {
return
}
metacharacters.append(
FlowCheatSheetMatch(
id: key.rawValue,
title: display ?? item.exp,
description: item.des
)
)
}
private mutating func addOperator(_ key: FlowCheatSheetKey, display: String? = nil) {
guard let item = catalog.operatorItem(for: key),
seenOperators.insert(key.rawValue).inserted else {
return
}
operators.append(
FlowCheatSheetMatch(
id: key.rawValue,
title: display ?? item.exp,
description: item.des
)
)
}
private func sourceText(for location: SourceLocation) -> String? {
guard location.start >= source.startIndex,
location.end <= source.endIndex,
location.start <= location.end else {
return nil
}
return String(source[location.start..<location.end])
}
}
private indirect enum FlowComponent {
case node(FlowNode)
case group(FlowGroup)
case sequence([FlowComponent])
case alternation([[FlowComponent]])
case quantified(FlowComponent, FlowQuantifier)
case empty
}
private struct FlowQuantifier {
let label: String
let isOptional: Bool
}
private struct FlowNode {
enum Style: Equatable {
case literal
case characterClass
case capturingGroup
case grouping
case assertion
case anchor
case wildcard
case directive
case special
case invalid
}
let style: Style
let label: String
}
private struct FlowGroup {
let style: FlowNode.Style
let title: String
let content: FlowComponent
}
private struct FlowDiagramView: View {
let component: FlowComponent
var body: some View {
FlowComponentView(component: component)
.fixedSize(horizontal: true, vertical: true)
}
}
private struct FlowComponentView: View {
let component: FlowComponent
var borderStyle: FlowBorderStyle = .solid
var body: some View {
switch component {
case .node(let node):
NodeView(node: node, borderStyle: borderStyle)
case .group(let group):
GroupView(group: group, borderStyle: borderStyle)
case .sequence(let components):
FlowSequenceView(components: components)
case .alternation(let branches):
AlternationView(branches: branches)
case .quantified(let child, let quantifier):
QuantifiedFlowView(
component: child,
quantifier: quantifier,
borderStyle: child.supportsBorderStyling && quantifier.isOptional ? .dashed : .solid
)
case .empty:
NodeView(node: FlowNode(style: .special, label: "Empty"), borderStyle: borderStyle)
}
}
}
private struct PatternBreakdownView: View {
let breakdown: FlowPatternBreakdown
var body: some View {
VStack(alignment: .leading, spacing: 12) {
if !breakdown.metacharacters.isEmpty {
PatternBreakdownSectionView(
title: "Metacharacters",
items: breakdown.metacharacters
)
}
if !breakdown.operators.isEmpty {
PatternBreakdownSectionView(
title: "Operators",
items: breakdown.operators
)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
private struct PatternBreakdownSectionView: View {
let title: LocalizedStringKey
let items: [FlowCheatSheetMatch]
var body: some View {
let tokenColumnWidth: CGFloat = 112
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(.caption.weight(.semibold))
.foregroundStyle(FlowPalette.sectionLabel)
.textCase(.uppercase)
.tracking(1.2)
VStack(alignment: .leading, spacing: 0) {
ForEach(Array(items.enumerated()), id: \.element.id) { index, item in
HStack(alignment: .center, spacing: 14) {
Text(verbatim: item.title)
.font(.callout.monospaced().weight(.semibold))
.foregroundStyle(FlowPalette.ink)
.lineLimit(2)
.minimumScaleFactor(0.7)
.multilineTextAlignment(.center)
.frame(width: tokenColumnWidth)
.frame(minHeight: 42)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(FlowPalette.tokenBoxFill)
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(FlowPalette.tokenBoxBorder, lineWidth: 1)
)
)
Text(verbatim: item.description)
.font(.subheadline)
.foregroundStyle(FlowPalette.secondaryText)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 10)
if index < items.count - 1 {
Divider()
}
}
}
}
}
}
private struct FlowSequenceView: View {
let components: [FlowComponent]
var body: some View {
HStack(alignment: .center, spacing: 0) {
ForEach(Array(components.enumerated()), id: \.offset) { index, component in
if index > 0 {
Spacer(minLength: 6)
Image(systemName: "arrow.right")
.font(.caption2.weight(.semibold))
.foregroundStyle(FlowPalette.connector)
.frame(width: 14)
Spacer(minLength: 6)
}
FlowComponentView(component: component)
}
}
.frame(maxWidth: .infinity, alignment: .center)
}
}
private struct AlternationView: View {
let branches: [[FlowComponent]]
var body: some View {
VStack(alignment: .center, spacing: 8) {
ForEach(Array(branches.enumerated()), id: \.offset) { index, branch in
if index > 0 {
FlowAlternationDivider()
}
FlowSequenceView(components: branch)
}
}
.frame(maxWidth: .infinity, alignment: .center)
.padding(.vertical, 2)
}
}
private struct GroupView: View {
let group: FlowGroup
let borderStyle: FlowBorderStyle
var body: some View {
VStack(alignment: .center, spacing: 8) {
Text(group.title.uppercased())
.font(.caption.weight(.bold))
.tracking(1.6)
.foregroundStyle(titleColor)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity, alignment: .center)
VStack(alignment: .center, spacing: 8) {
FlowComponentView(component: group.content)
}
.frame(maxWidth: .infinity, alignment: .center)
}
.padding(.horizontal, 10)
.padding(.vertical, 10)
.overlay(
RoundedRectangle(cornerRadius: 18, style: .continuous)
.stroke(borderColor, style: borderStyle.strokeStyle(lineWidth: 1.5))
)
}
private var borderColor: Color {
styleColor(for: group.style).opacity(0.28)
}
private var titleColor: Color {
styleColor(for: group.style)
}
}
private struct QuantifiedFlowView: View {
let component: FlowComponent
let quantifier: FlowQuantifier
let borderStyle: FlowBorderStyle
var body: some View {
VStack(spacing: 2) {
FlowComponentView(component: component, borderStyle: borderStyle)
Text(quantifier.label)
.font(.caption2.monospaced())
.foregroundStyle(quantifierColor)
}
}
private var quantifierColor: Color {
if quantifier.label.contains("?") {
return FlowPalette.quantifierSecondary
}
return FlowPalette.quantifierPrimary
}
}
private struct NodeView: View {
let node: FlowNode
let borderStyle: FlowBorderStyle
var body: some View {
Text(node.label)
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.lineLimit(1)
.foregroundStyle(labelColor)
.frame(minWidth: 40, minHeight: 40)
.padding(.horizontal, 8)
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(fillColor)
.overlay(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.stroke(borderColor, style: borderStyle.strokeStyle(lineWidth: 1))
)
)
.fixedSize()
}
private var fillColor: Color {
if isEndpoint {
return FlowPalette.endpointFill
}
return FlowPalette.nodeFill
}
private var borderColor: Color {
if isEndpoint {
return FlowPalette.endpointBorder
}
return color.opacity(0.28)
}
private var labelColor: Color {
FlowPalette.ink
}
private var isEndpoint: Bool {
node.style == .anchor && (node.label.contains("Start") || node.label.contains("End"))
}
private var color: Color {
styleColor(for: node.style)
}
}
private enum FlowBorderStyle {
case solid
case dashed
func strokeStyle(lineWidth: CGFloat) -> StrokeStyle {
switch self {
case .solid:
return StrokeStyle(lineWidth: lineWidth)
case .dashed:
return StrokeStyle(lineWidth: lineWidth, dash: [6, 4])
}
}
}
private extension FlowComponent {
var supportsBorderStyling: Bool {
switch self {
case .node, .group:
return true
case .sequence, .alternation, .quantified, .empty:
return false
}
}
}
private struct FlowAlternationDivider: View {
var body: some View {
HStack(spacing: 10) {
Rectangle()
.fill(FlowPalette.divider)
.frame(height: 1)
Text("OR")
.font(.caption.weight(.bold))
.tracking(1.4)
.foregroundStyle(FlowPalette.secondaryText)
Rectangle()
.fill(FlowPalette.divider)
.frame(height: 1)
}
.padding(.horizontal, 6)
}
}
private func styleColor(for style: FlowNode.Style) -> Color {
switch style {
case .literal:
return FlowPalette.literal
case .characterClass:
return FlowPalette.characterClass
case .capturingGroup:
return FlowPalette.group
case .grouping:
return FlowPalette.grouping
case .assertion:
return FlowPalette.assertion
case .anchor:
return FlowPalette.anchor
case .wildcard:
return FlowPalette.wildcard
case .directive:
return FlowPalette.directive
case .special:
return FlowPalette.special
case .invalid:
return FlowPalette.invalid
}
}
private enum FlowPalette {
static let nodeFill = Color(uiColor: .secondarySystemBackground)
static let endpointFill = Color.accentColor.opacity(0.18)
static let endpointBorder = Color.accentColor.opacity(0.45)
static let connector = Color.secondary.opacity(0.45)
static let divider = Color.secondary.opacity(0.22)
static let ink = Color.primary
static let secondaryText = Color.secondary
static let sectionLabel = Color.accentColor
static let tokenBoxFill = Color(uiColor: .secondarySystemBackground)
static let tokenBoxBorder = Color.accentColor.opacity(0.20)
static let quantifierPrimary = Color(red: 0.267, green: 0.553, blue: 0.942)
static let quantifierSecondary = Color(red: 0.000, green: 0.620, blue: 0.592)
static let literal = Color(red: 0.430, green: 0.620, blue: 0.920)
static let characterClass = Color(red: 0.336, green: 0.700, blue: 0.650)
static let group = Color(red: 0.290, green: 0.560, blue: 0.900)
static let grouping = Color(red: 0.500, green: 0.620, blue: 0.860)
static let assertion = Color(red: 0.396, green: 0.690, blue: 0.880)
static let anchor = Color(red: 0.420, green: 0.560, blue: 0.840)
static let wildcard = Color(red: 0.290, green: 0.700, blue: 0.820)
static let directive = Color(red: 0.510, green: 0.620, blue: 0.920)
static let special = Color(red: 0.470, green: 0.650, blue: 0.850)
static let invalid = Color(red: 0.650, green: 0.690, blue: 0.760)
}
private extension CheatSheetPlist {
static let localizedCheatSheet: CheatSheetPlist? = {
guard let url = Bundle.main.url(forResource: "CheatSheet", withExtension: "plist"),
let data = try? Data(contentsOf: url) else {
return nil
}
return try? PropertyListDecoder().decode(CheatSheetPlist.self, from: data)
}()
}
// swiftlint:enable file_length type_body_length cyclomatic_complexity
================================================
FILE: RegEx+/HomeView.swift
================================================
//
// TabView.swift
// RegEx+
//
// Created by Lex on 2020/4/21.
// Copyright © 2020 Lex.sh. All rights reserved.
//
import SwiftUI
struct HomeView: View {
@Environment(\.managedObjectContext) var managedObjectContext
var body: some View {
if #available(iOS 16.0, macOS 13.0, *) {
NavigationSplitView {
LibraryView()
} detail: {
Text(verbatim: "RegEx+")
.font(.largeTitle)
}
} else {
NavigationView {
LibraryView()
Text(verbatim: "RegEx+")
.font(.largeTitle)
}
.currentDeviceNavigationViewStyle()
}
}
}
private extension View {
func currentDeviceNavigationViewStyle() -> AnyView {
#if targetEnvironment(macCatalyst)
return AnyView(
navigationViewStyle(DoubleColumnNavigationViewStyle())
)
#else
if UIDevice.current.userInterfaceIdiom == .pad {
return AnyView(navigationViewStyle(DoubleColumnNavigationViewStyle()))
} else {
return AnyView(navigationViewStyle(DefaultNavigationViewStyle()))
}
#endif
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
================================================
FILE: RegEx+/Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>RegEx+</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationCategoryType</key>
<string>public.app-category.developer-tools</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UIStatusBarTintParameters</key>
<dict>
<key>UINavigationBar</key>
<dict>
<key>Style</key>
<string>UIBarStyleDefault</string>
<key>Translucent</key>
<false/>
</dict>
</dict>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
================================================
FILE: RegEx+/Library/LibraryItemView.swift
================================================
//
// LibraryItemView.swift
// RegEx+
//
// Created by Lex on 2020/5/3.
// Copyright © 2020 Lex.sh. All rights reserved.
//
import SwiftUI
import CoreData
struct LibraryItemView: View, Equatable {
@ObservedObject var regEx: RegEx
var body: some View {
NavigationLink {
EditorView(regEx: regEx)
.equatable()
.id(regEx.objectID)
} label: {
VStack(alignment: .leading, spacing: 4) {
Text(regEx.name)
.font(.headline)
if !regEx.raw.isEmpty {
Text(regEx.raw)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(1)
}
}
.frame(minHeight: 50, maxHeight: 200)
.paddingVertical()
}
.isDetailLink(true)
}
static func == (lhs: LibraryItemView, rhs: LibraryItemView) -> Bool {
lhs.regEx.objectID == rhs.regEx.objectID
&& lhs.regEx.name == rhs.regEx.name
&& lhs.regEx.raw == rhs.regEx.raw
}
}
private extension View {
func paddingVertical() -> AnyView {
#if targetEnvironment(macCatalyst)
AnyView(padding(.vertical))
#else
AnyView(self)
#endif
}
}
struct LibraryItemView_Previews: PreviewProvider {
private static var regEx: RegEx = {
var r: RegEx = RegEx(context: DataManager.shared.persistentContainer.viewContext)
r.name = "Dollars"
r.raw = #"\$?((\d+)\.?(\d\d)?)"#
r.sample = "$100.00 12.50 $10"
r.substitution = "$3"
return r
}()
static var previews: some View {
NavigationView {
List {
LibraryItemView(regEx: regEx)
LibraryItemView(regEx: regEx)
LibraryItemView(regEx: regEx)
}
}
.navigationTitle(Text(verbatim: "Test"))
}
}
================================================
FILE: RegEx+/Library/LibraryView+Data.swift
================================================
//
// LibraryView+Data.swift
// RegEx+
//
// Created by Lex on 2020/5/3.
// Copyright © 2020 Lex.sh. All rights reserved.
//
import CoreData
extension LibraryView {
func deleteRegEx(indexSet: IndexSet) {
let source = indexSet.first!
let regEx = regExItems[source]
managedObjectContext.delete(regEx)
save()
}
func addRegEx(withSample: Bool) {
let regEx = RegEx(context: managedObjectContext)
if withSample, let randomItem = sampleData().randomElement() {
regEx.name = randomItem.name
regEx.raw = randomItem.raw
regEx.sample = randomItem.sample
regEx.allowCommentsAndWhitespace = randomItem.allowComments
regEx.createdAt = Date()
} else {
regEx.name = NSLocalizedString("Untitled", comment: "Default item name")
regEx.raw = ""
regEx.createdAt = Date()
}
save()
editMode = .inactive
}
private func save() {
DataManager.shared.saveContext()
}
private func sampleData() -> [SampleItem] {
return [
SampleItem("Dollars", raw: #"(\$[\d]+)\.?(\d{2})?"#),
SampleItem("Hex", raw: #"#?([a-f0-9]{6}|[a-f0-9]{3})"#, sample: "#336699\n#F2A\nFF9933"),
SampleItem("Allow Comments", raw: #"(\$[\d]+) # Dollars symbol and digits"#, allowComments: true),
SampleItem("Roman Numeral", raw: #"M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})"#),
SampleItem("Email", raw: #"([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})"#, sample: "ive@apple.com"),
SampleItem("HTML <li> tag", raw: #"<li>(.*?)</li>"#, sample: "<li>iPhone</li>\n<li>iPad</li>"),
]
}
}
private struct SampleItem {
let name: String
let raw: String
let sample: String
let allowComments: Bool
init(_ name: String, raw: String, sample: String = "", allowComments: Bool = false) {
self.name = name
self.raw = raw
self.sample = sample
self.allowComments = allowComments
}
}
================================================
FILE: RegEx+/Library/LibraryView.swift
================================================
//
// LibraryView.swift
// RegEx+
//
// Created by Lex on 2020/4/21.
// Copyright © 2020 Lex.sh. All rights reserved.
//
import SwiftUI
import CoreData
struct LibraryView: View, Equatable {
static func == (lhs: LibraryView, rhs: LibraryView) -> Bool {
lhs.regExItems.map(\.objectID).hashValue == rhs.regExItems.map(\.objectID).hashValue
}
@Environment(\.managedObjectContext) var managedObjectContext
@FetchRequest(fetchRequest: RegEx.fetchAllRegEx()) var regExItems: FetchedResults<RegEx>
@State private var searchTerm = ""
@State var editMode = EditMode.inactive
private var filteredItems: [RegEx] {
if searchTerm.isEmpty {
return Array(regExItems)
}
return regExItems.filter { item in
item.name.localizedCaseInsensitiveContains(searchTerm) ||
item.raw.localizedCaseInsensitiveContains(searchTerm)
}
}
var body: some View {
VStack(alignment: .leading) {
SearchView(text: $searchTerm)
.padding(.horizontal)
if regExItems.isEmpty {
VStack(alignment: .center) {
Text("Your RegEx+ library is empty")
.font(.subheadline)
.foregroundStyle(.secondary)
Button {
addRegEx(withSample: true)
} label: {
Text("Create a sample")
}
.buttonStyle(.bordered)
}
.frame(maxWidth: .greatestFiniteMagnitude, maxHeight: .greatestFiniteMagnitude)
} else {
List {
ForEach(filteredItems, id: \.objectID) {
LibraryItemView(regEx: $0).equatable()
}
.onDelete(perform: deleteRegEx)
}
.currentDeviceListStyle()
.environment(\.editMode, $editMode)
}
}
.navigationTitle("RegEx+")
.setNavigationItems(libraryView: self)
}
var editButton: some View {
Button(action: {
editMode = editMode.isEditing ? .inactive : .active
}, label: {
Text(editMode.isEditing ? "Done" : "Edit")
})
}
var aboutButton: some View {
NavigationLink(destination: AboutView()) {
Image(systemName: "info.circle")
.imageScale(.large)
}
}
var addButton: some View {
Button {
addRegEx(withSample: false)
} label: {
Image(systemName: "plus.circle.fill")
.imageScale(.large)
}
}
}
private extension View {
@ViewBuilder
func currentDeviceListStyle() -> some View {
#if targetEnvironment(macCatalyst)
self.listStyle(.plain)
.padding(.horizontal)
#else
if #available(iOS 14.0, *) {
self.listStyle(.insetGrouped)
} else {
self.listStyle(.grouped)
}
#endif
}
}
private extension View {
@ViewBuilder
func setNavigationItems(libraryView: LibraryView) -> some View {
self.toolbar {
ToolbarItem(placement: .topBarLeading) {
libraryView.editButton
}
#if targetEnvironment(macCatalyst)
ToolbarItem(placement: .topBarTrailing) {
HStack {
libraryView.aboutButton
libraryView.addButton
}
}
#else
ToolbarItem(placement: .topBarTrailing) {
libraryView.aboutButton
}
ToolbarItem(placement: .topBarTrailing) {
libraryView.addButton
.padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 0))
}
#endif
}
}
}
#if DEBUG
struct LibraryView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
LibraryView()
.environment(\.managedObjectContext, DataManager.shared.persistentContainer.viewContext)
}
}
}
#endif
================================================
FILE: RegEx+/Localizable.xcstrings
================================================
{
"sourceLanguage" : "en",
"strings" : {
"%@" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@"
}
}
}
},
"%lld matches" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld Treffer"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld coincidencias"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld correspondances"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld corrispondenze"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld マッチ"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld개 일치"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld resultaten"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld dopasowań"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld 次匹配"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld 次匹配"
}
}
}
},
"1 match" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "1 Treffer"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "1 coincidencia"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "1 correspondance"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "1 corrispondenza"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "1マッチ"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "1개 일치"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "1 resultaat"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "1 dopasowanie"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "一次匹配"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "一次匹配"
}
}
}
},
"Allow Comments and Whitespace" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kommentare und Leerzeichen erlauben"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Permitir comentarios y espacios en blanco"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Autoriser les commentaires et les espaces"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Consenti commenti e spazi"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "コメントと空白を許可"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "주석 및 공백 허용"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Reacties en witruimte toestaan"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Zezwalaj na komentarze i białe znaki"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "允许注释和空格"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "允許註釋和空格"
}
}
}
},
"Anchors Match Lines" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Anker passen zu Zeilen"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Anclas coinciden con líneas"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Les ancres correspondent aux lignes"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ancore corrispondono alle righe"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "アンカーで行をマッチ"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "앵커로 라인 일치"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ankers komen overeen met regels"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kotwice dopasowują linie"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "锚点匹配行"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "錨點匹配行"
}
}
}
},
"Case Insensitive" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Groß-/Kleinschreibung ignorieren"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Insensible a mayúsculas/minúsculas"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Insensible à la casse"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ignora maiuscole/minuscole"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "大文字小文字を区別しない"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "대소문자 구분 안 함"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hoofdletterongevoelig"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ignoruj wielkość liter"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "不区分大小写"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "不區分大小寫"
}
}
}
},
"Cheat Sheet" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Spickzettel"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hoja de Referencia"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Antisèche"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Guida di Riferimento"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "チートシート"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "치트 시트"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Spiekbriefje"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ściąga"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "小抄"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "小抄"
}
}
}
},
"Copies the substitution result to clipboard" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ersetzungsergebnis in Zwischenablage kopieren"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Copia el resultado de sustitución al portapapeles"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Copie le résultat de substitution dans le presse-papiers"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Copia il risultato della sostituzione negli appunti"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "置換結果をクリップボードにコピー"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "치환 결과를 클립보드에 복사"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kopieert het vervangingsresultaat naar het klembord"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kopiuje wynik podstawienia do schowka"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "将替换结果复制到剪贴板"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "將替換結果複製到剪貼板"
}
}
}
},
"Copy substitution result" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ersetzungsergebnis kopieren"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Copiar resultado de sustitución"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Copier le résultat de substitution"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Copia risultato di sostituzione"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "置換結果をコピー"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "치환 결과 복사"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Vervangingsresultaat kopiëren"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kopiuj wynik podstawienia"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "复制替换结果"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "複製替換結果"
}
}
}
},
"Create a sample" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Beispiel erstellen"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Crear un ejemplo"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Créer un exemple"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Crea un esempio"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "サンプルを作成"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "샘플 만들기"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Voorbeeld maken"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Utwórz przykład"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "新建样例"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "新建樣例"
}
}
}
},
"Done" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Fertig"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hecho"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Terminé"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Fatto"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "完了"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "완료"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Gereed"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Gotowe"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "完成"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "完成"
}
}
}
},
"Dot Matches Line Separators" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Punkt entspricht Zeilentrenner"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Punto coincide con separadores de línea"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Le point correspond aux séparateurs de ligne"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Il punto corrisponde ai separatori di riga"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "ドットで改行文字をマッチ"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "점이 줄 구분 기호와 일치"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Punt komt overeen met regelscheidingstekens"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kropka dopasowuje separatory linii"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "点符号匹配行分隔符"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "點符號匹配行分隔符"
}
}
}
},
"Edit" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bearbeiten"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Editar"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Modifier"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Modifica"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "編集"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "편집"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bewerk"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Edytuj"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "编辑"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "編輯"
}
}
}
},
"Enter a regular expression to see the flow diagram" : {
},
"Flow Diagram" : {
},
"Ignore Metacharacters" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Metazeichen ignorieren"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ignorar metacaracteres"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ignorer les métacaractères"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ignora metacaratteri"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "メタ文字を無視"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "메타 문자 무시"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Metatekens negeren"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ignoruj metaznaki"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "忽略元字符"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "忽略元字符"
}
}
}
},
"Insert %@ into text" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@ in Text einfügen"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Insertar %@ en texto"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Insérer %@ dans le texte"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Inserisci %@ nel testo"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@をテキストに挿入"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@를 텍스트에 삽입"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Voeg %@ in tekst in"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Wstaw %@ do tekstu"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "将%@插入文本"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "將%@插入文字"
}
}
}
},
"Loading..." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Laden..."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Cargando..."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Chargement..."
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Caricamento..."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "読み込み中..."
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "로드 중..."
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Laden..."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ładowanie..."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "加载中..."
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "載入中..."
}
}
}
},
"Metacharacters" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Metazeichen"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Metacaracteres"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Métacaractères"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Metacaratteri"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "メタ文字"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "메타 문자"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Metatekens"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Metaznaki"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "元字符"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "元字符"
}
}
}
},
"Name" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Name"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nombre"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nom"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nome"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "名前"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "이름"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Naam"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nazwa"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "名字"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "名字"
}
}
}
},
"Operators" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Operatoren"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Operadores"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Opérateurs"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Operatori"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "演算子"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "연산자"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Operatoren"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Operatory"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "操作符"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "操作符"
}
}
}
},
"Options" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Optionen"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Opciones"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Options"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Opzioni"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "オプション"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "옵션"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Opties"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Opcje"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "选项"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "選項"
}
}
}
},
"OR" : {
},
"Price: $$$1\\.$2\\n" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Preis: $$$1\\.$2\\n"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Precio: $$$1\\.$2\\n"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Prix : $$$1\\.$2\\n"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Prezzo: $$$1\\.$2\\n"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "価格: $$$1\\.$2\\n"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "가격: $$$1\\.$2\\n"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Prijs: $$$1\\.$2\\n"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Cena: $$$1\\.$2\\n"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "价格: $$$1\\.$2\\n"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "價格: $$$1\\.$2\\n"
}
}
}
},
"RegEx+" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "RegEx+"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "RegEx+"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "RegEx+"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "RegEx+"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "RegEx+"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "RegEx+"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "RegEx+"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "RegEx+"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "RegEx+"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "RegEx+"
}
}
}
},
"Regular Expression" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Regulärer Ausdruck"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Expresión Regular"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Expression Régulière"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Espressione Regolare"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "正規表現"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "정규 표현식"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Reguliere expressie"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Wyrażenie regularne"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "正则表达式"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "正則表達式"
}
}
}
},
"Sample Text" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Beispieltext"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Texto de Ejemplo"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Texte d'Exemple"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Testo di Esempio"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "サンプルテキスト"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "샘플 텍스트"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Voorbeeldtekst"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Przykładowy tekst"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "示例文字"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "示例文字"
}
}
}
},
"Search..." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Suchen..."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Buscar..."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Rechercher..."
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Cerca..."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "検索..."
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "검색..."
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Zoeken..."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Szukaj..."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "搜索..."
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "搜索..."
}
}
}
},
"Share" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Teilen"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Compartir"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Partager"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Condividi"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "共有"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "공유"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Deel"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Udostępnij"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "分享"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "分享"
}
}
}
},
"Share this regular expression" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Diesen regulären Ausdruck teilen"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Compartir esta expresión regular"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Partager cette expression régulière"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Condividi questa espressione regolare"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "この正規表現を共有"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "이 정규 표현식 공유"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Deel deze reguliere expressie"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Udostępnij to wyrażenie regularne"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "分享这个正则表达式"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "分享這個正則表達式"
}
}
}
},
"Substitution Result" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ersetzungsergebnis"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Resultado de Sustitución"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Résultat de Substitution"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Risultato di Sostituzione"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "置換結果"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "치환 결과"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Vervangingsresultaat"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Wynik podstawienia"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "替换结果"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "替換結果"
}
}
}
},
"Substitution Template" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ersetzungsvorlage"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Plantilla de Sustitución"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Modèle de Substitution"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Modello di Sostituzione"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "置換テンプレート"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "치환 템플릿"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Vervangingssjabloon"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Szablon podstawienia"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "替换模板"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "替換模板"
}
}
}
},
"Untitled" : {
"comment" : "Default item name",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ohne Titel"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sin título"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sans titre"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Senza titolo"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "無題"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "제목 없음"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Naamloos"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bez tytułu"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "未命名"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "未命名"
}
}
}
},
"Use Unicode Word Boundaries" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unicode-Wortgrenzen verwenden"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Usar límites de palabra Unicode"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Utiliser les limites de mots Unicode"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Usa limiti di parola Unicode"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unicode単語境界を使用"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "유니코드 단어 경계 사용"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Gebruik Unicode-woordgrenzen"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Użyj granic słów Unicode"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "使用 Unicode 字符边界"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "使用 Unicode 字符邊界"
}
}
}
},
"Use Unix Line Separators" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unix-Zeilentrenner verwenden"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Usar separadores de línea Unix"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Utiliser les séparateurs de ligne Unix"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Usa separatori di riga Unix"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unix改行文字を使用"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unix 줄 구분 기호 사용"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Gebruik Unix-regelscheidingstekens"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Użyj separatorów linii Unix"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "使用 Unix 换行符"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "使用 Unix 換行符"
}
}
}
},
"View regular expression reference guide" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Regulärer Ausdruck-Referenzhandbuch anzeigen"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ver guía de referencia de expresiones regulares"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Voir le guide de référence des expressions régulières"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Visualizza la guida di riferimento delle espressioni regolari"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "正規表現リファレンスガイドを表示"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "정규 표현식 참조 가이드 보기"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bekijk de referentiegids voor reguliere expressies"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Zobacz przewodnik po wyrażeniach regularnych"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "查看正则表达式参考指南"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "查看正則表達式參考指南"
}
}
}
},
"Your RegEx+ library is empty" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ihre RegEx+ Bibliothek ist leer"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Tu biblioteca RegEx+ está vacía"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Votre bibliothèque RegEx+ est vide"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "La tua libreria RegEx+ è vuota"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "RegEx+ライブラリが空です"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "RegEx+ 라이브러리가 비어 있습니다"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Je RegEx+ bibliotheek is leeg"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Twoja biblioteka RegEx+ jest pusta"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "你的 RegEx+ 仓库是空的"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "你的 RegEx+ 倉庫是空的"
}
}
}
}
},
"version" : "1.0"
}
================================================
FILE: RegEx+/Preview Content/Preview Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: RegEx+/RegEx+.entitlements
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.RegExCatalyst</string>
</array>
<key>com.apple.developer.icloud-services</key>
<array>
<string>CloudKit</string>
</array>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
================================================
FILE: RegEx+/RegEx.xcdatamodeld/.xccurrentversion
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>RegEx.xcdatamodel</string>
</dict>
</plist>
================================================
FILE: RegEx+/RegEx.xcdatamodeld/RegEx.xcdatamodel/contents
================================================
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19E287" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="RegEx" representedClassName="RegEx" syncable="YES">
<attribute name="allowCommentsAndWhitespace" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="anchorsMatchLines" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="caseInsensitive" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="createdAt" attributeType="Date" defaultDateTimeInterval="599504400" usesScalarValueType="NO"/>
<attribute name="dotMatchesLineSeparators" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="ignoreMetacharacters" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String" defaultValueString="Untitled"/>
<attribute name="raw" optional="YES" attributeType="String"/>
<attribute name="sample" attributeType="String" defaultValueString=""/>
<attribute name="substitution" attributeType="String" defaultValueString=""/>
<attribute name="updatedAt" attributeType="Date" defaultDateTimeInterval="599504400" usesScalarValueType="NO"/>
<attribute name="useUnicodeWordBoundaries" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="useUnixLineSeparators" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
</entity>
<configuration name="Cloud" usedWithCloudKit="YES">
<memberEntity name="RegEx"/>
</configuration>
<elements>
<element name="RegEx" positionX="-63" positionY="-18" width="128" height="238"/>
</elements>
</model>
================================================
FILE: RegEx+/SceneDelegate.swift
================================================
//
// SceneDelegate.swift
// RegEx+
//
// Created by Lex on 2020/4/21.
// Copyright © 2020 Lex.sh. All rights reserved.
//
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let viewContext = DataManager.shared.persistentContainer.viewContext
let contentView = HomeView()
.environment(\.managedObjectContext, viewContext)
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
#if targetEnvironment(macCatalyst)
if let titlebar = windowScene.titlebar {
titlebar.titleVisibility = .visible
titlebar.toolbarStyle = .unified
}
#endif
}
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
}
}
================================================
FILE: RegEx+/Views/ActivityViewController.swift
================================================
//
// ActivityViewController.swift
// RegEx+
//
// Created by Lex on 2020/5/16.
// Copyright © 2020 Lex.sh. All rights reserved.
//
import UIKit
import SwiftUI
struct ActivityViewController: UIViewControllerRepresentable {
var activityItems: [Any]
var applicationActivities: [UIActivity]?
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
}
================================================
FILE: RegEx+/Views/RegExSyntaxView.swift
================================================
//
// RegExSyntaxView.swift
// RegExPro
//
// Created by Lex on 2020/4/23.
// Copyright © 2020 Lex.sh. All rights reserved.
//
import SwiftUI
import Combine
import UIKit
private struct UITextViewWrapper: UIViewRepresentable {
typealias UIViewType = UITextView
@Binding var text: String
@Binding var calculatedHeight: CGFloat
var onDone: (() -> Void)?
func makeUIView(context: UIViewRepresentableContext<UITextViewWrapper>) -> UITextView {
let tv = UITextView()
tv.delegate = context.coordinator
tv.isEditable = true
tv.font = UIFont.preferredFont(forTextStyle: .body)
tv.isSelectable = true
tv.isUserInteractionEnabled = true
tv.isScrollEnabled = false
tv.backgroundColor = UIColor.clear
tv.textContainerInset = .zero
tv.textContainer.lineFragmentPadding = 0
if nil != onDone {
tv.returnKeyType = .done
}
tv.textStorage.delegate = syntaxHighlighter
tv.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return tv
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
if uiView.text != self.text {
uiView.text = self.text
}
if uiView.window != nil, !uiView.isFirstResponder {
uiView.becomeFirstResponder()
}
UITextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight)
}
fileprivate static func recalculateHeight(view: UIView, result: Binding<CGFloat>) {
let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
if result.wrappedValue != newSize.height {
DispatchQueue.main.async {
result.wrappedValue = newSize.height // !! must be called asynchronously
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text, height: $calculatedHeight, onDone: onDone)
}
final class Coordinator: NSObject, UITextViewDelegate {
var text: Binding<String>
var calculatedHeight: Binding<CGFloat>
var onDone: (() -> Void)?
init(text: Binding<String>, height: Binding<CGFloat>, onDone: (() -> Void)? = nil) {
self.text = text
self.calculatedHeight = height
self.onDone = onDone
}
func textViewDidChange(_ uiView: UITextView) {
text.wrappedValue = uiView.text
UITextViewWrapper.recalculateHeight(view: uiView, result: calculatedHeight)
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if let onDone = self.onDone, text == "\n" {
textView.resignFirstResponder()
onDone()
return false
}
return true
}
}
private let syntaxHighlighter = RegExSyntaxHighlighter()
}
struct RegExTextView: View {
private var placeholder: String
private var onCommit: (() -> Void)?
@Binding private var text: String
private var internalText: Binding<String> {
Binding<String>(get: { self.text }) {
self.text = $0
self.showingPlaceholder = $0.isEmpty
}
}
@State private var dynamicHeight: CGFloat = 100
@State private var showingPlaceholder = false
init (_ placeholder: String = "", text: Binding<String>, onCommit: (() -> Void)? = nil) {
self.placeholder = placeholder
self.onCommit = onCommit
self._text = text
self._showingPlaceholder = State<Bool>(initialValue: self.text.isEmpty)
}
var body: some View {
UITextViewWrapper(text: self.internalText, calculatedHeight: $dynamicHeight, onDone: onCommit)
.frame(minHeight: dynamicHeight, maxHeight: dynamicHeight)
.background(placeholderView, alignment: .topLeading)
}
var placeholderView: some View {
Group {
if showingPlaceholder {
Text(placeholder).foregroundColor(.gray)
.padding(.leading, 4)
.padding(.top, 8)
}
}
}
}
#if DEBUG
struct MultilineTextField_Previews: PreviewProvider {
static var test: String = ""
static var testBinding = Binding<String>(get: { test }, set: { test = $0 })
static var previews: some View {
VStack(alignment: .leading) {
Text("Description:")
RegExTextView("Enter some text here", text: testBinding, onCommit: {
print("Final text: \(test)")
})
.overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.black))
Text("Something static here...")
Spacer()
}
.padding()
}
}
#endif
class RegExSyntaxHighlighter: NSObject, NSTextStorageDelegate {
var fontSize: CGFloat = 16
func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int) {
textStorage.addAttributes([
.font: UIFont.systemFont(ofSize: fontSize),
.foregroundColor: UIColor.black
], range: NSRange(location: 0, length: textStorage.length))
textStorage.string.ranges(of: #"\\[$$\w]"#, options: .regularExpression).forEach { range in
textStorage.addAttributes([
.foregroundColor: UIColor.red
], range: textStorage.string.nsRange(from: range))
}
textStorage.string.ranges(of: #"[\(\)]"#, options: .regularExpression).forEach { range in
textStorage.addAttributes([
.foregroundColor: UIColor(red: 0, green: 0.5, blue: 0.2, alpha: 1)
], range: textStorage.string.nsRange(from: range))
}
textStorage.string.ranges(of: #"(?:\{)[\d,]+(?:\})"#, options: .regularExpression).forEach { range in
textStorage.addAttributes([
.font: UIFont.boldSystemFont(ofSize: fontSize),
.foregroundColor: UIColor(red: 0, green: 0.3, blue: 0, alpha: 1)
], range: textStorage.string.nsRange(from: range))
}
textStorage.string.ranges(of: #"[\?\*\.]"#, options: .regularExpression).forEach { range in
textStorage.addAttributes([
.font: UIFont.boldSystemFont(ofSize: fontSize),
.foregroundColor: UIColor(red: 0, green: 0.3, blue: 0, alpha: 1)
], range: textStorage.string.nsRange(from: range))
}
textStorage.string.ranges(of: #"[\^\[\$\]]"#, options: .regularExpression).forEach { range in
textStorage.addAttributes([
.foregroundColor: UIColor(red: 0, green: 0, blue: 0.8, alpha: 1)
], range: textStorage.string.nsRange(from: range))
}
}
}
================================================
FILE: RegEx+/Views/RegExTextView/MatchesTextView.swift
================================================
//
// MatchesTextView.swift
// RegEx+
//
// Created by Lex on 2020/5/2.
// Copyright © 2020 Lex.sh. All rights reserved.
//
import UIKit
import SwiftUI
private struct MatchesTextViewWrapper: UIViewRepresentable {
typealias UIViewType = UITextView
@Binding var text: String
@Binding var calculatedHeight: CGFloat
var matches: [NSTextCheckingResult]
var onDone: (() -> Void)?
func makeUIView(context: UIViewRepresentableContext<MatchesTextViewWrapper>) -> UITextView {
let tv = UITextView()
tv.delegate = context.coordinator
tv.isEditable = true
tv.font = UIFont.preferredFont(forTextStyle: .body)
tv.isSelectable = true
tv.isUserInteractionEnabled = true
tv.isScrollEnabled = false
tv.backgroundColor = UIColor.clear
tv.textContainerInset = .zero
tv.textContainer.lineFragmentPadding = 0
if nil != onDone {
tv.returnKeyType = .done
}
tv.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return tv
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<MatchesTextViewWrapper>) {
if uiView.attributedText.string != text {
uiView.attributedText = NSAttributedString(string: text)
}
if !text.isEmpty {
uiView.textStorage.setAttributes([
.font: UIFont.preferredFont(forTextStyle: .body),
.foregroundColor: UIColor.label
], range: NSRange(location: 0, length: uiView.text.count))
matches.forEach { result in
for index in 0..<result.numberOfRanges {
let range = result.range(at: index)
if range.location + range.length > uiView.attributedText.length {
return
}
uiView.textStorage.setAttributes([
.font: UIFont.preferredFont(forTextStyle: .body),
.foregroundColor: UIColor.systemBlue
], range: range)
}
}
}
MatchesTextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight)
}
fileprivate static func recalculateHeight(view: UIView, result: Binding<CGFloat>) {
let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
if result.wrappedValue != newSize.height {
DispatchQueue.main.async {
result.wrappedValue = newSize.height // !! must be called asynchronously
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text, height: $calculatedHeight, onDone: onDone)
}
final class Coordinator: NSObject, UITextViewDelegate {
var text: Binding<String>
var calculatedHeight: Binding<CGFloat>
var onDone: (() -> Void)?
init(text: Binding<String>, height: Binding<CGFloat>, onDone: (() -> Void)? = nil) {
self.text = text
self.calculatedHeight = height
self.onDone = onDone
}
func textViewDidChange(_ uiView: UITextView) {
text.wrappedValue = uiView.text
MatchesTextViewWrapper.recalculateHeight(view: uiView, result: calculatedHeight)
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if let onDone, text == "\n" {
textView.resignFirstResponder()
onDone()
return false
}
return true
}
}
}
struct MatchesTextView: View, Equatable {
static func == (lhs: MatchesTextView, rhs: MatchesTextView) -> Bool {
lhs.text == rhs.text
&& lhs.matches == rhs.matches
}
private var placeholder: String
private var onCommit: (() -> Void)?
@Binding private var text: String
private var internalText: Binding<String> {
Binding<String>(get: { self.text }) {
self.text = $0
self.showingPlaceholder = $0.isEmpty
}
}
@Binding private var matches: [NSTextCheckingResult]
@State private var dynamicHeight: CGFloat = 100
@State private var showingPlaceholder = false
init (_ placeholder: String = "", text: Binding<String>, matches: Binding<[NSTextCheckingResult]>, onCommit: (() -> Void)? = nil) {
self.placeholder = placeholder
self.onCommit = onCommit
_matches = matches
_text = text
_showingPlaceholder = State<Bool>(initialValue: self.text.isEmpty)
}
var body: some View {
MatchesTextViewWrapper(
text: internalText,
calculatedHeight: $dynamicHeight,
matches: matches,
onDone: onCommit
)
.frame(minHeight: dynamicHeight, maxHeight: dynamicHeight)
.background(placeholderView, alignment: .topLeading)
}
var placeholderView: some View {
Group {
if showingPlaceholder {
VStack {
Text(placeholder)
.foregroundColor(.secondary)
}
}
}
}
}
#if DEBUG
struct MatchesTextView_Previews: PreviewProvider {
static var test = "^(\\d+)\\.(\\d{2}) (\\d+)\\.(\\d{2}) (\\d+)\\.(\\d{2}) (\\d+)\\.(\\d{2})"
static var testBinding = Binding<String>(get: { test }, set: { test = $0 })
static var matches = [NSTextCheckingResult]()
static var matchesBinding = Binding<[NSTextCheckingResult]>(get: { matches }, set: { matches = $0 })
static var previews: some View {
VStack(alignment: .leading) {
Text("Description:")
MatchesTextView("Enter some text here",
text: testBinding,
matches: matchesBinding) {
print("Final text: \(test)")
}
.overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.black))
Spacer()
}
.padding()
}
}
#endif
================================================
FILE: RegEx+/Views/RegExTextView/RegExSyntaxHighlighter.swift
================================================
//
// RegExSyntaxHighlighter.swift
// RegEx+
//
// Created by Lex on 2020/5/2.
// Copyright © 2020 Lex.sh. All rights reserved.
//
import Foundation
import UIKit
import _RegexParser
enum RegExTextHighlightingMode: Equatable {
case plainText
case regularExpression(NSRegularExpression.Options)
var signature: Int {
switch self {
case .plainText:
return 0
case .regularExpression(let options):
return Int(truncatingIfNeeded: options.rawValue) ^ 0x51A9
}
}
}
fileprivate extension NSMutableAttributedString {
func applyAttributes(_ attributes: [NSAttributedString.Key: Any], to range: Range<String.Index>) {
let nsRange = NSRange(range, in: string)
addAttributes(attributes, range: nsRange)
}
}
final class RegExSyntaxHighlighter: NSObject, NSTextStorageDelegate {
weak var textStorage: NSTextStorage?
var highlightingMode: RegExTextHighlightingMode = .regularExpression([])
private var lastHighlightSignature: Int?
func highlightRegularExpression(force: Bool = false) {
guard let textStorage else {
return
}
let signature = highlightSignature(for: textStorage.string)
if !force, signature == lastHighlightSignature {
return
}
let string = textStorage.string
let attributed = NSMutableAttributedString(attributedString: textStorage)
let fullRange = NSRange(location: 0, length: attributed.length)
attributed.removeAttribute(.foregroundColor, range: fullRange)
attributed.removeAttribute(.underlineStyle, range: fullRange)
attributed.removeAttribute(.underlineColor, range: fullRange)
attributed.addAttribute(.foregroundColor, value: UIColor.label, range: fullRange)
semanticTokens(for: string).sorted(by: tokenSort).forEach { token in
attributed.applyAttributes(token.attributes, to: token.range)
}
textStorage.setAttributedString(attributed)
lastHighlightSignature = signature
}
func textStorage(
_ textStorage: NSTextStorage,
didProcessEditing editedMask: NSTextStorage.EditActions,
range editedRange: NSRange,
changeInLength delta: Int
) {
self.textStorage = textStorage
highlightRegularExpression()
}
}
private extension RegExSyntaxHighlighter {
struct HighlightToken {
let range: Range<String.Index>
let attributes: [NSAttributedString.Key: Any]
let priority: Int
}
struct TokenStyle {
let color: UIColor
let priority: Int
}
enum HighlightColor {
static let quantifier = UIColor.systemGreen
static let quantifierRange = UIColor.systemPurple
static let structural = UIColor.systemPink
static let characterClass = UIColor.systemTeal
static let escape = UIColor.systemOrange
static let comment = UIColor.systemGray
static let directive = UIColor.systemPurple
static let error = UIColor.systemRed
}
enum HighlightStyle {
static let comment = TokenStyle(color: HighlightColor.comment, priority: 10)
static let structural = TokenStyle(color: HighlightColor.structural, priority: 20)
static let delimiter = TokenStyle(color: HighlightColor.structural, priority: 30)
static let characterClass = TokenStyle(color: HighlightColor.characterClass, priority: 30)
static let quantifier = TokenStyle(color: HighlightColor.quantifier, priority: 40)
static let quantifierRange = TokenStyle(color: HighlightColor.quantifierRange, priority: 40)
static let escape = TokenStyle(color: HighlightColor.escape, priority: 40)
static let directive = TokenStyle(color: HighlightColor.directive, priority: 40)
}
func tokenSort(lhs: HighlightToken, rhs: HighlightToken) -> Bool {
if lhs.priority != rhs.priority {
return lhs.priority < rhs.priority
}
if lhs.range.lowerBound != rhs.range.lowerBound {
return lhs.range.lowerBound < rhs.range.lowerBound
}
return lhs.range.upperBound < rhs.range.upperBound
}
func highlightSignature(for text: String) -> Int {
var hasher = Hasher()
hasher.combine(text)
hasher.combine(highlightingMode.signature)
return hasher.finalize()
}
func semanticTokens(for source: String) -> [HighlightToken] {
guard !source.isEmpty else {
return []
}
switch highlightingMode {
case .plainText:
return []
case .regularExpression(let options):
guard !options.contains(.ignoreMetacharacters) else {
return []
}
let ast = parseWithRecovery(source, syntaxOptions(for: options))
var tokens = [HighlightToken]()
if let globalOptions = ast.globalOptions {
globalOptions.options.forEach { option in
addColorToken(for: option.location, in: source, style: HighlightStyle.directive, to: &tokens)
}
}
collectTokens(from: ast.root, in: source, into: &tokens)
ast.diags.diags.forEach { diagnostic in
addUnderlineToken(for: diagnostic.location, in: source, color: HighlightColor.error, priority: 100, to: &tokens)
}
return tokens
}
}
func syntaxOptions(for options: NSRegularExpression.Options) -> SyntaxOptions {
var syntax: SyntaxOptions = .traditional
if options.contains(.allowCommentsAndWhitespace) {
syntax.formUnion(.extendedSyntax)
}
return syntax
}
// swiftlint:disable:next cyclomatic_complexity
func collectTokens(from node: AST.Node, in source: String, into tokens: inout [HighlightToken]) {
switch node {
case .alternation(let alternation):
collectTokens(from: alternation, in: source, into: &tokens)
case .concatenation(let concatenation):
concatenation.children.forEach { child in
collectTokens(from: child, in: source, into: &tokens)
}
case .group(let group):
collectTokens(from: group, in: source, into: &tokens)
case .conditional(let conditional):
collectTokens(from: conditional, in: source, into: &tokens)
case .quantification(let quantification):
collectTokens(from: quantification, in: source, into: &tokens)
case .quote(let quote):
addColorToken(for: quote.location, in: source, style: HighlightStyle.escape, to: &tokens)
case .trivia(let trivia):
addColorToken(for: trivia.location, in: source, style: HighlightStyle.comment, to: &tokens)
case .interpolation(let interpolation):
addColorToken(for: interpolation.location, in: source, style: HighlightStyle.directive, to: &tokens)
case .atom(let atom):
collectTokens(from: atom, in: source, into: &tokens)
case .customCharacterClass(let characterClass):
collectTokens(from: characterClass, in: source, into: &tokens)
case .absentFunction(let absentFunction):
addColorToken(for: absentFunction.start, in: source, style: HighlightStyle.delimiter, to: &tokens)
collectTokens(from: absentFunction, in: source, into: &tokens)
case .empty:
break
}
}
func collectTokens(from alternation: AST.Alternation, in source: String, into tokens: inout [HighlightToken]) {
alternation.pipes.forEach { pipe in
addColorToken(for: pipe, in: source, style: HighlightStyle.structural, to: &tokens)
}
alternation.children.forEach { child in
collectTokens(from: child, in: source, into: &tokens)
}
}
func collectTokens(from group: AST.Group, in source: String, into tokens: inout [HighlightToken]) {
addDelimitedToken(parent: group.location, child: group.child.location, in: source, style: HighlightStyle.delimiter, to: &tokens)
collectTokens(from: group.child, in: source, into: &tokens)
}
func collectTokens(from conditional: AST.Conditional, in source: String, into tokens: inout [HighlightToken]) {
addColorToken(for: conditional.location.start ..< conditional.condition.location.end, in: source, style: HighlightStyle.delimiter, to: &tokens)
if let pipe = conditional.pipe {
addColorToken(for: pipe, in: source, style: HighlightStyle.structural, to: &tokens)
}
addColorToken(for: conditional.falseBranch.location.end ..< conditional.location.end, in: source, style: HighlightStyle.delimiter, to: &tokens)
collectTokens(from: conditional.trueBranch, in: source, into: &tokens)
collectTokens(from: conditional.falseBranch, in: source, into: &tokens)
}
func collectTokens(from quantification: AST.Quantification, in source: String, into tokens: inout [HighlightToken]) {
collectTokens(from: quantification.child, in: source, into: &tokens)
addColorToken(for: quantification.amount.location, in: source, style: style(for: quantification.amount.value), to: &tokens)
addColorToken(for: quantification.kind.location, in: source, style: HighlightStyle.quantifier, to: &tokens)
}
func collectTokens(from atom: AST.Atom, in source: String, into tokens: inout [HighlightToken]) {
switch atom.kind {
case .dot:
addColorToken(for: atom.location, in: source, style: HighlightStyle.quantifier, to: &tokens)
case .caretAnchor, .dollarAnchor:
addColorToken(for: atom.location, in: source, style: HighlightStyle.characterClass, to: &tokens)
case .char:
if let range = sourceRange(for: atom.location, in: source), source[range].hasPrefix("\\") {
tokens.append(
HighlightToken(
range: range,
attributes: [.foregroundColor: HighlightStyle.escape.color],
priority: HighlightStyle.escape.priority
)
)
}
case .scalar, .scalarSequence, .property, .escaped,
.keyboardControl, .keyboardMeta, .keyboardMetaControl,
.namedCharacter, .backreference, .subpattern:
addColorToken(for: atom.location, in: source, style: HighlightStyle.escape, to: &tokens)
case .callout, .backtrackingDirective, .changeMatchingOptions:
addColorToken(for: atom.location, in: source, style: HighlightStyle.directive, to: &tokens)
case .invalid:
break
}
}
func collectTokens(from characterClass: AST.CustomCharacterClass, in source: String, into tokens: inout [HighlightToken]) {
addColorToken(for: characterClass.start.location, in: source, style: HighlightStyle.characterClass, to: &tokens)
if let closingRange = sourceRange(for: lastChildEnd(in: characterClass.members, defaultingTo: characterClass.start.location.end) ..< characterClass.location.end, in: source) {
tokens.append(
HighlightToken(
range: closingRange,
attributes: [.foregroundColor: HighlightStyle.characterClass.color],
priority: HighlightStyle.characterClass.priority
)
)
}
characterClass.members.forEach { member in
collectTokens(from: member, in: source, into: &tokens)
}
}
func collectTokens(from absentFunction: AST.AbsentFunction, in source: String, into tokens: inout [HighlightToken]) {
let closingStart: String.Index
switch absentFunction.kind {
case .repeater(let node), .stopper(let node):
collectTokens(from: node, in: source, into: &tokens)
closingStart = node.location.end
case .expression(let absentee, let pipe, let expression):
collectTokens(from: absentee, in: source, into: &tokens)
addColorToken(for: pipe, in: source, style: HighlightStyle.structural, to: &tokens)
collectTokens(from: expression, in: source, into: &tokens)
closingStart = expression.location.end
case .clearer:
closingStart = absentFunction.start.end
}
addColorToken(for: closingStart ..< absentFunction.location.end, in: source, style: HighlightStyle.delimiter, to: &tokens)
}
func collectTokens(from member: AST.CustomCharacterClass.Member, in source: String, into tokens: inout [HighlightToken]) {
switch member {
case .custom(let characterClass):
collectTokens(from: characterClass, in: source, into: &tokens)
case .range(let range):
collectTokens(from: range.lhs, in: source, into: &tokens)
collectTokens(from: range.rhs, in: source, into: &tokens)
addColorToken(for: range.dashLoc, in: source, style: HighlightStyle.characterClass, to: &tokens)
range.trivia.forEach { trivia in
addColorToken(for: trivia.location, in: source, style: HighlightStyle.comment, to: &tokens)
}
case .atom(let atom):
collectTokens(from: atom, in: source, into: &tokens)
case .quote(let quote):
addColorToken(for: quote.location, in: source, style: HighlightStyle.escape, to: &tokens)
case .trivia(let trivia):
addColorToken(for: trivia.location, in: source, style: HighlightStyle.comment, to: &tokens)
case .setOperation(let lhs, let operation, let rhs):
lhs.forEach { collectTokens(from: $0, in: source, into: &tokens) }
addColorToken(for: operation.location, in: source, style: HighlightStyle.characterClass, to: &tokens)
rhs.forEach { collectTokens(from: $0, in: source, into: &tokens) }
}
}
func addDelimitedToken(
parent: SourceLocation,
child: SourceLocation,
in source: String,
style: TokenStyle,
to tokens: inout [HighlightToken]
) {
addColorToken(for: parent.start ..< child.start, in: source, style: style, to: &tokens)
addColorToken(for: child.end ..< parent.end, in: source, style: style, to: &tokens)
}
func addColorToken(
for location: SourceLocation,
in source: String,
style: TokenStyle,
to tokens: inout [HighlightToken]
) {
addColorToken(for: location.range, in: source, style: style, to: &tokens)
}
func addColorToken(
for range: Range<String.Index>,
in source: String,
style: TokenStyle,
to tokens: inout [HighlightToken]
) {
guard let range = sourceRange(for: range, in: source) else {
return
}
tokens.append(
HighlightToken(
range: range,
attributes: [.foregroundColor: style.color],
priority: style.priority
)
)
}
func addUnderlineToken(
for location: SourceLocation,
in source: String,
color: UIColor,
prio
gitextract__mufejjy/ ├── .github/ │ └── workflow/ │ └── claude.yml ├── .gitignore ├── .swift-version ├── .swiftlint.yml ├── AGENTS.md ├── CLAUDE.md ├── LICENSE ├── README.md ├── RegEx+/ │ ├── About/ │ │ └── AboutView.swift │ ├── AppDelegate.swift │ ├── Assets.xcassets/ │ │ ├── AccentColor.colorset/ │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── AppIconForAboutView.imageset/ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj/ │ │ └── LaunchScreen.storyboard │ ├── CheatSheet/ │ │ └── CheatSheetView.swift │ ├── CoreData+CloudKit/ │ │ ├── DataManager.swift │ │ ├── RegEx.swift │ │ └── RegExFetch.swift │ ├── Editor/ │ │ ├── EditorView.swift │ │ ├── EditorViewModel.swift │ │ └── RegExFlowView.swift │ ├── HomeView.swift │ ├── Info.plist │ ├── Library/ │ │ ├── LibraryItemView.swift │ │ ├── LibraryView+Data.swift │ │ └── LibraryView.swift │ ├── Localizable.xcstrings │ ├── Preview Content/ │ │ └── Preview Assets.xcassets/ │ │ └── Contents.json │ ├── RegEx+.entitlements │ ├── RegEx.xcdatamodeld/ │ │ ├── .xccurrentversion │ │ └── RegEx.xcdatamodel/ │ │ └── contents │ ├── SceneDelegate.swift │ ├── Views/ │ │ ├── ActivityViewController.swift │ │ ├── RegExSyntaxView.swift │ │ ├── RegExTextView/ │ │ │ ├── MatchesTextView.swift │ │ │ ├── RegExSyntaxHighlighter.swift │ │ │ ├── RegExTextView.swift │ │ │ ├── ShortcutKeys.swift │ │ │ └── String+NSRange.swift │ │ ├── SafariView.swift │ │ └── SearchView.swift │ ├── de.lproj/ │ │ └── CheatSheet.plist │ ├── en.lproj/ │ │ └── CheatSheet.plist │ ├── es.lproj/ │ │ └── CheatSheet.plist │ ├── fr.lproj/ │ │ └── CheatSheet.plist │ ├── it.lproj/ │ │ └── CheatSheet.plist │ ├── ja.lproj/ │ │ └── CheatSheet.plist │ ├── ko.lproj/ │ │ └── CheatSheet.plist │ ├── nl.lproj/ │ │ └── CheatSheet.plist │ ├── pl.lproj/ │ │ └── CheatSheet.plist │ ├── zh-Hans.lproj/ │ │ └── CheatSheet.plist │ └── zh-Hant.lproj/ │ └── CheatSheet.plist ├── RegEx+.xcodeproj/ │ └── project.pbxproj ├── fastlane/ │ ├── Deliverfile │ ├── Fastfile │ └── metadata/ │ ├── copyright.txt │ ├── de-DE/ │ │ ├── apple_tv_privacy_policy.txt │ │ ├── description.txt │ │ ├── keywords.txt │ │ ├── marketing_url.txt │ │ ├── name.txt │ │ ├── privacy_url.txt │ │ ├── promotional_text.txt │ │ ├── release_notes.txt │ │ ├── subtitle.txt │ │ └── support_url.txt │ ├── en-US/ │ │ ├── apple_tv_privacy_policy.txt │ │ ├── description.txt │ │ ├── keywords.txt │ │ ├── marketing_url.txt │ │ ├── name.txt │ │ ├── privacy_url.txt │ │ ├── promotional_text.txt │ │ ├── release_notes.txt │ │ ├── subtitle.txt │ │ └── support_url.txt │ ├── es-ES/ │ │ ├── apple_tv_privacy_policy.txt │ │ ├── description.txt │ │ ├── keywords.txt │ │ ├── marketing_url.txt │ │ ├── name.txt │ │ ├── privacy_url.txt │ │ ├── promotional_text.txt │ │ ├── release_notes.txt │ │ ├── subtitle.txt │ │ └── support_url.txt │ ├── fr-FR/ │ │ ├── apple_tv_privacy_policy.txt │ │ ├── description.txt │ │ ├── keywords.txt │ │ ├── marketing_url.txt │ │ ├── name.txt │ │ ├── privacy_url.txt │ │ ├── promotional_text.txt │ │ ├── release_notes.txt │ │ ├── subtitle.txt │ │ └── support_url.txt │ ├── it/ │ │ ├── apple_tv_privacy_policy.txt │ │ ├── description.txt │ │ ├── keywords.txt │ │ ├── marketing_url.txt │ │ ├── name.txt │ │ ├── privacy_url.txt │ │ ├── promotional_text.txt │ │ ├── release_notes.txt │ │ ├── subtitle.txt │ │ └── support_url.txt │ ├── ja/ │ │ ├── apple_tv_privacy_policy.txt │ │ ├── description.txt │ │ ├── keywords.txt │ │ ├── marketing_url.txt │ │ ├── name.txt │ │ ├── privacy_url.txt │ │ ├── promotional_text.txt │ │ ├── release_notes.txt │ │ ├── subtitle.txt │ │ └── support_url.txt │ ├── nl-NL/ │ │ ├── apple_tv_privacy_policy.txt │ │ ├── description.txt │ │ ├── keywords.txt │ │ ├── marketing_url.txt │ │ ├── name.txt │ │ ├── privacy_url.txt │ │ ├── promotional_text.txt │ │ ├── release_notes.txt │ │ ├── subtitle.txt │ │ └── support_url.txt │ ├── pl/ │ │ ├── apple_tv_privacy_policy.txt │ │ ├── description.txt │ │ ├── keywords.txt │ │ ├── marketing_url.txt │ │ ├── name.txt │ │ ├── privacy_url.txt │ │ ├── promotional_text.txt │ │ ├── release_notes.txt │ │ ├── subtitle.txt │ │ └── support_url.txt │ ├── primary_category.txt │ ├── primary_first_sub_category.txt │ ├── primary_second_sub_category.txt │ ├── secondary_category.txt │ ├── secondary_first_sub_category.txt │ ├── secondary_second_sub_category.txt │ ├── zh-Hans/ │ │ ├── apple_tv_privacy_policy.txt │ │ ├── description.txt │ │ ├── keywords.txt │ │ ├── marketing_url.txt │ │ ├── name.txt │ │ ├── privacy_url.txt │ │ ├── promotional_text.txt │ │ ├── release_notes.txt │ │ ├── subtitle.txt │ │ └── support_url.txt │ └── zh-Hant/ │ ├── apple_tv_privacy_policy.txt │ ├── description.txt │ ├── keywords.txt │ ├── marketing_url.txt │ ├── name.txt │ ├── privacy_url.txt │ ├── promotional_text.txt │ ├── release_notes.txt │ ├── subtitle.txt │ └── support_url.txt └── mise.toml
Condensed preview — 164 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (451K chars).
[
{
"path": ".github/workflow/claude.yml",
"chars": 2320,
"preview": "name: Claude Code\n\non:\n issue_comment:\n types: [created]\n pull_request_review_comment:\n types: [created]\n issue"
},
{
"path": ".gitignore",
"chars": 1945,
"preview": "\n# Created by https://www.gitignore.io/api/xcode,macos,fastlane\n# Edit at https://www.gitignore.io/?templates=xcode,maco"
},
{
"path": ".swift-version",
"chars": 4,
"preview": "5.9\n"
},
{
"path": ".swiftlint.yml",
"chars": 352,
"preview": "disabled_rules:\n - todo\n - trailing_whitespace\n - colon\n - identifier_name\n - comma\n - vertical_whitespace\n - typ"
},
{
"path": "AGENTS.md",
"chars": 2686,
"preview": "# Repository Guidelines\n\n## Project Structure & Module Organization\n`RegEx+/` hosts the SwiftUI app: `HomeView.swift` dr"
},
{
"path": "CLAUDE.md",
"chars": 3042,
"preview": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## "
},
{
"path": "LICENSE",
"chars": 1088,
"preview": "The MIT License (MIT)\nCopyright © 2020 Lex Tang, https://lex.sh\n\nPermission is hereby granted, free of charge, to any pe"
},
{
"path": "README.md",
"chars": 1308,
"preview": "# RegEx+\n\n[](https://swift.org/download/)\n[![@"
},
{
"path": "RegEx+/About/AboutView.swift",
"chars": 779,
"preview": "//\n// AboutView.swift\n// RegEx+\n//\n// Created by Lex on 2020/5/24.\n// Copyright © 2020 Lex.sh. All rights reserved.\n"
},
{
"path": "RegEx+/AppDelegate.swift",
"chars": 1708,
"preview": "//\n// AppDelegate.swift\n// RegEx+\n//\n// Created by Lex on 2020/4/21.\n// Copyright © 2020 Lex.sh. All rights reserved"
},
{
"path": "RegEx+/Assets.xcassets/AccentColor.colorset/Contents.json",
"chars": 695,
"preview": "{\n \"colors\" : [\n {\n \"color\" : {\n \"color-space\" : \"srgb\",\n \"components\" : {\n \"alpha\" : \"1"
},
{
"path": "RegEx+/Assets.xcassets/AppIcon.appiconset/Contents.json",
"chars": 3284,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"icon_40pt.png\",\n \"idiom\" : \"iphone\",\n \"scale\" : \"2x\",\n \"size\" "
},
{
"path": "RegEx+/Assets.xcassets/AppIconForAboutView.imageset/Contents.json",
"chars": 317,
"preview": "{\n \"images\" : [\n {\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\"\n },\n {\n \"filename\" : \"AppIconForAbou"
},
{
"path": "RegEx+/Assets.xcassets/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "RegEx+/Base.lproj/LaunchScreen.storyboard",
"chars": 4721,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3"
},
{
"path": "RegEx+/CheatSheet/CheatSheetView.swift",
"chars": 3159,
"preview": "//\n// CheatSheetView.swift\n// RegEx+\n//\n// Created by Lex on 2020/4/21.\n// Copyright © 2020 Lex.sh. All rights reser"
},
{
"path": "RegEx+/CoreData+CloudKit/DataManager.swift",
"chars": 3410,
"preview": "//\n// DataManager.swift\n// RegEx+\n//\n// Created by Lex on 2020/5/3.\n// Copyright © 2020 Lex.sh. All rights reserved."
},
{
"path": "RegEx+/CoreData+CloudKit/RegEx.swift",
"chars": 3052,
"preview": "//\n// RegEx+CoreDataClass.swift\n// RegEx+\n//\n// Created by Lex on 2020/5/3.\n// Copyright © 2020 Lex.sh. All rights r"
},
{
"path": "RegEx+/CoreData+CloudKit/RegExFetch.swift",
"chars": 884,
"preview": "//\n// RegExFetch.swift\n// RegEx+\n//\n// Created by Lex on 2020/5/3.\n// Copyright © 2020 Lex.sh. All rights reserved.\n"
},
{
"path": "RegEx+/Editor/EditorView.swift",
"chars": 12246,
"preview": "//\n// EditorView.swift\n// RegEx+\n//\n// Created by Lex on 2020/4/21.\n// Copyright © 2020 Lex.sh. All rights reserved."
},
{
"path": "RegEx+/Editor/EditorViewModel.swift",
"chars": 3171,
"preview": "//\n// RegExEditorViewModel.swift\n// RegEx+\n//\n// Created by Lex on 2020/4/25.\n// Copyright © 2020 Lex.sh. All rights"
},
{
"path": "RegEx+/Editor/RegExFlowView.swift",
"chars": 45981,
"preview": "//\n// RegExFlowView.swift\n// RegEx+\n//\n// Created by Lex on 2026/4/11.\n// Copyright © 2020 Lex.sh. All rights reserv"
},
{
"path": "RegEx+/HomeView.swift",
"chars": 1367,
"preview": "//\n// TabView.swift\n// RegEx+\n//\n// Created by Lex on 2020/4/21.\n// Copyright © 2020 Lex.sh. All rights reserved.\n//"
},
{
"path": "RegEx+/Info.plist",
"chars": 2414,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "RegEx+/Library/LibraryItemView.swift",
"chars": 1989,
"preview": "//\n// LibraryItemView.swift\n// RegEx+\n//\n// Created by Lex on 2020/5/3.\n// Copyright © 2020 Lex.sh. All rights reser"
},
{
"path": "RegEx+/Library/LibraryView+Data.swift",
"chars": 2124,
"preview": "//\n// LibraryView+Data.swift\n// RegEx+\n//\n// Created by Lex on 2020/5/3.\n// Copyright © 2020 Lex.sh. All rights rese"
},
{
"path": "RegEx+/Library/LibraryView.swift",
"chars": 4166,
"preview": "//\n// LibraryView.swift\n// RegEx+\n//\n// Created by Lex on 2020/4/21.\n// Copyright © 2020 Lex.sh. All rights reserved"
},
{
"path": "RegEx+/Localizable.xcstrings",
"chars": 51319,
"preview": "{\n \"sourceLanguage\" : \"en\",\n \"strings\" : {\n \"%@\" : {\n \"localizations\" : {\n \"de\" : {\n \"stringUn"
},
{
"path": "RegEx+/Preview Content/Preview Assets.xcassets/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "RegEx+/RegEx+.entitlements",
"chars": 568,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "RegEx+/RegEx.xcdatamodeld/.xccurrentversion",
"chars": 258,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "RegEx+/RegEx.xcdatamodeld/RegEx.xcdatamodel/contents",
"chars": 1985,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<model type=\"com.apple.IDECoreDataModeler.DataModel\" documentVer"
},
{
"path": "RegEx+/SceneDelegate.swift",
"chars": 3114,
"preview": "//\n// SceneDelegate.swift\n// RegEx+\n//\n// Created by Lex on 2020/4/21.\n// Copyright © 2020 Lex.sh. All rights reserv"
},
{
"path": "RegEx+/Views/ActivityViewController.swift",
"chars": 750,
"preview": "//\n// ActivityViewController.swift\n// RegEx+\n//\n// Created by Lex on 2020/5/16.\n// Copyright © 2020 Lex.sh. All righ"
},
{
"path": "RegEx+/Views/RegExSyntaxView.swift",
"chars": 7007,
"preview": "//\n// RegExSyntaxView.swift\n// RegExPro\n//\n// Created by Lex on 2020/4/23.\n// Copyright © 2020 Lex.sh. All rights re"
},
{
"path": "RegEx+/Views/RegExTextView/MatchesTextView.swift",
"chars": 6178,
"preview": "//\n// MatchesTextView.swift\n// RegEx+\n//\n// Created by Lex on 2020/5/2.\n// Copyright © 2020 Lex.sh. All rights reser"
},
{
"path": "RegEx+/Views/RegExTextView/RegExSyntaxHighlighter.swift",
"chars": 16746,
"preview": "//\n// RegExSyntaxHighlighter.swift\n// RegEx+\n//\n// Created by Lex on 2020/5/2.\n// Copyright © 2020 Lex.sh. All right"
},
{
"path": "RegEx+/Views/RegExTextView/RegExTextView.swift",
"chars": 7254,
"preview": "//\n// RegExSyntaxView.swift\n// RegEx+\n//\n// Created by Lex on 2020/4/23.\n// Copyright © 2020 Lex.sh. All rights rese"
},
{
"path": "RegEx+/Views/RegExTextView/ShortcutKeys.swift",
"chars": 10278,
"preview": "//\n// ShortcutKeys.swift\n// RegEx+\n//\n// Created by Lex on 8/10/25.\n// Copyright © 2025 Lex.sh. All rights reserved."
},
{
"path": "RegEx+/Views/RegExTextView/String+NSRange.swift",
"chars": 934,
"preview": "//\n// String+NSRange.swift\n// RegEx+\n//\n// Created by Lex on 2020/5/2.\n// Copyright © 2020 Lex.sh. All rights reserv"
},
{
"path": "RegEx+/Views/SafariView.swift",
"chars": 786,
"preview": "//\n// SafariView.swift\n// RegEx+\n//\n// Created by Lex on 2020/5/5.\n// Copyright © 2020 Lex.sh. All rights reserved.\n"
},
{
"path": "RegEx+/Views/SearchView.swift",
"chars": 1767,
"preview": "//\n// SearchView.swift\n// RegEx+\n//\n// Created by Lex on 2020/10/4.\n// Copyright © 2020 Lex.sh. All rights reserved."
},
{
"path": "RegEx+/de.lproj/CheatSheet.plist",
"chars": 12856,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "RegEx+/en.lproj/CheatSheet.plist",
"chars": 12240,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "RegEx+/es.lproj/CheatSheet.plist",
"chars": 13233,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "RegEx+/fr.lproj/CheatSheet.plist",
"chars": 13209,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "RegEx+/it.lproj/CheatSheet.plist",
"chars": 13254,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "RegEx+/ja.lproj/CheatSheet.plist",
"chars": 9566,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "RegEx+/ko.lproj/CheatSheet.plist",
"chars": 9766,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "RegEx+/nl.lproj/CheatSheet.plist",
"chars": 13006,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "RegEx+/pl.lproj/CheatSheet.plist",
"chars": 12747,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "RegEx+/zh-Hans.lproj/CheatSheet.plist",
"chars": 8980,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "RegEx+/zh-Hant.lproj/CheatSheet.plist",
"chars": 8980,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "RegEx+.xcodeproj/project.pbxproj",
"chars": 32487,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "fastlane/Deliverfile",
"chars": 163,
"preview": "# The Deliverfile allows you to store various App Store Connect metadata\n# For more information, check out the docs\n# ht"
},
{
"path": "fastlane/Fastfile",
"chars": 656,
"preview": "# This file contains the fastlane.tools configuration\n# You can find the documentation at https://docs.fastlane.tools\n#\n"
},
{
"path": "fastlane/metadata/copyright.txt",
"chars": 12,
"preview": "2025 Lex.sh\n"
},
{
"path": "fastlane/metadata/de-DE/apple_tv_privacy_policy.txt",
"chars": 1,
"preview": "\n"
},
{
"path": "fastlane/metadata/de-DE/description.txt",
"chars": 690,
"preview": "Entfesseln Sie die Macht der regulären Ausdrücke mit RegEx+!\n\nVon Entwicklern für Entwickler konzipiert, ist RegEx+ Ihr "
},
{
"path": "fastlane/metadata/de-DE/keywords.txt",
"chars": 99,
"preview": "regexp,reguläre,ausdrücke,entwickler,programmieren,code,editor,pattern,syntax,ios,mac,software,tool"
},
{
"path": "fastlane/metadata/de-DE/marketing_url.txt",
"chars": 1,
"preview": "\n"
},
{
"path": "fastlane/metadata/de-DE/name.txt",
"chars": 7,
"preview": "RegEx+\n"
},
{
"path": "fastlane/metadata/de-DE/privacy_url.txt",
"chars": 40,
"preview": "https://lex.sh/regexplus/privacypolicy/\n"
},
{
"path": "fastlane/metadata/de-DE/promotional_text.txt",
"chars": 162,
"preview": "Teste, debugge und speichere Regex-Muster schneller auf iPhone, iPad und Mac. Live-Matching, Spickzettel und CloudKit-Sy"
},
{
"path": "fastlane/metadata/de-DE/release_notes.txt",
"chars": 173,
"preview": "Neues in RegEx+:\n\n• Unterstützung für Französisch, Italienisch, Koreanisch, Niederländisch und Polnisch hinzugefügt.\n• L"
},
{
"path": "fastlane/metadata/de-DE/subtitle.txt",
"chars": 30,
"preview": "Regex Spickzettel & Cloud-Sync"
},
{
"path": "fastlane/metadata/de-DE/support_url.txt",
"chars": 21,
"preview": "https://x.com/lexrus\n"
},
{
"path": "fastlane/metadata/en-US/apple_tv_privacy_policy.txt",
"chars": 1,
"preview": "\n"
},
{
"path": "fastlane/metadata/en-US/description.txt",
"chars": 612,
"preview": "Unleash the Power of Regular Expressions with RegEx+!\n\nDesigned for developers by developers, RegEx+ is your go-to tool "
},
{
"path": "fastlane/metadata/en-US/keywords.txt",
"chars": 101,
"preview": "regexp,tester,builder,match,replace,capture,group,validator,pattern,swift,nsregularexpression,editor\n"
},
{
"path": "fastlane/metadata/en-US/marketing_url.txt",
"chars": 1,
"preview": "\n"
},
{
"path": "fastlane/metadata/en-US/name.txt",
"chars": 7,
"preview": "RegEx+\n"
},
{
"path": "fastlane/metadata/en-US/privacy_url.txt",
"chars": 40,
"preview": "https://lex.sh/regexplus/privacypolicy/\n"
},
{
"path": "fastlane/metadata/en-US/promotional_text.txt",
"chars": 137,
"preview": "Test, debug, and save regex patterns faster on iPhone, iPad, and Mac. Live matching, cheat sheet, and CloudKit sync in o"
},
{
"path": "fastlane/metadata/en-US/release_notes.txt",
"chars": 147,
"preview": "What's New in RegEx+:\n\n• Added French, Italian, Korean, Dutch and Polish language support.\n• Performance optimizations a"
},
{
"path": "fastlane/metadata/en-US/subtitle.txt",
"chars": 31,
"preview": "Regex Cheat Sheet + Cloud Sync\n"
},
{
"path": "fastlane/metadata/en-US/support_url.txt",
"chars": 21,
"preview": "https://x.com/lexrus\n"
},
{
"path": "fastlane/metadata/es-ES/apple_tv_privacy_policy.txt",
"chars": 1,
"preview": "\n"
},
{
"path": "fastlane/metadata/es-ES/description.txt",
"chars": 701,
"preview": "¡Libera el Poder de las Expresiones Regulares con RegEx+!\n\nDiseñado por desarrolladores para desarrolladores, RegEx+ es "
},
{
"path": "fastlane/metadata/es-ES/keywords.txt",
"chars": 99,
"preview": "regexp,expresiones,regulares,programador,código,editor,patrón,sintaxis,ios,mac,software,herramienta"
},
{
"path": "fastlane/metadata/es-ES/marketing_url.txt",
"chars": 1,
"preview": "\n"
},
{
"path": "fastlane/metadata/es-ES/name.txt",
"chars": 7,
"preview": "RegEx+\n"
},
{
"path": "fastlane/metadata/es-ES/privacy_url.txt",
"chars": 40,
"preview": "https://lex.sh/regexplus/privacypolicy/\n"
},
{
"path": "fastlane/metadata/es-ES/promotional_text.txt",
"chars": 139,
"preview": "Pruebe, depure y guarde patrones regex más rápido en iPhone, iPad y Mac. Coincidencias en vivo, guía de consulta y sincr"
},
{
"path": "fastlane/metadata/es-ES/release_notes.txt",
"chars": 176,
"preview": "Novedades en RegEx+:\n\n• Se ha añadido compatibilidad con los idiomas francés, italiano, coreano, neerlandés y polaco.\n• "
},
{
"path": "fastlane/metadata/es-ES/subtitle.txt",
"chars": 27,
"preview": "Acordeón Regex y Cloud Sync"
},
{
"path": "fastlane/metadata/es-ES/support_url.txt",
"chars": 21,
"preview": "https://x.com/lexrus\n"
},
{
"path": "fastlane/metadata/fr-FR/apple_tv_privacy_policy.txt",
"chars": 0,
"preview": ""
},
{
"path": "fastlane/metadata/fr-FR/description.txt",
"chars": 751,
"preview": "Libérez la puissance des expressions régulières avec RegEx+ !\n\nConçu pour les développeurs par des développeurs, RegEx+ "
},
{
"path": "fastlane/metadata/fr-FR/keywords.txt",
"chars": 89,
"preview": "regexp,expression,régulière,développeur,code,éditeur,motif,syntaxe,ios,mac,logiciel,outil"
},
{
"path": "fastlane/metadata/fr-FR/marketing_url.txt",
"chars": 0,
"preview": ""
},
{
"path": "fastlane/metadata/fr-FR/name.txt",
"chars": 7,
"preview": "RegEx+\n"
},
{
"path": "fastlane/metadata/fr-FR/privacy_url.txt",
"chars": 21,
"preview": "https://x.com/lexrus\n"
},
{
"path": "fastlane/metadata/fr-FR/promotional_text.txt",
"chars": 127,
"preview": "Testez, déboguez et enregistrez vos regex plus vite sur iPhone, iPad et Mac. Matching en direct, antisèche et synchro Cl"
},
{
"path": "fastlane/metadata/fr-FR/release_notes.txt",
"chars": 196,
"preview": "Quoi de neuf dans RegEx+ :\n\n• Ajout de la prise en charge des langues française, italienne, coréenne, néerlandaise et po"
},
{
"path": "fastlane/metadata/fr-FR/subtitle.txt",
"chars": 29,
"preview": "Antisèche Regex et Cloud Sync"
},
{
"path": "fastlane/metadata/fr-FR/support_url.txt",
"chars": 21,
"preview": "https://x.com/lexrus\n"
},
{
"path": "fastlane/metadata/it/apple_tv_privacy_policy.txt",
"chars": 0,
"preview": ""
},
{
"path": "fastlane/metadata/it/description.txt",
"chars": 731,
"preview": "Scatena la potenza delle espressioni regolari con RegEx+!\n\nProgettato per sviluppatori da sviluppatori, RegEx+ è il tuo "
},
{
"path": "fastlane/metadata/it/keywords.txt",
"chars": 98,
"preview": "regexp,espressione,regolare,sviluppatore,codice,editor,pattern,sintassi,ios,mac,software,strumento"
},
{
"path": "fastlane/metadata/it/marketing_url.txt",
"chars": 0,
"preview": ""
},
{
"path": "fastlane/metadata/it/name.txt",
"chars": 7,
"preview": "RegEx+\n"
},
{
"path": "fastlane/metadata/it/privacy_url.txt",
"chars": 21,
"preview": "https://x.com/lexrus\n"
},
{
"path": "fastlane/metadata/it/promotional_text.txt",
"chars": 134,
"preview": "Testa, debugga e salva i pattern regex più velocemente su iPhone, iPad e Mac. Matching in tempo reale, guida rapida e si"
},
{
"path": "fastlane/metadata/it/release_notes.txt",
"chars": 173,
"preview": "Novità in RegEx+:\n\n• Aggiunto il supporto per le lingue francese, italiana, coreana, olandese e polacca.\n• Ottimizzazion"
},
{
"path": "fastlane/metadata/it/subtitle.txt",
"chars": 24,
"preview": "Guida Regex e Cloud Sync"
},
{
"path": "fastlane/metadata/it/support_url.txt",
"chars": 21,
"preview": "https://x.com/lexrus\n"
},
{
"path": "fastlane/metadata/ja/apple_tv_privacy_policy.txt",
"chars": 1,
"preview": "\n"
},
{
"path": "fastlane/metadata/ja/description.txt",
"chars": 263,
"preview": "RegEx+で正規表現の力を解き放とう!\n\n開発者による開発者のためのRegEx+は、macOSとiOSで正規表現をマスターするための必須ツールです。CloudKitを通じてデバイス間でデータが簡単に同期されるシームレスな体験に飛び込み、必"
},
{
"path": "fastlane/metadata/ja/keywords.txt",
"chars": 59,
"preview": "正規表現,開発者,プログラミング,コード,エディタ,パターン,構文,ソフトウェア,ツール,ios,mac,regexp"
},
{
"path": "fastlane/metadata/ja/marketing_url.txt",
"chars": 1,
"preview": "\n"
},
{
"path": "fastlane/metadata/ja/name.txt",
"chars": 7,
"preview": "RegEx+\n"
},
{
"path": "fastlane/metadata/ja/privacy_url.txt",
"chars": 40,
"preview": "https://lex.sh/regexplus/privacypolicy/\n"
},
{
"path": "fastlane/metadata/ja/promotional_text.txt",
"chars": 67,
"preview": "iPhone、iPad、Macで正規表現のテストと保存を高速化。ライブマッチング、リファレンス、CloudKit同期を一つのツールで。"
},
{
"path": "fastlane/metadata/ja/release_notes.txt",
"chars": 80,
"preview": "RegEx+ の新機能:\n\n• フランス語、イタリア語、韓国語、オランダ語、ポーランド語のサポートを追加しました。\n• パフォーマンスの最適化と安定性の向上。\n"
},
{
"path": "fastlane/metadata/ja/subtitle.txt",
"chars": 15,
"preview": "正規表現の早見表とクラウド同期"
},
{
"path": "fastlane/metadata/ja/support_url.txt",
"chars": 21,
"preview": "https://x.com/lexrus\n"
},
{
"path": "fastlane/metadata/nl-NL/apple_tv_privacy_policy.txt",
"chars": 0,
"preview": ""
},
{
"path": "fastlane/metadata/nl-NL/description.txt",
"chars": 673,
"preview": "Ontketen de kracht van Reguliere Expressies met RegEx+!\n\nRegEx+ is ontworpen door ontwikkelaars voor ontwikkelaars en is"
},
{
"path": "fastlane/metadata/nl-NL/keywords.txt",
"chars": 89,
"preview": "regexp,reguliere,expressies,ontwikkelaar,code,editor,patroon,syntax,ios,mac,software,tool"
},
{
"path": "fastlane/metadata/nl-NL/marketing_url.txt",
"chars": 0,
"preview": ""
},
{
"path": "fastlane/metadata/nl-NL/name.txt",
"chars": 6,
"preview": "RegEx+"
},
{
"path": "fastlane/metadata/nl-NL/privacy_url.txt",
"chars": 39,
"preview": "https://lex.sh/regexplus/privacypolicy/"
},
{
"path": "fastlane/metadata/nl-NL/promotional_text.txt",
"chars": 124,
"preview": "Test, debug en bewaar regex-patronen sneller op iPhone, iPad en Mac. Live matching, spiekbriefje en CloudKit-synchronisa"
},
{
"path": "fastlane/metadata/nl-NL/release_notes.txt",
"chars": 178,
"preview": "Wat is er nieuw in RegEx+:\n\n• Ondersteuning voor de Franse, Italiaanse, Koreaanse, Nederlandse en Poolse taal toegevoegd"
},
{
"path": "fastlane/metadata/nl-NL/subtitle.txt",
"chars": 29,
"preview": "Regex Spiekbrief & Cloud Sync"
},
{
"path": "fastlane/metadata/nl-NL/support_url.txt",
"chars": 20,
"preview": "https://x.com/lexrus"
},
{
"path": "fastlane/metadata/pl/apple_tv_privacy_policy.txt",
"chars": 0,
"preview": ""
},
{
"path": "fastlane/metadata/pl/description.txt",
"chars": 670,
"preview": "Uwolnij moc Wyrażeń Regularnych z RegEx+!\n\nZaprojektowany przez programistów dla programistów, RegEx+ to Twoje podstawow"
},
{
"path": "fastlane/metadata/pl/keywords.txt",
"chars": 96,
"preview": "regexp,wyrażenia,regularne,programista,kod,edytor,wzór,składnia,ios,mac,oprogramowanie,narzędzie"
},
{
"path": "fastlane/metadata/pl/marketing_url.txt",
"chars": 0,
"preview": ""
},
{
"path": "fastlane/metadata/pl/name.txt",
"chars": 6,
"preview": "RegEx+"
},
{
"path": "fastlane/metadata/pl/privacy_url.txt",
"chars": 39,
"preview": "https://lex.sh/regexplus/privacypolicy/"
},
{
"path": "fastlane/metadata/pl/promotional_text.txt",
"chars": 128,
"preview": "Testuj, debuguj i zapisuj wyrażenia regex szybciej na iPhone, iPad i Mac. Dopasowanie na żywo, ściąga i synchronizacja C"
},
{
"path": "fastlane/metadata/pl/release_notes.txt",
"chars": 162,
"preview": "Co nowego w RegEx+:\n\n• Dodano obsługę języka francuskiego, włoskiego, koreańskiego, holenderskiego i polskiego.\n• Optyma"
},
{
"path": "fastlane/metadata/pl/subtitle.txt",
"chars": 25,
"preview": "Ściąga Regex i Cloud Sync"
},
{
"path": "fastlane/metadata/pl/support_url.txt",
"chars": 20,
"preview": "https://x.com/lexrus"
},
{
"path": "fastlane/metadata/primary_category.txt",
"chars": 16,
"preview": "DEVELOPER_TOOLS\n"
},
{
"path": "fastlane/metadata/primary_first_sub_category.txt",
"chars": 1,
"preview": "\n"
},
{
"path": "fastlane/metadata/primary_second_sub_category.txt",
"chars": 1,
"preview": "\n"
},
{
"path": "fastlane/metadata/secondary_category.txt",
"chars": 10,
"preview": "UTILITIES\n"
},
{
"path": "fastlane/metadata/secondary_first_sub_category.txt",
"chars": 1,
"preview": "\n"
},
{
"path": "fastlane/metadata/secondary_second_sub_category.txt",
"chars": 1,
"preview": "\n"
},
{
"path": "fastlane/metadata/zh-Hans/apple_tv_privacy_policy.txt",
"chars": 1,
"preview": "\n"
},
{
"path": "fastlane/metadata/zh-Hans/description.txt",
"chars": 222,
"preview": "用 RegEx+ 释放正则表达式的力量!\n\n由开发者为开发者设计,RegEx+ 是您在 macOS 和 iOS 上掌握正则表达式的必备工具。沉浸在无缝体验中,您的数据通过 CloudKit 在设备间轻松同步,确保您在需要的时候、需要的地方都"
},
{
"path": "fastlane/metadata/zh-Hans/keywords.txt",
"chars": 48,
"preview": "正则表达式,正则,开发,编程,代码,编辑器,模式,语法,软件,工具,ios,mac,regexp"
},
{
"path": "fastlane/metadata/zh-Hans/marketing_url.txt",
"chars": 1,
"preview": "\n"
},
{
"path": "fastlane/metadata/zh-Hans/name.txt",
"chars": 7,
"preview": "RegEx+\n"
},
{
"path": "fastlane/metadata/zh-Hans/privacy_url.txt",
"chars": 40,
"preview": "https://lex.sh/regexplus/privacypolicy/\n"
},
{
"path": "fastlane/metadata/zh-Hans/promotional_text.txt",
"chars": 67,
"preview": "在 iPhone、iPad 和 Mac 上更快速地测试、调试和保存正则表达式。实时匹配、速查表和 CloudKit 同步,一站式搞定。"
},
{
"path": "fastlane/metadata/zh-Hans/release_notes.txt",
"chars": 53,
"preview": "RegEx+ 新功能:\n\n• 新增法语、意大利语、韩语、荷兰语和波兰语支持。\n• 性能优化和稳定性改进。\n"
},
{
"path": "fastlane/metadata/zh-Hans/subtitle.txt",
"chars": 12,
"preview": "正则表达式速查表与云同步"
},
{
"path": "fastlane/metadata/zh-Hans/support_url.txt",
"chars": 21,
"preview": "https://x.com/lexrus\n"
},
{
"path": "fastlane/metadata/zh-Hant/apple_tv_privacy_policy.txt",
"chars": 1,
"preview": "\n"
},
{
"path": "fastlane/metadata/zh-Hant/description.txt",
"chars": 224,
"preview": "用 RegEx+ 釋放正則表達式的力量!\n\n由開發者為開發者設計,RegEx+ 是您在 macOS 和 iOS 上掌握正則表達式的必備工具。沉浸在無縫體驗中,您的資料透過 CloudKit 在裝置間輕鬆同步,確保您在需要的時候、需要的地方都"
},
{
"path": "fastlane/metadata/zh-Hant/keywords.txt",
"chars": 48,
"preview": "正則表達式,正則,開發,編程,代碼,編輯器,模式,語法,軟件,工具,ios,mac,regexp"
},
{
"path": "fastlane/metadata/zh-Hant/marketing_url.txt",
"chars": 1,
"preview": "\n"
},
{
"path": "fastlane/metadata/zh-Hant/name.txt",
"chars": 7,
"preview": "RegEx+\n"
},
{
"path": "fastlane/metadata/zh-Hant/privacy_url.txt",
"chars": 40,
"preview": "https://lex.sh/regexplus/privacypolicy/\n"
},
{
"path": "fastlane/metadata/zh-Hant/promotional_text.txt",
"chars": 67,
"preview": "在 iPhone、iPad 和 Mac 上更快速地測試、調試和保存正則表達式。即時匹配、速查表和 CloudKit 同步,一站式搞定。"
},
{
"path": "fastlane/metadata/zh-Hant/release_notes.txt",
"chars": 53,
"preview": "RegEx+ 新功能:\n\n• 新增法語、義大利語、韓語、荷蘭語和波蘭語支援。\n• 效能優化和穩定性改進。\n"
},
{
"path": "fastlane/metadata/zh-Hant/subtitle.txt",
"chars": 12,
"preview": "正則表達式速查表與雲同步"
},
{
"path": "fastlane/metadata/zh-Hant/support_url.txt",
"chars": 21,
"preview": "https://x.com/lexrus\n"
},
{
"path": "mise.toml",
"chars": 857,
"preview": "[tasks.sc2tc]\ndescription = \"Convert Simplified Chinese to Traditional Chinese using OpenCC\"\nrun = \"opencc -c s2hk -i Re"
}
]
About this extraction
This page contains the full source code of the lexrus/RegExPlus GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 164 files (380.0 KB), approximately 119.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.