@stenway/oml
Version:
Oh My Lord - Yet another JavaScript Object Notation
910 lines • 34.2 kB
JavaScript
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-empty-function */
/* (C) Stefan John / Stenway / Stenway.com / 2023 */
import { Base64String, InvalidUtf16StringError, ReliableTxtDecoder, ReliableTxtEncoder, ReliableTxtEncoding, ReliableTxtLines, Utf16String } from "@stenway/reliabletxt";
// ----------------------------------------------------------------------
export class OmlDocument {
constructor(content, encoding = ReliableTxtEncoding.Utf8) {
this.content = content;
this.encoding = encoding;
}
toString(formatting = null, replacer = null) {
return Oml.stringify(this.content, formatting, replacer);
}
toBytes(formatting = null, replacer = null) {
const text = this.toString(formatting, replacer);
return ReliableTxtEncoder.encode(text, this.encoding);
}
toBase64String(formatting = null, replacer = null) {
const text = this.toString(formatting, replacer);
return Base64String.encodeText(text, this.encoding);
}
static parse(str, reviver = null, encoding = ReliableTxtEncoding.Utf8) {
const content = Oml.parse(str, reviver);
return new OmlDocument(content, encoding);
}
static fromBytes(bytes, reviver = null) {
const document = ReliableTxtDecoder.decode(bytes);
return this.parse(document.text, reviver, document.encoding);
}
static fromBase64String(base64Str, reviver = null) {
const bytes = Base64String.decodeAsBytes(base64Str);
return this.fromBytes(bytes, reviver);
}
}
// ----------------------------------------------------------------------
export class OmlParserError extends Error {
constructor(index, lineIndex, linePosition, message) {
super(`${message} (${lineIndex + 1}, ${linePosition + 1})`);
this.index = index;
this.lineIndex = lineIndex;
this.linePosition = linePosition;
}
}
// ----------------------------------------------------------------------
export class Oml {
static parse(text, reviver = null) {
return new OmlParser(text, reviver).parse();
}
static stringify(value, formatting = null, replacer = null) {
if (formatting !== null) {
if (formatting.indentation !== undefined) {
OmlSerializer.validateWhitespaceString(formatting.indentation);
}
else {
formatting.indentation = "\t";
}
if (formatting.beforeEqual !== undefined) {
OmlSerializer.validateWhitespaceString(formatting.beforeEqual);
}
else {
formatting.beforeEqual = " ";
}
if (formatting.afterEqual !== undefined) {
OmlSerializer.validateWhitespaceString(formatting.afterEqual);
}
else {
formatting.afterEqual = " ";
}
if (formatting.alignChar !== undefined && formatting.alignChar !== null) {
OmlSerializer.validateWhitespaceString(formatting.alignChar, true);
}
else if (formatting.alignChar === undefined) {
formatting.alignChar = " ";
}
if (formatting.reduceSimpleArray === undefined) {
formatting.reduceSimpleArray = true;
}
}
const strings = [];
OmlSerializer.serializeValue(value, formatting, "", replacer, value, strings, []);
return strings.join("");
}
}
// ----------------------------------------------------------------------
class OmlParser {
constructor(text, reviver) {
this.index = 0;
if (text.length === 0) {
throw new RangeError(`Empty string not allowed`);
}
this.text = text;
this.reviver = reviver;
}
skipWhitespaceAndComments() {
if (this.index >= this.text.length) {
return false;
}
let readWsOrComments = false;
wsLoop: for (;;) {
let codeUnit = this.text.charCodeAt(this.index);
switch (codeUnit) {
case 0x0009:
case 0x000A:
case 0x000B:
case 0x000C:
case 0x000D:
case 0x0020:
case 0x0085:
case 0x00A0:
case 0x1680:
case 0x2000:
case 0x2001:
case 0x2002:
case 0x2003:
case 0x2004:
case 0x2005:
case 0x2006:
case 0x2007:
case 0x2008:
case 0x2009:
case 0x200A:
case 0x2028:
case 0x2029:
case 0x202F:
case 0x205F:
case 0x3000:
readWsOrComments = true;
this.index++;
if (this.index >= this.text.length) {
break wsLoop;
}
break;
case 0x0023:
readWsOrComments = true;
for (;;) {
this.index++;
if (this.index >= this.text.length) {
break wsLoop;
}
codeUnit = this.text.charCodeAt(this.index);
if (codeUnit === 0x000A) {
this.index++;
break;
}
}
break;
default:
break wsLoop;
}
}
return readWsOrComments;
}
parseString() {
this.index++;
const strCodeUnits = [];
stringCharLoop: for (;;) {
if (this.index >= this.text.length) {
throw this.getError(OmlParser.stringNotClosed);
}
let codeUnit = this.text.charCodeAt(this.index);
this.index++;
switch (codeUnit) {
case 0x000A:
throw this.getError(OmlParser.stringNotClosed, -1);
case 0x0022:
if (this.index >= this.text.length) {
break stringCharLoop;
}
codeUnit = this.text.charCodeAt(this.index);
switch (codeUnit) {
case 0x0022:
strCodeUnits.push("\"");
this.index++;
break;
case 0x000A:
case 0x0023:
case 0x003D:
case 0x005D:
case 0x007D:
case 0x0009:
case 0x000B:
case 0x000C:
case 0x000D:
case 0x0020:
case 0x0085:
case 0x00A0:
case 0x1680:
case 0x2000:
case 0x2001:
case 0x2002:
case 0x2003:
case 0x2004:
case 0x2005:
case 0x2006:
case 0x2007:
case 0x2008:
case 0x2009:
case 0x200A:
case 0x2028:
case 0x2029:
case 0x202F:
case 0x205F:
case 0x3000:
break stringCharLoop;
case 0x002F:
this.index++;
if (this.index >= this.text.length) {
throw this.getError(OmlParser.invalidStringLineBreak);
}
codeUnit = this.text.charCodeAt(this.index);
if (codeUnit !== 0x0022) {
throw this.getError(OmlParser.invalidStringLineBreak);
}
strCodeUnits.push("\n");
this.index++;
break;
default:
throw this.getError(OmlParser.invalidCharacterAfterString);
}
break;
default:
strCodeUnits.push(String.fromCharCode(codeUnit));
if (codeUnit >= 0xD800 && codeUnit <= 0xDFFF) {
if (codeUnit >= 0xDC00 || this.index >= this.text.length) {
throw new InvalidUtf16StringError();
}
const secondCodeUnit = this.text.charCodeAt(this.index);
if (!(secondCodeUnit >= 0xDC00 && secondCodeUnit <= 0xDFFF)) {
throw new InvalidUtf16StringError();
}
strCodeUnits.push(String.fromCharCode(secondCodeUnit));
this.index++;
}
break;
}
}
return strCodeUnits.join("");
}
parseChar() {
let result = "";
this.index++;
if (this.index >= this.text.length) {
throw this.getError(OmlParser.charNotClosed);
}
let codeUnit = this.text.charCodeAt(this.index);
this.index++;
switch (codeUnit) {
case 0x000A:
throw this.getError(OmlParser.charNotClosed, -1);
case 0x0027:
if (this.index >= this.text.length) {
throw this.getError(OmlParser.invalidCharSequence);
}
codeUnit = this.text.charCodeAt(this.index);
switch (codeUnit) {
case 0x0027:
result = "'";
this.index++;
break;
case 0x002F:
this.index++;
if (this.index >= this.text.length) {
throw this.getError(OmlParser.invalidCharSequence);
}
codeUnit = this.text.charCodeAt(this.index);
if (codeUnit !== 0x0027) {
throw this.getError(OmlParser.invalidCharSequence);
}
result = "\n";
this.index++;
break;
default:
throw this.getError(OmlParser.invalidCharSequence);
}
break;
default:
result = String.fromCharCode(codeUnit);
if (codeUnit >= 0xD800 && codeUnit <= 0xDFFF) {
if (codeUnit >= 0xDC00 || this.index >= this.text.length) {
throw new InvalidUtf16StringError();
}
const secondCodeUnit = this.text.charCodeAt(this.index);
if (!(secondCodeUnit >= 0xDC00 && secondCodeUnit <= 0xDFFF)) {
throw new InvalidUtf16StringError();
}
result += String.fromCharCode(secondCodeUnit);
this.index++;
}
}
if (this.index >= this.text.length) {
throw this.getError(OmlParser.invalidCharSequence);
}
codeUnit = this.text.charCodeAt(this.index);
if (codeUnit !== 0x0027) {
throw this.getError(OmlParser.invalidCharSequence);
}
this.index++;
if (this.index < this.text.length) {
codeUnit = this.text.charCodeAt(this.index);
switch (codeUnit) {
case 0x000A:
case 0x0023:
case 0x005D:
case 0x007D:
case 0x0009:
case 0x000B:
case 0x000C:
case 0x000D:
case 0x0020:
case 0x0085:
case 0x00A0:
case 0x1680:
case 0x2000:
case 0x2001:
case 0x2002:
case 0x2003:
case 0x2004:
case 0x2005:
case 0x2006:
case 0x2007:
case 0x2008:
case 0x2009:
case 0x200A:
case 0x2028:
case 0x2029:
case 0x202F:
case 0x205F:
case 0x3000:
break;
default:
throw this.getError(OmlParser.invalidCharacterAfterChar);
}
}
return result;
}
parseArray() {
this.index++;
const array = [];
loop: for (;;) {
const hasReadWs = this.skipWhitespaceAndComments();
if (this.index >= this.text.length) {
throw this.getError(OmlParser.arrayNotClosed);
}
let codeUnit = this.text.charCodeAt(this.index);
if (codeUnit === 0x005D) {
this.index++;
if (this.index >= this.text.length) {
break loop;
}
codeUnit = this.text.charCodeAt(this.index);
switch (codeUnit) {
case 0x000A:
case 0x0023:
case 0x005D:
case 0x007D:
case 0x0009:
case 0x000B:
case 0x000C:
case 0x000D:
case 0x0020:
case 0x0085:
case 0x00A0:
case 0x1680:
case 0x2000:
case 0x2001:
case 0x2002:
case 0x2003:
case 0x2004:
case 0x2005:
case 0x2006:
case 0x2007:
case 0x2008:
case 0x2009:
case 0x200A:
case 0x2028:
case 0x2029:
case 0x202F:
case 0x205F:
case 0x3000:
break loop;
default:
throw this.getError(OmlParser.invalidCharacterAfterArray);
}
}
else {
if (array.length > 0 && !hasReadWs) {
throw this.getError(OmlParser.wsRequiredAfterArrayElement);
}
const element = this.parseAny(array, array.length);
array.push(element);
}
}
return array;
}
parseObject() {
this.index++;
const object = {};
let firstRun = true;
loop: for (;;) {
const hasReadWs = this.skipWhitespaceAndComments();
if (this.index >= this.text.length) {
throw this.getError(OmlParser.objectNotClosed);
}
let codeUnit = this.text.charCodeAt(this.index);
if (codeUnit === 0x007D) {
this.index++;
if (this.index >= this.text.length) {
break loop;
}
codeUnit = this.text.charCodeAt(this.index);
switch (codeUnit) {
case 0x000A:
case 0x0023:
case 0x005D:
case 0x007D:
case 0x0009:
case 0x000B:
case 0x000C:
case 0x000D:
case 0x0020:
case 0x0085:
case 0x00A0:
case 0x1680:
case 0x2000:
case 0x2001:
case 0x2002:
case 0x2003:
case 0x2004:
case 0x2005:
case 0x2006:
case 0x2007:
case 0x2008:
case 0x2009:
case 0x200A:
case 0x2028:
case 0x2029:
case 0x202F:
case 0x205F:
case 0x3000:
break loop;
default:
throw this.getError(OmlParser.invalidCharacterAfterObject);
}
}
if (firstRun) {
firstRun = false;
}
else if (!hasReadWs) {
throw this.getError(OmlParser.wsRequiredAfterArrayElement);
}
let key;
if (codeUnit === 0x0022) {
key = this.parseString();
}
else {
key = this.parseValueAsString();
}
this.skipWhitespaceAndComments();
if (this.index >= this.text.length) {
throw this.getError(OmlParser.objectNotClosed);
}
codeUnit = this.text.charCodeAt(this.index);
if (codeUnit !== 0x003D) {
throw this.getError(OmlParser.equalSignExpected);
}
this.index++;
this.skipWhitespaceAndComments();
const value = this.parseAny(object, key);
object[key] = value;
}
return object;
}
parseValueAsString() {
const startIndex = this.index;
let codeUnit = this.text.charCodeAt(this.index);
valueCharLoop: for (;;) {
switch (codeUnit) {
case 0x000A:
case 0x0023:
case 0x003D:
case 0x005D:
case 0x007D:
case 0x0009:
case 0x000B:
case 0x000C:
case 0x000D:
case 0x0020:
case 0x0085:
case 0x00A0:
case 0x1680:
case 0x2000:
case 0x2001:
case 0x2002:
case 0x2003:
case 0x2004:
case 0x2005:
case 0x2006:
case 0x2007:
case 0x2008:
case 0x2009:
case 0x200A:
case 0x2028:
case 0x2029:
case 0x202F:
case 0x205F:
case 0x3000:
break valueCharLoop;
case 0x0022:
case 0x0027:
case 0x005B:
case 0x007B:
throw this.getError(OmlParser.invalidCharInValue);
}
if (codeUnit >= 0xD800 && codeUnit <= 0xDFFF) {
this.index++;
if (codeUnit >= 0xDC00 || this.index >= this.text.length) {
throw new InvalidUtf16StringError();
}
const secondCodeUnit = this.text.charCodeAt(this.index);
if (!(secondCodeUnit >= 0xDC00 && secondCodeUnit <= 0xDFFF)) {
throw new InvalidUtf16StringError();
}
}
this.index++;
if (this.index >= this.text.length) {
break valueCharLoop;
}
codeUnit = this.text.charCodeAt(this.index);
}
if (startIndex === this.index) {
throw this.getError(OmlParser.invalidCharInValue);
}
const value = this.text.substring(startIndex, this.index);
return value;
}
interpretValue(value) {
if (value === "true") {
return true;
}
else if (value === "false") {
return false;
}
else if (value === "null") {
return null;
}
else if (value.match(/^[-+]?[0-9]+(\.[0-9]+([eE][-+]?[0-9]+)?)?$/)) {
return Number.parseFloat(value);
}
else {
return value;
}
}
parseAny(owner, key) {
this.skipWhitespaceAndComments();
if (this.index >= this.text.length) {
throw this.getError(`Unexpected end of document`);
}
const codeUnit = this.text.charCodeAt(this.index);
let value;
let sourceValue = null;
switch (codeUnit) {
case 0x0022:
value = this.parseString();
break;
case 0x0027:
value = this.parseChar();
break;
case 0x005B:
value = this.parseArray();
break;
case 0x007B:
value = this.parseObject();
break;
default:
value = this.parseValueAsString();
sourceValue = value;
value = this.interpretValue(value);
break;
}
if (this.reviver !== null) {
value = this.reviver(owner, key, value, sourceValue, this.index);
}
return value;
}
getError(message, offset = 0) {
const [charIndex, lineIndex, lineCharIndex] = ReliableTxtLines.getLineInfo(this.text, this.index - offset);
return new OmlParserError(charIndex, lineIndex, lineCharIndex, message);
}
parse() {
const result = this.parseAny(null, null);
this.skipWhitespaceAndComments();
if (this.index < this.text.length) {
throw this.getError(`Only one root value allowed`);
}
return result;
}
}
OmlParser.stringNotClosed = "String not closed";
OmlParser.invalidStringLineBreak = "Invalid string line break";
OmlParser.invalidCharacterAfterString = "Invalid character after string";
OmlParser.charNotClosed = "Char not closed";
OmlParser.invalidCharSequence = "Invalid char sequence";
OmlParser.invalidCharacterAfterChar = "Invalid character after char";
OmlParser.arrayNotClosed = "Array not closed";
OmlParser.wsRequiredAfterArrayElement = "Whitespace required after array element";
OmlParser.invalidCharacterAfterArray = "Invalid character after array";
OmlParser.objectNotClosed = "Object not closed";
OmlParser.equalSignExpected = "Equal sign expected";
OmlParser.wsRequiredAfterObjectElement = "Whitespace required after object element";
OmlParser.invalidCharacterAfterObject = "Invalid character after object";
OmlParser.invalidCharInValue = "Invalid character in value";
// ----------------------------------------------------------------------
class OmlSerializer {
static validateWhitespaceString(str, onlyChar = false) {
if (onlyChar && str.length !== 1) {
throw new TypeError(`Whitespace string must be of length 1`);
}
for (let i = 0; i < str.length; i++) {
const codeUnit = str.charCodeAt(i);
switch (codeUnit) {
case 0x0009:
case 0x000B:
case 0x000C:
case 0x000D:
case 0x0020:
case 0x0085:
case 0x00A0:
case 0x1680:
case 0x2000:
case 0x2001:
case 0x2002:
case 0x2003:
case 0x2004:
case 0x2005:
case 0x2006:
case 0x2007:
case 0x2008:
case 0x2009:
case 0x200A:
case 0x2028:
case 0x2029:
case 0x202F:
case 0x205F:
case 0x3000:
continue;
default:
throw new TypeError(`Invalid code unit '${codeUnit}' in whitespace string at index ${i}`);
}
}
}
static containsSpecialChar(value) {
for (let i = 0; i < value.length; i++) {
const c = value.charCodeAt(i);
switch (c) {
case 0x0022:
case 0x0023:
case 0x0027:
case 0x003D:
case 0x005B:
case 0x005D:
case 0x007B:
case 0x007D:
case 0x000A:
case 0x0009:
case 0x000B:
case 0x000C:
case 0x000D:
case 0x0020:
case 0x0085:
case 0x00A0:
case 0x1680:
case 0x2000:
case 0x2001:
case 0x2002:
case 0x2003:
case 0x2004:
case 0x2005:
case 0x2006:
case 0x2007:
case 0x2008:
case 0x2009:
case 0x200A:
case 0x2028:
case 0x2029:
case 0x202F:
case 0x205F:
case 0x3000:
return true;
}
if (c >= 0xD800 && c <= 0xDFFF) {
i++;
if (c >= 0xDC00 || i >= value.length) {
throw new InvalidUtf16StringError();
}
const secondCodeUnit = value.charCodeAt(i);
if (!(secondCodeUnit >= 0xDC00 && secondCodeUnit <= 0xDFFF)) {
throw new InvalidUtf16StringError();
}
}
}
return false;
}
static serializeString(value) {
if (value.length === 0) {
return `""`;
}
else if (value === "true" || value === "false" || value === "null") {
return `"${value}"`;
}
else if (this.containsSpecialChar(value)) {
let size = 2;
for (let i = 0; i < value.length; i++) {
const codeUnit = value.charCodeAt(i);
switch (codeUnit) {
case 0x000A:
size += 3;
break;
case 0x0022:
size += 2;
break;
default:
size++;
}
}
const bytes = new Uint8Array(size * 2);
const view = new DataView(bytes.buffer);
view.setUint16(0, 0x0022, false);
let index = 2;
for (let i = 0; i < value.length; i++) {
const codeUnit = value.charCodeAt(i);
switch (codeUnit) {
case 0x000A:
view.setUint16(index, 0x0022, false);
index += 2;
view.setUint16(index, 0x002F, false);
index += 2;
view.setUint16(index, 0x0022, false);
index += 2;
break;
case 0x0022:
view.setUint16(index, 0x0022, false);
index += 2;
view.setUint16(index, 0x0022, false);
index += 2;
break;
default:
view.setUint16(index, codeUnit, false);
index += 2;
}
}
view.setUint16(index, 0x0022, false);
return Utf16String.fromUtf16Bytes(bytes, false, false);
}
else if (value.match(/^[-+]?[0-9]+(\.[0-9]+([eE][-+]?[0-9]+)?)?$/)) {
return `"${value}"`;
}
return value;
}
static serializeValue(value, formatting, indentStr, replacer, rootValue, strings, stack) {
if (formatting !== null && formatting.maxLevel !== undefined && stack.length > formatting.maxLevel) {
formatting = null;
}
if (replacer !== null && stack.length === 0) {
value = replacer(rootValue, null, null, value, false);
}
if (value === null) {
strings.push("null");
}
else if (typeof value === 'string' || value instanceof String) {
strings.push(this.serializeString(value));
}
else if (typeof value === 'number' || value instanceof Number) {
if (!Number.isFinite(value)) {
throw new Error(`Not allowed: "${value}"`);
}
strings.push(value.toString());
}
else if (typeof value === 'boolean' || value instanceof Boolean) {
strings.push(value.toString());
}
else if (Array.isArray(value)) {
if (stack.indexOf(value) >= 0) {
throw new Error(`Cyclic`);
}
stack.push(value);
let array = value;
if (replacer !== null) {
array = [];
for (let i = 0; i < value.length; i++) {
const curValue = value[i];
array.push(replacer(rootValue, value, i, curValue, curValue instanceof Object && stack.indexOf(curValue) >= 0));
}
}
let isReduced = false;
if (formatting !== null && formatting.reduceSimpleArray === true) {
let wasSimple = true;
for (let i = 0; i < array.length; i++) {
const curValue = array[i];
if (!(curValue === null ||
typeof curValue === 'string' || curValue instanceof String ||
typeof curValue === 'number' || curValue instanceof Number ||
typeof curValue === 'boolean' || curValue instanceof Boolean)) {
wasSimple = false;
break;
}
}
if (wasSimple) {
isReduced = true;
}
}
strings.push("[");
let curIndentLevel = indentStr;
if (formatting !== null && !isReduced) {
curIndentLevel += formatting.indentation;
strings.push("\n" + curIndentLevel);
}
for (let i = 0; i < array.length; i++) {
if (i > 0) {
if (formatting !== null && !isReduced) {
strings.push("\n" + curIndentLevel);
}
else {
strings.push(" ");
}
}
const curValue = array[i];
this.serializeValue(curValue, formatting, curIndentLevel, replacer, rootValue, strings, stack);
}
if (formatting !== null && !isReduced) {
strings.push("\n" + indentStr);
}
stack.pop();
strings.push("]");
}
else if (value instanceof Object) {
if (stack.indexOf(value) >= 0) {
throw new Error(`Cyclic`);
}
stack.push(value);
if (value["toOml"] !== undefined && typeof value["toOml"] === "function") {
const toValue = value.toOml();
this.serializeValue(toValue, formatting, indentStr, replacer, rootValue, strings, stack);
return;
}
strings.push("{");
let curIndentLevel = indentStr;
let maxLength = 0;
const entries = Object.entries(value);
const serializedKeys = [];
const lengths = [];
for (const [key] of entries) {
const serializedKey = this.serializeString(key);
serializedKeys.push(serializedKey);
if (formatting !== null && formatting.alignChar !== null) {
const length = Utf16String.getCodePointCount(serializedKey);
maxLength = Math.max(maxLength, length);
lengths.push(length);
}
}
if (formatting !== null) {
curIndentLevel += formatting.indentation;
strings.push("\n" + curIndentLevel);
}
let isFirst = true;
for (let i = 0; i < entries.length; i++) {
const serializedKey = serializedKeys[i];
let curValue = entries[i][1];
if (isFirst) {
isFirst = false;
}
else {
if (formatting !== null) {
strings.push("\n" + curIndentLevel);
}
else {
strings.push(" ");
}
}
strings.push(serializedKey);
if (formatting !== null) {
if (formatting.alignChar !== null) {
const length = lengths[i];
const dif = maxLength - length;
if (dif > 0) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
strings.push(formatting.alignChar.repeat(dif));
}
}
strings.push(`${formatting.beforeEqual}=${formatting.afterEqual}`);
}
else {
strings.push("=");
}
if (replacer !== null) {
curValue = replacer(rootValue, value, entries[i][0], curValue, curValue instanceof Object && stack.indexOf(curValue) >= 0);
}
this.serializeValue(curValue, formatting, curIndentLevel, replacer, rootValue, strings, stack);
}
if (formatting !== null) {
strings.push("\n" + indentStr);
}
stack.pop();
strings.push("}");
}
else {
throw new Error(`Not allowed: "${value}"`);
}
}
}
//# sourceMappingURL=oml.js.map