Repository: SwiftFiddle/swiftregex Branch: main Commit: b1b10fef9496 Files: 73 Total size: 223.2 KB Directory structure: gitextract_upkxoslv/ ├── .github/ │ ├── FUNDING.yml │ ├── renovate.json │ └── workflows/ │ ├── codeql-analysis.yml │ └── test.yml ├── .gitignore ├── .swift-format ├── .swiftpm/ │ └── xcode/ │ ├── package.xcworkspace/ │ │ └── contents.xcworkspacedata │ └── xcshareddata/ │ └── xcschemes/ │ ├── App.xcscheme │ ├── DSLConverter.xcscheme │ ├── ExpressionParser.xcscheme │ ├── Matcher.xcscheme │ └── swiftregex-Package.xcscheme ├── .vscode/ │ ├── launch.json │ └── settings.json ├── DEPLOYMENT.md ├── Dockerfile ├── LICENSE ├── Package.resolved ├── Package.swift ├── Public/ │ ├── css/ │ │ ├── common.css │ │ └── highlight.css │ ├── error.html │ ├── favicons/ │ │ ├── browserconfig.xml │ │ └── site.webmanifest │ ├── index.html │ ├── index.js │ ├── js/ │ │ ├── app.js │ │ ├── docs/ │ │ │ └── reference.js │ │ ├── misc/ │ │ │ ├── icons.js │ │ │ └── utils.js │ │ ├── runner.js │ │ ├── state/ │ │ │ ├── decoder.js │ │ │ ├── encoder.js │ │ │ └── worker.js │ │ └── views/ │ │ ├── debugger_highlighter.js │ │ ├── debugger_text.js │ │ ├── dsl_editor.js │ │ ├── dsl_highlighter.js │ │ ├── dsl_view.js │ │ ├── editor.js │ │ ├── error_message.js │ │ ├── expression_field.js │ │ ├── expression_highlighter.js │ │ ├── match_options.js │ │ ├── test_editor.js │ │ └── test_highlighter.js │ ├── robots.txt │ └── scss/ │ └── default.scss ├── README.md ├── SECURITY.md ├── Sources/ │ ├── App/ │ │ ├── Debugger/ │ │ │ ├── Context.swift │ │ │ ├── Debugger.swift │ │ │ └── Executor.swift │ │ ├── Middlewares/ │ │ │ ├── CommonErrorMiddleware.swift │ │ │ └── CustomHeaderMiddleware.swift │ │ ├── Models/ │ │ │ ├── ExecRequest.swift │ │ │ └── ResultResponse.swift │ │ ├── configure.swift │ │ ├── entrypoint.swift │ │ └── routes.swift │ ├── DSLConverter/ │ │ ├── DSLConverter.swift │ │ └── Main.swift │ ├── ExpressionParser/ │ │ ├── ExpressionParser.swift │ │ └── Main.swift │ └── Matcher/ │ ├── Main.swift │ └── Matcher.swift ├── Tests/ │ └── RegexTests/ │ ├── ConverterTests.swift │ ├── ExpressionParserTests.swift │ └── MatcherTests.swift ├── package.json ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: kishikawakatsumi ================================================ FILE: .github/renovate.json ================================================ { "extends": [ "config:recommended" ], "packageRules": [ { "matchUpdateTypes": [ "minor", "patch", "pin", "digest" ], "automerge": true } ] } ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [main] pull_request: # The branches below must be a subset of the branches above branches: [main] schedule: - cron: "37 13 * * 1" jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: ["javascript"] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - name: Checkout repository uses: actions/checkout@v6 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v4 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 ================================================ FILE: .github/workflows/test.yml ================================================ name: CI on: pull_request: branches: [main] workflow_dispatch: env: FONTAWESOME_TOKEN: ${{ secrets.FONTAWESOME_TOKEN }} jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Build run: | set -ex docker build --rm --no-cache --build-arg FONTAWESOME_TOKEN=${{ env.FONTAWESOME_TOKEN }} . ================================================ FILE: .gitignore ================================================ ### Generated by gibo (https://github.com/simonwhitaker/gibo) ### https://raw.github.com/github/gitignore/e5323759e387ba347a9d50f8b0ddd16502eb71d4/Swift.gitignore # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore ## User settings xcuserdata/ ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) *.xcscmblueprint *.xccheckout ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) build/ DerivedData/ *.moved-aside *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 ## Obj-C/Swift specific *.hmap ## App packaging *.ipa *.dSYM.zip *.dSYM ## Playgrounds timeline.xctimeline playground.xcworkspace # Swift Package Manager # # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ # Package.pins # Package.resolved # *.xcodeproj # # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata # hence it is not needed unless you have added a package configuration file to your project # .swiftpm .build/ # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # # Pods/ # # Add this line if you want to avoid checking in source code from the Xcode workspace # *.xcworkspace # Carthage # # Add this line if you want to avoid checking in source code from Carthage dependencies. # Carthage/Checkouts Carthage/Build/ # Accio dependency management Dependencies/ .accio/ # fastlane # # It is recommended to not store the screenshots in the git repo. # Instead, use fastlane to re-generate the screenshots whenever they are needed. # For more information about the recommended setup visit: # https://docs.fastlane.tools/best-practices/source-control/#source-control fastlane/report.xml fastlane/Preview.html fastlane/screenshots/**/*.png fastlane/test_output # Code Injection # # After new code Injection tools there's a generated folder /iOSInjectionProject # https://github.com/johnno1962/injectionforxcode iOSInjectionProject/ ### https://raw.github.com/github/gitignore/e5323759e387ba347a9d50f8b0ddd16502eb71d4/Node.gitignore # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp .cache # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* ### https://raw.github.com/github/gitignore/e5323759e387ba347a9d50f8b0ddd16502eb71d4/Global/macOS.gitignore # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ================================================ FILE: .swift-format ================================================ { "version": 1, "lineLength": 10000, "indentation": { "spaces": 2 } } ================================================ FILE: .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: .swiftpm/xcode/xcshareddata/xcschemes/App.xcscheme ================================================ ================================================ FILE: .swiftpm/xcode/xcshareddata/xcschemes/DSLConverter.xcscheme ================================================ ================================================ FILE: .swiftpm/xcode/xcshareddata/xcschemes/ExpressionParser.xcscheme ================================================ ================================================ FILE: .swiftpm/xcode/xcshareddata/xcschemes/Matcher.xcscheme ================================================ ================================================ FILE: .swiftpm/xcode/xcshareddata/xcschemes/swiftregex-Package.xcscheme ================================================ ================================================ FILE: .vscode/launch.json ================================================ { "configurations": [ { "type": "lldb", "request": "launch", "name": "Debug DSLParser", "program": "${workspaceFolder:swiftregex}/.build/debug/DSLParser", "args": [], "cwd": "${workspaceFolder:swiftregex}", "preLaunchTask": "swift: Build Debug DSLParser" }, { "type": "lldb", "request": "launch", "name": "Release DSLParser", "program": "${workspaceFolder:swiftregex}/.build/release/DSLParser", "args": [], "cwd": "${workspaceFolder:swiftregex}", "preLaunchTask": "swift: Build Release DSLParser" }, { "type": "swift", "request": "launch", "name": "Debug DSLConverter", "args": [], "cwd": "${workspaceFolder:swiftregex}", "preLaunchTask": "swift: Build Debug DSLConverter", "target": "DSLConverter", "configuration": "debug" }, { "type": "swift", "request": "launch", "name": "Release DSLConverter", "args": [], "cwd": "${workspaceFolder:swiftregex}", "preLaunchTask": "swift: Build Release DSLConverter", "target": "DSLConverter", "configuration": "release" }, { "type": "swift", "request": "launch", "name": "Debug ExpressionParser", "args": [], "cwd": "${workspaceFolder:swiftregex}", "preLaunchTask": "swift: Build Debug ExpressionParser", "target": "ExpressionParser", "configuration": "debug" }, { "type": "swift", "request": "launch", "name": "Release ExpressionParser", "args": [], "cwd": "${workspaceFolder:swiftregex}", "preLaunchTask": "swift: Build Release ExpressionParser", "target": "ExpressionParser", "configuration": "release" }, { "type": "swift", "request": "launch", "name": "Debug Matcher", "args": [], "cwd": "${workspaceFolder:swiftregex}", "preLaunchTask": "swift: Build Debug Matcher", "target": "Matcher", "configuration": "debug" }, { "type": "swift", "request": "launch", "name": "Release Matcher", "args": [], "cwd": "${workspaceFolder:swiftregex}", "preLaunchTask": "swift: Build Release Matcher", "target": "Matcher", "configuration": "release" }, { "type": "lldb", "request": "launch", "name": "Debug PatternConverter", "program": ".build/debug/PatternConverter", "args": [], "cwd": "${workspaceFolder:swiftregex}", "preLaunchTask": "swift: Build Debug PatternConverter" }, { "type": "lldb", "request": "launch", "name": "Release PatternConverter", "program": ".build/release/PatternConverter", "args": [], "cwd": "${workspaceFolder:swiftregex}", "preLaunchTask": "swift: Build Release PatternConverter" }, { "type": "swift", "request": "launch", "sourceLanguages": ["swift"], "name": "Debug App", "args": [], "cwd": "${workspaceFolder:swiftregex}", "preLaunchTask": "swift: Build Debug App", "target": "App", "configuration": "debug" }, { "type": "swift", "request": "launch", "sourceLanguages": ["swift"], "name": "Release App", "args": [], "cwd": "${workspaceFolder:swiftregex}", "preLaunchTask": "swift: Build Release App", "target": "App", "configuration": "release" }, { "type": "lldb", "request": "launch", "args": [], "cwd": "${workspaceFolder:swiftregex}", "name": "Debug Debugger", "program": "${workspaceFolder:swiftregex}/.build/debug/Debugger", "preLaunchTask": "swift: Build Debug Debugger" }, { "type": "lldb", "request": "launch", "args": [], "cwd": "${workspaceFolder:swiftregex}", "name": "Release Debugger", "program": "${workspaceFolder:swiftregex}/.build/release/Debugger", "preLaunchTask": "swift: Build Release Debugger" } ] } ================================================ FILE: .vscode/settings.json ================================================ { "lldb.library": "/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/LLDB", "lldb.launch.expressions": "native" } ================================================ FILE: DEPLOYMENT.md ================================================ # Deployment Instructions ## Prerequisites Before deploying, make sure you have the following software installed on your machine: - Node.js (v14 or newer) - Docker (v20.10 or newer) The following environment variables are used for deployment: - `FONTAWESOME_TOKEN`: This token is used for authentication with the FontAwesome service. You need to obtain a valid token from your FontAwesome account and use it here. Please make sure not to expose this token publicly. ## Local Deployment ### Steps: 1. Install the dependencies: ```bash npm install ``` 2. Run Webpack to build the project: ```bash npm run prod ``` 3. Run the application: ```bash swift run ``` You should now be able to see the application running at `localhost:8080`. ## Production Deployment For deploying to production, we recommend using [Render](https://render.com/). Render is a platform that allows you to deploy your application to the cloud with ease. It also provides a free tier that is sufficient for deploying this application. ================================================ FILE: Dockerfile ================================================ FROM node:lts-slim as node WORKDIR /build ARG FONTAWESOME_TOKEN COPY package*.json ./ RUN echo "@fortawesome:registry=https://npm.fontawesome.com/\n//npm.fontawesome.com/:_authToken=${FONTAWESOME_TOKEN}" > ./.npmrc \ && npm ci \ && rm -f ./.npmrc COPY webpack.*.js ./ COPY Public ./Public/ RUN npx webpack --config webpack.prod.js FROM swiftlang/swift:nightly-main-jammy as swift RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ && apt-get -q update && apt-get -q dist-upgrade -y \ && apt-get install -y --no-install-recommends libsqlite3-dev \ && rm -rf /var/lib/apt/lists/* WORKDIR /build COPY --from=node /build /build COPY ./Package.* ./ RUN swift package resolve COPY . . RUN swift build -c release -Xswiftc -DPROCESSOR_MEASUREMENTS_ENABLED -Xswiftc -enable-testing WORKDIR /staging RUN BIN_PATH="$(swift build --package-path /build -c release --show-bin-path)" \ && cp "$BIN_PATH/App" ./ \ && cp "$BIN_PATH/DSLConverter" ./ \ && cp "$BIN_PATH/ExpressionParser" ./ \ && cp "$BIN_PATH/Matcher" ./ \ && find -L "$BIN_PATH/" -regex '.*\.resources$' -exec cp -Ra {} ./ \; RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w ./Resources; } || true FROM swiftlang/swift:nightly-main-jammy RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ && apt-get -q update \ && apt-get -q dist-upgrade -y \ && apt-get -q install -y \ ca-certificates \ tzdata \ && rm -r /var/lib/apt/lists/* RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app vapor WORKDIR /app COPY --from=swift --chown=vapor:vapor /staging /app USER vapor:vapor EXPOSE 8080 ENTRYPOINT ["./App"] CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 Kishikawa Katsumi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Package.resolved ================================================ { "pins" : [ { "identity" : "async-http-client", "kind" : "remoteSourceControl", "location" : "https://github.com/swift-server/async-http-client.git", "state" : { "revision" : "3a5b74a58782c3b4c1f0bc75e9b67b10c2494e8f", "version" : "1.33.1" } }, { "identity" : "async-kit", "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/async-kit.git", "state" : { "revision" : "6bbb83cbf9d886623a967a965c8fb1b73e6566f9", "version" : "1.22.0" } }, { "identity" : "console-kit", "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/console-kit.git", "state" : { "revision" : "32ad16dfc7677b927b225595ed18f3debb32f577", "version" : "4.16.0" } }, { "identity" : "leaf", "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/leaf.git", "state" : { "revision" : "b70a6108e4917f338f6b8848407bf655aa7e405f", "version" : "4.5.1" } }, { "identity" : "leaf-kit", "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/leaf-kit.git", "state" : { "revision" : "6044b844caa858a0c5f2505ac166f5a057c990dc", "version" : "1.14.2" } }, { "identity" : "multipart-kit", "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/multipart-kit.git", "state" : { "revision" : "3498e60218e6003894ff95192d756e238c01f44e", "version" : "4.7.1" } }, { "identity" : "routing-kit", "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/routing-kit.git", "state" : { "revision" : "1a10ccea61e4248effd23b6e814999ce7bdf0ee0", "version" : "4.9.3" } }, { "identity" : "swift-algorithms", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-algorithms.git", "state" : { "revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023", "version" : "1.2.1" } }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser", "state" : { "revision" : "626b5b7b2f45e1b0b1c6f4a309296d1d21d7311b", "version" : "1.7.1" } }, { "identity" : "swift-asn1", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-asn1.git", "state" : { "revision" : "9f542610331815e29cc3821d3b6f488db8715517", "version" : "1.6.0" } }, { "identity" : "swift-async-algorithms", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-async-algorithms.git", "state" : { "revision" : "9d349bcc328ac3c31ce40e746b5882742a0d1272", "version" : "1.1.3" } }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-atomics.git", "state" : { "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", "version" : "1.3.0" } }, { "identity" : "swift-certificates", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-certificates.git", "state" : { "revision" : "24ccdeeeed4dfaae7955fcac9dbf5489ed4f1a25", "version" : "1.18.0" } }, { "identity" : "swift-collections", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { "revision" : "6675bc0ff86e61436e615df6fc5174e043e57924", "version" : "1.4.1" } }, { "identity" : "swift-configuration", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-configuration.git", "state" : { "revision" : "be76c4ad929eb6c4bcaf3351799f2adf9e6848a9", "version" : "1.2.0" } }, { "identity" : "swift-crypto", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-crypto.git", "state" : { "revision" : "bb4ba815dab96d4edc1e0b86d7b9acf9ff973a84", "version" : "4.3.1" } }, { "identity" : "swift-distributed-tracing", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-distributed-tracing.git", "state" : { "revision" : "dc4030184203ffafbb2ec614352487235d747fe0", "version" : "1.4.1" } }, { "identity" : "swift-experimental-string-processing", "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-experimental-string-processing.git", "state" : { "branch" : "main", "revision" : "6a693bb139fa9e4ddf522f80fb7026b3abb0cf1d" } }, { "identity" : "swift-http-structured-headers", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-structured-headers.git", "state" : { "revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b", "version" : "1.6.0" } }, { "identity" : "swift-http-types", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-types.git", "state" : { "revision" : "45eb0224913ea070ec4fba17291b9e7ecf4749ca", "version" : "1.5.1" } }, { "identity" : "swift-log", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { "revision" : "8c0f217f01000dd30f60d6e536569ad4e74291f9", "version" : "1.11.0" } }, { "identity" : "swift-metrics", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-metrics.git", "state" : { "revision" : "f17c111cec972c2a4922cef38cf64f76f7e87886", "version" : "2.8.0" } }, { "identity" : "swift-nio", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { "revision" : "558f24a4647193b5a0e2104031b71c55d31ff83a", "version" : "2.97.1" } }, { "identity" : "swift-nio-extras", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { "revision" : "abcf5312eb8ed2fb11916078aef7c46b06f20813", "version" : "1.33.0" } }, { "identity" : "swift-nio-http2", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-http2.git", "state" : { "revision" : "6d8d596f0a9bfebb925733003731fe2d749b7e02", "version" : "1.42.0" } }, { "identity" : "swift-nio-ssl", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-ssl.git", "state" : { "revision" : "df9c3406028e3297246e6e7081977a167318b692", "version" : "2.36.1" } }, { "identity" : "swift-nio-transport-services", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-transport-services.git", "state" : { "revision" : "60c3e187154421171721c1a38e800b390680fb5d", "version" : "1.26.0" } }, { "identity" : "swift-numerics", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-numerics.git", "state" : { "revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2", "version" : "1.1.1" } }, { "identity" : "swift-service-context", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-service-context.git", "state" : { "revision" : "d0997351b0c7779017f88e7a93bc30a1878d7f29", "version" : "1.3.0" } }, { "identity" : "swift-service-lifecycle", "kind" : "remoteSourceControl", "location" : "https://github.com/swift-server/swift-service-lifecycle.git", "state" : { "revision" : "9829955b385e5bb88128b73f1b8389e9b9c3191a", "version" : "2.11.0" } }, { "identity" : "swift-system", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-system.git", "state" : { "revision" : "7c6ad0fc39d0763e0b699210e4124afd5041c5df", "version" : "1.6.4" } }, { "identity" : "vapor", "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/vapor.git", "state" : { "revision" : "cfd8f434843ac7850e2d97f46c1aa5ddb906cf1c", "version" : "4.121.4" } }, { "identity" : "websocket-kit", "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/websocket-kit.git", "state" : { "revision" : "90bbbdab3ede12c803cfbe91646f291c092517a3", "version" : "2.16.2" } } ], "version" : 2 } ================================================ FILE: Package.swift ================================================ // swift-tools-version:5.9 import PackageDescription let package = Package( name: "swiftregex", platforms: [ .macOS(.v12) ], dependencies: [ .package(url: "https://github.com/swiftlang/swift-experimental-string-processing.git", branch: "main"), .package(url: "https://github.com/vapor/vapor.git", from: "4.121.4"), .package(url: "https://github.com/vapor/leaf.git", from: "4.5.1"), ], targets: [ .executableTarget( name: "DSLConverter", dependencies: [ .product(name: "_StringProcessing", package: "swift-experimental-string-processing"), .product(name: "_RegexParser", package: "swift-experimental-string-processing"), ], swiftSettings: [ .unsafeFlags(["-Xfrontend", "-disable-availability-checking"]), .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)), ] ), .executableTarget( name: "ExpressionParser", dependencies: [ .product(name: "_StringProcessing", package: "swift-experimental-string-processing"), .product(name: "_RegexParser", package: "swift-experimental-string-processing"), ], swiftSettings: [ .unsafeFlags(["-Xfrontend", "-disable-availability-checking"]), .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)), ] ), .executableTarget( name: "Matcher", dependencies: [ .product(name: "_StringProcessing", package: "swift-experimental-string-processing"), .product(name: "_RegexParser", package: "swift-experimental-string-processing"), ], swiftSettings: [ .unsafeFlags(["-Xfrontend", "-disable-availability-checking"]), .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)), ] ), .executableTarget( name: "App", dependencies: [ .product(name: "_StringProcessing", package: "swift-experimental-string-processing"), .product(name: "_RegexParser", package: "swift-experimental-string-processing"), .product(name: "Vapor", package: "vapor"), .product(name: "Leaf", package: "leaf"), ], swiftSettings: [ .unsafeFlags(["-Xfrontend", "-disable-availability-checking"]), .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)), ] ), .testTarget( name: "RegexTests", dependencies: [ .target(name: "DSLConverter"), .target(name: "ExpressionParser"), .target(name: "Matcher"), .target(name: "App"), .product(name: "XCTVapor", package: "vapor"), ] ) ] ) ================================================ FILE: Public/css/common.css ================================================ .active-tick .checkable::after { font-family: "Font Awesome 6 Pro"; content: "\f00c"; color: #0d6efd; display: none; } .active-tick svg { color: #0d6efd; float: right; margin-top: 4px; margin-left: 12px; } .dropdown-item .text-muted { width: 86%; font-size: 80%; } .error-message { font-family: arial; font-size: 90%; background: rgba(255, 0, 0, 0.2); } .CodeMirror * { font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; max-height: 90vh; } .CodeMirror-line .cm-space::before, .CodeMirror-line .cm-special::before { color: rgba(127, 127, 127, 0.33); content: "␣"; position: absolute; } .CodeMirror-line .cm-special::before { color: #d22; } .CodeMirror-line .cm-tab { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAYAAAAkuj5RAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTM4IDc5LjE1OTgyNCwgMjAxNi8wOS8xNC0wMTowOTowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTcgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6REZENEEyN0Q3NTc0MTFFNzlFMTZGQ0Q1MEREODEyREEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6REZENEEyN0U3NTc0MTFFNzlFMTZGQ0Q1MEREODEyREEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpERkQ0QTI3Qjc1NzQxMUU3OUUxNkZDRDUwREQ4MTJEQSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpERkQ0QTI3Qzc1NzQxMUU3OUUxNkZDRDUwREQ4MTJEQSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PgWEz28AAABVSURBVHja7JVBCgAgCAQt+rf6cstjYN4iFxoQxJMjos3MCJlO4HyB26iqZLXyAswskQTUCmUSI7OruE4usxX9jCKELKK8w04e6Qqdmnfa/8SPmQIMANcZrZCVJGBIAAAAAElFTkSuQmCC); background-position: 100%; background-repeat: no-repeat; } .editor.multiline .CodeMirror-line:not(:last-child) > span:after { pointer-events: none; color: rgba(127, 127, 127, 0.33); content: "¬"; } .dropdown-menu { min-width: 200px; } .button-circle { width: 2rem; height: 2rem; } .btn:focus { outline: none; box-shadow: none; } #match-count { font-size: 90%; } #debugger-button { background-color: rgba(51, 51, 51, 0.05); border-radius: 15px; border-width: 1px; border-color: #dee2e6; border-style: solid; color: #333333; cursor: pointer; display: inline-block; list-style: none; margin: 0 6px; padding: 4px 16px; text-align: center; transition: all 200ms; vertical-align: baseline; white-space: nowrap; user-select: none; -webkit-user-select: none; touch-action: manipulation; font-size: 90%; font-weight: 600; } #debugger-button:hover { background-color: #2222221a; } #debugger-button:disabled { background-color: #e9ecef; color: #6c757d; cursor: not-allowed; } #debugger-regex { font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; } @media (min-width: 576px) { .root-container { height: 100%; } } ================================================ FILE: Public/css/highlight.css ================================================ .exp-related { border-bottom: solid 1px rgba(16, 17, 18, 0.3); border-top: solid 1px rgba(16, 17, 18, 0.3); margin-bottom: -1px; margin-top: -1px; } .exp-related-left { border-left: solid 1px rgba(16, 17, 18, 0.3); margin-left: -1px; } .exp-related-right { border-right: solid 1px rgba(16, 17, 18, 0.3); margin-right: -1px; } .exp-selected { border-top: solid 2px rgba(16, 17, 18, 0.3); border-bottom: solid 2px rgba(16, 17, 18, 0.3); } .exp-selected-left { border-left: solid 2px rgba(16, 17, 18, 0.3); margin-left: -2px; } .exp-selected-right { border-right: solid 2px rgba(16, 17, 18, 0.3); margin-right: -2px; } .exp-error { border-bottom: solid 2px #d22; } .exp-warning { border-bottom: dotted 2px #d22; } .exp-char { color: #101112; } .exp-decorator { color: #b7bcc0; } .exp-esc { color: #c0c; } .exp-lazy, .exp-possessive, .exp-quant { color: #58f; } .exp-alt { color: #0a0; } .exp-anchor { color: #840; } .exp-group, .exp-lookaround, .exp-ref { color: #0a0; } .exp-charclass, .exp-set, .exp-subst { color: #d70; } .exp-group-0 { background: rgba(0, 238, 0, 0.11); } .exp-group-1 { background: rgba(0, 238, 0, 0.22); } .exp-group-2 { background: rgba(0, 238, 0, 0.33); } .exp-group-3 { background: rgba(0, 238, 0, 0.44); } .exp-group-set { background: rgba(255, 238, 0, 0.3); } .exp-comment { color: #b7bcc0; background: rgba(16, 17, 18, 0.05); font-style: italic; border-bottom: solid 3px #d7dadc; } .exp-special { color: #c0c; } .exp-syntax-error { border-top: solid 2px #d22; } span.match-char { background: rgba(112, 176, 224, 0.5); color: #101112; border-right: solid 1px #ffffff; border-left: solid 1px #ffffff; margin-right: -2px; } span.match-left { border-left: solid 2px rgba(255, 0, 0, 0.2); margin-left: -2px; } span.match-right { border-right: solid 2px rgba(255, 0, 0, 0.2); margin-right: -2px; } span.error { color: #d22; font-weight: 700; } span.error.warning { color: #d22; } span.highlight { background: rgba(255, 128, 0, 0.5); color: #101112; } span.debuggermatch { background: rgba(112, 176, 224, 0.5); color: #101112; border-right: solid 1px rgba(112, 176, 224, 0.5); border-left: solid 1px rgba(112, 176, 224, 0.5); } span.debuggerbacktrack { background: rgba(255, 0, 0, 0.2); } ================================================ FILE: Public/error.html ================================================ #(title) | #(status) - #(error)

