Actually, the message format is LLSD notation not python (d'oh)
This commit is contained in:
@@ -38,9 +38,8 @@ import { ObjectResolvedEvent } from '../events/ObjectResolvedEvent';
|
||||
import { Avatar } from './public/Avatar';
|
||||
import Timer = NodeJS.Timer;
|
||||
import { GenericStreamingMessageMessage } from './messages/GenericStreamingMessage';
|
||||
import { PythonParser } from './python/PythonParser';
|
||||
import { PythonDict } from './python/PythonDict';
|
||||
import { PythonList } from './python/PythonList';
|
||||
import { LLSDNotationParser } from './llsd/LLSDNotationParser';
|
||||
import { LLSDMap } from './llsd/LLSDMap';
|
||||
|
||||
export class ObjectStoreLite implements IObjectStore
|
||||
{
|
||||
@@ -85,12 +84,12 @@ export class ObjectStoreLite implements IObjectStore
|
||||
const genMsg = packet.message as GenericStreamingMessageMessage;
|
||||
if (genMsg.MethodData.Method === 0x4175)
|
||||
{
|
||||
// Whoever decided to use python notation for this is a psychopath
|
||||
const result = PythonParser.parse(genMsg.DataBlock.Data.toString('utf-8'));
|
||||
if (result instanceof PythonDict)
|
||||
// LLSD Notation format
|
||||
const result = LLSDNotationParser.parse(genMsg.DataBlock.Data.toString('utf-8'));
|
||||
if (result instanceof LLSDMap)
|
||||
{
|
||||
const arr = result.get('te');
|
||||
if (arr instanceof PythonList)
|
||||
if (Array.isArray(arr))
|
||||
{
|
||||
if (arr.length === 0)
|
||||
{
|
||||
@@ -98,7 +97,7 @@ export class ObjectStoreLite implements IObjectStore
|
||||
}
|
||||
|
||||
// TODO: figure out what to do with this..
|
||||
// console.log(JSON.stringify(result, null, 4));
|
||||
console.log(JSON.stringify(result, null, 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,9 @@ export class CommunicationsCommands extends CommandsBase
|
||||
Message: Utils.StringToBuffer(itemOrFolder.name),
|
||||
BinaryBucket: bucket
|
||||
};
|
||||
im.EstateBlock = {
|
||||
EstateID: 0
|
||||
};
|
||||
const sequenceNo = circuit.sendMessage(im, PacketFlags.Reliable);
|
||||
return await circuit.waitForAck(sequenceNo, 10000);
|
||||
}
|
||||
@@ -99,6 +102,9 @@ export class CommunicationsCommands extends CommandsBase
|
||||
Message: Utils.StringToBuffer(message),
|
||||
BinaryBucket: Buffer.allocUnsafe(0)
|
||||
};
|
||||
im.EstateBlock = {
|
||||
EstateID: 0
|
||||
};
|
||||
const sequenceNo = circuit.sendMessage(im, PacketFlags.Reliable);
|
||||
return await circuit.waitForAck(sequenceNo, 10000);
|
||||
}
|
||||
@@ -223,6 +229,9 @@ export class CommunicationsCommands extends CommandsBase
|
||||
Message: Utils.StringToBuffer(''),
|
||||
BinaryBucket: Buffer.allocUnsafe(0)
|
||||
};
|
||||
im.EstateBlock = {
|
||||
EstateID: 0
|
||||
};
|
||||
const sequenceNo = circuit.sendMessage(im, PacketFlags.Reliable);
|
||||
return await circuit.waitForAck(sequenceNo, 10000);
|
||||
}
|
||||
@@ -254,6 +263,9 @@ export class CommunicationsCommands extends CommandsBase
|
||||
Message: Utils.StringToBuffer(''),
|
||||
BinaryBucket: Buffer.allocUnsafe(0)
|
||||
};
|
||||
im.EstateBlock = {
|
||||
EstateID: 0
|
||||
};
|
||||
const sequenceNo = circuit.sendMessage(im, PacketFlags.Reliable);
|
||||
return await circuit.waitForAck(sequenceNo, 10000);
|
||||
}
|
||||
@@ -407,6 +419,9 @@ export class CommunicationsCommands extends CommandsBase
|
||||
Message: Buffer.allocUnsafe(0),
|
||||
BinaryBucket: Buffer.allocUnsafe(0)
|
||||
};
|
||||
im.EstateBlock = {
|
||||
EstateID: 0
|
||||
};
|
||||
this.agent.deleteChatSession(groupID);
|
||||
const sequenceNo = this.circuit.sendMessage(im, PacketFlags.Reliable);
|
||||
return this.circuit.waitForAck(sequenceNo, 10000);
|
||||
@@ -445,6 +460,9 @@ export class CommunicationsCommands extends CommandsBase
|
||||
Message: Utils.StringToBuffer(message),
|
||||
BinaryBucket: Utils.StringToBuffer('')
|
||||
};
|
||||
im.EstateBlock = {
|
||||
EstateID: 0
|
||||
};
|
||||
circuit.sendMessage(im, PacketFlags.Reliable);
|
||||
await Utils.waitOrTimeOut(this.currentRegion.clientEvents.onGroupChatSessionJoin, 10000, (event: GroupChatSessionJoinEvent) =>
|
||||
{
|
||||
@@ -514,6 +532,9 @@ export class CommunicationsCommands extends CommandsBase
|
||||
Message: Utils.StringToBuffer(message),
|
||||
BinaryBucket: Utils.StringToBuffer('')
|
||||
};
|
||||
im.EstateBlock = {
|
||||
EstateID: 0
|
||||
};
|
||||
const sequenceNo = circuit.sendMessage(im, PacketFlags.Reliable);
|
||||
await this.circuit.waitForAck(sequenceNo, 10000);
|
||||
|
||||
|
||||
@@ -307,6 +307,9 @@ export class FriendCommands extends CommandsBase
|
||||
Message: Utils.StringToBuffer(message),
|
||||
BinaryBucket: Utils.StringToBuffer('')
|
||||
};
|
||||
im.EstateBlock = {
|
||||
EstateID: 0
|
||||
};
|
||||
const sequenceNo = this.circuit.sendMessage(im, PacketFlags.Reliable);
|
||||
return await this.circuit.waitForAck(sequenceNo, 10000);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,9 @@ export class GroupCommands extends CommandsBase
|
||||
Message: Utils.StringToBuffer(subject + '|' + message),
|
||||
BinaryBucket: Buffer.allocUnsafe(0)
|
||||
};
|
||||
im.EstateBlock = {
|
||||
EstateID: 0
|
||||
};
|
||||
const sequenceNo = circuit.sendMessage(im, PacketFlags.Reliable);
|
||||
return await circuit.waitForAck(sequenceNo, 10000);
|
||||
}
|
||||
@@ -137,6 +140,9 @@ export class GroupCommands extends CommandsBase
|
||||
Message: Utils.StringToBuffer(''),
|
||||
BinaryBucket: Buffer.allocUnsafe(0)
|
||||
};
|
||||
im.EstateBlock = {
|
||||
EstateID: 0
|
||||
};
|
||||
const sequenceNo = circuit.sendMessage(im, PacketFlags.Reliable);
|
||||
return await circuit.waitForAck(sequenceNo, 10000);
|
||||
}
|
||||
@@ -164,6 +170,9 @@ export class GroupCommands extends CommandsBase
|
||||
Message: Utils.StringToBuffer(''),
|
||||
BinaryBucket: Buffer.allocUnsafe(0)
|
||||
};
|
||||
im.EstateBlock = {
|
||||
EstateID: 0
|
||||
};
|
||||
const sequenceNo = circuit.sendMessage(im, PacketFlags.Reliable);
|
||||
return await circuit.waitForAck(sequenceNo, 10000);
|
||||
}
|
||||
|
||||
53
lib/classes/llsd/LLSDArray.ts
Normal file
53
lib/classes/llsd/LLSDArray.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { LLSDTokenGenerator } from './LLSDTokenGenerator';
|
||||
import { LLSDTokenType } from './LLSDTokenType';
|
||||
import { LLSDNotationParser } from './LLSDNotationParser';
|
||||
import { LLSDType } from './LLSDType';
|
||||
|
||||
export class LLSDArray
|
||||
{
|
||||
public static parse(gen: LLSDTokenGenerator): LLSDType[]
|
||||
{
|
||||
const arr: LLSDType[] = [];
|
||||
let value: LLSDType | undefined = undefined;
|
||||
while (true)
|
||||
{
|
||||
const token = gen();
|
||||
if (token === undefined)
|
||||
{
|
||||
throw new Error('Unexpected end of input in array');
|
||||
}
|
||||
switch (token.type)
|
||||
{
|
||||
case LLSDTokenType.WHITESPACE:
|
||||
{
|
||||
continue;
|
||||
}
|
||||
case LLSDTokenType.ARRAY_END:
|
||||
{
|
||||
if (value !== undefined)
|
||||
{
|
||||
arr.push(value);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
case LLSDTokenType.COMMA:
|
||||
{
|
||||
if (value === undefined)
|
||||
{
|
||||
throw new Error('Expected value before comma');
|
||||
}
|
||||
arr.push(value);
|
||||
value = undefined;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (value !== undefined)
|
||||
{
|
||||
throw new Error('Comma or end brace expected');
|
||||
}
|
||||
|
||||
value = LLSDNotationParser.parseValueToken(gen, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
115
lib/classes/llsd/LLSDMap.ts
Normal file
115
lib/classes/llsd/LLSDMap.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { LLSDObject } from './LLSDObject';
|
||||
import { LLSDTokenGenerator } from './LLSDTokenGenerator';
|
||||
import { LLSDTokenType } from './LLSDTokenType';
|
||||
import { LLSDNotationParser } from './LLSDNotationParser';
|
||||
import { LLSDType } from './LLSDType';
|
||||
|
||||
export class LLSDMap extends LLSDObject
|
||||
{
|
||||
public data: Map<LLSDType, LLSDType> = new Map<LLSDType, LLSDType>();
|
||||
|
||||
public static parse(gen: LLSDTokenGenerator): LLSDMap
|
||||
{
|
||||
const m = new LLSDMap();
|
||||
let expectsKey = true
|
||||
let key: LLSDType | undefined = undefined;
|
||||
let value: LLSDType | undefined = undefined;
|
||||
while (true)
|
||||
{
|
||||
const token = gen();
|
||||
if (token === undefined)
|
||||
{
|
||||
throw new Error('Unexpected end of input in map');
|
||||
}
|
||||
switch (token.type)
|
||||
{
|
||||
case LLSDTokenType.WHITESPACE:
|
||||
{
|
||||
continue;
|
||||
}
|
||||
case LLSDTokenType.MAP_END:
|
||||
{
|
||||
if (expectsKey)
|
||||
{
|
||||
throw new Error('Unexpected end of map');
|
||||
}
|
||||
if (key !== undefined && value !== undefined)
|
||||
{
|
||||
m.data.set(key, value);
|
||||
}
|
||||
else if (m.data.size > 0)
|
||||
{
|
||||
throw new Error('Expected value before end of map');
|
||||
}
|
||||
return m;
|
||||
}
|
||||
case LLSDTokenType.COLON:
|
||||
{
|
||||
if (!expectsKey)
|
||||
{
|
||||
throw new Error('Unexpected symbol: :');
|
||||
}
|
||||
if (key === undefined)
|
||||
{
|
||||
throw new Error('Empty key not allowed');
|
||||
}
|
||||
expectsKey = false;
|
||||
continue;
|
||||
}
|
||||
case LLSDTokenType.COMMA:
|
||||
{
|
||||
if (expectsKey)
|
||||
{
|
||||
throw new Error('Empty map entry not allowed');
|
||||
}
|
||||
if (value === undefined)
|
||||
{
|
||||
throw new Error('Empty map value not allowed');
|
||||
}
|
||||
if (key !== undefined)
|
||||
{
|
||||
m.data.set(key, value);
|
||||
}
|
||||
key = undefined;
|
||||
value = undefined;
|
||||
expectsKey = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (expectsKey && key !== undefined)
|
||||
{
|
||||
throw new Error('Colon expected');
|
||||
}
|
||||
else if (value !== undefined)
|
||||
{
|
||||
throw new Error('Comma or end brace expected');
|
||||
}
|
||||
|
||||
const val = LLSDNotationParser.parseValueToken(gen, token);
|
||||
if (expectsKey)
|
||||
{
|
||||
key = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get length(): number
|
||||
{
|
||||
return Object.keys(this.data).length;
|
||||
}
|
||||
|
||||
public get(key: LLSDType): LLSDType | undefined
|
||||
{
|
||||
return this.data.get(key);
|
||||
}
|
||||
|
||||
public toJSON(): unknown
|
||||
{
|
||||
return Object.fromEntries(this.data);
|
||||
}
|
||||
}
|
||||
204
lib/classes/llsd/LLSDNotationParser.spec.ts
Normal file
204
lib/classes/llsd/LLSDNotationParser.spec.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import * as assert from 'assert';
|
||||
import { LLSDNotationParser } from './LLSDNotationParser';
|
||||
import { LLSDMap } from './LLSDMap';
|
||||
import { UUID } from '../UUID';
|
||||
|
||||
describe('LLSDNotationParser', () =>
|
||||
{
|
||||
describe('parse', () =>
|
||||
{
|
||||
it('can parse a complex LLSD Notation document', () =>
|
||||
{
|
||||
const notationDoc = `{
|
||||
'nested_map': {
|
||||
'nested_again': {
|
||||
'array': [
|
||||
i0,
|
||||
'string',
|
||||
"string2",
|
||||
[
|
||||
"another",
|
||||
"array",
|
||||
r4.3,
|
||||
i22,
|
||||
!,
|
||||
{
|
||||
'isThis': i42
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
'undef': !,
|
||||
'booleans': [
|
||||
true,
|
||||
false,
|
||||
1,
|
||||
0,
|
||||
T,
|
||||
F,
|
||||
t,
|
||||
f,
|
||||
TRUE,
|
||||
FALSE
|
||||
],
|
||||
'integer': i69,
|
||||
'negInt': i-69,
|
||||
'real': r3.141,
|
||||
'realNeg': r-3.141,
|
||||
'uuid': ufb54f6b1-8120-40c9-9aa3-f9abef1a168f,
|
||||
'string1': "gday\\"mate",
|
||||
'string2': 'gday\\'mate',
|
||||
'string3': s(11)"hello"there",
|
||||
'uri': l"https://secondlife.com/",
|
||||
'date': d"2023-11-10T13:32:32.93Z",
|
||||
'binary': b64"amVsbHlmaXNo",
|
||||
'binary2': b16"6261636F6E62697473",
|
||||
'binary3': b(32)"KÚ~¬\béGÀt|Ïh,9µEK¹*;]ÆÁåb/"
|
||||
}`;
|
||||
const parsed = LLSDNotationParser.parse(notationDoc);
|
||||
if (!(parsed instanceof LLSDMap))
|
||||
{
|
||||
assert('Parsed document is not a map');
|
||||
return;
|
||||
}
|
||||
|
||||
const nested = parsed.get('nested_map');
|
||||
if (nested instanceof LLSDMap)
|
||||
{
|
||||
const nestedAgain = nested.get('nested_again');
|
||||
if (nestedAgain instanceof LLSDMap)
|
||||
{
|
||||
const arr = nestedAgain.get('array');
|
||||
if (!Array.isArray(arr))
|
||||
{
|
||||
assert(false, 'Nested array is not an array');
|
||||
}
|
||||
else
|
||||
{
|
||||
assert.equal(arr.length, 4);
|
||||
assert.equal(arr[0], 0);
|
||||
assert.equal(arr[1], 'string');
|
||||
assert.equal(arr[2], 'string2');
|
||||
const nestedAgainArr = arr[3];
|
||||
if (!Array.isArray(nestedAgainArr))
|
||||
{
|
||||
assert(false, 'Nested again array is not an array');
|
||||
}
|
||||
else
|
||||
{
|
||||
assert.equal(nestedAgainArr.length, 6);
|
||||
assert.equal(nestedAgainArr[0], 'another');
|
||||
assert.equal(nestedAgainArr[1], 'array');
|
||||
assert.equal(nestedAgainArr[2], 4.3);
|
||||
assert.equal(nestedAgainArr[3], 22);
|
||||
assert.equal(nestedAgainArr[4], null);
|
||||
const thirdNestedMap = nestedAgainArr[5];
|
||||
if (thirdNestedMap instanceof LLSDMap)
|
||||
{
|
||||
assert.equal(thirdNestedMap.get('isThis'), 42);
|
||||
}
|
||||
else
|
||||
{
|
||||
assert(false, 'Third nested map is not a map');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
assert(false, 'NestedAgain is not a map');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
assert(false, 'Nested is not a map');
|
||||
}
|
||||
assert.equal(parsed.get('undef'), null);
|
||||
const bools = parsed.get('booleans');
|
||||
if (!Array.isArray(bools))
|
||||
{
|
||||
assert(false, 'Booleans array is not bools');
|
||||
}
|
||||
else
|
||||
{
|
||||
assert.equal(bools.length, 10);
|
||||
assert.equal(bools[0], true);
|
||||
assert.equal(bools[1], false);
|
||||
assert.equal(bools[2], true);
|
||||
assert.equal(bools[3], false);
|
||||
assert.equal(bools[4], true);
|
||||
assert.equal(bools[5], false);
|
||||
assert.equal(bools[6], true);
|
||||
assert.equal(bools[7], false);
|
||||
assert.equal(bools[8], true);
|
||||
assert.equal(bools[9], false);
|
||||
}
|
||||
|
||||
/*
|
||||
'string1': "gday\"mate",
|
||||
'string2': 'gday\'mate',
|
||||
'string3': s(11)"hello"there",
|
||||
'uri': l"https://secondlife.com/",
|
||||
'date': d"2023-11-10T13:32:32.93Z",
|
||||
'binary': b64"amVsbHlmaXNo",
|
||||
'binary2': b16"6261636F6E62697473",
|
||||
'binary3': b(32)"KÚ~¬éGÀt|Ïh,9µEK¹*;]ÆÁåb/"
|
||||
*/
|
||||
assert.equal(parsed.get('integer'), 69);
|
||||
assert.equal(parsed.get('negInt'), -69);
|
||||
assert.equal(parsed.get('real'), 3.141);
|
||||
assert.equal(parsed.get('realNeg'), -3.141);
|
||||
const u = parsed.get('uuid');
|
||||
if (u instanceof UUID)
|
||||
{
|
||||
assert.equal(u.equals('fb54f6b1-8120-40c9-9aa3-f9abef1a168f'), true);
|
||||
}
|
||||
else
|
||||
{
|
||||
assert(false, 'UUID is not a UUID');
|
||||
}
|
||||
assert.equal(parsed.get('string1'), 'gday"mate');
|
||||
assert.equal(parsed.get('string2'), 'gday\'mate');
|
||||
assert.equal(parsed.get('string3'), 'hello"there');
|
||||
assert.equal(parsed.get('uri'), 'https://secondlife.com/');
|
||||
const d = parsed.get('date');
|
||||
if (d instanceof Date)
|
||||
{
|
||||
assert.equal(d.getTime(), 1699623152930);
|
||||
}
|
||||
else
|
||||
{
|
||||
assert(false, 'Date entry is not a date');
|
||||
}
|
||||
const buf1 = parsed.get('binary');
|
||||
if (buf1 instanceof Buffer)
|
||||
{
|
||||
assert.equal(buf1.toString('utf-8'), 'jellyfish');
|
||||
}
|
||||
else
|
||||
{
|
||||
assert(false, 'Buf1 is not a buffer');
|
||||
}
|
||||
const buf2 = parsed.get('binary2');
|
||||
if (buf2 instanceof Buffer)
|
||||
{
|
||||
assert.equal(buf2.toString('utf-8'), 'baconbits');
|
||||
}
|
||||
else
|
||||
{
|
||||
assert(false, 'Buf2 is not a buffer');
|
||||
}
|
||||
const buf3 = parsed.get('binary3');
|
||||
if (buf3 instanceof Buffer)
|
||||
{
|
||||
assert.equal(buf3.equals(Buffer.from('KÚ~¬\béGÀt|Ïh,9µEK¹*;]ÆÁåb/', 'binary')), true);
|
||||
}
|
||||
else
|
||||
{
|
||||
assert(false, 'Buf3 is not a buffer');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
320
lib/classes/llsd/LLSDNotationParser.ts
Normal file
320
lib/classes/llsd/LLSDNotationParser.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
import { LLSDTokenType } from './LLSDTokenType';
|
||||
import { LLSDType } from './LLSDType';
|
||||
import { LLSDTokenSpec } from './LLSDTokenSpec';
|
||||
import { LLSDToken } from './LLSDToken';
|
||||
import { LLSDTokenGenerator } from './LLSDTokenGenerator';
|
||||
import { LLSDMap } from './LLSDMap';
|
||||
import { UUID } from '../UUID';
|
||||
import { LLSDArray } from './LLSDArray';
|
||||
|
||||
export class LLSDNotationParser
|
||||
{
|
||||
private static tokenSpecs: LLSDTokenSpec[] =
|
||||
[
|
||||
{ regex: /^\s+/, type: LLSDTokenType.WHITESPACE },
|
||||
{ regex: /^!/, type: LLSDTokenType.NULL },
|
||||
{ regex: /^\{/, type: LLSDTokenType.MAP_START },
|
||||
{ regex: /^}/, type: LLSDTokenType.MAP_END },
|
||||
{ regex: /^:/, type: LLSDTokenType.COLON },
|
||||
{ regex: /^,/, type: LLSDTokenType.COMMA },
|
||||
{ regex: /^\[/, type: LLSDTokenType.ARRAY_START },
|
||||
{ regex: /^]/, type: LLSDTokenType.ARRAY_END },
|
||||
{ regex: /^(?:true|false|TRUE|FALSE|1|0|T|F|t|f)/, type: LLSDTokenType.BOOLEAN },
|
||||
{ regex: /^i(-?[0-9]+)/, type: LLSDTokenType.INTEGER },
|
||||
{ regex: /^r(-?[0-9.]+)/, type: LLSDTokenType.REAL },
|
||||
{ regex: /^u([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/, type: LLSDTokenType.UUID },
|
||||
{ regex: /^'([^'\\]*(?:\\.[^'\\\n]*)*)'/, type: LLSDTokenType.STRING_FIXED_SINGLE },
|
||||
{ regex: /^"([^"\\]*(?:\\.[^"\\\n]*)*)"/, type: LLSDTokenType.STRING_FIXED_DOUBLE },
|
||||
{ regex: /^s\(([0-9]+)\)"/, type: LLSDTokenType.STRING_DYNAMIC_START },
|
||||
{ regex: /^l"([^"]*?)"/, type: LLSDTokenType.URI },
|
||||
{ regex: /^d"([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{2}Z)"/, type: LLSDTokenType.DATE },
|
||||
{ regex: /^b[0-9]{2}"[0-9a-zA-Z+\/=]*?"/, type: LLSDTokenType.BINARY_STATIC },
|
||||
{ regex: /^b\(([0-9]+)\)"/, type: LLSDTokenType.BINARY_DYNAMIC_START }
|
||||
];
|
||||
|
||||
private static* tokenize(inpt: string): Generator<LLSDToken | void>
|
||||
{
|
||||
const dataContainer = {
|
||||
input: inpt,
|
||||
index: 0
|
||||
}
|
||||
while (dataContainer.index < dataContainer.input.length)
|
||||
{
|
||||
const currentInput = dataContainer.input.slice(dataContainer.index);
|
||||
|
||||
if (currentInput.length === 0)
|
||||
{
|
||||
return; // End of input
|
||||
}
|
||||
|
||||
let matched = false;
|
||||
for (const { regex, type } of this.tokenSpecs)
|
||||
{
|
||||
const tokenMatch = currentInput.match(regex);
|
||||
if (tokenMatch)
|
||||
{
|
||||
matched = true;
|
||||
let value = tokenMatch[0];
|
||||
if (tokenMatch.length > 1)
|
||||
{
|
||||
value = tokenMatch[tokenMatch.length - 1];
|
||||
}
|
||||
dataContainer.index += tokenMatch[0].length; // Move past this token
|
||||
|
||||
yield { type, value, rawValue: tokenMatch[0], dataContainer };
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched)
|
||||
{
|
||||
dataContainer.index++;
|
||||
yield { type: LLSDTokenType.UNKNOWN, value: dataContainer.input[dataContainer.index - 1], rawValue: dataContainer.input[dataContainer.index - 1], dataContainer };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static parseValueToken(gen: LLSDTokenGenerator, initialToken?: LLSDToken): LLSDType
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
let t: LLSDToken | undefined;
|
||||
if (initialToken !== undefined)
|
||||
{
|
||||
t = initialToken;
|
||||
initialToken = undefined;
|
||||
}
|
||||
else
|
||||
{
|
||||
t = gen();
|
||||
if (t === undefined)
|
||||
{
|
||||
throw new Error('Unexpected end of input');
|
||||
}
|
||||
}
|
||||
switch (t.type)
|
||||
{
|
||||
case LLSDTokenType.UNKNOWN:
|
||||
{
|
||||
throw new Error('Unexpected token: ' + t.value);
|
||||
}
|
||||
case LLSDTokenType.WHITESPACE:
|
||||
{
|
||||
continue;
|
||||
}
|
||||
case LLSDTokenType.NULL:
|
||||
{
|
||||
return null;
|
||||
}
|
||||
case LLSDTokenType.BOOLEAN:
|
||||
{
|
||||
return t.value === 'true' || t.value === 'TRUE' || t.value === 'T' || t.value === 't' || t.value === '1';
|
||||
}
|
||||
case LLSDTokenType.INTEGER:
|
||||
{
|
||||
return parseInt(t.value, 10);
|
||||
}
|
||||
case LLSDTokenType.REAL:
|
||||
{
|
||||
return parseFloat(t.value);
|
||||
}
|
||||
case LLSDTokenType.UUID:
|
||||
{
|
||||
return new UUID(t.value);
|
||||
}
|
||||
case LLSDTokenType.STRING_FIXED_SINGLE:
|
||||
{
|
||||
return t.value.replace(/\\'/, '\'')
|
||||
.replace(/\\\\/g, '\\');
|
||||
}
|
||||
case LLSDTokenType.STRING_FIXED_DOUBLE:
|
||||
{
|
||||
return t.value.replace(/\\"/, '"')
|
||||
.replace(/\\\\/g, '\\');
|
||||
}
|
||||
case LLSDTokenType.URI:
|
||||
{
|
||||
return t.value;
|
||||
}
|
||||
case LLSDTokenType.DATE:
|
||||
{
|
||||
return new Date(t.value);
|
||||
}
|
||||
case LLSDTokenType.BINARY_STATIC:
|
||||
{
|
||||
const b = t.value.match(/^b([0-9]{2})"([0-9a-zA-Z+\/=]*?)"/);
|
||||
if (b === null || b.length < 3)
|
||||
{
|
||||
throw new Error('Invalid BINARY_STATIC');
|
||||
}
|
||||
const base = parseInt(b[1], 10);
|
||||
if (base !== 16 && base !== 64)
|
||||
{
|
||||
throw new Error('Unsupported base ' + String(base));
|
||||
}
|
||||
return Buffer.from(b[2], base === 64 ? 'base64' : 'hex');
|
||||
}
|
||||
case LLSDTokenType.STRING_DYNAMIC_START:
|
||||
{
|
||||
const length = parseInt(t.value, 10);
|
||||
const s = t.dataContainer.input.slice(t.dataContainer.index, t.dataContainer.index + length);
|
||||
t.dataContainer.index += length;
|
||||
if (t.dataContainer.input[t.dataContainer.index] !== '"')
|
||||
{
|
||||
throw new Error('Expected " at end of dynamic string')
|
||||
}
|
||||
t.dataContainer.index += 1;
|
||||
return s;
|
||||
}
|
||||
case LLSDTokenType.BINARY_DYNAMIC_START:
|
||||
{
|
||||
const length = parseInt(t.value, 10);
|
||||
const s = t.dataContainer.input.slice(t.dataContainer.index, t.dataContainer.index + length);
|
||||
t.dataContainer.index += length;
|
||||
if (t.dataContainer.input[t.dataContainer.index] !== '"')
|
||||
{
|
||||
throw new Error('Expected " at end of dynamic binary string')
|
||||
}
|
||||
t.dataContainer.index += 1;
|
||||
return Buffer.from(s, 'binary');
|
||||
}
|
||||
case LLSDTokenType.MAP_START:
|
||||
{
|
||||
return LLSDMap.parse(gen);
|
||||
}
|
||||
case LLSDTokenType.ARRAY_START:
|
||||
{
|
||||
return LLSDArray.parse(gen);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static parse(input: string): LLSDType
|
||||
{
|
||||
const generator = this.tokenize(input);
|
||||
const getToken: LLSDTokenGenerator = (): LLSDToken | undefined =>
|
||||
{
|
||||
return generator.next().value;
|
||||
}
|
||||
const data = this.parseValueToken(getToken);
|
||||
do
|
||||
{
|
||||
const t = getToken();
|
||||
if (t === undefined)
|
||||
{
|
||||
return data;
|
||||
}
|
||||
if (t.type !== LLSDTokenType.WHITESPACE)
|
||||
{
|
||||
throw new Error('Unexpected token at end of document: ' + t.value);
|
||||
}
|
||||
}
|
||||
while (true);
|
||||
}
|
||||
|
||||
/*
|
||||
private static readObject(cont: LLSDParserContainer): LLSDType
|
||||
{
|
||||
let tokenType: LLSDTokenType | null = null;
|
||||
const stack: LLSDObject[] = [];
|
||||
while (cont.pos < cont.str.length)
|
||||
{
|
||||
const subString = cont.str.slice(cont.pos);
|
||||
let value: string | null = null;
|
||||
for (const { regex, type } of this.tokenSpecs)
|
||||
{
|
||||
const tokenMatch = subString.match(regex);
|
||||
if (tokenMatch)
|
||||
{
|
||||
value = tokenMatch[0];
|
||||
tokenType = type;
|
||||
if (tokenMatch.length > 1)
|
||||
{
|
||||
value = tokenMatch[tokenMatch.length - 1];
|
||||
}
|
||||
cont.pos += tokenMatch[0].length; // Move past this token
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tokenType === null)
|
||||
{
|
||||
tokenType = LLSDTokenType.UNKNOWN;
|
||||
value = cont.str[cont.pos++];
|
||||
}
|
||||
if (stack.length > 0)
|
||||
{
|
||||
if (stack[stack.length - 1].acceptToken(tokenType, value))
|
||||
{
|
||||
// stack object completed
|
||||
}
|
||||
}
|
||||
switch (tokenType)
|
||||
{
|
||||
case LLSDTokenType.WHITESPACE:
|
||||
{
|
||||
continue;
|
||||
}
|
||||
case LLSDTokenType.UNKNOWN:
|
||||
{
|
||||
throw new Error('Unexpected token: ' + value);
|
||||
}
|
||||
case LLSDTokenType.NULL:
|
||||
{
|
||||
return null;
|
||||
}
|
||||
case LLSDTokenType.MAP_START:
|
||||
{
|
||||
|
||||
break;
|
||||
}
|
||||
case LLSDTokenType.MAP_END:
|
||||
break;
|
||||
case LLSDTokenType.COLON:
|
||||
break;
|
||||
case LLSDTokenType.COMMA:
|
||||
break;
|
||||
case LLSDTokenType.ARRAY_START:
|
||||
break;
|
||||
case LLSDTokenType.ARRAY_END:
|
||||
break;
|
||||
case LLSDTokenType.BOOLEAN:
|
||||
break;
|
||||
case LLSDTokenType.INTEGER:
|
||||
break;
|
||||
case LLSDTokenType.REAL:
|
||||
break;
|
||||
case LLSDTokenType.UUID:
|
||||
break;
|
||||
case LLSDTokenType.STRING_FIXED:
|
||||
break;
|
||||
case LLSDTokenType.STRING_DYNAMIC_START:
|
||||
break;
|
||||
case LLSDTokenType.URI:
|
||||
break;
|
||||
case LLSDTokenType.DATE:
|
||||
break;
|
||||
case LLSDTokenType.BINARY_STATIC:
|
||||
break;
|
||||
case LLSDTokenType.BINARY_DYNAMIC_START:
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static parse(input: string): LLSDType
|
||||
{
|
||||
const cont: LLSDParserContainer = {
|
||||
str: input,
|
||||
pos: 0
|
||||
};
|
||||
|
||||
const token = this.readObject(cont);
|
||||
if (cont.pos < input.length)
|
||||
{
|
||||
throw new Error('Only one root object expected');
|
||||
}
|
||||
return token;
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export abstract class PythonObject
|
||||
export abstract class LLSDObject
|
||||
{
|
||||
public toString(): string
|
||||
{
|
||||
12
lib/classes/llsd/LLSDToken.ts
Normal file
12
lib/classes/llsd/LLSDToken.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { LLSDTokenType } from './LLSDTokenType';
|
||||
|
||||
export interface LLSDToken
|
||||
{
|
||||
type: LLSDTokenType;
|
||||
value: string;
|
||||
rawValue: string;
|
||||
dataContainer: {
|
||||
input: string,
|
||||
index: number
|
||||
}
|
||||
}
|
||||
9
lib/classes/llsd/LLSDTokenContainer.ts
Normal file
9
lib/classes/llsd/LLSDTokenContainer.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { LLSDToken } from './LLSDToken';
|
||||
import { LLSDTokenGenerator } from './LLSDTokenGenerator';
|
||||
|
||||
export interface LLSDTokenContainer
|
||||
{
|
||||
tokens: LLSDToken[];
|
||||
index: number;
|
||||
gen: LLSDTokenGenerator;
|
||||
}
|
||||
3
lib/classes/llsd/LLSDTokenGenerator.ts
Normal file
3
lib/classes/llsd/LLSDTokenGenerator.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { LLSDToken } from './LLSDToken';
|
||||
|
||||
export type LLSDTokenGenerator = () => LLSDToken | undefined;
|
||||
7
lib/classes/llsd/LLSDTokenSpec.ts
Normal file
7
lib/classes/llsd/LLSDTokenSpec.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { LLSDTokenType } from './LLSDTokenType';
|
||||
|
||||
export interface LLSDTokenSpec
|
||||
{
|
||||
regex: RegExp;
|
||||
type: LLSDTokenType;
|
||||
}
|
||||
23
lib/classes/llsd/LLSDTokenType.ts
Normal file
23
lib/classes/llsd/LLSDTokenType.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export enum LLSDTokenType
|
||||
{
|
||||
UNKNOWN = 0,
|
||||
WHITESPACE,
|
||||
NULL,
|
||||
MAP_START,
|
||||
MAP_END,
|
||||
COLON,
|
||||
COMMA,
|
||||
ARRAY_START,
|
||||
ARRAY_END,
|
||||
BOOLEAN,
|
||||
INTEGER,
|
||||
REAL,
|
||||
UUID,
|
||||
STRING_FIXED_SINGLE,
|
||||
STRING_FIXED_DOUBLE,
|
||||
STRING_DYNAMIC_START,
|
||||
URI,
|
||||
DATE,
|
||||
BINARY_STATIC,
|
||||
BINARY_DYNAMIC_START,
|
||||
}
|
||||
4
lib/classes/llsd/LLSDType.ts
Normal file
4
lib/classes/llsd/LLSDType.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { LLSDObject } from './LLSDObject';
|
||||
import { UUID } from '../UUID';
|
||||
|
||||
export type LLSDType = number | boolean | string | Buffer | LLSDObject | null | UUID | Date | LLSDType[];
|
||||
@@ -1,109 +0,0 @@
|
||||
import { PythonTokenContainer } from './PythonTokenContainer';
|
||||
import { PythonObject } from './PythonObject';
|
||||
import { PythonTokenType } from './PythonTokenType';
|
||||
import { PythonType } from './PythonType';
|
||||
import { PythonParser } from './PythonParser';
|
||||
import { PythonTuple } from './PythonTuple';
|
||||
|
||||
// Define PythonKey type for dictionary keys
|
||||
export type PythonKey = string | boolean | PythonType[] | number | PythonTuple;
|
||||
|
||||
export class PythonDict extends PythonObject
|
||||
{
|
||||
public data: Map<PythonKey, PythonType> = new Map<PythonKey, PythonType>();
|
||||
|
||||
public static parse(container: PythonTokenContainer): PythonDict
|
||||
{
|
||||
const dict = new PythonDict();
|
||||
|
||||
let isKey = true;
|
||||
let key: PythonKey | null = null;
|
||||
|
||||
while (container.index < container.tokens.length)
|
||||
{
|
||||
const token = container.tokens[container.index];
|
||||
|
||||
switch (token.type)
|
||||
{
|
||||
case PythonTokenType.BRACE_END:
|
||||
{
|
||||
if (isKey)
|
||||
{
|
||||
// The last token is a key, which is invalid
|
||||
throw new Error('Unexpected end of dictionary: Expected a key-value pair.');
|
||||
}
|
||||
|
||||
container.index++;
|
||||
return dict;
|
||||
}
|
||||
case PythonTokenType.COLON:
|
||||
{
|
||||
if (!isKey)
|
||||
{
|
||||
throw new Error('Expected a key before the colon in a dictionary.');
|
||||
}
|
||||
isKey = false;
|
||||
container.index++;
|
||||
break;
|
||||
}
|
||||
case PythonTokenType.COMMA:
|
||||
{
|
||||
if (isKey)
|
||||
{
|
||||
throw new Error('No value provided with dictionary key');
|
||||
}
|
||||
isKey = true;
|
||||
container.index++;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
if (isKey)
|
||||
{
|
||||
// Parse the key and check its type
|
||||
key = PythonParser.parseValueToken(container) as PythonKey;
|
||||
|
||||
if (
|
||||
typeof key !== 'string' &&
|
||||
typeof key !== 'number' &&
|
||||
typeof key !== 'boolean' &&
|
||||
!(key instanceof PythonTuple) && // Check if it's a PythonTuple
|
||||
typeof key !== 'object' // Allow floats
|
||||
)
|
||||
{
|
||||
throw new Error('Invalid key type in a dictionary.');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Parse the value
|
||||
if (key === null)
|
||||
{
|
||||
throw new Error('Key cannot be null in a dictionary.');
|
||||
}
|
||||
const value = PythonParser.parseValueToken(container);
|
||||
dict.data.set(key, value);
|
||||
key = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Expected close brace } in dictionary');
|
||||
}
|
||||
|
||||
get length(): number
|
||||
{
|
||||
return Object.keys(this.data).length;
|
||||
}
|
||||
|
||||
public get(key: PythonKey): PythonType | undefined
|
||||
{
|
||||
return this.data.get(key);
|
||||
}
|
||||
|
||||
public toJSON(): unknown
|
||||
{
|
||||
return Object.fromEntries(this.data);
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { PythonTokenContainer } from './PythonTokenContainer';
|
||||
import { PythonTokenType } from './PythonTokenType';
|
||||
import { PythonObject } from './PythonObject';
|
||||
import { PythonType } from './PythonType';
|
||||
import { PythonParser } from './PythonParser';
|
||||
|
||||
export class PythonList extends PythonObject
|
||||
{
|
||||
public data: PythonType[] = [];
|
||||
|
||||
public static parse(container: PythonTokenContainer): PythonList
|
||||
{
|
||||
let expectingComma = false;
|
||||
const list = new PythonList();
|
||||
do
|
||||
{
|
||||
const token = container.tokens[container.index];
|
||||
switch (token.type)
|
||||
{
|
||||
case PythonTokenType.LIST_END:
|
||||
{
|
||||
container.index++;
|
||||
return list;
|
||||
}
|
||||
case PythonTokenType.COMMA:
|
||||
{
|
||||
if (!expectingComma)
|
||||
{
|
||||
throw new Error('Unexpected comma in list');
|
||||
}
|
||||
expectingComma = false;
|
||||
container.index++;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
if (expectingComma)
|
||||
{
|
||||
throw new Error('Unexpected token')
|
||||
}
|
||||
list.data.push(PythonParser.parseValueToken(container));
|
||||
expectingComma = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (container.index < container.tokens.length);
|
||||
throw new Error('Expected ] end bracket in list')
|
||||
}
|
||||
|
||||
public get(index: number): PythonType | undefined
|
||||
{
|
||||
return this.data[index];
|
||||
}
|
||||
|
||||
get length(): number
|
||||
{
|
||||
return this.data.length;
|
||||
}
|
||||
|
||||
public toString(): string
|
||||
{
|
||||
return '[' + this.data.join(', ') + ']';
|
||||
}
|
||||
|
||||
public toJSON(): unknown
|
||||
{
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import { PythonParser } from './PythonParser';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { PythonDict } from './PythonDict';
|
||||
import { PythonList } from './PythonList';
|
||||
import { PythonTuple } from './PythonTuple';
|
||||
|
||||
describe('PythonParser', () =>
|
||||
{
|
||||
describe('parse', () =>
|
||||
{
|
||||
it('can parse a complex python dictionary notation', () =>
|
||||
{
|
||||
const notationDoc = `{
|
||||
"nested_dict": {
|
||||
"key1": "value1",
|
||||
"key2": {
|
||||
"inner_key": "inner_value"
|
||||
}
|
||||
},
|
||||
"list": [1, 2, 3, [4, 5]],
|
||||
"boolean": True,
|
||||
"tuple": (1, 2, ("nested_tuple", 3)),
|
||||
"bytes": b'hello',
|
||||
"float": 3.14,
|
||||
'integer': 42,
|
||||
"hex_number": 0x1A,
|
||||
"octal_number": 0o52,
|
||||
"string_single": 'single-quoted\\' string',
|
||||
"string_double": "double-quoted \\" string",
|
||||
"string_triple_single": '''triple-quoted\'
|
||||
single-quoted string''',
|
||||
"string_triple_double": """triple-quoted\"
|
||||
double-quoted string""",
|
||||
"raw_string_single": r'raw single-quoted\\ string',
|
||||
"raw_string_double": r"raw double-quoted\\ string",
|
||||
"raw_string_triple_single": r'''raw triple\\''-quoted
|
||||
single-quoted string''',
|
||||
"raw_string_triple_double": r"""raw triple\\""-quoted
|
||||
double-quoted string"""
|
||||
}`;
|
||||
const parsed = PythonParser.parse(notationDoc);
|
||||
if (!(parsed instanceof PythonDict))
|
||||
{
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const nested = parsed.get('nested_dict');
|
||||
assert.ok(nested);
|
||||
if (!(nested instanceof PythonDict))
|
||||
{
|
||||
assert(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
assert.equal(nested.get('key1'), 'value1');
|
||||
const key2 = nested.get('key2');
|
||||
if (!(key2 instanceof PythonDict))
|
||||
{
|
||||
assert(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
assert.equal(key2.get('inner_key'), 'inner_value');
|
||||
}
|
||||
}
|
||||
|
||||
const list = parsed.get('list');
|
||||
if (!(list instanceof PythonList))
|
||||
{
|
||||
assert(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
assert.equal(list.length, 4);
|
||||
assert.equal(list.get(0), 1);
|
||||
assert.equal(list.get(1), 2);
|
||||
assert.equal(list.get(2), 3);
|
||||
const nestedList = list.get(3);
|
||||
if (!(nestedList instanceof PythonList))
|
||||
{
|
||||
assert(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
assert.equal(nestedList.get(0), 4);
|
||||
assert.equal(nestedList.get(1), 5);
|
||||
}
|
||||
assert.equal(list.get(4), undefined);
|
||||
}
|
||||
|
||||
assert.equal(parsed.get('boolean'), true);
|
||||
const tuple = parsed.get('tuple');
|
||||
if (!(tuple instanceof PythonTuple))
|
||||
{
|
||||
assert(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
assert.equal(tuple.get(0), 1);
|
||||
assert.equal(tuple.get(1), 2);
|
||||
const nestedTuple = tuple.get(2);
|
||||
if (!(nestedTuple instanceof PythonTuple))
|
||||
{
|
||||
assert(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
assert.equal(nestedTuple.get(0), 'nested_tuple');
|
||||
assert.equal(nestedTuple.get(1), 3);
|
||||
}
|
||||
assert.equal(tuple.get(3), undefined);
|
||||
}
|
||||
const buf = parsed.get('bytes');
|
||||
if (buf instanceof Buffer)
|
||||
{
|
||||
assert.equal(Buffer.from('hello', 'binary').compare(buf), 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
assert(false);
|
||||
}
|
||||
assert.equal(parsed.get('float'), 3.14);
|
||||
assert.equal(parsed.get('integer'), 42);
|
||||
assert.equal(parsed.get('hex_number'), 26);
|
||||
assert.equal(parsed.get('octal_number'), 42);
|
||||
assert.equal(parsed.get('string_single'), 'single-quoted\\\' string');
|
||||
assert.equal(parsed.get('string_double'), 'double-quoted \\" string');
|
||||
assert.equal(parsed.get('string_triple_single'), 'triple-quoted\'\nsingle-quoted string');
|
||||
assert.equal(parsed.get('string_triple_double'), 'triple-quoted\"\ndouble-quoted string');
|
||||
|
||||
/*
|
||||
raw_string_single": r'raw single-quoted\ string',
|
||||
"raw_string_double": r"raw double-quoted\ string",
|
||||
"raw_string_triple_single": r'''raw triple\''-quoted
|
||||
single-quoted string''',
|
||||
"raw_string_triple_double": r"""raw triple\''-quoted
|
||||
double-quoted string"""
|
||||
*/
|
||||
assert.equal(parsed.get('raw_string_single'), 'raw single-quoted\\ string');
|
||||
assert.equal(parsed.get('raw_string_double'), 'raw double-quoted\\ string');
|
||||
assert.equal(parsed.get('raw_string_triple_single'), 'raw triple\\\'\'-quoted\nsingle-quoted string');
|
||||
assert.equal(parsed.get('raw_string_triple_double'), 'raw triple\\""-quoted\ndouble-quoted string');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
import { PythonTokenType } from './PythonTokenType';
|
||||
import { PythonToken } from './PythonToken';
|
||||
import { PythonSet } from './PythonSet';
|
||||
import { PythonTokenContainer } from './PythonTokenContainer';
|
||||
import { PythonType } from './PythonType';
|
||||
import { PythonList } from './PythonList';
|
||||
import { PythonTuple } from './PythonTuple';
|
||||
|
||||
interface TokenSpec
|
||||
{
|
||||
regex: RegExp;
|
||||
type: PythonTokenType;
|
||||
}
|
||||
|
||||
export class PythonParser
|
||||
{
|
||||
private static tokenSpecs: TokenSpec[] =
|
||||
[
|
||||
{ regex: /^\s+/, type: PythonTokenType.UNKNOWN }, // WHITESPACE is treated as UNKNOWN
|
||||
{ regex: /^{/, type: PythonTokenType.BRACE_START },
|
||||
{ regex: /^}/, type: PythonTokenType.BRACE_END },
|
||||
{ regex: /^[:]/, type: PythonTokenType.COLON },
|
||||
{ regex: /^[,]/, type: PythonTokenType.COMMA },
|
||||
{ regex: /^None\b/, type: PythonTokenType.NONE },
|
||||
{ regex: /^(True|False)\b/, type: PythonTokenType.BOOLEAN },
|
||||
{ regex: /^((?:-?[0-9]+\.[0-9]*)|(?:-?[0.9]*\.[0-9]+))/, type: PythonTokenType.FLOAT },
|
||||
{ regex: /^\d+\b/, type: PythonTokenType.INTEGER },
|
||||
{ regex: /^0x([0-9a-fA-F]+\b)/, type: PythonTokenType.HEX },
|
||||
{ regex: /^0o([0-7]+)/, type: PythonTokenType.OCTAL },
|
||||
{ regex: /^\(/, type: PythonTokenType.TUPLE_START },
|
||||
{ regex: /^\)/, type: PythonTokenType.TUPLE_END },
|
||||
{ regex: /^\[/, type: PythonTokenType.LIST_START },
|
||||
{ regex: /^\]/, type: PythonTokenType.LIST_END },
|
||||
{ regex: /^"""((?:[^"]*|\n|\\"|")*?)"""/, type: PythonTokenType.STRING }, // triple double quoted string
|
||||
{ regex: /^'''((?:[^']*|\n|\\'|')*?)'''/, type: PythonTokenType.STRING }, // triple single quoted string
|
||||
{ regex: /^'([^'\\]*(?:\\.[^'\\\n]*)*)'/, type: PythonTokenType.STRING }, // single quoted string
|
||||
{ regex: /^"([^"\\]*(?:\\.[^"\\\n]*)*)"/, type: PythonTokenType.STRING }, // double quoted string
|
||||
|
||||
{ regex: /^b"""((?:[^"]*|\n|\\"|")*?)"""/, type: PythonTokenType.BINARY_STRING }, // triple double quoted string
|
||||
{ regex: /^b'''((?:[^']*|\n|\\'|')*?)'''/, type: PythonTokenType.BINARY_STRING }, // triple single quoted string
|
||||
{ regex: /^b'([^'\\]*(?:\\.[^'\\\n]*)*)'/, type: PythonTokenType.BINARY_STRING }, // single quoted string
|
||||
{ regex: /^b"([^"\\]*(?:\\.[^"\\\n]*)*)"/, type: PythonTokenType.BINARY_STRING }, // double quoted string
|
||||
|
||||
{ regex: /^r"""((?:[^"]*|\n|")*?)"""/, type: PythonTokenType.RAW_STRING }, // triple double quoted string
|
||||
{ regex: /^r'''((?:[^']*|\n|')*?)'''/, type: PythonTokenType.RAW_STRING }, // triple single quoted string
|
||||
{ regex: /^r'([^'\n]*?)'/, type: PythonTokenType.RAW_STRING }, // single quoted string
|
||||
{ regex: /^r"([^"\n]*?)"/, type: PythonTokenType.RAW_STRING }, // double quoted string
|
||||
|
||||
{ regex: /^-?\d+\.?\d*[eE][-+]?\d+/, type: PythonTokenType.FLOAT }, // Scientific notation
|
||||
{ regex: /^\d+(_\d+)*\b/, type: PythonTokenType.INTEGER }, // Integer with underscores, e.g., 1_000_000
|
||||
{ regex: /^[^\s:{},"'\[\]\(\)]+/, type: PythonTokenType.UNKNOWN } // Catch all for other non-structured sequences
|
||||
];
|
||||
|
||||
private static* tokenize(input: string): Generator<PythonToken, void, undefined>
|
||||
{
|
||||
let index = 0;
|
||||
while (index < input.length)
|
||||
{
|
||||
const currentInput = input.slice(index);
|
||||
|
||||
if (currentInput.length === 0)
|
||||
{
|
||||
return; // End of input
|
||||
}
|
||||
|
||||
let matched = false;
|
||||
for (const { regex, type } of PythonParser.tokenSpecs)
|
||||
{
|
||||
const tokenMatch = currentInput.match(regex);
|
||||
if (tokenMatch)
|
||||
{
|
||||
matched = true;
|
||||
let value = tokenMatch[0];
|
||||
if (tokenMatch.length > 1)
|
||||
{
|
||||
value = tokenMatch[tokenMatch.length - 1];
|
||||
}
|
||||
index += tokenMatch[0].length; // Move past this token
|
||||
|
||||
if (type !== PythonTokenType.UNKNOWN) // WHITESPACE is UNKNOWN and not yielded
|
||||
{
|
||||
yield { type, value };
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched)
|
||||
{
|
||||
throw new Error(`Unexpected token at index ${index}: "${currentInput[0]}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static parseValueToken(container: PythonTokenContainer): PythonType
|
||||
{
|
||||
const t = container.tokens[container.index++];
|
||||
switch (t.type)
|
||||
{
|
||||
case PythonTokenType.BRACE_START:
|
||||
{
|
||||
return PythonSet.parse(container);
|
||||
}
|
||||
case PythonTokenType.STRING:
|
||||
{
|
||||
return t.value;
|
||||
}
|
||||
case PythonTokenType.BINARY_STRING:
|
||||
{
|
||||
return Buffer.from(t.value, 'binary');
|
||||
}
|
||||
case PythonTokenType.RAW_STRING:
|
||||
{
|
||||
return t.value;
|
||||
}
|
||||
case PythonTokenType.BOOLEAN:
|
||||
{
|
||||
return t.value === 'True';
|
||||
}
|
||||
case PythonTokenType.LIST_START:
|
||||
{
|
||||
return PythonList.parse(container);
|
||||
}
|
||||
case PythonTokenType.TUPLE_START:
|
||||
{
|
||||
return PythonTuple.parse(container);
|
||||
}
|
||||
case PythonTokenType.NONE:
|
||||
{
|
||||
return null;
|
||||
}
|
||||
case PythonTokenType.HEX:
|
||||
{
|
||||
return parseInt(t.value, 16);
|
||||
}
|
||||
case PythonTokenType.OCTAL:
|
||||
{
|
||||
return parseInt(t.value, 8);
|
||||
}
|
||||
case PythonTokenType.INTEGER:
|
||||
{
|
||||
return parseInt(t.value, 10);
|
||||
}
|
||||
case PythonTokenType.FLOAT:
|
||||
{
|
||||
return parseFloat(t.value);
|
||||
}
|
||||
default:
|
||||
throw new Error('Unexpected token: ' + PythonTokenType[t.type]);
|
||||
}
|
||||
}
|
||||
|
||||
public static parse(input: string): PythonType
|
||||
{
|
||||
const cont = new PythonTokenContainer()
|
||||
for (const token of PythonParser.tokenize(input))
|
||||
{
|
||||
cont.tokens.push(token);
|
||||
}
|
||||
|
||||
const parsedToken = this.parseValueToken(cont);
|
||||
if (cont.index < cont.tokens.length)
|
||||
{
|
||||
throw new Error('Only one token expected at root level');
|
||||
}
|
||||
return parsedToken;
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { PythonTokenContainer } from './PythonTokenContainer';
|
||||
import { PythonTokenType } from './PythonTokenType';
|
||||
import { PythonDict } from './PythonDict';
|
||||
import { PythonObject } from './PythonObject';
|
||||
import { PythonType } from './PythonType';
|
||||
import { PythonParser } from './PythonParser';
|
||||
|
||||
export class PythonSet extends PythonObject
|
||||
{
|
||||
public data = new Set<PythonType>();
|
||||
|
||||
public static parse(container: PythonTokenContainer): PythonSet | PythonDict
|
||||
{
|
||||
let expectingComma = false;
|
||||
const startIndex = container.index;
|
||||
const set = new PythonSet();
|
||||
do
|
||||
{
|
||||
const token = container.tokens[container.index];
|
||||
switch (token.type)
|
||||
{
|
||||
case PythonTokenType.BRACE_END:
|
||||
{
|
||||
if (container.index === startIndex)
|
||||
{
|
||||
// Empty braces, this is an empty PythonDict
|
||||
return new PythonDict();
|
||||
}
|
||||
else
|
||||
{
|
||||
container.index++;
|
||||
return set;
|
||||
}
|
||||
}
|
||||
case PythonTokenType.COMMA:
|
||||
{
|
||||
if (!expectingComma)
|
||||
{
|
||||
throw new Error('Unexpected comma in list');
|
||||
}
|
||||
expectingComma = false;
|
||||
container.index++;
|
||||
break;
|
||||
}
|
||||
case PythonTokenType.COLON:
|
||||
{
|
||||
// This is a dictionary, not a set, start again..
|
||||
container.index = startIndex;
|
||||
return PythonDict.parse(container);
|
||||
}
|
||||
default:
|
||||
{
|
||||
if (expectingComma)
|
||||
{
|
||||
throw new Error('Unexpected token')
|
||||
}
|
||||
set.data.add(PythonParser.parseValueToken(container));
|
||||
expectingComma = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (container.index < container.tokens.length);
|
||||
throw new Error('Expected } end brace in set')
|
||||
}
|
||||
|
||||
public toJSON(): unknown
|
||||
{
|
||||
return Array.from(this.data);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { PythonTokenType } from './PythonTokenType';
|
||||
|
||||
export interface PythonToken
|
||||
{
|
||||
type: PythonTokenType;
|
||||
value: string;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { PythonToken } from './PythonToken';
|
||||
|
||||
export class PythonTokenContainer
|
||||
{
|
||||
tokens: PythonToken[] = [];
|
||||
index = 0;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
export enum PythonTokenType
|
||||
{
|
||||
NONE,
|
||||
BRACE_START,
|
||||
BRACE_END,
|
||||
COLON,
|
||||
COMMA,
|
||||
BOOLEAN,
|
||||
INTEGER,
|
||||
FLOAT,
|
||||
STRING,
|
||||
BINARY_STRING,
|
||||
RAW_STRING,
|
||||
LIST_START,
|
||||
LIST_END,
|
||||
TUPLE_START,
|
||||
TUPLE_END,
|
||||
HEX,
|
||||
OCTAL,
|
||||
UNKNOWN // Catch all for other sequences
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { PythonTokenContainer } from './PythonTokenContainer';
|
||||
import { PythonTokenType } from './PythonTokenType';
|
||||
import { PythonObject } from './PythonObject';
|
||||
import { PythonType } from './PythonType';
|
||||
import { PythonParser } from './PythonParser';
|
||||
|
||||
export class PythonTuple extends PythonObject
|
||||
{
|
||||
public data: PythonType[] = [];
|
||||
|
||||
public static parse(container: PythonTokenContainer): PythonTuple
|
||||
{
|
||||
let expectingComma = false;
|
||||
const tuple = new PythonTuple();
|
||||
do
|
||||
{
|
||||
const token = container.tokens[container.index];
|
||||
switch (token.type)
|
||||
{
|
||||
case PythonTokenType.TUPLE_END:
|
||||
{
|
||||
container.index++;
|
||||
return tuple;
|
||||
}
|
||||
case PythonTokenType.COMMA:
|
||||
{
|
||||
if (!expectingComma)
|
||||
{
|
||||
throw new Error('Unexpected comma in list');
|
||||
}
|
||||
expectingComma = false;
|
||||
container.index++;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
if (expectingComma)
|
||||
{
|
||||
throw new Error('Unexpected token')
|
||||
}
|
||||
tuple.data.push(PythonParser.parseValueToken(container));
|
||||
expectingComma = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (container.index < container.tokens.length);
|
||||
throw new Error('Expected ) end bracket in tuple')
|
||||
}
|
||||
|
||||
public get(index: number): PythonType | undefined
|
||||
{
|
||||
return this.data[index];
|
||||
}
|
||||
|
||||
get length(): number
|
||||
{
|
||||
return this.data.length;
|
||||
}
|
||||
|
||||
public toString(): string
|
||||
{
|
||||
return '(' + this.data.join(', ') + ')';
|
||||
}
|
||||
|
||||
public toJSON(): unknown
|
||||
{
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import { PythonObject } from './PythonObject';
|
||||
|
||||
export type PythonType = number | boolean | string | Buffer | PythonObject | null;
|
||||
Reference in New Issue
Block a user