diff --git a/scripts/extract-elx/.gitignore b/scripts/extract-elx/.gitignore new file mode 100644 index 0000000..5de8473 --- /dev/null +++ b/scripts/extract-elx/.gitignore @@ -0,0 +1,38 @@ +# Logs +logs +*.log +npm-debug.log* + +# Dependency directories +node_modules + +# Build generated files +dist +lib +lib-dts +lib-commonjs +lib-esm +jest-output +release +solution +temp +*.sppkg +.heft + +# Coverage directory used by tools like istanbul +coverage + +# OSX +.DS_Store + +# Visual Studio files +.ntvs_analysis.dat +.vs +bin +obj + +# Resx Generated Code +*.resx.ts + +# Styles Generated Code +*.scss.ts diff --git a/scripts/extract-elx/.vscode/launch.json b/scripts/extract-elx/.vscode/launch.json new file mode 100644 index 0000000..ed63573 --- /dev/null +++ b/scripts/extract-elx/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch extract-elx (clean) with input", + "program": "${workspaceFolder}/extract-elx.js", + "args": [ + "${input:scriptParameter}", + "--clean" + ], + "console": "integratedTerminal", + "skipFiles": [ + "/**" + ] + } + ], + "inputs": [ + { + "id": "scriptParameter", + "type": "promptString", + "description": "Enter parameter for JLX script", + "default": "" + } + ] +} \ No newline at end of file diff --git a/scripts/extract-elx/extract-elx.js b/scripts/extract-elx/extract-elx.js new file mode 100644 index 0000000..d5c5218 --- /dev/null +++ b/scripts/extract-elx/extract-elx.js @@ -0,0 +1,684 @@ +#!/usr/bin/env node + +/** + * extract-elx.js + * + * Extracts root properties from ElixForms .elx JSON files into separate folders + * for better version control and diff visibility. + * + * Usage: + * node extract-elx.js [--overwrite] [--clean] + * + * Output: + * Creates a folder named after the input file (without extension) containing: + * - Subfolders for each root property + * - .xml files for XML strings (pretty-printed) + * - .json files for objects and metadata + * - .txt files for plain text strings + * - _manifest.json with extraction metadata + */ + +import { promises as fs } from 'fs'; +import { join as _join, dirname, basename as _basename, extname } from 'path'; +import { execSync } from 'child_process'; +import { existsSync } from 'fs'; +import crc32 from 'crc-32'; +import xmlFormatter from 'xml-formatter'; +import { XMLParser } from 'fast-xml-parser'; + +// ============================================================================ +// CONFIGURATION +// ============================================================================ + +const CONFIG = { + XML_INDENT: 2, + JSON_INDENT: 2, + MANIFEST_FILE: '_manifest.json', + INDEX_FILE: 'index', + ITEM_PREFIX: 'item', + INTERNALS_SUFFIX: '_internals', + VERSION: '0.0.1', +}; + +// ============================================================================ +// UTILITY FUNCTIONS +// ============================================================================ + +/** + * Detect if a string is XML content + */ +function isXmlContent(str) { + if (typeof str !== 'string') return false; + const trimmed = str.trim(); + return trimmed.startsWith(' true, // Keep all whitespace nodes + }); + return formatted; + } catch (error) { + console.error(`āš ļø Warning: Failed to format XML: ${error.message}`); + // Return original string if formatting fails + return xmlString; + } +} + +/** + * Pretty-print JSON with indentation + */ +function prettifyJson(obj, indent = 2) { + return JSON.stringify(obj, null, indent); +} + +/** + * Escape a string for use in RegExp + */ +/** + * Sanitize a string for safe file names. + */ +function sanitizeFileName(value) { + return value + .trim() + .replace(/\s+/g, '_') + .replace(/[^a-zA-Z0-9._-]/g, '_') + .replace(/_+/g, '_') + .replace(/^[_-]+|[_-]+$/g, ''); +} + +const XML_PARSE_OPTIONS = { + ignoreAttributes: false, + attributeNamePrefix: '@_', + textNodeName: '#text', + parseTagValue: false, + trimValues: false, +}; + +function parseXmlDocument(xmlString) { + if (typeof xmlString !== 'string') return null; + try { + return new XMLParser(XML_PARSE_OPTIONS).parse(xmlString); + } catch (error) { + return null; + } +} + +function getXmlNodeText(node) { + if (node == null) return ''; + if (typeof node === 'string') return node; + if (typeof node === 'object') { + if ('#text' in node) { + return String(node['#text']); + } + if (Object.keys(node).length === 0) { + return ''; + } + } + return ''; +} + +function toArray(value) { + if (value == null) return []; + return Array.isArray(value) ? value : [value]; +} + +/** + * Extract the text content of a named COL element inside a SECTION with a given title. + */ +function extractXmlValueFromSection(xmlString, sectionTitle, colName) { + const document = parseXmlDocument(xmlString); + if (!document || !document.EXT) return ''; + + const sections = toArray(document.EXT.SECTION); + for (const section of sections) { + const titleValue = section['@_title'] || section.title || ''; + if (String(titleValue).trim() === sectionTitle) { + return String(getXmlNodeText(section[colName] || '')).trim(); + } + } + + return ''; +} + +/** + * Compare string values with numeric fallback. + */ +function compareSortKeys(a, b) { + const valueA = `${a}`.trim(); + const valueB = `${b}`.trim(); + + const numericA = parseFloat(valueA); + const numericB = parseFloat(valueB); + + if (!Number.isNaN(numericA) && !Number.isNaN(numericB)) { + return numericA - numericB; + } + + return valueA.localeCompare(valueB, undefined, { numeric: true, sensitivity: 'base' }); +} + +/** + * Determine a sort key for a property array item. + */ +function getArraySortKey(propertyName, item) { + if (propertyName === 'moduleConfig' && typeof item === 'string') { + return extractXmlValueFromSection(item, 'Step', 'COL0005'); + } + if (propertyName === 'moduleTab' && typeof item === 'string') { + return extractXmlValueFromSection(item, 'Config', 'COL0006'); + } + + return null; +} + +/** + * Get the output file name for a property array item. + */ +function getArrayItemName(propertyName, item, index, totalCount) { + var itemSuffix = null; + + if (propertyName === 'formSchemaData' && item && typeof item === 'object' && item.id != null) { + itemSuffix = String(item.id); + } + + if (propertyName === 'moduleConfig' && typeof item === 'string') { + const rawOrderValue = extractXmlValueFromSection(item, 'Step', 'COL0005'); + if (rawOrderValue) { + const sanitized = sanitizeFileName(rawOrderValue); + if (sanitized) { + itemSuffix = sanitized; + } + } + } + + if (propertyName === 'moduleTab' && typeof item === 'string') { + const rawOrderValue = extractXmlValueFromSection(item, 'Config', 'COL0006'); + if (rawOrderValue) { + const sanitized = sanitizeFileName(rawOrderValue); + if (sanitized) { + itemSuffix = sanitized; + } + } + } + + if (propertyName === 'moduleUserValidation' && typeof item === 'string') { + const rawOrderValue = extractXmlValueFromSection(item, 'System', 'COL0001'); + if (rawOrderValue) { + const sanitized = sanitizeFileName(rawOrderValue); + if (sanitized) { + itemSuffix = sanitized; + } + } + } + + if (propertyName === 'moduleValidation' && typeof item === 'string') { + const rawOrderValue = extractXmlValueFromSection(item, 'System', 'COL0004'); + if (rawOrderValue) { + const sanitized = sanitizeFileName(rawOrderValue); + if (sanitized) { + itemSuffix = sanitized; + } + } + } + + if (propertyName === 'translationsTabMapper' && item && typeof item === 'object' && item.order != null) { + itemSuffix = String(item.order); + } + + if (propertyName === 'validationTypes' && typeof item === 'string') { + const rawOrderValue = extractXmlValueFromSection(item, 'Dati', 'COL0003'); + if (rawOrderValue) { + const sanitized = sanitizeFileName(rawOrderValue); + if (sanitized) { + itemSuffix = sanitized; + } + } + } + + if (propertyName === 'translationsValidationTabMapper' && item && typeof item === 'object' && item.code != null) { + itemSuffix = String(item.code); + } + + if (!itemSuffix) { + itemSuffix = padIndex(index, totalCount); + } + + return `${propertyName}_${CONFIG.ITEM_PREFIX}_${itemSuffix}`; +} + +/** + * Get file extension based on content type + */ +function getFileExtension(content) { + if (typeof content === 'string') { + return isXmlContent(content) ? 'xml' : 'txt'; + } + return 'json'; +} + +/** + * Pad number with zeros for consistent file naming + */ +function padIndex(index, totalCount) { + const maxDigits = totalCount.toString().length; + return index.toString().padStart(maxDigits, '0'); +} + +/** + * Format file size for display + */ +function formatFileSize(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +// ============================================================================ +// FILE OPERATIONS +// ============================================================================ + +async function calculateFileCrc32(filePath) { + try { + const buffer = await fs.readFile(filePath); + return (crc32.buf(buffer) >>> 0).toString(16).toUpperCase().padStart(8, '0'); + } catch (error) { + throw new Error(`Failed to calculate CRC32: ${error.message}`); + } +} + +/** + * Read and parse the input JSON file + */ +async function readInputFile(filePath) { + try { + const content = await fs.readFile(filePath, 'utf-8'); + const data = JSON.parse(content); + return data; + } catch (error) { + if (error instanceof SyntaxError) { + throw new Error(`Invalid JSON: ${error.message}`); + } + throw new Error(`Failed to read file: ${error.message}`); + } +} + +/** + * Create output directory for extracted properties + */ +async function createOutputDirectory(outputPath, allowOverwrite = false, cleanExisting = false) { + if (!allowOverwrite && !cleanExisting && existsSync(outputPath)) { + throw new Error( + `Output directory already exists: ${outputPath}\n` + + `Use --overwrite flag to replace it or --clean flag to remove its contents.` + ); + } + + // Optionally remove existing directory + if (cleanExisting) { + console.log(` Removing existing directory contents: ${outputPath}`); + await fs.rm(`${outputPath}`, { recursive: true, force: true }); + } else if (allowOverwrite) { + console.log(` Overwriting existing directory: ${outputPath}`); + } + + try { + await fs.mkdir(outputPath, { recursive: true }); + } catch (error) { + throw new Error(`Failed to create output directory: ${error.message}`); + } +} + +/** + * Save a string to file with appropriate extension + */ +async function saveStringProperty(dirPath, filename, content, options = {}) { + const ext = getFileExtension(content); + const filepath = _join(dirPath, `${filename}.${ext}`); + + let output = content; + + // Pretty-print XML if detected + if (ext === 'xml' && !options.skipFormat) { + output = prettifyXml(content, CONFIG.XML_INDENT); + } + + await fs.writeFile(filepath, output, 'utf-8'); + return { filepath, size: output.length, type: ext }; +} + +/** + * Save a JSON object to file + */ +async function saveJsonProperty(dirPath, filename, obj, options = {}) { + const filepath = _join(dirPath, `${filename}.json`); + const output = prettifyJson(obj, CONFIG.JSON_INDENT); + + await fs.writeFile(filepath, output, 'utf-8'); + return { filepath, size: output.length, type: 'json' }; +} + +/** + * Create subdirectory for a property + */ +async function createPropertyDirectory(outputPath, propertyName) { + const dirPath = _join(outputPath, propertyName); + await fs.mkdir(dirPath, { recursive: true }); + return dirPath; +} + +/** + * Save backup of original file + */ +async function saveBackup(outputPath, data) { + const backupPath = _join(outputPath, CONFIG.BACKUP_SUFFIX); + const output = prettifyJson(data, CONFIG.JSON_INDENT); + await fs.writeFile(backupPath, output, 'utf-8'); + return backupPath; +} + +/** + * Create CRC32 checksum file for integrity verification + */ +async function createCrc32Checksum(outputPath, inputPath) { + const fileCrc32 = await calculateFileCrc32(inputPath); + const checksumPath = _join(outputPath, `crc32.txt`); + await fs.writeFile(checksumPath, `${fileCrc32}\n`, 'utf-8'); + return { checksumPath, fileCrc32 }; +} + +// ============================================================================ +// PROPERTY PROCESSING +// ============================================================================ + +/** + * Process a string property + */ +async function processStringProperty(outputPath, propertyName, content) { + const result = { + property: propertyName, + type: 'string', + contentType: isXmlContent(content) ? 'xml' : 'text', + size: content.length, + files: [], + }; + + const dirPath = await createPropertyDirectory(outputPath, propertyName); + const fileInfo = await saveStringProperty(dirPath, CONFIG.INDEX_FILE, content); + result.files.push(fileInfo.filepath); + + return result; +} + +/** + * Process an array property + */ +async function processArrayProperty(outputPath, propertyName, arr) { + const result = { + property: propertyName, + type: 'array', + itemCount: arr.length, + itemTypes: {}, + files: [], + }; + + const dirPath = await createPropertyDirectory(outputPath, propertyName); + + // Create index.json with metadata + const indexData = { + count: arr.length, + itemTypes: [], + extractedAt: new Date().toISOString(), + order: propertyName, + }; + + const items = arr.map((item, originalIndex) => ({ + item, + originalIndex, + sortKey: getArraySortKey(propertyName, item), + })); + + const hasSortKey = items.some(entry => entry.sortKey !== null && entry.sortKey !== ''); + if (hasSortKey) { + items.sort((a, b) => { + const keyA = a.sortKey ?? ''; + const keyB = b.sortKey ?? ''; + const compare = compareSortKeys(keyA, keyB); + return compare !== 0 ? compare : a.originalIndex - b.originalIndex; + }); + } + + const usedNames = new Set(); + + for (let sortedIndex = 0; sortedIndex < items.length; sortedIndex++) { + const { item, originalIndex } = items[sortedIndex]; + const itemNameBase = getArrayItemName(propertyName, item, sortedIndex, arr.length); + let itemName = itemNameBase; + let duplicateCounter = 1; + + while (usedNames.has(itemName)) { + itemName = `${itemNameBase}_${duplicateCounter}`; + duplicateCounter += 1; + } + usedNames.add(itemName); + + const itemType = Array.isArray(item) ? 'array' : typeof item; + + // Track item type distribution + if (!result.itemTypes[itemType]) { + result.itemTypes[itemType] = 0; + } + result.itemTypes[itemType]++; + + let fileInfo; + if (typeof item === 'string') { + fileInfo = await saveStringProperty(dirPath, itemName, item); + indexData.itemTypes.push({ index: originalIndex, type: fileInfo.type, fileName: `${itemName}.${fileInfo.type}` }); + } else if (typeof item === 'object' && item !== null) { + fileInfo = await saveJsonProperty(dirPath, itemName, item); + indexData.itemTypes.push({ index: originalIndex, type: 'json', fileName: `${itemName}.json` }); + // Additional processing for some items (e.g. extract XML from strings inside objects) + for (const [key, value] of Object.entries(item)) { + if (typeof value === 'string' && isXmlContent(value)) { + const xmlFileName = `${itemName}_${key}`; + const xmlFileInfo = await saveStringProperty(dirPath, xmlFileName, value); + result.files.push(xmlFileInfo.filepath); + } + } + } else { + fileInfo = await saveJsonProperty(dirPath, itemName, item); + indexData.itemTypes.push({ index: originalIndex, type: typeof item, fileName: `${itemName}.json` }); + } + + result.files.push(fileInfo.filepath); + } + + // Save index metadata + const indexPath = _join(dirPath, `${CONFIG.INDEX_FILE}.json`); + const indexOutput = prettifyJson(indexData, CONFIG.JSON_INDENT); + await fs.writeFile(indexPath, indexOutput, 'utf-8'); + result.files.unshift(indexPath); // Put index first in list + + return result; +} + +/** + * Process an object property + */ +async function processObjectProperty(outputPath, propertyName, obj) { + const result = { + property: propertyName, + type: 'object', + keyCount: Object.keys(obj).length, + files: [], + }; + + const dirPath = await createPropertyDirectory(outputPath, propertyName); + const filepath = _join(dirPath, `${CONFIG.INDEX_FILE}.json`); + const output = prettifyJson(obj, CONFIG.JSON_INDENT); + + await fs.writeFile(filepath, output, 'utf-8'); + result.files.push(filepath); + + return result; +} + +/** + * Process a single root property + */ +async function processProperty(outputPath, propertyName, value) { + if (typeof value === 'string') { + return await processStringProperty(outputPath, propertyName, value); + } else if (Array.isArray(value)) { + return await processArrayProperty(outputPath, propertyName, value); + } else if (typeof value === 'object' && value !== null) { + return await processObjectProperty(outputPath, propertyName, value); + } else { + // Primitive value (number, boolean, null) + const result = { + property: propertyName, + type: typeof value, + value: value, + files: [], + }; + + const dirPath = await createPropertyDirectory(outputPath, propertyName); + const filepath = _join(dirPath, `${CONFIG.INDEX_FILE}.json`); + await fs.writeFile(filepath, prettifyJson(value, CONFIG.JSON_INDENT), 'utf-8'); + result.files.push(filepath); + + return result; + } +} + +// ============================================================================ +// MANIFEST GENERATION +// ============================================================================ + +/** + * Create manifest file with extraction metadata + */ +async function createManifest(outputPath, data, processedProperties) { + const manifest = { + extractedAt: new Date().toISOString(), + scriptVersion: CONFIG.VERSION, + properties: {}, + summary: { + totalProperties: processedProperties.length, + totalFiles: processedProperties.reduce((sum, p) => sum + p.files.length, 0), + }, + }; + + for (const prop of processedProperties) { + manifest.properties[prop.property] = { + type: prop.type, + ...(prop.itemCount !== undefined && { itemCount: prop.itemCount }), + ...(prop.itemTypes && Object.keys(prop.itemTypes).length > 0 && { itemTypes: prop.itemTypes }), + ...(prop.keyCount !== undefined && { keyCount: prop.keyCount }), + fileCount: prop.files.length, + }; + } + + const manifestPath = _join(outputPath, CONFIG.MANIFEST_FILE); + const output = prettifyJson(manifest, CONFIG.JSON_INDENT); + await fs.writeFile(manifestPath, output, 'utf-8'); + + return manifestPath; +} + +// ============================================================================ +// MAIN EXECUTION +// ============================================================================ + +async function main() { + try { + // Parse command-line arguments + const args = process.argv.slice(2); + + if (args.length === 0) { + console.error('Usage: node extract-elx.js [--overwrite] [--clean]'); + console.error('Example: node extract-elx.js elxforms_4951.elx'); + process.exit(1); + } + + const inputPath = args[0]; + const allowOverwrite = args.includes('--overwrite'); + const cleanExisting = args.includes('--clean'); + + // Validate input file + if (!existsSync(inputPath)) { + throw new Error(`File not found: ${inputPath}`); + } + + // Determine output directory + const dir = dirname(inputPath); + const basename = _basename(inputPath, extname(inputPath)); + const outputPath = _join(dir, `${basename}${CONFIG.INTERNALS_SUFFIX}`); + + console.log('\nšŸ“ ElixForms JSON Extractor\n'); + console.log(`šŸ“– Reading: ${inputPath}`); + + // Read input file + const data = await readInputFile(inputPath); + const fileStats = await fs.stat(inputPath); + console.log(` Size: ${formatFileSize(fileStats.size)}`); + + // Create output directory + console.log(`\nšŸ“‚ Creating output: ${outputPath}`); + await createOutputDirectory(outputPath, allowOverwrite, cleanExisting); + + // Create CRC32 checksum file for integrity verification + console.log(`\n🐾 Creating checksum file...`); + const crc32 = await createCrc32Checksum(outputPath, inputPath); + console.log(`\n Checksum file: ${crc32.checksumPath}`); + //console.log(` CRC32: ${crc32.fileCrc32}`); + + // Process each root property + console.log('\nāš™ļø Processing properties:\n'); + const processedProperties = []; + const rootProps = Object.keys(data); + + for (const propName of rootProps) { + process.stdout.write(` ${propName}... `); + const propResult = await processProperty(outputPath, propName, data[propName]); + processedProperties.push(propResult); + + let typeLabel = propResult.type; + if (propResult.itemCount !== undefined) { + typeLabel += ` (${propResult.itemCount} items)`; + } else if (propResult.keyCount !== undefined) { + typeLabel += ` (${propResult.keyCount} keys)`; + } + + console.log(`āœ“ ${typeLabel} → ${propResult.files.length} file(s)`); + } + + // Create manifest + console.log('\nšŸ“‹ Creating manifest...'); + await createManifest(outputPath, data, processedProperties); + + // Summary + const totalFiles = processedProperties.reduce((sum, p) => sum + p.files.length, 0); + console.log(`\nāœ… Extraction complete!\n`); + console.log(` Properties extracted: ${rootProps.length}`); + console.log(` Total files created: ${totalFiles + 1} (includes manifest)\n`); + + console.log(`šŸ“ Output location: ${outputPath}\n`); + + } catch (error) { + console.error(`\nāŒ Error: ${error.message}\n`); + process.exit(1); + } +} + +// Run main function +main(); diff --git a/scripts/extract-elx/package-lock.json b/scripts/extract-elx/package-lock.json new file mode 100644 index 0000000..f070112 --- /dev/null +++ b/scripts/extract-elx/package-lock.json @@ -0,0 +1,142 @@ +{ + "name": "extract-elx", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "extract-elx", + "version": "0.0.1", + "dependencies": { + "crc-32": "^1.2.2", + "fast-xml-parser": "^5.8.0", + "xml-formatter": "^3.7.0" + } + }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz", + "integrity": "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.2.0", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.3.0", + "xml-naming": "^0.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/xml-formatter": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/xml-formatter/-/xml-formatter-3.7.0.tgz", + "integrity": "sha512-+8qTc3zv2UcJ1v9IsSIce37Dl4MQG14Cp7tWrwmy202UaI1wqRukw5QMX1JHsV+DX64yw77EgGsj2s5wGvuMbQ==", + "license": "MIT", + "dependencies": { + "xml-parser-xo": "^4.1.5" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/xml-parser-xo": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-4.1.5.tgz", + "integrity": "sha512-TxyRxk9sTOUg3glxSIY6f0nfuqRll2OEF8TspLgh5mZkLuBgheCn3zClcDSGJ58TvNmiwyCCuat4UajPud/5Og==", + "license": "MIT", + "engines": { + "node": ">= 16" + } + } + } +} diff --git a/scripts/extract-elx/package.json b/scripts/extract-elx/package.json new file mode 100644 index 0000000..b6c29a0 --- /dev/null +++ b/scripts/extract-elx/package.json @@ -0,0 +1,14 @@ +{ + "name": "extract-elx", + "version": "0.0.1", + "description": "Extract root properties from ElixForms .elx JSON files", + "scripts": { + "start": "node extract-elx.js" + }, + "type": "module", + "dependencies": { + "crc-32": "^1.2.2", + "fast-xml-parser": "^5.8.0", + "xml-formatter": "^3.7.0" + } +}