@@ -81,4 +119,24 @@ const { t } = useI18n();
opacity: 0;
margin-bottom: 0;
}
+
+.ghost-favorites-draggable {
+ opacity: 0.4;
+ background-color: #ccc;
+ border: 2px dashed #666;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
+ transform: scale(1.1);
+ animation: ghost-favorites-draggable-animation 0.2s ease-out;
+}
+
+@keyframes ghost-favorites-draggable-animation {
+ 0% {
+ opacity: 0;
+ transform: scale(0.9);
+ }
+ 100% {
+ opacity: 0.4;
+ transform: scale(1.0);
+ }
+}
diff --git a/src/tools/html-wysiwyg-editor/editor/menu-bar.vue b/src/tools/html-wysiwyg-editor/editor/menu-bar.vue
index d3ad3168..9069673c 100644
--- a/src/tools/html-wysiwyg-editor/editor/menu-bar.vue
+++ b/src/tools/html-wysiwyg-editor/editor/menu-bar.vue
@@ -84,8 +84,8 @@ const items: MenuItem[] = [
type: 'button',
icon: H3,
title: 'Heading 3',
- action: () => editor.value.chain().focus().toggleHeading({ level: 4 }).run(),
- isActive: () => editor.value.isActive('heading', { level: 4 }),
+ action: () => editor.value.chain().focus().toggleHeading({ level: 3 }).run(),
+ isActive: () => editor.value.isActive('heading', { level: 3 }),
},
{
type: 'button',
diff --git a/src/tools/index.ts b/src/tools/index.ts
index d07bda17..aa0bb3ed 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -10,6 +10,9 @@ 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';
@@ -114,6 +117,7 @@ export const toolsByCategory: ToolCategory[] = [
xmlToJson,
jsonToXml,
currencyConverter,
+ markdownToHtml,
],
},
{
@@ -156,6 +160,8 @@ export const toolsByCategory: ToolCategory[] = [
xmlFormatter,
yamlViewer,
emailNormalizer,
+ regexTester,
+ regexMemo,
],
},
{
diff --git a/src/tools/markdown-to-html/index.ts b/src/tools/markdown-to-html/index.ts
new file mode 100644
index 00000000..73a6cfb3
--- /dev/null
+++ b/src/tools/markdown-to-html/index.ts
@@ -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'),
+});
diff --git a/src/tools/markdown-to-html/markdown-to-html.vue b/src/tools/markdown-to-html/markdown-to-html.vue
new file mode 100644
index 00000000..c84d44ec
--- /dev/null
+++ b/src/tools/markdown-to-html/markdown-to-html.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Print as PDF
+
+
+
+
diff --git a/src/tools/regex-memo/index.ts b/src/tools/regex-memo/index.ts
new file mode 100644
index 00000000..f1f56489
--- /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 00000000..0f779401
--- /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 00000000..62a5e234
--- /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: 'Test your regular expressions with sample text.',
+ 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 00000000..bd4efbbc
--- /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 00000000..ec8682c5
--- /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 00000000..a1fa7958
--- /dev/null
+++ b/src/tools/regex-tester/regex-tester.vue
@@ -0,0 +1,193 @@
+
+
+
+
+
+
+
+ See Regular Expression Cheatsheet
+
+
+
+ Global search. (g
)
+
+
+ Case-insensitive search. (i
)
+
+
+ Multiline(m
)
+
+
+ Singleline(s
)
+
+
+ Unicode(u
)
+
+
+ Unicode Sets (v
)
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Index in text
+ |
+
+ Value
+ |
+
+ Captures
+ |
+
+ Groups
+ |
+
+
+
+
+ {{ match.index }} |
+ {{ match.value }} |
+
+
+ -
+ "{{ capture.name }}" = {{ capture.value }} [{{ capture.start }} - {{ capture.end }}]
+
+
+ |
+
+
+ -
+ "{{ group.name }}" = {{ group.value }} [{{ group.start }} - {{ group.end }}]
+
+
+ |
+
+
+
+
+ No match
+
+
+
+
+ {{ sample }}
+
+
+
+
+
+
+
+
+
diff --git a/src/tools/tools.store.ts b/src/tools/tools.store.ts
index d952b7cb..fb12450d 100644
--- a/src/tools/tools.store.ts
+++ b/src/tools/tools.store.ts
@@ -14,6 +14,7 @@ export const useToolStore = defineStore('tools', () => {
return ({
...tool,
+ path: tool.path,
name: t(`tools.${toolI18nKey}.title`, tool.name),
description: t(`tools.${toolI18nKey}.description`, tool.description),
category: t(`tools.categories.${tool.category.toLowerCase()}`, tool.category),
@@ -23,8 +24,9 @@ export const useToolStore = defineStore('tools', () => {
const toolsByCategory = computed(() => {
return _.chain(tools.value)
.groupBy('category')
- .map((components, name) => ({
+ .map((components, name, path) => ({
name,
+ path,
components,
}))
.value();
@@ -32,7 +34,7 @@ export const useToolStore = defineStore('tools', () => {
const favoriteTools = computed(() => {
return favoriteToolsName.value
- .map(favoriteName => tools.value.find(({ name }) => name === favoriteName))
+ .map(favoriteName => tools.value.find(({ name, path }) => name === favoriteName || path === favoriteName))
.filter(Boolean) as ToolWithCategory[]; // cast because .filter(Boolean) does not remove undefined from type
});
@@ -43,15 +45,23 @@ export const useToolStore = defineStore('tools', () => {
newTools: computed(() => tools.value.filter(({ isNew }) => isNew)),
addToolToFavorites({ tool }: { tool: MaybeRef }) {
- favoriteToolsName.value.push(get(tool).name);
+ const toolPath = get(tool).path;
+ if (toolPath) {
+ favoriteToolsName.value.push(toolPath);
+ }
},
removeToolFromFavorites({ tool }: { tool: MaybeRef }) {
- favoriteToolsName.value = favoriteToolsName.value.filter(name => get(tool).name !== name);
+ favoriteToolsName.value = favoriteToolsName.value.filter(name => get(tool).name !== name && get(tool).path !== name);
},
isToolFavorite({ tool }: { tool: MaybeRef }) {
- return favoriteToolsName.value.includes(get(tool).name);
+ return favoriteToolsName.value.includes(get(tool).name)
+ || favoriteToolsName.value.includes(get(tool).path);
+ },
+
+ updateFavoriteTools(newOrder: ToolWithCategory[]) {
+ favoriteToolsName.value = newOrder.map(tool => tool.path);
},
};
});
diff --git a/src/tools/yaml-to-json-converter/yaml-to-json.e2e.spec.ts b/src/tools/yaml-to-json-converter/yaml-to-json.e2e.spec.ts
index 7b2a2d18..d6ed84c3 100644
--- a/src/tools/yaml-to-json-converter/yaml-to-json.e2e.spec.ts
+++ b/src/tools/yaml-to-json-converter/yaml-to-json.e2e.spec.ts
@@ -28,4 +28,53 @@ test.describe('Tool - Yaml to json', () => {
`.trim(),
);
});
+
+ test('Yaml is parsed with merge key and output correct json', async ({ page }) => {
+ await page.getByTestId('input').fill(`
+ default: &default
+ name: ''
+ age: 0
+
+ person:
+ *default
+
+ persons:
+ - <<: *default
+ age: 1
+ - <<: *default
+ name: John
+ - { age: 3, <<: *default }
+
+ `);
+
+ const generatedJson = await page.getByTestId('area-content').innerText();
+
+ expect(generatedJson.trim()).toEqual(
+ `
+{
+ "default": {
+ "name": "",
+ "age": 0
+ },
+ "person": {
+ "name": "",
+ "age": 0
+ },
+ "persons": [
+ {
+ "name": "",
+ "age": 1
+ },
+ {
+ "name": "John",
+ "age": 0
+ },
+ {
+ "age": 3,
+ "name": ""
+ }
+ ]
+}`.trim(),
+ );
+ });
});
diff --git a/src/tools/yaml-to-json-converter/yaml-to-json.vue b/src/tools/yaml-to-json-converter/yaml-to-json.vue
index 39c9297f..72608add 100644
--- a/src/tools/yaml-to-json-converter/yaml-to-json.vue
+++ b/src/tools/yaml-to-json-converter/yaml-to-json.vue
@@ -6,7 +6,7 @@ import { withDefaultOnError } from '@/utils/defaults';
function transformer(value: string) {
return withDefaultOnError(() => {
- const obj = parseYaml(value);
+ const obj = parseYaml(value, { merge: true });
return obj ? JSON.stringify(obj, null, 3) : '';
}, '');
}
diff --git a/src/ui/c-select/c-select.vue b/src/ui/c-select/c-select.vue
index 7b3607c9..38e2eb8f 100644
--- a/src/ui/c-select/c-select.vue
+++ b/src/ui/c-select/c-select.vue
@@ -151,7 +151,7 @@ function onSearchInput() {
>