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 <corentin.thomasset74@gmail.com>
This commit is contained in:
sharevb 2024-09-20 20:39:40 +02:00 committed by GitHub
parent 67094980c9
commit f5c4ab19bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 650 additions and 11 deletions

View file

@ -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'),
});

View file

@ -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: '(.)(?<g>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);
});
}
});

View file

@ -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<GroupCapture> = [];
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<GroupCapture> = [];
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;
}

View file

@ -0,0 +1,193 @@
<script setup lang="ts">
import RandExp from 'randexp';
import { render } from '@regexper/render';
import type { ShadowRootExpose } from 'vue-shadow-dom';
import { matchRegex } from './regex-tester.service';
import { useValidation } from '@/composable/validation';
import { useQueryParamOrStorage } from '@/composable/queryParams';
const regex = useQueryParamOrStorage({ name: 'regex', storageName: 'regex-tester:regex', defaultValue: '' });
const text = ref('');
const global = ref(true);
const ignoreCase = ref(false);
const multiline = ref(false);
const dotAll = ref(true);
const unicode = ref(true);
const unicodeSets = ref(false);
const visualizerSVG = ref<ShadowRootExpose>();
const regexValidation = useValidation({
source: regex,
rules: [
{
message: 'Invalid regex: {0}',
validator: value => new RegExp(value),
getErrorMessage: (value) => {
const _ = new RegExp(value);
return '';
},
},
],
});
const results = computed(() => {
let flags = 'd';
if (global.value) {
flags += 'g';
}
if (ignoreCase.value) {
flags += 'i';
}
if (multiline.value) {
flags += 'm';
}
if (dotAll.value) {
flags += 's';
}
if (unicode.value) {
flags += 'u';
}
else if (unicodeSets.value) {
flags += 'v';
}
try {
return matchRegex(regex.value, text.value, flags);
}
catch (_) {
return [];
}
});
const sample = computed(() => {
try {
const randexp = new RandExp(new RegExp(regex.value.replace(/\(\?\<[^\>]*\>/g, '(?:')));
return randexp.gen();
}
catch (_) {
return '';
}
});
watchEffect(
async () => {
const regexValue = regex.value;
// shadow root is required:
// @regexper/render append a <defs><style> that broke svg transparency of icons in the whole site
const visualizer = visualizerSVG.value?.shadow_root;
if (visualizer) {
while (visualizer.lastChild) {
visualizer.removeChild(visualizer.lastChild);
}
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
try {
await render(regexValue, svg);
}
catch (_) {
}
visualizer.appendChild(svg);
}
},
);
</script>
<template>
<div max-w-600px>
<c-card title="Regex" mb-1>
<c-input-text
v-model:value="regex"
label="Regex to test:"
placeholder="Put the regex to test"
multiline
rows="3"
:validation="regexValidation"
/>
<router-link target="_blank" to="/regex-memo" mb-1 mt-1>
See Regular Expression Cheatsheet
</router-link>
<n-space>
<n-checkbox v-model:checked="global">
<span title="Global search">Global search. (<code>g</code>)</span>
</n-checkbox>
<n-checkbox v-model:checked="ignoreCase">
<span title="Case-insensitive search">Case-insensitive search. (<code>i</code>)</span>
</n-checkbox>
<n-checkbox v-model:checked="multiline">
<span title="Allows ^ and $ to match next to newline characters.">Multiline(<code>m</code>)</span>
</n-checkbox>
<n-checkbox v-model:checked="dotAll">
<span title="Allows . to match newline characters.">Singleline(<code>s</code>)</span>
</n-checkbox>
<n-checkbox v-model:checked="unicode">
<span title="Unicode; treat a pattern as a sequence of Unicode code points.">Unicode(<code>u</code>)</span>
</n-checkbox>
<n-checkbox v-model:checked="unicodeSets">
<span title="An upgrade to the u mode with more Unicode features.">Unicode Sets (<code>v</code>)</span>
</n-checkbox>
</n-space>
<n-divider />
<c-input-text
v-model:value="text"
label="Text to match:"
placeholder="Put the text to match"
multiline
rows="5"
/>
</c-card>
<c-card title="Matches" mb-1 mt-3>
<n-table v-if="results?.length > 0">
<thead>
<tr>
<th scope="col">
Index in text
</th>
<th scope="col">
Value
</th>
<th scope="col">
Captures
</th>
<th scope="col">
Groups
</th>
</tr>
</thead>
<tbody>
<tr v-for="match of results" :key="match.index">
<td>{{ match.index }}</td>
<td>{{ match.value }}</td>
<td>
<ul>
<li v-for="capture in match.captures" :key="capture.name">
"{{ capture.name }}" = {{ capture.value }} [{{ capture.start }} - {{ capture.end }}]
</li>
</ul>
</td>
<td>
<ul>
<li v-for="group in match.groups" :key="group.name">
"{{ group.name }}" = {{ group.value }} [{{ group.start }} - {{ group.end }}]
</li>
</ul>
</td>
</tr>
</tbody>
</n-table>
<c-alert v-else>
No match
</c-alert>
</c-card>
<c-card title="Sample matching text" mt-3>
<pre style="white-space: pre-wrap; word-break: break-all;">{{ sample }}</pre>
</c-card>
<c-card title="Regex Diagram" style="overflow-x: scroll;" mt-3>
<shadow-root ref="visualizerSVG">
&#xa0;
</shadow-root>
</c-card>
</div>
</template>