@@ -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/email-normalizer/email-normalizer.vue b/src/tools/email-normalizer/email-normalizer.vue
new file mode 100644
index 00000000..eae97c4e
--- /dev/null
+++ b/src/tools/email-normalizer/email-normalizer.vue
@@ -0,0 +1,65 @@
+
+
+
+  
+    
+      Raw emails to normalize:
+    
+    
+
+    
+      Normalized emails:
+    
+    
+    
+      
+        Clear emails
+      
+      
+        Copy normalized emails
+      
+    
+  
diff --git a/src/tools/email-normalizer/index.ts b/src/tools/email-normalizer/index.ts
new file mode 100644
index 00000000..299a30f7
--- /dev/null
+++ b/src/tools/email-normalizer/index.ts
@@ -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'),
+});
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 da1fe145..1ce9eb80 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -2,11 +2,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 markdownTocGenerator } from './markdown-toc-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';
@@ -109,6 +115,9 @@ export const toolsByCategory: ToolCategory[] = [
       tomlToJson,
       tomlToYaml,
       markdownTocGenerator,
+      xmlToJson,
+      jsonToXml,
+      markdownToHtml,
     ],
   },
   {
@@ -150,6 +159,9 @@ export const toolsByCategory: ToolCategory[] = [
       dockerRunToDockerComposeConverter,
       xmlFormatter,
       yamlViewer,
+      emailNormalizer,
+      regexTester,
+      regexMemo,
     ],
   },
   {
diff --git a/src/tools/json-to-xml/index.ts b/src/tools/json-to-xml/index.ts
new file mode 100644
index 00000000..c35ace2b
--- /dev/null
+++ b/src/tools/json-to-xml/index.ts
@@ -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'),
+});
diff --git a/src/tools/json-to-xml/json-to-xml.vue b/src/tools/json-to-xml/json-to-xml.vue
new file mode 100644
index 00000000..96a7cf16
--- /dev/null
+++ b/src/tools/json-to-xml/json-to-xml.vue
@@ -0,0 +1,32 @@
+
+
+
+  
+
diff --git a/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue b/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue
index 9085725f..ccd8b519 100644
--- a/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue
+++ b/src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue
@@ -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' });
 
 
@@ -41,10 +43,13 @@ const { copy } = useCopy({ source: loremIpsumText, text: 'Lorem ipsum copied to
 
     
 
-    
+    
       
         Copy
       
+      
+        Refresh
+      
     
   
 
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 }}
+    
+
+    
+      
+ 
+      
+    
+