import { ScopeStringFor, ScopeSyntax } from './syntax.js' import { minIdx } from './util.js' /** * Translates a scope string into a {@link ScopeSyntax}. */ export class ScopeStringSyntax
implements ScopeSyntax
{
constructor(
readonly prefix: P,
readonly positional?: string,
readonly params?: Readonly
}
static fromString (
scopeValue: ScopeStringFor ,
): ScopeStringSyntax {
const paramIdx = scopeValue.indexOf('?')
const colonIdx = scopeValue.indexOf(':')
const prefixEnd = minIdx(paramIdx, colonIdx)
// No param or positional
if (prefixEnd === -1) {
return new ScopeStringSyntax(scopeValue as P)
}
const prefix = scopeValue.slice(0, prefixEnd) as P
// Parse the positional parameter if present
const positional =
colonIdx !== -1
? paramIdx === -1
? decodeURIComponent(scopeValue.slice(colonIdx + 1))
: colonIdx < paramIdx
? decodeURIComponent(scopeValue.slice(colonIdx + 1, paramIdx))
: undefined
: undefined
// Parse the query string if present and non empty
const params =
paramIdx !== -1 && paramIdx < scopeValue.length - 1
? new URLSearchParams(scopeValue.slice(paramIdx + 1))
: undefined
return new ScopeStringSyntax(prefix, positional, params)
}
}
/**
* Set of characters that are allowed in scope components without encoding. This
* is used to normalize scope components.
*/
const ALLOWED_SCOPE_CHARS = new Set(
// @NOTE This list must not contain "?" or "&" as it would interfere with
// query string parsing.
[':', '/', '+', ',', '@', '%'],
)
const NORMALIZABLE_CHARS_MAP = new Map(
Array.from(
ALLOWED_SCOPE_CHARS,
(c) => [encodeURIComponent(c), c] as const,
).filter(
([encoded, c]) =>
// Make sure that any char added to ALLOWED_SCOPE_CHARS that is a char
// that indeed needs encoding. Also, the normalizeURIComponent only
// supports three-character percent-encoded sequences.
encoded !== c && encoded.length === 3 && encoded.startsWith('%'),
),
)
/**
* Assumes a properly url-encoded string.
*/
function normalizeURIComponent(value: string): string {
// No need to read the last two characters since percent encoded characters
// are always three characters long.
let end = value.length - 2
for (let i = 0; i < end; i++) {
// Check if the character is a percent-encoded character
if (value.charCodeAt(i) === 0x25 /* % */) {
// Read the next encoded char. Current version only supports
// three-character percent-encoded sequences.
const encodedChar = value.slice(i, i + 3)
// Check if the encoded character is in the normalization map
const normalizedChar = NORMALIZABLE_CHARS_MAP.get(encodedChar)
if (normalizedChar) {
// Replace the encoded character with its normalized version
value = `${value.slice(0, i)}${normalizedChar}${value.slice(i + encodedChar.length)}`
// Adjust index to account for the length change
i += normalizedChar.length - 1
// Adjust end index since we replaced encoded char with normalized char
end -= encodedChar.length - normalizedChar.length
}
}
}
return value
}