UNPKG

locutus

Version:

Locutus other languages' standard libraries to JavaScript for fun and educational purposes

244 lines (243 loc) 7.71 kB
"use strict"; 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]; }