Merge branch 'main' into fix/phone-parser-storage

This commit is contained in:
sharevb 2024-09-22 19:49:51 +02:00 committed by GitHub
commit 2bba23635d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 942 additions and 22 deletions

View file

@ -7,6 +7,7 @@ import sqlHljs from 'highlight.js/lib/languages/sql';
import xmlHljs from 'highlight.js/lib/languages/xml';
import yamlHljs from 'highlight.js/lib/languages/yaml';
import iniHljs from 'highlight.js/lib/languages/ini';
import markdownHljs from 'highlight.js/lib/languages/markdown';
import { useCopy } from '@/composable/copy';
const props = withDefaults(
@ -30,6 +31,7 @@ hljs.registerLanguage('html', xmlHljs);
hljs.registerLanguage('xml', xmlHljs);
hljs.registerLanguage('yaml', yamlHljs);
hljs.registerLanguage('toml', iniHljs);
hljs.registerLanguage('markdown', markdownHljs);
const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props);
const { height } = followHeightOf.value ? useElementSize(followHeightOf) : { height: ref(null) };

View file

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

View file

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

View file

@ -0,0 +1,65 @@
<script setup lang="ts">
import { normalizeEmail } from 'email-normalizer';
import { withDefaultOnError } from '@/utils/defaults';
import { useCopy } from '@/composable/copy';
const emails = ref('');
const normalizedEmails = computed(() => {
if (!emails.value) {
return '';
}
return emails.value
.split('\n')
.map((email) => {
return withDefaultOnError(() => normalizeEmail({ email }), `Unable to parse email: ${email}`);
})
.join('\n');
});
const { copy } = useCopy({ source: normalizedEmails, text: 'Normalized emails copied to the clipboard', createToast: true });
</script>
<template>
<div>
<div class="mb-2">
Raw emails to normalize:
</div>
<c-input-text
v-model:value="emails"
placeholder="Put your emails here (one per line)..."
rows="3"
multiline
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
autofocus
monospace
/>
<div class="mb-2 mt-4">
Normalized emails:
</div>
<c-input-text
:value="normalizedEmails"
placeholder="Normalized emails will appear here..."
rows="3"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
multiline
readonly
monospace
/>
<div class="mt-4 flex justify-center gap-2">
<c-button @click="emails = ''">
Clear emails
</c-button>
<c-button :disabled="!normalizedEmails" @click="copy()">
Copy normalized emails
</c-button>
</div>
</div>
</template>

View file

@ -0,0 +1,12 @@
import { Mail } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Email normalizer',
path: '/email-normalizer',
description: 'Normalize email addresses to a standard format for easier comparison. Useful for deduplication and data cleaning.',
keywords: ['email', 'normalizer'],
component: () => import('./email-normalizer.vue'),
icon: Mail,
createdAt: new Date('2024-08-15'),
});

View file

@ -1,11 +1,17 @@
import { tool as base64FileConverter } from './base64-file-converter';
import { tool as base64StringConverter } from './base64-string-converter';
import { tool as basicAuthGenerator } from './basic-auth-generator';
import { tool as emailNormalizer } from './email-normalizer';
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 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';
import { tool as macAddressGenerator } from './mac-address-generator';
@ -107,6 +113,9 @@ export const toolsByCategory: ToolCategory[] = [
listConverter,
tomlToJson,
tomlToYaml,
xmlToJson,
jsonToXml,
markdownToHtml,
],
},
{
@ -148,6 +157,9 @@ export const toolsByCategory: ToolCategory[] = [
dockerRunToDockerComposeConverter,
xmlFormatter,
yamlViewer,
emailNormalizer,
regexTester,
regexMemo,
],
},
{

View file

@ -0,0 +1,12 @@
import { Braces } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'JSON to XML',
path: '/json-to-xml',
description: 'Convert JSON to XML',
keywords: ['json', 'xml'],
component: () => import('./json-to-xml.vue'),
icon: Braces,
createdAt: new Date('2024-08-09'),
});

View file

@ -0,0 +1,32 @@
<script setup lang="ts">
import convert from 'xml-js';
import JSON5 from 'json5';
import { withDefaultOnError } from '@/utils/defaults';
import type { UseValidationRule } from '@/composable/validation';
const defaultValue = '{"a":{"_attributes":{"x":"1.234","y":"It\'s"}}}';
function transformer(value: string) {
return withDefaultOnError(() => {
return convert.js2xml(JSON5.parse(value), { compact: true });
}, '');
}
const rules: UseValidationRule<string>[] = [
{
validator: (v: string) => v === '' || JSON5.parse(v),
message: 'Provided JSON is not valid.',
},
];
</script>
<template>
<format-transformer
input-label="Your JSON content"
:input-default="defaultValue"
input-placeholder="Paste your JSON content here..."
output-label="Converted XML"
output-language="xml"
:transformer="transformer"
:input-validation-rules="rules"
/>
</template>

View file

