diff options
author | 2023-06-18 12:27:26 +0200 | |
---|---|---|
committer | 2023-06-18 10:27:26 +0000 | |
commit | a6bbeaebd8e4f629ff20d6e2386be9c8734f6d8d (patch) | |
tree | 586b395e8c4854d362d844a6b15bb9cc85f52b64 /src | |
parent | f771e7a99f962463401cdd8addd455782605675f (diff) | |
download | it-tools-a6bbeaebd8e4f629ff20d6e2386be9c8734f6d8d.tar.gz it-tools-a6bbeaebd8e4f629ff20d6e2386be9c8734f6d8d.tar.zst it-tools-a6bbeaebd8e4f629ff20d6e2386be9c8734f6d8d.zip |
feat(new tool): xml formatter (#457)
* feat(new tool): xml formatter
* feat(xml-formatter): added happy path e2e tests
* refactor(xml-formatter): improved unit tests
* refactor(xml-formatter): add better suitable icon
* feat(xml-formatter): added happy path e2e tests
* feat(xml-formatter): registered xml as syntax highlighter
* chore(auto-import): removed unused NSpace
---------
Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/components/TextareaCopyable.vue | 1 | ||||
-rw-r--r-- | src/tools/index.ts | 2 | ||||
-rw-r--r-- | src/tools/xml-formatter/index.ts | 12 | ||||
-rw-r--r-- | src/tools/xml-formatter/xml-formatter.e2e.spec.ts | 23 | ||||
-rw-r--r-- | src/tools/xml-formatter/xml-formatter.service.test.ts | 27 | ||||
-rw-r--r-- | src/tools/xml-formatter/xml-formatter.service.ts | 28 | ||||
-rw-r--r-- | src/tools/xml-formatter/xml-formatter.vue | 46 |
7 files changed, 139 insertions, 0 deletions
diff --git a/src/components/TextareaCopyable.vue b/src/components/TextareaCopyable.vue index 78f26d7..16e1151 100644 --- a/src/components/TextareaCopyable.vue +++ b/src/components/TextareaCopyable.vue @@ -25,6 +25,7 @@ const props = withDefaults( hljs.registerLanguage('sql', sqlHljs); hljs.registerLanguage('json', jsonHljs); hljs.registerLanguage('html', xmlHljs); +hljs.registerLanguage('xml', xmlHljs); hljs.registerLanguage('yaml', yamlHljs); const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props); diff --git a/src/tools/index.ts b/src/tools/index.ts index 211a8a8..f451d67 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -57,6 +57,7 @@ import { tool as urlEncoder } from './url-encoder'; import { tool as urlParser } from './url-parser'; import { tool as uuidGenerator } from './uuid-generator'; import { tool as macAddressLookup } from './mac-address-lookup'; +import { tool as xmlFormatter } from './xml-formatter'; export const toolsByCategory: ToolCategory[] = [ { @@ -114,6 +115,7 @@ export const toolsByCategory: ToolCategory[] = [ sqlPrettify, chmodCalculator, dockerRunToDockerComposeConverter, + xmlFormatter, ], }, { diff --git a/src/tools/xml-formatter/index.ts b/src/tools/xml-formatter/index.ts new file mode 100644 index 0000000..fe28d3a --- /dev/null +++ b/src/tools/xml-formatter/index.ts @@ -0,0 +1,12 @@ +import { Code } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'XML formatter', + path: '/xml-formatter', + description: 'Prettify your XML string to a human friendly readable format.', + keywords: ['xml', 'prettify', 'format'], + component: () => import('./xml-formatter.vue'), + icon: Code, + createdAt: new Date('2023-06-17'), +}); diff --git a/src/tools/xml-formatter/xml-formatter.e2e.spec.ts b/src/tools/xml-formatter/xml-formatter.e2e.spec.ts new file mode 100644 index 0000000..11fbbd8 --- /dev/null +++ b/src/tools/xml-formatter/xml-formatter.e2e.spec.ts @@ -0,0 +1,23 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Tool - XML formatter', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/xml-formatter'); + }); + + test('Has correct title', async ({ page }) => { + await expect(page).toHaveTitle('XML formatter - IT Tools'); + }); + + test('XML is converted into a human readable format', async ({ page }) => { + await page.getByTestId('input').fill('<foo><bar>baz</bar><bar>baz</bar></foo>'); + + const formattedXml = await page.getByTestId('area-content').innerText(); + + expect(formattedXml.trim()).toEqual(` +<foo> + <bar>baz</bar> + <bar>baz</bar> +</foo>`.trim()); + }); +}); diff --git a/src/tools/xml-formatter/xml-formatter.service.test.ts b/src/tools/xml-formatter/xml-formatter.service.test.ts new file mode 100644 index 0000000..2b14558 --- /dev/null +++ b/src/tools/xml-formatter/xml-formatter.service.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest'; +import { formatXml } from './xml-formatter.service'; + +describe('xml-formatter service', () => { + describe('formatXml', () => { + it('converts XML into a human readable format', () => { + const initString = '<hello><world>foo</world><world>bar</world></hello>'; + + expect(formatXml(initString)).toMatchInlineSnapshot(` + "<hello> + <world> + foo + </world> + <world> + bar + </world> + </hello>" + `); + }); + + it('returns an empty string if the input is not valid XML', () => { + const initString = 'hello world'; + + expect(formatXml(initString)).toEqual(''); + }); + }); +}); diff --git a/src/tools/xml-formatter/xml-formatter.service.ts b/src/tools/xml-formatter/xml-formatter.service.ts new file mode 100644 index 0000000..3441f0b --- /dev/null +++ b/src/tools/xml-formatter/xml-formatter.service.ts @@ -0,0 +1,28 @@ +import xmlFormat, { type XMLFormatterOptions } from 'xml-formatter'; +import { withDefaultOnError } from '@/utils/defaults'; + +export { formatXml, isValidXML }; + +function cleanRawXml(rawXml: string): string { + return rawXml.trim(); +} + +function formatXml(rawXml: string, options?: XMLFormatterOptions): string { + return withDefaultOnError(() => xmlFormat(cleanRawXml(rawXml), options) ?? '', ''); +} + +function isValidXML(rawXml: string): boolean { + const cleanedRawXml = cleanRawXml(rawXml); + + if (cleanedRawXml === '') { + return true; + } + + try { + xmlFormat(cleanedRawXml); + return true; + } + catch (e) { + return false; + } +} diff --git a/src/tools/xml-formatter/xml-formatter.vue b/src/tools/xml-formatter/xml-formatter.vue new file mode 100644 index 0000000..d59cf8c --- /dev/null +++ b/src/tools/xml-formatter/xml-formatter.vue @@ -0,0 +1,46 @@ +<script setup lang="ts"> +import { formatXml, isValidXML } from './xml-formatter.service'; +import type { UseValidationRule } from '@/composable/validation'; + +const defaultValue = '<hello><world>foo</world><world>bar</world></hello>'; +const indentSize = useStorage('xml-formatter:indent-size', 2); +const collapseContent = useStorage('xml-formatter:collapse-content', true); + +function transformer(value: string) { + return formatXml(value, { + indentation: ' '.repeat(indentSize.value), + collapseContent: collapseContent.value, + lineSeparator: '\n', + }); +} + +const rules: UseValidationRule<string>[] = [ + { + validator: isValidXML, + message: 'Provided XML is not valid.', + }, +]; +</script> + +<template> + <div important:flex-full important:flex-shrink-0 important:flex-grow-0> + <div flex justify-center> + <n-form-item label="Collapse content:" label-placement="left"> + <n-switch v-model:value="collapseContent" /> + </n-form-item> + <n-form-item label="Indent size:" label-placement="left" label-width="100" :show-feedback="false"> + <n-input-number v-model:value="indentSize" min="0" max="10" w-100px /> + </n-form-item> + </div> + </div> + + <format-transformer + input-label="Your XML" + input-placeholder="Paste your XML here..." + output-label="Formatted XML from your XML" + output-language="xml" + :input-validation-rules="rules" + :transformer="transformer" + :input-default="defaultValue" + /> +</template> |