@makakwastaken/ts-edifact
Version:
Edifact parser library
222 lines • 10.4 kB
JavaScript
/**
* @author Roman Vottner
* @copyright 2020 Roman Vottner
* @license Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EventEmitter } from 'node:events';
import { Configuration } from './configuration';
import { EdifactSeparatorsBuilder } from './edi/separators';
import { Tokenizer } from './tokenizer';
var States;
(function (States) {
States[States["EMPTY"] = 0] = "EMPTY";
States[States["SEGMENT"] = 1] = "SEGMENT";
States[States["ELEMENT"] = 2] = "ELEMENT";
States[States["COMPONENT"] = 3] = "COMPONENT";
States[States["MODESET"] = 4] = "MODESET";
States[States["DATA"] = 5] = "DATA";
States[States["CONTINUED"] = 6] = "CONTINUED";
})(States || (States = {}));
export class Parser extends EventEmitter {
validator;
configuration;
tokenizer;
state;
segment;
element;
constructor(configuration) {
super();
EventEmitter.apply(this);
if (configuration) {
this.configuration = configuration;
}
else {
this.configuration = new Configuration();
}
this.validator = this.configuration.validator;
this.tokenizer = new Tokenizer(this.configuration);
this.state = States.EMPTY;
}
separators() {
const builder = new EdifactSeparatorsBuilder();
builder.elementSeparator(String.fromCharCode(this.configuration.config.dataElementSeparator));
builder.componentSeparator(String.fromCharCode(this.configuration.config.componentDataSeparator));
builder.decimalSeparator(String.fromCharCode(this.configuration.config.decimalMark));
builder.releaseIndicator(String.fromCharCode(this.configuration.config.releaseCharacter));
builder.segmentTerminator(String.fromCharCode(this.configuration.config.segmentTerminator));
return builder.build();
}
onOpenSegment(segment, segmentEntry) {
this.emit('openSegment', segment, segmentEntry);
}
onCloseSegment() {
this.emit('closeSegment');
}
onElement(element) {
this.emit('element', element);
}
onComponent(data) {
this.emit('component', data);
}
/**
* Set an encoding level.
* @param level - The encoding level name.
*/
updateCharset(charset) {
const previous = this.configuration.charset;
this.configuration.updateCharset(charset);
if (this.configuration.charset !== previous) {
this.tokenizer.setCharsetBasedOnConfig(this.configuration);
}
}
/**
* @summary Ends the EDI interchange.
* @throws {Error} If more data is expected.
*/
end() {
// The stream can only be closed if the last segment is complete. This
// means the parser is currently in a state accepting segment data, but no
// data was read so far.
if (this.state !== States.SEGMENT || this.tokenizer.buffer !== '') {
throw this.errors.incompleteMessage();
}
this.state = States.EMPTY;
}
una(chunk) {
if (/^UNA.... ./g.test(chunk)) {
this.configuration.config.componentDataSeparator = chunk.charCodeAt(3);
this.configuration.config.dataElementSeparator = chunk.charCodeAt(4);
this.configuration.config.decimalMark = chunk.charCodeAt(5);
this.configuration.config.releaseCharacter = chunk.charCodeAt(6);
this.configuration.config.segmentTerminator = chunk.charCodeAt(8);
return true;
}
return false;
}
write(chunk) {
// The position of the parser
let index = 0;
if (this.state === States.CONTINUED) {
this.state = States.MODESET;
}
while (index < chunk.length) {
switch (this.state) {
// biome-ignore lint/suspicious/noFallthroughSwitchClause: <explanation>
case States.EMPTY:
index = this.una(chunk) ? 9 : 0;
// If the first segment is interrupted by, for example, a line break, the
// parser will remain in the same state as it has here. Since we don't
// want the parser to detect another UNA header, in such a case, we put it
// in the segment state.
this.state = States.SEGMENT;
// Continue to read the first segment, otherwise the index increment add
// the end of the loop would cause the parser to skip the first character.
case States.SEGMENT:
index = this.tokenizer.segment(chunk, index);
// Determine the next parser state
switch (chunk.charCodeAt(index) ||
this.configuration.config.endOfTag) {
case this.configuration.config.dataElementSeparator:
this.segment = this.validator.onOpenSegment(this.tokenizer.buffer);
this.onOpenSegment(this.tokenizer.buffer, this.segment);
this.state = States.ELEMENT;
this.tokenizer.buffer = '';
break;
case this.configuration.config.segmentTerminator:
this.segment = this.validator.onOpenSegment(this.tokenizer.buffer);
this.onOpenSegment(this.tokenizer.buffer, this.segment);
this.validator.onCloseSegment('');
this.segment = undefined;
this.onCloseSegment();
this.state = States.SEGMENT;
this.tokenizer.buffer = '';
break;
case this.configuration.config.endOfTag:
case this.configuration.config.carriageReturn:
case this.configuration.config.lineFeed:
break;
default:
throw this.errors.invalidControlAfterSegment(this.tokenizer.buffer, chunk.charAt(index));
}
break;
// biome-ignore lint/suspicious/noFallthroughSwitchClause: <explanation>
case States.ELEMENT:
// Start reading a new element
this.element = this.validator.onElement();
this.onElement(this.element);
// biome-ignore lint/suspicious/noFallthroughSwitchClause: Fall through to process the available component data
case States.COMPONENT:
// Start reading a new component
this.validator.onOpenComponent(this.tokenizer);
case States.MODESET:
case States.DATA:
index = this.tokenizer.data(chunk, index);
// Determine the next parser state
switch (chunk.charCodeAt(index) ||
this.configuration.config.endOfTag) {
case this.configuration.config.componentDataSeparator:
this.validator.onCloseComponent(this.tokenizer);
this.onComponent(this.tokenizer.buffer);
this.state = States.COMPONENT;
this.tokenizer.buffer = '';
break;
case this.configuration.config.dataElementSeparator:
this.validator.onCloseComponent(this.tokenizer);
this.onComponent(this.tokenizer.buffer);
this.state = States.ELEMENT;
this.tokenizer.buffer = '';
break;
case this.configuration.config.segmentTerminator:
this.validator.onCloseComponent(this.tokenizer);
this.onComponent(this.tokenizer.buffer);
this.validator.onCloseSegment('');
this.onCloseSegment();
this.state = States.SEGMENT;
this.tokenizer.buffer = '';
break;
case this.configuration.config.decimalMark:
this.tokenizer.decimal(chunk, index);
this.state = States.DATA;
break;
case this.configuration.config.releaseCharacter:
index++;
this.tokenizer.release(chunk, index);
this.state = States.DATA;
break;
case this.configuration.config.endOfTag:
case this.configuration.config.carriageReturn:
case this.configuration.config.lineFeed:
this.state = States.DATA;
break;
default:
throw this.errors.invalidCharacter(chunk.charAt(index), chunk.charCodeAt(index), index);
}
}
// Consume the control character
index++;
}
}
errors = {
incompleteMessage: () => new Error('Cannot close an incomplete message'),
invalidCharacter: (character, charCode, index) => new Error(`Invalid character '${character}' at position ${index} (${charCode})`),
invalidControlAfterSegment: (segment, character) => {
let message = '';
message += `Invalid character '${character}`;
message += `' after reading segment name ${segment}`;
return new Error(message);
},
};
}
//# sourceMappingURL=parser.js.map