├── .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 | [![Swift 5.9](https://img.shields.io/badge/swift-5.9-ED523F.svg?style=flat)](https://swift.org/download/) 4 | [![@lexrus](https://img.shields.io/badge/contact-@lexrus-336699.svg?style=flat)](https://twitter.com/lexrus) 5 | [![codebeat badge](https://codebeat.co/badges/ee2c442b-d1ce-4d4e-9185-54b72faec4e2)](https://codebeat.co/projects/github-com-lexrus-regexplus-master) 6 | 7 | [AppStore](https://apps.apple.com/us/app/regex/id1511763524) 8 | 9 | > My first *learning-by-doing* **SwiftUI** app! A Regular Expression tool. 10 | 11 | ![HeroImage](https://github.com/lexrus/RegExPlus/assets/219689/739896bf-c843-46b9-82f9-e1462562fe4b) 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
  • \n
  • iPad
  • "), 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 | --------------------------------------------------------------------------------