UNPKG

@akala/core

Version:
785 lines 36.3 kB
import { ErrorWithStatus, HttpStatusCode } from "../errorWithStatus.js"; const first = { '': '', '+': '', '.': '.', '/': '/', ';': ';', '?': '?', '&': '&', '#': '#', }; const sep = { '': ',', '+': ',', '.': '.', '/': '/', ';': ';', '?': '&', '&': '&', '#': ',', }; const named = { '': false, '+': false, '.': false, '/': false, ';': true, '?': true, '&': true, '#': false, }; const ifemp = { '': '', '+': '', '.': '', '/': '', ';': '', '?': '=', '&': '=', '#': '', }; export const allow = { '': ['U'], '+': ['U', 'R'], '.': ['U'], '/': ['U'], ';': ['U'], '?': ['U'], '&': ['U'], '#': ['U', 'R'], }; 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) { return charcode >= 0x41 && charcode <= 0x5a || charcode >= 0x61 && charcode <= 0x7a; } function isDigit(charcode) { return charcode >= 0x30 && charcode <= 0x39; } function isUcschar(charcode) { 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) { return charcode >= 0xe0000 && charcode <= 0xeF8FF || charcode >= 0xf0000 && charcode <= 0xfFFFD || charcode >= 0x100000 && charcode <= 0x10FFFD; } function encodeURIComponent(value, operator, sub) { let lastOffset = 0; let result = []; 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 = []; 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) { // 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); } } var Lexer; (function (Lexer) { Lexer[Lexer["Literal"] = 0] = "Literal"; Lexer[Lexer["VariableExpansion"] = 1] = "VariableExpansion"; Lexer[Lexer["Sub"] = 2] = "Sub"; })(Lexer || (Lexer = {})); function isL2Operator(operator) { return operator == '+' || operator == '#'; } function isL3Operator(operator) { return operator == '.' || operator == '/' || operator == ';' || operator == '?' || operator == '&'; } function isReservedOperator(operator) { return operator == '=' || operator == ',' || operator == '!' || operator == '@' || operator == '|'; } function isOperator(operator) { return isL2Operator(operator) || isL3Operator(operator) || isReservedOperator(operator); } export function subOperatorx(operator, isMulti) { 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, isMulti) { switch (operator) { case "?": return '&'; case ".": case "/": return isMulti ? undefined : operator; case "&": case ";": return undefined; case ",": case undefined: default: return ','; } } export function prefixOperator(operator, isMulti) { 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, template) { 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 = -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], 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], 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, variables) { return expandInternal(template, variables).join(''); } function expandInternal(template, variables) { return template.flatMap((t, i) => { let isFirst = true; function result(v, prefix) { 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) { const result = tryParse(s); if (result.warnings.length) throw result.warnings[0]; return result.template; } export function tryParse(s) { const template = []; const warnings = []; let lastOffset = 0; let state; state = Lexer.Literal; let expansion = null; for (let i = 0; i < s.length; i++) { const char = s[i]; switch (state) { 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) { 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; } //# sourceMappingURL=index.js.map