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
This commit is contained in:
		
							parent
							
								
									cb5b462e11
								
							
						
					
					
						commit
						f61db56abb
					
				
							
								
								
									
										6
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -126,7 +126,9 @@ declare module '@vue/runtime-core' { | ||||
|     MenuLayout: typeof import('./src/components/MenuLayout.vue')['default'] | ||||
|     MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default'] | ||||
|     MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default'] | ||||
|     NA: typeof import('naive-ui')['NA'] | ||||
|     NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] | ||||
|     NCheckbox: typeof import('naive-ui')['NCheckbox'] | ||||
|     NCode: typeof import('naive-ui')['NCode'] | ||||
|     NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] | ||||
|     NConfigProvider: typeof import('naive-ui')['NConfigProvider'] | ||||
| @ -144,7 +146,9 @@ declare module '@vue/runtime-core' { | ||||
|     NLayoutSider: typeof import('naive-ui')['NLayoutSider'] | ||||
|     NMenu: typeof import('naive-ui')['NMenu'] | ||||
|     NScrollbar: typeof import('naive-ui')['NScrollbar'] | ||||
|     NSpace: typeof import('naive-ui')['NSpace'] | ||||
|     NSpin: typeof import('naive-ui')['NSpin'] | ||||
|     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'] | ||||
| @ -154,11 +158,13 @@ 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'] | ||||
|     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'] | ||||
|     RouterView: typeof import('vue-router')['RouterView'] | ||||
|     RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue')['default'] | ||||
|     SafelinkDecoder: typeof import('./src/tools/safelink-decoder/safelink-decoder.vue')['default'] | ||||
|     SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default'] | ||||
|     SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default'] | ||||
|     SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default'] | ||||
|  | ||||
| @ -80,6 +80,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", | ||||
|  | ||||
							
								
								
									
										34
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										34
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @ -140,6 +140,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 | ||||
| @ -3351,7 +3354,7 @@ packages: | ||||
|     dependencies: | ||||
|       '@unhead/dom': 0.5.1 | ||||
|       '@unhead/schema': 0.5.1 | ||||
|       '@vueuse/shared': 10.7.2(vue@3.3.4) | ||||
|       '@vueuse/shared': 10.9.0(vue@3.3.4) | ||||
|       unhead: 0.5.1 | ||||
|       vue: 3.3.4 | ||||
|     transitivePeerDependencies: | ||||
| @ -3993,10 +3996,10 @@ packages: | ||||
|       - vue | ||||
|     dev: false | ||||
| 
 | ||||
|   /@vueuse/shared@10.7.2(vue@3.3.4): | ||||
|     resolution: {integrity: sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA==} | ||||
|   /@vueuse/shared@10.9.0(vue@3.3.4): | ||||
|     resolution: {integrity: sha512-Uud2IWncmAfJvRaFYzv5OHDli+FbOzxiVEQdLCKQKLyhz94PIyFC3CHcH7EDMwIn8NPtD06+PNbC/PiO0LGLtw==} | ||||
|     dependencies: | ||||
|       vue-demi: 0.14.6(vue@3.3.4) | ||||
|       vue-demi: 0.14.7(vue@3.3.4) | ||||
|     transitivePeerDependencies: | ||||
|       - '@vue/composition-api' | ||||
|       - vue | ||||
| @ -4942,6 +4945,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 | ||||
| @ -7702,6 +7710,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: | ||||
| @ -7872,6 +7888,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'} | ||||
| @ -9151,8 +9172,8 @@ packages: | ||||
|       vue: 3.3.4 | ||||
|     dev: false | ||||
| 
 | ||||
|   /vue-demi@0.14.6(vue@3.3.4): | ||||
|     resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==} | ||||
|   /vue-demi@0.14.7(vue@3.3.4): | ||||
|     resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==} | ||||
|     engines: {node: '>=12'} | ||||
|     hasBin: true | ||||
|     requiresBuild: true | ||||
| @ -9442,6 +9463,7 @@ packages: | ||||
| 
 | ||||
