From f5c4ab19bc10ea468a453a1460b1de2d7ccab87b Mon Sep 17 00:00:00 2001 From: sharevb Date: Fri, 20 Sep 2024 20:39:40 +0200 Subject: [PATCH] feat(new tool): Regex Tester (and Cheatsheet) (#1030) * feat(new tool): Regex Tester Fix https://github.com/CorentinTh/it-tools/issues/1007, https://github.com/CorentinTh/it-tools/issues/991, https://github.com/CorentinTh/it-tools/issues/936, https://github.com/CorentinTh/it-tools/issues/761, https://github.com/CorentinTh/it-tools/issues/649 https://github.com/CorentinTh/it-tools/issues/644, https://github.com/CorentinTh/it-tools/issues/554 https://github.com/CorentinTh/it-tools/issues/308 * fix: refactor to service + add regex diagram + ui enhancements * fix: update queryParams * fix: deps * fix: svg style bug in @regexper/render @regexper/render use a stylesheet in svg that cause bugs in whole site. So add regexper in a shadow root * feat(new tool): added Regex Cheatsheet * Update src/tools/regex-memo/index.ts * Update src/tools/regex-tester/index.ts --------- Co-authored-by: Corentin THOMASSET --- components.d.ts | 15 +- package.json | 5 +- pnpm-lock.yaml | 46 +++++ src/composable/queryParams.ts | 33 ++- src/composable/validation.ts | 19 +- src/main.ts | 2 + src/tools/index.ts | 4 + src/tools/regex-memo/index.ts | 12 ++ src/tools/regex-memo/regex-memo.content.md | 121 +++++++++++ src/tools/regex-memo/regex-memo.vue | 32 +++ src/tools/regex-tester/index.ts | 12 ++ .../regex-tester/regex-tester.service.test.ts | 106 ++++++++++ .../regex-tester/regex-tester.service.ts | 61 ++++++ src/tools/regex-tester/regex-tester.vue | 193 ++++++++++++++++++ 14 files changed, 650 insertions(+), 11 deletions(-) create mode 100644 src/tools/regex-memo/index.ts create mode 100644 src/tools/regex-memo/regex-memo.content.md create mode 100644 src/tools/regex-memo/regex-memo.vue create mode 100644 src/tools/regex-tester/index.ts create mode 100644 src/tools/regex-tester/regex-tester.service.test.ts create mode 100644 src/tools/regex-tester/regex-tester.service.ts create mode 100644 src/tools/regex-tester/regex-tester.vue diff --git a/components.d.ts b/components.d.ts index 8e59366ab..3e65c3cc5 100644 --- a/components.d.ts +++ b/components.d.ts @@ -130,23 +130,19 @@ declare module '@vue/runtime-core' { MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default'] MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default'] NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] - NButton: typeof import('naive-ui')['NButton'] - NCode: typeof import('naive-ui')['NCode'] + NCheckbox: typeof import('naive-ui')['NCheckbox'] NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] NConfigProvider: typeof import('naive-ui')['NConfigProvider'] + NDivider: typeof import('naive-ui')['NDivider'] NEllipsis: typeof import('naive-ui')['NEllipsis'] - NForm: typeof import('naive-ui')['NForm'] - NFormItem: typeof import('naive-ui')['NFormItem'] NH1: typeof import('naive-ui')['NH1'] NH3: typeof import('naive-ui')['NH3'] NIcon: typeof import('naive-ui')['NIcon'] - NInputNumber: typeof import('naive-ui')['NInputNumber'] NLayout: typeof import('naive-ui')['NLayout'] NLayoutSider: typeof import('naive-ui')['NLayoutSider'] NMenu: typeof import('naive-ui')['NMenu'] - NScrollbar: typeof import('naive-ui')['NScrollbar'] - NSlider: typeof import('naive-ui')['NSlider'] - NSwitch: typeof import('naive-ui')['NSwitch'] + NSpace: typeof import('naive-ui')['NSpace'] + NTable: typeof import('naive-ui')['NTable'] NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] @@ -156,6 +152,9 @@ declare module '@vue/runtime-core' { PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default'] QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default'] RandomPortGenerator: typeof import('./src/tools/random-port-generator/random-port-generator.vue')['default'] + RegexMemo: typeof import('./src/tools/regex-memo/regex-memo.vue')['default'] + 'RegexMemo.content': typeof import('./src/tools/regex-memo/regex-memo.content.md')['default'] + RegexTester: typeof import('./src/tools/regex-tester/regex-tester.vue')['default'] ResultRow: typeof import('./src/tools/ipv4-range-expander/result-row.vue')['default'] RomanNumeralConverter: typeof import('./src/tools/roman-numeral-converter/roman-numeral-converter.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] diff --git a/package.json b/package.json index 63e5856a5..f5dedc0c6 100644 --- a/package.json +++ b/package.json @@ -37,12 +37,13 @@ "dependencies": { "@it-tools/bip39": "^0.0.4", "@it-tools/oggen": "^1.3.0", + "@regexper/render": "^1.0.0", "@sindresorhus/slugify": "^2.2.1", "@tiptap/pm": "2.1.6", "@tiptap/starter-kit": "2.1.6", "@tiptap/vue-3": "2.0.3", - "@types/markdown-it": "^13.0.7", "@types/figlet": "^1.5.8", + "@types/markdown-it": "^13.0.7", "@vicons/material": "^0.12.0", "@vicons/tabler": "^0.12.0", "@vueuse/core": "^10.3.0", @@ -84,6 +85,7 @@ "pinia": "^2.0.34", "plausible-tracker": "^0.3.8", "qrcode": "^1.5.1", + "randexp": "^0.5.3", "sql-formatter": "^13.0.0", "ua-parser-js": "^1.0.35", "ulid": "^2.3.0", @@ -93,6 +95,7 @@ "vue": "^3.3.4", "vue-i18n": "^9.9.1", "vue-router": "^4.1.6", + "vue-shadow-dom": "^4.2.0", "vue-tsc": "^1.8.1", "xml-formatter": "^3.3.2", "xml-js": "^1.6.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2311f3aff..e43a3217e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ dependencies: '@it-tools/oggen': specifier: ^1.3.0 version: 1.3.0 + '@regexper/render': + specifier: ^1.0.0 + version: 1.0.0 '@sindresorhus/slugify': specifier: ^2.2.1 version: 2.2.1 @@ -152,6 +155,9 @@ dependencies: qrcode: specifier: ^1.5.1 version: 1.5.1 + randexp: + specifier: ^0.5.3 + version: 0.5.3 sql-formatter: specifier: ^13.0.0 version: 13.0.0 @@ -179,6 +185,9 @@ dependencies: vue-router: specifier: ^4.1.6 version: 4.1.6(vue@3.3.4) + vue-shadow-dom: + specifier: ^4.2.0 + version: 4.2.0 vue-tsc: specifier: ^1.8.1 version: 1.8.1(typescript@5.2.2) @@ -2480,6 +2489,17 @@ packages: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: false + /@regexper/parser@1.0.0: + resolution: {integrity: sha512-S8AWIGpCNdl9PNHdbhI6TpXZsPk6FDU/RTZI+6UFF4rVFqDQKjCIbZSgFu7NihoEZKq57wKFPbbT1EzrjVvPHA==} + dev: false + + /@regexper/render@1.0.0: + resolution: {integrity: sha512-xYm9RUgnhhZotTtf8UZpK1PG2CcTRXQ3JPwfTlYUZsy2J+UcTVc7BaO/MJadpMoVuT8jrIyptH4Y0HLzqhI3hQ==} + dependencies: + '@regexper/parser': 1.0.0 + '@svgdotjs/svg.js': 3.2.4 + dev: false + /@remirror/core-constants@2.0.1: resolution: {integrity: sha512-ZR4aihtnnT9lMbhh5DEbsriJRlukRXmLZe7HmM+6ufJNNUDoazc75UX26xbgQlNUqgAqMcUdGFAnPc1JwgAdLQ==} dependencies: @@ -2629,6 +2649,10 @@ packages: string.prototype.matchall: 4.0.10 dev: true + /@svgdotjs/svg.js@3.2.4: + resolution: {integrity: sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==} + dev: false + /@tiptap/core@2.1.12(@tiptap/pm@2.1.6): resolution: {integrity: sha512-ZGc3xrBJA9KY8kln5AYTj8y+GDrKxi7u95xIl2eccrqTY5CQeRu6HRNM1yT4mAjuSaG9jmazyjGRlQuhyxCKxQ==} peerDependencies: @@ -4962,6 +4986,11 @@ packages: tslib: 2.5.0 dev: false + /drange@1.1.1: + resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==} + engines: {node: '>=4'} + dev: false + /duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} dev: true @@ -7758,6 +7787,14 @@ packages: ret: 0.1.15 dev: false + /randexp@0.5.3: + resolution: {integrity: sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==} + engines: {node: '>=4'} + dependencies: + drange: 1.1.1 + ret: 0.2.2 + dev: false + /randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: @@ -7928,6 +7965,11 @@ packages: engines: {node: '>=0.12'} dev: false + /ret@0.2.2: + resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==} + engines: {node: '>=4'} + dev: false + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -9268,6 +9310,10 @@ packages: vue: 3.3.4 dev: false + /vue-shadow-dom@4.2.0: + resolution: {integrity: sha512-lguI064rT2HT/dxqSmXtz860KOvCq+W3nU1jMqroTmX3K1H46q22BMR4emh/Ld3ozy35XJKOaNGcr6mkJ/t/yg==} + dev: false + /vue-template-compiler@2.7.14: resolution: {integrity: sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==} dependencies: diff --git a/src/composable/queryParams.ts b/src/composable/queryParams.ts index 9699abbc1..7cc8cc0dd 100644 --- a/src/composable/queryParams.ts +++ b/src/composable/queryParams.ts @@ -1,7 +1,8 @@ import { useRouteQuery } from '@vueuse/router'; import { computed } from 'vue'; +import { useStorage } from '@vueuse/core'; -export { useQueryParam }; +export { useQueryParam, useQueryParamOrStorage }; const transformers = { number: { @@ -16,6 +17,12 @@ const transformers = { fromQuery: (value: string) => value.toLowerCase() === 'true', toQuery: (value: boolean) => (value ? 'true' : 'false'), }, + object: { + fromQuery: (value: string) => { + return JSON.parse(value); + }, + toQuery: (value: object) => JSON.stringify(value), + }, }; function useQueryParam({ name, defaultValue }: { name: string; defaultValue: T }) { @@ -33,3 +40,27 @@ function useQueryParam({ name, defaultValue }: { name: string; defaultValue: }, }); } + +function useQueryParamOrStorage({ name, storageName, defaultValue }: { name: string; storageName: string; defaultValue: T }) { + const type = typeof defaultValue; + const transformer = transformers[type as keyof typeof transformers] ?? transformers.string; + + const storageRef = useStorage(storageName, defaultValue); + const proxyDefaultValue = transformer.toQuery(defaultValue as never); + const proxy = useRouteQuery(name, proxyDefaultValue); + + const r = ref(defaultValue); + + watch(r, + (value) => { + proxy.value = transformer.toQuery(value as never); + storageRef.value = value as never; + }, + { deep: true }); + + r.value = (proxy.value && proxy.value !== proxyDefaultValue + ? transformer.fromQuery(proxy.value) as unknown as T + : storageRef.value as T) as never; + + return r; +} diff --git a/src/composable/validation.ts b/src/composable/validation.ts index 472ca4b21..33a6a7b2e 100644 --- a/src/composable/validation.ts +++ b/src/composable/validation.ts @@ -3,9 +3,11 @@ import _ from 'lodash'; import { type Ref, reactive, watch } from 'vue'; type ValidatorReturnType = unknown; +type GetErrorMessageReturnType = string; export interface UseValidationRule { validator: (value: T) => ValidatorReturnType + getErrorMessage?: (value: T) => GetErrorMessageReturnType message: string } @@ -24,6 +26,15 @@ export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean { } } +export function getErrorMessageOrThrown(cb: () => GetErrorMessageReturnType): string { + try { + return cb() || ''; + } + catch (e: any) { + return e.toString(); + } +} + export interface ValidationAttrs { feedback: string validationStatus: string | undefined @@ -61,7 +72,13 @@ export function useValidation({ for (const rule of get(rules)) { if (isFalsyOrHasThrown(() => rule.validator(source.value))) { - state.message = rule.message; + if (rule.getErrorMessage) { + const getErrorMessage = rule.getErrorMessage; + state.message = rule.message.replace('{0}', getErrorMessageOrThrown(() => getErrorMessage(source.value))); + } + else { + state.message = rule.message; + } state.status = 'error'; } } diff --git a/src/main.ts b/src/main.ts index 36ba3b7f8..19d28bf2e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import { createPinia } from 'pinia'; import { createHead } from '@vueuse/head'; import { registerSW } from 'virtual:pwa-register'; +import shadow from 'vue-shadow-dom'; import { plausible } from './plugins/plausible.plugin'; import 'virtual:uno.css'; @@ -23,5 +24,6 @@ app.use(i18nPlugin); app.use(router); app.use(naive); app.use(plausible); +app.use(shadow); app.mount('#app'); diff --git a/src/tools/index.ts b/src/tools/index.ts index c9003fe81..388cfaf49 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -9,6 +9,8 @@ import { tool as textToUnicode } from './text-to-unicode'; import { tool as safelinkDecoder } from './safelink-decoder'; import { tool as xmlToJson } from './xml-to-json'; import { tool as jsonToXml } from './json-to-xml'; +import { tool as regexTester } from './regex-tester'; +import { tool as regexMemo } from './regex-memo'; import { tool as markdownToHtml } from './markdown-to-html'; import { tool as pdfSignatureChecker } from './pdf-signature-checker'; import { tool as numeronymGenerator } from './numeronym-generator'; @@ -156,6 +158,8 @@ export const toolsByCategory: ToolCategory[] = [ xmlFormatter, yamlViewer, emailNormalizer, + regexTester, + regexMemo, ], }, { diff --git a/src/tools/regex-memo/index.ts b/src/tools/regex-memo/index.ts new file mode 100644 index 000000000..f1f56489d --- /dev/null +++ b/src/tools/regex-memo/index.ts @@ -0,0 +1,12 @@ +import { BrandJavascript } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'Regex cheatsheet', + path: '/regex-memo', + description: 'Javascript Regex/Regular Expression cheatsheet', + keywords: ['regex', 'regular', 'expression', 'javascript', 'memo', 'cheatsheet'], + component: () => import('./regex-memo.vue'), + icon: BrandJavascript, + createdAt: new Date('2024-09-20'), +}); diff --git a/src/tools/regex-memo/regex-memo.content.md b/src/tools/regex-memo/regex-memo.content.md new file mode 100644 index 000000000..0f779401b --- /dev/null +++ b/src/tools/regex-memo/regex-memo.content.md @@ -0,0 +1,121 @@ +### Normal characters + +Expression | Description +:--|:-- +`.` or `[^\n\r]` | any character *excluding* a newline or carriage return +`[A-Za-z]` | alphabet +`[a-z]` | lowercase alphabet +`[A-Z]` | uppercase alphabet +`\d` or `[0-9]` | digit +`\D` or `[^0-9]` | non-digit +`_` | underscore +`\w` or `[A-Za-z0-9_]` | alphabet, digit or underscore +`\W` or `[^A-Za-z0-9_]` | inverse of `\w` +`\S` | inverse of `\s` + +### Whitespace characters + +Expression | Description +:--|:-- +` ` | space +`\t` | tab +`\n` | newline +`\r` | carriage return +`\s` | space, tab, newline or carriage return + +### Character set + +Expression | Description +:--|:-- +`[xyz]` | either `x`, `y` or `z` +`[^xyz]` | neither `x`, `y` nor `z` +`[1-3]` | either `1`, `2` or `3` +`[^1-3]` | neither `1`, `2` nor `3` + +- Think of a character set as an `OR` operation on the single characters that are enclosed between the square brackets. +- Use `^` after the opening `[` to “negate” the character set. +- Within a character set, `.` means a literal period. + +### Characters that require escaping + +#### Outside a character set + +Expression | Description +:--|:-- +`\.` | period +`\^` | caret +`\$` | dollar sign +`\|` | pipe +`\\` | back slash +`\/` | forward slash +`\(` | opening bracket +`\)` | closing bracket +`\[` | opening square bracket +`\]` | closing square bracket +`\{` | opening curly bracket +`\}` | closing curly bracket + +#### Inside a character set + +Expression | Description +:--|:-- +`\\` | back slash +`\]` | closing square bracket + +- A `^` must be escaped only if it occurs immediately after the opening `[` of the character set. +- A `-` must be escaped only if it occurs between two alphabets or two digits. + +### Quantifiers + +Expression | Description +:--|:-- +`{2}` | exactly 2 +`{2,}` | at least 2 +`{2,7}` | at least 2 but no more than 7 +`*` | 0 or more +`+` | 1 or more +`?` | exactly 0 or 1 + +- The quantifier goes *after* the expression to be quantified. + +### Boundaries + +Expression | Description +:--|:-- +`^` | start of string +`$` | end of string +`\b` | word boundary + +- How word boundary matching works: + - At the beginning of the string if the first character is `\w`. + - Between two adjacent characters within the string, if the first character is `\w` and the second character is `\W`. + - At the end of the string if the last character is `\w`. + +### Matching + +Expression | Description +:--|:-- +`foo\|bar` | match either `foo` or `bar` +`foo(?=bar)` | match `foo` if it’s before `bar` +`foo(?!bar)` | match `foo` if it’s *not* before `bar` +`(?<=bar)foo` | match `foo` if it’s after `bar` +`(? +import { useThemeVars } from 'naive-ui'; +import Memo from './regex-memo.content.md'; + +const themeVars = useThemeVars(); + + + + + diff --git a/src/tools/regex-tester/index.ts b/src/tools/regex-tester/index.ts new file mode 100644 index 000000000..9cc542f0e --- /dev/null +++ b/src/tools/regex-tester/index.ts @@ -0,0 +1,12 @@ +import { Language } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'Regex Tester', + path: '/regex-tester', + description: 'Regex Tester', + keywords: ['regex', 'tester', 'sample', 'expression'], + component: () => import('./regex-tester.vue'), + icon: Language, + createdAt: new Date('2024-09-20'), +}); diff --git a/src/tools/regex-tester/regex-tester.service.test.ts b/src/tools/regex-tester/regex-tester.service.test.ts new file mode 100644 index 000000000..bd4efbbc8 --- /dev/null +++ b/src/tools/regex-tester/regex-tester.service.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, it } from 'vitest'; +import { matchRegex } from './regex-tester.service'; + +const regexesData = [ + { + regex: '', + text: '', + flags: '', + result: [], + }, + { + regex: '.*', + text: '', + flags: '', + result: [], + }, + { + regex: '', + text: 'aaa', + flags: '', + result: [], + }, + { + regex: 'a', + text: 'baaa', + flags: '', + result: [ + { + captures: [], + groups: [], + index: 1, + value: 'a', + }, + ], + }, + { + regex: '(.)(?r)', + text: 'azertyr', + flags: 'g', + result: [ + { + captures: [ + { + end: 3, + name: '1', + start: 2, + value: 'e', + }, + { + end: 4, + name: '2', + start: 3, + value: 'r', + }, + ], + groups: [ + { + end: 4, + name: 'g', + start: 3, + value: 'r', + }, + ], + index: 2, + value: 'er', + }, + { + captures: [ + { + end: 6, + name: '1', + start: 5, + value: 'y', + }, + { + end: 7, + name: '2', + start: 6, + value: 'r', + }, + ], + groups: [ + { + end: 7, + name: 'g', + start: 6, + value: 'r', + }, + ], + index: 5, + value: 'yr', + }, + ], + }, +]; + +describe('regex-tester', () => { + for (const reg of regexesData) { + const { regex, text, flags, result: expected_result } = reg; + it(`Should matchRegex("${regex}","${text}","${flags}") return correct result`, async () => { + const result = matchRegex(regex, text, `${flags}d`); + + expect(result).to.deep.equal(expected_result); + }); + } +}); diff --git a/src/tools/regex-tester/regex-tester.service.ts b/src/tools/regex-tester/regex-tester.service.ts new file mode 100644 index 000000000..ec8682c5c --- /dev/null +++ b/src/tools/regex-tester/regex-tester.service.ts @@ -0,0 +1,61 @@ +interface RegExpGroupIndices { + [name: string]: [number, number] +} +interface RegExpIndices extends Array<[number, number]> { + groups: RegExpGroupIndices +} +interface RegExpExecArrayWithIndices extends RegExpExecArray { + indices: RegExpIndices +} +interface GroupCapture { + name: string + value: string + start: number + end: number +}; + +export function matchRegex(regex: string, text: string, flags: string) { + // if (regex === '' || text === '') { + // return []; + // } + + let lastIndex = -1; + const re = new RegExp(regex, flags); + const results = []; + let match = re.exec(text) as RegExpExecArrayWithIndices; + while (match !== null) { + if (re.lastIndex === lastIndex || match[0] === '') { + break; + } + const indices = match.indices; + const captures: Array = []; + Object.entries(match).forEach(([captureName, captureValue]) => { + if (captureName !== '0' && captureName.match(/\d+/)) { + captures.push({ + name: captureName, + value: captureValue, + start: indices[Number(captureName)][0], + end: indices[Number(captureName)][1], + }); + } + }); + const groups: Array = []; + Object.entries(match.groups || {}).forEach(([groupName, groupValue]) => { + groups.push({ + name: groupName, + value: groupValue, + start: indices.groups[groupName][0], + end: indices.groups[groupName][1], + }); + }); + results.push({ + index: match.index, + value: match[0], + captures, + groups, + }); + lastIndex = re.lastIndex; + match = re.exec(text) as RegExpExecArrayWithIndices; + } + return results; +} diff --git a/src/tools/regex-tester/regex-tester.vue b/src/tools/regex-tester/regex-tester.vue new file mode 100644 index 000000000..a1fa79584 --- /dev/null +++ b/src/tools/regex-tester/regex-tester.vue @@ -0,0 +1,193 @@ + + +