mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
feat(baselib): string.gsub takes 5.1 string and table replacements
1.12.1's str_gsub (0x7FD0E0) is Lua 5.0's. Its replacement argument
differs from 5.1 in four ways: a table is rejected ("string or function
expected"); in a replacement string, %0, and %1 when the pattern has no
captures, raise "invalid capture index" (add_s checks every %n with
check_capture, 0x7FCE20); a function that returns nil or false deletes
the match instead of keeping it; and one that returns a table deletes
it too instead of raising an error.
The first two only ever raised errors, so the 5.1 behavior cannot break
an existing addon. The function rules can: SuperAPI's
CombatText_AddMessage returns nil to delete the player's own " [0x...]"
GUID from heal text. So function replacements keep 1.12.1's behavior,
and only string and table replacements change. A scan of every gsub
call in the installed addons and the stock FrameXML found no other code
that depends on the old string or function results.
str_gsub's match loop, anchor, count and buffer stay the engine's. Two
parts are replaced:
- The 60-byte replacement-type gate at 0x7FD15E (lua_gettop / isstring /
lua_type == function, then luaL_argerror) becomes a call to a
__fastcall check with ECX = L that accepts a number, string, function
or table and raises "string/function/table expected" otherwise.
- The one call to add_s (0x7FD203 -> 0x7FD2A0, __fastcall ms/b + s/e on
the stack, ret 8) goes to a function with the same signature. A
function replacement calls the engine's add_s unchanged. A string or
number gets 5.1's add_s, and a table gets 5.1's lookup: the first
capture or whole match as the key, a nil or false value keeps the
match, and any other non-string value raises "invalid replacement
value". Both use the engine's push_onecapture (0x7FCF40, plus 5.1's
whole-match case for index 0 with no captures), luaL_addlstring
(0x6F51F0) and luaL_addvalue (0x6F5270).
Both patches check the original bytes or call target and are skipped
when they differ.
The table-replacement shim in StringLib is removed. It wrapped the
pattern in an outer capture so a closure could return the whole match,
and deleted the match for a missing key when the pattern had a
back-reference. string.gsub and gsub are the engine's function again.
This commit is contained in:
+34
-13
@@ -682,7 +682,7 @@ build instructions.
|
||||
- [Stale table lengths (5.1 healing)](#stale-table-lengths-51-healing)
|
||||
- [`Mixin(object, ...)` / `CreateFromMixins(...)`](#mixinobject---createfrommixins)
|
||||
- [`string.match` / `string.gmatch`](#stringmatch--stringgmatch)
|
||||
- [`string.gsub` table replacement](#stringgsub-table-replacement)
|
||||
- [`string.gsub` replacements](#stringgsub-replacements)
|
||||
- [`strsplit(sep, str [, pieces])`](#strsplitsep-str--pieces)
|
||||
- [`strjoin(delimiter, ...)`](#strjoindelimiter-)
|
||||
- [`strtrim(str [, chars])`](#strtrimstr--chars)
|
||||
@@ -16254,27 +16254,48 @@ Both call forms work: `string.match(s, p)` and `s:match(p)`. See
|
||||
global `strmatch`, as on 3.x clients, alongside 1.12's own `strfind`, `strsub`
|
||||
and the other globals for the 5.0 string functions.
|
||||
|
||||
### `string.gsub` table replacement
|
||||
### `string.gsub` replacements
|
||||
|
||||
Lua 5.1 allows a **table** as `gsub`'s replacement: every match looks up
|
||||
the first capture (the whole match when the pattern declares no
|
||||
captures) as a key and substitutes the value. A `nil` or `false` value
|
||||
keeps the original match. The stock 5.0 `gsub` accepts only a string or
|
||||
function replacement and errors on a table
|
||||
(`string or function expected`).
|
||||
`string.gsub(s, pattern, repl [, n])` replaces each match of `pattern` in
|
||||
`s`. The replacement `repl` can be a string, a number, a table, or a
|
||||
function.
|
||||
|
||||
- **String or number**: the text replaces the match. In this text, `%0` is
|
||||
the whole match and `%1` to `%9` are the captures. If the pattern has no
|
||||
captures, `%1` is also the whole match. `%%` is one `%` character.
|
||||
- **Table**: the first capture is the key. If the pattern has no captures,
|
||||
the whole match is the key. The value at that key replaces the match.
|
||||
- **Function**: the function gets the captures as arguments. If the pattern
|
||||
has no captures, it gets the whole match. Its first return value replaces
|
||||
the match.
|
||||
|
||||
If a table value is `nil` or `false`, the original match stays in the
|
||||
string. Any other table value that is not a string or a number causes the
|
||||
error `invalid replacement value (a <type>)`.
|
||||
|
||||
If a function returns a value that is not a string or a number, the match is
|
||||
removed. This includes `nil` and `false`. To keep a match, return the match
|
||||
itself.
|
||||
|
||||
If `repl` is not one of the four types, the call fails with the error
|
||||
`string/function/table expected`.
|
||||
|
||||
```lua
|
||||
("hello world"):gsub("%w+", "<%0>")
|
||||
-- "<hello> <world>", 2
|
||||
|
||||
("$name the $class"):gsub("%$(%w+)", { name = "Bob", class = "Druid" })
|
||||
-- "Bob the Druid", 2
|
||||
|
||||
("$name and $unknown"):gsub("%$(%w+)", { name = "Bob" })
|
||||
-- "Bob and $unknown", 2 (a missing key keeps the match, as in 5.1)
|
||||
-- "Bob and $unknown", 2
|
||||
|
||||
("a b c"):gsub("%a", function(c) if c == "b" then return "B" end end)
|
||||
-- " B ", 3 (the function returns nil for "a" and "c")
|
||||
```
|
||||
|
||||
Both `string.gsub` and the bare `gsub` global have the upgrade; string
|
||||
and function replacements behave exactly as before. One residual: when
|
||||
the pattern itself contains a `%1`..`%9` back-reference, a missing table
|
||||
key removes the match instead of keeping it.
|
||||
`string.gsub`, the global `gsub`, and the method form `s:gsub(...)` are the
|
||||
same function.
|
||||
|
||||
### `strsplit(sep, str [, pieces])`
|
||||
|
||||
|
||||
+26
-7
@@ -8468,6 +8468,11 @@ enum Offsets {
|
||||
// `rawset(nil)` clear leaves it stale and subsequent `table.insert`
|
||||
// appends past the wiped slots.
|
||||
LUAL_SETN = 0x6F4EA0,
|
||||
// `luaL_addlstring(B, s, len)` — __fastcall (len on the stack, ret 4) —
|
||||
// and `luaL_addvalue(B)` — __fastcall, pops the string on top into B.
|
||||
// Both as called by str_gsub (0x007FD26A) and add_s (0x007FD35E).
|
||||
LUAL_ADD_LSTRING = 0x6F51F0,
|
||||
LUAL_ADD_VALUE = 0x6F5270,
|
||||
// `luaL_getn(L, tableIndex)` — Lua 5.0 auxlib table-length getter,
|
||||
// `int __fastcall(L /*ecx*/, idx /*edx*/)`, plain RET (no stack
|
||||
// args), length in eax. Verified by disassembly: a negative idx is
|
||||
@@ -8898,14 +8903,28 @@ enum Offsets {
|
||||
// entry before the registration table's NULL terminator, matching
|
||||
// stock 5.0 order). Verified by decode: args (s, pat, repl, maxN),
|
||||
// `^` anchor pre-strip, and the luaL_argerror(3) "string or
|
||||
// function expected" gate. Its add_value helper (`FUN_007fd2a0`)
|
||||
// matches stock 5.0: a FUNCTION replacement's non-string result is
|
||||
// popped and NOTHING is appended (match → empty) — 5.1 instead
|
||||
// keeps the original match for nil/false. That difference is why
|
||||
// `BaseLib::StringLib`'s 5.1 table-replacement shim wraps the
|
||||
// pattern in an outer capture (arg 1 = whole match) so its adapter
|
||||
// closure can hand the match back for missing keys.
|
||||
// function expected" gate. BaseLib::StringGsub replaces the gate and
|
||||
// the add_s call below.
|
||||
FUN_LUA_STR_GSUB = 0x007FD0E0,
|
||||
// str_gsub's replacement-type gate, ECX = EDI = L on entry: `call
|
||||
// lua_gettop; cmp eax,3; jl` → argerror, `lua_isstring(L,3)` → pass,
|
||||
// `lua_type(L,3) == 6` → pass, else `luaL_argerror(L, 3, "string or
|
||||
// function expected")`. 60 bytes, ending at 0x007FD19A `lea edx,[b]`
|
||||
// (luaL_buffinit). Nothing outside the range jumps into it.
|
||||
SITE_LUA_STR_GSUB_REPL_CHECK = 0x007FD15E,
|
||||
// `add_s(ms, b, s, e)` — __fastcall (ECX = MatchState, EDX = luaL_Buffer,
|
||||
// s/e on the stack, ret 8): Lua 5.0's add_s, which also does the
|
||||
// function replacement (a non-string result is popped and nothing is
|
||||
// added). Each `%n` goes through check_capture (0x007FCE20), so `%0`,
|
||||
// and `%1` in a pattern with no captures, raise "invalid capture index".
|
||||
// Its one caller is the call below; BaseLib::StringGsub still calls it
|
||||
// for a function replacement.
|
||||
FUN_LUA_STR_ADD_S = 0x007FD2A0,
|
||||
CALL_LUA_STR_GSUB_ADD_S = 0x007FD203,
|
||||
// `push_onecapture(ms, i)` — __fastcall, pushes capture `i`: "unfinished
|
||||
// capture" for length -1, the position for -2, else the substring. 5.0's
|
||||
// has no `i >= level` test (5.1's pushes the whole match for i == 0).
|
||||
FUN_LUA_STR_PUSH_ONECAPTURE = 0x007FCF40,
|
||||
// `str_format` — the Lua 5.0 `string.format` C function (`int
|
||||
// __fastcall(void *L)`), strlib entry at `0x00822dc0`. Verified by decode:
|
||||
// reads the format at arg 1, handles `%%`, `%d/i/c`, `%e/E/f/g/G`,
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `string.gsub` takes 5.1's string and table replacements.
|
||||
//
|
||||
// 1.12's str_gsub (0x7FD0E0) is Lua 5.0's, and differs from 5.1 in its
|
||||
// replacement argument:
|
||||
// - it takes a string or a function, not a table;
|
||||
// - `%0` in a replacement string, and `%1` when the pattern has no
|
||||
// captures, raise "invalid capture index" (5.1: the whole match);
|
||||
// - a function that returns nil or false deletes the match (5.1 keeps it),
|
||||
// and one that returns a table deletes it too (5.1 raises an error).
|
||||
//
|
||||
// The first two only ever raised errors, so 5.1's behavior cannot break a
|
||||
// 1.12 addon. The third does: SuperAPI's CombatText_AddMessage returns nil
|
||||
// to delete the player's own " [0x...]" GUID from heal text. A function
|
||||
// replacement therefore stays the engine's own add_s.
|
||||
//
|
||||
// str_gsub itself stays: its match loop, anchor, count and buffer are the
|
||||
// same in both. Two parts are replaced:
|
||||
// - the argument gate (SITE_LUA_STR_GSUB_REPL_CHECK) becomes CheckReplacement;
|
||||
// - the one add_s call (CALL_LUA_STR_GSUB_ADD_S) goes to AddValue: 5.1's
|
||||
// add_s for a string or number, 5.1's table lookup for a table, and the
|
||||
// engine's add_s for a function. They use the engine's capture helpers.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace BaseLib::StringGsub {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
// Lua 5.0's MatchState, as str_gsub builds it on its stack (str_format's
|
||||
// spec scan reads the same layout: level at +0x0C, capture[0].len at +0x14).
|
||||
struct MatchState {
|
||||
const char *srcInit;
|
||||
const char *srcEnd;
|
||||
void *L;
|
||||
int level;
|
||||
struct {
|
||||
const char *init;
|
||||
int len;
|
||||
} capture[32];
|
||||
};
|
||||
|
||||
using add_s_t = void(__fastcall *)(MatchState *ms, void *b, const char *s, const char *e);
|
||||
using push_onecapture_t = void(__fastcall *)(MatchState *ms, int i);
|
||||
using luaL_addlstring_t = void(__fastcall *)(void *b, const char *s, unsigned int len);
|
||||
using luaL_addvalue_t = void(__fastcall *)(void *b);
|
||||
|
||||
const auto EngineAddS = reinterpret_cast<add_s_t>(Offsets::FUN_LUA_STR_ADD_S);
|
||||
const auto EnginePushOneCapture =
|
||||
reinterpret_cast<push_onecapture_t>(Offsets::FUN_LUA_STR_PUSH_ONECAPTURE);
|
||||
const auto AddLString = reinterpret_cast<luaL_addlstring_t>(Offsets::LUAL_ADD_LSTRING);
|
||||
const auto AddBufferValue = reinterpret_cast<luaL_addvalue_t>(Offsets::LUAL_ADD_VALUE);
|
||||
|
||||
// 5.1's push_onecapture: capture `i`, or the whole match for `i == 0` when
|
||||
// the pattern has no captures.
|
||||
void PushOneCapture(MatchState *ms, int i, const char *s, const char *e) {
|
||||
if (i < ms->level)
|
||||
EnginePushOneCapture(ms, i);
|
||||
else if (i == 0)
|
||||
Lua::PushLString(ms->L, s, static_cast<unsigned int>(e - s));
|
||||
else
|
||||
Lua::Error(ms->L, "invalid capture index");
|
||||
}
|
||||
|
||||
// 5.1's add_s: a replacement string, where `%0` is the whole match and `%1`
|
||||
// to `%9` are captures.
|
||||
void AddString(MatchState *ms, void *b, const char *s, const char *e) {
|
||||
void *L = ms->L;
|
||||
const char *news = Lua::ToString(L, 3);
|
||||
const unsigned int l = Lua::StrLen(L, 3);
|
||||
for (unsigned int i = 0; i < l; i++) {
|
||||
if (news[i] != '%') {
|
||||
AddLString(b, news + i, 1);
|
||||
continue;
|
||||
}
|
||||
i++; // skip the escape
|
||||
const char c = news[i];
|
||||
if (c < '0' || c > '9') {
|
||||
AddLString(b, news + i, 1);
|
||||
} else if (c == '0') {
|
||||
AddLString(b, s, static_cast<unsigned int>(e - s));
|
||||
} else {
|
||||
PushOneCapture(ms, c - '1', s, e);
|
||||
AddBufferValue(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5.1's add_value for a string or table, the engine's add_s for a function.
|
||||
// Called as add_s: __fastcall, ret 8.
|
||||
void __fastcall AddValue(MatchState *ms, void *b, const char *s, const char *e) {
|
||||
void *L = ms->L;
|
||||
switch (Lua::Type(L, 3)) {
|
||||
case Lua::TYPE_FUNCTION:
|
||||
// A result that is not a string or number deletes the match.
|
||||
EngineAddS(ms, b, s, e);
|
||||
return;
|
||||
case Lua::TYPE_TABLE:
|
||||
break;
|
||||
default: // number or string
|
||||
AddString(ms, b, s, e);
|
||||
return;
|
||||
}
|
||||
PushOneCapture(ms, 0, s, e);
|
||||
Lua::GetTable(L, 3);
|
||||
if (!Lua::ToBoolean(L, -1)) {
|
||||
Lua::SetTop(L, -2);
|
||||
Lua::PushLString(L, s, static_cast<unsigned int>(e - s)); // keep the match
|
||||
} else if (!Lua::IsString(L, -1)) {
|
||||
Lua::Error(L, "invalid replacement value (a %s)", Lua::TypeName(L, Lua::Type(L, -1)));
|
||||
}
|
||||
AddBufferValue(b);
|
||||
}
|
||||
|
||||
// 5.1's gate on the replacement argument. Called in place of the engine's,
|
||||
// with ECX = L.
|
||||
void __fastcall CheckReplacement(void *L) {
|
||||
const int t = Lua::Type(L, 3);
|
||||
if (t != Lua::TYPE_NUMBER && t != Lua::TYPE_STRING && t != Lua::TYPE_FUNCTION &&
|
||||
t != Lua::TYPE_TABLE)
|
||||
Lua::ArgError(L, 3, "string/function/table expected");
|
||||
}
|
||||
|
||||
// Runs on every registration pass; a site already patched no longer matches
|
||||
// and is skipped.
|
||||
void Apply() {
|
||||
static const uint8_t kGate[60] = {
|
||||
0xE8, 0x0D, 0x5F, 0xEF, 0xFF, 0x83, 0xF8, 0x03, 0x7C, 0x21, 0xBA, 0x03,
|
||||
0x00, 0x00, 0x00, 0x8B, 0xCF, 0xE8, 0x9C, 0x63, 0xEF, 0xFF, 0x85, 0xC0,
|
||||
0x75, 0x22, 0xBA, 0x03, 0x00, 0x00, 0x00, 0x8B, 0xCF, 0xE8, 0x7C, 0x62,
|
||||
0xEF, 0xFF, 0x83, 0xF8, 0x06, 0x74, 0x11, 0x68, 0x84, 0x23, 0x88, 0x00,
|
||||
0xBA, 0x03, 0x00, 0x00, 0x00, 0x8B, 0xCF, 0xE8, 0x76, 0x76, 0xEF, 0xFF,
|
||||
};
|
||||
Common::ReplaceWithCall(Offsets::SITE_LUA_STR_GSUB_REPL_CHECK, kGate, sizeof kGate,
|
||||
reinterpret_cast<const void *>(&CheckReplacement));
|
||||
Common::RedirectCall(Offsets::CALL_LUA_STR_GSUB_ADD_S, Offsets::FUN_LUA_STR_ADD_S,
|
||||
reinterpret_cast<const void *>(&AddValue));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Apply};
|
||||
const Game::GlueModuleAutoRegister _glueAutoreg{&Apply};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::StringGsub
|
||||
+7
-133
@@ -16,7 +16,6 @@
|
||||
// - `string.match(s, pattern [, init])` — first-match extraction (5.1),
|
||||
// also the global `strmatch`, as `string.find` is `strfind`.
|
||||
// - `string.gmatch(s, pattern)` — match iterator (5.1).
|
||||
// - `string.gsub` TABLE replacement — 5.1 form (see the shim below).
|
||||
// - `strsplit(sep, str [, pieces])` — WoW global; split on any char.
|
||||
// - `strjoin(delimiter, ...)` — WoW global; join varargs.
|
||||
// - `strtrim(str [, chars])` — WoW global; trim a char set.
|
||||
@@ -341,128 +340,6 @@ int __fastcall Script_string_reverse(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// --- string.gsub table-replacement upgrade (Lua 5.1) ------------------------
|
||||
//
|
||||
// 5.1 allows a TABLE as gsub's replacement: each match looks up the first
|
||||
// capture (the whole match when the pattern declares no captures) in the
|
||||
// table; a nil/false value keeps the original match; string/number values
|
||||
// substitute. This engine's gsub (FUN_LUA_STR_GSUB) is stock 5.0 —
|
||||
// string/function only ("string or function expected" otherwise), and its
|
||||
// add_value appends NOTHING when a replacement function returns a
|
||||
// non-string (match → empty; verified by decode, see Offsets.h). So a
|
||||
// plain table→closure adapter would EAT unmatched tokens where 5.1 keeps
|
||||
// them, and the closure can't reconstruct the whole match from capture
|
||||
// args alone.
|
||||
//
|
||||
// The shim therefore rewrites the pattern with one OUTER capture —
|
||||
// `pat` → `(pat)`, keeping a leading `^` and an unescaped trailing `$`
|
||||
// outside (both are anchors only at their positional ends; inside the
|
||||
// parens they'd turn literal) — so the adapter closure always receives
|
||||
// the whole match as arg 1, the pattern's own first capture as arg 2, and
|
||||
// can hand arg 1 back to reproduce 5.1's keep-match for missing keys.
|
||||
// Patterns containing a `%1`..`%9` back-reference can't be wrapped (the
|
||||
// outer capture would renumber what `%N` refers to); those fall back to
|
||||
// an unwrapped adapter whose missing-key behavior is the engine's own
|
||||
// (match → empty) — the one documented divergence, and the combination
|
||||
// (table replacement + pattern back-references) is essentially absent
|
||||
// from real code. String/function replacements tail-call the engine gsub
|
||||
// untouched.
|
||||
|
||||
// True when the pattern contains a `%1`..`%9` back-reference. Escape-aware:
|
||||
// the char after any `%` is consumed, so `%%1` (literal percent, then '1')
|
||||
// doesn't count.
|
||||
bool PatternHasBackref(const char *p, unsigned len) {
|
||||
for (unsigned i = 0; i + 1 < len; ++i) {
|
||||
if (p[i] != '%')
|
||||
continue;
|
||||
const char c = p[i + 1];
|
||||
if (c >= '1' && c <= '9')
|
||||
return true;
|
||||
++i; // skip the escaped char
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Adapter for the wrapped-pattern path: arg 1 = whole match, arg 2 = the
|
||||
// pattern's own first capture (when it declared any). Upvalue 1 = the
|
||||
// replacement table. Missing/false value → return the whole match (5.1's
|
||||
// keep-match).
|
||||
int __fastcall GsubTableLookupWrapped(void *L) {
|
||||
const int nargs = Game::Lua::GetTop(L);
|
||||
Game::Lua::PushValue(L, nargs >= 2 ? 2 : 1); // the 5.1 lookup key
|
||||
Game::Lua::GetTable(L, Game::Lua::UpvalueIndex(1)); // tbl[key]
|
||||
const int t = Game::Lua::Type(L, -1);
|
||||
if (t == Game::Lua::TYPE_NIL ||
|
||||
(t == Game::Lua::TYPE_BOOLEAN && Game::Lua::ToBoolean(L, -1) == 0))
|
||||
Game::Lua::PushValue(L, 1); // keep the original match
|
||||
return 1; // top value is the result
|
||||
}
|
||||
|
||||
// Adapter for back-reference patterns (unwrapped): arg 1 is already the
|
||||
// 5.1 key. A missing key returns nil → the engine appends nothing.
|
||||
int __fastcall GsubTableLookupPlain(void *L) {
|
||||
Game::Lua::PushValue(L, 1);
|
||||
Game::Lua::GetTable(L, Game::Lua::UpvalueIndex(1));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_string_gsub(void *L) {
|
||||
const auto engineGsub =
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_LUA_STR_GSUB);
|
||||
if (Game::Lua::Type(L, 3) != Game::Lua::TYPE_TABLE)
|
||||
return engineGsub(L); // string/function/error paths untouched
|
||||
|
||||
const char *pat = Game::Lua::ToString(L, 2);
|
||||
if (pat == nullptr)
|
||||
return engineGsub(L); // let the engine raise its own arg error
|
||||
const unsigned patLen = Game::Lua::StrLen(L, 2);
|
||||
|
||||
Game::Lua::SetTop(L, 4); // normalize to (s, pat, tbl, n) — gsub reads ≤4
|
||||
|
||||
if (!PatternHasBackref(pat, patLen)) {
|
||||
// Build `(body)` with the anchors outside the capture.
|
||||
unsigned b = 0, e = patLen;
|
||||
std::string wrapped;
|
||||
wrapped.reserve(patLen + 3);
|
||||
if (e > b && pat[0] == '^') {
|
||||
wrapped += '^';
|
||||
b = 1;
|
||||
}
|
||||
bool anchoredBack = false;
|
||||
if (e > b && pat[e - 1] == '$') {
|
||||
// `$` is an anchor only when not %-escaped: an even number of
|
||||
// `%` immediately before it means the `$` itself is unescaped.
|
||||
unsigned pc = 0;
|
||||
for (int idx = static_cast<int>(e) - 2;
|
||||
idx >= static_cast<int>(b) && pat[idx] == '%'; --idx)
|
||||
++pc;
|
||||
anchoredBack = (pc % 2) == 0;
|
||||
if (anchoredBack)
|
||||
--e;
|
||||
}
|
||||
wrapped += '(';
|
||||
wrapped.append(pat + b, e - b);
|
||||
wrapped += ')';
|
||||
if (anchoredBack)
|
||||
wrapped += '$';
|
||||
|
||||
Game::Lua::PushLString(L, wrapped.data(),
|
||||
static_cast<unsigned int>(wrapped.size())); // 5
|
||||
Game::Lua::PushValue(L, 3); // 6: tbl
|
||||
Game::Lua::PushCClosure(L, &GsubTableLookupWrapped, 1); // 6: closure
|
||||
Game::Lua::Insert(L, 2); // closure→2: s, closure, pat, tbl, n, wrapped
|
||||
Game::Lua::Insert(L, 2); // wrapped→2: s, wrapped, closure, pat, tbl, n
|
||||
Game::Lua::Remove(L, 4); // drop the original pattern
|
||||
Game::Lua::Remove(L, 4); // drop the table → (s, wrapped, closure, n)
|
||||
} else {
|
||||
Game::Lua::PushValue(L, 3); // 5: tbl
|
||||
Game::Lua::PushCClosure(L, &GsubTableLookupPlain, 1); // 5: closure
|
||||
Game::Lua::Insert(L, 3); // → s, pat, closure, tbl, n
|
||||
Game::Lua::Remove(L, 4); // → s, pat, closure, n
|
||||
}
|
||||
return engineGsub(L);
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kStrlenutf8Args[] = {
|
||||
@@ -515,22 +392,19 @@ const Game::Doc::Function kStrconcat{
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("string", "match", &Script_string_match);
|
||||
Game::Lua::RegisterTableFunction("string", "gmatch", Script_string_gmatch);
|
||||
Game::Lua::RegisterTableFunction("string", "gsub", &Script_string_gsub);
|
||||
Game::Lua::RegisterTableFunction("string", "reverse", &Script_string_reverse);
|
||||
Game::Lua::RegisterTableFunction("string", "split", &Script_strsplit);
|
||||
Game::Lua::RegisterTableFunction("string", "trim", &Script_strtrim);
|
||||
Game::Lua::RegisterTableFunction("string", "join", &Script_strjoin);
|
||||
Game::Lua::RegisterTableFunction("string", "concat", &Script_strconcat, &kStrconcat);
|
||||
// 4.3.4's compat.lua binds each short global and its string-table twin
|
||||
// to ONE function by assignment (`gsub = str.gsub`, `strmatch =
|
||||
// str.match`, `strrev = str.reverse`, `str.trim = strtrim`, `str.split =
|
||||
// strsplit`, `str.join = strjoin`), so `strtrim == string.trim`. A second
|
||||
// Register* call would mint a second closure over the same C function, so
|
||||
// the globals alias the table entries by value instead. The string-table
|
||||
// half is what lets `(" x "):trim()` work and libraries capture it at
|
||||
// file scope (AceConfigCmd-3.0: `strtrim = string.trim`); the bare `gsub`
|
||||
// gets the table-replacement upgrade by being the same function.
|
||||
Game::Lua::RegisterGlobalAlias("gsub", "string", "gsub");
|
||||
// to ONE function by assignment (`strmatch = str.match`, `strrev =
|
||||
// str.reverse`, `str.trim = strtrim`, `str.split = strsplit`, `str.join =
|
||||
// strjoin`), so `strtrim == string.trim`. A second Register* call would
|
||||
// mint a second closure over the same C function, so the globals alias
|
||||
// the table entries by value instead. The string-table half is what lets
|
||||
// `(" x "):trim()` work and libraries capture it at file scope
|
||||
// (AceConfigCmd-3.0: `strtrim = string.trim`).
|
||||
Game::Lua::RegisterGlobalAlias("strmatch", "string", "match");
|
||||
Game::Lua::RegisterGlobalAlias("strsplit", "string", "split");
|
||||
Game::Lua::RegisterGlobalAlias("strjoin", "string", "join");
|
||||
|
||||
Reference in New Issue
Block a user