feat(xml): add XML tools for validation, beautification, and viewing

This commit is contained in:
AshAnand34
2025-07-08 12:56:31 -07:00
parent d47fc0812d
commit 6b2070b39f
20 changed files with 372 additions and 3 deletions

31
package-lock.json generated
View File

@@ -29,6 +29,7 @@
"cron-validator": "^1.3.1",
"cronstrue": "^3.0.0",
"dayjs": "^1.11.13",
"fast-xml-parser": "^5.2.5",
"formik": "^2.4.6",
"jimp": "^0.22.12",
"js-quantities": "^1.8.0",
@@ -5893,6 +5894,24 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true
},
"node_modules/fast-xml-parser": {
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz",
"integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"strnum": "^2.1.0"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/fastq": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
@@ -10502,6 +10521,18 @@
"integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==",
"dev": true
},
"node_modules/strnum": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz",
"integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/strtok3": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz",

View File

@@ -46,6 +46,7 @@
"cron-validator": "^1.3.1",
"cronstrue": "^3.0.0",
"dayjs": "^1.11.13",
"fast-xml-parser": "^5.2.5",
"formik": "^2.4.6",
"jimp": "^0.22.12",
"js-quantities": "^1.8.0",

View File

@@ -0,0 +1,4 @@
import { tool as xmlXmlValidator } from './xml-validator/meta';
import { tool as xmlXmlBeautifier } from './xml-beautifier/meta';
import { tool as xmlXmlViewer } from './xml-viewer/meta';
export const xmlTools = [xmlXmlViewer, xmlXmlBeautifier, xmlXmlValidator];

View File

