@akala/core
Version:
953 lines (888 loc) • 39 kB
text/typescript
import { ErrorWithStatus, HttpStatusCode } from "../errorWithStatus.js";
/**
* Level 2 operators used in URI templates (RFC 6570).
*/
export type OperatorL2 = '+' | '#';
/**
* Level 3 operators used in URI templates (RFC 6570).
*/
export type OperatorL3 = '.' | '/' | ';' | '?' | '&';
/**
* Reserved operators used in URI templates (RFC 6570).
*/
export type OperatorReserved = '=' | ',' | '!' | '@' | '|';
/**
* Union type combining all URI template operators.
*/
export type Operators = OperatorL2 | OperatorL3 | OperatorReserved;
const first = {
'': '',
'+': '',
'.': '.',
'/': '/',
';': ';',
'?': '?',
'&': '&',
'#': '#',
} as const
const sep = {
'': ',',
'+': ',',
'.': '.',
'/': '/',
';': ';',
'?': '&',
'&': '&',
'#': ',',
} as const
const named = {
'': false,
'+': false,
'.': false,
'/': false,
';': true,
'?': true,
'&': true,
'#': false,
} as const
const ifemp = {
'': '',
'+': '',
'.': '',
'/': '',
';': '',
'?': '=',
'&': '=',
'#': '',
} as const
export const allow = {
'': ['U'],
'+': ['U', 'R'],
'.': ['U'],
'/': ['U'],
';': ['U'],
'?': ['U'],
'&': ['U'],
'#': ['U', 'R'],
} as const
export const restricted = {
' ': 1,
'<': 1,
'>': 1,
'{': 1,
'}': 1,
'%': 2,
':': 2,
'/': 2,
'?': 2,
'#': 2,
'[': 2,
']': 2,
'@': 2,
'!': 3,
'$': 3,
'&': 3,
'\'': 3,
'(': 3,
')': 3,
'*': 3,
'+': 3,
',': 3,
';': 3,
'=': 3,
}
/**
* Checks if a character code represents an ASCII alphabetic character (A-Z, a-z).
*
* @param charcode - Unicode code point of the character
* @returns True if the character is ASCII A-Z or a-z
*/
function isAscii(charcode: number)
{
return charcode >= 0x41 && charcode <= 0x5a ||
charcode >= 0x61 && charcode <= 0x7a;
}
function isDigit(charcode: number)
{
return charcode >= 0x30 && charcode <= 0x39;
}
function isUcschar(charcode: number)
{
return charcode >= 0xA0 && charcode <= 0xD7FF ||
charcode >= 0xF900 && charcode <= 0xFDCF ||
charcode >= 0xFDF0 && charcode <= 0xFFEF ||
charcode >= 0x10000 && charcode <= 0x1FFFD ||
charcode >= 0x20000 && charcode <= 0x2FFFD ||
charcode >= 0x30000 && charcode <= 0x3FFFD ||
charcode >= 0x40000 && charcode <= 0x4FFFD ||
charcode >= 0x50000 && charcode <= 0x5FFFD ||
charcode >= 0x60000 && charcode <= 0x6FFFD ||
charcode >= 0x70000 && charcode <= 0x7FFFD ||
charcode >= 0x80000 && charcode <= 0x8FFFD ||
charcode >= 0x90000 && charcode <= 0x9FFFD ||
charcode >= 0xa0000 && charcode <= 0xaFFFD ||
charcode >= 0xb0000 && charcode <= 0xbFFFD ||
charcode >= 0xc0000 && charcode <= 0xcFFFD ||
charcode >= 0xd0000 && charcode <= 0xdFFFD ||
charcode >= 0xe1000 && charcode <= 0xeFFFD;
}
function isiPrivate(charcode: number)
{
return charcode >= 0xe0000 && charcode <= 0xeF8FF ||
charcode >= 0xf0000 && charcode <= 0xfFFFD ||
charcode >= 0x100000 && charcode <= 0x10FFFD;
}
function encodeURIComponent(value: string | number | bigint | boolean, operator: Operators, sub: number | undefined): string
{
let lastOffset = 0;
let result: string[] = [];
switch (typeof value)
{
case 'undefined':
return undefined;
case 'bigint':
case 'boolean':
case 'number':
return encodeURIComponent(value.toString(), operator, sub);
case 'string':
if (sub)
value = value.substring(0, sub);
for (let i = 0; i < value.length; i++)
{
switch (restricted[value[i]])
{
case 1:
result.push(value.substring(lastOffset, i), '%' + value.charCodeAt(i).toString(16).toUpperCase());
lastOffset = i + 1;
break;
case 2:
case 3:
if ((value[i] == '%' && (i > value.length - 2 || !isHex(value.substring(i + 1, i + 3)))) || !allow[operator || '']?.includes('R'))
{
result.push(value.substring(lastOffset, i), '%' + value.charCodeAt(i).toString(16).toUpperCase());
lastOffset = i + 1;
}
break;
default:
const charCode = value.charCodeAt(i);
if (isAscii(charCode) || isDigit(charCode) || charCode == 0x5f || charCode == 0x7e || charCode == 0x2d || charCode == 0x2e)
break;
if (charCode == 0x21 || charCode == 0x23 || charCode == 0x24 || charCode == 0x26 || charCode == 0x28 && charCode < 0x3b ||
charCode == 0x3d || charCode == 0x3f && charCode < 0x5b || charCode == 0x5d || charCode == 0x5f || charCode == 0x61 && charCode < 0x7a || charCode == 0x7e ||
isUcschar(charCode) || isiPrivate(charCode)
)
{
const chars: number[] = [];
if (charCode < 0x80)
chars.push(charCode);
else if (charCode < 0x800)
{
chars.push(0xc0 | (charCode >> 6))
chars.push(0x80 | (charCode & 0b00111111))
}
else if (charCode < 0x10000)
{
chars.push(0xe0 | (charCode >> 12))
chars.push(0x80 | ((charCode >> 6) & 0b00111111))
chars.push(0x80 | (charCode & 0b00111111))
}
else
{
chars.push(0b1111000 | (charCode >> 18))
chars.push(0x80 | ((charCode >> 12) & 0b00111111))
chars.push(0x80 | ((charCode >> 6) & 0b00111111))
chars.push(0x80 | (charCode & 0b00111111))
}
result.push(value.substring(lastOffset, i), '%' + chars.map(n => n.toString(16)).join('%').toUpperCase());
lastOffset = i + 1;
}
}
}
if (lastOffset < value.length && result.length)
result.push(value.substring(lastOffset));
// if (value.length > 0 && operator == '#')
// if (result.length)
// result.unshift('#');
// else
// value = '#' + value;
if (result.length)
return result.join('');
return value;
// case 'object':
// if (Array.isArray(value))
// return encodeURIComponents(value, operator, sub, explode).join(joinOperator(operator, false));
// if (explode)
// return Object.entries(value).map(v => encodeURIComponent(v[0], subOperator(operator, false), undefined, false) + (v[1] ? '=' + encodeURIComponent(v[1], subOperator(operator, false), undefined, false) : '')).join(joinOperator(operator, false));
// return Object.entries(value).flatMap(v => encodeURIComponent(v, subOperator(operator, false), undefined, false)).join(',');
default:
throw new Error('unsupported value type ' + typeof value);
}
}
function decodeURIComponent(value: string | number | bigint | boolean): string
{
// let lastOffset = 0;
// let result: string[] = [];
switch (typeof value)
{
case 'undefined':
return undefined;
case 'bigint':
case 'boolean':
case 'number':
return value.toString();
case 'string':
return globalThis.decodeURIComponent(value);
// for (let i = 0; i < value.length; i++)
// {
// const charCode = value.charCodeAt(i);
// if (charCode == 37) //%
// {
// const chars: number[] = [];
// if (charCode < 0x80)
// chars.push(charCode);
// else if (charCode < 0x800)
// {
// chars.push(0xc0 | (charCode >> 6))
// chars.push(0x80 | (charCode & 0b00111111))
// }
// else if (charCode < 0x10000)
// {
// chars.push(0xe0 | (charCode >> 12))
// chars.push(0x80 | ((charCode >> 6) & 0b00111111))
// chars.push(0x80 | (charCode & 0b00111111))
// }
// else
// {
// chars.push(0b1111000 | (charCode >> 18))
// chars.push(0x80 | ((charCode >> 12) & 0b00111111))
// chars.push(0x80 | ((charCode >> 6) & 0b00111111))
// chars.push(0x80 | (charCode & 0b00111111))
// }
// result.push(value.substring(lastOffset, i), String.fromCharCode(...chars));
// lastOffset = i + 1;
// }
// }
// if (lastOffset < value.length && result.length)
// result.push(value.substring(lastOffset));
// if (result.length)
// return result.join('');
// return value;
default:
throw new Error('unsupported value type ' + typeof value);
}
}
enum Lexer
{
Literal,
VariableExpansion,
Sub
}
export type UriTemplate = (string | UriTemplateExpansion)[];
export type UriTemplateExpansion = { ref: string | UriTemplateExpansion[], sub?: number, operator?: Operators, explode?: boolean };
function isL2Operator(operator: string): operator is OperatorL2
{
return operator == '+' || operator == '#'
}
function isL3Operator(operator: string): operator is OperatorL3
{
return operator == '.' || operator == '/' || operator == ';' || operator == '?' || operator == '&'
}
function isReservedOperator(operator: string): operator is OperatorReserved
{
return operator == '=' || operator == ',' || operator == '!' || operator == '@' || operator == '|'
}
function isOperator(operator: string): operator is Operators
{
return isL2Operator(operator) || isL3Operator(operator) || isReservedOperator(operator);
}
export function subOperatorx(operator: Operators | undefined, isMulti: boolean): Operators | undefined
{
switch (operator)
{
case "#":
return '+';
case undefined:
case ",":
case ".":
case "/":
if (isMulti)
return operator;
return undefined;
case "&":
case ";":
case "+":
case "?":
case "=":
case "!":
case "@":
case "|":
return operator;
}
}
export function joinOperator(operator: Operators | undefined, isMulti: boolean): Operators | undefined
{
switch (operator)
{
case "?":
return '&';
case ".":
case "/":
return isMulti ? undefined : operator;
case "&":
case ";":
return undefined;
case ",":
case undefined:
default:
return ','
}
}
export function prefixOperator(operator: Operators | undefined, isMulti: boolean): Operators | undefined
{
switch (operator)
{
case ";":
case "&":
case ".":
case "/":
if (isMulti)
return undefined;
return operator;
case "#":
case "?":
return operator;
case ",":
case undefined:
default:
return undefined;
}
}
export function match(s: string, template: UriTemplate): null | { remainder: string, variables: Record<string, unknown> }
{
let lastOffset = 0;
const result = {};
for (let i = 0; i < template.length; i++)
{
const t = template[i];
switch (typeof t)
{
case 'string':
if (s.indexOf(t, lastOffset) != lastOffset)
return null;
lastOffset += t.length;
break;
case 'object':
let indexOfSep: number = -1;
if (typeof t.ref !== 'string')
{
if ((first[t.operator || ''] || ''))
lastOffset += (first[t.operator || ''] || '').length;
for (let j = 0; j < t.ref.length; j++)
{
const subT = t.ref[j];
let keepSep = 1;
if (j < t.ref.length - 1)
indexOfSep = s.indexOf(sep[subT.operator || ''] || sep[''], lastOffset);
else if (i < template.length - 1 && typeof template[i + 1] == 'string')
{
indexOfSep = s.indexOf(template[i + 1] as string, lastOffset);
keepSep = 0;
}
else
indexOfSep = s.indexOf('/', lastOffset);
if (typeof subT.ref !== 'string')
throw new Error('invalid template');
if (!result[subT.ref] || typeof result[subT.ref] == 'string' && !t.sub)
{
if (i == template.length - 1 && j == t.ref.length - 1)
{
if (indexOfSep == -1)
{
result[subT.ref] = decodeURIComponent(s.substring(lastOffset));
lastOffset = s.length;
}
else
{
result[subT.ref] = decodeURIComponent(s.substring(lastOffset, indexOfSep));
lastOffset = indexOfSep + keepSep;
}
}
else
{
if (indexOfSep == -1)
return null;
result[subT.ref] = decodeURIComponent(s.substring(lastOffset, indexOfSep));
lastOffset = indexOfSep + keepSep;
}
if (named[t.operator || ''])
{
const ifempty = ifemp[t.operator || ''] || '';
const namePrefix = subT.ref + ifempty;
if (result[subT.ref].startsWith(namePrefix))
if (result[subT.ref].length > namePrefix.length)
result[subT.ref] = result[subT.ref].substring(namePrefix.length - ifempty.length + 1)
else
result[subT.ref] = '';
}
}
}
}
else
{
if ((first[t.operator || ''] || ''))
lastOffset += (first[t.operator || ''] || '').length;
if (!result[t.ref] || typeof result[t.ref] == 'string' && !t.sub)
{
if (i < template.length - 1 && typeof template[i + 1] == 'string')
{
indexOfSep = s.indexOf(template[i + 1] as string, lastOffset);
if (indexOfSep > -1)
{
result[t.ref] = decodeURIComponent(s.substring(lastOffset, indexOfSep));
lastOffset = indexOfSep;
}
}
else // if (i < template.length - 1)
{
for (let j = lastOffset; j < s.length; j++)
{
if (s[j] == sep[t.explode ? t.operator || '' : ''])
{
if (!result[t.ref])
if (t.explode)
result[t.ref] = [decodeURIComponent(s.substring(lastOffset, j))];
else
result[t.ref] = decodeURIComponent(s.substring(lastOffset, j));
else if (Array.isArray(result[t.ref]))
result[t.ref].push(decodeURIComponent(s.substring(lastOffset, j)));
else
result[t.ref] = [result[t.ref], decodeURIComponent(s.substring(lastOffset, j))]
lastOffset = j + 1;
}
// else
// {
// throw new Error('je sias pas comment gerer ce cas')
// }
}
// else
// indexOfSep = s.length;
// // else
// // indexOfSep = s.indexOf('/', lastOffset);
// if (i == template.length - 1)
// {
if (lastOffset < s.length)
{
if (!result[t.ref])
if (t.explode)
result[t.ref] = [decodeURIComponent(s.substring(lastOffset))];
else
result[t.ref] = decodeURIComponent(s.substring(lastOffset));
else if (Array.isArray(result[t.ref]))
result[t.ref].push(decodeURIComponent(s.substring(lastOffset)));
else
result[t.ref] = [result[t.ref], decodeURIComponent(s.substring(lastOffset))]
lastOffset = s.length;
}
}
// }
// else
// switch (typeof template[i + 1])
// {
// case 'string':
// const indexOfSep = s.indexOf(template[i + 1] as string, lastOffset);
// if (indexOfSep == -1)
// return null;
// result[t.ref] = decodeURIComponent(s.substring(lastOffset, indexOfSep));
// lastOffset = indexOfSep;
// break;
// default:
// break;
// }
if (named[t.operator || ''])
{
const ifempty = ifemp[t.operator || ''] || '';
const namePrefix = t.ref + ifempty;
if (typeof result[t.ref] == 'string')
{
if (result[t.ref].startsWith(namePrefix))
if (result[t.ref].length > namePrefix.length)
result[t.ref] = result[t.ref].substring(namePrefix.length - ifempty.length + 1)
else
result[t.ref] = '';
}
else
{
let caseHandled = true;
if (!t.explode)
{
if (result[t.ref][0].startsWith(namePrefix))
if (result[t.ref][0].length > namePrefix.length)
result[t.ref][0] = result[t.ref][0].substring(namePrefix.length - ifempty.length + 1)
else if (result[t.ref].length == 0)
result[t.ref][0] = '';
else
caseHandled = false;
else
caseHandled = false;
}
else
{
if (Array.isArray(result[t.ref]))
result[t.ref] = result[t.ref].map(v => v !== namePrefix ? v.startsWith(namePrefix) ? v.substring(namePrefix.length - ifempty.length + 1) : v : '');
else
caseHandled = false;
}
if (!caseHandled)
throw new Error(`${JSON.stringify(result[t.ref])} is not a string`);
}
}
if (t.explode)
{
let allValid = true;
const objEntries = result[t.ref].map((x, i) =>
{
if (!allValid)
return null;
const indexOfEqual = x.indexOf('=');
if (indexOfEqual > -1)
return [x.substring(0, indexOfEqual), x.substring(indexOfEqual + 1)];
allValid = false;
return null;
});
if (allValid)
result[t.ref] = Object.fromEntries(objEntries);
}
}
}
}
}
if (lastOffset !== s.length)
return { remainder: s.substring(lastOffset), variables: result };
return { remainder: '', variables: result };
}
export function expand(template: UriTemplate, variables: Record<string, unknown>): string
{
return expandInternal(template, variables).join('');
}
function expandInternal(template: UriTemplate, variables: Record<string, unknown>): string[]
{
return template.flatMap((t, i) =>
{
let isFirst = true;
function result(v: string, prefix?: string)
{
if (isFirst && !!v)
{
isFirst = false;
if (prefix)
return prefix + v;
}
return v;
}
if (typeof t == 'string')
return encodeURIComponent(t, '+', undefined);
const refs = Array.isArray(t.ref) ? t.ref : [t]
// if (Array.isArray(t.ref))
// {
// const expansions = expandInternal(t.ref, variables);
// if (expansions.length)
// return (first[t.operator || ''] || '') + expansions.join(sep[t.operator || ''] || ',')
// return undefined;
// }
// else
return refs.flatMap(subT =>
{
if (typeof subT.ref !== 'string')
throw new Error('invalid template');
let value = variables[subT.ref];
if (!subT.explode)
{
switch (typeof value)
{
case "undefined":
return undefined;
case "string":
case "number":
case "bigint":
case "boolean":
if (named[subT.operator || ''])
if (!value)
return result(encodeURIComponent(subT.ref, '+', undefined) + (ifemp[subT.operator || ''] || ''), first[subT.operator || ''] || '');
else
return result(encodeURIComponent(subT.ref, '+', undefined) + '=' + encodeURIComponent(value, subT.operator, subT.sub), first[subT.operator || ''] || '');
return result(encodeURIComponent(value, subT.operator, subT.sub), first[subT.operator || ''] || '');
case "object":
if (subT.sub)
throw new Error('cannot apply sub on composite values');
if (named[subT.operator || ''])
if ((value && Array.isArray(value) ? !value.length : !value || !Object.keys(value).length))
return undefined;
else
{
if (Array.isArray(value))
value = value.map(v => encodeURIComponent(v, subT.operator, subT.sub)).filter(v => v).join(',');
else
value = Object.entries(value).flatMap(e => e.map(v => encodeURIComponent(v, subT.operator, subT.sub))).join(',');
if (!value)
return result(encodeURIComponent(subT.ref, '+', undefined) + (ifemp[subT.operator || ''] || ''), first[subT.operator || ''] || '');
else
return result(encodeURIComponent(subT.ref, '+', undefined) + '=' + value, first[subT.operator || ''] || '');
}
if (Array.isArray(value))
return result(value.map(v => encodeURIComponent(v, subT.operator, subT.sub)).filter(v => v).join(','), first[subT.operator || ''] || '')
// else if (t.explode)
// return encodeURIComponent(t.ref, '+', undefined, undefined) + '=' + encodeURIComponent(value, t.operator, t.sub, t.explode);
return result(Object.entries(value).flatMap(e => e.map(v => encodeURIComponent(v, subT.operator, subT.sub))).join(','), first[subT.operator || ''] || '');
case "symbol":
case "function":
throw new ErrorWithStatus(HttpStatusCode.NotImplemented)
}
}
else
{
switch (typeof value)
{
case "string":
case "number":
case "undefined":
case "bigint":
case "boolean":
if (named[subT.operator || ''])
if (!value)
return result(encodeURIComponent(subT.ref, '+', undefined) + (ifemp[subT.operator || ''] || ''), first[subT.operator || ''] || '');
else
return result(encodeURIComponent(subT.ref, '+', undefined) + '=' + encodeURIComponent(value, subT.operator, subT.sub), first[subT.operator || ''] || '');
return result(encodeURIComponent(value, subT.operator, subT.sub), first[subT.operator || ''] || '');
case "object":
if (named[subT.operator || ''])
if ((value && Array.isArray(value) ? !value.length : !value || !Object.keys(value).length))
return undefined;
// return result(encodeURIComponent(subT.ref, '+', undefined) + (ifemp[subT.operator || ''] || ''), first[subT.operator || ''] || '');
else
if (Array.isArray(value))
return result(encodeURIComponent(subT.ref, '+', undefined) + '=' + value.map(v => encodeURIComponent(v, undefined, undefined)).filter(x => x).join((sep[subT.operator || ''] || ',') + encodeURIComponent(subT.ref, '+', undefined) + '='), first[subT.operator || ''] || '')
else
return result(Object.entries(value).map(v => encodeURIComponent(v[0], '+', undefined) + '=' + encodeURIComponent(v[1], subT.operator, subT.sub)).filter(x => x).join((sep[subT.operator || ''] || ',')), first[subT.operator || ''] || '')
else if (Array.isArray(value))
return result(value.map(v => encodeURIComponent(v, undefined, undefined)).filter(x => x).join((sep[subT.operator || ''] || ',')), first[subT.operator || ''] || '')
else
return result(Object.entries(value).map(v => encodeURIComponent(v[0], '+', undefined) + '=' + encodeURIComponent(v[1], subT.operator, subT.sub)).join((sep[subT.operator || ''] || ',')), first[subT.operator || ''] || '')
// else if (t.explode)
// return encodeURIComponent(t.ref, '+', undefined, undefined) + '=' + encodeURIComponent(value, t.operator, t.sub, t.explode);
case "symbol":
case "function":
throw new ErrorWithStatus(HttpStatusCode.NotImplemented)
}
}
// if (varia)
// const preOperator = prefixOperator(t.operator, false);
// if (Array.isArray(variables[t.ref]))
// {
// const values = encodeURIComponents(variables[t.ref] as unknown[], t.operator, t.sub, t.explode);
// if (typeof preOperator !== 'undefined')
// if (preOperator == '&' || preOperator == '?' || preOperator == ';')
// {
// return values.map(value =>
// {
// if (!value && t.operator == ';')
// return preOperator + t.ref;
// else
// return `${preOperator}${t.ref}=${value}`;
// }).join(joinOperator(t.operator, true));
// }
// else
// return preOperator + values.join(joinOperator(t.operator, !t.explode));
// return values.join(joinOperator(t.operator, !t.explode));
// }
// else
// value = encodeURIComponent(variables[t.ref], t.operator, t.sub, t.explode);
// if (typeof preOperator !== 'undefined' && typeof value !== 'undefined')
// if (preOperator == '&' || preOperator == '?' || preOperator == ';')
// {
// if (!value && t.operator == ';')
// return preOperator + t.ref;
// else
// return `${preOperator}${t.ref}=${value}`;
// }
// else
// return preOperator + value;
// return value;
}).filter(x => x).join(sep[t.operator || ''] || '');
}).filter(x => x)
}
export function parse(s: string): UriTemplate
{
const result = tryParse(s);
if (result.warnings.length)
throw result.warnings[0]
return result.template;
}
export function tryParse(s: string): { template: UriTemplate, warnings: Error[] }
{
const template: UriTemplate = [];
const warnings: Error[] = [];
let lastOffset = 0;
let state: Lexer;
state = Lexer.Literal;
let expansion: UriTemplateExpansion = null;
for (let i = 0; i < s.length; i++)
{
const char = s[i];
switch (state as Lexer)
{
case Lexer.Literal:
if (char == '}')
warnings.push(new Error('found closing brace without a matching open brace'))
if (char == '{')
{
if (lastOffset < i)
template.push(s.substring(lastOffset, i));
state = Lexer.VariableExpansion;
}
break;
case Lexer.VariableExpansion:
if (isOperator(char))
{
if (char == ',')
{
if (lastOffset < i)
{
if (!Array.isArray(expansion.ref))
expansion.ref = [{ ref: s.substring(lastOffset, i), operator: expansion.operator, sub: expansion.sub, explode: expansion.explode }];
else
expansion.ref.push({ ref: s.substring(lastOffset, i), operator: expansion.operator });
}
else if (!Array.isArray(expansion.ref))
expansion.ref = [{ ref: expansion.ref, operator: expansion.operator, sub: expansion.sub, explode: expansion.explode }];
lastOffset = i + 1;
}
else
{
if (expansion === null)
{
if (isReservedOperator(char))
warnings.push(new Error(`${char} is a reserved operator, for which the operation is not defined yet`))
expansion = { ref: '', operator: char };
lastOffset = i + 1;
}
else if (!expansion.ref)
warnings.push(new Error(`found 2 operators (${expansion.operator},${char}) in the same variable expansion expression`))
}
}
else
{
if (expansion == null)
{
expansion = { ref: '' };
lastOffset = i;
}
if (/[^a-z0-9_\}\*\:%]/i.test(char))
warnings.push(new Error(`an invalid character (${char}) war found`));
}
if (char == ':' || char == '*')
{
if (expansion == null)
warnings.push(new Error('invalid url template: empty variable name (' + s + ')'));
if (Array.isArray(expansion.ref))
expansion.ref.push({ ref: s.substring(lastOffset, i), operator: expansion.operator });
else
expansion.ref = s.substring(lastOffset, i);
if (char == ':')
state = Lexer.Sub;
else
if (Array.isArray(expansion.ref))
expansion.ref[expansion.ref.length - 1].explode = true;
else
expansion.explode = true;
lastOffset = i + 1;
}
else if (char == '}')
{
if (expansion == null)
throw new Error('invalid url template: empty variable name (' + s + ')');
state = Lexer.Literal;
if (lastOffset < i)
{
if (Array.isArray(expansion.ref))
expansion.ref.push({ ref: s.substring(lastOffset, i), operator: expansion.operator });
else
expansion.ref = s.substring(lastOffset, i);
}
lastOffset = i + 1;
template.push(expansion);
expansion = null;
}
break;
case Lexer.Sub:
if (char == ',')
{
if (!Array.isArray(expansion.ref))
expansion.ref = [{ ref: expansion.ref, operator: expansion.operator, sub: Number(s.substring(lastOffset, i)) }];
else
expansion.ref[expansion.ref.length - 1].sub = Number(s.substring(lastOffset, i));
lastOffset = i + 1;
state = Lexer.VariableExpansion;
}
else if (char == '}')
{
if (expansion == null)
warnings.push(new Error('invalid url template: empty variable name (' + s + ')'));
state = Lexer.Literal;
if (lastOffset < i)
{
if (!Array.isArray(expansion.ref))
expansion.sub = Number(s.substring(lastOffset, i))
else
expansion.ref[expansion.ref.length - 1].sub = Number(s.substring(lastOffset, i));
}
lastOffset = i + 1;
template.push(expansion);
expansion = null;
}
else if (isNaN(Number(char)))
warnings.push(new Error('sub can only contain numbers'));
}
}
if (state != Lexer.Literal)
warnings.push(new Error('Invalid lexer state after parsing (' + s + ')'));
if (lastOffset < s.length)
template.push(s.substring(lastOffset));
if (state !== Lexer.Literal)
warnings.push(new Error('template is not terminated'));
return { template, warnings };
}
function isHex(arg0: string): boolean
{
for (let i = 0; i < arg0.length; i++)
{
switch (arg0[i])
{
case 'a':
case 'b':
case 'c':
case 'd':
case 'e':
case 'f':
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'F':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
continue;
default:
return false;
}
}
return !!arg0.length;
}