#(error) Error #(status)

#(reason)

================================================ FILE: Public/favicons/browserconfig.xml ================================================ #2d89ef ================================================ FILE: Public/favicons/site.webmanifest ================================================ { "name": "", "short_name": "", "icons": [ { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } ], "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" } ================================================ FILE: Public/index.html ================================================ Swift Regex
================================================ FILE: Public/index.js ================================================ "use strict"; import "./scss/default.scss"; import "codemirror/lib/codemirror.css"; import "tippy.js/dist/tippy.css"; import "./css/common.css"; import "./css/highlight.css"; import "./js/misc/icons"; import "bootstrap"; import { App } from "./js/app"; new App(); ================================================ FILE: Public/js/app.js ================================================ "use strict"; import { Tooltip } from "bootstrap"; import { ExpressionField } from "./views/expression_field"; import { MatchOptions } from "./views/match_options"; import { TestEditor } from "./views/test_editor"; import { DSLView } from "./views/dsl_view"; import { DSLEditor } from "./views/dsl_editor"; import { DebuggerText } from "./views/debugger_text"; import { Runner } from "./runner"; export class App { constructor() { this.init(); } init() { [].slice .call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) .map((trigger) => { return new Tooltip(trigger); }); this.expressionField = new ExpressionField( document.getElementById("expression-field-container"), ); this.expressionField.addEventListener("change", () => this.onExpressionFieldChange(), ); this.matchOptions = new MatchOptions(); this.matchOptions.addEventListener("change", () => this.onExpressionFieldChange(), ); this.patternTestEditor = new TestEditor( document.querySelector(".test-editor-container"), ); this.patternTestEditor.addEventListener("change", () => this.onPatternTestEditorChange(), ); this.debuggerText = new DebuggerText( document.getElementById("debugger-text-container"), ); this.debuggerGoStartButton = document.getElementById("debugger-go-start"); this.debuggerGoStartButton.addEventListener("click", () => { const matchStepRange = document.getElementById("debugger-step-range"); matchStepRange.value = 1; this.onDebuggerStepChange(); }); this.debuggerStepBackwardButton = document.getElementById( "debugger-step-backward", ); this.debuggerStepBackwardButton.addEventListener("click", () => { const matchStepRange = document.getElementById("debugger-step-range"); matchStepRange.value = Math.max(1, parseInt(matchStepRange.value) - 1); this.onDebuggerStepChange(); }); this.debuggerStepForwardButton = document.getElementById( "debugger-step-forward", ); this.debuggerStepForwardButton.addEventListener("click", () => { const matchStepRange = document.getElementById("debugger-step-range"); matchStepRange.value = Math.min( parseInt(matchStepRange.value) + 1, parseInt(matchStepRange.max), ); this.onDebuggerStepChange(); }); this.debuggerGoEndButton = document.getElementById("debugger-go-end"); this.debuggerGoEndButton.addEventListener("click", () => { const matchStepRange = document.getElementById("debugger-step-range"); matchStepRange.value = matchStepRange.max; this.onDebuggerStepChange(); }); const matchStepRange = document.getElementById("debugger-step-range"); matchStepRange.addEventListener("input", () => { this.onDebuggerStepChange(); }); this.debuggerModal = document.getElementById("debugger-modal"); this.debuggerModal.addEventListener("shown.bs.modal", () => this.launchDebugger(), ); this.dslView = new DSLView(document.getElementById("dsl-view-container")); this.runner = new Runner(); this.runner.onready = this.onRunnerReady.bind(this); this.runner.onresponse = this.onRunnerResponse.bind(this); this.stateProxy = { builder: "", text2: "", }; if (window.Worker) { this.stateRestorationWorker = new Worker( new URL("./state/worker.js", import.meta.url), ); if (window.location.search) { this.decodeState(); } else { this.expressionField.setDefaultValue(); this.patternTestEditor.setDefaultValue(); this.stateProxy.builder = DSLEditor.defaultValue; this.stateProxy.text2 = TestEditor.defaultValue; } this.startStateRestoration(); } } startStateRestoration() { if (!this.stateRestorationWorker) { return; } const debounce = (() => { const timers = {}; return function (callback, delay, id) { delay = delay || 400; id = id || "duplicated event"; if (timers[id]) { clearTimeout(timers[id]); } timers[id] = setTimeout(callback, delay); }; })(); this.stateRestorationWorker.onmessage = (e) => { if (e.data && e.data.type === "encode") { debounce( () => { history.replaceState(null, "", e.data.value); }, 400, "update_location", ); } if (e.data && e.data.type === "decode") { const expressionField = this.expressionField; const matchOptions = this.matchOptions; const patternTestEditor = this.patternTestEditor; if (expressionField) { expressionField.value = e.data.value.pattern; } if (matchOptions) { matchOptions.value = e.data.value.options; } if (patternTestEditor) { patternTestEditor.value = e.data.value.text1; } } }; } encodeState() { if (!this.stateRestorationWorker) { return; } const expressionField = this.expressionField; const matchOptions = this.matchOptions; const patternTestEditor = this.patternTestEditor; this.stateRestorationWorker.postMessage({ type: "encode", value: { pattern: expressionField ? expressionField.value : "", options: matchOptions ? matchOptions.value : [], text1: patternTestEditor ? patternTestEditor.value : "", }, }); } decodeState() { if (!this.stateRestorationWorker) { return; } this.stateRestorationWorker.postMessage({ type: "decode", value: window.location.search, }); } updateMatchCount(count, id) { const matchCount = document.getElementById(id); if (count > 1) { matchCount.textContent = `${count} matches`; } else if (count > 0) { matchCount.textContent = "1 match"; } else { matchCount.textContent = "no match"; } } launchDebugger() { const expressionField = this.expressionField; const patternTestEditor = this.patternTestEditor; const expression = expressionField.value; const text = patternTestEditor.value; const matchStepRange = document.getElementById("debugger-step-range"); matchStepRange.value = 1; matchStepRange.min = 1; const matchStep = document.getElementById("debugger-match-step"); matchStep.textContent = "1"; const debuggerPattern = document.getElementById("debugger-regex"); debuggerPattern.value = expression; this.debuggerText.value = text; this.onDebuggerStepChange(); } onExpressionFieldChange() { if (!this.expressionField.value) { this.expressionField.tokens = []; this.expressionField.error = null; this.dslView.value = ""; this.dslView.error = null; this.updateMatchCount(0, "match-count"); return; } this.run(); this.encodeState(); } run() { const methods = ["parseExpression", "convertToDSL", "match"]; const params = { pattern: this.expressionField.value, text: this.patternTestEditor.value, matchOptions: this.matchOptions.value, }; if (this.runner.isReady) { for (const method of methods) { this.runner.run({ method: method, ...params, }); } } else { const headers = { Accept: "application/json", "Content-Type": "application/json", }; for (const method of methods) { const body = JSON.stringify({ method: method, ...params, }); fetch(`/api/rest/${method}`, { method: "POST", headers, body }) .then((response) => { return response.json(); }) .then((response) => { this.onRunnerResponse(response); }); } } } onMatchOptionsChange() { this.onPatternTestEditorChange(); } onPatternTestEditorChange() { const method = "match"; const params = { method, pattern: this.expressionField.value, text: this.patternTestEditor.value, matchOptions: this.matchOptions.value, }; if (this.runner.isReady) { this.runner.run(params); } else { const headers = { Accept: "application/json", "Content-Type": "application/json", }; const body = JSON.stringify(params); fetch(`/api/rest/${method}`, { method: "POST", headers, body }) .then((response) => { return response.json(); }) .then((response) => { this.onRunnerResponse(response); }); } this.encodeState(); } onDebuggerStepChange() { const method = "debug"; const params = { method, pattern: document.getElementById("debugger-regex").value, text: this.debuggerText.value, matchOptions: this.matchOptions.value, step: document.getElementById("debugger-step-range").value, }; if (this.runner.isReady) { this.runner.run(params); } else { const headers = { Accept: "application/json", "Content-Type": "application/json", }; const body = JSON.stringify(params); fetch(`/api/rest/${method}`, { method: "POST", headers, body }) .then((response) => { return response.json(); }) .then((response) => { this.onRunnerResponse(response); }); } } onRunnerReady() { const value = this.expressionField.value; if (value) { this.onExpressionFieldChange(); } } onRunnerResponse(response) { switch (response.method) { case "parseExpression": if (response.result) { const tokens = JSON.parse(response.result); this.expressionField.tokens = tokens; } else { this.expressionField.tokens = []; } if (response.error) { try { const error = JSON.parse(response.error); if (error) { this.expressionField.error = error; } } catch (e) { this.expressionField.error = response.error; } } else { this.expressionField.error = null; } break; case "convertToDSL": if (response.result) { this.dslView.value = JSON.parse(response.result); } if (response.error) { try { const error = JSON.parse(response.error); if (error) { this.dslView.error = error; } } catch (e) { this.dslView.error = response.error; } } else { this.dslView.error = null; } break; case "match": const debuggerButton = document.getElementById("debugger-button"); if (response.result) { const matches = JSON.parse(response.result); this.patternTestEditor.matches = matches; this.updateMatchCount(matches.length, "match-count"); debuggerButton.disabled = matches.length === 0; } else { this.patternTestEditor.matches = []; this.updateMatchCount(0, "match-count"); debuggerButton.disabled = true; } this.patternTestEditor.error = response.error; break; case "debug": if (response.result) { const metrics = JSON.parse(response.result); const matchStep = document.getElementById("debugger-match-step"); matchStep.textContent = metrics.step; const matchStepRange = document.getElementById("debugger-step-range"); matchStepRange.max = metrics.stepCount; const matchStepRangeMax = document.getElementById( "debugger-step-range-max", ); matchStepRangeMax.textContent = metrics.stepCount; const instructions = document.getElementById("debugger-instructions"); instructions.innerHTML = ""; metrics.instructions.forEach((instruction, i) => { const tr = document.createElement("tr"); if (i === metrics.programCounter) { tr.classList.add("table-primary"); } const programCounter = document.createElement("td"); programCounter.style = "width: 1%; text-align: right; white-space: nowrap; padding-left: 1em; padding-right: 1em;"; programCounter.textContent = i + 1; tr.appendChild(programCounter); const inst = document.createElement("td"); inst.style = "white-space: nowrap;"; inst.textContent = instruction; tr.appendChild(inst); instructions.appendChild(tr); }); const totalCycleCount = document.getElementById( "debugger-total-cycle-count", ); totalCycleCount.textContent = metrics.totalCycleCount; const resets = document.getElementById("debugger-resets"); resets.textContent = metrics.resets; const backtracks = document.getElementById("debugger-backtracks"); const previousBacktracks = Number(backtracks.textContent); backtracks.textContent = metrics.backtracks; this.debuggerText.highlighter.draw( metrics.traces, previousBacktracks < metrics.backtracks ? metrics.failure : null, ); } break; } } } ================================================ FILE: Public/js/docs/reference.js ================================================ "use strict"; export class Reference { static get(category, key) { return references[category][key]; } } const references = { charclasses: { label: "Character classes", desc: "Character classes match a character from a specific set. There are a number of predefined character classes and you can also define your own sets.", set: { title: "Character set", detail: "Match any character in the set.", }, setnot: { title: "Negated set", detail: "Match any character that is not in the set.", }, range: { title: "Range", detail: "Matches a character in the range {{getChar(prev)}} to {{getChar(next)}} (char code {{prev.code}} to {{next.code}}). {{getInsensitive()}}", }, posixcharclass: { title: "POSIX class", detail: "Matches any character in the specified POSIX class. Must be in a character set. For example, [[:alnum:]$] will match alphanumeric characters and $.", }, dot: { title: "Dot", detail: "Matches any character {{getDotAll()}}.", }, matchanyset: { title: "Match any", detail: "A character set that can be used to match any character, including line breaks, without the dotall flag (s)." + "

An alternative is [^], but it is not supported in all browsers.

", }, unicodegrapheme: { title: "Unicode grapheme", detail: "Matches any single unicode grapheme (ie. character).", }, word: { title: "Word", detail: "Matches any word character (alphanumeric & underscore).", }, notword: { title: "Not word", detail: "Matches any character that is not a word character (alphanumeric & underscore).", }, digit: { title: "Digit", detail: "Matches any digit character (0-9).", }, notdigit: { title: "Not digit", detail: "Matches any character that is not a digit character (0-9).", }, whitespace: { title: "Whitespace", detail: "Matches any whitespace character (spaces, tabs, line breaks).", }, notwhitespace: { title: "Not whitespace", detail: "Matches any character that is not a whitespace character (spaces, tabs, line breaks).", }, hwhitespace: { title: "Horizontal whitespace", detail: "Matches any horizontal whitespace character (spaces, tabs).", }, nothwhitespace: { title: "Not horizontal whitespace", detail: "Matches any character that is not a horizontal whitespace character (spaces, tabs).", }, vwhitespace: { title: "Vertical whitespace", detail: "Matches any vertical whitespace character (line breaks).", }, notvwhitespace: { title: "Not vertical whitespace", detail: "Matches any character that is not a vertical whitespace character (line breaks).", }, linebreak: { title: "Line break", detail: "Matches any line break character, including the CRLF pair, and CR / LF individually.", }, notlinebreak: { title: "Not line break", detail: "Matches any character that is not a line break.", }, unicodecat: { title: "Unicode category", detail: "Matches any character in the '{{getUniCat()}}' unicode category.", }, notunicodecat: { title: "Not unicode category", detail: "Matches any character that is not in the '{{getUniCat()}}' unicode category.", }, unicodescript: { title: "Unicode script", detail: "Matches any character in the '{{value}}' unicode script.", }, notunicodescript: { title: "Not unicode script", detail: "Matches any character that is not in the '{{value}}' unicode script.", }, binary: { title: "Unicode property escapes", detail: "Allows for matching characters based on their Unicode properties.", }, script: { title: "Script", detail: "No overview available.", }, scriptextension: { title: "Script extension", detail: "No overview available.", }, named: { title: "Named", detail: "No overview available.", }, numerictype: { title: "Numeric type", detail: "No overview available.", }, numericvalue: { title: "Numeric value", detail: "No overview available.", }, mapping: { title: "Mapping", detail: "No overview available.", }, ccc: { title: "Custom character class", detail: "No overview available.", }, age: { title: "Age", detail: "No overview available.", }, block: { title: "Block", detail: "No overview available.", }, pcrespecial: { title: "PCRE special", detail: "No overview available.", }, javaspecial: { title: "Java special", detail: "No overview available.", }, graphemecluster: { title: "Grapheme cluster", detail: "No overview available.", }, trueanychar: { title: "Any character", detail: "Equivalent to (?m:.)", }, textsegment: { title: "Text segment", detail: `Equivalent to (?>\O(?:\Y\O)*)`, }, nottextsegment: { title: "Not text segment", detail: "Text segment non-boundary", }, keyboardcontrol: { title: "Control char", detail: "No overview available.", }, keyboardmeta: { title: "Meta", detail: "No overview available.", }, keyboardmetacontrol: { title: "Meta control char", detail: "No overview available.", }, namedcharacter: { title: "Named character", detail: "No overview available.", }, subpattern: { title: "Subpattern", detail: "No overview available.", }, callout: { title: "Callout", detail: "No overview available.", }, accept: { title: "Backtracking control", detail: `This verb causes the match to end successfully, skipping the remainder of the pattern. When inside a recursion, only the innermost pattern is ended immediately.`, }, fail: { title: "Backtracking control", detail: `This verb causes the match to fail, forcing backtracking to occur. It is equivalent to (?!) but easier to read. The Perl documentation notes that it is probably useful only when combined with (?{}) or (??{}).`, }, mark: { title: "Backtracking control", detail: "No overview available.", }, commit: { title: "Backtracking control", detail: `This verb causes the whole match to fail outright if the rest of the pattern does not match. Even if the pattern is unanchored, no further attempts to find a match by advancing the start point take place. Once (*COMMIT) has been passed, re:run/3 is committed to finding a match at the current starting point, or not at all.`, }, prune: { title: "Backtracking control", detail: `acktracking cannot cross (*PRUNE). In simple cases, the use of (*PRUNE) is just an alternative to an atomic group or possessive quantifier, but there are some uses of (*PRUNE) that cannot be expressed in any other way.`, }, skip: { title: "Backtracking control", detail: `This verb is like (*PRUNE), except that if the pattern is unanchored, the "bumpalong" advance is not to the next character, but to the position in the subject where (*SKIP) was encountered. (*SKIP) signifies that whatever text was matched leading up to it cannot be part of a successful match.`, }, then: { title: "Backtracking control", detail: `This verb causes a skip to the next alternation if the rest of the pattern does not match. That is, it cancels pending backtracking, but only within the current alternation.`, }, }, anchors: { label: "Anchors", desc: "Anchors are unique in that they match a position within a string, not a character.", bos: { title: "Beginning of string", detail: "Matches the beginning of the string.", }, eos: { title: "End of string", detail: "Matches the end of the string.", }, abseos: { title: "Strict end of string", detail: "Matches the end of the string. Unlike $ or \\Z, it does not allow for a trailing newline.", }, bof: { title: "Beginning", detail: "Matches the beginning of the string, or the beginning of a line if the multiline flag (m) is enabled.", }, eof: { title: "End", detail: "Matches the end of the string, or the end of a line if the multiline flag (m) is enabled.", }, wordboundary: { title: "Word boundary", detail: "Matches a word boundary position between a word character and non-word character or position (start / end of string).", }, notwordboundary: { title: "Not word boundary", detail: "Matches any position that is not a word boundary.", }, prevmatchend: { title: "Previous match end", detail: "Matches the end position of the previous match.", }, invalid: { title: "Invalid character class", detail: "No overview available.", }, }, escchars: { label: "Escaped characters", desc: "Escape sequences can be used to insert reserved, special, and unicode characters. All escaped characters begin with the \\ character.", reservedchar: { title: "Reserved characters", detail: "The following character have special meaning, and should be preceded by a \\ (backslash) to represent a literal character:" + "

{{getEscChars()}}

" + "

Within a character set, only \\, -, and ] need to be escaped.

", }, escoctal: { title: "Octal escape", detail: "Octal escaped character in the form \\000.", }, eschexadecimal: { title: "Hexadecimal escape", detail: "Hexadecimal escaped character in the form \\xFF.", }, escunicodeu: { title: "Unicode escape", detail: "Unicode escaped character in the form \\uFFFF", }, escunicodeub: { title: "Extended unicode escape", detail: "Unicode escaped character in the form \\u{FFFF}.", }, escunicodexb: { title: "Unicode escape", detail: "Unicode escaped character in the form \\x{FF}.", }, esccontrolchar: { title: "Control character escape", detail: "Escaped control character in the form \\cZ.", }, escsequence: { title: "Escape sequence", detail: "Matches the literal string '{{value}}'.", }, }, groups: { label: "Groups & References", desc: "Groups allow you to combine a sequence of tokens to operate on them together. Capture groups can be referenced by a backreference and accessed separately in the results.", group: { title: "Capturing group #{{group.num}}", detail: "Groups multiple tokens together and creates a capture group for extracting a substring or using a backreference.", }, namedgroup: { title: "Named capturing group", detail: "Creates a capturing group named '{{name}}'.", }, namedref: { title: "Named reference", detail: "Matches the results of the capture group named '{{group.name}}'.", }, numref: { title: "Numeric reference", detail: "Matches the results of capture group #{{group.num}}.", }, branchreset: { title: "Branch reset group", detail: "Define alternative groups that share the same group numbers.", }, noncapgroup: { title: "Non-capturing group", detail: "Groups multiple tokens together without creating a capture group.", }, atomic: { title: "Atomic group", detail: "Non-capturing group that discards backtracking positions once matched.", }, define: { title: "Define", detail: "Used to define named groups for use as subroutines without including them in the match.", }, numsubroutine: { title: "Numeric subroutine", detail: "Matches the expression in capture group #{{group.num}}.", }, namedsubroutine: { title: "Named subroutine", detail: "Matches the expression in the capture group named '{{group.name}}'.", }, balancedcapture: { title: "Balancing group", detail: "This allows nested constructs to be matched, such as parentheses or HTML tags. The previously defined group to balance against is specified by previous. Captures subpattern as a named group specified by name, or name can be omitted to capture as an unnamed group.", }, absentfunction: { title: "Absent function", detail: "No overview available.", }, }, lookaround: { label: "Lookaround", desc: "Lookaround lets you match a group before (lookbehind) or after (lookahead) your main pattern without including it in the result." + "

Negative lookarounds specify a group that can NOT match before or after the pattern.

", poslookahead: { title: "Positive lookahead", detail: "Matches a group after the main expression without including it in the result.", }, neglookahead: { title: "Negative lookahead", detail: "Specifies a group that can not match after the main expression (if it matches, the result is discarded).", }, poslookbehind: { title: "Positive lookbehind", detail: "Matches a group before the main expression without including it in the result.", }, neglookbehind: { title: "Negative lookbehind", detail: "Specifies a group that can not match before the main expression (if it matches, the result is discarded).", }, keepout: { title: "Keep out", detail: "Keep text matched so far out of the returned match, essentially discarding the match up to this point.", }, nonatomicposlookahead: { title: "Non-atomic Positive lookahead", detail: "No overview available.", }, nonatomicposlookbehind: { title: "Non-atomic Positive lookbehind", detail: "No overview available.", }, scriptrun: { title: "Script run", detail: "No overview available.", }, atomicscriptrun: { title: "Atomic script run", detail: "No overview available.", }, changematchingoptions: { title: "Change matching options", detail: "No overview available.", }, }, quants: { label: "Quantifiers & Alternation", desc: "Quantifiers indicate that the preceding token must be matched a certain number of times. By default, quantifiers are greedy, and will match as many characters as possible." + "
Alternation acts like a boolean OR, matching one sequence or another.", plus: { title: "Plus", detail: "Matches 1 or more of the preceding token.", }, star: { title: "Star", detail: "Matches 0 or more of the preceding token.", }, quant: { title: "Quantifier", detail: "Match {{getQuant()}} of the preceding token.", }, opt: { title: "Optional", detail: "Matches 0 or 1 of the preceding token, effectively making it optional.", }, lazy: { title: "Lazy", detail: "Makes the preceding quantifier {{getLazy()}}, causing it to match as {{getLazyFew()}} characters as possible.", }, possessive: { title: "Possessive", detail: "Makes the preceding quantifier possessive. It will match as many characters as possible, and will not release them to match subsequent tokens.", }, alt: { title: "Alternation", detail: "Acts like a boolean OR. Matches the expression before or after the |.", }, }, other: { label: "Special", desc: "Tokens that don't quite fit anywhere else.", comment: { title: "Comment", detail: "Allows you to insert a comment into your expression that is ignored when finding a match.", }, conditional: { title: "Conditional", detail: "Conditionally matches one of two options based on whether a lookaround is matched.", }, conditionalgroup: { title: "Group conditional", detail: "Conditionally matches one of two options based on whether group '{{name}}' matched.", }, recursion: { title: "Recursion", detail: "Attempts to match the full expression again at the current position.", }, mode: { title: "Mode modifier", detail: "{{~getDesc()}}{{~getModes()}}", }, interpolation: { title: "Interpolation", detail: "No overview available.", }, }, subst: { label: "Substitution", desc: "These tokens are used in a substitution string to insert different parts of the match.", "subst_$&match": { title: "Match", detail: "Inserts the matched text.", }, subst_0match: { title: "Match", detail: "Inserts the matched text.", }, subst_group: { title: "Capture group", detail: "Inserts the results of capture group #{{group.num}}.", }, subst_$before: { title: "Before match", detail: "Inserts the portion of the source string that precedes the match.", }, subst_$after: { title: "After match", detail: "Inserts the portion of the source string that follows the match.", }, subst_$esc: { title: "Escaped $", detail: "Inserts a dollar sign character ($).", }, subst_esc: { title: "Escaped characters", detail: "For convenience, these escaped characters are supported in the Replace string in RegExr: \\n, \\r, \\t, \\\\, and unicode escapes \\uFFFF. This may vary in your deploy environment.", }, }, flags: { label: "Flags", desc: "Expression flags change how the expression is interpreted. Flags follow the closing forward slash of the expression (ex. /.+/igm ).", caseinsensitive: { title: "Ignore case", detail: "Makes the whole expression case-insensitive.", }, global: { title: "Global search", detail: "Retain the index of the last match, allowing iterative searches.", }, multiline: { title: "Multiline", detail: "Beginning/end anchors (^/$) will match the start/end of a line.", }, unicode: { title: "Unicode", detail: "Enables \\x{FFFFF} unicode escapes.", }, sticky: { title: "Sticky", detail: "The expression will only match from its lastIndex position and ignores the global (g) flag if set.", }, dotall: { title: "Dot all", detail: "Dot (.) will match any character, including newline.", }, extended: { title: "Extended", detail: "Literal whitespace characters are ignored, except in character sets.", }, ungreedy: { title: "Ungreedy", detail: "Makes quantifiers ungreedy (lazy) by default.", }, }, misc: { label: "Miscellaneous", desc: "No overview available.", ignorews: { title: "Ignored whitespace", detail: "Whitespace character ignored due to the extended flag or mode.", }, extnumref: { title: "Numeric reference", detail: "Matches the results of capture group #{{group.num}}.", }, char: { title: "Character", detail: "Matches a {{getChar()}} character (char code {{code}}). {{getInsensitive()}}", }, escchar: { title: "Escaped character", detail: "Matches a {{getChar()}} character (char code {{code}}).", }, open: { title: "Open", detail: "Indicates the start of a regular expression.", }, close: { title: "Close", detail: "Indicates the end of a regular expression and the start of expression flags.", }, condition: { title: "Condition", detail: "The lookaround to match in resolving the enclosing conditional statement. See 'conditional' in the Reference for info.", }, conditionalelse: { title: "Conditional else", detail: "Delimits the 'else' portion of the conditional.", }, ERROR: { title: "Error", detail: "Errors in the expression are underlined in red. Roll over errors for more info.", }, PREG_INTERNAL_ERROR: { title: "Internal error", detail: "Internal PCRE error", }, PREG_BACKTRACK_LIMIT_ERROR: { title: "Backtrack limit error", detail: "Backtrack limit was exhausted.", }, PREG_RECURSION_LIMIT_ERROR: { title: "Recursion limit error", detail: "Recursion limit was exhausted", }, PREG_BAD_UTF8_ERROR: { title: "Bad UTF8 error", detail: "Malformed UTF-8 data", }, PREG_BAD_UTF8_OFFSET_ERROR: { title: "Bad UTF8 offset error", detail: "Malformed UTF-8 data", }, any: { title: "Any", detail: "No overview available.", }, assigned: { title: "Assigned", detail: "No overview available.", }, ascii: { title: "Unicode property", detail: "Matches any characters in the ASCII script extension.", }, }, errors: { groupopen: "Unmatched opening parenthesis.", groupclose: "Unmatched closing parenthesis.", setopen: "Unmatched opening square bracket.", rangerev: "Range values reversed. Start char code is greater than end char code.", quanttarg: "Invalid target for quantifier.", quantrev: "Quantifier minimum is greater than maximum.", esccharopen: "Dangling backslash.", esccharbad: "Unrecognized or malformed escape character.", unicodebad: "Unrecognized unicode category or script.", posixcharclassbad: "Unrecognized POSIX character class.", posixcharclassnoset: "POSIX character class must be in a character set.", notsupported: 'The "{{~getLabel()}}" feature is not supported in this flavor of RegEx.', fwdslash: "Unescaped forward slash. This may cause issues if copying/pasting this expression into code.", esccharbad: "Invalid escape sequence.", servercomm: "An error occurred while communicating with the server.", extraelse: "Extra else in conditional group.", unmatchedref: 'Reference to non-existent group "{{name}}".', modebad: 'Unrecognized mode flag "{{errmode}}".', badname: "Group name can not start with a digit.", dupname: "Duplicate group name.", branchreseterr: "Branch Reset. Results will be ok, but RegExr's parser does not number branch reset groups correctly. Coming soon!", timeout: "The expression took longer than 250ms to execute.", // TODO: can we couple this to the help content somehow? // warnings: jsfuture: 'The "{{~getLabel()}}" feature may not be supported in all browsers.', infinite: "The expression can return empty matches, and may match infinitely in some use cases.", // TODO: can we couple this to the help content somehow? }, empty: { empty: { title: "Empty", detail: "No overview available.", }, }, }; ================================================ FILE: Public/js/misc/icons.js ================================================ "use strict"; import { config, library, dom } from "@fortawesome/fontawesome-svg-core"; import { faFlag, faOctagonXmark, faStethoscope, faHeart, faBackwardStep, faForwardStep, faCaretLeft, faCaretRight, } from "@fortawesome/pro-solid-svg-icons"; import { faAt, faCheck, faCommentAltSmile, } from "@fortawesome/pro-regular-svg-icons"; import { faMonitorHeartRate } from "@fortawesome/pro-light-svg-icons"; import { faSwift, faGithub } from "@fortawesome/free-brands-svg-icons"; config.searchPseudoElements = true; library.add( faFlag, faOctagonXmark, faStethoscope, faHeart, faBackwardStep, faForwardStep, faCaretLeft, faCaretRight, faAt, faCheck, faCommentAltSmile, faMonitorHeartRate, faSwift, faGithub ); dom.watch(); ================================================ FILE: Public/js/misc/utils.js ================================================ "use strict"; const Utils = {}; export default Utils; Utils.copy = function (target, source) { for (let n in source) { target[n] = source[n]; } return target; }; Utils.clone = function (o) { // this seems hacky, but it's the fastest, easiest approach for now: return JSON.parse(JSON.stringify(o)); }; Utils.htmlSafe = function (str) { return str == null ? "" : ("" + str).replace(/&/g, "&").replace(/ 0 && str.length > length; if (b) { str = str.substr(0, length - 1); } if (htmlSafe) { str = Utils.htmlSafe(str); } return !b ? str : str + (tag && "<" + tag + ">") + "\u2026" + (tag && ""); }; Utils.unescSubstStr = function (str) { if (!str) { return ""; } return str.replace( Utils.SUBST_ESC_RE, (a, b, c) => Utils.SUBST_ESC_CHARS[b] || String.fromCharCode(parseInt(c, 16)) ); }; Utils.getRegExp = function (str) { // returns a JS RegExp object. let match = str.match(/^\/(.+)\/([a-z]+)?$/), regex = null; try { regex = match ? new RegExp(match[1], match[2] || "") : new RegExp(str, "g"); } catch (e) {} return regex; }; Utils.decomposeRegEx = function (str, delim = "/") { let re = new RegExp("^" + delim + "(.*)" + delim + "([igmsuUxy]*)$"); let match = re.exec(str); if (match) { return { source: match[1], flags: match[2] }; } else { return { source: str, flags: "g" }; } }; Utils.isMac = function () { return !!navigator.userAgent.match(/Mac\sOS/i); }; Utils.getCtrlKey = function () { return Utils.isMac() ? "cmd" : "ctrl"; }; Utils.now = function () { return window.performance ? performance.now() : Date.now(); }; Utils.getUrlParams = function () { let match, re = /([^&=]+)=?([^&]*)/g, params = {}; let url = window.location.search.substr(1).replace(/\+/g, " "); while ((match = re.exec(url))) { params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]); } return params; }; let deferIds = {}; Utils.defer = function (f, id, t = 1) { clearTimeout(deferIds[id]); if (f === null) { delete deferIds[id]; return; } deferIds[id] = setTimeout(() => { delete deferIds[id]; f(); }, t); }; Utils.getHashCode = function (s) { let hash = 0, l = s.length, i; for (i = 0; i < l; i++) { hash = ((hash << 5) - hash + s.charCodeAt(i)) | 0; } return hash; }; Utils.getPatternURL = function (pattern) { let a = Utils.isLocal ? "?id=" : "/"; let url = window.location.origin, id = (pattern && pattern.id) || ""; return url + a + id; }; Utils.isLocal = window.location.hostname === "localhost"; Utils.getPatternURLStr = function (pattern) { if (!pattern || !pattern.id) { return null; } let a = Utils.isLocal ? "?id=" : "/"; let url = window.location.host, id = pattern.id; return url + a + id; }; Utils.SUBST_ESC_CHARS = { // this is just the list supported in Replace. Others: b, f, ", etc. n: "\n", r: "\r", t: "\t", "\\": "\\", }; Utils.SUBST_ESC_RE = /\\([nrt\\]|u([A-Z0-9]{4}))/gi; ================================================ FILE: Public/js/runner.js ================================================ "use strict"; import ReconnectingWebSocket from "reconnecting-websocket"; export class Runner { constructor() { this.connection = this.createConnection(this.endpoint()); this.onconnect = () => {}; this.onready = () => {}; this.onresponse = () => {}; } get isReady() { return this.connection.readyState === 1; } run(request) { const encoder = new TextEncoder(); this.connection.send(encoder.encode(JSON.stringify(request))); } createConnection(endpoint) { if ( this.connection && (this.connection.readyState === 0 || this.connection.readyState === 1) ) { return this.connection; } const connection = new ReconnectingWebSocket(endpoint, [], { maxReconnectionDelay: 10000, minReconnectionDelay: 1000, reconnectionDelayGrowFactor: 1.3, connectionTimeout: 10000, maxRetries: Infinity, debug: false, }); connection.bufferType = "arraybuffer"; connection.onopen = () => { this.onconnect(); this.onready(); }; connection.onerror = (event) => { connection.close(); }; connection.onmessage = (event) => { if (event.data.trim()) { this.onresponse(JSON.parse(event.data)); } }; return connection; } endpoint() { let endpoint; if (window.location.protocol === "https:") { endpoint = "wss:"; } else { endpoint = "ws:"; } endpoint += "//" + window.location.host; endpoint += window.location.pathname + "api/ws"; return endpoint; } } ================================================ FILE: Public/js/state/decoder.js ================================================ "use strict"; import { ungzip } from "pako"; export class Decoder { static decode(string) { const base64 = decodeURIComponent(string); const gziped = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); const json = ungzip(gziped, { to: "string" }); return JSON.parse(json); } } ================================================ FILE: Public/js/state/encoder.js ================================================ "use strict"; import { gzip } from "pako"; export class Encoder { static encode(data) { const json = JSON.stringify(data); const gziped = gzip(json); const base64 = btoa(String.fromCharCode(...gziped)); return encodeURIComponent(base64); } } ================================================ FILE: Public/js/state/worker.js ================================================ "use strict"; import { Decoder } from "./decoder.js"; import { Encoder } from "./encoder.js"; onmessage = (e) => { if (!e.data || !e.data.type || !e.data.value) { return; } switch (e.data.type) { case "decode": { const searchParams = new URLSearchParams(e.data.value); const query = Object.fromEntries(searchParams.entries()); if (!query.s) { return; } try { const data = Decoder.decode(query.s); if (!data) { return; } const pattern = data.p; const options = data.o; const text1 = data.t1; const builder = data.b; const text2 = data.t2; postMessage({ type: e.data.type, value: { pattern, options, text1, builder, text2, }, }); } catch (error) {} break; } case "encode": { postMessage({ type: e.data.type, value: `?s=${Encoder.encode({ p: e.data.value.pattern, o: e.data.value.options, t1: e.data.value.text1, b: e.data.value.builder, t2: e.data.value.text2, })}`, }); break; } } }; ================================================ FILE: Public/js/views/debugger_highlighter.js ================================================ "use strict"; import Editor from "./editor"; export default class DebuggerHighlighter { constructor(editor) { this.editor = editor; this.activeMarks = []; this.widgets = []; } draw(traces, backtrack) { this.clear(); const editor = this.editor; editor.operation(() => { const doc = editor.getDoc(); const marks = this.activeMarks; const defaultTextHeight = editor.defaultTextHeight(); for (const trace of traces) { const className = "debuggermatch"; if (trace.location.start !== trace.location.end) { const location = Editor.calcRangePos( this.editor, trace.location.start, trace.location.end - trace.location.start ); marks.push( doc.markText(location.startPos, location.endPos, { className: className, }) ); } else { const pos = doc.posFromIndex(trace.location.start); const widget = document.createElement("span"); widget.className = className; widget.style.height = `${defaultTextHeight * 1.5}px`; widget.style.width = "1px"; widget.style.zIndex = "10"; this.editor.addWidget(pos, widget); const coords = editor.charCoords(pos, "local"); widget.style.left = `${coords.left}px`; widget.style.top = `${coords.top + 2}px`; this.widgets.push(widget); } } if (backtrack) { const pos = doc.posFromIndex(backtrack.start); const widget = document.createElement("span"); widget.className = "debuggerbacktrack"; widget.style.height = `${defaultTextHeight * 1.5}px`; widget.style.width = `${editor.defaultCharWidth()}px`; widget.style.zIndex = "10"; this.editor.addWidget(pos, widget); const coords = editor.charCoords(pos, "local"); widget.style.left = `${coords.left}px`; widget.style.top = `${coords.top + 2}px`; this.widgets.push(widget); } }); } clear() { this.editor.operation(() => { let marks = this.activeMarks; for (var i = 0, l = marks.length; i < l; i++) { marks[i].clear(); } marks.length = 0; for (const widget of this.widgets) { widget.parentNode.removeChild(widget); } this.widgets.length = 0; }); } } ================================================ FILE: Public/js/views/debugger_text.js ================================================ "use strict"; import Editor from "./editor"; import DebuggerHighlighter from "./debugger_highlighter"; export class DebuggerText { constructor(container) { this.container = container; this.init(container); } get value() { return this.editor.getValue(); } set value(val) { this.editor.setValue(val); } init(container) { const editor = Editor.create( container, { lineWrapping: true, screenReaderLabel: "Debugger Test View", readOnly: true, }, "100%", "100%" ); this.editor = editor; this.highlighter = new DebuggerHighlighter(editor); } } ================================================ FILE: Public/js/views/dsl_editor.js ================================================ "use strict"; import { EventDispatcher } from "@createjs/easeljs"; import Editor from "./editor"; import ErrorMessage from "./error_message"; import Utils from "../misc/utils"; const defaultValue = `Regex { Capture { ChoiceOf { "CREDIT" "DEBIT" } } OneOrMore(.whitespace) Capture { Regex { Repeat(1...2) { One(.digit) } "/" Repeat(1...2) { One(.digit) } "/" Repeat(count: 4) { One(.digit) } } } } `; export class DSLEditor extends EventDispatcher { static defaultValue = defaultValue; constructor(container) { super(); this.container = container; this.init(container); } get value() { return this.editor.getValue(); } set value(val) { this.editor.setValue(val); } set error(error) { const editor = this.editor; const widgets = this.widgets; editor.operation(function () { for (const widget of widgets) { editor.removeLineWidget(widget); } widgets.length = 0; if (!error) { return; } widgets.push( editor.addLineWidget(0, ErrorMessage.create(error), { coverGutter: false, noHScroll: true, above: true, }) ); }); } init(container) { this.editor = Editor.create( container, { lineNumbers: true, lineWrapping: false, matchBrackets: true, mode: "swift", screenReaderLabel: "Build DSL Editor", }, "100%", "100%" ); this.editor.setValue(defaultValue); this.editor.setCursor(this.editor.lineCount(), 0); this.editor.on("change", (editor, event) => this.onEditorChange(editor, event) ); this.widgets = []; } setDefaultValue() { this.editor.setValue(defaultValue); } deferUpdate() { Utils.defer(() => this.update(), "DSLEditor.update"); } update() { this.dispatchEvent("change"); } onEditorChange(editor, event) { this.deferUpdate(); } } ================================================ FILE: Public/js/views/dsl_highlighter.js ================================================ "use strict"; import { EventDispatcher } from "@createjs/easeljs"; import Editor from "./editor"; export class DSLHighlighter extends EventDispatcher { constructor(editor) { super(); this.editor = editor; this.activeMarks = []; } clear() { this.editor.operation(() => { let marks = this.activeMarks; for (var i = 0, l = marks.length; i < l; i++) { marks[i].clear(); } marks.length = 0; }); } draw(tokens) { const editor = this.editor; this.clear(); editor.operation(() => { const doc = editor.getDoc(); const marks = this.activeMarks; for (const token of tokens) { const className = "highlight"; const location = Editor.calcRangePos( this.editor, token.sourceLocation.start, token.sourceLocation.end - token.sourceLocation.start ); marks.push( doc.markText(location.startPos, location.endPos, { className: className, }) ); } }); } } ================================================ FILE: Public/js/views/dsl_view.js ================================================ "use strict"; import { EventDispatcher } from "@createjs/easeljs"; import Editor from "./editor"; import ErrorMessage from "./error_message"; export class DSLView extends EventDispatcher { constructor(container) { super(); this.container = container; this.init(container); } get value() { return this.editor.getValue(); } set value(val) { this.editor.setValue(val); } set error(error) { const editor = this.editor; const widgets = this.widgets; editor.operation(function () { for (const widget of widgets) { editor.removeLineWidget(widget); } widgets.length = 0; if (!error) { return; } if (typeof error === "string" || error instanceof String) { widgets.push( editor.addLineWidget(0, ErrorMessage.create(error), { coverGutter: false, noHScroll: true, above: true, }), ); } else { for (const e of error) { const message = ErrorMessage.create(e.message); widgets.push( editor.addLineWidget(0, message, { coverGutter: false, noHScroll: true, above: true, }), ); } } }); } init(container) { this.editor = Editor.create( container, { lineNumbers: true, lineWrapping: false, matchBrackets: true, mode: "swift", readOnly: true, screenReaderLabel: "Build DSL View", }, "100%", "100%", ); this.widgets = []; } } ================================================ FILE: Public/js/views/editor.js ================================================ "use strict"; import CodeMirror from "codemirror"; import "codemirror/mode/swift/swift"; import Utils from "../misc/utils"; const Editor = {}; export default Editor; Editor.create = (target, opts = {}, width = "100%", height = "100%") => { const keys = {}; const o = Utils.copy( { extraKeys: keys, indentWithTabs: false, lineNumbers: false, mode: "null", specialChars: /[ \u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200f\u2028\u2029\ufeff]/, specialCharPlaceholder: (ch) => createElement("span", ch === " " ? "cm-space" : "cm-special", " "), // needs to be a space so wrapping works tabSize: 2, }, opts, ); const cm = CodeMirror(target, o); cm.setSize(width, height); if (cm.getOption("maxLength")) { cm.on("beforeChange", Editor.enforceMaxLength); } if (cm.getOption("singleLine")) { cm.on("beforeChange", Editor.enforceSingleLine); } return cm; }; Editor.enforceMaxLength = (cm, change) => { let maxLength = cm.getOption("maxLength"); if (maxLength && change.update) { let str = change.text.join("\n"); let delta = str.length - (cm.indexFromPos(change.to) - cm.indexFromPos(change.from)); if (delta <= 0) { return true; } delta = cm.getValue().length + delta - maxLength; if (delta > 0) { str = str.substr(0, str.length - delta); change.update(change.from, change.to, str.split("\n")); } } return true; }; Editor.enforceSingleLine = (cm, change) => { if (change.update) { let str = change.text.join("").replace(/(\n|\r)/g, ""); change.update(change.from, change.to, [str]); } return true; }; Editor.selectAll = (cm) => { cm.focus(); cm.setSelection({ ch: 0, line: 0 }, { ch: 0, line: cm.lineCount() }); }; Editor.calcRangePos = (cm, i, l = 0, o = {}) => { let doc = cm.getDoc(); o.startPos = doc.posFromIndex(i); o.endPos = doc.posFromIndex(i + l); return o; }; function createElement(type, className, content, parent) { let element = document.createElement(type || "div"); if (className) { element.className = className; } if (content) { if (content instanceof HTMLElement) { element.appendChild(content); } else { element.innerHTML = content; } } if (parent) { parent.appendChild(element); } return element; } ================================================ FILE: Public/js/views/error_message.js ================================================ "use strict"; const ErrorMessage = {}; export default ErrorMessage; ErrorMessage.create = (message) => { const container = document.createElement("div"); container.classList.add("error-message", "d-flex", "flex-row"); const wrapper = document.createElement("div"); wrapper.classList.add("d-flex", "flex-row", "overflow-hidden", "w-100"); container.appendChild(wrapper); const iconWrapper = document.createElement("div"); wrapper.appendChild(iconWrapper); const icon = document.createElement("span"); icon.classList.add( "fa-solid", "fa-octagon-xmark", "fa-xs", "text-danger", "px-2", ); iconWrapper.appendChild(icon); const messageWrapper = document.createElement("div"); messageWrapper.classList.add("text-nowrap"); messageWrapper.textContent = message; wrapper.appendChild(messageWrapper); return container; }; ================================================ FILE: Public/js/views/expression_field.js ================================================ "use strict"; import { EventDispatcher } from "@createjs/easeljs"; import tippy from "tippy.js"; import Editor from "./editor"; import ExpressionHighlighter from "./expression_highlighter"; import Utils from "../misc/utils"; export class ExpressionField extends EventDispatcher { constructor(container) { super(); this.container = container; this.init(container); } get value() { return this.editor.getValue(); } set value(val) { this.editor.setValue(val); } set tokens(tokens) { this.expressionTokens = tokens; this.highlighter.draw(tokens); this.resetTooltips(); } set error(error) { if (error.length) { let message = ""; if (typeof error === "string" || error instanceof String) { const errorMessage = Utils.htmlSafe(error); message = `Parse Error: ${errorMessage}`; } else { message = error .map((e) => { const errorMessage = Utils.htmlSafe(e.message); return `${e.behavior}: ${errorMessage}`; }) .join("
"); this.highlighter.drawError(error); } this.errorMessageTooltip.setContent(message); document .getElementById("expression-field-error") .classList.remove("d-none"); } else { this.errorMessageTooltip.setContent(""); document.getElementById("expression-field-error").classList.add("d-none"); this.highlighter.clearError(); } tippy(".exp-syntax-error", { allowHTML: true, animation: false, placement: "bottom", }); } init(container) { this.editor = Editor.create( container, { autofocus: true, maxLength: 2500, singleLine: true, screenReaderLabel: "Regular Expression Field", }, "100%", "100%", ); this.editor.on("change", (editor, event) => this.onEditorChange(editor, event), ); this.highlighter = new ExpressionHighlighter(this.editor); this.expressionTokens = []; this.activeTooltips = []; this.errorMessageTooltip = tippy( document.getElementById("expression-field-error"), { ...tooltipProps, }, ); } setDefaultValue() { this.editor.setValue(defaultValue); this.editor.setCursor(this.editor.lineCount(), 0); } resetTooltips() { for (const tooltip of this.activeTooltips) { tooltip.destroy(); } this.activeTooltips = tippy(tooltipSelector, { ...tooltipProps, onShow: (instance) => { const index = instance.reference.dataset.tokenIndex; if (index === undefined) { return false; } const token = this.expressionTokens[index]; this.onHover(token, instance); return false; }, }); } deferUpdate() { Utils.defer(() => this.update(), "ExpressionField.update"); } update() { this.dispatchEvent("change"); } onEditorChange(editor, event) { this.deferUpdate(); } onHover(token, tippyInstance) { this.hoverToken = token; this.highlighter.drawHover(token); this.dispatchEvent("hover"); for (const tooltip of this.activeTooltips) { if (tooltip !== tippyInstance) { tooltip.destroy(); } } this.activeTooltips = tippy(tooltipSelector, { ...tooltipProps, onUntrigger: (instance) => { this.highlighter.clearHover(); this.resetTooltips(); this.dispatchEvent("unhover"); }, }); } } const defaultValue = `(CREDIT|DEBIT)\\s+(\\d{1,2}/\\d{1,2}/\\d{4})`; const tooltipSelector = "#expression-field-container span[data-tippy-content]"; const tooltipProps = { allowHTML: true, animation: false, placement: "bottom", }; ================================================ FILE: Public/js/views/expression_highlighter.js ================================================ "use strict"; import { EventDispatcher } from "@createjs/easeljs"; import { Reference } from "../docs/reference"; import Editor from "./editor"; export default class ExpressionHighlighter extends EventDispatcher { constructor(editor) { super(); this.editor = editor; this.activeMarks = []; this.hoverMarks = []; this.widgets = []; } draw(tokens) { this.clear(); const pre = ExpressionHighlighter.CSS_PREFIX; const editor = this.editor; editor.operation(() => { const doc = editor.getDoc(); const marks = this.activeMarks; for (const [i, token] of Object.entries(tokens)) { const location = Editor.calcRangePos( this.editor, token.location.start, token.location.end - token.location.start, ); const tooltipAttr = (() => { if (token.tooltip) { const reference = Reference.get( token.tooltip.category, token.tooltip.key, ); let title = reference ? reference.title : token.tooltip.category; let detail = reference ? reference.detail : token.tooltip.key; for (const [k, v] of Object.entries(token.tooltip.substitution)) { title = title.replaceAll(k, v); detail = detail.replaceAll(k, v); } return { "data-tippy-content": makeTooltip(title, detail), }; } else { return {}; } })(); marks.push( doc.markText(location.startPos, location.endPos, { className: `${token.classes.map((c) => `${pre}-${c}`).join(" ")}`, attributes: { ...tooltipAttr, "data-token-index": i, }, }), ); } }); } drawError(errors) { this.clearError(); const pre = ExpressionHighlighter.CSS_PREFIX; const editor = this.editor; editor.operation(() => { for (const error of errors) { const location = Editor.calcRangePos( this.editor, error.location.start, error.location.end - error.location.start, ); const widget = document.createElement("span"); widget.className = `${pre}-syntax-error`; widget.style.height = `5px`; widget.style.zIndex = "10"; widget.setAttribute( "data-tippy-content", `${error.behavior}: ${error.message}`, ); editor.addWidget(location.startPos, widget); const startCoords = editor.charCoords(location.startPos, "local"); const endCoords = editor.charCoords(location.endPos, "local"); widget.style.left = `${startCoords.left + 1}px`; widget.style.top = `${startCoords.bottom - 1}px`; if (error.location.start === error.location.end) { widget.style.width = `${editor.defaultCharWidth()}px`; } else { widget.style.width = `${endCoords.left - startCoords.left - 2}px`; } this.widgets.push(widget); } }); } clear() { this.editor.operation(() => { for (const mark of this.activeMarks) { mark.clear(); } this.activeMarks.length = 0; }); } clearError() { this.editor.operation(() => { for (const widget of this.widgets) { widget.parentNode.removeChild(widget); } this.widgets.length = 0; }); } drawHover(token) { const selection = token.selection; const related = token.related; if ((!selection && !related) || this.hoverMarks.length) { return; } this.clearHover(); if (selection) { this.drawBorder(selection, "selected"); } if (related) { this.drawBorder(related.location, "related"); } } drawBorder(range, className) { const editor = this.editor; const doc = editor.getDoc(); const pre = ExpressionHighlighter.CSS_PREFIX; const left = Editor.calcRangePos(this.editor, range.start, 1); const location = Editor.calcRangePos( this.editor, range.start, range.end - range.start, ); const right = Editor.calcRangePos(this.editor, range.end - 1, 1); this.hoverMarks.push( doc.markText(left.startPos, left.endPos, { className: `${pre}-${className}-left`, }), ); this.hoverMarks.push( doc.markText(location.startPos, location.endPos, { className: `${pre}-${className}`, }), ); this.hoverMarks.push( doc.markText(right.startPos, right.endPos, { className: `${pre}-${className}-right`, }), ); } clearHover() { this.editor.operation(() => { for (const mark of this.hoverMarks) { mark.clear(); } this.hoverMarks.length = 0; }); } } function makeTooltip(label, desc) { return `
${label}. ${desc}
`; } ExpressionHighlighter.CSS_PREFIX = "exp"; ================================================ FILE: Public/js/views/match_options.js ================================================ "use strict"; import { EventDispatcher } from "@createjs/easeljs"; export class MatchOptions extends EventDispatcher { constructor() { super(); this.init(); } init() { document.querySelectorAll(".match-options-item").forEach((listItem) => { listItem.addEventListener("click", (event) => { listItem.classList.toggle("active-tick"); this.dispatchEvent("change"); }); }); } get value() { const options = []; document.querySelectorAll(".match-options-item").forEach((listItem) => { if (listItem.classList.contains("active-tick")) { options.push(listItem.dataset.value); } }); return options; } set value(options) { document.querySelectorAll(".match-options-item").forEach((listItem) => { if (options.includes(listItem.dataset.value)) { listItem.classList.add("active-tick"); } }); } setDefaultValue() { this.value = ["g", "m"]; } } ================================================ FILE: Public/js/views/test_editor.js ================================================ "use strict"; import { EventDispatcher } from "@createjs/easeljs"; import tippy from "tippy.js"; import Editor from "./editor"; import TestHighlighter from "./test_highlighter"; import ErrorMessage from "./error_message"; import Utils from "../misc/utils"; const defaultValue = `KIND DATE INSTITUTION AMOUNT ---------------------------------------------------------------- CREDIT 03/01/2022 Payroll from employer $200.23 CREDIT 03/03/2022 Suspect A $2,000,000.00 DEBIT 03/03/2022 Ted's Pet Rock Sanctuary $2,000,000.00 DEBIT 03/05/2022 Doug's Dugout Dogs $33.27 DEBIT 06/03/2022 Oxford Comma Supply Ltd. £57.33 `; export class TestEditor extends EventDispatcher { static defaultValue = defaultValue; constructor(container) { super(); this.container = container; this.init(container); } get value() { return this.editor.getValue(); } set value(val) { this.editor.setValue(val); } set error(error) { const editor = this.editor; const widgets = this.widgets; editor.operation(function () { for (const widget of widgets) { editor.removeLineWidget(widget); } widgets.length = 0; if (!error) { return; } widgets.push( editor.addLineWidget(0, ErrorMessage.create(error), { coverGutter: false, noHScroll: true, above: true, }), ); }); } set matches(matches) { this.highlighter.draw(matches); tippy(".test-editor-container span[data-tippy-content]", { allowHTML: true, animation: false, placement: "bottom", }); } init(container) { const editor = Editor.create( container, { lineWrapping: true, screenReaderLabel: "Pattern Test View" }, "100%", "100%", ); this.editor = editor; this.highlighter = new TestHighlighter(editor); this.widgets = []; editor.on("change", (editor, event) => this.onEditorChange(editor, event)); } setDefaultValue() { this.editor.setValue(defaultValue); } deferUpdate() { Utils.defer(() => this.update(), "TestEditor.update"); } update() { this.dispatchEvent("change"); } onEditorChange(editor, event) { this.deferUpdate(); } } ================================================ FILE: Public/js/views/test_highlighter.js ================================================ "use strict"; import { EventDispatcher } from "@createjs/easeljs"; import Editor from "./editor"; import Utils from "../misc/utils"; export default class TestHighlighter extends EventDispatcher { constructor(editor) { super(); this.editor = editor; this.activeMarks = []; this.widgets = []; this.textHeight = editor.defaultTextHeight(); } draw(tokens) { this.clear(); const editor = this.editor; editor.operation(() => { const doc = editor.getDoc(); const marks = this.activeMarks; for (const token of tokens) { const match = Utils.htmlSafe(token.value); let tooltip = `
match: ${match}
range: ${token.location.start}-${token.location.end}
`; if (token.captures.length) { tooltip += "
"; for (const [i, capture] of token.captures.entries()) { const value = Utils.htmlSafe(capture.value || ""); const name = Utils.htmlSafe(capture.name || ""); tooltip += `
group #${i + 1}${ name ? ` ${name}` : "" }: ${value === "" ? "empty string" : value}
`; } } if (token.location.start < token.location.end) { const location = Editor.calcRangePos( editor, token.location.start, token.location.end - token.location.start, ); marks.push( doc.markText(location.startPos, location.endPos, { className: "match-char", attributes: { "data-tippy-content": tooltip, }, }), ); } else { const location = Editor.calcRangePos(editor, token.location.start, 1); if ( location.startPos.line === location.endPos.line && location.startPos.ch === location.endPos.ch ) { this.addLeftAnchor(location, { "data-tippy-content": tooltip }); } if (location.startPos.line === location.endPos.line) { if (location.startPos.ch < location.endPos.ch) { marks.push( doc.markText(location.startPos, location.endPos, { className: "match-left", attributes: { "data-tippy-content": tooltip, }, }), ); } else { // this.addRightAnchor(location, { "data-tippy-content": tooltip }); } } else { if (location.startPos.ch === 0 && location.endPos.ch === 0) { this.addLeftAnchor(location, { "data-tippy-content": tooltip }); } else { this.addRightAnchor(location, { "data-tippy-content": tooltip }); } } } } }); } addLeftAnchor(location, attributes = {}) { const widget = document.createElement("span"); widget.className = "match-left"; widget.style.height = `${this.textHeight * 1.5}px`; widget.style.width = "1px"; widget.style.zIndex = "10"; for (const [key, value] of Object.entries(attributes)) { widget.setAttribute(key, value); } this.editor.addWidget(location.startPos, widget); const coords = this.editor.charCoords(location.startPos, "local"); widget.style.left = `${coords.left}px`; widget.style.top = `${coords.top + 2}px`; this.widgets.push(widget); } addRightAnchor(location, attributes = {}) { const widget = document.createElement("span"); widget.className = "match-right"; widget.style.height = `${this.textHeight * 1.5}px`; widget.style.width = "1px"; widget.style.zIndex = "10"; for (const [key, value] of Object.entries(attributes)) { widget.setAttribute(key, value); } this.editor.addWidget(location.endPos, widget); const coords = this.editor.charCoords(location.startPos, "local"); widget.style.left = `${coords.left}px`; widget.style.top = `${coords.top + 2}px`; this.widgets.push(widget); } clear() { this.editor.operation(() => { let marks = this.activeMarks; for (var i = 0, l = marks.length; i < l; i++) { marks[i].clear(); } marks.length = 0; for (const widget of this.widgets) { widget.parentNode.removeChild(widget); } this.widgets.length = 0; }); } } ================================================ FILE: Public/robots.txt ================================================ ================================================ FILE: Public/scss/default.scss ================================================ $table-cell-padding-y-sm: 0.05rem; @import "bootstrap/scss/functions"; @import "bootstrap/scss/variables"; @import "bootstrap/scss/variables-dark"; @import "bootstrap/scss/mixins"; @import "bootstrap/scss/maps"; @import "bootstrap/scss/utilities"; @import "bootstrap/scss/root"; @import "bootstrap/scss/reboot"; @import "bootstrap/scss/type"; @import "bootstrap/scss/containers"; @import "bootstrap/scss/grid"; @import "bootstrap/scss/tables"; @import "bootstrap/scss/forms"; @import "bootstrap/scss/buttons"; @import "bootstrap/scss/transitions"; @import "bootstrap/scss/dropdown"; @import "bootstrap/scss/badge"; @import "bootstrap/scss/close"; @import "bootstrap/scss/modal"; @import "bootstrap/scss/tooltip"; @import "bootstrap/scss/helpers"; @import "bootstrap/scss/utilities/api"; ================================================ FILE: README.md ================================================

macOS/Linux Swift 5.7 Compatible MIT

# Swift Regex Regular Expression Tester with highlighting for Swift Regex. Quickly test and debug your regex and Regex Builder. Screen Shot Screen Shot https://swiftregex.com ================================================ FILE: SECURITY.md ================================================ # Security Policy For security related problems, please don't use the public issue tracker, but email [@kishikawakatsumi](https://github.com/kishikawakatsumi). ================================================ FILE: Sources/App/Debugger/Context.swift ================================================ import Foundation extension Debugger { class Context { var instructions: [String] = [] var programCounter = 0 var stepCount = 0 var breakPoint: Int? var start: Int = 0 var current: Int = 0 var failurePosition: Int = 0 var totalCycleCount = 0 var resets = 0 var backtracks = 0 init(stepCount: Int = 0, breakPoint: Int? = nil) { self.stepCount = stepCount self.breakPoint = breakPoint } func reset() { instructions = [] programCounter = 0 stepCount = 0 breakPoint = nil start = 0 current = 0 failurePosition = 0 totalCycleCount = 0 resets = 0 backtracks = 0 } } } ================================================ FILE: Sources/App/Debugger/Debugger.swift ================================================ import Foundation @testable import _RegexParser @testable @_spi(RegexBenchmark) import _StringProcessing struct Debugger { func run(pattern: String, text: String, matchingOptions: [String] = [], context: Debugger.Context) throws { let ast = try _RegexParser.parse(pattern, .traditional) var sequence = [AST.MatchingOption]() if matchingOptions.contains("i") { sequence.append(.init(.caseInsensitive, location: .fake)) } if matchingOptions.contains("s") { sequence.append(.init(.singleLine, location: .fake)) } if matchingOptions.contains("asciiOnlyWordCharacters") { sequence.append(.init(.asciiOnlyWord, location: .fake)) } if matchingOptions.contains("asciiOnlyDigits") { sequence.append(.init(.asciiOnlyDigit, location: .fake)) } if matchingOptions.contains("asciiOnlyWhitespace") { sequence.append(.init(.asciiOnlySpace, location: .fake)) } if matchingOptions.contains("asciiOnlyCharacterClasses") { sequence.append(.init(.asciiOnlyPOSIXProps, location: .fake)) } sequence.append(.init(.graphemeClusterSemantics, location: .fake)) var options = MatchingOptions() options.apply(AST.MatchingOptionSequence(adding: sequence)) let program = try compile(ast, options: options) context.instructions = program.instructions.map { $0.description } let inputRange = text.startIndex..._firstMatch( program, using: &cpu, context: context ) } catch {} } func compile(_ ast: AST, options: MatchingOptions) throws -> MEProgram { let compiler = Compiler(tree: ast.dslTree, compileOptions: [.enableMetrics]) compiler.options = options return try compiler.emit() } struct Metrics: Codable { var instructions: [String] var programCounter: Int var stepCount: Int var step: Int var totalCycleCount: Int var resets: Int var backtracks: Int var traces: [Trace] var failure: Location } struct Trace: Codable { let location: Location } struct Location: Codable { let start: Int let end: Int } } ================================================ FILE: Sources/App/Debugger/Executor.swift ================================================ @testable import _RegexParser @testable import _StringProcessing enum Executor { static func prefixMatch( _ program: MEProgram, _ input: String, subjectBounds: Range, searchBounds: Range, context: Debugger.Context ) throws -> Regex.Match? { try Executor._run( program, input, subjectBounds: subjectBounds, searchBounds: searchBounds, mode: .partialFromFront, context: context ) } static func wholeMatch( _ program: MEProgram, _ input: String, subjectBounds: Range, searchBounds: Range, context: Debugger.Context ) throws -> Regex.Match? { try Executor._run( program, input, subjectBounds: subjectBounds, searchBounds: searchBounds, mode: .wholeString, context: context ) } static func firstMatch( _ program: MEProgram, _ input: String, subjectBounds: Range, searchBounds: Range, context: Debugger.Context ) throws -> Regex.Match? { var cpu = Processor( program: program, input: input, subjectBounds: subjectBounds, searchBounds: searchBounds, matchMode: .partialFromFront ) return try Executor._firstMatch( program, using: &cpu, context: context ) } static func _firstMatch( _ program: MEProgram, using cpu: inout Processor, context: Debugger.Context ) throws -> Regex.Match? { let isGraphemeSemantic = program.initialOptions.semanticLevel == .graphemeCluster var low = cpu.searchBounds.lowerBound let high = cpu.searchBounds.upperBound while true { if let m = try Executor._run(program, &cpu, context) { return m } // Fast-path for start-anchored regex if program.canOnlyMatchAtStart { return nil } if low == high { return nil } if isGraphemeSemantic { cpu.input.formIndex(after: &low) } else { cpu.input.unicodeScalars.formIndex(after: &low) } guard low <= high else { return nil } cpu.reset(currentPosition: low, searchBounds: cpu.searchBounds) } } } extension Executor { static func _run( _ program: MEProgram, _ input: String, subjectBounds: Range, searchBounds: Range, mode: MatchMode, context: Debugger.Context ) throws -> Regex.Match? { var cpu = Processor( program: program, input: input, subjectBounds: subjectBounds, searchBounds: searchBounds, matchMode: mode) return try _run(program, &cpu, context) } static func _run( _ program: MEProgram, _ cpu: inout Processor, _ context: Debugger.Context ) throws -> Regex.Match? { let startPosition = cpu.currentPosition context.start = startPosition.utf16Offset(in: cpu.input) guard let endIdx = try cpu.run(context) else { return nil } let range = startPosition.. Input.Index? { if self.state == .fail { if let e = failureReason { throw e } return nil } assert(isReset()) while true { context.programCounter = controller.pc.rawValue switch self.state { case .accept: return self.currentPosition case .fail: if let e = failureReason { throw e } return nil case .inProgress: let failurePosition = currentPosition.utf16Offset(in: input) self.cycle() context.stepCount += 1 context.current = currentPosition.utf16Offset(in: input) context.failurePosition = failurePosition #if PROCESSOR_MEASUREMENTS_ENABLED context.totalCycleCount = metrics.cycleCount context.resets = metrics.resets context.backtracks = metrics.backtracks #endif if context.stepCount == context.breakPoint { throw CancellationError() } } } } } ================================================ FILE: Sources/App/Middlewares/CommonErrorMiddleware.swift ================================================ import Vapor final class CommonErrorMiddleware: Middleware { func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture { return next.respond(to: request).flatMapError { (error) in let headers: HTTPHeaders let status: HTTPResponseStatus switch error { case let abort as AbortError: headers = abort.headers status = abort.status default: headers = [:] status = .internalServerError } let errorTitles: [UInt: String] = [ 400: "Bad Request", 401: "Unauthorized", 403: "Access Denied", 404: "Resource not found", 500: "Webservice currently unavailable", 503: "Webservice currently unavailable", ] let errorReasons: [UInt: String] = [ 400: "The server cannot process the request due to something that is perceived to be a client error.", 401: "The requested resource requires an authentication.", 403: "The requested resource requires an authentication.", 404: "The requested resource could not be found but may be available again in the future.", 500: "An unexpected condition was encountered. Our service team has been dispatched to bring it back online.", 503: "We've got some trouble with our backend upstream cluster. Our service team has been dispatched to bring it back online.", ] if request.headers[.accept].map({ $0.lowercased() }).contains("application/json") { return request.eventLoop.makeSucceededFuture(["error": status.code]) .encodeResponse(status: status, headers: headers, for: request) } else { return request.view.render( "error", [ "title": "We've got some trouble", "error": errorTitles[status.code], "reason": errorReasons[status.code], "status": "\(status.code)", ] ) .encodeResponse(status: status, headers: headers, for: request) } } } } ================================================ FILE: Sources/App/Middlewares/CustomHeaderMiddleware.swift ================================================ import Vapor final class CustomHeaderMiddleware: Middleware { func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture { return next.respond(to: request).map { (response) in response.headers.add(name: "X-Frame-Options", value: "DENY") response.headers.add(name: "Permissions-Policy", value: "interest-cohort=()") return response } } } ================================================ FILE: Sources/App/Models/ExecRequest.swift ================================================ import Foundation struct ExecRequest: Codable { let method: RequestMethod let pattern: String let text: String let matchOptions: [String] let step: String? } enum RequestMethod: String, Codable { case parseExpression case convertToDSL case match case debug } ================================================ FILE: Sources/App/Models/ResultResponse.swift ================================================ import Foundation import Vapor struct ResultResponse: Content { let method: RequestMethod let result: String let error: String } ================================================ FILE: Sources/App/configure.swift ================================================ import Leaf import Vapor public func configure(_ app: Application) async throws { app.middleware = Middlewares() app.middleware.use(CommonErrorMiddleware()) app.middleware.use(CustomHeaderMiddleware()) let publicDirectory = "\(app.directory.publicDirectory)/dist" app.middleware.use(FileMiddleware(publicDirectory: publicDirectory)) app.http.server.configuration.port = Environment.process.PORT.flatMap { Int($0) } ?? 8080 app.http.server.configuration.requestDecompression = .enabled app.http.server.configuration.responseCompression = .enabled app.http.server.configuration.supportPipelining = true app.views.use(.leaf) app.leaf.configuration.rootDirectory = publicDirectory app.leaf.cache.isEnabled = app.environment.isRelease try routes(app) } ================================================ FILE: Sources/App/entrypoint.swift ================================================ import Vapor @main enum Entrypoint { static func main() async throws { var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) let app = try await Application.make(env) do { try await configure(app) try await app.execute() } catch { app.logger.report(error: error) try? await app.asyncShutdown() throw error } try await app.asyncShutdown() } } ================================================ FILE: Sources/App/routes.swift ================================================ import Vapor func routes(_ app: Application) throws { app.get("health") { _ in ["status": "pass"] } app.get("healthz") { _ in ["status": "pass"] } app.get { (req) in req.view.render("index") } app.webSocket("api", "ws") { (req, ws) in ws.onBinary { (ws, buffer) in do { guard let data = buffer.getData(at: 0, length: buffer.readableBytes) else { return } let decoder = JSONDecoder() let request = try decoder.decode(ExecRequest.self, from: data) let encoder = JSONEncoder() switch request.method { case .parseExpression: let pattern = request.pattern let matchOptions = request.matchOptions let response = try parseExpression(pattern: pattern, matchOptions: matchOptions) if let message = String(data: try encoder.encode(response), encoding: .utf8) { ws.send(message) } case .convertToDSL: let pattern = request.pattern let matchOptions = request.matchOptions let response = try convertToDSL(pattern: pattern, matchOptions: matchOptions) if let message = String(data: try encoder.encode(response), encoding: .utf8) { ws.send(message) } case .match: let pattern = request.pattern let text = request.text let matchOptions = request.matchOptions let response = try match(pattern: pattern, text: text, matchOptions: matchOptions) if let message = String(data: try encoder.encode(response), encoding: .utf8) { ws.send(message) } case .debug: let pattern = request.pattern let text = request.text let matchOptions = request.matchOptions let step = request.step let response = try debug(pattern: pattern, text: text, matchOptions: matchOptions, step: step) if let message = String(data: try encoder.encode(response), encoding: .utf8) { ws.send(message) } } } catch { req.logger.error("\(error)") } } } app.on(.POST, "api", "rest", "parseExpression", body: .collect(maxSize: "1mb")) { (req) -> ResultResponse in guard let request = try? req.content.decode(ExecRequest.self) else { throw Abort(.badRequest) } let pattern = request.pattern let matchOptions = request.matchOptions let response = try parseExpression(pattern: pattern, matchOptions: matchOptions) return response } app.on(.POST, "api", "rest", "convertToDSL", body: .collect(maxSize: "1mb")) { (req) -> ResultResponse in guard let request = try? req.content.decode(ExecRequest.self) else { throw Abort(.badRequest) } let pattern = request.pattern let matchOptions = request.matchOptions let response = try convertToDSL(pattern: pattern, matchOptions: matchOptions) return response } app.on(.POST, "api", "rest", "match", body: .collect(maxSize: "1mb")) { (req) -> ResultResponse in guard let request = try? req.content.decode(ExecRequest.self) else { throw Abort(.badRequest) } let pattern = request.pattern let text = request.text let matchOptions = request.matchOptions let response = try match(pattern: pattern, text: text, matchOptions: matchOptions) return response } app.on(.POST, "api", "rest", "debug", body: .collect(maxSize: "1mb")) { (req) -> ResultResponse in guard let request = try? req.content.decode(ExecRequest.self) else { throw Abort(.badRequest) } let pattern = request.pattern let text = request.text let matchOptions = request.matchOptions let step = request.step let response = try debug(pattern: pattern, text: text, matchOptions: matchOptions, step: step) return response } func parseExpression(pattern: String, matchOptions: [String]) throws -> ResultResponse { let (stdout, stderr) = try exec(command: "ExpressionParser", arguments: pattern, matchOptions.joined(separator: ",")) return ResultResponse(method: .parseExpression, result: stdout, error: stderr) } func convertToDSL(pattern: String, matchOptions: [String]) throws -> ResultResponse { let (stdout, stderr) = try exec(command: "DSLConverter", arguments: pattern, matchOptions.joined(separator: ",")) return ResultResponse(method: .convertToDSL, result: stdout, error: stderr) } func match(pattern: String, text: String, matchOptions: [String]) throws -> ResultResponse { let (stdout, stderr) = try exec(command: "Matcher", arguments: pattern, text, matchOptions.joined(separator: ",")) return ResultResponse(method: .match, result: stdout, error: stderr) } func debug(pattern: String, text: String, matchOptions: [String], step: String?) throws -> ResultResponse { let context = Debugger.Context() func run(pattern: String, text: String, matchingOptions: [String] = [], until step: Int? = nil) throws { context.reset() context.breakPoint = step let debugger = Debugger() try debugger.run(pattern: pattern, text: text, matchingOptions: matchingOptions, context: context) } let breakPoint: Int? if let step { breakPoint = Int(step) } else { breakPoint = nil } try run(pattern: pattern, text: text, matchingOptions: matchOptions) let stepCount = context.stepCount try run(pattern: pattern, text: text, matchingOptions: matchOptions, until: breakPoint) let metrics = Debugger.Metrics( instructions: context.instructions, programCounter: context.programCounter, stepCount: stepCount, step: breakPoint ?? 1, totalCycleCount: context.totalCycleCount, resets: context.resets, backtracks: context.backtracks, traces: [ Debugger.Trace( location: Debugger.Location( start: context.start, end: context.current ) ) ], failure: Debugger.Location(start: context.current, end: context.failurePosition), ) let data = try JSONEncoder().encode(metrics) let result = String(data: data, encoding: .utf8) ?? "" let response = ResultResponse(method: .debug, result: result, error: "") return response } func exec(command: String, arguments: String...) throws -> (stdout: String, stderr: String) { let process = Process() let executableURL = URL( fileURLWithPath: app.directory.workingDirectory ) .appendingPathComponent(command) process.executableURL = executableURL process.arguments = arguments let standardOutput = Pipe() let standardError = Pipe() process.standardOutput = standardOutput process.standardError = standardError var stdoutData = Data() var stderrData = Data() let group = DispatchGroup() group.enter() standardOutput.fileHandleForReading.readabilityHandler = { handle in let chunk = handle.availableData if chunk.isEmpty { standardOutput.fileHandleForReading.readabilityHandler = nil group.leave() } else { stdoutData.append(chunk) } } group.enter() standardError.fileHandleForReading.readabilityHandler = { handle in let chunk = handle.availableData if chunk.isEmpty { standardError.fileHandleForReading.readabilityHandler = nil group.leave() } else { stderrData.append(chunk) } } try process.run() group.wait() process.waitUntilExit() guard let stdout = String(data: stdoutData, encoding: .utf8) else { throw Abort(.internalServerError) } guard let stderr = String(data: stderrData, encoding: .utf8) else { throw Abort(.internalServerError) } return (stdout, stderr) } } ================================================ FILE: Sources/DSLConverter/DSLConverter.swift ================================================ import Foundation @testable import _RegexParser @testable @_spi(RegexBuilder) import _StringProcessing @testable @_spi(PatternConverter) import _StringProcessing class DSLConverter { private(set) var diagnostics: Diagnostics? func convert(_ pattern: String, matchingOptions: [String] = []) throws -> String { let ast = _RegexParser.parseWithRecovery(pattern, .traditional) diagnostics = ast.diags var builderDSL = renderAsBuilderDSL(ast: ast) if builderDSL.last == "\n" { builderDSL = String(builderDSL.dropLast()) } if matchingOptions.contains("m") { builderDSL.append("\n") builderDSL.append(".anchorsMatchLineEndings()") } if matchingOptions.contains("i") { builderDSL.append("\n") builderDSL.append(".ignoresCase()") } if matchingOptions.contains("s") { builderDSL.append("\n") builderDSL.append(".dotMatchesNewlines()") } if matchingOptions.contains("asciiOnlyWordCharacters") { builderDSL.append("\n") builderDSL.append(".asciiOnlyWordCharacters()") } if matchingOptions.contains("asciiOnlyDigits") { builderDSL.append("\n") builderDSL.append(".asciiOnlyDigits()") } if matchingOptions.contains("asciiOnlyWhitespace") { builderDSL.append("\n") builderDSL.append(".asciiOnlyWhitespace()") } if matchingOptions.contains("asciiOnlyCharacterClasses") { builderDSL.append("\n") builderDSL.append(".asciiOnlyCharacterClasses()") } builderDSL.append("\n") return builderDSL } } ================================================ FILE: Sources/DSLConverter/Main.swift ================================================ import Foundation @main struct Main { static func main() throws { do { let pattern = CommandLine.arguments[1] let matchingOptions = CommandLine.arguments[2] .split(separator: ",", omittingEmptySubsequences: true) .map { String($0) } let converter = DSLConverter() let builderDSL = try converter.convert(pattern, matchingOptions: matchingOptions) let data = try JSONEncoder().encode(builderDSL) print(String(data: data, encoding: .utf8) ?? "") if let diagnostics = converter.diagnostics { let errors = diagnostics.diags.map { let location = $0.location let (start, end) = (location.start, location.end) let behavior = switch $0.behavior { case .fatalError: "Fatal Error" case .error: "Error" case .warning: "Warning" } return LocatedMessage( behavior: behavior, message: $0.message, location: Location( start: start.utf16Offset(in: pattern), end: end.utf16Offset(in: pattern) ) ) } let data = try JSONEncoder().encode(errors) print(String(data: data, encoding: .utf8) ?? "", to: &standardError) } } catch { print("\(error)", to: &standardError) } } } var standardError = FileHandle.standardError extension FileHandle: @retroactive TextOutputStream { public func write(_ string: String) { guard let data = string.data(using: .utf8) else { return } self.write(data) } } struct Location: Codable { let start: Int let end: Int } struct LocatedMessage: Codable { let behavior: String let message: String let location: Location } ================================================ FILE: Sources/ExpressionParser/ExpressionParser.swift ================================================ import Foundation @testable import _RegexParser @testable @_spi(RegexBuilder) import _StringProcessing struct ExpressionParser { struct Modes { let matchingOptions: [String] var i: Bool { matchingOptions.contains("i") } var s: Bool { matchingOptions.contains("s") } } private(set) var tokens = [Token]() private(set) var diagnostics: Diagnostics? private let pattern: String private let matchingOptions: [String] private let modes: Modes private var depth = 0 private var groupCount = 0 init(pattern: String, matchingOptions: [String]) { self.pattern = pattern self.matchingOptions = matchingOptions modes = Modes(matchingOptions: matchingOptions) } mutating func parse() { let ast = _RegexParser.parseWithRecovery(pattern, .traditional) diagnostics = ast.diags emitNode(ast.root) } private mutating func emitNode(_ node: AST.Node) { switch node { case .alternation(let alt): emitAlternation(alt) case .concatenation(let concatenation): for node in concatenation.children { emitNode(node) } case .group(let group): emitGroup(group) case .conditional(let conditional): emitConditional(conditional) case .quantification(let quant): emitQuantification(quant) case .quote(let quote): emitQuote(quote) case .trivia(let trivia): emitTrivia(trivia) case .interpolation(let interpolation): emitInterpolation(interpolation) case .atom(let atom): emitAtom(atom) case .customCharacterClass(let ccc): emitCustomCharacterClass(ccc) case .absentFunction(let absentFunction): emitAbsentFunction(absentFunction) case .empty(let empty): emitEmpty(empty) } } private mutating func emitAlternation(_ alt: AST.Alternation) { let children = alt.children for node in children.dropLast() { emitNode(node) } for pipe in alt.pipes { tokens.append( Token( classes: ["alt"], location: Location( start: pipe.start.utf16Offset(in: pattern), end: pipe.end.utf16Offset(in: pattern) ), selection: Location( start: pipe.start.utf16Offset(in: pattern), end: pipe.end.utf16Offset(in: pattern) ), related: Related( location: Location( start: alt.startPosition.utf16Offset(in: pattern), end: alt.endPosition.utf16Offset(in: pattern) ) ), tooltip: Tooltip(category: "quants", key: "alt") ) ) } emitNode(children.last!) } private mutating func emitGroup(_ group: AST.Group) { let category: String let key: String var substitution = [String: String]() switch group.kind.value { case .capture: groupCount += 1 category = "groups" key = "group" substitution = ["{{group.num}}": "\(groupCount)"] case .namedCapture(let name): groupCount += 1 category = "groups" key = "namedgroup" substitution = ["{{name}}": name.value] case .balancedCapture(_): groupCount += 1 category = "groups" key = "balancedcapture" case .nonCapture: category = "groups" key = "noncapgroup" case .nonCaptureReset: category = "groups" key = "branchreset" case .atomicNonCapturing: category = "groups" key = "atomic" case .lookahead: category = "lookaround" key = "poslookahead" case .negativeLookahead: category = "lookaround" key = "neglookahead" case .nonAtomicLookahead: category = "lookaround" key = "nonatomicposlookahead" case .lookbehind: category = "lookaround" key = "poslookbehind" case .negativeLookbehind: category = "lookaround" key = "neglookbehind" case .nonAtomicLookbehind: category = "lookaround" key = "nonatomicposlookbehind" case .scriptRun: category = "Script run. " key = "" case .atomicScriptRun: category = "Atomic script run. " key = "" case .changeMatchingOptions(_): category = "Change matching options" key = "" } // Content tokens.append( Token( classes: ["group-\(depth)"], location: Location( start: group.startPosition.utf16Offset(in: pattern), end: group.endPosition.utf16Offset(in: pattern) ) ) ) // Open parenthesis tokens.append( Token( classes: ["group", "group-\(depth)"], location: Location( start: group.kind.location.start.utf16Offset(in: pattern), end: group.kind.location.end.utf16Offset(in: pattern) ), selection: Location( start: group.startPosition.utf16Offset(in: pattern), end: group.endPosition.utf16Offset(in: pattern) ), tooltip: Tooltip(category: category, key: key, substitution: substitution) ) ) // Close parenthesis tokens.append( Token( classes: ["group", "group-\(depth)"], location: Location( start: group.child.location.end.utf16Offset(in: pattern), end: group.endPosition.utf16Offset(in: pattern) ), selection: Location( start: group.startPosition.utf16Offset(in: pattern), end: group.endPosition.utf16Offset(in: pattern) ), tooltip: Tooltip(category: category, key: key, substitution: substitution) ) ) depth += 1 for node in group.children { emitNode(node) } depth -= 1 } private mutating func emitConditional(_ conditional: AST.Conditional) { let category: String let key: String var substitution = [String: String]() switch conditional.condition.kind { case .groupMatched(let ref): switch ref.kind { case .absolute(let n): category = "other" key = "conditionalgroup" substitution = ["{{name}}": "\(n)"] case .relative(let n): category = "other" key = "conditionalgroup" substitution = ["{{name}}": "\(n)"] case .named(let name): category = "other" key = "conditionalgroup" substitution = ["{{name}}": "\(name)"] } tokens.append( Token( classes: ["special"], location: Location( start: conditional.startPosition.utf16Offset(in: pattern), end: conditional.condition.location.end.utf16Offset(in: pattern) + 1 ), selection: Location( start: conditional.startPosition.utf16Offset(in: pattern), end: conditional.endPosition.utf16Offset(in: pattern) ), tooltip: Tooltip(category: category, key: key, substitution: substitution) ) ) case .recursionCheck: break case .groupRecursionCheck(_): tokens.append( Token( classes: ["special"], location: Location( start: conditional.startPosition.utf16Offset(in: pattern), end: conditional.condition.location.end.utf16Offset(in: pattern) + 1 ), selection: Location( start: conditional.startPosition.utf16Offset(in: pattern), end: conditional.endPosition.utf16Offset(in: pattern) ), tooltip: Tooltip(category: "other", key: "recursion") ) ) case .defineGroup: break case .pcreVersionCheck(_): break case .group(let group): tokens.append( Token( classes: ["special"], location: Location( start: conditional.startPosition.utf16Offset(in: pattern), end: conditional.condition.location.start.utf16Offset(in: pattern) ), selection: Location( start: conditional.startPosition.utf16Offset(in: pattern), end: conditional.endPosition.utf16Offset(in: pattern) ), tooltip: Tooltip(category: "other", key: "conditional") ) ) // Open parenthesis tokens.append( Token( classes: ["special"], location: Location( start: group.kind.location.start.utf16Offset(in: pattern), end: group.kind.location.end.utf16Offset(in: pattern) ), selection: Location( start: group.startPosition.utf16Offset(in: pattern), end: group.endPosition.utf16Offset(in: pattern) ), tooltip: Tooltip(category: "misc", key: "condition") ) ) // Close parenthesis tokens.append( Token( classes: ["special"], location: Location( start: group.child.location.end.utf16Offset(in: pattern), end: group.endPosition.utf16Offset(in: pattern) ), selection: Location( start: group.startPosition.utf16Offset(in: pattern), end: group.endPosition.utf16Offset(in: pattern) ), tooltip: Tooltip(category: "misc", key: "condition") ) ) for node in group.children { emitNode(node) } } emitNode(conditional.trueBranch) if let pipe = conditional.pipe { tokens.append( Token( classes: ["special"], location: Location( start: pipe.start.utf16Offset(in: pattern), end: pipe.end.utf16Offset(in: pattern) ), selection: Location( start: pipe.start.utf16Offset(in: pattern), end: pipe.end.utf16Offset(in: pattern) ), related: Related( location: Location( start: conditional.startPosition.utf16Offset(in: pattern), end: conditional.endPosition.utf16Offset(in: pattern) ) ), tooltip: Tooltip(category: "misc", key: "conditionalelse") ) ) } emitNode(conditional.falseBranch) tokens.append( Token( classes: ["special"], location: Location( start: conditional.falseBranch.location.end.utf16Offset(in: pattern), end: conditional.endPosition.utf16Offset(in: pattern) ), selection: Location( start: conditional.startPosition.utf16Offset(in: pattern), end: conditional.endPosition.utf16Offset(in: pattern) ), tooltip: Tooltip(category: "other", key: "conditional") ) ) } private mutating func emitQuantification(_ quant: AST.Quantification) { emitNode(quant.child) let substitution: [String: String] switch quant.amount.value { case .zeroOrMore: // * substitution = ["{{getQuant()}}": "0 or more"] case .oneOrMore: // + substitution = ["{{getQuant()}}": "1 or more"] case .zeroOrOne: // ? substitution = ["{{getQuant()}}": "between 0 and 1"] case .exactly(let n): // {n} substitution = ["{{getQuant()}}": String(pattern[n.location.range])] case .nOrMore(let n): // {n,} substitution = ["{{getQuant()}}": "\(pattern[n.location.range]) or more"] case .upToN(let n): // {,n} substitution = ["{{getQuant()}}": "between 0 and \(pattern[n.location.range])"] case .range(let n, let m): // {n,m} substitution = ["{{getQuant()}}": "between \(pattern[n.location.range]) and \(pattern[m.location.range])"] } tokens.append( Token( classes: ["quant"], location: Location( start: quant.amount.location.start.utf16Offset(in: pattern), end: quant.amount.location.end.utf16Offset(in: pattern) ), selection: Location( start: quant.amount.location.start.utf16Offset(in: pattern), end: quant.amount.location.end.utf16Offset(in: pattern) ), related: Related( location: Location( start: quant.startPosition.utf16Offset(in: pattern), end: quant.endPosition.utf16Offset(in: pattern) ) ), tooltip: Tooltip(category: "quants", key: "quant", substitution: substitution) ) ) switch quant.kind.value { case .eager: break case .reluctant: tokens.append( Token( classes: ["lazy"], location: Location( start: quant.kind.location.start.utf16Offset(in: pattern), end: quant.kind.location.end.utf16Offset(in: pattern) ), selection: Location( start: quant.kind.location.start.utf16Offset(in: pattern), end: quant.kind.location.end.utf16Offset(in: pattern) ), related: Related( location: Location( start: quant.amount.location.start.utf16Offset(in: pattern), end: quant.amount.location.end.utf16Offset(in: pattern) ) ), tooltip: Tooltip(category: "quants", key: "lazy") ) ) case .possessive: tokens.append( Token( classes: ["possessive"], location: Location( start: quant.kind.location.start.utf16Offset(in: pattern), end: quant.kind.location.end.utf16Offset(in: pattern) ), selection: Location( start: quant.kind.location.start.utf16Offset(in: pattern), end: quant.kind.location.end.utf16Offset(in: pattern) ), related: Related( location: Location( start: quant.amount.location.start.utf16Offset(in: pattern), end: quant.amount.location.end.utf16Offset(in: pattern) ) ), tooltip: Tooltip(category: "quants", key: "possessive") ) ) } } private mutating func emitQuote(_ quote: AST.Quote) { tokens.append( Token( classes: ["esc"], location: Location( start: quote.startPosition.utf16Offset(in: pattern), end: quote.endPosition.utf16Offset(in: pattern) ), selection: Location( start: quote.startPosition.utf16Offset(in: pattern), end: quote.endPosition.utf16Offset(in: pattern) ), tooltip: Tooltip(category: "escchars", key: "escsequence", substitution: ["{{value}}": quote.literal]) ) ) } private mutating func emitTrivia(_ trivia: AST.Trivia) { tokens.append( Token( classes: ["comment"], location: Location( start: trivia.startPosition.utf16Offset(in: pattern), end: trivia.endPosition.utf16Offset(in: pattern) ), selection: Location( start: trivia.startPosition.utf16Offset(in: pattern), end: trivia.endPosition.utf16Offset(in: pattern) ), tooltip: Tooltip(category: "other", key: "comment") ) ) } private mutating func emitInterpolation(_ interpolation: AST.Interpolation) { tokens.append( Token( classes: ["interpolation"], location: Location( start: interpolation.startPosition.utf16Offset(in: pattern), end: interpolation.endPosition.utf16Offset(in: pattern) ), selection: Location( start: interpolation.startPosition.utf16Offset(in: pattern), end: interpolation.endPosition.utf16Offset(in: pattern) ), tooltip: Tooltip(category: "other", key: "interpolation") ) ) } private mutating func emitAtom(_ atom: AST.Atom) { let `class`: String let category: String let key: String var substitution = [String: String]() switch atom.kind { case .char(let c): let charcode = c.unicodeScalars.map { String(format: "U+%X", $0.value) }.joined(separator: " ") let value = String(pattern[atom.location.range]) if value.hasPrefix("\\") { `class` = "esc" category = "misc" key = "escchar" substitution = [ "{{getChar()}}": #""\#(c)""#, "{{code}}": charcode, "{{getInsensitive()}}": "Case \(modes.i ? "in" : "")sensitive", ] } else { `class` = "char" category = "misc" key = "char" substitution = [ "{{getChar()}}": #""\#(c)""#, "{{code}}": charcode, "{{getInsensitive()}}": "Case \(modes.i ? "in" : "")sensitive", ] } case .scalar(let scalar): `class` = "char" category = "misc" key = "char" substitution = [ "{{getChar()}}": #""\#(String(scalar.value))""#, "{{code}}": String(format: "U+%X", scalar.value.value), "{{getInsensitive()}}": "Case \(modes.i ? "in" : "")sensitive", ] case .scalarSequence(let scalarSequence): let scalars = scalarSequence.scalars let value = scalars.map { String($0.value) }.joined() let charcode = scalars.map { String(format: "U+%X", $0.value.value) }.joined(separator: " ") `class` = "char" category = "misc" key = "char" substitution = [ "{{getChar()}}": #""\#(value)""#, "{{code}}": charcode, "{{getInsensitive()}}": "Case \(modes.i ? "in" : "")sensitive", ] case .property(let prop): `class` = "charclass" switch prop.kind { case .any: category = "misc" key = "any" case .assigned: category = "misc" key = "assigned" case .ascii: category = "misc" key = "ascii" case .generalCategory(let cat): let uniCat: String switch cat { case .other: uniCat = "Other" case .control: uniCat = "Control" case .format: uniCat = "Format" case .unassigned: uniCat = "Unassigned" case .privateUse: uniCat = "Private use" case .surrogate: uniCat = "Surrogate" case .letter: uniCat = "Letter" case .casedLetter: uniCat = "Cased letter" case .lowercaseLetter: uniCat = "Lower case letter" case .modifierLetter: uniCat = "Modifier letter" case .otherLetter: uniCat = "Other letter" case .titlecaseLetter: uniCat = "Title case letter" case .uppercaseLetter: uniCat = "Upper case letter" case .mark: uniCat = "Mark" case .spacingMark: uniCat = "Spacing mark" case .enclosingMark: uniCat = "Enclosing mark" case .nonspacingMark: uniCat = "Non-spacing mark" case .number: uniCat = "Number" case .decimalNumber: uniCat = "Decimal number" case .letterNumber: uniCat = "Letter number" case .otherNumber: uniCat = "Other number" case .punctuation: uniCat = "Punctuation" case .connectorPunctuation: uniCat = "Connector punctuation" case .dashPunctuation: uniCat = "Dash punctuation" case .closePunctuation: uniCat = "Close punctuation" case .finalPunctuation: uniCat = "Final punctuation" case .initialPunctuation: uniCat = "Initial punctuation" case .otherPunctuation: uniCat = "Other punctuation" case .openPunctuation: uniCat = "Open punctuation" case .symbol: uniCat = "Symbol" case .currencySymbol: uniCat = "Currency symbol" case .modifierSymbol: uniCat = "Modifier symbol" case .mathSymbol: uniCat = "Mathematical symbol" case .otherSymbol: uniCat = "Other symbol" case .separator: uniCat = "Separator" case .lineSeparator: uniCat = "Line separator" case .paragraphSeparator: uniCat = "Paragraph separator" case .spaceSeparator: uniCat = "Space separator" } category = "charclasses" key = "unicodecat" substitution = ["{{getUniCat()}}": uniCat] case .binary(let property, let value): switch property { case .asciiHexDigit: break case .alphabetic: break case .bidiControl: break case .bidiMirrored: break case .cased: break case .compositionExclusion: break case .caseIgnorable: break case .changesWhenCasefolded: break case .changesWhenCasemapped: break case .changesWhenNFKCCasefolded: break case .changesWhenLowercased: break case .changesWhenTitlecased: break case .changesWhenUppercased: break case .dash: break case .deprecated: break case .defaultIgnorableCodePoint: break case .diacratic: break case .emojiModifierBase: break case .emojiComponent: break case .emojiModifier: break case .emoji: break case .emojiPresentation: break case .extender: break case .extendedPictographic: break case .fullCompositionExclusion: break case .graphemeBase: break case .graphemeExtended: break case .graphemeLink: break case .hexDigit: break case .hyphen: break case .idContinue: break case .ideographic: break case .idStart: break case .idsBinaryOperator: break case .idsTrinaryOperator: break case .joinControl: break case .logicalOrderException: break case .lowercase: break case .math: break case .noncharacterCodePoint: break case .otherAlphabetic: break case .otherDefaultIgnorableCodePoint: break case .otherGraphemeExtended: break case .otherIDContinue: break case .otherIDStart: break case .otherLowercase: break case .otherMath: break case .otherUppercase: break case .patternSyntax: break case .patternWhitespace: break case .prependedConcatenationMark: break case .quotationMark: break case .radical: break case .regionalIndicator: break case .softDotted: break case .sentenceTerminal: break case .terminalPunctuation: break case .unifiedIdiograph: break case .uppercase: break case .variationSelector: break case .whitespace: break case .xidContinue: break case .xidStart: break case .expandsOnNFC: break case .expandsOnNFD: break case .expandsOnNFKC: break case .expandsOnNFKD: break } category = "charclasses" key = "binary" case .script(_): category = "charclasses" key = "script" case .scriptExtension(_): category = "charclasses" key = "scriptextension" case .named(_): category = "charclasses" key = "named" case .numericType(_): category = "charclasses" key = "numerictype" case .numericValue(_): category = "charclasses" key = "numericvalue" case .mapping(_, _): category = "charclasses" key = "mapping" case .ccc(_): category = "charclasses" key = "ccc" case .age(let major, let minor): category = "charclasses" key = "age" case .block(_): category = "charclasses" key = "block" case .posix(let property): switch property { case .alnum: break case .blank: break case .graph: break case .print: break case .word: break case .xdigit: break } category = "charclasses" key = "posixcharclass" substitution = ["{{value}}": "\(property)"] case .pcreSpecial(_): category = "pcreSpecial" key = "pcrespecial" case .javaSpecial(_): category = "javaSpecial" key = "javaspecial" case .invalid(key: let k, value: let v): category = "charclasses" key = "invalid" } case .escaped(let escaped): switch escaped { case .alarm: `class` = "esc" category = "misc" key = "escchar" substitution = ["{{getChar()}}": "ALARM", "{{code}}": "(bell, 0x07)"] case .escape: `class` = "esc" category = "misc" key = "escchar" substitution = ["{{getChar()}}": "ESCAPE", "{{code}}": "(escape, 0x1B)"] case .formfeed: `class` = "esc" category = "misc" key = "escchar" substitution = ["{{getChar()}}": "FORM FEED", "{{code}}": "(form feed, 0x0C)"] case .newline: `class` = "esc" category = "misc" key = "escchar" substitution = ["{{getChar()}}": "LINE FEED", "{{code}}": "(ASCII 0x0A)"] case .carriageReturn: `class` = "esc" category = "misc" key = "escchar" substitution = ["{{getChar()}}": "CARRIAGE RETURN", "{{code}}": "(ASCII 0x0D)"] case .tab: `class` = "esc" category = "misc" key = "escchar" substitution = ["{{getChar()}}": "TAB", "{{code}}": "(ASCII 0x09)"] case .singleDataUnit: `class` = "esc" category = "misc" key = "escchar" substitution = ["{{getChar()}}": "SINGLE DATA UNIT", "{{code}}": "N/A"] case .decimalDigit: `class` = "charclass" category = "charclasses" key = "digit" case .notDecimalDigit: `class` = "charclass" category = "charclasses" key = "notdigit" case .horizontalWhitespace: `class` = "charclass" category = "charclasses" key = "hwhitespace" case .notHorizontalWhitespace: `class` = "charclass" category = "charclasses" key = "nothwhitespace" case .notNewline: `class` = "charclass" category = "charclasses" key = "notlinebreak" case .newlineSequence: `class` = "charclass" category = "charclasses" key = "linebreak" case .whitespace: `class` = "charclass" category = "charclasses" key = "whitespace" case .notWhitespace: `class` = "charclass" category = "charclasses" key = "notwhitespace" case .verticalTab: `class` = "charclass" category = "charclasses" key = "vwhitespace" case .notVerticalTab: `class` = "charclass" category = "charclasses" key = "notvwhitespace" case .wordCharacter: `class` = "charclass" category = "charclasses" key = "word" case .notWordCharacter: `class` = "charclass" category = "charclasses" key = "notword" case .backspace: `class` = "anchor" category = "charclasses" key = "wordboundary" case .graphemeCluster: `class` = "charclass" category = "charclasses" key = "graphemecluster" case .wordBoundary: `class` = "anchor" category = "anchors" key = "wordboundary" case .notWordBoundary: `class` = "anchor" category = "anchors" key = "notwordboundary" case .startOfSubject: `class` = "anchor" category = "anchors" key = "bos" case .endOfSubjectBeforeNewline: `class` = "anchor" category = "anchors" key = "eos" case .endOfSubject: `class` = "anchor" category = "anchors" key = "abseos" case .firstMatchingPositionInSubject: `class` = "anchor" category = "anchors" key = "prevmatchend" case .resetStartOfMatch: `class` = "charclass" category = "lookaround" key = "keepout" case .trueAnychar: `class` = "charclass" category = "charclass" key = "trueanychar" case .textSegment: `class` = "charclass" category = "charclass" key = "textsegment" case .notTextSegment: `class` = "charclass" category = "charclass" key = "nottextsegment" } case .keyboardControl(_): `class` = "charclass" category = "charclass" key = "keyboardcontrol" case .keyboardMeta(_): `class` = "charclass" category = "charclass" key = "keyboardmeta" case .keyboardMetaControl(_): `class` = "charclass" category = "charclass" key = "keyboardmetacontrol" case .namedCharacter(_): `class` = "charclass" category = "charclass" key = "namedcharacter" case .dot: `class` = "charclass" category = "charclasses" key = "dot" substitution = ["{{getDotAll()}}": "\(modes.s ? "including" : "except") line breaks"] case .caretAnchor: `class` = "anchor" category = "anchors" key = "bof" case .dollarAnchor: `class` = "anchor" category = "anchors" key = "eof" case .backreference(let ref): `class` = "ref" switch ref.kind { case .absolute(let n): category = "groups" key = "numref" substitution = ["{{group.num}}": "\(n)"] case .relative(let n): category = "groups" key = "numref" substitution = ["{{group.num}}": "\(n)"] case .named(let name): category = "groups" key = "namedref" substitution = ["{{group.name}}": name] } case .subpattern(let ref): if ref.kind.recursesWholePattern { `class` = "special" category = "other" key = "recursion" } else { `class` = "charclass" category = "charclasses" key = "subpattern" } case .callout(_): `class` = "charclass" category = "charclasses" key = "callout" case .backtrackingDirective(let directive): `class` = "charclass" switch directive.kind.value { case .accept: category = "charclass" key = "accept" case .fail: category = "charclass" key = "fail" case .mark: category = "charclass" key = "mark" case .commit: category = "charclass" key = "commit" case .prune: category = "charclass" key = "skip" case .skip: category = "charclass" key = "skip" case .then: category = "charclass" key = "then" } case .changeMatchingOptions(let matchingOptionSequence): var set = Set() let removing = matchingOptionSequence.removing.filter { set.insert($0).inserted } set = Set() let adding = matchingOptionSequence.adding.filter { set.insert($0).inserted }.filter { !removing.contains($0) } let enable = adding.map { pattern[$0.location.range] } let disable = removing.map { pattern[$0.location.range] } var modes = "" if !enable.isEmpty { modes += #" Enable "\#(enable.joined())"."# } if !disable.isEmpty { modes += #" Disable "\#(disable.joined())"."# } `class` = "special" category = "other" key = "mode" substitution = [ "{{~getDesc()}}": "Enables or disables modes for the remainder of the expression.", "{{~getModes()}}": modes, ] case .invalid: `class` = "charclass" category = "charclasses" key = "invalid" } tokens.append( Token( classes: [`class`], location: Location( start: atom.startPosition.utf16Offset(in: pattern), end: atom.endPosition.utf16Offset(in: pattern) ), selection: Location( start: atom.startPosition.utf16Offset(in: pattern), end: atom.endPosition.utf16Offset(in: pattern) ), tooltip: Tooltip(category: category, key: key, substitution: substitution) ) ) } private mutating func emitCustomCharacterClass(_ ccc: AST.CustomCharacterClass) { let category: String let key: String switch ccc.start.value { case .normal: category = "charclasses" key = "set" case .inverted: category = "charclasses" key = "setnot" } tokens.append( Token( classes: ["set"], location: Location( start: ccc.start.location.start.utf16Offset(in: pattern), end: ccc.start.location.end.utf16Offset(in: pattern) ), selection: Location( start: ccc.startPosition.utf16Offset(in: pattern), end: ccc.endPosition.utf16Offset(in: pattern) ), tooltip: Tooltip(category: category, key: key) ) ) tokens.append( Token( classes: ["group-set"], location: Location( start: ccc.startPosition.utf16Offset(in: pattern), end: ccc.endPosition.utf16Offset(in: pattern) ), selection: Location( start: ccc.startPosition.utf16Offset(in: pattern), end: ccc.endPosition.utf16Offset(in: pattern) ), tooltip: Tooltip(category: category, key: key) ) ) for member in ccc.members { switch member { case .custom(let custom): emitCustomCharacterClass(custom) case .range(let range): let lhs: String let rhs: String let dash = String(pattern[range.dashLoc.range]) switch range.lhs.kind { case .char(let c): lhs = String(c) case .scalar(let scalar): lhs = String(scalar.value) case .scalarSequence(let scalarSequence): lhs = String(scalarSequence.scalarValues) case .property(_): lhs = String(pattern[range.lhs.startPosition.. [Match] { let regex = try Regex(pattern) .anchorsMatchLineEndings(matchingOptions.contains("m")) .ignoresCase(matchingOptions.contains("i")) .dotMatchesNewlines(matchingOptions.contains("s")) .asciiOnlyWordCharacters(matchingOptions.contains("asciiOnlyWordCharacters")) .asciiOnlyDigits(matchingOptions.contains("asciiOnlyDigits")) .asciiOnlyWhitespace(matchingOptions.contains("asciiOnlyWhitespace")) .asciiOnlyCharacterClasses(matchingOptions.contains("asciiOnlyCharacterClasses")) let matches = matchingOptions.contains("g") ? text.matches(of: regex) : text.firstMatch(of: regex).flatMap { [$0] } ?? [] return matches.map { let captures: [Group] = $0.lazy.elements.dropFirst().map { if let range = $0.range { return Group( location: Location( start: range.lowerBound.utf16Offset(in: text), end: range.upperBound.utf16Offset(in: text) ), value: String(text[range]), name: $0.name ) } else { return Group( location: nil, value: nil, name: $0.name ) } } return Match( location: Location( start: $0.range.lowerBound.utf16Offset(in: text), end: $0.range.upperBound.utf16Offset(in: text) ), value: String(text[$0.range]), captures: captures ) } } } struct Match: Codable { let location: Location let value: String let captures: [Group] } struct Group: Codable { let location: Location? let value: String? let name: String? } struct Location: Codable { let start: Int let end: Int } ================================================ FILE: Tests/RegexTests/ConverterTests.swift ================================================ import Foundation import XCTest @testable import DSLConverter class ConverterTests: XCTestCase { func testConvertPattern() throws { do { let converter = DSLConverter() let builderDSL = try converter.convert(#"gray|grey"#) print(builderDSL) } do { let converter = DSLConverter() let builderDSL = try converter.convert(#"\b(?:[a-eg-z]|f(?!oo))\w*\b"#) print(builderDSL) } do { let converter = DSLConverter() let builderDSL = try converter.convert(#"\K\K"#) print(builderDSL) } catch { print(error) } } } ================================================ FILE: Tests/RegexTests/ExpressionParserTests.swift ================================================ import Foundation import XCTest @testable import ExpressionParser class ParserTests: XCTestCase { func testParseExpression() { let options: [String] = [] do { var parser = ExpressionParser(pattern: #"a(?R)?b"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"\d+(?(?=regex)then|else(?(?=regex)then|else))(a)^(START)?\d+(?(1)END|\b)"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"^[^<>]*(((?'Open'<)[^<>]*)+((?'Close-Open'>)[^<>]*)+)*(?(Open)(?!))$"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"hello"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"gray|grey"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"gr(a|e)y"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"gr[ae]y"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"colou?r"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"rege(x(es)?|xps?)"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"go*gle"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"go+gle"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"g(oog)+le"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"z{3}"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"z{3,6}"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"z{3,}"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"[Bb]rainf\*\*k"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"\d"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"\d+"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"\d{5}(-\d{4})?"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"1\d{10}"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"[2-9]|[12]\d|3[0-6]"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"Hello\nworld"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"mi.....ft"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"\d+(\.\d\d)?"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"[^i*&2@]"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"//[^\r\n]*[\r\n]"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"^dog"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"dog$"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"^dog$"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"\w++\d\d\w+"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"<(\w+)>[^<]*"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"Hillary(?=\s+Clinton)"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"q(?!u)"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"(?<=-)\p{L}+"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"[\x41-\x45]{3}"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"(?(?=regex)then|else)"#, matchingOptions: options) parser.parse() print(parser.tokens) } do { var parser = ExpressionParser(pattern: #"(?\w+)\W+(?<-word>\w+)"#, matchingOptions: options) parser.parse() print(parser.tokens) } } } ================================================ FILE: Tests/RegexTests/MatcherTests.swift ================================================ import Foundation import XCTest @testable import Matcher class MatchTest: XCTestCase { func testMatch() throws { do { let pattern = #"[A-Z]\w+"# let text = """ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. """ let matches = try Matcher.match(pattern: pattern, text: text) print(matches) } do { let pattern = #"\d+"# let text = """ KIND DATE INSTITUTION AMOUNT ---------------------------------------------------------------- CREDIT 03/01/2022 Payroll from employer $200.23 CREDIT 03/03/2022 Suspect A $2,000,000.00 DEBIT 03/03/2022 Ted's Pet Rock Sanctuary $2,000,000.00 DEBIT 03/05/2022 Doug's Dugout Dogs $33.27 DEBIT 06/03/2022 Oxford Comma Supply Ltd. £57.33 """ let matches = try Matcher.match(pattern: pattern, text: text) print(matches) } do { let pattern = #"((\d{3})(?:\.|-))?(\d{3})(?:\.|-)(\d{4})"# let text = """ Call 555-1212 for info """ let matches = try Matcher.match(pattern: pattern, text: text) print(matches) } } } ================================================ FILE: package.json ================================================ { "name": "swiftregex", "scripts": { "prod": "webpack --progress --config webpack.prod.js", "dev": "webpack --progress --config webpack.dev.js" }, "dependencies": { "@createjs/easeljs": "2.0.0-beta.4", "@fortawesome/fontawesome-svg-core": "7.2.0", "@fortawesome/free-brands-svg-icons": "7.2.0", "@fortawesome/free-solid-svg-icons": "^7.0.0", "@fortawesome/pro-duotone-svg-icons": "6.1.1", "@fortawesome/pro-light-svg-icons": "6.1.1", "@fortawesome/pro-regular-svg-icons": "6.1.1", "@fortawesome/pro-solid-svg-icons": "6.1.1", "@popperjs/core": "2.11.8", "bootstrap": "5.3.8", "codemirror": "5.65.21", "pako": "2.1.0", "reconnecting-websocket": "4.4.0", "tippy.js": "6.3.7" }, "devDependencies": { "autoprefixer": "10.5.0", "copy-webpack-plugin": "14.0.0", "css-loader": "7.1.4", "html-webpack-plugin": "5.6.7", "mini-css-extract-plugin": "2.10.2", "postcss": "8.5.14", "postcss-loader": "8.2.1", "sass": "1.99.0", "sass-loader": "16.0.8", "style-loader": "4.0.0", "webpack": "5.106.2", "webpack-bundle-analyzer": "5.3.0", "webpack-cli": "7.0.2", "webpack-merge": "6.0.1" } } ================================================ FILE: webpack.common.js ================================================ const path = require("path"); const CopyWebbackPlugin = require("copy-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); module.exports = { entry: { index: "./Public/index.js", }, output: { globalObject: "self", filename: "[name].[contenthash].js", path: path.resolve(__dirname, "Public/dist"), publicPath: "/", clean: true, }, module: { rules: [ { test: /\.scss$/, use: [ { loader: MiniCssExtractPlugin.loader, }, { loader: "css-loader", options: { url: false, sourceMap: true, importLoaders: 2, }, }, { loader: "postcss-loader", options: { sourceMap: true, postcssOptions: { plugins: ["autoprefixer"], }, }, }, { loader: "sass-loader", options: { sourceMap: true, }, }, ], }, { test: /\.css$/, use: ["style-loader", "css-loader"], }, { test: /\.(woff|woff2|eot|ttf|otf)$/i, type: "asset/resource", }, ], }, plugins: [ new CopyWebbackPlugin({ patterns: [ { from: "./Public/images/*.*", to: "images/[name][ext]" }, { from: "./Public/favicons/*.*", to: "[name][ext]" }, { from: "./Public/error.html", to: "error.leaf" }, { from: "./Public/robots.txt", to: "robots.txt" }, ], }), new HtmlWebpackPlugin({ chunks: ["index"], filename: "index.leaf", template: "./Public/index.html", }), new MiniCssExtractPlugin({ filename: "[name].[contenthash].css", }), ], }; ================================================ FILE: webpack.dev.js ================================================ const { merge } = require("webpack-merge"); const common = require("./webpack.common.js"); module.exports = merge(common, { mode: "development", devtool: "inline-source-map", }); ================================================ FILE: webpack.prod.js ================================================ const { merge } = require("webpack-merge"); const common = require("./webpack.common.js"); const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin; module.exports = merge(common, { mode: "production", devtool: "source-map", plugins: [ new BundleAnalyzerPlugin({ analyzerMode: "static", openAnalyzer: false }), ], });