diff --git a/components.d.ts b/components.d.ts index 15cda0fd..0db2d095 100644 --- a/components.d.ts +++ b/components.d.ts @@ -161,6 +161,7 @@ declare module '@vue/runtime-core' { UserAgentParser: typeof import('./src/tools/user-agent-parser/user-agent-parser.vue')['default'] UserAgentResultCards: typeof import('./src/tools/user-agent-parser/user-agent-result-cards.vue')['default'] UuidGenerator: typeof import('./src/tools/uuid-generator/uuid-generator.vue')['default'] + XmlFormatter: typeof import('./src/tools/xml-formatter/xml-formatter.vue')['default'] YamlToJson: typeof import('./src/tools/yaml-to-json-converter/yaml-to-json.vue')['default'] } } diff --git a/package.json b/package.json index 26a3d5d3..277da558 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "uuid": "^8.3.2", "vue": "^3.2.47", "vue-router": "^4.1.6", + "xml-formatter": "^3.3.2", "yaml": "^2.2.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 914a9e12..99488feb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,6 +130,9 @@ dependencies: vue-router: specifier: ^4.1.6 version: 4.1.6(vue@3.2.47) + xml-formatter: + specifier: ^3.3.2 + version: 3.3.2 yaml: specifier: ^2.2.1 version: 2.2.1 @@ -8883,11 +8886,23 @@ packages: optional: true dev: true + /xml-formatter@3.3.2: + resolution: {integrity: sha512-ld34F1b7+2UQGNkfsAV4MN3/b7cdUstyMj3XJhzKFasOPtMToVCkqmrNcmrRuSlPxgH1K9tXPkqr75gAT3ix2g==} + engines: {node: '>= 14'} + dependencies: + xml-parser-xo: 4.0.5 + dev: false + /xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} dev: true + /xml-parser-xo@4.0.5: + resolution: {integrity: sha512-UWXOHMQ4ySxpUiU3S/9KzPOhninlL8SN1xFfWgX9WjgoZWoLKtEeJIEz4jhKtdFsoZBCYjg9rDEP3qfnpiHagQ==} + engines: {node: '>= 14'} + dev: false + /xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} dev: true diff --git a/src/components/TextareaCopyable.vue b/src/components/TextareaCopyable.vue index 78f26d76..16e11512 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 211a8a81..f451d679 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 00000000..fe28d3ae --- /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 00000000..11fbbd8e --- /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('bazbaz'); + + const formattedXml = await page.getByTestId('area-content').innerText(); + + expect(formattedXml.trim()).toEqual(` + + baz + baz +`.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 00000000..2b14558c --- /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 = 'foobar'; + + expect(formatXml(initString)).toMatchInlineSnapshot(` + " + + foo + + + bar + + " + `); + }); + + 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 00000000..3441f0bb --- /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 00000000..d59cf8c7 --- /dev/null +++ b/src/tools/xml-formatter/xml-formatter.vue @@ -0,0 +1,46 @@ + + +