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..456d9fc --- /dev/null +++ b/src/pages/tools/xml/xml-beautifier/index.tsx @@ -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[] = [ + { + title: 'Beautify XML', + description: 'Beautify a compact XML string for readability.', + sampleText: '12', + sampleResult: `\n 1\n 2\n`, + sampleOptions: {} + } +]; + +const getGroups = () => []; + +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={getGroups} + 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..51371cf --- /dev/null +++ b/src/pages/tools/xml/xml-beautifier/service.ts @@ -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}`; + } +} 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..ac2cb1b --- /dev/null +++ b/src/pages/tools/xml/xml-validator/index.tsx @@ -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[] = [ + { + 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={getGroups} + 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..91aef1f --- /dev/null +++ b/src/pages/tools/xml/xml-viewer/index.tsx @@ -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[] = [ + { + title: 'Pretty Print XML', + description: 'View and pretty-print a compact XML string.', + sampleText: '12', + sampleResult: `\n 1\n 2\n`, + sampleOptions: {} + } +]; + +const getGroups = () => []; + +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={getGroups} + 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..569f6f8 --- /dev/null +++ b/src/pages/tools/xml/xml-viewer/service.ts @@ -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}`; + } +} 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 0ea3935..7e2e6e5 100644 --- a/src/tools/defineTool.tsx +++ b/src/tools/defineTool.tsx @@ -24,7 +24,8 @@ export type ToolCategory = | 'time' | 'csv' | 'pdf' - | 'image-generic'; + | 'image-generic' + | 'xml'; export interface DefinedTool { type: ToolCategory; diff --git a/src/tools/index.ts b/src/tools/index.ts index 08a2cf8..5e4fe33 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -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