@@ -0,0 +1,56 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import { CardExampleType } from '@components/examples/ToolExamples';
import { beautifyXml } from './service';
import { InitialValuesType } from './types';
const initialValues: InitialValuesType = {};
const exampleCards: CardExampleType<InitialValuesType>[] = [
{
title: 'Beautify XML',
description: 'Beautify a compact XML string for readability.',
sampleText: '<root><item>1</item><item>2</item></root>',
sampleResult: `<root>\n <item>1</item>\n <item>2</item>\n</root>`,
sampleOptions: {}
}
];
const getGroups = () => [];
export default function XmlBeautifier({
title,
longDescription
}: ToolComponentProps) {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const compute = (_values: InitialValuesType, input: string) => {
setResult(beautifyXml(input, {}));
};
return (
<ToolContent
title={title}
input={input}
inputComponent={
<ToolTextInput
value={input}
onChange={setInput}
placeholder="Paste or import XML here..."
/>
}
resultComponent={<ToolTextResult value={result} extension="xml" />}
initialValues={initialValues}
exampleCards={exampleCards}
getGroups={getGroups}
setInput={setInput}
compute={compute}
toolInfo={{ title: `What is a ${title}?`, description: longDescription }}
/>
);
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('xml', {
name: 'XML Beautifier',
path: 'xml-beautifier',
icon: 'mdi:format-align-left',
description:
'Beautify and reformat XML for improved readability and structure.',
shortDescription: 'Beautify XML for readability.',
keywords: ['xml', 'beautify', 'format', 'pretty', 'indent'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,16 @@
import { InitialValuesType } from './types';
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
export function beautifyXml(
input: string,
_options: InitialValuesType
): string {
try {
const parser = new XMLParser();
const obj = parser.parse(input);
const builder = new XMLBuilder({ format: true, indentBy: ' ' });
return builder.build(obj);
} catch (e: any) {
return `Invalid XML: ${e.message}`;
}
}

View File

@@ -0,0 +1,3 @@
export type InitialValuesType = {
// splitSeparator: string;
};

View File

@@ -0,0 +1,18 @@
import { expect, describe, it } from 'vitest';
import { beautifyXml } from './service';
describe('xml-beautifier', () => {
it('beautifies valid XML', () => {
const input = '<root><a>1</a><b>2</b></root>';
const result = beautifyXml(input, {});
expect(result).toContain('<root>');
expect(result).toContain(' <a>1</a>');
expect(result).toContain(' <b>2</b>');
});
it('returns error for invalid XML', () => {
const input = '<root><a>1</b></root>';
const result = beautifyXml(input, {});
expect(result).toMatch(/Invalid XML/i);
});
});

View File

@@ -0,0 +1,63 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import { CardExampleType } from '@components/examples/ToolExamples';
import { validateXml } from './service';
import { InitialValuesType } from './types';
const initialValues: InitialValuesType = {};
const getGroups = () => [];
const exampleCards: CardExampleType<InitialValuesType>[] = [
{
title: 'Validate XML',
description: 'Check if an XML string is well-formed.',
sampleText: '<root><item>1</item><item>2</item></root>',
sampleResult: 'Valid XML',
sampleOptions: {}
},
{
title: 'Invalid XML',
description: 'Example of malformed XML.',
sampleText: '<root><item>1</item><item>2</root>',
sampleResult: 'Invalid XML: ...',
sampleOptions: {}
}
];
export default function XmlValidator({
title,
longDescription
}: ToolComponentProps) {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const compute = (_values: InitialValuesType, input: string) => {
setResult(validateXml(input, {}));
};
return (
<ToolContent
title={title}
input={input}
inputComponent={
<ToolTextInput
value={input}
onChange={setInput}
placeholder="Paste or import XML here..."
/>
}
resultComponent={<ToolTextResult value={result} extension="txt" />}
initialValues={initialValues}
exampleCards={exampleCards}
getGroups={getGroups}
setInput={setInput}
compute={compute}
toolInfo={{ title: `What is a ${title}?`, description: longDescription }}
/>
);
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('xml', {
name: 'XML Validator',
path: 'xml-validator',
icon: 'mdi:check-decagram',
description:
'Validate XML files or strings to ensure they are well-formed and error-free.',
shortDescription: 'Validate XML for errors.',
keywords: ['xml', 'validate', 'check', 'syntax', 'error'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,16 @@
import { InitialValuesType } from './types';
import { XMLValidator } from 'fast-xml-parser';
export function validateXml(
input: string,
_options: InitialValuesType
): string {
const result = XMLValidator.validate(input);
if (result === true) {
return 'Valid XML';
} else if (typeof result === 'object' && result.err) {
return `Invalid XML: ${result.err.msg} (line ${result.err.line}, col ${result.err.col})`;
} else {
return 'Invalid XML: Unknown error';
}
}

View File

@@ -0,0 +1,3 @@
export type InitialValuesType = {
// splitSeparator: string;
};

View File

@@ -0,0 +1,16 @@
import { expect, describe, it } from 'vitest';
import { validateXml } from './service';
describe('xml-validator', () => {
it('returns Valid XML for well-formed XML', () => {
const input = '<root><a>1</a><b>2</b></root>';
const result = validateXml(input, {});
expect(result).toBe('Valid XML');
});
it('returns error for invalid XML', () => {
const input = '<root><a>1</b></root>';
const result = validateXml(input, {});
expect(result).toMatch(/Invalid XML/i);
});
});

View File

@@ -0,0 +1,56 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import { CardExampleType } from '@components/examples/ToolExamples';
import { prettyPrintXml } from './service';
import { InitialValuesType } from './types';
const initialValues: InitialValuesType = {};
const exampleCards: CardExampleType<InitialValuesType>[] = [
{
title: 'Pretty Print XML',
description: 'View and pretty-print a compact XML string.',
sampleText: '<root><item>1</item><item>2</item></root>',
sampleResult: `<root>\n <item>1</item>\n <item>2</item>\n</root>`,
sampleOptions: {}
}
];
const getGroups = () => [];
export default function XmlViewer({
title,
longDescription
}: ToolComponentProps) {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const compute = (_values: InitialValuesType, input: string) => {
setResult(prettyPrintXml(input, {}));
};
return (
<ToolContent
title={title}
input={input}
inputComponent={
<ToolTextInput
value={input}
onChange={setInput}
placeholder="Paste or import XML here..."
/>
}
resultComponent={<ToolTextResult value={result} extension="xml" />}
initialValues={initialValues}
exampleCards={exampleCards}
getGroups={getGroups}
setInput={setInput}
compute={compute}
toolInfo={{ title: `What is a ${title}?`, description: longDescription }}
/>
);
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('xml', {
name: 'XML Viewer',
path: 'xml-viewer',
icon: 'mdi:eye-outline',
description:
'View and pretty-print XML files or strings for easier reading and debugging.',
shortDescription: 'Pretty-print and view XML.',
keywords: ['xml', 'viewer', 'pretty print', 'format', 'inspect'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,16 @@
import { InitialValuesType } from './types';
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
export function prettyPrintXml(
input: string,
_options: InitialValuesType
): string {
try {
const parser = new XMLParser();
const obj = parser.parse(input);
const builder = new XMLBuilder({ format: true, indentBy: ' ' });
return builder.build(obj);
} catch (e: any) {
return `Invalid XML: ${e.message}`;
}
}

View File

@@ -0,0 +1,3 @@
export type InitialValuesType = {
// splitSeparator: string;
};

View File

@@ -0,0 +1,18 @@
import { expect, describe, it } from 'vitest';
import { prettyPrintXml } from './service';
describe('xml-viewer', () => {
it('pretty prints valid XML', () => {
const input = '<root><a>1</a><b>2</b></root>';
const result = prettyPrintXml(input, {});
expect(result).toContain('<root>');
expect(result).toContain(' <a>1</a>');
expect(result).toContain(' <b>2</b>');
});
it('returns error for invalid XML', () => {
const input = '<root><a>1</b></root>';
const result = prettyPrintXml(input, {});
expect(result).toMatch(/Invalid XML/i);
});
});

View File

@@ -24,7 +24,8 @@ export type ToolCategory =
| 'time'
| 'csv'
| 'pdf'
| 'image-generic';
| 'image-generic'
| 'xml';
export interface DefinedTool {
type: ToolCategory;

View File

@@ -11,6 +11,7 @@ import { csvTools } from '../pages/tools/csv';
import { timeTools } from '../pages/tools/time';
import { IconifyIcon } from '@iconify/react';
import { pdfTools } from '../pages/tools/pdf';
import { xmlTools } from 'pages/tools/xml';
const toolCategoriesOrder: ToolCategory[] = [
'image-generic',
@@ -23,7 +24,8 @@ const toolCategoriesOrder: ToolCategory[] = [
'number',
'png',
'time',
'gif'
'gif',
'xml'
];
export const tools: DefinedTool[] = [
...imageTools,
@@ -34,7 +36,8 @@ export const tools: DefinedTool[] = [
...csvTools,
...videoTools,
...numberTools,
...timeTools
...timeTools,
...xmlTools
];
const categoriesConfig: {
type: ToolCategory;
@@ -115,6 +118,12 @@ const categoriesConfig: {
icon: 'material-symbols-light:image-outline-rounded',
value:
'Tools for working with pictures compress, resize, crop, convert to JPG, rotate, remove background and much more.'
},
{
type: 'xml',
icon: 'mdi-light:xml',
value:
'Tools for working with XML data structures - viewer, beautifier, validator and much more'
}
];
// use for changelogs