├── .github
└── workflows
│ └── ios.yml
├── .gitignore
├── .swift-version
├── .swiftlint.yml
├── LICENSE
├── README.md
├── Rakefile
├── RegEx+.xcodeproj
├── project.pbxproj
└── xcshareddata
│ └── xcschemes
│ └── RegEx+.xcscheme
└── RegEx+
├── About
└── AboutView.swift
├── AppDelegate.swift
├── Assets.xcassets
├── AccentColor.colorset
│ └── Contents.json
├── AppIcon.appiconset
│ ├── Contents.json
│ ├── Icon.png
│ ├── icon_20pt.png
│ ├── icon_20pt@3x.png
│ ├── icon_29pt.png
│ ├── icon_29pt@2x.png
│ ├── icon_29pt@3x.png
│ ├── icon_40pt.png
│ ├── icon_40pt@2x.png
│ ├── icon_60pt@2x.png
│ ├── icon_60pt@3x.png
│ ├── icon_76pt.png
│ ├── icon_76pt@2x.png
│ ├── icon_83.5@2x.png
│ ├── mac.png
│ ├── mac128.png
│ ├── mac256.png
│ └── mac512.png
└── Contents.json
├── Base.lproj
└── LaunchScreen.storyboard
├── CheatSheet
└── CheatSheetView.swift
├── CoreData+CloudKit
├── DataManager.swift
├── RegEx.swift
└── RegExFetch.swift
├── Editor
├── EditorView.swift
└── EditorViewModel.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
│ └── String+NSRange.swift
├── SafariView.swift
└── SearchView.swift
├── en.lproj
└── CheatSheet.plist
├── zh-Hans.lproj
└── CheatSheet.plist
└── zh-Hant.lproj
└── CheatSheet.plist
/.github/workflows/ios.yml:
--------------------------------------------------------------------------------
1 | name: iOS starter workflow
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "master" ]
8 |
9 | jobs:
10 | build:
11 | name: Build and Test default scheme using any available iPhone simulator
12 | runs-on: macos-latest
13 |
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v3
17 | - name: Set Default Scheme
18 | run: |
19 | scheme_list=$(xcodebuild -list -json | tr -d "\n")
20 | default=$(echo $scheme_list | ruby -e "require 'json'; puts JSON.parse(STDIN.gets)['project']['targets'][0]")
21 | echo $default | cat >default
22 | echo Using default scheme: $default
23 | - name: Build
24 | env:
25 | scheme: ${{ 'default' }}
26 | platform: ${{ 'iOS Simulator' }}
27 | run: |
28 | # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959)
29 | device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"`
30 | if [ $scheme = default ]; then scheme=$(cat default); fi
31 | if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi
32 | file_to_build=`echo $file_to_build | awk '{$1=$1;print}'`
33 | xcodebuild build-for-testing -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device"
34 | - name: Test
35 | env:
36 | scheme: ${{ 'default' }}
37 | platform: ${{ 'iOS Simulator' }}
38 | run: |
39 | # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959)
40 | device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"`
41 | if [ $scheme = default ]; then scheme=$(cat default); fi
42 | if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi
43 | file_to_build=`echo $file_to_build | awk '{$1=$1;print}'`
44 | xcodebuild test-without-building -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device"
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/xcode,macos,fastlane
3 | # Edit at https://www.gitignore.io/?templates=xcode,macos,fastlane
4 |
5 | ### fastlane ###
6 | # fastlane - A streamlined workflow tool for Cocoa deployment
7 | #
8 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
9 | # screenshots whenever they are needed.
10 | # For more information about the recommended setup visit:
11 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
12 |
13 | # fastlane specific
14 | fastlane/report.xml
15 | fastlane/Appfile
16 |
17 | # deliver temporary files
18 | fastlane/Preview.html
19 |
20 | # snapshot generated screenshots
21 | fastlane/screenshots/**/*.png
22 | fastlane/screenshots/screenshots.html
23 |
24 | # scan temporary files
25 | fastlane/test_output
26 |
27 | ### macOS ###
28 | # General
29 | .DS_Store
30 | .AppleDouble
31 | .LSOverride
32 |
33 | # Icon must end with two \r
34 | Icon
35 |
36 | # Thumbnails
37 | ._*
38 |
39 | # Files that might appear in the root of a volume
40 | .DocumentRevisions-V100
41 | .fseventsd
42 | .Spotlight-V100
43 | .TemporaryItems
44 | .Trashes
45 | .VolumeIcon.icns
46 | .com.apple.timemachine.donotpresent
47 |
48 | # Directories potentially created on remote AFP share
49 | .AppleDB
50 | .AppleDesktop
51 | Network Trash Folder
52 | Temporary Items
53 | .apdisk
54 |
55 | ### Xcode ###
56 | # Xcode
57 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
58 |
59 | ## User settings
60 | xcuserdata/
61 |
62 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
63 | *.xcscmblueprint
64 | *.xccheckout
65 |
66 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
67 | build/
68 | DerivedData/
69 | *.moved-aside
70 | *.pbxuser
71 | !default.pbxuser
72 | *.mode1v3
73 | !default.mode1v3
74 | *.mode2v3
75 | !default.mode2v3
76 | *.perspectivev3
77 | !default.perspectivev3
78 |
79 | ## Xcode Patch
80 | *.xcodeproj/*
81 | !*.xcodeproj/project.pbxproj
82 | !*.xcodeproj/xcshareddata/
83 | !*.xcworkspace/contents.xcworkspacedata
84 | /*.gcno
85 |
86 | ### Xcode Patch ###
87 | **/xcshareddata/WorkspaceSettings.xcsettings
88 |
89 | # End of https://www.gitignore.io/api/xcode,macos,fastlane
90 |
--------------------------------------------------------------------------------
/.swift-version:
--------------------------------------------------------------------------------
1 | 5.9
2 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - todo
3 | - trailing_whitespace
4 | - colon
5 | - identifier_name
6 | - comma
7 | - vertical_whitespace
8 | - type_name
9 | - trailing_comma
10 | - multiple_closures_with_trailing_closure
11 | excluded:
12 | - Carthage
13 | - Pods
14 | line_length:
15 | - 200
16 | - 220
17 | function_body_length:
18 | - 200
19 | - 300
20 | type_body_length:
21 | - 300
22 | - 500
23 | file_length:
24 | - 500
25 | - 700
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright © 2020 Lex Tang, https://lex.sh
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the “Software”), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in
12 | all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 | THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RegEx+
2 |
3 | [](https://swift.org/download/)
4 | [](https://twitter.com/lexrus)
5 | [](https://codebeat.co/projects/github-com-lexrus-regexplus-master)
6 |
7 | [
](https://apps.apple.com/us/app/regex/id1511763524)
8 |
9 | > My first *learning-by-doing* **SwiftUI** app! A Regular Expression tool.
10 |
11 | 
12 |
13 | ## Features
14 |
15 | - [x] macOS app and iOS app in one codebase
16 | - [x] Regular Expression syntax highlight
17 | - [x] Sync data automatically with CloudKit
18 | - [x] Regular Expression Cheat Sheet
19 | - [x] Internationalization
20 | - [x] English
21 | - [x] Simplified Chinese
22 | - [x] Traditional Chinese
23 |
24 | ## License
25 |
26 | This code is distributed under the terms and conditions of the MIT license.
27 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | task default: [:sc2tc]
2 |
3 | task :sc2tc do
4 | system "opencc -c s2hk -i RegEx+/zh-Hans.lproj/CheatSheet.plist -o RegEx+/zh-Hant.lproj/CheatSheet.plist"
5 | end
--------------------------------------------------------------------------------
/RegEx+.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 54;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | D709006C245DB72C00326016 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D709006B245DB72C00326016 /* CloudKit.framework */; };
11 | D7090092245DB87C00326016 /* RegEx.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D7090090245DB87C00326016 /* RegEx.xcdatamodeld */; };
12 | D71871E6245EE9AC001F1E4E /* RegExFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71871E5245EE9AC001F1E4E /* RegExFetch.swift */; };
13 | D71F8F10246F8E5300A11283 /* ActivityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71F8F0F246F8E5300A11283 /* ActivityViewController.swift */; };
14 | D721780B2451776000F17390 /* RegExTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D721780A2451776000F17390 /* RegExTextView.swift */; };
15 | D731F64B245EF4C500EEE17F /* RegEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = D731F649245EF4C500EEE17F /* RegEx.swift */; };
16 | D731F64E245EF77600EEE17F /* DataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D731F64D245EF77600EEE17F /* DataManager.swift */; };
17 | D731F650245F053A00EEE17F /* LibraryView+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = D731F64F245F053A00EEE17F /* LibraryView+Data.swift */; };
18 | D731F652245F0B9A00EEE17F /* LibraryItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D731F651245F0B9A00EEE17F /* LibraryItemView.swift */; };
19 | D731F6562460F2BA00EEE17F /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D731F6552460F2BA00EEE17F /* SafariView.swift */; };
20 | D74BF66D247A3C0A0032EE35 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74BF66C247A3C0A0032EE35 /* AboutView.swift */; };
21 | D75902AD2513C6300032013F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D75902AC2513C6300032013F /* AppDelegate.swift */; };
22 | D75902B02513C6620032013F /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D75902AF2513C6620032013F /* SceneDelegate.swift */; };
23 | D7732029245D1877003C0CF8 /* RegExSyntaxHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7732028245D1877003C0CF8 /* RegExSyntaxHighlighter.swift */; };
24 | D7732033245D3388003C0CF8 /* String+NSRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7732032245D3388003C0CF8 /* String+NSRange.swift */; };
25 | D7732035245D3456003C0CF8 /* MatchesTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7732034245D3456003C0CF8 /* MatchesTextView.swift */; };
26 | D77C7970247EBC6D00D3B1B2 /* CheatSheet.plist in Resources */ = {isa = PBXBuildFile; fileRef = D77C7972247EBC6D00D3B1B2 /* CheatSheet.plist */; };
27 | D7A925DC2B81057E0023E8EA /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = D7A925DB2B81057E0023E8EA /* Localizable.xcstrings */; };
28 | D7BB73F9244F47E400343AC5 /* EditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7BB73F8244F47E400343AC5 /* EditorView.swift */; };
29 | D7CCA7D9244E010D00D81C74 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CCA7D8244E010D00D81C74 /* HomeView.swift */; };
30 | D7CCA7DB244E010F00D81C74 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D7CCA7DA244E010F00D81C74 /* Assets.xcassets */; };
31 | D7CCA7DE244E010F00D81C74 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D7CCA7DD244E010F00D81C74 /* Preview Assets.xcassets */; };
32 | D7CCA7E1244E010F00D81C74 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D7CCA7DF244E010F00D81C74 /* LaunchScreen.storyboard */; };
33 | D7CCA7EC244E038E00D81C74 /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CCA7EB244E038E00D81C74 /* LibraryView.swift */; };
34 | D7CCA7EF244E041000D81C74 /* CheatSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CCA7EE244E041000D81C74 /* CheatSheetView.swift */; };
35 | D7DC514A2529645F00A495E2 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DC51492529645F00A495E2 /* SearchView.swift */; };
36 | D7EF735A2453E7FB005974F0 /* EditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EF73592453E7FB005974F0 /* EditorViewModel.swift */; };
37 | /* End PBXBuildFile section */
38 |
39 | /* Begin PBXFileReference section */
40 | D709006B245DB72C00326016 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
41 | D7090091245DB87C00326016 /* RegEx.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = RegEx.xcdatamodel; sourceTree = ""; };
42 | D7129F942567FDFF0031D31B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
43 | D71871E5245EE9AC001F1E4E /* RegExFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegExFetch.swift; sourceTree = ""; };
44 | D71F8F0F246F8E5300A11283 /* ActivityViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityViewController.swift; sourceTree = ""; };
45 | D721780A2451776000F17390 /* RegExTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegExTextView.swift; sourceTree = ""; };
46 | D731F649245EF4C500EEE17F /* RegEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegEx.swift; sourceTree = ""; };
47 | D731F64D245EF77600EEE17F /* DataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataManager.swift; sourceTree = ""; };
48 | D731F64F245F053A00EEE17F /* LibraryView+Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibraryView+Data.swift"; sourceTree = ""; };
49 | D731F651245F0B9A00EEE17F /* LibraryItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryItemView.swift; sourceTree = ""; };
50 | D731F6552460F2BA00EEE17F /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = ""; };
51 | D74BF66C247A3C0A0032EE35 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; };
52 | D75902AC2513C6300032013F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
53 | D75902AF2513C6620032013F /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
54 | D7732028245D1877003C0CF8 /* RegExSyntaxHighlighter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegExSyntaxHighlighter.swift; sourceTree = ""; };
55 | D7732032245D3388003C0CF8 /* String+NSRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+NSRange.swift"; sourceTree = ""; };
56 | D7732034245D3456003C0CF8 /* MatchesTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchesTextView.swift; sourceTree = ""; };
57 | D77C7971247EBC6D00D3B1B2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = en; path = en.lproj/CheatSheet.plist; sourceTree = ""; };
58 | D77C7973247EBC7300D3B1B2 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "zh-Hans"; path = "zh-Hans.lproj/CheatSheet.plist"; sourceTree = ""; };
59 | D7A925DB2B81057E0023E8EA /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; };
60 | D7BB73F8244F47E400343AC5 /* EditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorView.swift; sourceTree = ""; };
61 | D7CCA7D1244E010D00D81C74 /* RegEx.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RegEx.app; sourceTree = BUILT_PRODUCTS_DIR; };
62 | D7CCA7D8244E010D00D81C74 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; };
63 | D7CCA7DA244E010F00D81C74 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
64 | D7CCA7DD244E010F00D81C74 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
65 | D7CCA7E2244E010F00D81C74 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
66 | D7CCA7E8244E011C00D81C74 /* RegEx+.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "RegEx+.entitlements"; sourceTree = ""; };
67 | D7CCA7EB244E038E00D81C74 /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = ""; };
68 | D7CCA7EE244E041000D81C74 /* CheatSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatSheetView.swift; sourceTree = ""; };
69 | D7D0E96F249F4FA800F6B9DF /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "zh-Hant"; path = "zh-Hant.lproj/CheatSheet.plist"; sourceTree = ""; };
70 | D7DC51492529645F00A495E2 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; };
71 | D7E9174A246C47D400F2BE17 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; };
72 | D7EF73592453E7FB005974F0 /* EditorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorViewModel.swift; sourceTree = ""; };
73 | /* End PBXFileReference section */
74 |
75 | /* Begin PBXFrameworksBuildPhase section */
76 | D7CCA7CE244E010D00D81C74 /* Frameworks */ = {
77 | isa = PBXFrameworksBuildPhase;
78 | buildActionMask = 2147483647;
79 | files = (
80 | D709006C245DB72C00326016 /* CloudKit.framework in Frameworks */,
81 | );
82 | runOnlyForDeploymentPostprocessing = 0;
83 | };
84 | /* End PBXFrameworksBuildPhase section */
85 |
86 | /* Begin PBXGroup section */
87 | D709006A245DB72C00326016 /* Frameworks */ = {
88 | isa = PBXGroup;
89 | children = (
90 | D709006B245DB72C00326016 /* CloudKit.framework */,
91 | );
92 | name = Frameworks;
93 | sourceTree = "";
94 | };
95 | D7090093245DC43800326016 /* CoreData+CloudKit */ = {
96 | isa = PBXGroup;
97 | children = (
98 | D731F64D245EF77600EEE17F /* DataManager.swift */,
99 | D731F649245EF4C500EEE17F /* RegEx.swift */,
100 | D71871E5245EE9AC001F1E4E /* RegExFetch.swift */,
101 | );
102 | path = "CoreData+CloudKit";
103 | sourceTree = "";
104 | };
105 | D7090094245DC46200326016 /* Editor */ = {
106 | isa = PBXGroup;
107 | children = (
108 | D7BB73F8244F47E400343AC5 /* EditorView.swift */,
109 | D7EF73592453E7FB005974F0 /* EditorViewModel.swift */,
110 | );
111 | path = Editor;
112 | sourceTree = "";
113 | };
114 | D74BF66E247A3C0E0032EE35 /* About */ = {
115 | isa = PBXGroup;
116 | children = (
117 | D74BF66C247A3C0A0032EE35 /* AboutView.swift */,
118 | );
119 | path = About;
120 | sourceTree = "";
121 | };
122 | D761DDC5245DC4DF00FE0678 /* Library */ = {
123 | isa = PBXGroup;
124 | children = (
125 | D7CCA7EB244E038E00D81C74 /* LibraryView.swift */,
126 | D731F651245F0B9A00EEE17F /* LibraryItemView.swift */,
127 | D731F64F245F053A00EEE17F /* LibraryView+Data.swift */,
128 | );
129 | path = Library;
130 | sourceTree = "";
131 | };
132 | D761DDC6245DC52A00FE0678 /* CheatSheet */ = {
133 | isa = PBXGroup;
134 | children = (
135 | D7CCA7EE244E041000D81C74 /* CheatSheetView.swift */,
136 | );
137 | path = CheatSheet;
138 | sourceTree = "";
139 | };
140 | D7732027245D185C003C0CF8 /* RegExTextView */ = {
141 | isa = PBXGroup;
142 | children = (
143 | D721780A2451776000F17390 /* RegExTextView.swift */,
144 | D7732028245D1877003C0CF8 /* RegExSyntaxHighlighter.swift */,
145 | D7732034245D3456003C0CF8 /* MatchesTextView.swift */,
146 | D7732032245D3388003C0CF8 /* String+NSRange.swift */,
147 | );
148 | path = RegExTextView;
149 | sourceTree = "";
150 | };
151 | D7CCA7C8244E010D00D81C74 = {
152 | isa = PBXGroup;
153 | children = (
154 | D7CCA7D3244E010D00D81C74 /* RegEx+ */,
155 | D7CCA7D2244E010D00D81C74 /* Products */,
156 | D709006A245DB72C00326016 /* Frameworks */,
157 | );
158 | sourceTree = "";
159 | };
160 | D7CCA7D2244E010D00D81C74 /* Products */ = {
161 | isa = PBXGroup;
162 | children = (
163 | D7CCA7D1244E010D00D81C74 /* RegEx.app */,
164 | );
165 | name = Products;
166 | sourceTree = "";
167 | };
168 | D7CCA7D3244E010D00D81C74 /* RegEx+ */ = {
169 | isa = PBXGroup;
170 | children = (
171 | D7E9174A246C47D400F2BE17 /* README.md */,
172 | D7CCA7E8244E011C00D81C74 /* RegEx+.entitlements */,
173 | D75902AC2513C6300032013F /* AppDelegate.swift */,
174 | D75902AF2513C6620032013F /* SceneDelegate.swift */,
175 | D7CCA7D8244E010D00D81C74 /* HomeView.swift */,
176 | D7090093245DC43800326016 /* CoreData+CloudKit */,
177 | D761DDC5245DC4DF00FE0678 /* Library */,
178 | D7090094245DC46200326016 /* Editor */,
179 | D74BF66E247A3C0E0032EE35 /* About */,
180 | D761DDC6245DC52A00FE0678 /* CheatSheet */,
181 | D7CCA7ED244E039D00D81C74 /* Views */,
182 | D7CCA7DA244E010F00D81C74 /* Assets.xcassets */,
183 | D7CCA7DF244E010F00D81C74 /* LaunchScreen.storyboard */,
184 | D7CCA7E2244E010F00D81C74 /* Info.plist */,
185 | D7090090245DB87C00326016 /* RegEx.xcdatamodeld */,
186 | D7CCA7DC244E010F00D81C74 /* Preview Content */,
187 | D77C7972247EBC6D00D3B1B2 /* CheatSheet.plist */,
188 | D7A925DB2B81057E0023E8EA /* Localizable.xcstrings */,
189 | );
190 | path = "RegEx+";
191 | sourceTree = "";
192 | };
193 | D7CCA7DC244E010F00D81C74 /* Preview Content */ = {
194 | isa = PBXGroup;
195 | children = (
196 | D7CCA7DD244E010F00D81C74 /* Preview Assets.xcassets */,
197 | );
198 | path = "Preview Content";
199 | sourceTree = "";
200 | };
201 | D7CCA7ED244E039D00D81C74 /* Views */ = {
202 | isa = PBXGroup;
203 | children = (
204 | D7732027245D185C003C0CF8 /* RegExTextView */,
205 | D731F6552460F2BA00EEE17F /* SafariView.swift */,
206 | D71F8F0F246F8E5300A11283 /* ActivityViewController.swift */,
207 | D7DC51492529645F00A495E2 /* SearchView.swift */,
208 | );
209 | path = Views;
210 | sourceTree = "";
211 | };
212 | /* End PBXGroup section */
213 |
214 | /* Begin PBXNativeTarget section */
215 | D7CCA7D0244E010D00D81C74 /* RegEx+ */ = {
216 | isa = PBXNativeTarget;
217 | buildConfigurationList = D7CCA7E5244E010F00D81C74 /* Build configuration list for PBXNativeTarget "RegEx+" */;
218 | buildPhases = (
219 | D734CBE224AA41E900C1F385 /* SwiftLint */,
220 | D7CCA7CD244E010D00D81C74 /* Sources */,
221 | D7CCA7CE244E010D00D81C74 /* Frameworks */,
222 | D7CCA7CF244E010D00D81C74 /* Resources */,
223 | );
224 | buildRules = (
225 | );
226 | dependencies = (
227 | );
228 | name = "RegEx+";
229 | packageProductDependencies = (
230 | );
231 | productName = RegExPro;
232 | productReference = D7CCA7D1244E010D00D81C74 /* RegEx.app */;
233 | productType = "com.apple.product-type.application";
234 | };
235 | /* End PBXNativeTarget section */
236 |
237 | /* Begin PBXProject section */
238 | D7CCA7C9244E010D00D81C74 /* Project object */ = {
239 | isa = PBXProject;
240 | attributes = {
241 | BuildIndependentTargetsInParallel = YES;
242 | LastSwiftUpdateCheck = 1150;
243 | LastUpgradeCheck = 1520;
244 | ORGANIZATIONNAME = Lex.sh;
245 | TargetAttributes = {
246 | D7CCA7D0244E010D00D81C74 = {
247 | CreatedOnToolsVersion = 11.4.1;
248 | };
249 | };
250 | };
251 | buildConfigurationList = D7CCA7CC244E010D00D81C74 /* Build configuration list for PBXProject "RegEx+" */;
252 | compatibilityVersion = "Xcode 11.0";
253 | developmentRegion = en;
254 | hasScannedForEncodings = 0;
255 | knownRegions = (
256 | en,
257 | "zh-Hans",
258 | "zh-Hant",
259 | Base,
260 | );
261 | mainGroup = D7CCA7C8244E010D00D81C74;
262 | packageReferences = (
263 | );
264 | productRefGroup = D7CCA7D2244E010D00D81C74 /* Products */;
265 | projectDirPath = "";
266 | projectRoot = "";
267 | targets = (
268 | D7CCA7D0244E010D00D81C74 /* RegEx+ */,
269 | );
270 | };
271 | /* End PBXProject section */
272 |
273 | /* Begin PBXResourcesBuildPhase section */
274 | D7CCA7CF244E010D00D81C74 /* Resources */ = {
275 | isa = PBXResourcesBuildPhase;
276 | buildActionMask = 2147483647;
277 | files = (
278 | D7CCA7E1244E010F00D81C74 /* LaunchScreen.storyboard in Resources */,
279 | D7A925DC2B81057E0023E8EA /* Localizable.xcstrings in Resources */,
280 | D77C7970247EBC6D00D3B1B2 /* CheatSheet.plist in Resources */,
281 | D7CCA7DE244E010F00D81C74 /* Preview Assets.xcassets in Resources */,
282 | D7CCA7DB244E010F00D81C74 /* Assets.xcassets in Resources */,
283 | );
284 | runOnlyForDeploymentPostprocessing = 0;
285 | };
286 | /* End PBXResourcesBuildPhase section */
287 |
288 | /* Begin PBXShellScriptBuildPhase section */
289 | D734CBE224AA41E900C1F385 /* SwiftLint */ = {
290 | isa = PBXShellScriptBuildPhase;
291 | buildActionMask = 2147483647;
292 | files = (
293 | );
294 | inputFileListPaths = (
295 | );
296 | inputPaths = (
297 | );
298 | name = SwiftLint;
299 | outputFileListPaths = (
300 | );
301 | outputPaths = (
302 | );
303 | runOnlyForDeploymentPostprocessing = 0;
304 | shellPath = /bin/sh;
305 | shellScript = "export PATH=/opt/homebrew/bin/:$PATH\nif which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
306 | };
307 | /* End PBXShellScriptBuildPhase section */
308 |
309 | /* Begin PBXSourcesBuildPhase section */
310 | D7CCA7CD244E010D00D81C74 /* Sources */ = {
311 | isa = PBXSourcesBuildPhase;
312 | buildActionMask = 2147483647;
313 | files = (
314 | D731F64B245EF4C500EEE17F /* RegEx.swift in Sources */,
315 | D7CCA7EC244E038E00D81C74 /* LibraryView.swift in Sources */,
316 | D7732035245D3456003C0CF8 /* MatchesTextView.swift in Sources */,
317 | D71871E6245EE9AC001F1E4E /* RegExFetch.swift in Sources */,
318 | D71F8F10246F8E5300A11283 /* ActivityViewController.swift in Sources */,
319 | D731F6562460F2BA00EEE17F /* SafariView.swift in Sources */,
320 | D7732033245D3388003C0CF8 /* String+NSRange.swift in Sources */,
321 | D75902B02513C6620032013F /* SceneDelegate.swift in Sources */,
322 | D7CCA7EF244E041000D81C74 /* CheatSheetView.swift in Sources */,
323 | D721780B2451776000F17390 /* RegExTextView.swift in Sources */,
324 | D7DC514A2529645F00A495E2 /* SearchView.swift in Sources */,
325 | D7090092245DB87C00326016 /* RegEx.xcdatamodeld in Sources */,
326 | D731F64E245EF77600EEE17F /* DataManager.swift in Sources */,
327 | D7EF735A2453E7FB005974F0 /* EditorViewModel.swift in Sources */,
328 | D731F652245F0B9A00EEE17F /* LibraryItemView.swift in Sources */,
329 | D7BB73F9244F47E400343AC5 /* EditorView.swift in Sources */,
330 | D74BF66D247A3C0A0032EE35 /* AboutView.swift in Sources */,
331 | D7732029245D1877003C0CF8 /* RegExSyntaxHighlighter.swift in Sources */,
332 | D75902AD2513C6300032013F /* AppDelegate.swift in Sources */,
333 | D731F650245F053A00EEE17F /* LibraryView+Data.swift in Sources */,
334 | D7CCA7D9244E010D00D81C74 /* HomeView.swift in Sources */,
335 | );
336 | runOnlyForDeploymentPostprocessing = 0;
337 | };
338 | /* End PBXSourcesBuildPhase section */
339 |
340 | /* Begin PBXVariantGroup section */
341 | D77C7972247EBC6D00D3B1B2 /* CheatSheet.plist */ = {
342 | isa = PBXVariantGroup;
343 | children = (
344 | D77C7971247EBC6D00D3B1B2 /* en */,
345 | D77C7973247EBC7300D3B1B2 /* zh-Hans */,
346 | D7D0E96F249F4FA800F6B9DF /* zh-Hant */,
347 | );
348 | name = CheatSheet.plist;
349 | sourceTree = "";
350 | };
351 | D7CCA7DF244E010F00D81C74 /* LaunchScreen.storyboard */ = {
352 | isa = PBXVariantGroup;
353 | children = (
354 | D7129F942567FDFF0031D31B /* Base */,
355 | );
356 | name = LaunchScreen.storyboard;
357 | sourceTree = "";
358 | };
359 | /* End PBXVariantGroup section */
360 |
361 | /* Begin XCBuildConfiguration section */
362 | D7CCA7E3244E010F00D81C74 /* Debug */ = {
363 | isa = XCBuildConfiguration;
364 | buildSettings = {
365 | ALWAYS_SEARCH_USER_PATHS = NO;
366 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
367 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
368 | CLANG_ANALYZER_NONNULL = YES;
369 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
370 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
371 | CLANG_CXX_LIBRARY = "libc++";
372 | CLANG_ENABLE_MODULES = YES;
373 | CLANG_ENABLE_OBJC_ARC = YES;
374 | CLANG_ENABLE_OBJC_WEAK = YES;
375 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
376 | CLANG_WARN_BOOL_CONVERSION = YES;
377 | CLANG_WARN_COMMA = YES;
378 | CLANG_WARN_CONSTANT_CONVERSION = YES;
379 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
380 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
381 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
382 | CLANG_WARN_EMPTY_BODY = YES;
383 | CLANG_WARN_ENUM_CONVERSION = YES;
384 | CLANG_WARN_INFINITE_RECURSION = YES;
385 | CLANG_WARN_INT_CONVERSION = YES;
386 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
387 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
388 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
389 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
390 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
391 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
392 | CLANG_WARN_STRICT_PROTOTYPES = YES;
393 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
394 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
395 | CLANG_WARN_UNREACHABLE_CODE = YES;
396 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
397 | COPY_PHASE_STRIP = NO;
398 | DEBUG_INFORMATION_FORMAT = dwarf;
399 | ENABLE_STRICT_OBJC_MSGSEND = YES;
400 | ENABLE_TESTABILITY = YES;
401 | ENABLE_USER_SCRIPT_SANDBOXING = NO;
402 | GCC_C_LANGUAGE_STANDARD = gnu11;
403 | GCC_DYNAMIC_NO_PIC = NO;
404 | GCC_NO_COMMON_BLOCKS = YES;
405 | GCC_OPTIMIZATION_LEVEL = 0;
406 | GCC_PREPROCESSOR_DEFINITIONS = (
407 | "DEBUG=1",
408 | "$(inherited)",
409 | );
410 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
411 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
412 | GCC_WARN_UNDECLARED_SELECTOR = YES;
413 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
414 | GCC_WARN_UNUSED_FUNCTION = YES;
415 | GCC_WARN_UNUSED_VARIABLE = YES;
416 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
417 | MACOSX_DEPLOYMENT_TARGET = 13.0;
418 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
419 | MTL_FAST_MATH = YES;
420 | ONLY_ACTIVE_ARCH = YES;
421 | SDKROOT = iphoneos;
422 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx";
423 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
424 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
425 | SWIFT_VERSION = 5.0;
426 | };
427 | name = Debug;
428 | };
429 | D7CCA7E4244E010F00D81C74 /* Release */ = {
430 | isa = XCBuildConfiguration;
431 | buildSettings = {
432 | ALWAYS_SEARCH_USER_PATHS = NO;
433 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
434 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
435 | CLANG_ANALYZER_NONNULL = YES;
436 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
437 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
438 | CLANG_CXX_LIBRARY = "libc++";
439 | CLANG_ENABLE_MODULES = YES;
440 | CLANG_ENABLE_OBJC_ARC = YES;
441 | CLANG_ENABLE_OBJC_WEAK = YES;
442 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
443 | CLANG_WARN_BOOL_CONVERSION = YES;
444 | CLANG_WARN_COMMA = YES;
445 | CLANG_WARN_CONSTANT_CONVERSION = YES;
446 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
447 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
448 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
449 | CLANG_WARN_EMPTY_BODY = YES;
450 | CLANG_WARN_ENUM_CONVERSION = YES;
451 | CLANG_WARN_INFINITE_RECURSION = YES;
452 | CLANG_WARN_INT_CONVERSION = YES;
453 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
454 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
455 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
456 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
457 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
458 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
459 | CLANG_WARN_STRICT_PROTOTYPES = YES;
460 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
461 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
462 | CLANG_WARN_UNREACHABLE_CODE = YES;
463 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
464 | COPY_PHASE_STRIP = NO;
465 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
466 | ENABLE_NS_ASSERTIONS = NO;
467 | ENABLE_STRICT_OBJC_MSGSEND = YES;
468 | ENABLE_USER_SCRIPT_SANDBOXING = NO;
469 | GCC_C_LANGUAGE_STANDARD = gnu11;
470 | GCC_NO_COMMON_BLOCKS = YES;
471 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
472 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
473 | GCC_WARN_UNDECLARED_SELECTOR = YES;
474 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
475 | GCC_WARN_UNUSED_FUNCTION = YES;
476 | GCC_WARN_UNUSED_VARIABLE = YES;
477 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
478 | MACOSX_DEPLOYMENT_TARGET = 13.0;
479 | MTL_ENABLE_DEBUG_INFO = NO;
480 | MTL_FAST_MATH = YES;
481 | SDKROOT = iphoneos;
482 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx";
483 | SWIFT_COMPILATION_MODE = wholemodule;
484 | SWIFT_OPTIMIZATION_LEVEL = "-O";
485 | SWIFT_VERSION = 5.0;
486 | VALIDATE_PRODUCT = YES;
487 | };
488 | name = Release;
489 | };
490 | D7CCA7E6244E010F00D81C74 /* Debug */ = {
491 | isa = XCBuildConfiguration;
492 | buildSettings = {
493 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
494 | CODE_SIGN_ENTITLEMENTS = "RegEx+/RegEx+.entitlements";
495 | CODE_SIGN_STYLE = Automatic;
496 | CURRENT_PROJECT_VERSION = 20;
497 | DEVELOPMENT_ASSET_PATHS = "RegEx+/Preview\\ Content/Preview\\ Assets.xcassets";
498 | DEVELOPMENT_TEAM = 5SKD83S59G;
499 | ENABLE_PREVIEWS = YES;
500 | INFOPLIST_FILE = "$(SRCROOT)/RegEx+/Info.plist";
501 | LD_RUNPATH_SEARCH_PATHS = (
502 | "$(inherited)",
503 | "@executable_path/Frameworks",
504 | );
505 | MARKETING_VERSION = 0.7.2;
506 | PRODUCT_BUNDLE_IDENTIFIER = sh.lex.RegExCatalyst;
507 | PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)App";
508 | PRODUCT_NAME = RegEx;
509 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
510 | SUPPORTS_MACCATALYST = YES;
511 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
512 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
513 | SWIFT_EMIT_LOC_STRINGS = YES;
514 | TARGETED_DEVICE_FAMILY = "1,2";
515 | };
516 | name = Debug;
517 | };
518 | D7CCA7E7244E010F00D81C74 /* Release */ = {
519 | isa = XCBuildConfiguration;
520 | buildSettings = {
521 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
522 | CODE_SIGN_ENTITLEMENTS = "RegEx+/RegEx+.entitlements";
523 | CODE_SIGN_STYLE = Automatic;
524 | CURRENT_PROJECT_VERSION = 20;
525 | DEVELOPMENT_ASSET_PATHS = "RegEx+/Preview\\ Content/Preview\\ Assets.xcassets";
526 | DEVELOPMENT_TEAM = 5SKD83S59G;
527 | ENABLE_PREVIEWS = YES;
528 | INFOPLIST_FILE = "$(SRCROOT)/RegEx+/Info.plist";
529 | LD_RUNPATH_SEARCH_PATHS = (
530 | "$(inherited)",
531 | "@executable_path/Frameworks",
532 | );
533 | MARKETING_VERSION = 0.7.2;
534 | PRODUCT_BUNDLE_IDENTIFIER = sh.lex.RegExCatalyst;
535 | PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)App";
536 | PRODUCT_NAME = RegEx;
537 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
538 | SUPPORTS_MACCATALYST = YES;
539 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
540 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
541 | SWIFT_EMIT_LOC_STRINGS = YES;
542 | TARGETED_DEVICE_FAMILY = "1,2";
543 | };
544 | name = Release;
545 | };
546 | /* End XCBuildConfiguration section */
547 |
548 | /* Begin XCConfigurationList section */
549 | D7CCA7CC244E010D00D81C74 /* Build configuration list for PBXProject "RegEx+" */ = {
550 | isa = XCConfigurationList;
551 | buildConfigurations = (
552 | D7CCA7E3244E010F00D81C74 /* Debug */,
553 | D7CCA7E4244E010F00D81C74 /* Release */,
554 | );
555 | defaultConfigurationIsVisible = 0;
556 | defaultConfigurationName = Release;
557 | };
558 | D7CCA7E5244E010F00D81C74 /* Build configuration list for PBXNativeTarget "RegEx+" */ = {
559 | isa = XCConfigurationList;
560 | buildConfigurations = (
561 | D7CCA7E6244E010F00D81C74 /* Debug */,
562 | D7CCA7E7244E010F00D81C74 /* Release */,
563 | );
564 | defaultConfigurationIsVisible = 0;
565 | defaultConfigurationName = Release;
566 | };
567 | /* End XCConfigurationList section */
568 |
569 | /* Begin XCVersionGroup section */
570 | D7090090245DB87C00326016 /* RegEx.xcdatamodeld */ = {
571 | isa = XCVersionGroup;
572 | children = (
573 | D7090091245DB87C00326016 /* RegEx.xcdatamodel */,
574 | );
575 | currentVersion = D7090091245DB87C00326016 /* RegEx.xcdatamodel */;
576 | path = RegEx.xcdatamodeld;
577 | sourceTree = "";
578 | versionGroupType = wrapper.xcdatamodel;
579 | };
580 | /* End XCVersionGroup section */
581 | };
582 | rootObject = D7CCA7C9244E010D00D81C74 /* Project object */;
583 | }
584 |
--------------------------------------------------------------------------------
/RegEx+.xcodeproj/xcshareddata/xcschemes/RegEx+.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
57 |
58 |
61 |
62 |
65 |
66 |
69 |
70 |
71 |
72 |
78 |
80 |
86 |
87 |
88 |
89 |
91 |
92 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/RegEx+/About/AboutView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AboutView.swift
3 | // RegEx+
4 | //
5 | // Created by Lex on 2020/5/24.
6 | // Copyright © 2020 Lex.sh. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import StoreKit
11 |
12 |
13 | private let kAppStoreReviewUrl = "https://apps.apple.com/app/regex/id1511763524?action=write-review"
14 | private let kGitHubUrl = "https://github.com/lexrus/RegExPlus"
15 |
16 |
17 | struct AboutView: View {
18 | @State private var showingAppStore = false
19 | @State private var showingGitHub = false
20 | @State private var showingAcknowledgements = false
21 |
22 | private var rateButton: some View {
23 | Button(action: {
24 | if #available(iOS 14.0, *) {
25 | guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
26 | return
27 | }
28 | SKStoreReviewController.requestReview(in: scene)
29 | } else {
30 | SKStoreReviewController.requestReview()
31 | }
32 | }) {
33 | Text("Rate RegEx+")
34 | }
35 | }
36 |
37 | private var appStoreButton: some View {
38 | let url = URL(string: kAppStoreReviewUrl)!
39 |
40 | return Button(action: {
41 | #if targetEnvironment(macCatalyst)
42 | UIApplication.shared.open(url)
43 | #else
44 | showingAppStore.toggle()
45 | #endif
46 | }) {
47 | Text("Write a review")
48 | }
49 | .sheet(isPresented: $showingAppStore) {
50 | SafariView(url: url)
51 | }
52 | }
53 |
54 | private var gitHubButton: some View {
55 | let url = URL(string: kGitHubUrl)!
56 |
57 | return Button(action: {
58 | #if targetEnvironment(macCatalyst)
59 | UIApplication.shared.open(url)
60 | #else
61 | showingGitHub.toggle()
62 | #endif
63 | }) {
64 | Text(kGitHubUrl)
65 | }
66 | .sheet(isPresented: $showingGitHub) {
67 | SafariView(url: url)
68 | }
69 | }
70 |
71 | var body: some View {
72 | List {
73 | Section(header: Text("\(Bundle.main.releaseVersionNumber ?? "")")) {
74 | rateButton
75 | appStoreButton
76 | gitHubButton
77 | }
78 | }
79 | .listStyle(InsetGroupedListStyle())
80 | .navigationBarTitle("RegEx+")
81 | }
82 | }
83 |
84 | extension Bundle {
85 |
86 | var releaseVersionNumber: String? {
87 | return infoDictionary?["CFBundleShortVersionString"] as? String
88 | }
89 |
90 | var buildVersionNumber: String? {
91 | return infoDictionary?["CFBundleVersion"] as? String
92 | }
93 |
94 | }
95 |
96 | struct AboutView_Previews: PreviewProvider {
97 | static var previews: some View {
98 | AboutView()
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/RegEx+/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // RegEx+
4 | //
5 | // Created by Lex on 2020/4/21.
6 | // Copyright © 2020 Lex.sh. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import CoreData
11 | import CloudKit
12 |
13 |
14 | @UIApplicationMain
15 | class AppDelegate: UIResponder, UIApplicationDelegate {
16 |
17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
18 | // Override point for customization after application launch.
19 | return true
20 | }
21 |
22 | // MARK: UISceneSession Lifecycle
23 |
24 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
25 | // Called when a new scene session is being created.
26 | // Use this method to select a configuration to create the new scene with.
27 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
28 | }
29 |
30 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
31 | // Called when the user discards a scene session.
32 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
33 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
34 | }
35 |
36 | override func buildMenu(with builder: UIMenuBuilder) {
37 | super.buildMenu(with: builder)
38 |
39 | // Ensure that the builder is modifying the menu bar system.
40 | guard builder.system == UIMenuSystem.main else { return }
41 |
42 | builder.remove(menu: .help)
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/RegEx+/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "1.000",
9 | "green" : "0.400",
10 | "red" : "0.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.900",
27 | "green" : "0.300",
28 | "red" : "0.000"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/RegEx+/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon_40pt.png",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "icon_20pt@3x.png",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "icon_29pt@2x.png",
17 | "idiom" : "iphone",
18 | "scale" : "2x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "icon_29pt@3x.png",
23 | "idiom" : "iphone",
24 | "scale" : "3x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "icon_40pt@2x.png",
29 | "idiom" : "iphone",
30 | "scale" : "2x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "filename" : "icon_60pt@2x.png",
35 | "idiom" : "iphone",
36 | "scale" : "3x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "icon_60pt@2x.png",
41 | "idiom" : "iphone",
42 | "scale" : "2x",
43 | "size" : "60x60"
44 | },
45 | {
46 | "filename" : "icon_60pt@3x.png",
47 | "idiom" : "iphone",
48 | "scale" : "3x",
49 | "size" : "60x60"
50 | },
51 | {
52 | "filename" : "icon_20pt.png",
53 | "idiom" : "ipad",
54 | "scale" : "1x",
55 | "size" : "20x20"
56 | },
57 | {
58 | "filename" : "icon_40pt.png",
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "20x20"
62 | },
63 | {
64 | "filename" : "icon_29pt.png",
65 | "idiom" : "ipad",
66 | "scale" : "1x",
67 | "size" : "29x29"
68 | },
69 | {
70 | "filename" : "icon_29pt@2x.png",
71 | "idiom" : "ipad",
72 | "scale" : "2x",
73 | "size" : "29x29"
74 | },
75 | {
76 | "filename" : "icon_40pt.png",
77 | "idiom" : "ipad",
78 | "scale" : "1x",
79 | "size" : "40x40"
80 | },
81 | {
82 | "filename" : "icon_40pt@2x.png",
83 | "idiom" : "ipad",
84 | "scale" : "2x",
85 | "size" : "40x40"
86 | },
87 | {
88 | "filename" : "icon_76pt.png",
89 | "idiom" : "ipad",
90 | "scale" : "1x",
91 | "size" : "76x76"
92 | },
93 | {
94 | "filename" : "icon_76pt@2x.png",
95 | "idiom" : "ipad",
96 | "scale" : "2x",
97 | "size" : "76x76"
98 | },
99 | {
100 | "filename" : "icon_83.5@2x.png",
101 | "idiom" : "ipad",
102 | "scale" : "2x",
103 | "size" : "83.5x83.5"
104 | },
105 | {
106 | "filename" : "Icon.png",
107 | "idiom" : "ios-marketing",
108 | "scale" : "1x",
109 | "size" : "1024x1024"
110 | },
111 | {
112 | "idiom" : "mac",
113 | "scale" : "1x",
114 | "size" : "16x16"
115 | },
116 | {
117 | "idiom" : "mac",
118 | "scale" : "2x",
119 | "size" : "16x16"
120 | },
121 | {
122 | "idiom" : "mac",
123 | "scale" : "1x",
124 | "size" : "32x32"
125 | },
126 | {
127 | "idiom" : "mac",
128 | "scale" : "2x",
129 | "size" : "32x32"
130 | },
131 | {
132 | "filename" : "mac128.png",
133 | "idiom" : "mac",
134 | "scale" : "1x",
135 | "size" : "128x128"
136 | },
137 | {
138 | "filename" : "mac256.png",
139 | "idiom" : "mac",
140 | "scale" : "2x",
141 | "size" : "128x128"
142 | },
143 | {
144 | "filename" : "mac256.png",
145 | "idiom" : "mac",
146 | "scale" : "1x",
147 | "size" : "256x256"
148 | },
149 | {
150 | "filename" : "mac512.png",
151 | "idiom" : "mac",
152 | "scale" : "2x",
153 | "size" : "256x256"
154 | },
155 | {
156 | "filename" : "mac512.png",
157 | "idiom" : "mac",
158 | "scale" : "1x",
159 | "size" : "512x512"
160 | },
161 | {
162 | "filename" : "mac.png",
163 | "idiom" : "mac",
164 | "scale" : "2x",
165 | "size" : "512x512"
166 | }
167 | ],
168 | "info" : {
169 | "author" : "xcode",
170 | "version" : 1
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/RegEx+/Assets.xcassets/AppIcon.appiconset/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexrus/RegExPlus/1a9bc340ac0a406645d9179c92a2faf509d35c84/RegEx+/Assets.xcassets/AppIcon.appiconset/Icon.png
--------------------------------------------------------------------------------
/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_20pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexrus/RegExPlus/1a9bc340ac0a406645d9179c92a2faf509d35c84/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_20pt.png
--------------------------------------------------------------------------------
/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexrus/RegExPlus/1a9bc340ac0a406645d9179c92a2faf509d35c84/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png
--------------------------------------------------------------------------------
/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_29pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexrus/RegExPlus/1a9bc340ac0a406645d9179c92a2faf509d35c84/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_29pt.png
--------------------------------------------------------------------------------
/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexrus/RegExPlus/1a9bc340ac0a406645d9179c92a2faf509d35c84/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png
--------------------------------------------------------------------------------
/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexrus/RegExPlus/1a9bc340ac0a406645d9179c92a2faf509d35c84/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png
--------------------------------------------------------------------------------
/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_40pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexrus/RegExPlus/1a9bc340ac0a406645d9179c92a2faf509d35c84/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_40pt.png
--------------------------------------------------------------------------------
/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexrus/RegExPlus/1a9bc340ac0a406645d9179c92a2faf509d35c84/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png
--------------------------------------------------------------------------------
/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexrus/RegExPlus/1a9bc340ac0a406645d9179c92a2faf509d35c84/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png
--------------------------------------------------------------------------------
/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexrus/RegExPlus/1a9bc340ac0a406645d9179c92a2faf509d35c84/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png
--------------------------------------------------------------------------------
/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_76pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexrus/RegExPlus/1a9bc340ac0a406645d9179c92a2faf509d35c84/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_76pt.png
--------------------------------------------------------------------------------
/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexrus/RegExPlus/1a9bc340ac0a406645d9179c92a2faf509d35c84/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png
--------------------------------------------------------------------------------
/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexrus/RegExPlus/1a9bc340ac0a406645d9179c92a2faf509d35c84/RegEx+/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png
--------------------------------------------------------------------------------
/RegEx+/Assets.xcassets/AppIcon.appiconset/mac.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexrus/RegExPlus/1a9bc340ac0a406645d9179c92a2faf509d35c84/RegEx+/Assets.xcassets/AppIcon.appiconset/mac.png
--------------------------------------------------------------------------------
/RegEx+/Assets.xcassets/AppIcon.appiconset/mac128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexrus/RegExPlus/1a9bc340ac0a406645d9179c92a2faf509d35c84/RegEx+/Assets.xcassets/AppIcon.appiconset/mac128.png
--------------------------------------------------------------------------------
/RegEx+/Assets.xcassets/AppIcon.appiconset/mac256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexrus/RegExPlus/1a9bc340ac0a406645d9179c92a2faf509d35c84/RegEx+/Assets.xcassets/AppIcon.appiconset/mac256.png
--------------------------------------------------------------------------------
/RegEx+/Assets.xcassets/AppIcon.appiconset/mac512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lexrus/RegExPlus/1a9bc340ac0a406645d9179c92a2faf509d35c84/RegEx+/Assets.xcassets/AppIcon.appiconset/mac512.png
--------------------------------------------------------------------------------
/RegEx+/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/RegEx+/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
26 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/RegEx+/CheatSheet/CheatSheetView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CheatSheetView.swift
3 | // RegEx+
4 | //
5 | // Created by Lex on 2020/4/21.
6 | // Copyright © 2020 Lex.sh. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 |
12 | // Official documentation of NSRegularExpression
13 | private let kNSRegularExpressionDocumentLink = "https://developer.apple.com/documentation/foundation/nsregularexpression"
14 |
15 |
16 | struct CheatSheetView: View {
17 | @State var showingSafari = false
18 |
19 | @State var metacharacters: [CheatSheetPlist.Item] = []
20 | @State var operators: [CheatSheetPlist.Item] = []
21 |
22 | var body: some View {
23 | List {
24 | Section(header: Text("Metacharacters")) {
25 | ForEach(metacharacters, id: \.exp) {
26 | RowView(title: $0.exp, content: $0.des)
27 | }
28 | }
29 |
30 | Section(header: Text("Operators")) {
31 | ForEach(operators, id: \.exp) {
32 | RowView(title: $0.exp, content: $0.des)
33 | }
34 | }
35 | }
36 | .navigationBarTitle("Cheat Sheet")
37 | .navigationBarItems(trailing: safariButton)
38 | .onAppear(perform: loadPlist)
39 | }
40 |
41 | private func loadPlist() {
42 | guard let url = Bundle.main.url(forResource: "CheatSheet", withExtension: "plist") else {
43 | assertionFailure("Missing CheatSheet.plist!")
44 | return
45 | }
46 |
47 | let plistDecoder = PropertyListDecoder()
48 |
49 | do {
50 | let data = try Data(contentsOf: url)
51 | let dict = try plistDecoder.decode(CheatSheetPlist.self, from: data)
52 | metacharacters = dict.metacharacters
53 | operators = dict.operators
54 | } catch {
55 | print(error.localizedDescription)
56 | }
57 | }
58 |
59 | private var safariButton: some View {
60 | let url = URL(string: kNSRegularExpressionDocumentLink)!
61 |
62 | return Button(action: {
63 | #if targetEnvironment(macCatalyst)
64 | UIApplication.shared.open(url)
65 | #else
66 | showingSafari.toggle()
67 | #endif
68 | }) {
69 | Image(systemName: "safari")
70 | .imageScale(.large)
71 | .padding(EdgeInsets(top: 8, leading: 24, bottom: 8, trailing: 0))
72 | }
73 | .sheet(isPresented: $showingSafari, content: {
74 | SafariView(url: url)
75 | })
76 | }
77 | }
78 |
79 | struct CheatSheetView_Previews: PreviewProvider {
80 | static var previews: some View {
81 | NavigationView {
82 | CheatSheetView()
83 | .navigationBarTitle("Cheat Sheet")
84 | }
85 | .navigationViewStyle(StackNavigationViewStyle())
86 | }
87 | }
88 |
89 | private struct RowView: View {
90 | var title: String
91 | var content: String
92 |
93 | var body: some View {
94 | VStack(alignment: .leading, spacing: 3) {
95 | Text(title)
96 | .font(.headline)
97 | .foregroundColor(.accentColor)
98 | Text(content)
99 | .font(.subheadline)
100 | }
101 | .padding(.vertical, 6)
102 | }
103 | }
104 |
105 | struct CheatSheetPlist: Decodable {
106 | struct Item: Decodable, Hashable {
107 | var exp: String
108 | var des: String
109 | }
110 |
111 | var metacharacters: [Item]
112 | var operators: [Item]
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/RegEx+/CoreData+CloudKit/DataManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataManager.swift
3 | // RegEx+
4 | //
5 | // Created by Lex on 2020/5/3.
6 | // Copyright © 2020 Lex.sh. All rights reserved.
7 | //
8 |
9 | import CoreData
10 | import CloudKit
11 |
12 |
13 | class DataManager {
14 |
15 | static let shared = DataManager()
16 |
17 | lazy var persistentContainer: NSPersistentCloudKitContainer = {
18 |
19 | let container = NSPersistentCloudKitContainer(name: "RegEx")
20 |
21 | guard let description = container.persistentStoreDescriptions.first else {
22 | fatalError("No Descriptions found")
23 | }
24 | description.setOption(true as NSObject, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
25 |
26 | container.viewContext.automaticallyMergesChangesFromParent = true
27 | container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump
28 |
29 | container.loadPersistentStores(completionHandler: { (_, error) in
30 | if let error = error as NSError? {
31 | fatalError("Unresolved error \(error), \(error.userInfo)")
32 | }
33 | })
34 |
35 | // NotificationCenter.default.addObserver(self, selector: #selector(self.processUpdate), name: .NSPersistentStoreRemoteChange, object: nil)
36 |
37 | return container
38 | }()
39 |
40 | // MARK: - Initialize CloudKit schema
41 |
42 | func initializeCloudKitSchema() {
43 | do {
44 | try persistentContainer.initializeCloudKitSchema(options: NSPersistentCloudKitContainerSchemaInitializationOptions.printSchema)
45 | } catch {
46 | print(error.localizedDescription)
47 | }
48 | }
49 |
50 | // MARK: - Core Data Saving support
51 |
52 | func saveContext() {
53 | let context = persistentContainer.viewContext
54 | guard context.hasChanges else {
55 | return
56 | }
57 | do {
58 | try context.save()
59 | } catch {
60 | // Replace this implementation with code to handle the error appropriately.
61 | // 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.
62 | let nserror = error as NSError
63 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
64 | }
65 | }
66 |
67 | @objc
68 | func processUpdate(notification: NSNotification) {
69 | operationQueue.addOperation {
70 | let context = self.persistentContainer.newBackgroundContext()
71 | context.performAndWait {
72 | let items: [RegEx]
73 | do {
74 | try items = context.fetch(RegEx.fetchAllRegEx())
75 | } catch {
76 | let nserror = error as NSError
77 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
78 | }
79 |
80 | items.forEach {
81 | print("NAME: \($0.name) !!!!")
82 | }
83 |
84 | if context.hasChanges {
85 | do {
86 | try context.save()
87 | } catch {
88 | let nserror = error as NSError
89 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
90 | }
91 | }
92 | }
93 |
94 | }
95 | }
96 |
97 | private lazy var operationQueue: OperationQueue = {
98 | var queue = OperationQueue()
99 | queue.maxConcurrentOperationCount = 1
100 | return queue
101 | }()
102 |
103 | }
104 |
--------------------------------------------------------------------------------
/RegEx+/CoreData+CloudKit/RegEx.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RegEx+CoreDataClass.swift
3 | // RegEx+
4 | //
5 | // Created by Lex on 2020/5/3.
6 | // Copyright © 2020 Lex.sh. All rights reserved.
7 | //
8 | //
9 |
10 | import Foundation
11 | import CoreData
12 |
13 | @objc(RegEx)
14 | public class RegEx: NSManagedObject {
15 |
16 | @NSManaged public var name: String
17 | @NSManaged public var raw: String
18 | @NSManaged public var sample: String
19 | @NSManaged public var substitution: String
20 | @NSManaged public var createdAt: Date
21 | @NSManaged public var updatedAt: Date
22 |
23 | @NSManaged public var allowCommentsAndWhitespace: Bool
24 | @NSManaged public var anchorsMatchLines: Bool
25 | @NSManaged public var caseInsensitive: Bool
26 | @NSManaged public var dotMatchesLineSeparators: Bool
27 | @NSManaged public var ignoreMetacharacters: Bool
28 | @NSManaged public var useUnicodeWordBoundaries: Bool
29 | @NSManaged public var useUnixLineSeparators: Bool
30 |
31 | convenience init(name: String = "Untitled") {
32 | self.init()
33 | self.name = name
34 | self.raw = ""
35 | self.sample = ""
36 | self.substitution = ""
37 | self.createdAt = Date()
38 | self.updatedAt = Date()
39 | }
40 |
41 | public var regularExpressionOptions: NSRegularExpression.Options {
42 | var options: NSRegularExpression.Options = []
43 | if allowCommentsAndWhitespace {
44 | options.insert(.allowCommentsAndWhitespace)
45 | }
46 | if anchorsMatchLines {
47 | options.insert(.anchorsMatchLines)
48 | }
49 | if caseInsensitive {
50 | options.insert(.caseInsensitive)
51 | }
52 | if dotMatchesLineSeparators {
53 | options.insert(.dotMatchesLineSeparators)
54 | }
55 | if ignoreMetacharacters {
56 | options.insert(.ignoreMetacharacters)
57 | }
58 | if useUnicodeWordBoundaries {
59 | options.insert(.useUnicodeWordBoundaries)
60 | }
61 | if useUnixLineSeparators {
62 | options.insert(.useUnixLineSeparators)
63 | }
64 | return options
65 | }
66 |
67 | public var flagOptions: String {
68 | ""
69 | + (caseInsensitive ? "i" : "")
70 | + (allowCommentsAndWhitespace ? "x" : "")
71 | + (dotMatchesLineSeparators ? "." : "")
72 | + (anchorsMatchLines ? "m" : "")
73 | + (useUnicodeWordBoundaries ? "w" : "")
74 | }
75 |
76 | public override var description: String {
77 | "/\(raw)/\(flagOptions)"
78 | }
79 |
80 | public func isEqual(to object: RegEx) -> Bool {
81 | name == object.name
82 | && regularExpressionOptions == object.regularExpressionOptions
83 | && raw == object.raw
84 | && sample == object.sample
85 | && substitution == object.substitution
86 | && allowCommentsAndWhitespace == object.allowCommentsAndWhitespace
87 | && anchorsMatchLines == object.anchorsMatchLines
88 | && caseInsensitive == object.caseInsensitive
89 | && dotMatchesLineSeparators == object.dotMatchesLineSeparators
90 | && ignoreMetacharacters == object.ignoreMetacharacters
91 | && useUnicodeWordBoundaries == object.useUnicodeWordBoundaries
92 | && useUnixLineSeparators == object.useUnixLineSeparators
93 | }
94 |
95 | }
96 |
--------------------------------------------------------------------------------
/RegEx+/CoreData+CloudKit/RegExFetch.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RegExFetch.swift
3 | // RegEx+
4 | //
5 | // Created by Lex on 2020/5/3.
6 | // Copyright © 2020 Lex.sh. All rights reserved.
7 | //
8 |
9 | import CoreData
10 |
11 |
12 | extension RegEx {
13 |
14 | @nonobjc public class func fetchRequest() -> NSFetchRequest {
15 | NSFetchRequest(entityName: "RegEx")
16 | }
17 |
18 | @nonobjc public class func fetchAllRegEx() -> NSFetchRequest {
19 | let req: NSFetchRequest = RegEx.fetchRequest()
20 | req.sortDescriptors = [
21 | NSSortDescriptor(key: "createdAt", ascending: false),
22 | NSSortDescriptor(key: "updatedAt", ascending: false)
23 | ]
24 | return req
25 | }
26 |
27 | @nonobjc public class func fetch(byID ID: NSManagedObjectID) -> NSFetchRequest {
28 | let req: NSFetchRequest = RegEx.fetchRequest()
29 | req.predicate = NSPredicate(format: "self.objectID IN %@", ID)
30 | return req
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/RegEx+/Editor/EditorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EditorView.swift
3 | // RegEx+
4 | //
5 | // Created by Lex on 2020/4/21.
6 | // Copyright © 2020 Lex.sh. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct EditorView: View, Equatable {
12 |
13 | static func == (lhs: EditorView, rhs: EditorView) -> Bool {
14 | lhs.viewModel == rhs.viewModel
15 | }
16 |
17 | @ObservedObject var viewModel: EditorViewModel
18 | @State private var isSharePresented = false
19 | @State private var copyButtonText = "Copy"
20 |
21 | init(regEx: RegEx) {
22 | viewModel = EditorViewModel(regEx: regEx)
23 | }
24 |
25 | var body: some View {
26 | List {
27 | Section(header: Text("Name")) {
28 | TextField("Name", text: $viewModel.regEx.name)
29 | .font(.headline)
30 | }
31 |
32 | RegExTextViewSection(regEx: $viewModel.regEx)
33 |
34 | Section(header: SampleHeaderView(count: viewModel.matches.count)) {
35 | MatchesTextView(
36 | "$56.78 $90.12",
37 | text: $viewModel.regEx.sample,
38 | matches: $viewModel.matches
39 | )
40 | .equatable()
41 | .padding(kTextFieldPadding)
42 | }
43 |
44 | Section(header: Text("Substitution Template")) {
45 | TextField("Price: $$$1\\.$2\\n", text: $viewModel.regEx.substitution)
46 | .padding(kTextFieldPadding)
47 | }
48 |
49 | if !viewModel.regEx.substitution.isEmpty {
50 | Section(header: Text("Substitution Result")) {
51 | HStack {
52 | Text(viewModel.substitutionResult)
53 | .padding(kTextFieldPadding)
54 | if !viewModel.substitutionResult.isEmpty {
55 | Spacer()
56 | Button(action: copyToClipboard) {
57 | Text("\(copyButtonText)")
58 | .font(.footnote)
59 | .foregroundColor(Color.accentColor)
60 | .padding(EdgeInsets(top: 1, leading: 6, bottom: 1, trailing: 6))
61 | .overlay(
62 | RoundedRectangle(cornerRadius: 20)
63 | .stroke(Color.accentColor, lineWidth: 1)
64 | )
65 | }
66 | }
67 | }
68 | }
69 | }
70 | }
71 | .navigationViewStyle(StackNavigationViewStyle())
72 | .navigationTitle(viewModel.regEx.name)
73 | .navigationBarItems(trailing: HStack(spacing: 8) {
74 | #if !targetEnvironment(macCatalyst)
75 | shareButton.padding()
76 | cheatSheetButton().padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 0))
77 | #else
78 | EmptyView()
79 | #endif
80 | })
81 | .gesture(dismissKeyboardDesture)
82 | .listStyle(InsetGroupedListStyle())
83 | .onDisappear(perform: {
84 | viewModel.updateLastModified()
85 | DataManager.shared.saveContext()
86 | })
87 | }
88 |
89 | private func copyToClipboard() {
90 | UIPasteboard.general.string = viewModel.substitutionResult
91 | copyButtonText = "Copied"
92 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
93 | copyButtonText = "Copy"
94 | }
95 | }
96 |
97 | // https://stackoverflow.com/questions/56491386/how-to-hide-keyboard-when-using-swiftui
98 | private var dismissKeyboardDesture: some Gesture {
99 | DragGesture().onChanged { _ in
100 | UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
101 | }
102 | }
103 |
104 | private var shareButton: some View {
105 | Button(action: {
106 | self.isSharePresented = true
107 | }) {
108 | Image(systemName: "square.and.arrow.up")
109 | .imageScale(.large)
110 | }
111 | .sheet(isPresented: $isSharePresented) {
112 | ActivityViewController(activityItems: [self.viewModel.regEx.description])
113 | }
114 | }
115 | }
116 |
117 | private func cheatSheetButton() -> some View {
118 | #if targetEnvironment(macCatalyst)
119 | ZStack {
120 | Image(systemName: "wand.and.stars")
121 | .imageScale(.large)
122 | .foregroundColor(.accentColor)
123 | NavigationLink(destination: CheatSheetView()) {
124 | EmptyView()
125 | }
126 | .opacity(0)
127 | }
128 | #else
129 | NavigationLink(destination: CheatSheetView()) {
130 | Image(systemName: "wand.and.stars")
131 | .imageScale(.large)
132 | .foregroundColor(.accentColor)
133 | }
134 | #endif
135 | }
136 |
137 | private struct RegExTextViewSection: View {
138 |
139 | @Binding var regEx: RegEx
140 | @State private var isOptionsVisible = false
141 |
142 | var body: some View {
143 | Section(header: Text("Regular Expression")) {
144 | #if targetEnvironment(macCatalyst)
145 |
146 | HStack {
147 | RegExTextView("Type RegEx here", text: $regEx.raw)
148 | .equatable()
149 | .padding(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 5))
150 |
151 | cheatSheetButton()
152 | .frame(width: 20)
153 | }
154 |
155 | #else
156 |
157 | RegExTextView("Type RegEx here", text: $regEx.raw)
158 | .padding(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 5))
159 |
160 | #endif
161 |
162 | Button(action: {
163 | isOptionsVisible.toggle()
164 | }) {
165 | HStack {
166 | Text("Options")
167 |
168 | if !isOptionsVisible {
169 | Spacer()
170 | VStack(alignment: .trailing) {
171 | if regEx.caseInsensitive {
172 | Text("Case Insensitive")
173 | }
174 | if regEx.allowCommentsAndWhitespace {
175 | Text("Allow Comments and Whitespace")
176 | }
177 | if regEx.ignoreMetacharacters {
178 | Text("Ignore Metacharacters")
179 | }
180 | if regEx.anchorsMatchLines {
181 | Text("Anchors Match Lines")
182 | }
183 | if regEx.dotMatchesLineSeparators {
184 | Text("Dot Matches Line Separators")
185 | }
186 | if regEx.useUnixLineSeparators {
187 | Text("Use Unix Line Separators")
188 | }
189 | if regEx.useUnicodeWordBoundaries {
190 | Text("Use Unicode Word Boundaries")
191 | }
192 | }
193 | .font(.footnote)
194 | }
195 | }
196 | .foregroundColor(isOptionsVisible ? .secondary : .accentColor)
197 | }
198 |
199 | if isOptionsVisible {
200 | Toggle("Case Insensitive", isOn: $regEx.caseInsensitive)
201 | Toggle("Allow Comments and Whitespace", isOn: $regEx.allowCommentsAndWhitespace)
202 | Toggle("Ignore Metacharacters", isOn: $regEx.ignoreMetacharacters)
203 | Toggle("Anchors Match Lines", isOn: $regEx.anchorsMatchLines)
204 | Toggle("Dot Matches Line Separators", isOn: $regEx.dotMatchesLineSeparators)
205 | Toggle("Use Unix Line Separators", isOn: $regEx.useUnixLineSeparators)
206 | Toggle("Use Unicode Word Boundaries", isOn: $regEx.useUnicodeWordBoundaries)
207 | }
208 | }
209 | }
210 | }
211 |
212 | private struct SampleFooterView: View {
213 | var count: Int
214 |
215 | private var matchesString: String {
216 | return self.count == 1 ? "1 match" : "\(count) matches"
217 | }
218 |
219 | var body: some View {
220 | Text(matchesString)
221 | }
222 | }
223 |
224 | private struct SampleHeaderView: View {
225 | var count: Int
226 |
227 | var body: some View {
228 | HStack {
229 | Text("Sample Text")
230 | if count > 0 {
231 | Spacer()
232 | Text(count == 1 ? "1 match" : "\(count) matches")
233 | .font(.footnote)
234 | .foregroundColor(Color.secondary)
235 | .padding(EdgeInsets(top: 1, leading: 6, bottom: 1, trailing: 6))
236 | .overlay(
237 | RoundedRectangle(cornerRadius: 20)
238 | .stroke(Color.secondary, lineWidth: 1)
239 | )
240 | }
241 | }
242 | .frame(minHeight: 20)
243 | }
244 | }
245 |
246 | private let kTextFieldPadding = EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 5)
247 |
248 | #if DEBUG
249 | struct EditorView_Previews: PreviewProvider {
250 | private static var regEx: RegEx = {
251 | var r: RegEx = RegEx(context: DataManager.shared.persistentContainer.viewContext)
252 | r.name = "Dollars"
253 | r.raw = #"\$?((\d+)\.?(\d\d)?)"#
254 | r.sample = "$100.00 12.50 $10"
255 | r.substitution = "$3"
256 | return r
257 | }()
258 |
259 | static var previews: some View {
260 | Group {
261 | NavigationView {
262 | EditorView(regEx: regEx)
263 | }
264 | .environment(\.sizeCategory, .extraLarge)
265 | .previewLayout(.device)
266 | .previewDevice("iPhone 11")
267 | NavigationView {
268 | EditorView(regEx: regEx)
269 | }
270 | .previewDevice("iPhone 11")
271 | .preferredColorScheme(.dark)
272 | .environment(\.sizeCategory, .large)
273 | }
274 | }
275 | }
276 | #endif
277 |
--------------------------------------------------------------------------------
/RegEx+/Editor/EditorViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RegExEditorViewModel.swift
3 | // RegEx+
4 | //
5 | // Created by Lex on 2020/4/25.
6 | // Copyright © 2020 Lex.sh. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Combine
11 |
12 |
13 | class EditorViewModel : ObservableObject, Equatable {
14 |
15 | @Published var regEx: RegEx
16 |
17 | @Published var matches = [NSTextCheckingResult]()
18 | @Published var substitutionResult = ""
19 |
20 | var matchCancellable: AnyCancellable?
21 | var substitutionCancellable: AnyCancellable?
22 |
23 | init(regEx: RegEx) {
24 | self.regEx = regEx
25 |
26 | let optionsObservable = $regEx
27 | .map(\.regularExpressionOptions)
28 |
29 | let regExObservable = $regEx
30 | .map(\.raw)
31 | .throttle(for: 0.2, scheduler: RunLoop.main, latest: true)
32 | .removeDuplicates()
33 | .combineLatest(optionsObservable)
34 | .compactMap { (raw, options) in
35 | try? NSRegularExpression(pattern: raw, options: options)
36 | }
37 |
38 | let sampleObservable = $regEx
39 | .map(\.sample)
40 | .throttle(for: 0.2, scheduler: RunLoop.main, latest: true)
41 | .removeDuplicates()
42 |
43 | let substitutionObservalbe = $regEx
44 | .map(\.substitution)
45 | .throttle(for: 0.2, scheduler: RunLoop.main, latest: true)
46 | .removeDuplicates()
47 |
48 | let subAndSampleObservable = substitutionObservalbe.combineLatest(sampleObservable)
49 | .map { ($0.0, $0.1) }
50 |
51 | matchCancellable = regExObservable
52 | .combineLatest(sampleObservable)
53 | .sink { [weak self] (reg: NSRegularExpression, sample: String) in
54 | let range = NSRange(location: 0, length: sample.count)
55 | self?.matches = reg.matches(in: sample, options: [], range: range)
56 | }
57 |
58 | substitutionCancellable = regExObservable
59 | .combineLatest(subAndSampleObservable)
60 | .map { ($0, $1.0, $1.1) }
61 | .sink { [weak self] (reg: NSRegularExpression, sub: String, sample: String) in
62 | let range = NSRange(location: 0, length: sample.count)
63 | self?.substitutionResult = reg.stringByReplacingMatches(
64 | in: sample,
65 | options: [],
66 | range: range,
67 | withTemplate: sub
68 | )
69 | }
70 | }
71 |
72 | func updateLastModified() {
73 | if regEx.hasChanges {
74 | regEx.updatedAt = Date()
75 | }
76 | }
77 |
78 | static func == (lhs: EditorViewModel, rhs: EditorViewModel) -> Bool {
79 | lhs.regEx.isEqual(to: rhs.regEx)
80 | && lhs.substitutionResult == rhs.substitutionResult
81 | && lhs.matches == rhs.matches
82 | }
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/RegEx+/HomeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TabView.swift
3 | // RegEx+
4 | //
5 | // Created by Lex on 2020/4/21.
6 | // Copyright © 2020 Lex.sh. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct HomeView: View {
12 | @Environment(\.managedObjectContext) var managedObjectContext
13 |
14 | var body: some View {
15 | return NavigationView {
16 | LibraryView()
17 | .environment(\.managedObjectContext, managedObjectContext)
18 | Text(verbatim: "RegEx+")
19 | .font(.largeTitle)
20 | }
21 | .environment(\.managedObjectContext, managedObjectContext)
22 | .currentDeviceNavigationViewStyle()
23 | }
24 | }
25 |
26 | private extension View {
27 | func currentDeviceNavigationViewStyle() -> AnyView {
28 | #if targetEnvironment(macCatalyst)
29 |
30 | return AnyView(
31 | navigationViewStyle(DoubleColumnNavigationViewStyle())
32 | )
33 |
34 | #else
35 |
36 | if UIDevice.current.userInterfaceIdiom == .pad {
37 | return AnyView(navigationViewStyle(DoubleColumnNavigationViewStyle()))
38 | } else {
39 | return AnyView(navigationViewStyle(DefaultNavigationViewStyle()))
40 | }
41 |
42 | #endif
43 | }
44 | }
45 |
46 | struct ContentView_Previews: PreviewProvider {
47 | static var previews: some View {
48 | HomeView()
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/RegEx+/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | RegEx+
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | ITSAppUsesNonExemptEncryption
24 |
25 | LSApplicationCategoryType
26 | public.app-category.developer-tools
27 | LSRequiresIPhoneOS
28 |
29 | UIApplicationSceneManifest
30 |
31 | UIApplicationSupportsMultipleScenes
32 |
33 | UISceneConfigurations
34 |
35 | UIWindowSceneSessionRoleApplication
36 |
37 |
38 | UISceneConfigurationName
39 | Default Configuration
40 | UISceneDelegateClassName
41 | $(PRODUCT_MODULE_NAME).SceneDelegate
42 |
43 |
44 |
45 |
46 | UIBackgroundModes
47 |
48 | remote-notification
49 |
50 | UILaunchStoryboardName
51 | LaunchScreen
52 | UIRequiredDeviceCapabilities
53 |
54 | armv7
55 |
56 | UIStatusBarTintParameters
57 |
58 | UINavigationBar
59 |
60 | Style
61 | UIBarStyleDefault
62 | Translucent
63 |
64 |
65 |
66 | UISupportedInterfaceOrientations
67 |
68 | UIInterfaceOrientationPortrait
69 | UIInterfaceOrientationLandscapeLeft
70 | UIInterfaceOrientationLandscapeRight
71 |
72 | UISupportedInterfaceOrientations~ipad
73 |
74 | UIInterfaceOrientationPortrait
75 | UIInterfaceOrientationPortraitUpsideDown
76 | UIInterfaceOrientationLandscapeLeft
77 | UIInterfaceOrientationLandscapeRight
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/RegEx+/Library/LibraryItemView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LibraryItemView.swift
3 | // RegEx+
4 | //
5 | // Created by Lex on 2020/5/3.
6 | // Copyright © 2020 Lex.sh. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import CoreData
11 |
12 |
13 | struct LibraryItemView: View, Equatable {
14 |
15 | @ObservedObject var regEx: RegEx
16 |
17 | var body: some View {
18 | NavigationLink {
19 | EditorView(regEx: regEx).equatable()
20 | } label: {
21 | VStack(alignment: .leading, spacing: 4) {
22 | Text(regEx.name)
23 | .font(.headline)
24 |
25 | if !regEx.raw.isEmpty {
26 | Text(regEx.raw)
27 | .font(.subheadline)
28 | .foregroundColor(.secondary)
29 | .lineLimit(1)
30 | }
31 | }
32 | .frame(minHeight: 50, maxHeight: 200)
33 | .paddingVertical()
34 | }
35 | .isDetailLink(true)
36 | }
37 |
38 | static func == (lhs: LibraryItemView, rhs: LibraryItemView) -> Bool {
39 | lhs.regEx.objectID == rhs.regEx.objectID
40 | && lhs.regEx.name == rhs.regEx.name
41 | && lhs.regEx.raw == rhs.regEx.raw
42 | }
43 | }
44 |
45 | private extension View {
46 |
47 | func paddingVertical() -> AnyView {
48 | #if targetEnvironment(macCatalyst)
49 | AnyView(padding(.vertical))
50 | #else
51 | AnyView(self)
52 | #endif
53 | }
54 |
55 | }
56 |
57 | struct LibraryItemView_Previews: PreviewProvider {
58 | private static var regEx: RegEx = {
59 | var r: RegEx = RegEx(context: DataManager.shared.persistentContainer.viewContext)
60 | r.name = "Dollars"
61 | r.raw = #"\$?((\d+)\.?(\d\d)?)"#
62 | r.sample = "$100.00 12.50 $10"
63 | r.substitution = "$3"
64 | return r
65 | }()
66 |
67 | static var previews: some View {
68 | NavigationView {
69 | List {
70 | LibraryItemView(regEx: regEx)
71 | LibraryItemView(regEx: regEx)
72 | LibraryItemView(regEx: regEx)
73 | }
74 | }
75 | .navigationTitle(Text(verbatim: "Test"))
76 | .navigationViewStyle(StackNavigationViewStyle())
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/RegEx+/Library/LibraryView+Data.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LibraryView+Data.swift
3 | // RegEx+
4 | //
5 | // Created by Lex on 2020/5/3.
6 | // Copyright © 2020 Lex.sh. All rights reserved.
7 | //
8 |
9 | import CoreData
10 |
11 |
12 | extension LibraryView {
13 |
14 | func deleteRegEx(indexSet: IndexSet) {
15 | let source = indexSet.first!
16 | let regEx = regExItems[source]
17 | managedObjectContext.delete(regEx)
18 |
19 | save()
20 | }
21 |
22 | func addRegEx(withSample: Bool) {
23 | let regEx = RegEx(context: managedObjectContext)
24 |
25 | if withSample, let randomItem = sampleData().randomElement() {
26 | regEx.name = randomItem.name
27 | regEx.raw = randomItem.raw
28 | regEx.sample = randomItem.sample
29 | regEx.allowCommentsAndWhitespace = randomItem.allowComments
30 | regEx.createdAt = Date()
31 | } else {
32 | regEx.name = NSLocalizedString("Untitled", comment: "Default item name")
33 | regEx.raw = ""
34 | regEx.createdAt = Date()
35 | }
36 |
37 | save()
38 | editMode = .inactive
39 | }
40 |
41 | private func save() {
42 | DataManager.shared.saveContext()
43 | }
44 |
45 | private func sampleData() -> [SampleItem] {
46 | return [
47 | SampleItem("Dollars", raw: #"(\$[\d]+)\.?(\d{2})?"#),
48 | SampleItem("Hex", raw: #"#?([a-f0-9]{6}|[a-f0-9]{3})"#, sample: "#336699\n#F2A\nFF9933"),
49 | SampleItem("Allow Comments", raw: #"(\$[\d]+) # Dollars symbol and digits"#, allowComments: true),
50 | 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})"#),
51 | SampleItem("Email", raw: #"([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})"#, sample: "ive@apple.com"),
52 | SampleItem("HTML tag", raw: #"(.*?)"#, sample: "iPhone\niPad"),
53 | ]
54 | }
55 |
56 | }
57 |
58 | private struct SampleItem {
59 | let name: String
60 | let raw: String
61 | let sample: String
62 | let allowComments: Bool
63 |
64 | init(_ name: String, raw: String, sample: String = "", allowComments: Bool = false) {
65 | self.name = name
66 | self.raw = raw
67 | self.sample = sample
68 | self.allowComments = allowComments
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/RegEx+/Library/LibraryView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LibraryView.swift
3 | // RegEx+
4 | //
5 | // Created by Lex on 2020/4/21.
6 | // Copyright © 2020 Lex.sh. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import CoreData
11 |
12 |
13 | struct LibraryView: View, Equatable {
14 |
15 | static func == (lhs: LibraryView, rhs: LibraryView) -> Bool {
16 | lhs.regExItems.map(\.objectID).hashValue == rhs.regExItems.map(\.objectID).hashValue
17 | }
18 |
19 | @Environment(\.managedObjectContext) var managedObjectContext
20 | @FetchRequest(fetchRequest: RegEx.fetchAllRegEx()) var regExItems: FetchedResults
21 |
22 | @State private var searchTerm = ""
23 | @State var editMode = EditMode.inactive
24 |
25 | var body: some View {
26 | VStack(alignment: .leading) {
27 | SearchView(text: $searchTerm)
28 | .padding(.horizontal)
29 |
30 | if regExItems.isEmpty {
31 | VStack(alignment: .center) {
32 | Text("Your RegEx+ library is empty")
33 | .font(.subheadline)
34 | .foregroundStyle(.secondary)
35 | Button {
36 | addRegEx(withSample: true)
37 | } label: {
38 | Text("Create a sample")
39 | }
40 | .buttonStyle(.bordered)
41 | }
42 | .frame(maxWidth: .greatestFiniteMagnitude, maxHeight: .greatestFiniteMagnitude)
43 | } else {
44 | List {
45 | ForEach(regExItems.filter(filterByTerm), id: \.objectID) {
46 | LibraryItemView(regEx: $0).equatable()
47 | }
48 | .onDelete(perform: deleteRegEx)
49 | }
50 | .currentDeviceListStyle()
51 | .environment(\.editMode, $editMode)
52 | }
53 | }
54 | .navigationTitle("RegEx+")
55 | .setNavigationItems(libraryView: self)
56 | }
57 |
58 | private func filterByTerm(_ item: FetchedResults.Element) -> Bool {
59 | if searchTerm.isEmpty {
60 | return true
61 | }
62 |
63 | return item.name.lowercased().contains(searchTerm.lowercased())
64 | || item.raw.lowercased().contains(searchTerm.lowercased())
65 | }
66 |
67 | var editButton: some View {
68 | Button(action: {
69 | editMode = editMode.isEditing ? .inactive : .active
70 | }, label: {
71 | Text(editMode.isEditing ? "Done" : "Edit")
72 | })
73 | }
74 |
75 | var aboutButton: some View {
76 | NavigationLink(destination: AboutView()) {
77 | Image(systemName: "info.circle")
78 | .imageScale(.large)
79 | }
80 | }
81 |
82 | var addButton: some View {
83 | Button {
84 | addRegEx(withSample: false)
85 | } label: {
86 | Image(systemName: "plus.circle.fill")
87 | .imageScale(.large)
88 | }
89 | }
90 | }
91 |
92 | private extension View {
93 | func currentDeviceListStyle() -> AnyView {
94 | #if targetEnvironment(macCatalyst)
95 | return AnyView(listStyle(PlainListStyle()).padding(.horizontal))
96 | #else
97 | if #available(iOS 14.0, *) {
98 | return AnyView(listStyle(InsetGroupedListStyle()))
99 | } else {
100 | return AnyView(listStyle(GroupedListStyle()))
101 | }
102 | #endif
103 | }
104 | }
105 |
106 | private extension View {
107 | func setNavigationItems(libraryView: LibraryView) -> AnyView {
108 | #if targetEnvironment(macCatalyst)
109 | AnyView(toolbar {
110 | ToolbarItem(placement: .topBarLeading) {
111 | libraryView.editButton
112 | }
113 | ToolbarItem(placement: .topBarTrailing) {
114 | HStack {
115 | libraryView.aboutButton
116 | libraryView.addButton
117 | }
118 | }
119 | })
120 | #else
121 | AnyView(toolbar {
122 | ToolbarItem(placement: .topBarLeading) {
123 | libraryView.editButton
124 | }
125 | ToolbarItem(placement: .topBarTrailing) {
126 | libraryView.aboutButton
127 | }
128 | ToolbarItem(placement: .topBarTrailing) {
129 | libraryView.addButton.padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 0))
130 | }
131 | })
132 | #endif
133 | }
134 | }
135 |
136 | #if DEBUG
137 | struct LibraryView_Previews: PreviewProvider {
138 | static var previews: some View {
139 | NavigationView {
140 | LibraryView()
141 | .environment(\.managedObjectContext, DataManager.shared.persistentContainer.viewContext)
142 | }
143 | }
144 | }
145 | #endif
146 |
--------------------------------------------------------------------------------
/RegEx+/Localizable.xcstrings:
--------------------------------------------------------------------------------
1 | {
2 | "sourceLanguage" : "en",
3 | "strings" : {
4 | "%@" : {
5 | "localizations" : {
6 | "zh-Hans" : {
7 | "stringUnit" : {
8 | "state" : "translated",
9 | "value" : "%@"
10 | }
11 | },
12 | "zh-Hant" : {
13 | "stringUnit" : {
14 | "state" : "translated",
15 | "value" : "%@"
16 | }
17 | }
18 | }
19 | },
20 | "%lld matches" : {
21 | "localizations" : {
22 | "zh-Hans" : {
23 | "stringUnit" : {
24 | "state" : "translated",
25 | "value" : "%d 次匹配"
26 | }
27 | },
28 | "zh-Hant" : {
29 | "stringUnit" : {
30 | "state" : "translated",
31 | "value" : "%d 次匹配"
32 | }
33 | }
34 | }
35 | },
36 | "1 match" : {
37 | "localizations" : {
38 | "zh-Hans" : {
39 | "stringUnit" : {
40 | "state" : "translated",
41 | "value" : "一次匹配"
42 | }
43 | },
44 | "zh-Hant" : {
45 | "stringUnit" : {
46 | "state" : "translated",
47 | "value" : "一次匹配"
48 | }
49 | }
50 | }
51 | },
52 | "Allow Comments and Whitespace" : {
53 | "localizations" : {
54 | "zh-Hans" : {
55 | "stringUnit" : {
56 | "state" : "translated",
57 | "value" : "允许注释和空格"
58 | }
59 | },
60 | "zh-Hant" : {
61 | "stringUnit" : {
62 | "state" : "translated",
63 | "value" : "允許註釋和空格"
64 | }
65 | }
66 | }
67 | },
68 | "Anchors Match Lines" : {
69 | "localizations" : {
70 | "zh-Hans" : {
71 | "stringUnit" : {
72 | "state" : "translated",
73 | "value" : "锚点匹配行"
74 | }
75 | },
76 | "zh-Hant" : {
77 | "stringUnit" : {
78 | "state" : "translated",
79 | "value" : "錨點匹配行"
80 | }
81 | }
82 | }
83 | },
84 | "Case Insensitive" : {
85 | "localizations" : {
86 | "zh-Hans" : {
87 | "stringUnit" : {
88 | "state" : "translated",
89 | "value" : "不区分大小写"
90 | }
91 | },
92 | "zh-Hant" : {
93 | "stringUnit" : {
94 | "state" : "translated",
95 | "value" : "不區分大小寫"
96 | }
97 | }
98 | }
99 | },
100 | "Cheat Sheet" : {
101 | "localizations" : {
102 | "zh-Hans" : {
103 | "stringUnit" : {
104 | "state" : "translated",
105 | "value" : "小抄"
106 | }
107 | },
108 | "zh-Hant" : {
109 | "stringUnit" : {
110 | "state" : "translated",
111 | "value" : "小抄"
112 | }
113 | }
114 | }
115 | },
116 | "Create a sample" : {
117 | "localizations" : {
118 | "zh-Hans" : {
119 | "stringUnit" : {
120 | "state" : "translated",
121 | "value" : "新建样例"
122 | }
123 | },
124 | "zh-Hant" : {
125 | "stringUnit" : {
126 | "state" : "translated",
127 | "value" : "新建樣例"
128 | }
129 | }
130 | }
131 | },
132 | "Description:" : {
133 | "localizations" : {
134 | "zh-Hans" : {
135 | "stringUnit" : {
136 | "state" : "translated",
137 | "value" : "描述:"
138 | }
139 | },
140 | "zh-Hant" : {
141 | "stringUnit" : {
142 | "state" : "translated",
143 | "value" : "描述:"
144 | }
145 | }
146 | }
147 | },
148 | "Done" : {
149 | "localizations" : {
150 | "zh-Hans" : {
151 | "stringUnit" : {
152 | "state" : "translated",
153 | "value" : "完成"
154 | }
155 | },
156 | "zh-Hant" : {
157 | "stringUnit" : {
158 | "state" : "translated",
159 | "value" : "完成"
160 | }
161 | }
162 | }
163 | },
164 | "Dot Matches Line Separators" : {
165 | "localizations" : {
166 | "zh-Hans" : {
167 | "stringUnit" : {
168 | "state" : "translated",
169 | "value" : "点符号匹配行分隔符"
170 | }
171 | },
172 | "zh-Hant" : {
173 | "stringUnit" : {
174 | "state" : "translated",
175 | "value" : "點符號匹配行分隔符"
176 | }
177 | }
178 | }
179 | },
180 | "Edit" : {
181 | "localizations" : {
182 | "zh-Hans" : {
183 | "stringUnit" : {
184 | "state" : "translated",
185 | "value" : "编辑"
186 | }
187 | },
188 | "zh-Hant" : {
189 | "stringUnit" : {
190 | "state" : "translated",
191 | "value" : "編輯"
192 | }
193 | }
194 | }
195 | },
196 | "Ignore Metacharacters" : {
197 | "localizations" : {
198 | "zh-Hans" : {
199 | "stringUnit" : {
200 | "state" : "translated",
201 | "value" : "忽略元字符"
202 | }
203 | },
204 | "zh-Hant" : {
205 | "stringUnit" : {
206 | "state" : "translated",
207 | "value" : "忽略元字符"
208 | }
209 | }
210 | }
211 | },
212 | "Metacharacters" : {
213 | "localizations" : {
214 | "zh-Hans" : {
215 | "stringUnit" : {
216 | "state" : "translated",
217 | "value" : "元字符"
218 | }
219 | },
220 | "zh-Hant" : {
221 | "stringUnit" : {
222 | "state" : "translated",
223 | "value" : "元字符"
224 | }
225 | }
226 | }
227 | },
228 | "Name" : {
229 | "localizations" : {
230 | "zh-Hans" : {
231 | "stringUnit" : {
232 | "state" : "translated",
233 | "value" : "名字"
234 | }
235 | },
236 | "zh-Hant" : {
237 | "stringUnit" : {
238 | "state" : "translated",
239 | "value" : "名字"
240 | }
241 | }
242 | }
243 | },
244 | "Operators" : {
245 | "localizations" : {
246 | "zh-Hans" : {
247 | "stringUnit" : {
248 | "state" : "translated",
249 | "value" : "操作符"
250 | }
251 | },
252 | "zh-Hant" : {
253 | "stringUnit" : {
254 | "state" : "translated",
255 | "value" : "操作符"
256 | }
257 | }
258 | }
259 | },
260 | "Options" : {
261 | "localizations" : {
262 | "zh-Hans" : {
263 | "stringUnit" : {
264 | "state" : "translated",
265 | "value" : "选项"
266 | }
267 | },
268 | "zh-Hant" : {
269 | "stringUnit" : {
270 | "state" : "translated",
271 | "value" : "選項"
272 | }
273 | }
274 | }
275 | },
276 | "Price: $$$1\\.$2\\n" : {
277 | "localizations" : {
278 | "zh-Hans" : {
279 | "stringUnit" : {
280 | "state" : "translated",
281 | "value" : "价格: $$$1\\.$2\\n"
282 | }
283 | },
284 | "zh-Hant" : {
285 | "stringUnit" : {
286 | "state" : "translated",
287 | "value" : "價格: $$$1\\.$2\\n"
288 | }
289 | }
290 | }
291 | },
292 | "Rate RegEx+" : {
293 | "localizations" : {
294 | "zh-Hans" : {
295 | "stringUnit" : {
296 | "state" : "translated",
297 | "value" : "评价 RegEx+"
298 | }
299 | },
300 | "zh-Hant" : {
301 | "stringUnit" : {
302 | "state" : "translated",
303 | "value" : "評價 RegEx+"
304 | }
305 | }
306 | }
307 | },
308 | "RegEx+" : {
309 | "localizations" : {
310 | "zh-Hans" : {
311 | "stringUnit" : {
312 | "state" : "translated",
313 | "value" : "RegEx+"
314 | }
315 | },
316 | "zh-Hant" : {
317 | "stringUnit" : {
318 | "state" : "translated",
319 | "value" : "RegEx+"
320 | }
321 | }
322 | }
323 | },
324 | "Regular Expression" : {
325 | "localizations" : {
326 | "zh-Hans" : {
327 | "stringUnit" : {
328 | "state" : "translated",
329 | "value" : "正则表达式"
330 | }
331 | },
332 | "zh-Hant" : {
333 | "stringUnit" : {
334 | "state" : "translated",
335 | "value" : "正則表達式"
336 | }
337 | }
338 | }
339 | },
340 | "Sample Text" : {
341 | "localizations" : {
342 | "zh-Hans" : {
343 | "stringUnit" : {
344 | "state" : "translated",
345 | "value" : "示例文字"
346 | }
347 | },
348 | "zh-Hant" : {
349 | "stringUnit" : {
350 | "state" : "translated",
351 | "value" : "示例文字"
352 | }
353 | }
354 | }
355 | },
356 | "Search..." : {
357 | "localizations" : {
358 | "zh-Hans" : {
359 | "stringUnit" : {
360 | "state" : "translated",
361 | "value" : "搜索..."
362 | }
363 | },
364 | "zh-Hant" : {
365 | "stringUnit" : {
366 | "state" : "translated",
367 | "value" : "搜索..."
368 | }
369 | }
370 | }
371 | },
372 | "Substitution Result" : {
373 | "localizations" : {
374 | "zh-Hans" : {
375 | "stringUnit" : {
376 | "state" : "translated",
377 | "value" : "替换结果"
378 | }
379 | },
380 | "zh-Hant" : {
381 | "stringUnit" : {
382 | "state" : "translated",
383 | "value" : "替換結果"
384 | }
385 | }
386 | }
387 | },
388 | "Substitution Template" : {
389 | "localizations" : {
390 | "zh-Hans" : {
391 | "stringUnit" : {
392 | "state" : "translated",
393 | "value" : "替换模板"
394 | }
395 | },
396 | "zh-Hant" : {
397 | "stringUnit" : {
398 | "state" : "translated",
399 | "value" : "替換模板"
400 | }
401 | }
402 | }
403 | },
404 | "Untitled" : {
405 | "comment" : "Default item name",
406 | "localizations" : {
407 | "zh-Hans" : {
408 | "stringUnit" : {
409 | "state" : "translated",
410 | "value" : "未命名"
411 | }
412 | },
413 | "zh-Hant" : {
414 | "stringUnit" : {
415 | "state" : "translated",
416 | "value" : "未命名"
417 | }
418 | }
419 | }
420 | },
421 | "Use Unicode Word Boundaries" : {
422 | "localizations" : {
423 | "zh-Hans" : {
424 | "stringUnit" : {
425 | "state" : "translated",
426 | "value" : "使用 Unicode 字符边界"
427 | }
428 | },
429 | "zh-Hant" : {
430 | "stringUnit" : {
431 | "state" : "translated",
432 | "value" : "使用 Unicode 字符邊界"
433 | }
434 | }
435 | }
436 | },
437 | "Use Unix Line Separators" : {
438 | "localizations" : {
439 | "zh-Hans" : {
440 | "stringUnit" : {
441 | "state" : "translated",
442 | "value" : "使用 Unix 换行符"
443 | }
444 | },
445 | "zh-Hant" : {
446 | "stringUnit" : {
447 | "state" : "translated",
448 | "value" : "使用 Unix 換行符"
449 | }
450 | }
451 | }
452 | },
453 | "Write a review" : {
454 | "localizations" : {
455 | "zh-Hans" : {
456 | "stringUnit" : {
457 | "state" : "translated",
458 | "value" : "写评论"
459 | }
460 | },
461 | "zh-Hant" : {
462 | "stringUnit" : {
463 | "state" : "translated",
464 | "value" : "寫評論"
465 | }
466 | }
467 | }
468 | },
469 | "Your RegEx+ library is empty" : {
470 | "localizations" : {
471 | "zh-Hans" : {
472 | "stringUnit" : {
473 | "state" : "translated",
474 | "value" : "你的 RegEx+ 仓库是空的"
475 | }
476 | },
477 | "zh-Hant" : {
478 | "stringUnit" : {
479 | "state" : "translated",
480 | "value" : "你的 RegEx+ 倉庫是空的"
481 | }
482 | }
483 | }
484 | }
485 | },
486 | "version" : "1.0"
487 | }
488 |
--------------------------------------------------------------------------------
/RegEx+/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/RegEx+/RegEx+.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | aps-environment
6 | development
7 | com.apple.developer.icloud-container-identifiers
8 |
9 | iCloud.RegExCatalyst
10 |
11 | com.apple.developer.icloud-services
12 |
13 | CloudKit
14 |
15 | com.apple.security.app-sandbox
16 |
17 | com.apple.security.network.client
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/RegEx+/RegEx.xcdatamodeld/.xccurrentversion:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | _XCCurrentVersionName
6 | RegEx.xcdatamodel
7 |
8 |
9 |
--------------------------------------------------------------------------------
/RegEx+/RegEx.xcdatamodeld/RegEx.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/RegEx+/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // RegEx+
4 | //
5 | // Created by Lex on 2020/4/21.
6 | // Copyright © 2020 Lex.sh. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SwiftUI
11 |
12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
13 |
14 | var window: UIWindow?
15 |
16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
20 |
21 | // Create the SwiftUI view that provides the window contents.
22 | let viewContext = DataManager.shared.persistentContainer.viewContext
23 |
24 | let contentView = HomeView()
25 | .environment(\.managedObjectContext, viewContext)
26 |
27 | // Use a UIHostingController as window root view controller.
28 | if let windowScene = scene as? UIWindowScene {
29 | let window = UIWindow(windowScene: windowScene)
30 | window.rootViewController = UIHostingController(rootView: contentView)
31 | self.window = window
32 | window.makeKeyAndVisible()
33 |
34 | #if targetEnvironment(macCatalyst)
35 | if let titlebar = windowScene.titlebar {
36 | titlebar.titleVisibility = .visible
37 | titlebar.toolbarStyle = .unified
38 | }
39 | #endif
40 | }
41 | }
42 |
43 | func sceneDidDisconnect(_ scene: UIScene) {
44 | // Called as the scene is being released by the system.
45 | // This occurs shortly after the scene enters the background, or when its session is discarded.
46 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
47 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
48 | }
49 |
50 | func sceneDidBecomeActive(_ scene: UIScene) {
51 | // Called when the scene has moved from an inactive state to an active state.
52 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
53 | }
54 |
55 | func sceneWillResignActive(_ scene: UIScene) {
56 | // Called when the scene will move from an active state to an inactive state.
57 | // This may occur due to temporary interruptions (ex. an incoming phone call).
58 | }
59 |
60 | func sceneWillEnterForeground(_ scene: UIScene) {
61 | // Called as the scene transitions from the background to the foreground.
62 | // Use this method to undo the changes made on entering the background.
63 | }
64 |
65 | func sceneDidEnterBackground(_ scene: UIScene) {
66 | // Called as the scene transitions from the foreground to the background.
67 | // Use this method to save data, release shared resources, and store enough scene-specific state information
68 | // to restore the scene back to its current state.
69 | }
70 |
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/RegEx+/Views/ActivityViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ActivityViewController.swift
3 | // RegEx+
4 | //
5 | // Created by Lex on 2020/5/16.
6 | // Copyright © 2020 Lex.sh. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SwiftUI
11 |
12 | struct ActivityViewController: UIViewControllerRepresentable {
13 |
14 | var activityItems: [Any]
15 | var applicationActivities: [UIActivity]?
16 |
17 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIActivityViewController {
18 | let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
19 | return controller
20 | }
21 |
22 | func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext) {}
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/RegEx+/Views/RegExSyntaxView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RegExSyntaxView.swift
3 | // RegExPro
4 | //
5 | // Created by Lex on 2020/4/23.
6 | // Copyright © 2020 Lex.sh. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import Combine
11 | import UIKit
12 |
13 |
14 | private struct UITextViewWrapper: UIViewRepresentable {
15 | typealias UIViewType = UITextView
16 |
17 | @Binding var text: String
18 | @Binding var calculatedHeight: CGFloat
19 | var onDone: (() -> Void)?
20 |
21 | func makeUIView(context: UIViewRepresentableContext) -> UITextView {
22 | let tv = UITextView()
23 | tv.delegate = context.coordinator
24 |
25 | tv.isEditable = true
26 | tv.font = UIFont.preferredFont(forTextStyle: .body)
27 | tv.isSelectable = true
28 | tv.isUserInteractionEnabled = true
29 | tv.isScrollEnabled = false
30 | tv.backgroundColor = UIColor.clear
31 | tv.textContainerInset = .zero
32 | tv.textContainer.lineFragmentPadding = 0
33 | if nil != onDone {
34 | tv.returnKeyType = .done
35 | }
36 | tv.textStorage.delegate = syntaxHighlighter
37 |
38 | tv.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
39 | return tv
40 | }
41 |
42 | func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext) {
43 | if uiView.text != self.text {
44 | uiView.text = self.text
45 | }
46 | if uiView.window != nil, !uiView.isFirstResponder {
47 | uiView.becomeFirstResponder()
48 | }
49 | UITextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight)
50 | }
51 |
52 | fileprivate static func recalculateHeight(view: UIView, result: Binding) {
53 | let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
54 | if result.wrappedValue != newSize.height {
55 | DispatchQueue.main.async {
56 | result.wrappedValue = newSize.height // !! must be called asynchronously
57 | }
58 | }
59 | }
60 |
61 | func makeCoordinator() -> Coordinator {
62 | return Coordinator(text: $text, height: $calculatedHeight, onDone: onDone)
63 | }
64 |
65 | final class Coordinator: NSObject, UITextViewDelegate {
66 | var text: Binding
67 | var calculatedHeight: Binding
68 | var onDone: (() -> Void)?
69 |
70 | init(text: Binding, height: Binding, onDone: (() -> Void)? = nil) {
71 | self.text = text
72 | self.calculatedHeight = height
73 | self.onDone = onDone
74 | }
75 |
76 | func textViewDidChange(_ uiView: UITextView) {
77 | text.wrappedValue = uiView.text
78 | UITextViewWrapper.recalculateHeight(view: uiView, result: calculatedHeight)
79 | }
80 |
81 | func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
82 | if let onDone = self.onDone, text == "\n" {
83 | textView.resignFirstResponder()
84 | onDone()
85 | return false
86 | }
87 | return true
88 | }
89 | }
90 |
91 | private let syntaxHighlighter = RegExSyntaxHighlighter()
92 | }
93 |
94 | struct RegExTextView: View {
95 |
96 | private var placeholder: String
97 | private var onCommit: (() -> Void)?
98 |
99 | @Binding private var text: String
100 | private var internalText: Binding {
101 | Binding(get: { self.text }) {
102 | self.text = $0
103 | self.showingPlaceholder = $0.isEmpty
104 | }
105 | }
106 |
107 | @State private var dynamicHeight: CGFloat = 100
108 | @State private var showingPlaceholder = false
109 |
110 | init (_ placeholder: String = "", text: Binding, onCommit: (() -> Void)? = nil) {
111 | self.placeholder = placeholder
112 | self.onCommit = onCommit
113 | self._text = text
114 | self._showingPlaceholder = State(initialValue: self.text.isEmpty)
115 | }
116 |
117 | var body: some View {
118 | UITextViewWrapper(text: self.internalText, calculatedHeight: $dynamicHeight, onDone: onCommit)
119 | .frame(minHeight: dynamicHeight, maxHeight: dynamicHeight)
120 | .background(placeholderView, alignment: .topLeading)
121 | }
122 |
123 | var placeholderView: some View {
124 | Group {
125 | if showingPlaceholder {
126 | Text(placeholder).foregroundColor(.gray)
127 | .padding(.leading, 4)
128 | .padding(.top, 8)
129 | }
130 | }
131 | }
132 | }
133 |
134 | #if DEBUG
135 | struct MultilineTextField_Previews: PreviewProvider {
136 | static var test: String = ""
137 | static var testBinding = Binding(get: { test }, set: { test = $0 })
138 |
139 | static var previews: some View {
140 | VStack(alignment: .leading) {
141 | Text("Description:")
142 | RegExTextView("Enter some text here", text: testBinding, onCommit: {
143 | print("Final text: \(test)")
144 | })
145 | .overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.black))
146 | Text("Something static here...")
147 | Spacer()
148 | }
149 | .padding()
150 | }
151 | }
152 | #endif
153 |
154 | class RegExSyntaxHighlighter: NSObject, NSTextStorageDelegate {
155 | var fontSize: CGFloat = 16
156 |
157 | func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int) {
158 |
159 | textStorage.addAttributes([
160 | .font: UIFont.systemFont(ofSize: fontSize),
161 | .foregroundColor: UIColor.black
162 | ], range: NSRange(location: 0, length: textStorage.length))
163 |
164 | textStorage.string.ranges(of: #"\\[$$\w]"#, options: .regularExpression).forEach { range in
165 | textStorage.addAttributes([
166 | .foregroundColor: UIColor.red
167 | ], range: textStorage.string.nsRange(from: range))
168 | }
169 |
170 | textStorage.string.ranges(of: #"[\(\)]"#, options: .regularExpression).forEach { range in
171 | textStorage.addAttributes([
172 | .foregroundColor: UIColor(red: 0, green: 0.5, blue: 0.2, alpha: 1)
173 | ], range: textStorage.string.nsRange(from: range))
174 | }
175 |
176 | textStorage.string.ranges(of: #"(?:\{)[\d,]+(?:\})"#, options: .regularExpression).forEach { range in
177 | textStorage.addAttributes([
178 | .font: UIFont.boldSystemFont(ofSize: fontSize),
179 | .foregroundColor: UIColor(red: 0, green: 0.3, blue: 0, alpha: 1)
180 | ], range: textStorage.string.nsRange(from: range))
181 | }
182 |
183 | textStorage.string.ranges(of: #"[\?\*\.]"#, options: .regularExpression).forEach { range in
184 | textStorage.addAttributes([
185 | .font: UIFont.boldSystemFont(ofSize: fontSize),
186 | .foregroundColor: UIColor(red: 0, green: 0.3, blue: 0, alpha: 1)
187 | ], range: textStorage.string.nsRange(from: range))
188 | }
189 |
190 | textStorage.string.ranges(of: #"[\^\[\$\]]"#, options: .regularExpression).forEach { range in
191 | textStorage.addAttributes([
192 | .foregroundColor: UIColor(red: 0, green: 0, blue: 0.8, alpha: 1)
193 | ], range: textStorage.string.nsRange(from: range))
194 | }
195 |
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/RegEx+/Views/RegExTextView/MatchesTextView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MatchesTextView.swift
3 | // RegEx+
4 | //
5 | // Created by Lex on 2020/5/2.
6 | // Copyright © 2020 Lex.sh. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SwiftUI
11 |
12 |
13 | private struct UITextViewWrapper: UIViewRepresentable {
14 | typealias UIViewType = UITextView
15 |
16 | @Binding var text: String
17 | @Binding var calculatedHeight: CGFloat
18 | var matches: [NSTextCheckingResult]
19 | var onDone: (() -> Void)?
20 |
21 | func makeUIView(context: UIViewRepresentableContext) -> UITextView {
22 | let tv = UITextView()
23 | tv.delegate = context.coordinator
24 |
25 | tv.isEditable = true
26 | tv.font = UIFont.preferredFont(forTextStyle: .body)
27 | tv.isSelectable = true
28 | tv.isUserInteractionEnabled = true
29 | tv.isScrollEnabled = false
30 | tv.backgroundColor = UIColor.clear
31 |
32 | tv.textContainerInset = .zero
33 | tv.textContainer.lineFragmentPadding = 0
34 | if nil != onDone {
35 | tv.returnKeyType = .done
36 | }
37 |
38 | tv.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
39 | return tv
40 | }
41 |
42 | func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext) {
43 | if uiView.attributedText.string != text {
44 | uiView.attributedText = NSAttributedString(string: text)
45 | }
46 |
47 | if !text.isEmpty {
48 | uiView.textStorage.setAttributes([
49 | .font: UIFont.preferredFont(forTextStyle: .body),
50 | .foregroundColor: UIColor.label
51 | ], range: NSRange(location: 0, length: uiView.text.count))
52 |
53 | matches.forEach { result in
54 | for index in 0.. uiView.attributedText.length {
57 | return
58 | }
59 | uiView.textStorage.setAttributes([
60 | .font: UIFont.preferredFont(forTextStyle: .body),
61 | .foregroundColor: UIColor.systemBlue
62 | ], range: range)
63 | }
64 | }
65 | }
66 |
67 | UITextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight)
68 | }
69 |
70 | fileprivate static func recalculateHeight(view: UIView, result: Binding) {
71 | let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
72 | if result.wrappedValue != newSize.height {
73 | DispatchQueue.main.async {
74 | result.wrappedValue = newSize.height // !! must be called asynchronously
75 | }
76 | }
77 | }
78 |
79 | func makeCoordinator() -> Coordinator {
80 | return Coordinator(text: $text, height: $calculatedHeight, onDone: onDone)
81 | }
82 |
83 | final class Coordinator: NSObject, UITextViewDelegate {
84 | var text: Binding
85 | var calculatedHeight: Binding
86 | var onDone: (() -> Void)?
87 |
88 | init(text: Binding, height: Binding, onDone: (() -> Void)? = nil) {
89 | self.text = text
90 | self.calculatedHeight = height
91 | self.onDone = onDone
92 | }
93 |
94 | func textViewDidChange(_ uiView: UITextView) {
95 | text.wrappedValue = uiView.text
96 | UITextViewWrapper.recalculateHeight(view: uiView, result: calculatedHeight)
97 | }
98 |
99 | func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
100 | if let onDone, text == "\n" {
101 | textView.resignFirstResponder()
102 | onDone()
103 | return false
104 | }
105 | return true
106 | }
107 | }
108 |
109 | }
110 |
111 | struct MatchesTextView: View, Equatable {
112 |
113 | static func == (lhs: MatchesTextView, rhs: MatchesTextView) -> Bool {
114 | lhs.text == rhs.text
115 | && lhs.matches == rhs.matches
116 | }
117 |
118 | private var placeholder: String
119 | private var onCommit: (() -> Void)?
120 |
121 | @Binding private var text: String
122 | private var internalText: Binding {
123 | Binding(get: { self.text }) {
124 | self.text = $0
125 | self.showingPlaceholder = $0.isEmpty
126 | }
127 | }
128 |
129 | @Binding private var matches: [NSTextCheckingResult]
130 | @State private var dynamicHeight: CGFloat = 100
131 | @State private var showingPlaceholder = false
132 |
133 | init (_ placeholder: String = "", text: Binding, matches: Binding<[NSTextCheckingResult]>, onCommit: (() -> Void)? = nil) {
134 | self.placeholder = placeholder
135 | self.onCommit = onCommit
136 | _matches = matches
137 | _text = text
138 | _showingPlaceholder = State(initialValue: self.text.isEmpty)
139 | }
140 |
141 | var body: some View {
142 | UITextViewWrapper(
143 | text: internalText,
144 | calculatedHeight: $dynamicHeight,
145 | matches: matches,
146 | onDone: onCommit
147 | )
148 | .frame(minHeight: dynamicHeight, maxHeight: dynamicHeight)
149 | .background(placeholderView, alignment: .topLeading)
150 | }
151 |
152 | var placeholderView: some View {
153 | Group {
154 | if showingPlaceholder {
155 | VStack {
156 | Text(placeholder)
157 | .foregroundColor(.secondary)
158 | }
159 | }
160 | }
161 | }
162 |
163 | }
164 |
165 | #if DEBUG
166 | struct MatchesTextView_Previews: PreviewProvider {
167 | static var test = "^(\\d+)\\.(\\d{2}) (\\d+)\\.(\\d{2}) (\\d+)\\.(\\d{2}) (\\d+)\\.(\\d{2})"
168 | static var testBinding = Binding(get: { test }, set: { test = $0 })
169 | static var matches = [NSTextCheckingResult]()
170 | static var matchesBinding = Binding<[NSTextCheckingResult]>(get: { matches }, set: { matches = $0 })
171 |
172 | static var previews: some View {
173 | VStack(alignment: .leading) {
174 | Text("Description:")
175 | MatchesTextView("Enter some text here",
176 | text: testBinding,
177 | matches: matchesBinding) {
178 | print("Final text: \(test)")
179 | }
180 | .overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.black))
181 | Spacer()
182 | }
183 | .padding()
184 | }
185 | }
186 | #endif
187 |
--------------------------------------------------------------------------------
/RegEx+/Views/RegExTextView/RegExSyntaxHighlighter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RegExSyntaxHighlighter.swift
3 | // RegEx+
4 | //
5 | // Created by Lex on 2020/5/2.
6 | // Copyright © 2020 Lex.sh. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SwiftUI
11 |
12 |
13 | fileprivate extension NSMutableAttributedString {
14 |
15 | func highlight(range: Range, color: UIColor) {
16 | let nsRange = NSRange(range, in: string)
17 | removeAttribute(.foregroundColor, range: nsRange)
18 | addAttributes([
19 | .foregroundColor: color
20 | ], range: nsRange)
21 | }
22 |
23 | }
24 |
25 | class RegExSyntaxHighlighter: NSObject, NSTextStorageDelegate {
26 |
27 | weak var textStorage: NSTextStorage?
28 |
29 | private var contentHash: Int = 0
30 |
31 | func highlightRegularExpression() {
32 | guard let ts = textStorage else {
33 | return
34 | }
35 |
36 | let str = NSMutableAttributedString(attributedString: ts)
37 |
38 | let regColorMap: [String: UIColor] = [
39 | #"[\?\*\.\+]"# : UIColor.systemGreen,
40 | #"(?:\{)[\d\w,]+(?:\})"# : UIColor.systemPurple,
41 | #"[\^\[\$\]]"# : UIColor.systemTeal,
42 | #"\\[$$\w\.\u0023]"# : UIColor.systemOrange,
43 | #"\s\u0023\s?[^\r\n]+[\r\n]*"# : UIColor.systemGray,
44 | #"[\(\)]"# : UIColor.systemPink,
45 | ]
46 |
47 | let r = NSRange(location: 0, length: str.length)
48 | str.removeAttribute(.foregroundColor, range: r)
49 | str.addAttribute(.foregroundColor, value: UIColor.label, range: r)
50 |
51 | var colorMap = [Range: UIColor]()
52 |
53 | regColorMap.forEach { regMap in
54 | str.string
55 | .ranges(of: regMap.key, options: .regularExpression)
56 | .forEach { range in
57 | colorMap[range] = regMap.value
58 | }
59 | }
60 |
61 | colorMap.forEach(str.highlight)
62 |
63 | ts.setAttributedString(str)
64 | }
65 |
66 | func textStorage(
67 | _ textStorage: NSTextStorage,
68 | didProcessEditing editedMask: NSTextStorage.EditActions,
69 | range editedRange: NSRange,
70 | changeInLength delta: Int
71 | ) {
72 | self.textStorage = textStorage
73 |
74 | let hash = textStorage.string.hash
75 | if hash != contentHash {
76 | highlightRegularExpression()
77 | contentHash = hash
78 | }
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/RegEx+/Views/RegExTextView/RegExTextView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RegExSyntaxView.swift
3 | // RegEx+
4 | //
5 | // Created by Lex on 2020/4/23.
6 | // Copyright © 2020 Lex.sh. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import Combine
11 | import UIKit
12 |
13 |
14 | // Credit to: Asperi https://stackoverflow.com/users/12299030/asperi
15 | // https://stackoverflow.com/a/58639072/1209135
16 | private struct UITextViewWrapper: UIViewRepresentable {
17 | typealias UIViewType = UITextView
18 |
19 | @Binding var text: String
20 | @Binding var calculatedHeight: CGFloat
21 | var onDone: (() -> Void)?
22 |
23 | private let syntaxHighlighter = RegExSyntaxHighlighter()
24 |
25 | func makeUIView(context: UIViewRepresentableContext) -> UITextView {
26 | let tv = UITextView()
27 | tv.delegate = context.coordinator
28 | tv.textStorage.delegate = syntaxHighlighter
29 |
30 | let font = UIFont.preferredFont(forTextStyle: .body)
31 |
32 | tv.isEditable = true
33 | tv.font = font.withSize(font.pointSize + 2)
34 | tv.isSelectable = true
35 | tv.isUserInteractionEnabled = true
36 | tv.isScrollEnabled = false
37 | tv.backgroundColor = UIColor.clear
38 |
39 | tv.textContainerInset = .zero
40 | tv.textContainer.lineFragmentPadding = 0
41 | if nil != onDone {
42 | tv.returnKeyType = .done
43 | }
44 |
45 | return tv
46 | }
47 |
48 | func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext) {
49 | guard uiView.text != text else {
50 | return
51 | }
52 |
53 | uiView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
54 | uiView.textContainer.lineBreakMode = .byCharWrapping
55 | uiView.text = text
56 |
57 | syntaxHighlighter.textStorage = uiView.textStorage
58 | syntaxHighlighter.highlightRegularExpression()
59 |
60 | DispatchQueue.main.async {
61 | UITextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight)
62 | }
63 | }
64 |
65 | fileprivate static func recalculateHeight(view: UIView, result: Binding) {
66 | let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
67 | if result.wrappedValue != newSize.height {
68 | result.wrappedValue = newSize.height
69 | }
70 | }
71 |
72 | func makeCoordinator() -> Coordinator {
73 | return Coordinator(text: $text, height: $calculatedHeight, onDone: onDone)
74 | }
75 |
76 | final class Coordinator: NSObject, UITextViewDelegate {
77 | var text: Binding
78 | var calculatedHeight: Binding
79 | var onDone: (() -> Void)?
80 |
81 | init(text: Binding, height: Binding, onDone: (() -> Void)? = nil) {
82 | self.text = text
83 | self.calculatedHeight = height
84 | self.onDone = onDone
85 | }
86 |
87 | func textViewDidChange(_ uiView: UITextView) {
88 | text.wrappedValue = uiView.text
89 | UITextViewWrapper.recalculateHeight(view: uiView, result: calculatedHeight)
90 | }
91 |
92 | func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
93 | if let onDone = self.onDone, text == "\n" {
94 | textView.resignFirstResponder()
95 | onDone()
96 | return false
97 | }
98 | return true
99 | }
100 | }
101 |
102 | }
103 |
104 | struct RegExTextView: View, Equatable {
105 |
106 | private var placeholder: String
107 | private var onCommit: (() -> Void)?
108 |
109 | @Binding private var text: String
110 | private var internalText: Binding {
111 | Binding(get: { self.text }) {
112 | self.text = $0
113 | self.showingPlaceholder = $0.isEmpty
114 | }
115 | }
116 |
117 | @State private var dynamicHeight: CGFloat = 20
118 | @State private var showingPlaceholder = false
119 |
120 | init (_ placeholder: String = "", text: Binding, onCommit: (() -> Void)? = nil) {
121 | self.placeholder = placeholder
122 | self.onCommit = onCommit
123 | self._text = text
124 | self._showingPlaceholder = State(initialValue: self.text.isEmpty)
125 | }
126 |
127 | var body: some View {
128 | UITextViewWrapper(
129 | text: internalText,
130 | calculatedHeight: $dynamicHeight,
131 | onDone: onCommit
132 | )
133 | .frame(minHeight: dynamicHeight, maxHeight: dynamicHeight)
134 | .background(placeholderView, alignment: .topLeading)
135 | }
136 |
137 | var placeholderView: some View {
138 | Group {
139 | if showingPlaceholder {
140 | VStack {
141 | Text(placeholder)
142 | .foregroundColor(.secondary)
143 | }
144 | }
145 | }
146 | }
147 |
148 | static func == (lhs: RegExTextView, rhs: RegExTextView) -> Bool {
149 | lhs.text == rhs.text
150 | }
151 |
152 | }
153 |
154 | #if DEBUG
155 | struct RegExTextView_Previews: PreviewProvider {
156 | static var test = #"^(\d+)\.(\d{2}) (\d+)\.(\d{2}) (\d+)\.(\d{2}) (\d+)\.(\d{2})"#
157 | static var testBinding = Binding(get: { test }, set: { test = $0 })
158 |
159 | static var previews: some View {
160 | Group {
161 | VStack(alignment: .leading) {
162 | RegExTextView("Enter some text here", text: testBinding) {
163 | print("Final text: \(test)")
164 | }
165 | .overlay(
166 | RoundedRectangle(cornerRadius: 4).stroke(Color.black)
167 | )
168 | }
169 | VStack(alignment: .leading) {
170 | RegExTextView("Enter some text here", text: testBinding) {
171 | print("Final text: \(test)")
172 | }
173 | .overlay(
174 | RoundedRectangle(cornerRadius: 4).stroke(Color.black)
175 | )
176 | }
177 | }
178 | }
179 | }
180 | #endif
181 |
--------------------------------------------------------------------------------
/RegEx+/Views/RegExTextView/String+NSRange.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+NSRange.swift
3 | // RegEx+
4 | //
5 | // Created by Lex on 2020/5/2.
6 | // Copyright © 2020 Lex.sh. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | extension String {
13 |
14 | func nsRange(from range: Range) -> NSRange {
15 | let startPos = self.distance(from: self.startIndex, to: range.lowerBound)
16 | let endPos = self.distance(from: self.startIndex, to: range.upperBound)
17 | return NSRange(location: startPos, length: endPos - startPos)
18 | }
19 |
20 | }
21 |
22 | extension String {
23 |
24 | func ranges(of substring: String, options: CompareOptions = [], locale: Locale? = nil) -> [Range] {
25 | var ranges: [Range] = []
26 | while let range = range(of: substring,
27 | options: options,
28 | range: (ranges.last?.upperBound ?? self.startIndex)..) -> SFSafariViewController {
19 | return SFSafariViewController(url: url)
20 | }
21 |
22 | func updateUIViewController(_ safariViewController: SFSafariViewController, context: UIViewControllerRepresentableContext) {
23 | }
24 | }
25 |
26 | #if DEBUG
27 | struct SafariView_Previews: PreviewProvider {
28 | static var previews: some View {
29 | SafariView(url: URL(string: "https://www.apple.com")!)
30 | }
31 | }
32 | #endif
33 |
--------------------------------------------------------------------------------
/RegEx+/Views/SearchView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchView.swift
3 | // RegEx+
4 | //
5 | // Created by Lex on 2020/10/4.
6 | // Copyright © 2020 Lex.sh. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct SearchView: View {
12 | @Binding var text: String
13 |
14 | @State private var isEditing = false
15 |
16 | private var cornerRadius: CGFloat {
17 | #if targetEnvironment(macCatalyst)
18 | 5
19 | #else
20 | 18
21 | #endif
22 | }
23 |
24 | var body: some View {
25 | TextField("Search...", text: $text)
26 | .padding(.horizontal, 32)
27 | .padding(.vertical, 5)
28 | .background(Color(.systemGray6))
29 | .cornerRadius(cornerRadius)
30 | .overlay(
31 | HStack {
32 | Image(systemName: "magnifyingglass")
33 | .foregroundColor(.gray)
34 | .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
35 | .padding(.leading, 10)
36 |
37 | if isEditing && !text.isEmpty {
38 | Button(action: {
39 | self.text = ""
40 | }) {
41 | Image(systemName: "multiply.circle.fill")
42 | .foregroundColor(.gray)
43 | .padding(.trailing, 8)
44 | }
45 | .buttonStyle(.borderless)
46 | }
47 | }
48 | .id(text)
49 | )
50 | .disableAutocorrection(true)
51 | .onTapGesture {
52 | self.isEditing = true
53 | }
54 | }
55 | }
56 |
57 | struct SearchView_Previews: PreviewProvider {
58 | static var previews: some View {
59 | VStack {
60 | SearchView(text: .constant(""))
61 | .preferredColorScheme(.dark)
62 | }
63 | .padding()
64 | .background(Color.white)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/RegEx+/en.lproj/CheatSheet.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | metacharacters
6 |
7 |
8 | exp
9 | \a
10 | des
11 | Match a BELL, \u0007
12 |
13 |
14 | exp
15 | \A
16 | des
17 | Match at the beginning of the input. Differs from ^ in that \A will not match after a new line within the input.
18 |
19 |
20 | exp
21 | \b, outside of a [Set]
22 | des
23 | Match if the current position is a word boundary. Boundaries occur at the transitions between word (\w) and non-word (\W) characters, with combining marks ignored. For better word boundaries, see useUnicodeWordBoundaries.
24 |
25 |
26 | exp
27 | \b, within a [Set]
28 | des
29 | Match a BACKSPACE, \u0008.
30 |
31 |
32 | exp
33 | \B
34 | des
35 | Match if the current position is not a word boundary.
36 |
37 |
38 | exp
39 | \cX
40 | des
41 | Match a control-X character
42 |
43 |
44 | exp
45 | \d
46 | des
47 | Match any character with the Unicode General Category of Nd (Number, Decimal Digit.)
48 |
49 |
50 | exp
51 | \D
52 | des
53 | Match any character that is not a decimal digit.
54 |
55 |
56 | exp
57 | \e
58 | des
59 | Match an ESCAPE, \u001B.
60 |
61 |
62 | exp
63 | \E
64 | des
65 | Terminates a \Q ... \E quoted sequence.
66 |
67 |
68 | exp
69 | \f
70 | des
71 | Match a FORM FEED, \u000C.
72 |
73 |
74 | exp
75 | \G
76 | des
77 | Match if the current position is at the end of the previous match.
78 |
79 |
80 | exp
81 | \n
82 | des
83 | Match a LINE FEED, \u000A.
84 |
85 |
86 | exp
87 | \N{UNICODE CHARACTER NAME}
88 | des
89 | Match the named character.
90 |
91 |
92 | exp
93 | \p{UNICODE PROPERTY NAME}
94 | des
95 | Match any character with the specified Unicode Property.
96 |
97 |
98 | exp
99 | \P{UNICODE PROPERTY NAME}
100 | des
101 | Match any character not having the specified Unicode Property.
102 |
103 |
104 | exp
105 | \Q
106 | des
107 | Quotes all following characters until \E.
108 |
109 |
110 | exp
111 | \r
112 | des
113 | Match a CARRIAGE RETURN, \u000D.
114 |
115 |
116 | exp
117 | \s
118 | des
119 | Match a white space character. White space is defined as [\t\n\f\r\p{Z}].
120 |
121 |
122 | exp
123 | \S
124 | des
125 | Match a non-white space character.
126 |
127 |
128 | exp
129 | \t
130 | des
131 | Match a HORIZONTAL TABULATION, \u0009.
132 |
133 |
134 | exp
135 | \uhhhh
136 | des
137 | Match the character with the hex value hhhh.
138 |
139 |
140 | exp
141 | \Uhhhhhhhh
142 | des
143 | Match the character with the hex value hhhhhhhh. Exactly eight hex digits must be provided, even though the largest Unicode code point is \U0010ffff.
144 |
145 |
146 | exp
147 | \w
148 | des
149 | Match a word character. Word characters are [\p{Ll}\p{Lu}\p{Lt}\p{Lo}\p{Nd}].
150 |
151 |
152 | exp
153 | \W
154 | des
155 | Match a non-word character.
156 |
157 |
158 | exp
159 | \x{hhhh}
160 | des
161 | Match the character with hex value hhhh. From one to six hex digits may be supplied.
162 |
163 |
164 | exp
165 | \xhh
166 | des
167 | Match the character with two digit hex value hh.
168 |
169 |
170 | exp
171 | \X
172 | des
173 | Match a Grapheme Cluster.
174 |
175 |
176 | exp
177 | \Z
178 | des
179 | Match if the current position is at the end of input, but before the final line terminator, if one exists.
180 |
181 |
182 | exp
183 | \z
184 | des
185 | Match if the current position is at the end of input.
186 |
187 |
188 | exp
189 | \n
190 | des
191 | Back Reference. Match whatever the nth capturing group matched. n must be a number ≥ 1 and ≤ total number of capture groups in the pattern.
192 |
193 |
194 | exp
195 | \0ooo
196 | des
197 | Match an Octal character. ooo is from one to three octal digits. 0377 is the largest allowed Octal character. The leading zero is required; it distinguishes Octal constants from back references.
198 |
199 |
200 | exp
201 | [pattern]
202 | des
203 | Match any one character from the pattern.
204 |
205 |
206 | exp
207 | .
208 | des
209 | Match any character.
210 |
211 |
212 | exp
213 | ^
214 | des
215 | Match at the beginning of a line.
216 |
217 |
218 | exp
219 | $
220 | des
221 | Match at the end of a line.
222 |
223 |
224 | exp
225 | \
226 | des
227 | Quotes the following character. Characters that must be quoted to be treated as literals are * ? + [ ( ) { } ^ $ | \ . /
228 |
229 |
230 | operators
231 |
232 |
233 | exp
234 | |
235 | des
236 | Alternation. A|B matches either A or B.
237 |
238 |
239 | exp
240 | *
241 | des
242 | Match 0 or more times. Match as many times as possible.
243 |
244 |
245 | exp
246 | +
247 | des
248 | Match 1 or more times. Match as many times as possible.
249 |
250 |
251 | exp
252 | ?
253 | des
254 | Match zero or one times. Prefer one.
255 |
256 |
257 | exp
258 | {n}
259 | des
260 | Match exactly n times.
261 |
262 |
263 | exp
264 | {n,}
265 | des
266 | Match at least n times. Match as many times as possible.
267 |
268 |
269 | exp
270 | {n,m}
271 | des
272 | Match between n and m times. Match as many times as possible, but not more than m.
273 |
274 |
275 | exp
276 | *?
277 | des
278 | Match 0 or more times. Match as few times as possible.
279 |
280 |
281 | exp
282 | +?
283 | des
284 | Match 1 or more times. Match as few times as possible.
285 |
286 |
287 | exp
288 | ??
289 | des
290 | Match zero or one times. Prefer zero.
291 |
292 |
293 | exp
294 | {n}?
295 | des
296 | Match exactly n times.
297 |
298 |
299 | exp
300 | {n,}?
301 | des
302 | Match at least n times, but no more than required for an overall pattern match.
303 |
304 |
305 | exp
306 | {n,m}?
307 | des
308 | Match between n and m times. Match as few times as possible, but not less than n.
309 |
310 |
311 | exp
312 | *+
313 | des
314 | Match 0 or more times. Match as many times as possible when first encountered, do not retry with fewer even if overall match fails (Possessive Match).
315 |
316 |
317 | exp
318 | ++
319 | des
320 | Match 1 or more times. Possessive match.
321 |
322 |
323 | exp
324 | ?+
325 | des
326 | Match zero or one times. Possessive match.
327 |
328 |
329 | exp
330 | {n}+
331 | des
332 | Match exactly n times.
333 |
334 |
335 | exp
336 | {n,}+
337 | des
338 | Match at least n times. Possessive Match.
339 |
340 |
341 | exp
342 | {n,m}+
343 | des
344 | Match between n and m times. Possessive Match.
345 |
346 |
347 | exp
348 | (...)
349 | des
350 | Capturing parentheses. Range of input that matched the parenthesized subexpression is available after the match.
351 |
352 |
353 | exp
354 | (?:...)
355 | des
356 | Non-capturing parentheses. Groups the included pattern, but does not provide capturing of matching text. Somewhat more efficient than capturing parentheses.
357 |
358 |
359 | exp
360 | (?>...)
361 | des
362 | Atomic-match parentheses. First match of the parenthesized subexpression is the only one tried; if it does not lead to an overall pattern match, back up the search for a match to a position before the "(?>"
363 |
364 |
365 | exp
366 | (?# ... )
367 | des
368 | Free-format comment (?# comment ).
369 |
370 |
371 | exp
372 | (?= ... )
373 | des
374 | Look-ahead assertion. True if the parenthesized pattern matches at the current input position, but does not advance the input position.
375 |
376 |
377 | exp
378 | (?! ... )
379 | des
380 | Negative look-ahead assertion. True if the parenthesized pattern does not match at the current input position. Does not advance the input position.
381 |
382 |
383 | exp
384 | (?<= ... )
385 | des
386 | Look-behind assertion. True if the parenthesized pattern matches text preceding the current input position, with the last character of the match being the input character just before the current position. Does not alter the input position. The length of possible strings matched by the look-behind pattern must not be unbounded (no * or + operators.
387 |
388 |
389 | exp
390 | (?<! ... )
391 | des
392 | Negative Look-behind assertion. True if the parenthesized pattern does not match text preceding the current input position, with the last character of the match being the input character just before the current position. Does not alter the input position. The length of possible strings matched by the look-behind pattern must not be unbounded (no * or + operators.
393 |
394 |
395 | exp
396 | (?ismwx-ismwx: ... )
397 | des
398 | Flag settings. Evaluate the parenthesized expression with the specified flags enabled or -disabled. The flags are defined in Flag Options.
399 |
400 |
401 | exp
402 | (?ismwx-ismwx)
403 | des
404 | Flag settings. Change the flag settings. Changes apply to the portion of the pattern following the setting. For example, (?i) changes to a case insensitive match.The flags are defined in Flag Options.
405 |
406 |
407 |
408 |
409 |
--------------------------------------------------------------------------------
/RegEx+/zh-Hans.lproj/CheatSheet.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | metacharacters
6 |
7 |
8 | exp
9 | \a
10 | des
11 | 匹配一个警报声, \u0007
12 |
13 |
14 | exp
15 | \A
16 | des
17 | 匹配输入的开端。和 ^ 不同的是,\A 不会匹配换行后的开端。
18 |
19 |
20 | exp
21 | \b, 在 [集合] 外
22 | des
23 | 当前位置是词语边界时匹配。词语边界是指词语 (\w) 和非词语 (\W) 字符之间的界限,这种边界会忽略连字符。为了更好地使用词语边界,请打开『使用 unicode 词语边界』选项。
24 |
25 |
26 | exp
27 | \b, 在 [集合] 内
28 | des
29 | 匹配一个退格, \u0008.
30 |
31 |
32 | exp
33 | \B
34 | des
35 | 当前位置不是词语边界时匹配。
36 |
37 |
38 | exp
39 | \cX
40 | des
41 | 匹配一个 control-X 字符。
42 |
43 |
44 | exp
45 | \d
46 | des
47 | 匹配任何符合 Unicode 通用类别的数字字符。
48 |
49 |
50 | exp
51 | \D
52 | des
53 | 匹配任何不为数字的字符。
54 |
55 |
56 | exp
57 | \e
58 | des
59 | 匹配一个 ESCAPE, \u001B。
60 |
61 |
62 | exp
63 | \E
64 | des
65 | 终止一个 \Q ... \E 元字符引用序列。
66 |
67 |
68 | exp
69 | \f
70 | des
71 | 匹配换页符, \u000C。
72 |
73 |
74 | exp
75 | \G
76 | des
77 | 当前位置是上一个匹配的结束位置时匹配。
78 |
79 |
80 | exp
81 | \n
82 | des
83 | 匹配一个换行符, \u000A。
84 |
85 |
86 | exp
87 | \N{UNICODE CHARACTER NAME}
88 | des
89 | 匹配 Unicode 命名字符。
90 |
91 |
92 | exp
93 | \p{UNICODE PROPERTY NAME}
94 | des
95 | 匹配任意有指定 Unicode 属性的字符。
96 |
97 |
98 | exp
99 | \P{UNICODE PROPERTY NAME}
100 | des
101 | 匹配任意不包括指定 Unicode 属性的字符。
102 |
103 |
104 | exp
105 | \Q
106 | des
107 | 开始元字符引用序列,直到遇到 \E。
108 |
109 |
110 | exp
111 | \r
112 | des
113 | 匹配一个 <CR> 回车, \u000D.
114 |
115 |
116 | exp
117 | \s
118 | des
119 | 匹配一个空白字符,它的定义是 [\t\n\f\r\p{Z}]。
120 |
121 |
122 | exp
123 | \S
124 | des
125 | 匹配一个非空白字符。
126 |
127 |
128 | exp
129 | \t
130 | des
131 | 匹配一个横向 TAB 缩进,\u0009。
132 |
133 |
134 | exp
135 | \uhhhh
136 | des
137 | 匹配 hex 值为 hhhh 的字符串。
138 |
139 |
140 | exp
141 | \Uhhhhhhhh
142 | des
143 | 匹配 hex 值为 hhhhhhhh 的字符串。尽管最大的 Unicode 值是 \U0010ffff,但还是要提供完整的八个十六进制数字。
144 |
145 |
146 | exp
147 | \w
148 | des
149 | 匹配一个词语字符,它的字符定义是 [\p{Ll}\p{Lu}\p{Lt}\p{Lo}\p{Nd}]。
150 |
151 |
152 | exp
153 | \W
154 | des
155 | 匹配一个非词语字符。
156 |
157 |
158 | exp
159 | \x{hhhh}
160 | des
161 | 匹配 hex 值为 hhhh 的字符串,可以是一到六位的十六进制数字。
162 |
163 |
164 | exp
165 | \xhh
166 | des
167 | 匹配 hex 值为 hh 的两位十六进制数字对应的字符。
168 |
169 |
170 | exp
171 | \X
172 | des
173 | 匹配一个字符簇(Grapheme Cluster)。
174 |
175 |
176 | exp
177 | \Z
178 | des
179 | 当前位置是输入的结尾时匹配,但仅匹配到行尾标识前,如果有的话。
180 |
181 |
182 | exp
183 | \z
184 | des
185 | 当前位置是输入的结尾时匹配。
186 |
187 |
188 | exp
189 | \n
190 | des
191 | 换行引用。无论第 n 个捕获组匹配到什么,n 必须是一个 ≥ 1 并 ≤ 总捕获数的数字。
192 |
193 |
194 | exp
195 | \0ooo
196 | des
197 | 匹配一个八进制字符。ooo 为一个到三个八进制数字。0377 是有效的最大八进制字符。前置的零是必须的;它将八进制常量与之后的引用值区分开来。
198 |
199 |
200 | exp
201 | [pattern]
202 | des
203 | 匹配任何在这个方括号内的字符。
204 |
205 |
206 | exp
207 | .
208 | des
209 | 匹配所有字符。
210 |
211 |
212 | exp
213 | ^
214 | des
215 | 匹配行首。
216 |
217 |
218 | exp
219 | $
220 | des
221 | 匹配行尾。
222 |
223 |
224 | exp
225 | \
226 | des
227 | 转义后续字符。有些字符必须转义后才能被当成字面量使用,如 * ? + [ ( ) { } ^ $ | \ . /
228 |
229 |
230 | operators
231 |
232 |
233 | exp
234 | |
235 | des
236 | 可选匹配。A|B 表示是 A 或者是 B 的一个字符。
237 |
238 |
239 | exp
240 | *
241 | des
242 | 匹配零次或多次,优先匹配多次。
243 |
244 |
245 | exp
246 | +
247 | des
248 | 匹配一次或多次,优先匹配多次。
249 |
250 |
251 | exp
252 | ?
253 | des
254 | 匹配零到一次,优先匹配一次。
255 |
256 |
257 | exp
258 | {n}
259 | des
260 | 明确匹配 n 次。
261 |
262 |
263 | exp
264 | {n,}
265 | des
266 | 匹配最少 n 次,最多无限次。
267 |
268 |
269 | exp
270 | {n,m}
271 | des
272 | 匹配 n 到 m 次,优先匹配尽可能多的次数,但不会超过 m 次。
273 |
274 |
275 | exp
276 | *?
277 | des
278 | 匹配零次或多次,尽可能少匹配。
279 |
280 |
281 | exp
282 | +?
283 | des
284 | 匹配一次或多次,尽可能少匹配。
285 |
286 |
287 | exp
288 | ??
289 | des
290 | 匹配零次或一次,尽可能少匹配。
291 |
292 |
293 | exp
294 | {n}?
295 | des
296 | 明确匹配 n 次。
297 |
298 |
299 | exp
300 | {n,}?
301 | des
302 | 匹配至少 n 次,但不超过整体模式匹配的要求。
303 |
304 |
305 | exp
306 | {n,m}?
307 | des
308 | 匹配 n 到 m 次,尽可能少匹配,但不少于 n 次。
309 |
310 |
311 | exp
312 | *+
313 | des
314 | 匹配零次或以上。第一次遇到时,尽量多匹配,即使整体匹配失败,也不要用较少的次数重试(独占模式)。
315 |
316 |
317 | exp
318 | ++
319 | des
320 | 匹配一次或多次,独占模式。
321 |
322 |
323 | exp
324 | ?+
325 | des
326 | 匹配零次或一次,独占模式。
327 |
328 |
329 | exp
330 | {n}+
331 | des
332 | 明确匹配 n 次。
333 |
334 |
335 | exp
336 | {n,}+
337 | des
338 | 匹配至少 n 次,独占模式,匹配得越多越好。
339 |
340 |
341 | exp
342 | {n,m}+
343 | des
344 | 匹配 n 到 m 次,独占模式,匹配得越多越好。
345 |
346 |
347 | exp
348 | (...)
349 | des
350 | 圆括号匹配。整个表达式匹配结束后,圆括号内的子表达式所捕获到的区域能从匹配信息中单独获得。
351 |
352 |
353 | exp
354 | (?:...)
355 | des
356 | 非捕获圆括号。匹配括号内冒号之后的表达式但不做捕获,无法从匹配信息中获得,某些时候比捕获圆括号更加高效。
357 |
358 |
359 | exp
360 | (?>...)
361 | des
362 | 原子匹配圆括号。括号子表达式的第一次匹配是唯一一次尝试;如果没有导致整体模式匹配,则搜索匹配回到 "(?>" 之前的位置。
363 |
364 |
365 | exp
366 | (?# ... )
367 | des
368 | 自由格式的注释 (?# 注释 ).
369 |
370 |
371 | exp
372 | (?= ... )
373 | des
374 | 向后断言。当括号内的表达式在当前输入位置匹配时生效,但不移动输入位置。
375 |
376 |
377 | exp
378 | (?! ... )
379 | des
380 | 否定的向后断言。当括号内的表达式在当前输入位置不匹配时生效。不移动输入位置。
381 |
382 |
383 | exp
384 | (?<= ... )
385 | des
386 | 向前断言。如果括号内的表达式与当前输入位置之前的文本相匹配,且匹配的最后一个字符是当前位置之前的输入字符,则生效,且不改变输入位置。向前断言模式匹配的字符串长度不能是无限制的(没有 * 或 + 运算符)。
387 |
388 |
389 | exp
390 | (?<! ... )
391 | des
392 | 否定的向前断言。如果括号内的表达式与当前输入位置之前的文本不匹配,则生效,匹配的最后一个字符是当前位置之前的输入字符。不改变输入位置。否定的向前断言模式匹配的字符串长度不能是无限制的(没有 * 或 + 运算符)。
393 |
394 |
395 | exp
396 | (?ismwx-ismwx: ... )
397 | des
398 | 标志设置。在启用或禁用指定的标志时评估括号内的表达式。这些标志在标志选项中定义。例如,(?i:Aa) 启用不区分大小写,(?-i:Aa) 禁用不区分大小写。
399 |
400 |
401 | exp
402 | (?ismwx-ismwx)
403 | des
404 | 标志设置。更改标志设置。更改适用于设置后的模式部分。例如,(?i) 改变为不区分大小写的匹配。标志在标志选项中定义。
405 |
406 |
407 |
408 |
409 |
--------------------------------------------------------------------------------
/RegEx+/zh-Hant.lproj/CheatSheet.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | metacharacters
6 |
7 |
8 | exp
9 | \a
10 | des
11 | 匹配一個警報聲, \u0007
12 |
13 |
14 | exp
15 | \A
16 | des
17 | 匹配輸入的開端。和 ^ 不同的是,\A 不會匹配換行後的開端。
18 |
19 |
20 | exp
21 | \b, 在 [集合] 外
22 | des
23 | 當前位置是詞語邊界時匹配。詞語邊界是指詞語 (\w) 和非詞語 (\W) 字符之間的界限,這種邊界會忽略連字符。為了更好地使用詞語邊界,請打開『使用 unicode 詞語邊界』選項。
24 |
25 |
26 | exp
27 | \b, 在 [集合] 內
28 | des
29 | 匹配一個退格, \u0008.
30 |
31 |
32 | exp
33 | \B
34 | des
35 | 當前位置不是詞語邊界時匹配。
36 |
37 |
38 | exp
39 | \cX
40 | des
41 | 匹配一個 control-X 字符。
42 |
43 |
44 | exp
45 | \d
46 | des
47 | 匹配任何符合 Unicode 通用類別的數字字符。
48 |
49 |
50 | exp
51 | \D
52 | des
53 | 匹配任何不為數字的字符。
54 |
55 |
56 | exp
57 | \e
58 | des
59 | 匹配一個 ESCAPE, \u001B。
60 |
61 |
62 | exp
63 | \E
64 | des
65 | 終止一個 \Q ... \E 元字符引用序列。
66 |
67 |
68 | exp
69 | \f
70 | des
71 | 匹配換頁符, \u000C。
72 |
73 |
74 | exp
75 | \G
76 | des
77 | 當前位置是上一個匹配的結束位置時匹配。
78 |
79 |
80 | exp
81 | \n
82 | des
83 | 匹配一個換行符, \u000A。
84 |
85 |
86 | exp
87 | \N{UNICODE CHARACTER NAME}
88 | des
89 | 匹配 Unicode 命名字符。
90 |
91 |
92 | exp
93 | \p{UNICODE PROPERTY NAME}
94 | des
95 | 匹配任意有指定 Unicode 屬性的字符。
96 |
97 |
98 | exp
99 | \P{UNICODE PROPERTY NAME}
100 | des
101 | 匹配任意不包括指定 Unicode 屬性的字符。
102 |
103 |
104 | exp
105 | \Q
106 | des
107 | 開始元字符引用序列,直到遇到 \E。
108 |
109 |
110 | exp
111 | \r
112 | des
113 | 匹配一個 <CR> 回車, \u000D.
114 |
115 |
116 | exp
117 | \s
118 | des
119 | 匹配一個空白字符,它的定義是 [\t\n\f\r\p{Z}]。
120 |
121 |
122 | exp
123 | \S
124 | des
125 | 匹配一個非空白字符。
126 |
127 |
128 | exp
129 | \t
130 | des
131 | 匹配一個橫向 TAB 縮進,\u0009。
132 |
133 |
134 | exp
135 | \uhhhh
136 | des
137 | 匹配 hex 值為 hhhh 的字符串。
138 |
139 |
140 | exp
141 | \Uhhhhhhhh
142 | des
143 | 匹配 hex 值為 hhhhhhhh 的字符串。儘管最大的 Unicode 值是 \U0010ffff,但還是要提供完整的八個十六進制數字。
144 |
145 |
146 | exp
147 | \w
148 | des
149 | 匹配一個詞語字符,它的字符定義是 [\p{Ll}\p{Lu}\p{Lt}\p{Lo}\p{Nd}]。
150 |
151 |
152 | exp
153 | \W
154 | des
155 | 匹配一個非詞語字符。
156 |
157 |
158 | exp
159 | \x{hhhh}
160 | des
161 | 匹配 hex 值為 hhhh 的字符串,可以是一到六位的十六進制數字。
162 |
163 |
164 | exp
165 | \xhh
166 | des
167 | 匹配 hex 值為 hh 的兩位十六進制數字對應的字符。
168 |
169 |
170 | exp
171 | \X
172 | des
173 | 匹配一個字符簇(Grapheme Cluster)。
174 |
175 |
176 | exp
177 | \Z
178 | des
179 | 當前位置是輸入的結尾時匹配,但僅匹配到行尾標識前,如果有的話。
180 |
181 |
182 | exp
183 | \z
184 | des
185 | 當前位置是輸入的結尾時匹配。
186 |
187 |
188 | exp
189 | \n
190 | des
191 | 換行引用。無論第 n 個捕獲組匹配到什麼,n 必須是一個 ≥ 1 並 ≤ 總捕獲數的數字。
192 |
193 |
194 | exp
195 | \0ooo
196 | des
197 | 匹配一個八進制字符。ooo 為一個到三個八進制數字。0377 是有效的最大八進制字符。前置的零是必須的;它將八進制常量與之後的引用值區分開來。
198 |
199 |
200 | exp
201 | [pattern]
202 | des
203 | 匹配任何在這個方括號內的字符。
204 |
205 |
206 | exp
207 | .
208 | des
209 | 匹配所有字符。
210 |
211 |
212 | exp
213 | ^
214 | des
215 | 匹配行首。
216 |
217 |
218 | exp
219 | $
220 | des
221 | 匹配行尾。
222 |
223 |
224 | exp
225 | \
226 | des
227 | 轉義後續字符。有些字符必須轉義後才能被當成字面量使用,如 * ? + [ ( ) { } ^ $ | \ . /
228 |
229 |
230 | operators
231 |
232 |
233 | exp
234 | |
235 | des
236 | 可選匹配。A|B 表示是 A 或者是 B 的一個字符。
237 |
238 |
239 | exp
240 | *
241 | des
242 | 匹配零次或多次,優先匹配多次。
243 |
244 |
245 | exp
246 | +
247 | des
248 | 匹配一次或多次,優先匹配多次。
249 |
250 |
251 | exp
252 | ?
253 | des
254 | 匹配零到一次,優先匹配一次。
255 |
256 |
257 | exp
258 | {n}
259 | des
260 | 明確匹配 n 次。
261 |
262 |
263 | exp
264 | {n,}
265 | des
266 | 匹配最少 n 次,最多無限次。
267 |
268 |
269 | exp
270 | {n,m}
271 | des
272 | 匹配 n 到 m 次,優先匹配儘可能多的次數,但不會超過 m 次。
273 |
274 |
275 | exp
276 | *?
277 | des
278 | 匹配零次或多次,儘可能少匹配。
279 |
280 |
281 | exp
282 | +?
283 | des
284 | 匹配一次或多次,儘可能少匹配。
285 |
286 |
287 | exp
288 | ??
289 | des
290 | 匹配零次或一次,儘可能少匹配。
291 |
292 |
293 | exp
294 | {n}?
295 | des
296 | 明確匹配 n 次。
297 |
298 |
299 | exp
300 | {n,}?
301 | des
302 | 匹配至少 n 次,但不超過整體模式匹配的要求。
303 |
304 |
305 | exp
306 | {n,m}?
307 | des
308 | 匹配 n 到 m 次,儘可能少匹配,但不少於 n 次。
309 |
310 |
311 | exp
312 | *+
313 | des
314 | 匹配零次或以上。第一次遇到時,儘量多匹配,即使整體匹配失敗,也不要用較少的次數重試(獨佔模式)。
315 |
316 |
317 | exp
318 | ++
319 | des
320 | 匹配一次或多次,獨佔模式。
321 |
322 |
323 | exp
324 | ?+
325 | des
326 | 匹配零次或一次,獨佔模式。
327 |
328 |
329 | exp
330 | {n}+
331 | des
332 | 明確匹配 n 次。
333 |
334 |
335 | exp
336 | {n,}+
337 | des
338 | 匹配至少 n 次,獨佔模式,匹配得越多越好。
339 |
340 |
341 | exp
342 | {n,m}+
343 | des
344 | 匹配 n 到 m 次,獨佔模式,匹配得越多越好。
345 |
346 |
347 | exp
348 | (...)
349 | des
350 | 圓括號匹配。整個表達式匹配結束後,圓括號內的子表達式所捕獲到的區域能從匹配信息中單獨獲得。
351 |
352 |
353 | exp
354 | (?:...)
355 | des
356 | 非捕獲圓括號。匹配括號內冒號之後的表達式但不做捕獲,無法從匹配信息中獲得,某些時候比捕獲圓括號更加高效。
357 |
358 |
359 | exp
360 | (?>...)
361 | des
362 | 原子匹配圓括號。括號子表達式的第一次匹配是唯一一次嘗試;如果沒有導致整體模式匹配,則搜索匹配回到 "(?>" 之前的位置。
363 |
364 |
365 | exp
366 | (?# ... )
367 | des
368 | 自由格式的註釋 (?# 註釋 ).
369 |
370 |
371 | exp
372 | (?= ... )
373 | des
374 | 向後斷言。當括號內的表達式在當前輸入位置匹配時生效,但不移動輸入位置。
375 |
376 |
377 | exp
378 | (?! ... )
379 | des
380 | 否定的向後斷言。當括號內的表達式在當前輸入位置不匹配時生效。不移動輸入位置。
381 |
382 |
383 | exp
384 | (?<= ... )
385 | des
386 | 向前斷言。如果括號內的表達式與當前輸入位置之前的文本相匹配,且匹配的最後一個字符是當前位置之前的輸入字符,則生效,且不改變輸入位置。向前斷言模式匹配的字符串長度不能是無限制的(沒有 * 或 + 運算符)。
387 |
388 |
389 | exp
390 | (?<! ... )
391 | des
392 | 否定的向前斷言。如果括號內的表達式與當前輸入位置之前的文本不匹配,則生效,匹配的最後一個字符是當前位置之前的輸入字符。不改變輸入位置。否定的向前斷言模式匹配的字符串長度不能是無限制的(沒有 * 或 + 運算符)。
393 |
394 |
395 | exp
396 | (?ismwx-ismwx: ... )
397 | des
398 | 標誌設置。在啟用或禁用指定的標誌時評估括號內的表達式。這些標誌在標誌選項中定義。例如,(?i:Aa) 啟用不區分大小寫,(?-i:Aa) 禁用不區分大小寫。
399 |
400 |
401 | exp
402 | (?ismwx-ismwx)
403 | des
404 | 標誌設置。更改標誌設置。更改適用於設置後的模式部分。例如,(?i) 改變為不區分大小寫的匹配。標誌在標誌選項中定義。
405 |
406 |
407 |
408 |
409 |
--------------------------------------------------------------------------------