diff --git a/package-lock.json b/package-lock.json index 5cefa39..91db444 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 86276e5..6405554 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/pages/tools/xml/index.ts b/src/pages/tools/xml/index.ts new file mode 100644 index 0000000..3be414b --- /dev/null +++ b/src/pages/tools/xml/index.ts @@ -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]; diff --git a/src/pages/tools/xml/xml-beautifier/index.tsx b/src/pages/tools/xml/xml-beautifier/index.tsx new file mode 100644 index 0000000..66172d2 --- /dev/null +++ b/src/pages/tools/xml/xml-beautifier/index.tsx @@ -0,0 +1,54 @@ +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[] = [ + { + title: 'Beautify XML', + description: 'Beautify a compact XML string for readability.', + sampleText: '12', + sampleResult: `\n 1\n 2\n`, + sampleOptions: {} + } +]; + +export default function XmlBeautifier({ + title, + longDescription +}: ToolComponentProps) { + const [input, setInput] = useState(''); + const [result, setResult] = useState(''); + + const compute = (_values: InitialValuesType, input: string) => { + setResult(beautifyXml(input, {})); + }; + + return ( + + } + resultComponent={} + initialValues={initialValues} + exampleCards={exampleCards} + getGroups={null} + setInput={setInput} + compute={compute} + toolInfo={{ title: `What is a ${title}?`, description: longDescription }} + /> + ); +} diff --git a/src/pages/tools/xml/xml-beautifier/meta.ts b/src/pages/tools/xml/xml-beautifier/meta.ts new file mode 100644 index 0000000..097a200 --- /dev/null +++ b/src/pages/tools/xml/xml-beautifier/meta.ts @@ -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')) +}); diff --git a/src/pages/tools/xml/xml-beautifier/service.ts b/src/pages/tools/xml/xml-beautifier/service.ts new file mode 100644 index 0000000..8da4262 --- /dev/null +++ b/src/pages/tools/xml/xml-beautifier/service.ts @@ -0,0 +1,23 @@ +import { InitialValuesType } from './types'; +import { XMLParser, XMLBuilder, XMLValidator } from 'fast-xml-parser'; + +export function beautifyXml( + input: string, + _options: InitialValuesType +): string { + const valid = XMLValidator.validate(input); + if (valid !== true) { + if (typeof valid === 'object' && valid.err) { + return `Invalid XML: ${valid.err.msg} (line ${valid.err.line}, col ${valid.err.col})`; + } + return 'Invalid XML'; + } + 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}`; + } +} diff --git a/src/pages/tools/xml/xml-beautifier/types.ts b/src/pages/tools/xml/xml-beautifier/types.ts new file mode 100644 index 0000000..d4135c9 --- /dev/null +++ b/src/pages/tools/xml/xml-beautifier/types.ts @@ -0,0 +1,3 @@ +export type InitialValuesType = { + // splitSeparator: string; +}; diff --git a/src/pages/tools/xml/xml-beautifier/xml-beautifier.service.test.ts b/src/pages/tools/xml/xml-beautifier/xml-beautifier.service.test.ts new file mode 100644 index 0000000..e832bb8 --- /dev/null +++ b/src/pages/tools/xml/xml-beautifier/xml-beautifier.service.test.ts @@ -0,0 +1,18 @@ +import { expect, describe, it } from 'vitest'; +import { beautifyXml } from './service'; + +describe('xml-beautifier', () => { + it('beautifies valid XML', () => { + const input = '12'; + const result = beautifyXml(input, {}); + expect(result).toContain(''); + expect(result).toContain(' 1'); + expect(result).toContain(' 2'); + }); + + it('returns error for invalid XML', () => { + const input = '1'; + const result = beautifyXml(input, {}); + expect(result).toMatch(/Invalid XML/i); + }); +}); diff --git a/src/pages/tools/xml/xml-validator/index.tsx b/src/pages/tools/xml/xml-validator/index.tsx new file mode 100644 index 0000000..96c50dc --- /dev/null +++ b/src/pages/tools/xml/xml-validator/index.tsx @@ -0,0 +1,61 @@ +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 exampleCards: CardExampleType[] = [ + { + title: 'Validate XML', + description: 'Check if an XML string is well-formed.', + sampleText: '12', + sampleResult: 'Valid XML', + sampleOptions: {} + }, + { + title: 'Invalid XML', + description: 'Example of malformed XML.', + sampleText: '12', + sampleResult: 'Invalid XML: ...', + sampleOptions: {} + } +]; + +export default function XmlValidator({ + title, + longDescription +}: ToolComponentProps) { + const [input, setInput] = useState(''); + const [result, setResult] = useState(''); + + const compute = (_values: InitialValuesType, input: string) => { + setResult(validateXml(input, {})); + }; + + return ( + + } + resultComponent={} + initialValues={initialValues} + exampleCards={exampleCards} + getGroups={null} + setInput={setInput} + compute={compute} + toolInfo={{ title: `What is a ${title}?`, description: longDescription }} + /> + ); +} diff --git a/src/pages/tools/xml/xml-validator/meta.ts b/src/pages/tools/xml/xml-validator/meta.ts new file mode 100644 index 0000000..d3d2a1c --- /dev/null +++ b/src/pages/tools/xml/xml-validator/meta.ts @@ -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')) +}); diff --git a/src/pages/tools/xml/xml-validator/service.ts b/src/pages/tools/xml/xml-validator/service.ts new file mode 100644 index 0000000..fae2aff --- /dev/null +++ b/src/pages/tools/xml/xml-validator/service.ts @@ -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'; + } +} diff --git a/src/pages/tools/xml/xml-validator/types.ts b/src/pages/tools/xml/xml-validator/types.ts new file mode 100644 index 0000000..d4135c9 --- /dev/null +++ b/src/pages/tools/xml/xml-validator/types.ts @@ -0,0 +1,3 @@ +export type InitialValuesType = { + // splitSeparator: string; +}; diff --git a/src/pages/tools/xml/xml-validator/xml-validator.service.test.ts b/src/pages/tools/xml/xml-validator/xml-validator.service.test.ts new file mode 100644 index 0000000..5440d32 --- /dev/null +++ b/src/pages/tools/xml/xml-validator/xml-validator.service.test.ts @@ -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 = '12'; + const result = validateXml(input, {}); + expect(result).toBe('Valid XML'); + }); + + it('returns error for invalid XML', () => { + const input = '1'; + const result = validateXml(input, {}); + expect(result).toMatch(/Invalid XML/i); + }); +}); diff --git a/src/pages/tools/xml/xml-viewer/index.tsx b/src/pages/tools/xml/xml-viewer/index.tsx new file mode 100644 index 0000000..c6262d6 --- /dev/null +++ b/src/pages/tools/xml/xml-viewer/index.tsx @@ -0,0 +1,54 @@ +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[] = [ + { + title: 'Pretty Print XML', + description: 'View and pretty-print a compact XML string.', + sampleText: '12', + sampleResult: `\n 1\n 2\n`, + sampleOptions: {} + } +]; + +export default function XmlViewer({ + title, + longDescription +}: ToolComponentProps) { + const [input, setInput] = useState(''); + const [result, setResult] = useState(''); + + const compute = (_values: InitialValuesType, input: string) => { + setResult(prettyPrintXml(input, {})); + }; + + return ( + + } + resultComponent={} + initialValues={initialValues} + exampleCards={exampleCards} + getGroups={null} + setInput={setInput} + compute={compute} + toolInfo={{ title: `What is a ${title}?`, description: longDescription }} + /> + ); +} diff --git a/src/pages/tools/xml/xml-viewer/meta.ts b/src/pages/tools/xml/xml-viewer/meta.ts new file mode 100644 index 0000000..c63e153 --- /dev/null +++ b/src/pages/tools/xml/xml-viewer/meta.ts @@ -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')) +}); diff --git a/src/pages/tools/xml/xml-viewer/service.ts b/src/pages/tools/xml/xml-viewer/service.ts new file mode 100644 index 0000000..ba4e7e3 --- /dev/null +++ b/src/pages/tools/xml/xml-viewer/service.ts @@ -0,0 +1,23 @@ +import { InitialValuesType } from './types'; +import { XMLParser, XMLBuilder, XMLValidator } from 'fast-xml-parser'; + +export function prettyPrintXml( + input: string, + _options: InitialValuesType +): string { + const valid = XMLValidator.validate(input); + if (valid !== true) { + if (typeof valid === 'object' && valid.err) { + return `Invalid XML: ${valid.err.msg} (line ${valid.err.line}, col ${valid.err.col})`; + } + return 'Invalid XML'; + } + 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}`; + } +} diff --git a/src/pages/tools/xml/xml-viewer/types.ts b/src/pages/tools/xml/xml-viewer/types.ts new file mode 100644 index 0000000..d4135c9 --- /dev/null +++ b/src/pages/tools/xml/xml-viewer/types.ts @@ -0,0 +1,3 @@ +export type InitialValuesType = { + // splitSeparator: string; +}; diff --git a/src/pages/tools/xml/xml-viewer/xml-viewer.service.test.ts b/src/pages/tools/xml/xml-viewer/xml-viewer.service.test.ts new file mode 100644 index 0000000..f8e671e --- /dev/null +++ b/src/pages/tools/xml/xml-viewer/xml-viewer.service.test.ts @@ -0,0 +1,18 @@ +import { expect, describe, it } from 'vitest'; +import { prettyPrintXml } from './service'; + +describe('xml-viewer', () => { + it('pretty prints valid XML', () => { + const input = '12'; + const result = prettyPrintXml(input, {}); + expect(result).toContain(''); + expect(result).toContain(' 1'); + expect(result).toContain(' 2'); + }); + + it('returns error for invalid XML', () => { + const input = '1'; + const result = prettyPrintXml(input, {}); + expect(result).toMatch(/Invalid XML/i); + }); +}); diff --git a/src/tools/defineTool.tsx b/src/tools/defineTool.tsx index 57a0e0f..ecb056c 100644 --- a/src/tools/defineTool.tsx +++ b/src/tools/defineTool.tsx @@ -25,7 +25,8 @@ export type ToolCategory = | 'csv' | 'pdf' | 'image-generic' - | 'audio'; + | 'audio' + | 'xml'; export interface DefinedTool { type: ToolCategory; diff --git a/src/tools/index.ts b/src/tools/index.ts index 1cd3684..b0e9bc6 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -12,6 +12,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', @@ -25,6 +26,8 @@ const toolCategoriesOrder: ToolCategory[] = [ 'csv', 'number', 'png', + 'time', + 'xml', 'gif' ]; export const tools: DefinedTool[] = [ @@ -37,7 +40,8 @@ export const tools: DefinedTool[] = [ ...videoTools, ...numberTools, ...timeTools, - ...audioTools + ...audioTools, + ...xmlTools ]; const categoriesConfig: { type: ToolCategory; @@ -124,6 +128,12 @@ const categoriesConfig: { icon: 'ic:twotone-audiotrack', value: 'Tools for working with audio – extract audio from video, adjusting audio speed, merging multiple audio files 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