@ -2,6 +2,7 @@
import { generateLoremIpsum } from './lorem-ipsum-generator.service';
import { useCopy } from '@/composable/copy';
import { randIntFromInterval } from '@/utils/random';
import { computedRefreshable } from '@/composable/computedRefreshable';
const paragraphs = ref(1);
const sentences = ref([3, 8]);
@ -9,7 +10,7 @@ const words = ref([8, 15]);
const startWithLoremIpsum = ref(true);
const asHTML = ref(false);
const loremIpsumText = computed(() =>
const [loremIpsumText, refreshLoremIpsum] = computedRefreshable(() =>
generateLoremIpsum({
paragraphCount: paragraphs.value,
asHTML: asHTML.value,
@ -18,6 +19,7 @@ const loremIpsumText = computed(() =>
startWithLoremIpsum: startWithLoremIpsum.value,
}),
);
const { copy } = useCopy({ source: loremIpsumText, text: 'Lorem ipsum copied to the clipboard' });
</script>
@ -41,10 +43,13 @@ const { copy } = useCopy({ source: loremIpsumText, text: 'Lorem ipsum copied to
<c-input-text :value="loremIpsumText" multiline placeholder="Your lorem ipsum..." readonly mt-5 rows="5" />
<div mt-5 flex justify-center>
<div mt-5 flex justify-center gap-3>
<c-button autofocus @click="copy()">
Copy
</c-button>
<c-button @click="refreshLoremIpsum">
Refresh
</c-button>
</div>
</c-card>
</template>

View file

@ -0,0 +1,12 @@
import { Markdown } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Markdown to HTML',
path: '/markdown-to-html',
description: 'Convert Markdown to Html and allow to print (as PDF)',
keywords: ['markdown', 'html', 'converter', 'pdf'],
component: () => import('./markdown-to-html.vue'),
icon: Markdown,
createdAt: new Date('2024-08-25'),
});

View file

@ -0,0 +1,44 @@
<script setup lang="ts">
import markdownit from 'markdown-it';
import TextareaCopyable from '@/components/TextareaCopyable.vue';
const inputMarkdown = ref('');
const outputHtml = computed(() => {
const md = markdownit();
return md.render(inputMarkdown.value);
});
function printHtml() {
const w = window.open();
if (w === null) {
return;
}
w.document.body.innerHTML = outputHtml.value;
w.print();
}
</script>
<template>
<div>
<c-input-text
v-model:value="inputMarkdown"
multiline raw-text
placeholder="Your Markdown content..."
rows="8"
autofocus
label="Your Markdown to convert:"
/>
<n-divider />
<n-form-item label="Output HTML:">
<TextareaCopyable :value="outputHtml" :word-wrap="true" language="html" />
</n-form-item>
<div flex justify-center>
<n-button @click="printHtml">
Print as PDF
</n-button>
</div>
</div>
</template>

View file

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

View file

@ -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 its before `bar`
`foo(?!bar)` | match `foo` if its *not* before `bar`
`(?<=bar)foo` | match `foo` if its after `bar`
`(?<!bar)foo` | match `foo` if its *not* after `bar`
### Grouping and capturing
Expression | Description
:--|:--
`(foo)` | capturing group; match and capture `foo`
`(?:foo)` | non-capturing group; match `foo` but *without* capturing `foo`
`(foo)bar\1` | `\1` is a backreference to the 1st capturing group; match `foobarfoo`
- Capturing groups are only relevant in the following methods:
- `string.match(regexp)`
- `string.matchAll(regexp)`
- `string.replace(regexp, callback)`
- `\N` is a backreference to the `Nth` capturing group. Capturing groups are numbered starting from 1.
## References and tools
- [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions)
- [RegExplained](https://leaverou.github.io/regexplained/)

View file

@ -0,0 +1,32 @@
<script setup lang="ts">
import { useThemeVars } from 'naive-ui';
import Memo from './regex-memo.content.md';
const themeVars = useThemeVars();
</script>
<template>
<div>
<Memo />
</div>
</template>
<style lang="less" scoped>
::v-deep(pre) {
margin: 0;
padding: 15px 22px;
background-color: v-bind('themeVars.cardColor');
border-radius: 4px;
overflow: auto;
}
::v-deep(table) {
border-collapse: collapse;
}
::v-deep(table), ::v-deep(td), ::v-deep(th) {
border: 1px solid v-bind('themeVars.textColor1');
padding: 5px;
}
::v-deep(a) {
color: v-bind('themeVars.textColor1');
}
</style>

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>

View file

@ -0,0 +1,12 @@
import { Braces } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'XML to JSON',
path: '/xml-to-json',
description: 'Convert XML to JSON',
keywords: ['xml', 'json'],
component: () => import('./xml-to-json.vue'),
icon: Braces,
createdAt: new Date('2024-08-09'),
});

View file

@ -0,0 +1,32 @@
<script setup lang="ts">
import convert from 'xml-js';
import { isValidXML } from '../xml-formatter/xml-formatter.service';
import { withDefaultOnError } from '@/utils/defaults';
import type { UseValidationRule } from '@/composable/validation';
const defaultValue = '<a x="1.234" y="It\'s"/>';
function transformer(value: string) {
return withDefaultOnError(() => {
return JSON.stringify(convert.xml2js(value, { compact: true }), null, 2);
}, '');
}
const rules: UseValidationRule<string>[] = [
{
validator: isValidXML,
message: 'Provided XML is not valid.',
},
];
</script>
<template>
<format-transformer
input-label="Your XML content"
:input-default="defaultValue"
input-placeholder="Paste your XML content here..."
output-label="Converted JSON"
output-language="json"
:transformer="transformer"
:input-validation-rules="rules"
/>
</template>