@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
195 lines • 7 kB
JavaScript
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { PARAMETER_NAME } from "./constants.js";
/**
* Recognize a plain dollar reference, returning its name and end.
*
* Names use Bash's ASCII identifier grammar. Unbraced positionals
* consume one digit; braces permit multiple digits. Special parameters
* consume one character. The caller owns quoting and must only scan a
* live dollar. Complex braced operators remain the expansion parser's
* responsibility and return null here, as do non-reference dollars.
* `start` and the returned end are offsets into `text`.
*/
export function scanParameter(text, start) {
if (text[start] !== '$')
return null;
let begin = start + 1;
const braced = text[begin] === '{';
if (braced)
begin += 1;
const match = PARAMETER_NAME.exec(text.slice(begin));
if (match === null)
return null;
let name = match[0];
if (!braced && /^[0-9]/.test(name))
name = name.slice(0, 1);
let end = begin + name.length;
if (braced) {
if (text[end] !== '}')
return null;
end += 1;
}
return [name, end];
}
// What bash reads a braced name against when it expands `${...}`: what ends
// a name, what ends one after a special parameter, the specials a `#`
// measures and a `!` follows, the special parameters themselves, the
// operators a `:` arms, and what may follow a name.
const NAME_ENDS = '#%^,~:-=?+/@}';
const SPECIAL_ENDS = '#%:-=?+/@}';
const LENGTH_SPECIALS = '-?#@';
const INDIRECT_SPECIALS = '#?@*';
const SPECIALS = '@*#?-$!';
const NULL_OPERATORS = '-=?+';
const OPERATORS = '}@#%-=?+/^,~';
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
const DIGITS = /^[0-9]+$/;
/**
* Whether bash refuses a `${...}` for its spelling alone.
*
* bash reads the braces only when the word holding them expands, and then
* reports `bad substitution` for a name that is no identifier, positional or
* special parameter (`${a b}`, `${ a}`, `${}`), an element reference that is
* not one whole (`${a[1]x}`), a `#` measuring more than a name (`${#a-x}`),
* or a name followed by anything but an operator (`${a:}`, `${a*}`). The
* braces are read as bash reads them, so `${!a b*}`, which bash reads as a
* prefix, passes here as it does in bash. `text` runs from `${` through
* its closing `}`. Mirrors Python's bad_substitution.
*/
export function badSubstitution(text) {
const s = text.slice(2);
let end = nameEnd(s, 0, s.startsWith('#') && startsName(s.charAt(1)) ? '}' : NAME_ENDS);
let name = s.slice(0, end);
if (name === '' && s.startsWith('@')) {
name = '@';
end = 1;
}
else if (name.startsWith('!') && s.startsWith('@}', end)) {
name += '@';
end += 1;
}
if ((name === '' && s !== '' && LENGTH_SPECIALS.includes(s.charAt(0))) ||
(name === '!' && s.length > 1 && INDIRECT_SPECIALS.includes(s.charAt(1)))) {
end = nameEnd(s, name.length + 1, SPECIAL_ENDS, false);
name = s.slice(0, end);
}
let c = s.charAt(end);
let i = end + 1;
let substring = false;
if (c === ':' && s.length > i && NULL_OPERATORS.includes(s.charAt(i))) {
c = s.charAt(i);
i += 1;
}
else if (c === ':' && s.charAt(i) !== '}') {
substring = true;
}
else if (name === '#' && s.charAt(i) === '}' && c !== '') {
if (LENGTH_SPECIALS.includes(c)) {
name += c;
c = '}';
}
else if ('%:=+/'.includes(c)) {
return true;
}
}
const second = name.charAt(1);
const indirect = name.startsWith('!') &&
second !== '' &&
(startsName(second) || /[0-9]/.test(second) || INDIRECT_SPECIALS.includes(second));
if (name.startsWith('#') && name.length > 1)
return c !== '}' || !lengthName(name.slice(1));
const last = name.charAt(name.length - 1);
if (indirect &&
c === '}' &&
(('*@'.includes(last) && startsName(second)) || (last === ']' && element(name.slice(1)))))
return false;
const word = indirect ? name.slice(1) : name;
if (word === '' || !lengthName(word))
return true;
return !substring && (c === '' || !OPERATORS.includes(c));
}
function startsName(char) {
return /^[A-Za-z_]$/.test(char);
}
/**
* Where a name read from `start` stops: at one of `ends`, past a backslash's
* character and, reading a variable name, past a whole `[...]`.
*/
function nameEnd(text, start, ends, subscripts = true) {
let index = start;
while (index < text.length) {
const char = text.charAt(index);
if (char === '\\') {
index += 2;
continue;
}
if (subscripts && char === '[') {
const close = subscriptEnd(text, index);
if (close !== null) {
index = close + 1;
continue;
}
}
else if (ends.includes(char)) {
return index;
}
index += 1;
}
return text.length;
}
function subscriptEnd(text, start) {
let depth = 0;
let index = start;
while (index < text.length) {
const char = text.charAt(index);
if (char === '\\') {
index += 2;
continue;
}
if (char === "'" || char === '"') {
const close = text.indexOf(char, index + 1);
if (close < 0)
return null;
index = close;
}
else if (char === '[') {
depth += 1;
}
else if (char === ']') {
depth -= 1;
if (depth === 0)
return index;
}
index += 1;
}
return null;
}
/** Whether `name` is one whole element reference, `a[...]` with a subscript that is not empty. */
function element(name) {
const bracket = name.indexOf('[');
if (bracket < 1 || !IDENTIFIER.test(name.slice(0, bracket)))
return false;
const close = subscriptEnd(name, bracket);
return close === name.length - 1 && close > bracket + 1;
}
/** Whether `${#name}` measures something. */
function lengthName(name) {
return (name === '' ||
(name.length === 1 && SPECIALS.includes(name)) ||
DIGITS.test(name) ||
element(name) ||
IDENTIFIER.test(name));
}
//# sourceMappingURL=parameter.js.map