├── public ├── favicon.ico ├── favicon.svg └── icons.svg ├── vitest.config.js ├── src ├── entry-client.jsx ├── components │ ├── ErrorMessage.module.css │ ├── HighlightWhiteSpace.module.css │ ├── Range.jsx │ ├── Table.module.css │ ├── ErrorMessage.jsx │ ├── EnhancedTextarea.module.css │ ├── HighlightWhiteSpace.jsx │ ├── Table.jsx │ └── EnhancedTextarea.jsx ├── lib │ ├── range.js │ ├── featureDetection.js │ ├── range.test.js │ ├── getRegExpFromString.js │ ├── getRegExpFromString.test.js │ ├── createShortcut.js │ ├── markText.js │ └── markText.test.js ├── routes │ ├── cheat-sheet │ │ ├── index.module.css │ │ └── index.jsx │ ├── [...404].jsx │ └── (home) │ │ ├── index.module.css │ │ └── index.jsx ├── entry-server.jsx ├── settings.js ├── app.jsx ├── app.css └── context │ └── app.jsx ├── app.config.js ├── jsconfig.json ├── .gitignore ├── README.md ├── .eslintrc.cjs ├── package.json └── .github └── workflows └── main.yml /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loonkwil/regexify/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /src/entry-client.jsx: -------------------------------------------------------------------------------- 1 | // @refresh reload 2 | import { mount, StartClient } from "@solidjs/start/client"; 3 | 4 | mount(() => , document.getElementById("app")); 5 | -------------------------------------------------------------------------------- /app.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@solidjs/start/config"; 2 | 3 | export default defineConfig({ 4 | server: { 5 | baseURL: process.env.BASE_PATH, 6 | preset: "static", 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "preserve", 4 | "jsxImportSource": "solid-js", 5 | "paths": { 6 | "~/*": [ 7 | "./src/*" 8 | ] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/ErrorMessage.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | display: grid; 3 | justify-content: center; 4 | align-content: center; 5 | gap: 2rem; 6 | text-align: center; 7 | 8 | & div { 9 | font-weight: normal; 10 | font-size: 3rem; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/range.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a list of numbers from "start" (inclusive) to "end" (exclusive) 3 | * 4 | * @param {number} start 5 | * @param {number} end 6 | */ 7 | export default (start, end) => 8 | Array.from({ length: Math.abs(end - start) }, (_, i) => start + i); 9 | -------------------------------------------------------------------------------- /src/routes/cheat-sheet/index.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | display: grid; 3 | grid-template-columns: auto 1fr; 4 | gap: 1.5rem; 5 | 6 | & dl { 7 | display: grid; 8 | grid-template-columns: auto 1fr; 9 | gap: 0.5rem; 10 | } 11 | 12 | & dd { 13 | margin: 0; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/featureDetection.js: -------------------------------------------------------------------------------- 1 | /** @returns {boolean} */ 2 | export const isCSSNestingSupported = () => CSS.supports("selector(&)"); 3 | 4 | /** @returns {boolean} */ 5 | export const isModernRegExpSupported = () => 6 | typeof String.prototype.matchAll === "function" && 7 | "hasIndices" in RegExp.prototype; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | dist 3 | .solid 4 | .output 5 | .vercel 6 | .netlify 7 | .vinxi 8 | app.config.timestamp_*.js 9 | 10 | # Environment 11 | .env 12 | .env*.local 13 | 14 | # dependencies 15 | /node_modules 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | *.launch 22 | .settings/ 23 | 24 | # Temp 25 | gitignore 26 | 27 | # System Files 28 | .DS_Store 29 | Thumbs.db 30 | -------------------------------------------------------------------------------- /src/components/HighlightWhiteSpace.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | [data-char] { 3 | vertical-align: bottom; 4 | position: relative; 5 | display: inline-block; 6 | height: 1lh; 7 | cursor: text; 8 | 9 | &:after { 10 | content: attr(data-char); 11 | color: var(--text-secondary); 12 | position: absolute; 13 | left: 0; 14 | top: 0; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Range.jsx: -------------------------------------------------------------------------------- 1 | import { Index } from "solid-js"; 2 | import range from "~/lib/range"; 3 | 4 | /** 5 | * @param {Object} props 6 | * @param {number} props.start 7 | * @param {number} props.end 8 | * @param {function(function(): number, number)} props.children 9 | * @returns {import('solid-js').JSX.Element} 10 | */ 11 | export default (props) => ( 12 | {props.children} 13 | ); 14 | -------------------------------------------------------------------------------- /src/lib/range.test.js: -------------------------------------------------------------------------------- 1 | import range from "./range"; 2 | 3 | describe("range", () => { 4 | test.each([ 5 | // start, end, expected 6 | [-1, 2, [-1, 0, 1]], 7 | [1, 2, [1]], 8 | ])("it should create the range", (start, end, expected) => { 9 | expect(range(start, end)).toMatchObject(expected); 10 | }); 11 | 12 | test("it should create an empty range if the start and the end is equal", () => { 13 | expect(range(1, 1)).toHaveLength(0); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/routes/[...404].jsx: -------------------------------------------------------------------------------- 1 | import { HttpStatusCode } from "@solidjs/start"; 2 | import { A } from "@solidjs/router"; 3 | import ErrorMessage from "~/components/ErrorMessage"; 4 | 5 | export default () => ( 6 | <> 7 | 8 | 11 | 404 Page Not Found 12 |
13 | Go Back to the Home Page 14 | 15 | } 16 | /> 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Regexify 2 | 3 | ![Preview of the app](https://github.com/loonkwil/regexify/assets/1401202/56db1bcb-68da-449c-9c32-87e8f044edf8) 4 | 5 | https://loonkwil.github.io/regexify/ 6 | 7 | A web-based tool that allows you to interactively test and experiment with 8 | regular expressions in real-time. 9 | 10 | The primary goal of this application is to experiment with 11 | [SolidJS](https://solidjs.com/), 12 | [SolidStart](https://start.solidjs.com/), 13 | Server Side Rendering, 14 | [nested CSS](https://drafts.csswg.org/css-nesting/), 15 | and accessibility. 16 | -------------------------------------------------------------------------------- /src/components/Table.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | & table { 3 | white-space: pre-wrap; 4 | width: 100%; 5 | border-collapse: collapse; 6 | 7 | & thead { 8 | text-align: left; 9 | } 10 | 11 | :is(th, td) { 12 | padding: 0.5rem 0.75rem; 13 | } 14 | 15 | & td { 16 | border-top: 1px solid var(--border-secondary); 17 | 18 | &:empty:after { 19 | content: " "; 20 | } 21 | } 22 | } 23 | 24 | & button { 25 | padding: 0.5rem 0.75rem; 26 | cursor: pointer; 27 | text-decoration: underline; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/getRegExpFromString.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @example 3 | * // returns /[0-9]/g 4 | * getRegExpFromString(`/ 5 | * [0-9] 6 | * /g`) 7 | * @param {string} 8 | * @returns {RegExp} 9 | * @throws {SyntaxError} 10 | */ 11 | export default (str) => { 12 | const oneline = str.replaceAll("\n", "").trim(); 13 | const match = oneline.match(/\/(?.*?)\/(?[a-z]+)?$/); 14 | if (!match) { 15 | throw new SyntaxError("Invalid RegExp"); 16 | } 17 | 18 | const { 19 | groups: { pattern, flags = "" }, 20 | } = match; 21 | return new RegExp(pattern, flags); 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/ErrorMessage.jsx: -------------------------------------------------------------------------------- 1 | import styles from "./ErrorMessage.module.css"; 2 | import { createEffect } from "solid-js"; 3 | 4 | /** 5 | * @param {Object} props 6 | * @param {string} props.message 7 | * @param {Error=} props.error 8 | * @returns {import('solid-js').JSX.Element} 9 | */ 10 | export default (props) => { 11 | createEffect(() => { 12 | if (props.error) { 13 | console.error(props.error); 14 | } 15 | }); 16 | 17 | return ( 18 |
19 | 20 |

{props.message}

21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es2021: true, 6 | }, 7 | plugins: ["solid"], 8 | extends: ["eslint:recommended", "plugin:solid/recommended"], 9 | 10 | overrides: [], 11 | parserOptions: { 12 | ecmaVersion: "latest", 13 | sourceType: "module", 14 | }, 15 | rules: {}, 16 | globals: { 17 | suite: true, 18 | test: true, 19 | describe: true, 20 | it: true, 21 | expect: true, 22 | assert: true, 23 | vitest: true, 24 | vi: true, 25 | beforeAll: true, 26 | afterAll: true, 27 | beforeEach: true, 28 | afterEach: true, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Regexify", 3 | "description": "Live JavaScript RegExp tester", 4 | "scripts": { 5 | "dev": "vinxi dev", 6 | "build": "vinxi build", 7 | "test:unit": "vitest", 8 | "test": "npm run test:unit" 9 | }, 10 | "type": "module", 11 | "devDependencies": { 12 | "@solidjs/meta": "^0.29.4", 13 | "@solidjs/router": "^0.15.3", 14 | "@solidjs/start": "^1.1.1", 15 | "eslint": "^8.42.0", 16 | "eslint-plugin-solid": "^0.14.5", 17 | "postcss": "^8.5.3", 18 | "solid-js": "^1.9.5", 19 | "vinxi": "^0.5.3", 20 | "vite": "^6.2.0", 21 | "vitest": "^3.0.7" 22 | }, 23 | "version": "1.3.3" 24 | } 25 | -------------------------------------------------------------------------------- /src/entry-server.jsx: -------------------------------------------------------------------------------- 1 | // @refresh reload 2 | import { createHandler, StartServer } from "@solidjs/start/server"; 3 | 4 | export default createHandler(() => ( 5 | ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | {assets} 14 | 15 | 16 |
{children}
17 | {scripts} 18 | 19 | 20 | )} 21 | /> 22 | )); 23 | -------------------------------------------------------------------------------- /src/lib/getRegExpFromString.test.js: -------------------------------------------------------------------------------- 1 | import getRegExpFromString from "./getRegExpFromString"; 2 | 3 | describe("getRegExpFromString", () => { 4 | test.each([ 5 | "/", 6 | "/(?:)/z", // Invalid flag 7 | "/[/g", 8 | ])("it should throw an error if the RegExp is invalid", (pattern) => { 9 | expect(() => getRegExpFromString(pattern)).toThrowError(SyntaxError); 10 | }); 11 | 12 | test("it should convert the string literal to a RegExp object", () => { 13 | const str = "/./g"; 14 | const regexp = getRegExpFromString(str); 15 | expect(regexp).toBeInstanceOf(RegExp); 16 | expect(regexp.toString()).toBe(str); 17 | }); 18 | 19 | test("it should work with a multiline pattern", () => { 20 | const str = `/ 21 | [0-9]+/`; 22 | const regexp = getRegExpFromString(str); 23 | expect("1234".match(regexp)[0]).toEqual("1234"); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | export const initialPattern = 2 | "/(?ctrl|alt|cmd)\\s?[+]\\s?(?\\w)/gi"; 3 | 4 | export const initialInput = `🐭 Size and Speed 5 | It uses SolidJS and Server Side Rendering to be fast. 6 | The initial page is less than 40kB. 7 | 8 | ♿️ Accessibility 9 | Keyboard navigation, screen readers, light and dark modes, and high contrast are supported. 10 | 11 | ⌨️ Keyboard Shortcuts 12 | Select pattern: Ctrl + P. 13 | Select input field: Ctrl + I. 14 | Copy RegExp: Ctrl + S. 15 | Open cheat sheet: Ctrl + M (hit Ctrl + M again to close it). 16 | 17 | 🔒 Privacy 18 | Everything is calculated in your browser. 19 | Your data will not be uploaded or stored anywhere. 20 | There are no ads or cookies. 21 | 22 | ✨ Extended RegExp 23 | You can use a multiline string as a pattern.`; 24 | 25 | // Debounce input events if the text is too large 26 | export const largeInput = 1e4; 27 | 28 | export const debounceTimeoutInMs = 200; 29 | -------------------------------------------------------------------------------- /src/lib/createShortcut.js: -------------------------------------------------------------------------------- 1 | import { isServer } from "solid-js/web"; 2 | import { onCleanup } from "solid-js"; 3 | 4 | /** 5 | * @param {Object} options 6 | * @param {string} options.key 7 | * @param {boolean=} options.ctrlKey 8 | * @param {boolean=} options.altKey 9 | * @param {boolean=} options.metaKey 10 | * @param {boolean=} options.shiftKey 11 | * @param {function(KeyboardEvent)} cb 12 | */ 13 | export default ( 14 | { key, ctrlKey = false, altKey = false, metaKey = false, shiftKey = false }, 15 | cb 16 | ) => { 17 | if (isServer) { 18 | return; 19 | } 20 | 21 | const handler = (e) => { 22 | if ( 23 | key.toLowerCase() === e.key && 24 | ctrlKey === e.ctrlKey && 25 | altKey === e.altKey && 26 | metaKey === e.metaKey && 27 | shiftKey === e.shiftKey 28 | ) { 29 | cb(e); 30 | } 31 | }; 32 | window.addEventListener("keydown", handler); 33 | onCleanup(() => { 34 | window.removeEventListener("keydown", handler); 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/EnhancedTextarea.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | resize: vertical; 3 | border: 2px solid var(--border-default); 4 | border-radius: 0.5rem; 5 | background: var(--input-bg); 6 | padding: 0.5rem 0.75rem 0.5rem; 7 | overflow: hidden; 8 | display: grid; 9 | gap: 0.5rem; 10 | grid-template-rows: auto 1fr; 11 | min-height: 4rem; 12 | user-select: none; 13 | transition: border-color 150ms; 14 | 15 | &:focus-within { 16 | border-color: var(--border-active); 17 | } 18 | 19 | &:has(textarea:invalid) { 20 | border-color: var(--border-error); 21 | } 22 | 23 | & label { 24 | font-size: var(--text-sm); 25 | font-weight: bold; 26 | color: var(--text-secondary); 27 | } 28 | } 29 | 30 | .container { 31 | overflow: auto; 32 | height: 100%; 33 | display: grid; 34 | 35 | & > * { 36 | grid-area: 1 / 1; 37 | border: none; 38 | padding: 0; 39 | margin: 0; 40 | white-space: pre-wrap; 41 | background: transparent; 42 | color: transparent; 43 | } 44 | 45 | & > textarea { 46 | color: var(--text-main); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/routes/(home)/index.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | display: grid; 3 | gap: 1.5rem; 4 | grid-template-columns: 1fr auto; 5 | grid-template-rows: auto 1fr; 6 | } 7 | 8 | .main { 9 | grid-column-end: span 2; 10 | display: grid; 11 | gap: 1.5rem; 12 | grid-template-areas: 13 | "pattern" 14 | "input" 15 | "matches"; 16 | grid-template-columns: 100%; 17 | grid-template-rows: auto auto 1fr; 18 | 19 | @media screen and (min-width: 1024px) { 20 | grid-template-areas: 21 | "pattern matches" 22 | "input matches"; 23 | grid-template-columns: 1fr 1fr; 24 | grid-template-rows: auto 1fr; 25 | } 26 | } 27 | 28 | .pattern { 29 | grid-area: pattern; 30 | } 31 | 32 | .animate { 33 | animation: blink 150ms 1; 34 | } 35 | 36 | .input { 37 | grid-area: input; 38 | 39 | & mark { 40 | background-color: var(--mark-bg-default); 41 | transition: background-color 150ms; 42 | 43 | & span { 44 | background-color: transparent; 45 | transition: inherit; 46 | } 47 | } 48 | } 49 | 50 | .matches { 51 | grid-area: matches; 52 | } 53 | 54 | @keyframes blink { 55 | from { 56 | opacity: 1; 57 | } 58 | 59 | 50% { 60 | opacity: 0; 61 | } 62 | 63 | to { 64 | opacity: 1; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /public/icons.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | 14 | 15 | 16 | 17 | 18 | 19 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/components/HighlightWhiteSpace.jsx: -------------------------------------------------------------------------------- 1 | import { Show, createMemo } from "solid-js"; 2 | import markText from "~/lib/markText"; 3 | import styles from "./HighlightWhiteSpace.module.css"; 4 | 5 | const replacements = { 6 | "\u{0020}": "·", // Space 7 | "\u{00A0}": "⍽", // No-Break Space 8 | "\u{0009}": "→", // Tab 9 | "\u{000A}": "⏎", // New Line 10 | }; 11 | 12 | /** 13 | * @param {Object} props 14 | * @param {string} props.children 15 | * @returns {Element} 16 | */ 17 | export default (props) => { 18 | const positions = createMemo(() => 19 | props.children 20 | ? Array.from(props.children.matchAll(/\s/g), ({ index }) => [ 21 | index, 22 | index + 1, 23 | ]) 24 | : [], 25 | ); 26 | 27 | return ( 28 | 29 |
30 | 45 |
46 |
47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/lib/markText.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import('solid-js').JSX.Element} Element */ 2 | 3 | /** @typedef {Array} Boundary */ 4 | 5 | /** 6 | * @callback MarkerFn 7 | * @param {string} text - Selected text 8 | * @param {number} count - Iteration number (0-indexed) 9 | * @param {Boundary} boundary 10 | * @returns {Element} 11 | */ 12 | 13 | /** 14 | * @example 15 | * // returns ['ABC', DE, 'F'] 16 | * markText('ABCDEF', [[2, 4]], (text) => {text}) 17 | * @param {string} text 18 | * @param {Array} boundaries 19 | * @param {MarkerFn} markerFn 20 | * @param {number=} offset - Offset the coordinates in the boundaries array 21 | * @returns {Array} 22 | */ 23 | export default (text, boundaries, markerFn, offset = 0) => { 24 | const fragments = []; 25 | for (let i = 0; i < boundaries.length; i += 1) { 26 | const boundary = boundaries[i]; 27 | if (!boundary) { 28 | continue; 29 | } 30 | 31 | const start = boundary[0] - offset; 32 | const end = boundary[1] - offset; 33 | 34 | const before = text.substring(0, start); 35 | const selected = text.substring(start, end); 36 | const after = text.substring(end); 37 | 38 | fragments.push(before, markerFn(selected, i, boundary)); 39 | 40 | text = after; 41 | offset = boundary[1]; 42 | } 43 | 44 | if (text) { 45 | fragments.push(text); 46 | } 47 | 48 | return fragments; 49 | }; 50 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | 3 | on: 4 | release: 5 | types: [published] 6 | # Allows you to run this workflow manually from the Actions tab 7 | workflow_dispatch: 8 | 9 | 10 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 11 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 12 | concurrency: 13 | group: "pages" 14 | cancel-in-progress: false 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: read 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | - name: Setup Node 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: "lts/*" 28 | - name: Install Dependencies 29 | run: npm ci --ignore-scripts 30 | - name: Run Unit Tests 31 | run: npm test 32 | - name: Build 33 | run: npm run build 34 | env: 35 | BASE_PATH: /regexify/ 36 | - name: Upload Artifact 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | path: ".output/public" 40 | 41 | deploy: 42 | needs: build 43 | environment: 44 | name: github-pages 45 | url: ${{ steps.deployment.outputs.page_url }} 46 | runs-on: ubuntu-latest 47 | permissions: 48 | pages: write 49 | id-token: write 50 | steps: 51 | - name: Deploy 52 | id: deployment 53 | uses: actions/deploy-pages@v4 54 | -------------------------------------------------------------------------------- /src/lib/markText.test.js: -------------------------------------------------------------------------------- 1 | import { vi } from "vitest"; 2 | import markText from "./markText"; 3 | 4 | describe("markText", () => { 5 | test("it should mark the text", () => { 6 | const text = "foo bar baz"; 7 | const boundaries = [ 8 | [0, 3], 9 | [4, 7], 10 | ]; 11 | const marker = (selected) => `*${selected}*`; 12 | expect(markText(text, boundaries, marker).join("")).toBe("*foo* *bar* baz"); 13 | }); 14 | 15 | test("it should return with the original text if there are no indices", () => { 16 | const text = "foo bar baz"; 17 | const boundaries = []; 18 | const marker = (selected) => selected; 19 | expect(markText(text, boundaries, marker)).toEqual([text]); 20 | }); 21 | 22 | test("it should call the marker function with the proper arguments", () => { 23 | const text = "foo bar baz"; 24 | const boundaries = [ 25 | [0, 3], 26 | [4, 7], 27 | [8, 11], 28 | ]; 29 | const marker = vi.fn((selected) => selected); 30 | markText(text, boundaries, marker); 31 | 32 | expect(marker).toHaveBeenCalledTimes(3); 33 | expect(marker).toHaveBeenNthCalledWith(1, "foo", 0, boundaries[0]); 34 | expect(marker).toHaveBeenNthCalledWith(2, "bar", 1, boundaries[1]); 35 | expect(marker).toHaveBeenNthCalledWith(3, "baz", 2, boundaries[2]); 36 | }); 37 | 38 | test("it should offset the coordinates", () => { 39 | const text = "foo bar"; 40 | const boundaries = [[5, 8]]; 41 | const offset = 1; 42 | const marker = (selected) => `*${selected}*`; 43 | expect(markText(text, boundaries, marker, offset).join("")).toBe( 44 | "foo *bar*" 45 | ); 46 | }); 47 | 48 | test("it should skip the coordinates if it is undefined", () => { 49 | const text = "foo bar"; 50 | const boundaries = [undefined, [4, 7]]; 51 | const marker = vi.fn((selected) => selected); 52 | markText(text, boundaries, marker); 53 | 54 | expect(marker).toHaveBeenCalledTimes(1); 55 | expect(marker).toHaveBeenNthCalledWith(1, "bar", 1, boundaries[1]); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/app.jsx: -------------------------------------------------------------------------------- 1 | import { MetaProvider, Title } from "@solidjs/meta"; 2 | import { Router } from "@solidjs/router"; 3 | import { FileRoutes } from "@solidjs/start/router"; 4 | import { isServer } from "solid-js/web"; 5 | import { Suspense, Show, ErrorBoundary } from "solid-js"; 6 | import { AppProvider } from "~/context/app"; 7 | import ErrorMessage from "~/components/ErrorMessage"; 8 | import { 9 | isModernRegExpSupported, 10 | isCSSNestingSupported, 11 | } from "~/lib/featureDetection"; 12 | import "./app.css"; 13 | 14 | export default function App() { 15 | const base = import.meta.env.SERVER_BASE_URL.replace(/\/$/, ""); 16 | return ( 17 | ( 20 | 21 | Regexify 22 | 30 | Your Browser is not Supported 31 |
32 | Try to use a browser that supports{" "} 33 | 38 | nested CSS 39 | {" "} 40 | and the{" "} 41 | 46 | matchAll 47 | {" "} 48 | RegExp function (Chrome 112+, Safari 16.5+, Firefox 117+). 49 | 50 | } 51 | /> 52 | } 53 | > 54 | ( 56 | 59 | Something Went Wrong 60 |
61 | Try to reload the page 62 | 63 | } 64 | error={e} 65 | /> 66 | )} 67 | > 68 | 69 | {props.children} 70 | 71 |
72 |
73 |
74 | )} 75 | > 76 | 77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | /* Variable declarations */ 2 | :root { 3 | --black: #26262c; 4 | --gray-900: #747486; 5 | --gray-600: #b2b2bd; 6 | --gray-300: #f2f2f2; 7 | --yellow: #f6d578; 8 | --red: #ff565e; 9 | --white: #ffffff; 10 | 11 | --bg: var(--gray-300); 12 | --text-main: var(--black); 13 | --text-secondary: var(--gray-900); 14 | --border-default: var(--black); 15 | --border-active: var(--yellow); 16 | --border-error: var(--red); 17 | --border-secondary: var(--gray-600); 18 | --mark-bg-default: var(--gray-600); 19 | --mark-bg-active: var(--yellow); 20 | --input-bg: var(--white); 21 | 22 | --text-xl: 1.5rem; /* 24px */ 23 | --text-lg: 1.25rem; /* 20px */ 24 | --text-md: 1rem; /* 16px */ 25 | --text-sm: 0.75rem; /* 12px */ 26 | --text-xs: 0.5rem; /* 8px */ 27 | 28 | @media (prefers-contrast: more) { 29 | --black: #000000; 30 | --yellow: #ffff00; 31 | --red: #ff0000; 32 | 33 | --bg: var(--white); 34 | --text-secondary: var(--black); 35 | --border-secondary: var(--black); 36 | } 37 | 38 | @media (prefers-color-scheme: dark) { 39 | --bg: var(--black); 40 | --text-main: var(--white); 41 | --text-secondary: var(--gray-600); 42 | --border-default: var(--white); 43 | --border-active: var(--yellow); 44 | --border-error: var(--red); 45 | --border-secondary: var(--gray-900); 46 | --mark-bg-default: var(--gray-900); 47 | --mark-bg-active: var(--yellow); 48 | --input-bg: var(--black); 49 | 50 | @media (prefers-contrast: more) { 51 | --text-secondary: var(--white); 52 | --border-secondary: var(--white); 53 | } 54 | } 55 | } 56 | 57 | /* Resets */ 58 | html { 59 | box-sizing: border-box; 60 | } 61 | 62 | *, 63 | *:before, 64 | *:after { 65 | box-sizing: inherit; 66 | } 67 | 68 | /* Global formatting */ 69 | html { 70 | font: 71 | var(--text-md) / 1.25rem "SF Mono", 72 | "Consolas", 73 | "DejaVu Sans Mono", 74 | "Roboto Mono", 75 | monospace; 76 | color: var(--text-main); 77 | background: var(--bg); 78 | } 79 | 80 | body { 81 | margin: 0; 82 | padding: 1.5rem 1rem; 83 | display: grid; 84 | min-height: 100vh; 85 | } 86 | 87 | h1, 88 | h2, 89 | h3, 90 | h4, 91 | h5, 92 | h6 { 93 | margin: 0; 94 | } 95 | 96 | h1 { 97 | font-size: var(--text-xl); 98 | font-weight: 700; 99 | } 100 | 101 | h2 { 102 | font-size: var(--text-lg); 103 | font-weight: 700; 104 | } 105 | 106 | p { 107 | margin: 0; 108 | } 109 | 110 | a { 111 | color: inherit; 112 | } 113 | 114 | button { 115 | font: inherit; 116 | color: inherit; 117 | border: none; 118 | padding: 0; 119 | background: transparent; 120 | } 121 | 122 | textarea { 123 | font: inherit; 124 | outline: none; 125 | resize: none; 126 | border: none; 127 | color: inherit; 128 | } 129 | 130 | mark { 131 | color: inherit; 132 | } 133 | 134 | code { 135 | background: var(--dark-gray); 136 | } 137 | -------------------------------------------------------------------------------- /src/components/Table.jsx: -------------------------------------------------------------------------------- 1 | import { Show, Index, createMemo, mergeProps, createUniqueId } from "solid-js"; 2 | import Range from "~/components/Range"; 3 | import styles from "./Table.module.css"; 4 | 5 | /** @typedef {import('solid-js').JSX.Element} Element */ 6 | 7 | /** 8 | * @param {Object} props 9 | * @param {number=} props.rowLimit - Only show the first "n" row and a link to show the rest. 10 | * @param {Array>} props.data 11 | * @param {Array} props.header 12 | * @param {function(Element): Element=} props.renderCell 13 | * @param {Function=} props.onShowAllRequest 14 | * @param {function(Array)=} props.onHover 15 | * @param {Function=} props.onLeave 16 | * @returns {Element} 17 | */ 18 | export default (props) => { 19 | props = mergeProps( 20 | { 21 | rowLimit: Infinity, 22 | renderCell(el) { 23 | return el; 24 | }, 25 | onShowAllRequest() {}, 26 | onHover() {}, 27 | onLeave() {}, 28 | }, 29 | props, 30 | ); 31 | 32 | const id = createUniqueId(); 33 | const length = createMemo(() => Math.min(props.rowLimit, props.data.length)); 34 | 35 | /** 36 | * @param {HTMLElement} el 37 | * @returns {?Array} 38 | */ 39 | const getCoordinates = (el) => { 40 | let elWithColProp = el; 41 | while (elWithColProp && !elWithColProp.dataset.col) { 42 | elWithColProp = elWithColProp.parentElement; 43 | } 44 | 45 | if (!elWithColProp) { 46 | return null; 47 | } 48 | 49 | const { 50 | dataset: { col }, 51 | } = elWithColProp; 52 | const { 53 | dataset: { row }, 54 | } = elWithColProp.parentElement; 55 | return [parseInt(row, 10), parseInt(col, 10)]; 56 | }; 57 | 58 | const handleMouseLeave = () => props.onLeave(); 59 | const handleMouseOver = ({ target }) => { 60 | const coordinates = getCoordinates(target); 61 | if (coordinates) { 62 | props.onHover(coordinates); 63 | } 64 | }; 65 | 66 | return ( 67 |
68 | 74 | 75 | 76 | 77 | {(item, colIndex) => } 78 | 79 | 80 | 81 | 82 | 83 | {(rowIndex) => ( 84 | 85 | 86 | {(item, colIndex) => ( 87 | 88 | )} 89 | 90 | 91 | )} 92 | 93 | 94 |
{item()}
{props.renderCell(item())}
95 | 96 | 103 | 104 |
105 | ); 106 | }; 107 | -------------------------------------------------------------------------------- /src/components/EnhancedTextarea.jsx: -------------------------------------------------------------------------------- 1 | import { createUniqueId, createEffect, mergeProps, Show } from "solid-js"; 2 | import markText from "~/lib/markText"; 3 | import styles from "./EnhancedTextarea.module.css"; 4 | 5 | /** 6 | * @param {Object} props 7 | * @param {string} props.initialValue 8 | * @param {Array>>=} props.highlights 9 | * @param {string=} props.invalid 10 | * @param {string} props.label 11 | * @param {string=} props.id 12 | * @param {string=} props.spellcheck 13 | * @param {string=} props.autofocus 14 | * @param {Function=} props.ref 15 | * @param {string=} props.title 16 | * @param {string=} props.containerClasses 17 | * @param {string=} props.ariaLabel 18 | * @param {string=} props.ariaKeyShortcuts 19 | * @returns {Element} 20 | */ 21 | export default (props) => { 22 | props = mergeProps( 23 | { highlights: [], ref() {}, containerClasses: "", id: createUniqueId() }, 24 | props, 25 | ); 26 | 27 | let textareaEl; 28 | createEffect(() => { 29 | if (textareaEl) { 30 | const message = props.invalid ?? ""; 31 | textareaEl.setCustomValidity(message); 32 | } 33 | }); 34 | 35 | const highlightedText = () => { 36 | // Hack: if the text ends with a new line character, we have to insert an 37 | // extra "\n" in order to have the same height as the textarea 38 | const rawText = props.value.replace(/\n$/, "\n\n"); 39 | 40 | if (props.highlights.length === 0) { 41 | return rawText; 42 | } 43 | 44 | const mainMarks = []; 45 | const subMarks = []; 46 | for (const positions of props.highlights) { 47 | mainMarks.push(positions[0]); 48 | subMarks.push(positions.slice(1)); 49 | } 50 | 51 | return markText(rawText, mainMarks, (mainText, rowIndex, boundary) => ( 52 | 53 | {markText( 54 | mainText, 55 | subMarks[rowIndex], 56 | (subText, colIndex) => ( 57 | {subText} 58 | ), 59 | boundary[0], 60 | )} 61 | 62 | )); 63 | }; 64 | 65 | return ( 66 |
67 | 68 | 71 | 72 |
73 | 74 | { 75 | // By default, Firefox will persist the value of the textarea across page loads. 76 | // With the feature, it is hard to keep the props.value the highlighted text and the textarea in sync. 77 | // This feature can be disabled with the "autocomplete" attribute. 78 | } 79 | 97 |
98 |
99 | ); 100 | }; 101 | -------------------------------------------------------------------------------- /src/context/app.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | createContext, 3 | useContext, 4 | createSignal, 5 | createMemo, 6 | createEffect, 7 | onCleanup, 8 | batch, 9 | on, 10 | } from "solid-js"; 11 | import { 12 | initialPattern, 13 | initialInput, 14 | largeInput, 15 | debounceTimeoutInMs, 16 | } from "~/settings"; 17 | import getRegExpFromString from "~/lib/getRegExpFromString"; 18 | 19 | /** 20 | * @typedef {Object} Matches 21 | * @property {Array>} texts 22 | * @property {Array>} indices 23 | */ 24 | 25 | /** 26 | * @typedef {Object} AppState 27 | * @property {string} patternString 28 | * @property {RegExp|SyntaxError} patternRegExp 29 | * @property {boolean} animatePattern 30 | * @property {string} inputString 31 | * @property {?Array} hoverPosition 32 | * @property {boolean} processing 33 | * @property {Matches} matches 34 | */ 35 | 36 | /** 37 | * @typedef {Object} Setters 38 | * @property {function(string)} setPattern 39 | * @property {function(boolean)} setAnimatePattern 40 | * @property {function(string)} setInput 41 | * @property {function(?Array=)} setHoverPosition 42 | */ 43 | 44 | const AppContext = createContext(); 45 | 46 | /** 47 | * @returns {import('solid-js').JSX.Element} 48 | */ 49 | export function AppProvider(props) { 50 | const [patternString, setPatternString] = createSignal(initialPattern); 51 | const patternRegExp = createMemo(() => { 52 | try { 53 | return getRegExpFromString(patternString()); 54 | } catch (e) { 55 | return e; 56 | } 57 | }); 58 | const [animatePattern, setAnimatePattern] = createSignal(false); 59 | 60 | const [inputString, setInputString] = createSignal(initialInput); 61 | 62 | const [hoverPosition, setHoverPosition] = createSignal(null); 63 | 64 | const [processing, setProcessing] = createSignal(false); 65 | 66 | /** @returns {Matches} */ 67 | const calculateMatches = () => { 68 | const texts = []; 69 | const indices = []; 70 | 71 | const regExp = patternRegExp(); 72 | const input = inputString(); 73 | const isValid = regExp instanceof RegExp; 74 | if (isValid) { 75 | // Set the "d" flag in order to read the indices of the matching groups 76 | const regExpWithIndices = regExp.hasIndices 77 | ? regExp 78 | : new RegExp(regExp, `${regExp.flags}d`); 79 | 80 | let matches; 81 | if (regExpWithIndices.global) { 82 | matches = input.matchAll(regExpWithIndices); 83 | } else { 84 | const match = input.match(regExpWithIndices); 85 | matches = match ? [match] : []; 86 | } 87 | 88 | for (let match of matches) { 89 | texts.push(Array.from(match)); 90 | indices.push(Array.from(match.indices)); 91 | } 92 | } 93 | 94 | return { texts, indices }; 95 | }; 96 | 97 | const updateMatches = () => { 98 | batch(() => { 99 | setMatches(calculateMatches()); 100 | setProcessing(false); 101 | }); 102 | }; 103 | 104 | const [matches, setMatches] = createSignal(calculateMatches()); 105 | 106 | let tick; 107 | createEffect( 108 | on( 109 | [patternRegExp, inputString], 110 | () => { 111 | clearTimeout(tick); 112 | 113 | if (inputString().length > largeInput) { 114 | setProcessing(true); 115 | tick = setTimeout(updateMatches, debounceTimeoutInMs); 116 | } else { 117 | updateMatches(); 118 | } 119 | 120 | onCleanup(() => clearTimeout(tick)); 121 | }, 122 | // Do not run the computation immediately. 123 | // The initial value of the matches signal has the correct values. 124 | { defer: true } 125 | ) 126 | ); 127 | 128 | const app = [ 129 | { 130 | patternString, 131 | patternRegExp, 132 | animatePattern, 133 | inputString, 134 | hoverPosition, 135 | processing, 136 | matches, 137 | }, 138 | { 139 | setPattern: setPatternString, 140 | setAnimatePattern, 141 | setInput: setInputString, 142 | setHoverPosition, 143 | }, 144 | ]; 145 | 146 | return ( 147 | {props.children} 148 | ); 149 | } 150 | 151 | /** 152 | * @returns {[AppState, Setters]} 153 | */ 154 | export function useAppState() { 155 | return useContext(AppContext); 156 | } 157 | -------------------------------------------------------------------------------- /src/routes/(home)/index.jsx: -------------------------------------------------------------------------------- 1 | import { Title, Style } from "@solidjs/meta"; 2 | import { A, useNavigate } from "@solidjs/router"; 3 | import { createMemo, createSignal, Show, Switch, Match } from "solid-js"; 4 | import EnhancedTextarea from "~/components/EnhancedTextarea"; 5 | import HighlightWhiteSpace from "~/components/HighlightWhiteSpace"; 6 | import Table from "~/components/Table"; 7 | import range from "~/lib/range"; 8 | import { useAppState } from "~/context/app"; 9 | import styles from "./index.module.css"; 10 | import createShortcut from "~/lib/createShortcut"; 11 | 12 | function Header(props) { 13 | return ( 14 |
15 |

JavaScript RegExp Tester

16 |
17 | ); 18 | } 19 | 20 | function Navigation() { 21 | return ( 22 | 34 | ); 35 | } 36 | 37 | /** 38 | * @param {Object} props 39 | * @param {Function} props.ref 40 | */ 41 | function Pattern(props) { 42 | const [state, { setPattern, setAnimatePattern }] = useAppState(); 43 | return ( 44 |
setAnimatePattern(false)} 47 | > 48 | 65 |
66 | ); 67 | } 68 | 69 | /** 70 | * @param {Object} props 71 | * @param {Function} props.ref 72 | */ 73 | function Input(props) { 74 | const [state, { setInput }] = useAppState(); 75 | const selector = createMemo(() => { 76 | if (!state.hoverPosition()) { 77 | return null; 78 | } 79 | 80 | const [row, col] = state.hoverPosition(); 81 | const isHeader = row === -1; 82 | const isFirstColumn = col === 0; 83 | 84 | if (isHeader && isFirstColumn) { 85 | return null; 86 | } 87 | 88 | let selector = `.${styles.input}`; 89 | 90 | if (!isHeader) { 91 | selector += ` [data-row="${row}"]`; 92 | } 93 | 94 | if (!isFirstColumn) { 95 | selector += ` [data-col="${col - 1}"]`; 96 | } 97 | 98 | return selector; 99 | }); 100 | 101 | const highlights = createMemo(() => { 102 | if (state.processing()) { 103 | return []; 104 | } 105 | return state.matches().indices; 106 | }); 107 | 108 | return ( 109 | <> 110 | 119 |
120 | 133 |
134 | 135 | ); 136 | } 137 | 138 | function Matches() { 139 | const [state, { setHoverPosition }] = useAppState(); 140 | const [showAll, setShowAll] = createSignal(false); 141 | return ( 142 | 148 | 149 | 150 |

Processing...

151 |
152 | 153 |

No match

154 |
155 | 156 | `$${i}`), 160 | ]} 161 | data={state.matches().texts} 162 | rowLimit={showAll() ? Infinity : 10} 163 | onShowAllRequest={() => setShowAll(true)} 164 | renderCell={(cell) => ( 165 | {cell} 166 | )} 167 | onHover={setHoverPosition} 168 | onLeave={setHoverPosition} 169 | /> 170 | 171 | 172 | 173 | ); 174 | } 175 | 176 | export default () => { 177 | const navigate = useNavigate(); 178 | createShortcut({ key: "m", ctrlKey: true }, (e) => { 179 | e.preventDefault(); 180 | navigate("/cheat-sheet"); 181 | }); 182 | 183 | let patternEl; 184 | createShortcut({ key: "p", ctrlKey: true }, (e) => { 185 | e.preventDefault(); 186 | patternEl?.focus(); 187 | }); 188 | 189 | let inputEl; 190 | createShortcut({ key: "i", ctrlKey: true }, (e) => { 191 | e.preventDefault(); 192 | inputEl?.focus(); 193 | }); 194 | 195 | const [state, { setAnimatePattern }] = useAppState(); 196 | createShortcut({ key: "s", ctrlKey: true }, (e) => { 197 | e.preventDefault(); 198 | const text = state.patternRegExp().toString(); 199 | navigator.clipboard.writeText(text).then(() => { 200 | setAnimatePattern(true); 201 | }); 202 | }); 203 | 204 | return ( 205 |
206 | Home Page - Regexify 207 | 208 |
209 | 210 |
211 | 212 | 213 | 214 |
215 |
216 | ); 217 | }; 218 | -------------------------------------------------------------------------------- /src/routes/cheat-sheet/index.jsx: -------------------------------------------------------------------------------- 1 | import { Title } from "@solidjs/meta"; 2 | import { A, useNavigate } from "@solidjs/router"; 3 | import styles from "./index.module.css"; 4 | import createShortcut from "~/lib/createShortcut"; 5 | 6 | function Navigation() { 7 | return ( 8 | 20 | ); 21 | } 22 | 23 | function Content() { 24 | return ( 25 | <> 26 |

27 | 32 | Flags 33 | 34 |

35 |
36 |
37 | d 38 |
39 |
Generate indices for substring matches.
40 |
41 | g 42 |
43 | 44 |
Global search.
45 |
46 | i 47 |
48 |
Case-insensitive search.
49 | 50 |
51 | m 52 |
53 |
54 | Allows ^ and $ to match newline characters. 55 |
56 | 57 |
58 | s 59 |
60 |
61 | Allows . to match newline characters. 62 |
63 | 64 |
65 | u 66 |
67 |
68 | "Unicode"; treat a pattern as a sequence of Unicode code points. 69 |
70 | 71 |
72 | y 73 |
74 |
75 | Perform a "sticky" search that matches starting at the current 76 | position in the target string. 77 |
78 |
79 | 80 |

81 | 86 | Groups and backreferences 87 | 88 |

89 |
90 |
91 | (x) 92 |
93 |
Capturing group
94 | 95 |
96 | (?<Name>x) 97 |
98 |
Named capturing group
99 | 100 |
101 | (?:x) 102 |
103 |
Non-capturing group
104 | 105 |
106 | \N 107 |
108 |
109 | Where "N" is a positive integer. A back reference to the last 110 | substring matching the N parenthetical in the regular expression 111 | (counting left parentheses). 112 |
113 | 114 |
115 | \k<Name> 116 |
117 |
118 | A back reference to the last substring matching the named capture 119 | group specified by <Name>. 120 |
121 |
122 | 123 |

124 | 129 | Character classes 130 | 131 |

132 |
133 |
134 | [xyz] 135 |
136 | [a-c] 137 |
138 |
A character class.
139 | 140 |
141 | [^xyz] 142 |
143 | [^a-c] 144 |
145 |
A negated or complemented character class.
146 | 147 |
148 | . 149 |
150 |
151 | Matches any single character except line terminators:{" "} 152 | \n, \r, \u2028 or{" "} 153 | \u2029. 154 |
155 | Inside a character class, the dot loses its special meaning and 156 | matches a literal dot. 157 |
158 | Note that the m multiline flag doesn't change the dot behavior. So to 159 | match a pattern across multiple lines, the character class{" "} 160 | [^] can be used — it will match any character including 161 | newlines. 162 |
163 | The s "dotAll" flag allows the dot to also match line 164 | terminators. 165 |
166 | 167 |
168 | \d 169 |
170 |
171 | Matches any digit. Equivalent to [0-9]. 172 |
173 | 174 |
175 | \D 176 |
177 |
178 | Matches any character that is not a digit. Equivalent to{" "} 179 | [^0-9]. 180 |
181 | 182 |
183 | \w 184 |
185 |
186 | Matches any alphanumeric character from the basic Latin alphabet, 187 | including the underscore. Equivalent to [A-Za-z0-9_]. 188 |
189 | 190 |
191 | \W 192 |
193 |
194 | Matches any character that is not a word character from the basic 195 | Latin alphabet. Equivalent to [^A-Za-z0-9_]. 196 |
197 | 198 |
199 | \s 200 |
201 |
202 | Matches a single white space character, including space, tab, form 203 | feed, line feed, and other Unicode spaces. 204 |
205 | 206 |
207 | \S 208 |
209 |
Matches a single character other than white space.
210 | 211 |
212 | \t 213 |
214 |
Matches a horizontal tab.
215 | 216 |
217 | \r 218 |
219 |
Matches a carriage return.
220 | 221 |
222 | \n 223 |
224 |
Matches a linefeed.
225 | 226 |
227 | \v 228 |
229 |
Matches a vertical tab.
230 | 231 |
232 | \f 233 |
234 |
Matches a form-feed.
235 | 236 |
237 | [\b] 238 |
239 |
Matches a backspace.
240 | 241 |
242 | \0 243 |
244 |
Matches a NUL character. Do not follow this with another digit.
245 | 246 |
247 | \cX 248 |
249 |
250 | Matches a control character using caret notation, where "X" is a 251 | letter from A–Z (corresponding to code points U+0001– 252 | U+001A). 253 |
254 | 255 |
256 | \xhh 257 |
258 |
259 | Matches the character with the code hh (two hexadecimal digits). 260 |
261 | 262 |
263 | \uhhhh 264 |
265 |
266 | Matches a UTF-16 code-unit with the value hhhh (four hexadecimal 267 | digits). 268 |
269 | 270 |
271 | \u{hhhh} or 272 |
273 | \u{hhhhh} 274 |
275 |
276 | (Only when the u flag is set.) Matches the character with 277 | the Unicode value U+hhhh or U+hhhhh{" "} 278 | (hexadecimal digits). 279 |
280 | 281 |
282 | \p{UnicodeProperty}, 283 |
284 | \P{UnicodeProperty} 285 |
286 |
287 | Matches a character based on its{" "} 288 | 293 | Unicode character properties. 294 | 295 |
296 |
297 | 298 |

299 | 304 | Assertions 305 | 306 |

307 |
308 |
309 | ^ 310 |
311 |
312 | Matches the beginning of input. If the multiline flag is set to true, 313 | also matches immediately after a line break character. 314 |
315 | 316 |
317 | $ 318 |
319 |
320 | Matches the end of input. If the multiline flag is set to true, also 321 | matches immediately before a line break character. 322 |
323 | 324 |
325 | \b 326 |
327 |
328 | Matches a word boundary. This is the position where a word character 329 | is not followed or preceded by another word-character, such as between 330 | a letter and a space. Note that a matched word boundary is not 331 | included in the match. 332 |
333 | 334 |
335 | \B 336 |
337 |
Matches a non-word boundary.
338 | 339 |
340 | x(?=y) 341 |
342 |
343 | Lookahead assertion: Matches "x" only if "x" is followed by "y". 344 |
345 | 346 |
347 | x(?!y) 348 |
349 |
350 | Negative lookahead assertion: Matches "x" only if "x" is not followed 351 | by "y". 352 |
353 | 354 |
355 | (?<=y)x 356 |
357 |
358 | Lookbehind assertion: Matches "x" only if "x" is preceded by "y". 359 |
360 | 361 |
362 | (?<!y)x 363 |
364 |
365 | Negative lookbehind assertion: Matches "x" only if "x" is not preceded 366 | by "y". 367 |
368 |
369 | 370 |

371 | 376 | Quantifiers 377 | 378 |

379 |
380 |
381 | x* 382 |
383 |
Matches the preceding item "x" 0 or more times.
384 | 385 |
386 | x+ 387 |
388 |
Matches the preceding item "x" 1 or more times.
389 | 390 |
391 | x? 392 |
393 |
Matches the preceding item "x" 0 or 1 times.
394 | 395 |
396 | x{n} 397 |
398 |
399 | Where "n" is a positive integer, matches exactly "n" occurrences of 400 | the preceding item "x". 401 |
402 | 403 |
404 | x{n,} 405 |
406 |
407 | Where "n" is a positive integer, matches at least "n" occurrences of 408 | the preceding item "x". 409 |
410 | 411 |
412 | x{n,m} 413 |
414 |
415 | Where "n" is 0 or a positive integer, "m" is a positive integer, and{" "} 416 | m > n, matches at least "n" and at most "m" 417 | occurrences of the preceding item "x".{" "} 418 |
419 | 420 |
421 | x*? 422 |
423 | x+? 424 |
425 | x?? 426 |
427 | x{n}? 428 |
429 | x{n,}? 430 |
431 | x{n,m}? 432 |
433 |
434 | The ? character after the quantifier makes the quantifier 435 | "non-greedy": meaning that it will stop as soon as it finds a match. 436 |
437 |
438 |

439 | Source:{" "} 440 | 445 | MDN Web Docs 446 | 447 |

448 | 449 | ); 450 | } 451 | 452 | export default () => { 453 | const navigate = useNavigate(); 454 | createShortcut({ key: "m", ctrlKey: true }, (e) => { 455 | e.preventDefault(); 456 | navigate("/"); 457 | }); 458 | 459 | return ( 460 |
461 | Cheat Sheet - Regexify 462 | 463 | 464 |
465 | 466 |
467 |
468 | ); 469 | }; 470 | --------------------------------------------------------------------------------