Full Code of lexrus/RegExPlus for AI

master f10137f28dff cached
164 files
380.0 KB
119.5k tokens
1 requests
Download .txt
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+

[![Swift 5.9](https://img.shields.io/badge/swift-5.9-ED523F.svg?style=flat)](https://swift.org/download/)
[![@lexrus](https://img.shields.io/badge/contact-@lexrus-336699.svg?style=flat)](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.

![HeroImage](https://github.com/lexrus/RegExPlus/assets/219689/739896bf-c843-46b9-82f9-e1462562fe4b)

## 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
Download .txt
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[![Swift 5.9](https://img.shields.io/badge/swift-5.9-ED523F.svg?style=flat)](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.

Copied to clipboard!