|   /workbox-google-analytics@7.0.0: | ||||
|     resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} | ||||
|     deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained | ||||
|     dependencies: | ||||
|       workbox-background-sync: 7.0.0 | ||||
|       workbox-core: 7.0.0 | ||||
|  | ||||
| @ -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: { | ||||
| @ -33,3 +34,31 @@ function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue: | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function useQueryParamOrStorage<T>({ 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 storageDefaultValue = storageRef.value ?? defaultValue; | ||||
| 
 | ||||
|   const proxy = useRouteQuery(name, transformer.toQuery(storageDefaultValue as never)); | ||||
| 
 | ||||
|   const ref = computed<T>({ | ||||
|     get() { | ||||
|       return transformer.fromQuery(proxy.value) as unknown as T; | ||||
|     }, | ||||
|     set(value) { | ||||
|       proxy.value = transformer.toQuery(value as never); | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   watch( | ||||
|     ref, | ||||
|     (newValue) => { | ||||
|       storageRef.value = newValue; | ||||
|     }, | ||||
|   ); | ||||
| 
 | ||||
|   return ref; | ||||
| } | ||||
|  | ||||
| @ -3,9 +3,11 @@ import _ from 'lodash'; | ||||
| import { type Ref, reactive, watch } from 'vue'; | ||||
| 
 | ||||
| type ValidatorReturnType = unknown; | ||||
| type GetErrorMessageReturnType = string; | ||||
| 
 | ||||
| export interface UseValidationRule<T> { | ||||
|   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<T>({ | ||||
| 
 | ||||
|       for (const rule of get(rules)) { | ||||
|         if (isFalsyOrHasThrown(() => rule.validator(source.value))) { | ||||
|           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'; | ||||
|         } | ||||
|       } | ||||
|  | ||||
| @ -6,6 +6,7 @@ import { tool as asciiTextDrawer } from './ascii-text-drawer'; | ||||
| 
 | ||||
| import { tool as textToUnicode } from './text-to-unicode'; | ||||
| import { tool as safelinkDecoder } from './safelink-decoder'; | ||||
| import { tool as regexTester } from './regex-tester'; | ||||
| import { tool as pdfSignatureChecker } from './pdf-signature-checker'; | ||||
| import { tool as numeronymGenerator } from './numeronym-generator'; | ||||
| import { tool as macAddressGenerator } from './mac-address-generator'; | ||||
| @ -148,6 +149,7 @@ export const toolsByCategory: ToolCategory[] = [ | ||||
|       dockerRunToDockerComposeConverter, | ||||
|       xmlFormatter, | ||||
|       yamlViewer, | ||||
|       regexTester, | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|  | ||||
							
								
								
									
										12
									
								
								src/tools/regex-tester/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/tools/regex-tester/index.ts
									
									
									
									
									
										Normal 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-04-20'), | ||||
| }); | ||||
							
								
								
									
										215
									
								
								src/tools/regex-tester/regex-tester.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								src/tools/regex-tester/regex-tester.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,215 @@ | ||||
| <script setup lang="ts"> | ||||
| import RandExp from 'randexp'; | ||||
| import { useValidation } from '@/composable/validation'; | ||||
| import { useQueryParamOrStorage } from '@/composable/queryParams'; | ||||
| 
 | ||||
| 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 | ||||
| }; | ||||
| 
 | ||||
| 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 regexValidation = useValidation({ | ||||
|   source: regex, | ||||
|   rules: [ | ||||
|     { | ||||
|       message: 'Invalid regex: {0}', | ||||
|       validator: value => new RegExp(value), | ||||
|       getErrorMessage: (value) => { | ||||
|         const _ = new RegExp(value); | ||||
|         return ''; | ||||
|       }, | ||||
|     }, | ||||
|   ], | ||||
| }); | ||||
| const results = computed(() => { | ||||
|   if (regex.value === '' || text.value === '') { | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   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 { | ||||
|     const re = new RegExp(regex.value, flags); | ||||
|     const results = []; | ||||
|     let match = re.exec(text.value) as RegExpExecArrayWithIndices; | ||||
|     while (match !== null) { | ||||
|       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, | ||||
|       }); | ||||
|       match = re.exec(text.value) as RegExpExecArrayWithIndices; | ||||
|     } | ||||
|     return results; | ||||
|   } | ||||
|   catch (_) { | ||||
|     return []; | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
| const sample = computed(() => { | ||||
|   try { | ||||
|     const randexp = new RandExp(new RegExp(regex.value.replace(/\(\?\<[^\>]*\>/g, '(?:'))); | ||||
|     return randexp.gen(); | ||||
|   } | ||||
|   catch (_) { | ||||
|     return ''; | ||||
|   } | ||||
| }); | ||||
| </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" | ||||
|       /> | ||||
|       <n-a target="_blank" href="https://www.regular-expressions.info/javascript.html" mb-1 mt-1> | ||||
|         See documentation on <code>regular-expressions.info</code> | ||||
|       </n-a> | ||||
|       <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> | ||||
|       <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"> | ||||
|       <pre style="white-space: pre-wrap">{{ sample }}</pre> | ||||
|     </c-card> | ||||
|   </div> | ||||
| </template> | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user