read-gedcom
Version:
Gedcom file reader
124 lines • 5.32 kB
JavaScript
import { detectCharset } from './decoder';
import { BOM_UTF16_BE, BOM_UTF16_LE, BOM_UTF32_BE, BOM_UTF32_LE, BOM_UTF8, decodeAnsel, decodeCp1252, decodeCp850, decodeMacintosh, decodeUtf, } from './decoding';
import { ErrorInvalidFileType, ErrorTreeStructure, ErrorUnsupportedCharset } from './error';
import { indexTree } from './indexer';
import { buildTree } from './structurer';
import { tokenize } from './tokenizer';
/**
* Reads a Gedcom file and returns it as a tree representation.
* @param buffer The content of the file
* @param options Optional parameters
* @throws ErrorParse If the file cannot be interpreted correctly
* @category Gedcom parser
*/
export const parseGedcom = (buffer, options = {}) => {
checkMagicHeader(buffer);
const charset = options.forcedCharset == null ? detectCharset(buffer) : options.forcedCharset;
const callback = options.progressCallback;
const totalBytes = buffer.byteLength;
const decodingCallback = callback ? (bytesRead) => callback(0 /* GedcomReadingPhase.Decoding */, bytesRead / totalBytes) : undefined;
let input;
if (charset === "UTF-8" /* FileEncoding.Utf8 */ || charset === "UTF-16be" /* FileEncoding.Utf16be */ || charset === "UTF-16le" /* FileEncoding.Utf16le */) {
input = decodeUtf(buffer, decodingCallback);
}
else if (charset === "Cp1252" /* FileEncoding.Cp1252 */) {
input = decodeCp1252(buffer, decodingCallback);
}
else if (charset === "ANSEL" /* FileEncoding.Ansel */) {
input = decodeAnsel(buffer, decodingCallback, !!options.doStrictDecoding);
}
else if (charset === "Macintosh" /* FileEncoding.Macintosh */) {
input = decodeMacintosh(buffer, decodingCallback);
}
else if (charset === "Cp850" /* FileEncoding.Cp850 */) {
input = decodeCp850(buffer, decodingCallback);
}
else {
throw new ErrorUnsupportedCharset(`Unsupported charset: ${charset}`, charset);
}
const totalChars = input.length;
const tokensIterator = tokenize(input);
const rootNode = buildTree(tokensIterator, !!options.noInlineContinuations, callback ? charsRead => callback(1 /* GedcomReadingPhase.TokenizationAndStructuring */, charsRead / totalChars) : null);
checkTreeStructure(rootNode);
if (!options.noIndex) {
indexTree(rootNode, !!options.noBackwardsReferencesIndex, !!options.doHideIndex, callback ? () => callback(2 /* GedcomReadingPhase.Indexing */, null) : null);
}
if (options.doFreeze) {
if (callback) {
callback(3 /* GedcomReadingPhase.Freezing */, 0);
}
deepFreeze(rootNode);
if (callback) {
callback(3 /* GedcomReadingPhase.Freezing */, 1);
}
}
return rootNode;
};
/**
* A simple and fast testing procedure to eliminate files that are clearly not Gedcoms.
* @param buffer The content of the file
*/
const checkMagicHeader = (buffer) => {
const headStr = '0 HEAD';
const head = [];
for (let i = 0; i < headStr.length; i++) {
head.push(headStr.charCodeAt(i));
}
const byteBuffer = new Uint8Array(buffer);
const startsWith = (bytes) => {
if (byteBuffer.byteLength < bytes.length) {
return false;
}
for (let i = 0; i < bytes.length; i++) {
if (bytes[i] !== byteBuffer[i]) {
return false;
}
}
return true;
};
if (!startsWith(head) && !startsWith(BOM_UTF8.concat(head))) { // Not ASCII nor any of its extensions, including UTF-8
const headUtf16 = head.map(b => [0, b]);
if (!startsWith(BOM_UTF16_BE.concat(headUtf16.flat())) &&
!startsWith(BOM_UTF16_LE.concat(headUtf16.map(bs => bs.slice().reverse()).flat()))) { // Not UTF-16
const headUtf32 = head.map(b => [0, 0, 0, b]);
if (!startsWith(BOM_UTF32_BE.concat(headUtf32.flat())) &&
!startsWith(BOM_UTF32_LE.concat(headUtf32.map(bs => bs.slice().reverse()).flat()))) { // Not UTF-32
throw new ErrorInvalidFileType('Probably not a Gedcom file');
}
}
}
};
/**
* A simple procedure verifying that the root node starts with a {@link Tag.Header} and ends with a {@link Tag.Trailer}.
* @param rootNode The root node
*/
const checkTreeStructure = (rootNode) => {
const root = rootNode.children;
// Assumes that `root.length > 0`
const header = root[0];
if (header.tag !== "HEAD" /* Tag.Header */) {
throw new ErrorTreeStructure(`First node is not a header (got ${header.tag})`);
}
const trailer = root[root.length - 1];
if (trailer.tag !== "TRLR" /* Tag.Trailer */) {
throw new ErrorTreeStructure(`Last node is not a trailer (got ${trailer.tag})`);
}
};
/**
* Freezes this object to prevent further modifications.
* @param object The object to be frozen
*/
const deepFreeze = (object) => {
let queue = [object];
while (queue.length > 0) {
const next = [];
queue.forEach(obj => {
if (obj != null && typeof obj === 'object' && !Object.isFrozen(obj)) {
Object.freeze(obj);
Object.getOwnPropertyNames(obj).forEach(property => next.push(obj[property]));
}
});
queue = next;
}
};
//# sourceMappingURL=reader.js.map