locutus
Version:
Locutus other languages' standard libraries to JavaScript for fun and educational purposes
244 lines (243 loc) • 7.71 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.gsub = gsub;
const LUA_PATTERN_ESCAPES = {
a: '[A-Za-z]',
A: '[^A-Za-z]',
c: '[\\x00-\\x1F\\x7F]',
C: '[^\\x00-\\x1F\\x7F]',
d: '[0-9]',
D: '[^0-9]',
l: '[a-z]',
L: '[^a-z]',
p: '[!-/:-@[-`{-~]',
P: '[^!-/:-@[-`{-~]',
s: '\\s',
S: '\\S',
u: '[A-Z]',
U: '[^A-Z]',
w: '[A-Za-z0-9]',
W: '[^A-Za-z0-9]',
x: '[A-Fa-f0-9]',
X: '[^A-Fa-f0-9]',
z: '\\x00',
Z: '[^\\x00]',
};
const LUA_CHAR_CLASS_ATOMS = {
a: '[A-Za-z]',
A: '[^A-Za-z]',
c: '[\\x00-\\x1F\\x7F]',
C: '[^\\x00-\\x1F\\x7F]',
d: '[0-9]',
D: '[^0-9]',
l: '[a-z]',
L: '[^a-z]',
p: '[!-/:-@\\[-`\\{-~]',
P: '[^!-/:-@\\[-`\\{-~]',
s: '\\s',
S: '\\S',
u: '[A-Z]',
U: '[^A-Z]',
w: '[A-Za-z0-9]',
W: '[^A-Za-z0-9]',
x: '[A-Fa-f0-9]',
X: '[^A-Fa-f0-9]',
z: '\\x00',
Z: '[^\\x00]',
};
function escapeRegexLiteral(char) {
return /[\\^$.*+?()[\]{}|/]/.test(char) ? `\\${char}` : char;
}
function escapeRegexClassLiteral(char) {
return char === '\\' || char === ']' || char === '^' || char === '-' ? `\\${char}` : char;
}
function translateLuaCharClass(chars) {
let negate = false;
let source = chars;
if (source.startsWith('^')) {
negate = true;
source = source.slice(1);
}
const atoms = [];
for (let index = 0; index < source.length;) {
const current = parseLuaClassToken(source, index);
const hyphen = source[index + current.length] ?? '';
const nextIndex = index + current.length + 1;
const canRange = hyphen === '-' &&
nextIndex < source.length &&
current.literal !== null &&
current.rangeable &&
source[nextIndex] !== ']';
if (canRange) {
const right = parseLuaClassToken(source, nextIndex);
if (current.literal !== null && right.literal !== null && right.rangeable) {
const leftLiteral = current.literal;
const rightLiteral = right.literal;
atoms.push(`[${escapeRegexClassLiteral(leftLiteral)}-${escapeRegexClassLiteral(rightLiteral)}]`);
index = nextIndex + right.length;
continue;
}
}
atoms.push(current.atom);
index += current.length;
}
const union = atoms.length === 1 ? (atoms[0] ?? '') : `(?:${atoms.join('|')})`;
return negate ? `(?:(?!${union})[\\s\\S])` : union;
}
function parseLuaClassToken(source, index) {
const char = source[index] ?? '';
if (char === '%') {
const next = source[index + 1] ?? '';
const mapped = LUA_CHAR_CLASS_ATOMS[next];
if (mapped) {
return { atom: mapped, length: 2, literal: null, rangeable: false };
}
return { atom: escapeRegexLiteral(next), length: 2, literal: next, rangeable: false };
}
return {
atom: escapeRegexLiteral(char),
length: 1,
literal: char,
rangeable: char !== '\\',
};
}
function canApplyLuaQuantifier(parts) {
if (parts.length === 0) {
return false;
}
const last = parts[parts.length - 1] ?? '';
return last !== '(' && last !== '^' && last !== '$';
}
function translateLuaPattern(pattern) {
const parts = [];
for (let i = 0; i < pattern.length; i++) {
const char = pattern[i] ?? '';
if (char === '%') {
const next = pattern[i + 1] ?? '';
if (next === '') {
parts.push('%');
continue;
}
if (next === 'b' || next === 'f') {
throw new Error('gsub(): unsupported Lua pattern feature');
}
i++;
parts.push(LUA_PATTERN_ESCAPES[next] ?? escapeRegexLiteral(next));
continue;
}
if (char === '[') {
let classBody = '';
let closed = false;
i++;
if (i < pattern.length && pattern[i] === '^') {
classBody += '^';
i++;
}
if (i < pattern.length && pattern[i] === ']') {
classBody += ']';
i++;
}
while (i < pattern.length && pattern[i] !== ']') {
classBody += pattern[i] ?? '';
i++;
}
if (i < pattern.length && pattern[i] === ']') {
closed = true;
}
if (!closed) {
throw new Error('gsub(): malformed Lua pattern');
}
parts.push(translateLuaCharClass(classBody));
continue;
}
if (char === '.') {
parts.push('[\\s\\S]');
continue;
}
if (char === '-' || char === '*' || char === '+' || char === '?') {
if (canApplyLuaQuantifier(parts)) {
parts[parts.length - 1] += char === '-' ? '*?' : char;
}
else {
parts.push(escapeRegexLiteral(char));
}
continue;
}
if (char === '^') {
parts.push(i === 0 ? '^' : '\\^');
continue;
}
if (char === '$') {
parts.push(i === pattern.length - 1 ? '$' : '\\$');
continue;
}
if ('()'.includes(char)) {
parts.push(char);
continue;
}
parts.push(escapeRegexLiteral(char));
}
return new RegExp(parts.join(''), 'g');
}
function applyLuaReplacement(template, match, captures) {
let out = '';
for (let i = 0; i < template.length; i++) {
const char = template[i] ?? '';
if (char !== '%') {
out += char;
continue;
}
const next = template[i + 1] ?? '';
if (next === '%') {
out += '%';
i++;
continue;
}
if (/\d/.test(next)) {
i++;
if (next === '0') {
out += match;
}
else {
const capture = captures[Number(next) - 1];
if (capture === undefined) {
throw new Error(`invalid capture index %${next}`);
}
out += capture;
}
continue;
}
out += next;
i++;
}
return out;
}
function gsub(source, pattern, replacement, limit) {
// discuss at: https://locutus.io/lua/gsub/
// parity verified: Lua 5.4
// original by: Kevin van Zonneveld (https://kvz.io)
// note 1: Supports common Lua pattern classes and capture substitutions like %1 in replacement strings.
// example 1: gsub('hello world', 'l', 'L')
// returns 1: ['heLLo worLd', 3]
// example 2: gsub('123-456', '(%d+)', '[%1]', 1)
// returns 2: ['[123]-456', 1]
// example 3: gsub('abc', '%a', 'x')
// returns 3: ['xxx', 3]
const value = String(source);
const regex = translateLuaPattern(String(pattern));
const maxReplacements = limit === undefined ? Number.POSITIVE_INFINITY : Math.max(0, Math.trunc(Number(limit)));
let count = 0;
if (maxReplacements === 0) {
return [value, 0];
}
const out = value.replace(regex, (...args) => {
const match = String(args[0] ?? '');
if (count >= maxReplacements) {
return match;
}
count++;
const captures = args.slice(1, -2).map((item) => String(item ?? ''));
return applyLuaReplacement(String(replacement), match, captures);
});
return [out, count];
}