p6-xer
Version:
A TypeScript module for parsing and processing Primavera P6 XER files
261 lines (260 loc) • 9.6 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.XerParser = exports.EncodingError = exports.XerParserError = void 0;
const fs = __importStar(require("fs"));
const chardet = __importStar(require("chardet"));
const iconv = __importStar(require("iconv-lite"));
const XLSX = __importStar(require("xlsx"));
class XerParserError extends Error {
constructor(message) {
super(message);
this.name = 'XerParserError';
}
}
exports.XerParserError = XerParserError;
class EncodingError extends XerParserError {
constructor(message) {
super(message);
this.name = 'EncodingError';
}
}
exports.EncodingError = EncodingError;
class XerParser {
constructor(options = {}) {
// Alias for parseAsync for backward compatibility
this.parse = this.parseAsync;
// Alias for exportToXlsxAsync for backward compatibility
this.exportToXlsx = this.exportToXlsxAsync;
this.options = {
encoding: options.encoding || 'utf8',
skipEmptyTables: options.skipEmptyTables || false,
};
}
handleTableStart(currentTable, values, data) {
if (currentTable && (!this.options.skipEmptyTables || currentTable.rows.length > 0)) {
data.tables.push(currentTable);
}
return {
name: values[0],
fields: [],
rows: []
};
}
handleFieldDefinition(currentTable, values) {
if (currentTable) {
currentTable.fields = values;
}
}
handleDataRow(currentTable, values) {
if (currentTable && currentTable.fields.length > 0) {
const row = {};
currentTable.fields.forEach((field, index) => {
row[field] = values[index] || '';
});
currentTable.rows.push(row);
}
}
handleTableEnd(currentTable, data) {
if (currentTable) {
if (!this.options.skipEmptyTables || currentTable.rows.length > 0) {
data.tables.push(currentTable);
}
return null;
}
return currentTable;
}
parseErmhdrLine(line) {
const parts = line.split('\t');
if (parts[0] !== 'ERMHDR' || parts.length < 9) {
return undefined;
}
return {
version: parts[1],
date: parts[2],
project: parts[3],
id: parts[4],
user: parts[5],
database: parts[6],
system: parts[7],
currency: parts[8]
};
}
parseContent(content) {
const lines = content.split(/\r?\n/);
const data = {
tables: []
};
// Check for ERMHDR line at the start
if (lines.length > 0) {
data.header = this.parseErmhdrLine(lines[0]);
}
let currentTable = null;
// Start from line 1 if we found a header, otherwise start from line 0
for (let i = data.header ? 1 : 0; i < lines.length; i++) {
const line = lines[i].trim();
if (!line)
continue;
const [type, ...values] = line.split('\t');
switch (type) {
case '%T':
currentTable = this.handleTableStart(currentTable, values, data);
break;
case '%F':
this.handleFieldDefinition(currentTable, values);
break;
case '%R':
this.handleDataRow(currentTable, values);
break;
case '%E':
currentTable = this.handleTableEnd(currentTable, data);
break;
}
}
return data;
}
parseSync(filePath) {
try {
const buffer = fs.readFileSync(filePath);
const encoding = this.detectEncoding(buffer);
const content = iconv.decode(buffer, encoding);
return this.parseContent(content);
}
catch (error) {
if (error instanceof Error) {
throw new XerParserError(error.message);
}
throw new XerParserError('Unknown error occurred while parsing XER file');
}
}
async parseAsync(filePath) {
try {
const buffer = await fs.promises.readFile(filePath);
const encoding = this.detectEncoding(buffer);
const content = iconv.decode(buffer, encoding);
return this.parseContent(content);
}
catch (error) {
if (error instanceof Error) {
throw new XerParserError(error.message);
}
throw new XerParserError('Unknown error occurred while parsing XER file');
}
}
detectEncoding(buffer) {
var _a;
try {
const detected = chardet.analyse(buffer);
const encoding = ((_a = detected[0]) === null || _a === void 0 ? void 0 : _a.name) || 'UTF-8';
if (!iconv.encodingExists(encoding)) {
throw new EncodingError(`Unsupported encoding: ${encoding}`);
}
return encoding;
}
catch (error) {
if (error instanceof EncodingError) {
throw error;
}
throw new EncodingError('Failed to detect file encoding');
}
}
exportToWorkbook(data, options) {
const workbook = XLSX.utils.book_new();
// Add header information if available
if (data.header) {
const headerData = [{
Field: 'Version',
Value: data.header.version
}, {
Field: 'Date',
Value: data.header.date
}, {
Field: 'Project',
Value: data.header.project
}, {
Field: 'ID',
Value: data.header.id
}, {
Field: 'User',
Value: data.header.user
}, {
Field: 'Database',
Value: data.header.database
}, {
Field: 'System',
Value: data.header.system
}, {
Field: 'Currency',
Value: data.header.currency
}];
const headerSheet = XLSX.utils.json_to_sheet(headerData);
XLSX.utils.book_append_sheet(workbook, headerSheet, 'Header');
}
// Add each table as a separate sheet
data.tables.forEach(table => {
const sheetName = options.sheetNamePrefix
? `${options.sheetNamePrefix}_${table.name}`.substring(0, 31)
: table.name.substring(0, 31);
// Process rows to handle long text
const processedRows = table.rows.map(row => {
const processedRow = {};
for (const [key, value] of Object.entries(row)) {
// Truncate values that are too long for Excel
processedRow[key] = value.length > 32000
? value.substring(0, 32000) + '... (truncated)'
: value;
}
return processedRow;
});
const worksheet = XLSX.utils.json_to_sheet(processedRows);
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
});
// If no sheets were added, add an empty sheet
if (workbook.SheetNames.length === 0) {
const emptySheet = XLSX.utils.json_to_sheet([]);
XLSX.utils.book_append_sheet(workbook, emptySheet, 'Empty');
}
return workbook;
}
exportToXlsxSync(data, options) {
const workbook = this.exportToWorkbook(data, options);
XLSX.writeFile(workbook, options.outputPath);
}
async exportToXlsxAsync(data, options) {
const workbook = this.exportToWorkbook(data, options);
await fs.promises.writeFile(options.outputPath, XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }));
}
}
exports.XerParser = XerParser;