Merge remote-tracking branch 'dev-tools/main'
This commit is contained in:
@@ -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
|
||||||
+27
@@ -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": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "scriptParameter",
|
||||||
|
"type": "promptString",
|
||||||
|
"description": "Enter parameter for JLX script",
|
||||||
|
"default": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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 <input-file-path> [--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('<?xml') || trimmed.startsWith('<');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pretty-print XML with indentation using xml-formatter
|
||||||
|
*/
|
||||||
|
function prettifyXml(xmlString, indent = 2) {
|
||||||
|
try {
|
||||||
|
const formatted = xmlFormatter(xmlString, {
|
||||||
|
indentation: ' '.repeat(indent),
|
||||||
|
collapseContent: false,
|
||||||
|
lineSeparator: '\n',
|
||||||
|
filter: () => 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 <input-file-path> [--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();
|
||||||
Generated
+142
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user