From 1b09da3731c67abb1bc3f02a5944e1f536693616 Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Tue, 12 May 2026 14:47:37 +0200 Subject: [PATCH 01/18] add extract-elx.js script to ease elx files versioning --- scripts/extract-elx/extract-elx.js | 626 +++++++++++++++++++++++++++++ 1 file changed, 626 insertions(+) create mode 100644 scripts/extract-elx/extract-elx.js diff --git a/scripts/extract-elx/extract-elx.js b/scripts/extract-elx/extract-elx.js new file mode 100644 index 0000000..f95b0d7 --- /dev/null +++ b/scripts/extract-elx/extract-elx.js @@ -0,0 +1,626 @@ +#!/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] + * + * 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 + * - .elx.original.json backup + */ + +const fs = require('fs').promises; +const path = require('path'); +const { execSync } = require('child_process'); +const { existsSync } = require('fs'); +const { XMLParser } = require('fast-xml-parser'); + +// Import xml-formatter (try multiple locations) +let xmlFormatter; +try { + // Try local node_modules first + xmlFormatter = require('xml-formatter'); +} catch (err) { + try { + // Try to find npm global prefix and load from there + const npmPrefix = execSync('npm config get prefix', { encoding: 'utf-8' }).trim(); + const globalXmlFormatter = path.join(npmPrefix, 'node_modules', 'xml-formatter'); + xmlFormatter = require(globalXmlFormatter); + } catch (err2) { + console.error( + '\nāŒ Missing dependency: xml-formatter\n' + + 'Please install globally with: npm install -g xml-formatter\n' + + 'Or locally in the current directory with: npm install xml-formatter\n' + ); + process.exit(1); + } +} + +// ============================================================================ +// CONFIGURATION +// ============================================================================ + +const CONFIG = { + XML_INDENT: 2, + JSON_INDENT: 2, + BACKUP_SUFFIX: '.elx.original.json', + MANIFEST_FILE: '_manifest.json', + INDEX_FILE: 'index', + ITEM_PREFIX: 'item', + ALLOW_OVERWRITE: false, +}; + +// ============================================================================ +// 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, +}; + +const xmlParser = new XMLParser(XML_PARSE_OPTIONS); + +function parseXmlDocument(xmlString) { + if (typeof xmlString !== 'string') return null; + try { + return xmlParser.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) { + if (propertyName === 'translationsTabMapper' && item && typeof item === 'object' && item.order != null) { + return `${CONFIG.ITEM_PREFIX}_${String(item.order)}`; + } + + if (propertyName === 'formSchemaData' && item && typeof item === 'object' && item.id != null) { + return `schemaId_${String(item.id)}`; + } + + if (propertyName === 'moduleTab' && typeof item === 'string') { + const rawOrderValue = extractXmlValueFromSection(item, 'Config', 'COL0006'); + if (rawOrderValue) { + const sanitized = sanitizeFileName(rawOrderValue); + if (sanitized) { + return `moduleTabItem_${sanitized}`; + } + } + } + + return `${CONFIG.ITEM_PREFIX}_${padIndex(index, totalCount)}`; +} + +/** + * 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 +// ============================================================================ + +/** + * 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) { + if (existsSync(outputPath)) { + if (!allowOverwrite) { + throw new Error( + `Output directory already exists: ${outputPath}\n` + + `Use --overwrite flag to replace it.` + ); + } + // Optionally remove existing directory + console.log(`Removing 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 = path.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 = path.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 = path.join(outputPath, propertyName); + await fs.mkdir(dirPath, { recursive: true }); + return dirPath; +} + +/** + * Save backup of original file + */ +async function saveBackup(outputPath, data) { + const backupPath = path.join(outputPath, CONFIG.BACKUP_SUFFIX); + const output = prettifyJson(data, CONFIG.JSON_INDENT); + await fs.writeFile(backupPath, output, 'utf-8'); + return backupPath; +} + +// ============================================================================ +// 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` }); + } 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 = path.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 = path.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 = path.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: '1.0.0', + 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 = path.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]'); + console.error('Example: node extract-elx.js elxforms_4951.elx'); + process.exit(1); + } + + const inputPath = args[0]; + const allowOverwrite = args.includes('--overwrite'); + + // Validate input file + if (!existsSync(inputPath)) { + throw new Error(`File not found: ${inputPath}`); + } + + // Determine output directory + const dir = path.dirname(inputPath); + const basename = path.basename(inputPath, path.extname(inputPath)); + const outputPath = path.join(dir, basename); + + 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); + + // Save backup + console.log(' Saving backup...'); + await saveBackup(outputPath, data); + + // Process each root property + console.log('\nāš™ļø Processing properties:\n'); + const processedProperties = []; + const rootProps = Object.keys(data).filter(name => name !== 'powershellScript'); + + 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)`); + } + + if (data.hasOwnProperty('powershellScript')) { + console.log(' powershellScript... skipped'); + } + + // 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(); From 0e67c8ed4435e8ac56a0e6644946b610744cf7c8 Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Tue, 12 May 2026 14:57:27 +0200 Subject: [PATCH 02/18] add configuration files for extract-elx.js script development --- scripts/extract-elx/.gitignore | 38 ++++++++++ scripts/extract-elx/package-lock.json | 104 ++++++++++++++++++++++++++ scripts/extract-elx/package.json | 5 ++ 3 files changed, 147 insertions(+) create mode 100644 scripts/extract-elx/.gitignore create mode 100644 scripts/extract-elx/package-lock.json create mode 100644 scripts/extract-elx/package.json 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/package-lock.json b/scripts/extract-elx/package-lock.json new file mode 100644 index 0000000..609a5b2 --- /dev/null +++ b/scripts/extract-elx/package-lock.json @@ -0,0 +1,104 @@ +{ + "name": "extract-elx", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "fast-xml-parser": "^5.8.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/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-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" + } + } + } +} diff --git a/scripts/extract-elx/package.json b/scripts/extract-elx/package.json new file mode 100644 index 0000000..09d099d --- /dev/null +++ b/scripts/extract-elx/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "fast-xml-parser": "^5.8.0" + } +} From 787c53858b6d80549c7889dab904b00f499242bf Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Tue, 12 May 2026 14:58:31 +0200 Subject: [PATCH 03/18] update file naming for moduleTab items --- scripts/extract-elx/extract-elx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/extract-elx/extract-elx.js b/scripts/extract-elx/extract-elx.js index f95b0d7..0bed60e 100644 --- a/scripts/extract-elx/extract-elx.js +++ b/scripts/extract-elx/extract-elx.js @@ -218,7 +218,7 @@ function getArrayItemName(propertyName, item, index, totalCount) { if (rawOrderValue) { const sanitized = sanitizeFileName(rawOrderValue); if (sanitized) { - return `moduleTabItem_${sanitized}`; + return `moduleTab_item_${sanitized}`; } } } From 63adeedf82fa01f6c0181f7ed3f83b5a84a127ee Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Tue, 12 May 2026 14:58:51 +0200 Subject: [PATCH 04/18] update file naming for formSchemaData items --- scripts/extract-elx/extract-elx.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/extract-elx/extract-elx.js b/scripts/extract-elx/extract-elx.js index 0bed60e..9b9d7af 100644 --- a/scripts/extract-elx/extract-elx.js +++ b/scripts/extract-elx/extract-elx.js @@ -210,7 +210,17 @@ function getArrayItemName(propertyName, item, index, totalCount) { } if (propertyName === 'formSchemaData' && item && typeof item === 'object' && item.id != null) { - return `schemaId_${String(item.id)}`; + return `formSchemaData_item_${String(item.id)}`; + } + + if (propertyName === 'moduleConfig' && typeof item === 'string') { + const rawOrderValue = extractXmlValueFromSection(item, 'Step', 'COL0005'); + if (rawOrderValue) { + const sanitized = sanitizeFileName(rawOrderValue); + if (sanitized) { + return `moduleConfig_item_${sanitized}`; + } + } } if (propertyName === 'moduleTab' && typeof item === 'string') { From 3bf5a6893e2210f1569f423c1c5cb06ed2383081 Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Tue, 12 May 2026 15:20:38 +0200 Subject: [PATCH 05/18] clean up item naming code --- scripts/extract-elx/extract-elx.js | 36 +++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/scripts/extract-elx/extract-elx.js b/scripts/extract-elx/extract-elx.js index 9b9d7af..de187f9 100644 --- a/scripts/extract-elx/extract-elx.js +++ b/scripts/extract-elx/extract-elx.js @@ -205,12 +205,14 @@ function getArraySortKey(propertyName, item) { * Get the output file name for a property array item. */ function getArrayItemName(propertyName, item, index, totalCount) { + var itemSuffix = null; + if (propertyName === 'translationsTabMapper' && item && typeof item === 'object' && item.order != null) { - return `${CONFIG.ITEM_PREFIX}_${String(item.order)}`; + itemSuffix = String(item.order); } if (propertyName === 'formSchemaData' && item && typeof item === 'object' && item.id != null) { - return `formSchemaData_item_${String(item.id)}`; + itemSuffix = String(item.id); } if (propertyName === 'moduleConfig' && typeof item === 'string') { @@ -218,7 +220,7 @@ function getArrayItemName(propertyName, item, index, totalCount) { if (rawOrderValue) { const sanitized = sanitizeFileName(rawOrderValue); if (sanitized) { - return `moduleConfig_item_${sanitized}`; + itemSuffix = sanitized; } } } @@ -228,12 +230,36 @@ function getArrayItemName(propertyName, item, index, totalCount) { if (rawOrderValue) { const sanitized = sanitizeFileName(rawOrderValue); if (sanitized) { - return `moduleTab_item_${sanitized}`; + itemSuffix = sanitized; } } } - return `${CONFIG.ITEM_PREFIX}_${padIndex(index, totalCount)}`; + 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', 'COL0003'); + if (rawOrderValue) { + const sanitized = sanitizeFileName(rawOrderValue); + if (sanitized) { + itemSuffix = sanitized; + } + } + } + + if (!itemSuffix) { + itemSuffix = padIndex(index, totalCount); + } + + return `${propertyName}_${CONFIG.ITEM_PREFIX}_${itemSuffix}`; } /** From 25d2f24cadba69e2df3db265ba4dd2866c862d8c Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Tue, 12 May 2026 15:26:29 +0200 Subject: [PATCH 06/18] remove backup creation of original file clean up internal configuration --- scripts/extract-elx/extract-elx.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/scripts/extract-elx/extract-elx.js b/scripts/extract-elx/extract-elx.js index de187f9..9ea1354 100644 --- a/scripts/extract-elx/extract-elx.js +++ b/scripts/extract-elx/extract-elx.js @@ -16,7 +16,6 @@ * - .json files for objects and metadata * - .txt files for plain text strings * - _manifest.json with extraction metadata - * - .elx.original.json backup */ const fs = require('fs').promises; @@ -53,11 +52,9 @@ try { const CONFIG = { XML_INDENT: 2, JSON_INDENT: 2, - BACKUP_SUFFIX: '.elx.original.json', MANIFEST_FILE: '_manifest.json', INDEX_FILE: 'index', - ITEM_PREFIX: 'item', - ALLOW_OVERWRITE: false, + ITEM_PREFIX: 'item' }; // ============================================================================ @@ -612,10 +609,6 @@ async function main() { console.log(`\nšŸ“‚ Creating output: ${outputPath}`); await createOutputDirectory(outputPath, allowOverwrite); - // Save backup - console.log(' Saving backup...'); - await saveBackup(outputPath, data); - // Process each root property console.log('\nāš™ļø Processing properties:\n'); const processedProperties = []; From a28e2968f8b9c3738369eb4c620fa479ae721802 Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Tue, 12 May 2026 15:26:52 +0200 Subject: [PATCH 07/18] add more custom file namings --- scripts/extract-elx/extract-elx.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/scripts/extract-elx/extract-elx.js b/scripts/extract-elx/extract-elx.js index 9ea1354..6242832 100644 --- a/scripts/extract-elx/extract-elx.js +++ b/scripts/extract-elx/extract-elx.js @@ -204,10 +204,6 @@ function getArraySortKey(propertyName, item) { function getArrayItemName(propertyName, item, index, totalCount) { var itemSuffix = null; - if (propertyName === 'translationsTabMapper' && item && typeof item === 'object' && item.order != null) { - itemSuffix = String(item.order); - } - if (propertyName === 'formSchemaData' && item && typeof item === 'object' && item.id != null) { itemSuffix = String(item.id); } @@ -252,6 +248,24 @@ function getArrayItemName(propertyName, item, index, totalCount) { } } + 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); } From f4da370e0853786870471d7c4a7b6d47b5f17b74 Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Tue, 12 May 2026 15:36:09 +0200 Subject: [PATCH 08/18] add fast-xml-parser dependency management --- scripts/extract-elx/extract-elx.js | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/scripts/extract-elx/extract-elx.js b/scripts/extract-elx/extract-elx.js index 6242832..c950e47 100644 --- a/scripts/extract-elx/extract-elx.js +++ b/scripts/extract-elx/extract-elx.js @@ -22,7 +22,6 @@ const fs = require('fs').promises; const path = require('path'); const { execSync } = require('child_process'); const { existsSync } = require('fs'); -const { XMLParser } = require('fast-xml-parser'); // Import xml-formatter (try multiple locations) let xmlFormatter; @@ -45,6 +44,27 @@ try { } } +// Import fast-xml-parser (try multiple locations) +let fastXmlParser; +try { + // Try local node_modules first + fastXmlParser = require('fast-xml-parser'); +} catch (err) { + try { + // Try to find npm global prefix and load from there + const npmPrefix = execSync('npm config get prefix', { encoding: 'utf-8' }).trim(); + const globalFastXmlParser = path.join(npmPrefix, 'node_modules', 'fast-xml-parser'); + fastXmlParser = require(globalFastXmlParser); + } catch (err2) { + console.error( + '\nāŒ Missing dependency: fast-xml-parser\n' + + 'Please install globally with: npm install -g fast-xml-parser\n' + + 'Or locally in the current directory with: npm install fast-xml-parser\n' + ); + process.exit(1); + } +} + // ============================================================================ // CONFIGURATION // ============================================================================ @@ -119,12 +139,10 @@ const XML_PARSE_OPTIONS = { trimValues: false, }; -const xmlParser = new XMLParser(XML_PARSE_OPTIONS); - function parseXmlDocument(xmlString) { if (typeof xmlString !== 'string') return null; try { - return xmlParser.parse(xmlString); + return new fastXmlParser.XMLParser(XML_PARSE_OPTIONS).parse(xmlString); } catch (error) { return null; } From 1ee3b709108628e9ea6a71a797badcc07339a7b9 Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Tue, 12 May 2026 15:41:18 +0200 Subject: [PATCH 09/18] add suffix to internals folder --- scripts/extract-elx/extract-elx.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/extract-elx/extract-elx.js b/scripts/extract-elx/extract-elx.js index c950e47..d1564f4 100644 --- a/scripts/extract-elx/extract-elx.js +++ b/scripts/extract-elx/extract-elx.js @@ -74,7 +74,8 @@ const CONFIG = { JSON_INDENT: 2, MANIFEST_FILE: '_manifest.json', INDEX_FILE: 'index', - ITEM_PREFIX: 'item' + ITEM_PREFIX: 'item', + INTERNALS_SUFFIX: '_internals' }; // ============================================================================ @@ -627,7 +628,7 @@ async function main() { // Determine output directory const dir = path.dirname(inputPath); const basename = path.basename(inputPath, path.extname(inputPath)); - const outputPath = path.join(dir, basename); + const outputPath = path.join(dir, `${basename}${CONFIG.INTERNALS_SUFFIX}`); console.log('\nšŸ“ ElixForms JSON Extractor\n'); console.log(`šŸ“– Reading: ${inputPath}`); From b509637bbb9e6e4f558897a1c6e2b2c8b0811708 Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Tue, 12 May 2026 16:05:25 +0200 Subject: [PATCH 10/18] add clean option to CLI --- scripts/extract-elx/extract-elx.js | 31 +++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/scripts/extract-elx/extract-elx.js b/scripts/extract-elx/extract-elx.js index d1564f4..aa12a8d 100644 --- a/scripts/extract-elx/extract-elx.js +++ b/scripts/extract-elx/extract-elx.js @@ -7,7 +7,7 @@ * for better version control and diff visibility. * * Usage: - * node extract-elx.js [--overwrite] + * node extract-elx.js [--overwrite] [--clean] * * Output: * Creates a folder named after the input file (without extension) containing: @@ -344,16 +344,20 @@ async function readInputFile(filePath) { /** * Create output directory for extracted properties */ -async function createOutputDirectory(outputPath, allowOverwrite = false) { - if (existsSync(outputPath)) { - if (!allowOverwrite) { - throw new Error( - `Output directory already exists: ${outputPath}\n` + - `Use --overwrite flag to replace it.` - ); - } - // Optionally remove existing directory - console.log(`Removing existing directory: ${outputPath}`); +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 { @@ -612,13 +616,14 @@ async function main() { const args = process.argv.slice(2); if (args.length === 0) { - console.error('Usage: node extract-elx.js [--overwrite]'); + 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)) { @@ -640,7 +645,7 @@ async function main() { // Create output directory console.log(`\nšŸ“‚ Creating output: ${outputPath}`); - await createOutputDirectory(outputPath, allowOverwrite); + await createOutputDirectory(outputPath, allowOverwrite, cleanExisting); // Process each root property console.log('\nāš™ļø Processing properties:\n'); From e55f3b2a4b43172800768db6591a0bc3f7ecab80 Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Tue, 12 May 2026 16:05:49 +0200 Subject: [PATCH 11/18] fix file naming of moduleValidation items --- scripts/extract-elx/extract-elx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/extract-elx/extract-elx.js b/scripts/extract-elx/extract-elx.js index aa12a8d..13e18db 100644 --- a/scripts/extract-elx/extract-elx.js +++ b/scripts/extract-elx/extract-elx.js @@ -258,7 +258,7 @@ function getArrayItemName(propertyName, item, index, totalCount) { } if (propertyName === 'moduleValidation' && typeof item === 'string') { - const rawOrderValue = extractXmlValueFromSection(item, 'System', 'COL0003'); + const rawOrderValue = extractXmlValueFromSection(item, 'System', 'COL0004'); if (rawOrderValue) { const sanitized = sanitizeFileName(rawOrderValue); if (sanitized) { From f87b3639631dabd59d7ee31837ddb513148db712 Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Wed, 13 May 2026 15:23:35 +0200 Subject: [PATCH 12/18] add crc-32 package and rebuild package lock files --- scripts/extract-elx/extract-elx.js | 21 +++++++++++++++ scripts/extract-elx/package-lock.json | 37 ++++++++++++++++++++++++++- scripts/extract-elx/package.json | 4 ++- 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/scripts/extract-elx/extract-elx.js b/scripts/extract-elx/extract-elx.js index 13e18db..f7c37ca 100644 --- a/scripts/extract-elx/extract-elx.js +++ b/scripts/extract-elx/extract-elx.js @@ -65,6 +65,27 @@ try { } } +// Import crc-32 for file integrity checks (try multiple locations) +let crc32; +try { + // Try local node_modules first + crc32 = require('crc-32'); +} catch (err) { + try { + // Try to find npm global prefix and load from there + const npmPrefix = execSync('npm config get prefix', { encoding: 'utf-8' }).trim(); + const globalCrc32 = join(npmPrefix, 'node_modules', 'crc-32'); + crc32 = require(globalCrc32); + } catch (err2) { + console.error( + '\nāŒ Missing dependency: crc-32\n' + + 'Please install globally with: npm install -g crc-32\n' + + 'Or locally in the current directory with: npm install crc-32\n' + ); + process.exit(1); + } +} + // ============================================================================ // CONFIGURATION // ============================================================================ diff --git a/scripts/extract-elx/package-lock.json b/scripts/extract-elx/package-lock.json index 609a5b2..45c9187 100644 --- a/scripts/extract-elx/package-lock.json +++ b/scripts/extract-elx/package-lock.json @@ -5,7 +5,9 @@ "packages": { "": { "dependencies": { - "fast-xml-parser": "^5.8.0" + "crc-32": "^1.2.2", + "fast-xml-parser": "^5.8.0", + "xml-formatter": "^3.7.0" } }, "node_modules/@nodable/entities": { @@ -20,6 +22,18 @@ ], "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", @@ -85,6 +99,18 @@ ], "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", @@ -99,6 +125,15 @@ "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 index 09d099d..8faa62d 100644 --- a/scripts/extract-elx/package.json +++ b/scripts/extract-elx/package.json @@ -1,5 +1,7 @@ { "dependencies": { - "fast-xml-parser": "^5.8.0" + "crc-32": "^1.2.2", + "fast-xml-parser": "^5.8.0", + "xml-formatter": "^3.7.0" } } From 930fa30e2d39983d60dac0960dc67708e5b7c858 Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Wed, 13 May 2026 15:24:03 +0200 Subject: [PATCH 13/18] move script version to config struct --- scripts/extract-elx/extract-elx.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/extract-elx/extract-elx.js b/scripts/extract-elx/extract-elx.js index f7c37ca..8e85d81 100644 --- a/scripts/extract-elx/extract-elx.js +++ b/scripts/extract-elx/extract-elx.js @@ -96,7 +96,8 @@ const CONFIG = { MANIFEST_FILE: '_manifest.json', INDEX_FILE: 'index', ITEM_PREFIX: 'item', - INTERNALS_SUFFIX: '_internals' + INTERNALS_SUFFIX: '_internals', + VERSION: '1.0.0' }; // ============================================================================ @@ -602,7 +603,7 @@ async function processProperty(outputPath, propertyName, value) { async function createManifest(outputPath, data, processedProperties) { const manifest = { extractedAt: new Date().toISOString(), - scriptVersion: '1.0.0', + scriptVersion: CONFIG.VERSION, properties: {}, summary: { totalProperties: processedProperties.length, From 3e65395116a725a3665f35fa8eb30e8fc279d107 Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Wed, 13 May 2026 15:24:35 +0200 Subject: [PATCH 14/18] remove unneeded powershellScript folder references --- scripts/extract-elx/extract-elx.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/scripts/extract-elx/extract-elx.js b/scripts/extract-elx/extract-elx.js index 8e85d81..5c34317 100644 --- a/scripts/extract-elx/extract-elx.js +++ b/scripts/extract-elx/extract-elx.js @@ -672,7 +672,7 @@ async function main() { // Process each root property console.log('\nāš™ļø Processing properties:\n'); const processedProperties = []; - const rootProps = Object.keys(data).filter(name => name !== 'powershellScript'); + const rootProps = Object.keys(data); for (const propName of rootProps) { process.stdout.write(` ${propName}... `); @@ -689,10 +689,6 @@ async function main() { console.log(`āœ“ ${typeLabel} → ${propResult.files.length} file(s)`); } - if (data.hasOwnProperty('powershellScript')) { - console.log(' powershellScript... skipped'); - } - // Create manifest console.log('\nšŸ“‹ Creating manifest...'); await createManifest(outputPath, data, processedProperties); From 29d7ea2a7a6fad7955a08d62dfcb0ec294c9d3e6 Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Wed, 13 May 2026 15:45:49 +0200 Subject: [PATCH 15/18] add crc32 checksum generation of original file clean up log statements --- scripts/extract-elx/extract-elx.js | 33 ++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/scripts/extract-elx/extract-elx.js b/scripts/extract-elx/extract-elx.js index 5c34317..bf9ba96 100644 --- a/scripts/extract-elx/extract-elx.js +++ b/scripts/extract-elx/extract-elx.js @@ -347,6 +347,15 @@ function formatFileSize(bytes) { // 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 */ @@ -376,10 +385,10 @@ async function createOutputDirectory(outputPath, allowOverwrite = false, cleanEx // Optionally remove existing directory if (cleanExisting) { - console.log(`Removing existing directory contents: ${outputPath}`); + console.log(` Removing existing directory contents: ${outputPath}`); await fs.rm(`${outputPath}`, { recursive: true, force: true }); } else if (allowOverwrite) { - console.log(`Overwriting existing directory: ${outputPath}`); + console.log(` Overwriting existing directory: ${outputPath}`); } try { @@ -437,6 +446,16 @@ async function saveBackup(outputPath, data) { return backupPath; } +/** + * Create CRC32 checksum file for integrity verification + */ +async function createCrc32Checksum(outputPath, inputPath) { + const fileCrc32 = await calculateFileCrc32(inputPath); + const checksumPath = path.join(outputPath, `crc32.txt`); + await fs.writeFile(checksumPath, `${fileCrc32}\n`, 'utf-8'); + return { checksumPath, fileCrc32 }; +} + // ============================================================================ // PROPERTY PROCESSING // ============================================================================ @@ -664,13 +683,19 @@ async function main() { 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'); + console.log('\nāš™ļø Processing properties:\n'); const processedProperties = []; const rootProps = Object.keys(data); From 845e777f08bc6f6a20611851043b012f675fb3d9 Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Wed, 13 May 2026 16:02:45 +0200 Subject: [PATCH 16/18] convert script to ES module conventions --- scripts/extract-elx/extract-elx.js | 102 ++++++-------------------- scripts/extract-elx/package-lock.json | 3 + scripts/extract-elx/package.json | 7 ++ 3 files changed, 31 insertions(+), 81 deletions(-) diff --git a/scripts/extract-elx/extract-elx.js b/scripts/extract-elx/extract-elx.js index bf9ba96..577f975 100644 --- a/scripts/extract-elx/extract-elx.js +++ b/scripts/extract-elx/extract-elx.js @@ -18,73 +18,13 @@ * - _manifest.json with extraction metadata */ -const fs = require('fs').promises; -const path = require('path'); -const { execSync } = require('child_process'); -const { existsSync } = require('fs'); - -// Import xml-formatter (try multiple locations) -let xmlFormatter; -try { - // Try local node_modules first - xmlFormatter = require('xml-formatter'); -} catch (err) { - try { - // Try to find npm global prefix and load from there - const npmPrefix = execSync('npm config get prefix', { encoding: 'utf-8' }).trim(); - const globalXmlFormatter = path.join(npmPrefix, 'node_modules', 'xml-formatter'); - xmlFormatter = require(globalXmlFormatter); - } catch (err2) { - console.error( - '\nāŒ Missing dependency: xml-formatter\n' + - 'Please install globally with: npm install -g xml-formatter\n' + - 'Or locally in the current directory with: npm install xml-formatter\n' - ); - process.exit(1); - } -} - -// Import fast-xml-parser (try multiple locations) -let fastXmlParser; -try { - // Try local node_modules first - fastXmlParser = require('fast-xml-parser'); -} catch (err) { - try { - // Try to find npm global prefix and load from there - const npmPrefix = execSync('npm config get prefix', { encoding: 'utf-8' }).trim(); - const globalFastXmlParser = path.join(npmPrefix, 'node_modules', 'fast-xml-parser'); - fastXmlParser = require(globalFastXmlParser); - } catch (err2) { - console.error( - '\nāŒ Missing dependency: fast-xml-parser\n' + - 'Please install globally with: npm install -g fast-xml-parser\n' + - 'Or locally in the current directory with: npm install fast-xml-parser\n' - ); - process.exit(1); - } -} - -// Import crc-32 for file integrity checks (try multiple locations) -let crc32; -try { - // Try local node_modules first - crc32 = require('crc-32'); -} catch (err) { - try { - // Try to find npm global prefix and load from there - const npmPrefix = execSync('npm config get prefix', { encoding: 'utf-8' }).trim(); - const globalCrc32 = join(npmPrefix, 'node_modules', 'crc-32'); - crc32 = require(globalCrc32); - } catch (err2) { - console.error( - '\nāŒ Missing dependency: crc-32\n' + - 'Please install globally with: npm install -g crc-32\n' + - 'Or locally in the current directory with: npm install crc-32\n' - ); - process.exit(1); - } -} +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 @@ -97,7 +37,7 @@ const CONFIG = { INDEX_FILE: 'index', ITEM_PREFIX: 'item', INTERNALS_SUFFIX: '_internals', - VERSION: '1.0.0' + VERSION: '0.0.1', }; // ============================================================================ @@ -165,7 +105,7 @@ const XML_PARSE_OPTIONS = { function parseXmlDocument(xmlString) { if (typeof xmlString !== 'string') return null; try { - return new fastXmlParser.XMLParser(XML_PARSE_OPTIONS).parse(xmlString); + return new XMLParser(XML_PARSE_OPTIONS).parse(xmlString); } catch (error) { return null; } @@ -403,7 +343,7 @@ async function createOutputDirectory(outputPath, allowOverwrite = false, cleanEx */ async function saveStringProperty(dirPath, filename, content, options = {}) { const ext = getFileExtension(content); - const filepath = path.join(dirPath, `${filename}.${ext}`); + const filepath = _join(dirPath, `${filename}.${ext}`); let output = content; @@ -420,7 +360,7 @@ async function saveStringProperty(dirPath, filename, content, options = {}) { * Save a JSON object to file */ async function saveJsonProperty(dirPath, filename, obj, options = {}) { - const filepath = path.join(dirPath, `${filename}.json`); + const filepath = _join(dirPath, `${filename}.json`); const output = prettifyJson(obj, CONFIG.JSON_INDENT); await fs.writeFile(filepath, output, 'utf-8'); @@ -431,7 +371,7 @@ async function saveJsonProperty(dirPath, filename, obj, options = {}) { * Create subdirectory for a property */ async function createPropertyDirectory(outputPath, propertyName) { - const dirPath = path.join(outputPath, propertyName); + const dirPath = _join(outputPath, propertyName); await fs.mkdir(dirPath, { recursive: true }); return dirPath; } @@ -440,7 +380,7 @@ async function createPropertyDirectory(outputPath, propertyName) { * Save backup of original file */ async function saveBackup(outputPath, data) { - const backupPath = path.join(outputPath, CONFIG.BACKUP_SUFFIX); + const backupPath = _join(outputPath, CONFIG.BACKUP_SUFFIX); const output = prettifyJson(data, CONFIG.JSON_INDENT); await fs.writeFile(backupPath, output, 'utf-8'); return backupPath; @@ -451,7 +391,7 @@ async function saveBackup(outputPath, data) { */ async function createCrc32Checksum(outputPath, inputPath) { const fileCrc32 = await calculateFileCrc32(inputPath); - const checksumPath = path.join(outputPath, `crc32.txt`); + const checksumPath = _join(outputPath, `crc32.txt`); await fs.writeFile(checksumPath, `${fileCrc32}\n`, 'utf-8'); return { checksumPath, fileCrc32 }; } @@ -555,7 +495,7 @@ async function processArrayProperty(outputPath, propertyName, arr) { } // Save index metadata - const indexPath = path.join(dirPath, `${CONFIG.INDEX_FILE}.json`); + 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 @@ -575,7 +515,7 @@ async function processObjectProperty(outputPath, propertyName, obj) { }; const dirPath = await createPropertyDirectory(outputPath, propertyName); - const filepath = path.join(dirPath, `${CONFIG.INDEX_FILE}.json`); + const filepath = _join(dirPath, `${CONFIG.INDEX_FILE}.json`); const output = prettifyJson(obj, CONFIG.JSON_INDENT); await fs.writeFile(filepath, output, 'utf-8'); @@ -604,7 +544,7 @@ async function processProperty(outputPath, propertyName, value) { }; const dirPath = await createPropertyDirectory(outputPath, propertyName); - const filepath = path.join(dirPath, `${CONFIG.INDEX_FILE}.json`); + const filepath = _join(dirPath, `${CONFIG.INDEX_FILE}.json`); await fs.writeFile(filepath, prettifyJson(value, CONFIG.JSON_INDENT), 'utf-8'); result.files.push(filepath); @@ -640,7 +580,7 @@ async function createManifest(outputPath, data, processedProperties) { }; } - const manifestPath = path.join(outputPath, CONFIG.MANIFEST_FILE); + const manifestPath = _join(outputPath, CONFIG.MANIFEST_FILE); const output = prettifyJson(manifest, CONFIG.JSON_INDENT); await fs.writeFile(manifestPath, output, 'utf-8'); @@ -672,9 +612,9 @@ async function main() { } // Determine output directory - const dir = path.dirname(inputPath); - const basename = path.basename(inputPath, path.extname(inputPath)); - const outputPath = path.join(dir, `${basename}${CONFIG.INTERNALS_SUFFIX}`); + 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}`); diff --git a/scripts/extract-elx/package-lock.json b/scripts/extract-elx/package-lock.json index 45c9187..f070112 100644 --- a/scripts/extract-elx/package-lock.json +++ b/scripts/extract-elx/package-lock.json @@ -1,9 +1,12 @@ { "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", diff --git a/scripts/extract-elx/package.json b/scripts/extract-elx/package.json index 8faa62d..b6c29a0 100644 --- a/scripts/extract-elx/package.json +++ b/scripts/extract-elx/package.json @@ -1,4 +1,11 @@ { + "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", From 80784fb47ad7292be97efa189623d066cc534d40 Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Thu, 14 May 2026 09:42:43 +0200 Subject: [PATCH 17/18] add automatic saving of properties containing XML data --- scripts/extract-elx/extract-elx.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/extract-elx/extract-elx.js b/scripts/extract-elx/extract-elx.js index 577f975..d5c5218 100644 --- a/scripts/extract-elx/extract-elx.js +++ b/scripts/extract-elx/extract-elx.js @@ -486,6 +486,14 @@ async function processArrayProperty(outputPath, propertyName, arr) { } 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` }); From 8b99313746d0273541d06f59db6473828793fa7f Mon Sep 17 00:00:00 2001 From: Pier-Paolo Mammi Date: Thu, 14 May 2026 09:43:26 +0200 Subject: [PATCH 18/18] add vscode launch file for debugging --- scripts/extract-elx/.vscode/launch.json | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 scripts/extract-elx/.vscode/launch.json 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