summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorruki <[email protected]>2026-01-21 11:59:57 +0800
committerGitHub <[email protected]>2026-01-21 11:59:57 +0800
commitf8d44002372ca91bbb29cf3548c306f226e6ef4e (patch)
treebdc4a15863537a347084a5e85bf75beb75056c00
parent9e58120568d395b5a636daa7d0f23150fe87bb95 (diff)
parent6ff8486cd3297ca0e93518c33e32678c97128a3f (diff)
Merge pull request #7246 from xmake-io/utf8
Add utf8 module
-rw-r--r--core/src/xmake/engine.c37
-rw-r--r--core/src/xmake/prefix.h9
-rw-r--r--core/src/xmake/string/lastof.c42
-rw-r--r--core/src/xmake/utf8/byte.c52
-rw-r--r--core/src/xmake/utf8/char.c67
-rw-r--r--core/src/xmake/utf8/codepoint.c71
-rw-r--r--core/src/xmake/utf8/codes.c76
-rw-r--r--core/src/xmake/utf8/find.c134
-rw-r--r--core/src/xmake/utf8/lastof.c65
-rw-r--r--core/src/xmake/utf8/len.c55
-rw-r--r--core/src/xmake/utf8/offset.c56
-rw-r--r--core/src/xmake/utf8/prefix.h9
-rw-r--r--core/src/xmake/utf8/reverse.c57
-rw-r--r--core/src/xmake/utf8/sub.c47
-rw-r--r--core/src/xmake/utf8/utf8.c512
-rw-r--r--core/src/xmake/utf8/utf8.h60
-rw-r--r--core/src/xmake/utf8/width.c45
-rwxr-xr-xcore/src/xmake/xmake.sh1
-rw-r--r--tests/modules/path/test.lua17
-rw-r--r--tests/modules/string/test.lua7
-rw-r--r--tests/modules/utf8/test.lua197
-rw-r--r--tests/runner.lua2
-rw-r--r--xmake/core/base/string.lua172
-rw-r--r--xmake/core/base/utf8.lua46
-rw-r--r--xmake/core/sandbox/modules/interpreter/utf8.lua22
-rw-r--r--xmake/core/sandbox/modules/utf8.lua21
-rw-r--r--xmake/core/ui/label.lua21
27 files changed, 1720 insertions, 180 deletions
diff --git a/core/src/xmake/engine.c b/core/src/xmake/engine.c
index d81828f1f..9ceabd156 100644
--- a/core/src/xmake/engine.c
+++ b/core/src/xmake/engine.c
@@ -279,6 +279,19 @@ tb_int_t xm_winos_registry_values(lua_State *lua);
tb_int_t xm_winos_short_path(lua_State *lua);
#endif
+// the utf8 functions
+tb_int_t xm_utf8_len(lua_State *lua);
+tb_int_t xm_utf8_char(lua_State *lua);
+tb_int_t xm_utf8_byte(lua_State *lua);
+tb_int_t xm_utf8_codepoint(lua_State *lua);
+tb_int_t xm_utf8_offset(lua_State *lua);
+tb_int_t xm_utf8_codes(lua_State *lua);
+tb_int_t xm_utf8_sub(lua_State *lua);
+tb_int_t xm_utf8_reverse(lua_State *lua);
+tb_int_t xm_utf8_lastof(lua_State *lua);
+tb_int_t xm_utf8_find(lua_State *lua);
+tb_int_t xm_utf8_width(lua_State *lua);
+
// the string functions
tb_int_t xm_string_trim(lua_State *lua);
tb_int_t xm_string_split(lua_State *lua);
@@ -583,6 +596,24 @@ static luaL_Reg const g_bloom_filter_functions[] = {
{ tb_null, tb_null },
};
+// the utf8 functions
+static luaL_Reg const g_utf8_functions[] = {
+ {"char", xm_utf8_char},
+ {"byte", xm_utf8_byte},
+ {"codes", xm_utf8_codes},
+ {"codepoint", xm_utf8_codepoint},
+ {"len", xm_utf8_len},
+ {"offset", xm_utf8_offset},
+ {"sub", xm_utf8_sub},
+ {"reverse", xm_utf8_reverse},
+ {"lastof", xm_utf8_lastof},
+ {"find", xm_utf8_find},
+ {"width", xm_utf8_width},
+ {"wcwidth", xm_utf8_width},
+ {"wcswidth", xm_utf8_width},
+ {tb_null, tb_null}
+};
+
// the string functions
static luaL_Reg const g_string_functions[] = {
{ "trim", xm_string_trim },
@@ -722,6 +753,9 @@ static luaL_Reg const g_thread_functions[] = {
{ tb_null, tb_null },
};
+// the utf8 functions
+
+
// the lua global instance for signal handler
static lua_State *g_lua = tb_null;
@@ -1547,6 +1581,9 @@ xm_engine_ref_t xm_engine_init(tb_char_t const *name, xm_engine_lni_initalizer_c
// bind base64 functions
xm_lua_register(engine->lua, "base64", g_base64_functions);
+ // bind utf8 functions
+ xm_lua_register(engine->lua, "utf8", g_utf8_functions);
+
// bind string functions
xm_lua_register(engine->lua, "string", g_string_functions);
diff --git a/core/src/xmake/prefix.h b/core/src/xmake/prefix.h
index 4b159741e..7cd2ea418 100644
--- a/core/src/xmake/prefix.h
+++ b/core/src/xmake/prefix.h
@@ -43,6 +43,15 @@
#endif
/* //////////////////////////////////////////////////////////////////////////////////////
+ * types
+ */
+
+// define lua_Unsigned for luajit/lua5.1
+#if defined(USE_LUAJIT) || LUA_VERSION_NUM < 503
+typedef size_t lua_Unsigned;
+#endif
+
+/* //////////////////////////////////////////////////////////////////////////////////////
* private interfaces
*/
diff --git a/core/src/xmake/string/lastof.c b/core/src/xmake/string/lastof.c
index 9ac6c6784..912647a9c 100644
--- a/core/src/xmake/string/lastof.c
+++ b/core/src/xmake/string/lastof.c
@@ -29,39 +29,7 @@
* includes
*/
#include "prefix.h"
-
-/* //////////////////////////////////////////////////////////////////////////////////////
- * private implementation
- */
-static tb_void_t xm_string_lastof_str(
- lua_State *lua, tb_char_t const *cstr, tb_size_t nstr, tb_char_t const *csubstr, tb_size_t nsubstr) {
- // find it
- tb_char_t const *curr = tb_null;
- tb_char_t const *next = cstr;
- do {
- next = tb_strstr(next, csubstr); // faster than tb_strnstr()
- if (next) {
- curr = next;
- next += nsubstr;
- }
-
- } while (!next);
-
- // found?
- if (curr) {
- lua_pushinteger(lua, curr - cstr + 1);
- } else {
- lua_pushnil(lua);
- }
-}
-static tb_void_t xm_string_lastof_chr(lua_State *lua, tb_char_t const *cstr, tb_size_t nstr, tb_char_t ch) {
- tb_char_t const *pos = tb_strrchr(cstr, ch); // faster than tb_strnrchr()
- if (pos) {
- lua_pushinteger(lua, pos - cstr + 1);
- } else {
- lua_pushnil(lua);
- }
-}
+#include "../utf8/utf8.h"
/* //////////////////////////////////////////////////////////////////////////////////////
* implementation
@@ -84,11 +52,11 @@ tb_int_t xm_string_lastof(lua_State *lua) {
tb_char_t const *csubstr = luaL_checklstring(lua, 2, &nsubstr);
// lastof it
- lua_newtable(lua);
- if (nsubstr == 1) {
- xm_string_lastof_chr(lua, cstr, (tb_size_t)nstr, csubstr[0]);
+ tb_long_t char_pos = xm_utf8_lastof_impl(cstr, nstr, csubstr, nsubstr);
+ if (char_pos > 0) {
+ lua_pushinteger(lua, char_pos);
} else {
- xm_string_lastof_str(lua, cstr, (tb_size_t)nstr, csubstr, nsubstr);
+ lua_pushnil(lua);
}
return 1;
}
diff --git a/core/src/xmake/utf8/byte.c b/core/src/xmake/utf8/byte.c
new file mode 100644
index 000000000..18cd5d7c9
--- /dev/null
+++ b/core/src/xmake/utf8/byte.c
@@ -0,0 +1,52 @@
+/*!A cross-platform build utility based on Lua
+ *
+ * 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 (C) 2015-present, Xmake Open Source Community.
+ *
+ * @author ruki
+ * @file byte.c
+ *
+ */
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * includes
+ */
+#include "utf8.h"
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * private implementation
+ */
+static tb_bool_t xm_utf8_byte_cb(xm_utf8_int_t code, tb_cpointer_t udata) {
+ lua_State* lua = (lua_State*)udata;
+ tb_assert_and_check_return_val(lua, tb_false);
+
+ luaL_checkstack(lua, 1, "too many results");
+ lua_pushinteger(lua, code);
+ return tb_true;
+}
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * implementation
+ */
+
+/* utf8.byte(s, i [, j])
+ */
+tb_int_t xm_utf8_byte(lua_State* lua) {
+ size_t len;
+ tb_char_t const* s = luaL_checklstring(lua, 1, &len);
+ lua_Integer i = luaL_optinteger(lua, 2, 1);
+ lua_Integer j = luaL_optinteger(lua, 3, i);
+
+ return (tb_int_t)xm_utf8_byte_impl(s, len, (tb_long_t)i, (tb_long_t)j, xm_utf8_byte_cb, lua);
+}
diff --git a/core/src/xmake/utf8/char.c b/core/src/xmake/utf8/char.c
new file mode 100644
index 000000000..760dab264
--- /dev/null
+++ b/core/src/xmake/utf8/char.c
@@ -0,0 +1,67 @@
+/*!A cross-platform build utility based on Lua
+ *
+ * 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 (C) 2015-present, Xmake Open Source Community.
+ *
+ * @author ruki
+ * @file char.c
+ *
+ */
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * includes
+ */
+#include "utf8.h"
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * private implementation
+ */
+
+static void xm_utf8_char_push(lua_State *lua, tb_int_t arg) {
+ lua_Unsigned code = (lua_Unsigned)luaL_checkinteger(lua, arg);
+ luaL_argcheck(lua, code <= XM_UTF8_MAXUTF, arg, "value out of range");
+
+ tb_char_t buf[8];
+ tb_size_t n = xm_utf8_encode(buf, (xm_utf8_int_t)code);
+ if (n > 0) {
+ lua_pushlstring(lua, buf, n);
+ } else {
+ luaL_error(lua, "value out of range");
+ }
+}
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * implementation
+ */
+
+/* utfchar(n1, n2, ...) -> char(n1)..char(n2)...
+ */
+tb_int_t xm_utf8_char(lua_State *lua) {
+ tb_assert_and_check_return_val(lua, 0);
+
+ tb_int_t n = lua_gettop(lua); // number of arguments
+ if (n == 1) { // optimize common case of single char
+ xm_utf8_char_push(lua, 1);
+ } else {
+ tb_int_t i;
+ luaL_Buffer b;
+ luaL_buffinit(lua, &b);
+ for (i = 1; i <= n; i++) {
+ xm_utf8_char_push(lua, i);
+ luaL_addvalue(&b);
+ }
+ luaL_pushresult(&b);
+ }
+ return 1;
+}
diff --git a/core/src/xmake/utf8/codepoint.c b/core/src/xmake/utf8/codepoint.c
new file mode 100644
index 000000000..a8fff91dd
--- /dev/null
+++ b/core/src/xmake/utf8/codepoint.c
@@ -0,0 +1,71 @@
+/*!A cross-platform build utility based on Lua
+ *
+ * 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 (C) 2015-present, Xmake Open Source Community.
+ *
+ * @author ruki
+ * @file codepoint.c
+ *
+ */
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * includes
+ */
+#include "utf8.h"
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * private implementation
+ */
+static tb_bool_t xm_utf8_codepoint_cb(xm_utf8_int_t code, tb_cpointer_t udata) {
+ lua_State* lua = (lua_State*)udata;
+ tb_assert_and_check_return_val(lua, tb_false);
+
+ lua_pushinteger(lua, code);
+ return tb_true;
+}
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * implementation
+ */
+
+/* codepoint(s, [i, [j [, lax]]]) -> returns codepoints for all
+ * characters that start in the range [i,j]
+ */
+tb_int_t xm_utf8_codepoint(lua_State *lua) {
+ size_t len;
+ tb_char_t const* s = luaL_checklstring(lua, 1, &len);
+ lua_Integer posi = xm_utf8_posrelat((tb_long_t)luaL_optinteger(lua, 2, 1), len);
+ lua_Integer pose = xm_utf8_posrelat((tb_long_t)luaL_optinteger(lua, 3, posi), len);
+ tb_bool_t lax = lua_toboolean(lua, 4);
+
+ luaL_argcheck(lua, posi >= 1, 2, "out of bounds");
+ luaL_argcheck(lua, pose <= (lua_Integer)len, 3, "out of bounds");
+
+ if (posi > pose) {
+ return 0; // empty interval; return no values
+ }
+ if (pose - posi >= INT_MAX) { // (lua_Integer -> int) overflow?
+ return luaL_error(lua, "string slice too long");
+ }
+
+ tb_int_t n = (tb_int_t)(pose - posi) + 1;
+ luaL_checkstack(lua, n, "string slice too long");
+
+ int nresults = lua_gettop(lua);
+ if (!xm_utf8_codepoint_impl(s, len, (tb_long_t)posi, (tb_long_t)pose, !lax, xm_utf8_codepoint_cb, lua)) {
+ return luaL_error(lua, XM_UTF8_MSGInvalid);
+ }
+
+ return lua_gettop(lua) - nresults;
+}
diff --git a/core/src/xmake/utf8/codes.c b/core/src/xmake/utf8/codes.c
new file mode 100644
index 000000000..bfae6397d
--- /dev/null
+++ b/core/src/xmake/utf8/codes.c
@@ -0,0 +1,76 @@
+/*!A cross-platform build utility based on Lua
+ *
+ * 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 (C) 2015-present, Xmake Open Source Community.
+ *
+ * @author ruki
+ * @file codes.c
+ *
+ */
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * includes
+ */
+#include "utf8.h"
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * private implementation
+ */
+
+static tb_int_t xm_utf8_codes_iter(lua_State *lua, tb_bool_t strict) {
+ tb_assert_and_check_return_val(lua, 0);
+
+ size_t len;
+ tb_char_t const* s = luaL_checklstring(lua, 1, &len);
+ lua_Unsigned n = (lua_Unsigned)lua_tointeger(lua, 2);
+ if (n < len) {
+ while (n < len && xm_utf8_iscontp(s + n)) {
+ n++; // go to next character
+ }
+ }
+ if (n >= len) { // (also handles original 'n' being negative)
+ return 0; // no more codepoints
+ } else {
+ xm_utf8_int_t code;
+ tb_char_t const* next = xm_utf8_decode(s + n, &code, strict);
+ if (next == NULL || (next < s + len && xm_utf8_iscontp(next))) {
+ return luaL_error(lua, XM_UTF8_MSGInvalid);
+ }
+ lua_pushinteger(lua, n + 1);
+ lua_pushinteger(lua, code);
+ return 2;
+ }
+}
+
+static int xm_utf8_codes_iter_strict(lua_State *lua) {
+ return xm_utf8_codes_iter(lua, tb_true);
+}
+
+static int xm_utf8_codes_iter_lax(lua_State *lua) {
+ return xm_utf8_codes_iter(lua, tb_false);
+}
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * implementation
+ */
+
+tb_int_t xm_utf8_codes(lua_State *lua) {
+ tb_bool_t lax = lua_toboolean(lua, 2);
+ tb_char_t const* s = luaL_checkstring(lua, 1);
+ luaL_argcheck(lua, !xm_utf8_iscontp(s), 1, XM_UTF8_MSGInvalid);
+ lua_pushcfunction(lua, lax ? xm_utf8_codes_iter_lax : xm_utf8_codes_iter_strict);
+ lua_pushvalue(lua, 1);
+ lua_pushinteger(lua, 0);
+ return 3;
+}
diff --git a/core/src/xmake/utf8/find.c b/core/src/xmake/utf8/find.c
new file mode 100644
index 000000000..f2268f9b4
--- /dev/null
+++ b/core/src/xmake/utf8/find.c
@@ -0,0 +1,134 @@
+/*!A cross-platform build utility based on Lua
+ *
+ * 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 (C) 2015-present, Xmake Open Source Community.
+ *
+ * @author ruki
+ * @file find.c
+ *
+ */
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * includes
+ */
+#include "utf8.h"
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * private implementation
+ */
+
+static tb_int_t xm_utf8_find_impl_plain(lua_State* lua, tb_char_t const* s, size_t len, tb_char_t const* sub, size_t sublen, lua_Integer init) {
+ tb_long_t char_end = 0;
+ tb_long_t char_start = xm_utf8_find_impl(s, len, sub, sublen, (tb_long_t)init, &char_end);
+ if (char_start > 0) {
+ lua_pushinteger(lua, char_start);
+ lua_pushinteger(lua, char_end);
+ return 2;
+ }
+ lua_pushnil(lua);
+ return 1;
+}
+
+static tb_int_t xm_utf8_find_impl_pattern(lua_State* lua, tb_char_t const* s, size_t len, lua_Integer init) {
+ int base = lua_gettop(lua);
+ tb_long_t byte_init = 1;
+ if (init > 0) {
+ if (init > 1) {
+ byte_init = xm_utf8_offset_impl(s, len, (tb_long_t)init, 1);
+ if (byte_init <= 0) {
+ lua_pushnil(lua);
+ return 1;
+ }
+ }
+ } else if (init < 0) {
+ byte_init = xm_utf8_offset_impl(s, len, (tb_long_t)init, len + 1);
+ if (byte_init <= 0) {
+ lua_pushnil(lua);
+ return 1;
+ }
+ }
+
+ lua_getglobal(lua, "string");
+ lua_getfield(lua, -1, "find");
+ lua_pushvalue(lua, 1); // s
+ lua_pushvalue(lua, 2); // pattern
+ lua_pushinteger(lua, byte_init); // init (byte)
+ lua_pushboolean(lua, 0); // plain
+
+ lua_call(lua, 4, LUA_MULTRET);
+
+ // Stack: [args, string_table, results...]
+ int nres = lua_gettop(lua) - (base + 1);
+ if (nres <= 0 || lua_isnil(lua, base + 2)) {
+ lua_pushnil(lua);
+ lua_remove(lua, base + 1);
+ return 1;
+ }
+
+ lua_Integer b_start = lua_tointeger(lua, base + 2);
+ lua_Integer b_end = lua_tointeger(lua, base + 3);
+
+ tb_long_t char_start = 1;
+ if (b_start > 1) {
+ tb_long_t count = xm_utf8_len_impl(s, len, 1, (tb_long_t)b_start - 1, tb_true, tb_null);
+ if (count < 0) {
+ lua_pushnil(lua);
+ lua_remove(lua, base + 1);
+ return 1;
+ }
+ char_start = count + 1;
+ }
+
+ tb_long_t match_char_len = 0;
+ if (b_end >= b_start) {
+ match_char_len = xm_utf8_len_impl(s, len, (tb_long_t)b_start, (tb_long_t)b_end, tb_true, tb_null);
+ if (match_char_len < 0) {
+ lua_pushnil(lua);
+ lua_remove(lua, base + 1);
+ return 1;
+ }
+ }
+
+ lua_pushinteger(lua, char_start);
+ lua_replace(lua, base + 2);
+ lua_pushinteger(lua, char_start + match_char_len - 1);
+ lua_replace(lua, base + 3);
+
+ lua_remove(lua, base + 1);
+
+ return nres;
+}
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * implementation
+ */
+
+/* utf8.find(s, target [, init [, plain]])
+ */
+tb_int_t xm_utf8_find(lua_State *lua) {
+ tb_assert_and_check_return_val(lua, 0);
+
+ size_t len;
+ tb_char_t const* s = luaL_checklstring(lua, 1, &len);
+ size_t sublen;
+ tb_char_t const* sub = luaL_checklstring(lua, 2, &sublen);
+ lua_Integer init = luaL_optinteger(lua, 3, 1);
+ tb_int_t plain = lua_toboolean(lua, 4);
+
+ if (plain) {
+ return xm_utf8_find_impl_plain(lua, s, len, sub, sublen, init);
+ } else {
+ return xm_utf8_find_impl_pattern(lua, s, len, init);
+ }
+}
diff --git a/core/src/xmake/utf8/lastof.c b/core/src/xmake/utf8/lastof.c
new file mode 100644
index 000000000..165da3b69
--- /dev/null
+++ b/core/src/xmake/utf8/lastof.c
@@ -0,0 +1,65 @@
+/*!A cross-platform build utility based on Lua
+ *
+ * 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 (C) 2015-present, Xmake Open Source Community.
+ *
+ * @author ruki
+ * @file lastof.c
+ *
+ */
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * includes
+ */
+#include "utf8.h"
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * implementation
+ */
+
+/* utf8.lastof(s, pattern, plain)
+ */
+tb_int_t xm_utf8_lastof(lua_State *lua) {
+ size_t len;
+ tb_char_t const* s = luaL_checklstring(lua, 1, &len);
+ size_t sublen;
+ tb_char_t const* sub = luaL_checklstring(lua, 2, &sublen);
+ tb_int_t plain = lua_toboolean(lua, 3);
+ tb_long_t byte_pos = 0;
+
+ if (plain) {
+ byte_pos = xm_utf8_lastof_impl(s, len, sub, sublen);
+ } else {
+ lua_getglobal(lua, "string");
+ lua_getfield(lua, -1, "lastof");
+ lua_pushvalue(lua, 1); // s
+ lua_pushvalue(lua, 2); // pattern
+ lua_pushboolean(lua, 0); // plain = false
+ lua_call(lua, 3, 1);
+ if (!lua_isnil(lua, -1)) {
+ byte_pos = (tb_long_t)lua_tointeger(lua, -1);
+ }
+ }
+
+ if (byte_pos > 0) {
+ tb_long_t char_pos = xm_utf8_charpos(s, len, byte_pos);
+ if (char_pos > 0) {
+ lua_pushinteger(lua, char_pos);
+ return 1;
+ }
+ }
+
+ lua_pushnil(lua);
+ return 1;
+}
diff --git a/core/src/xmake/utf8/len.c b/core/src/xmake/utf8/len.c
new file mode 100644
index 000000000..3e1f35bf5
--- /dev/null
+++ b/core/src/xmake/utf8/len.c
@@ -0,0 +1,55 @@
+/*!A cross-platform build utility based on Lua
+ *
+ * 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 (C) 2015-present, Xmake Open Source Community.
+ *
+ * @author ruki
+ * @file len.c
+ *
+ */
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * includes
+ */
+#include "utf8.h"
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * implementation
+ */
+
+/* utf8len(s [, i [, j [, lax]]]) --> number of characters that
+ * start in the range [i,j], or nil + current position if 's' is not
+ * well formed in that interval
+ */
+tb_int_t xm_utf8_len(lua_State *lua) {
+ tb_assert_and_check_return_val(lua, 0);
+
+ size_t len;
+ tb_char_t const* s = luaL_checklstring(lua, 1, &len);
+ lua_Integer posi = xm_utf8_posrelat((tb_long_t)luaL_optinteger(lua, 2, 1), len);
+ lua_Integer posj = xm_utf8_posrelat((tb_long_t)luaL_optinteger(lua, 3, -1), len);
+ tb_bool_t lax = lua_toboolean(lua, 4);
+ luaL_argcheck(lua, 1 <= posi && --posi <= (lua_Integer)len, 2, "initial position out of bounds");
+ luaL_argcheck(lua, --posj < (lua_Integer)len, 3, "final position out of bounds");
+
+ tb_size_t errpos = 0;
+ tb_long_t n = xm_utf8_len_impl(s, len, (tb_long_t)posi + 1, (tb_long_t)posj + 1, !lax, &errpos);
+ if (n < 0) {
+ lua_pushnil(lua);
+ lua_pushinteger(lua, errpos);
+ return 2;
+ }
+ lua_pushinteger(lua, n);
+ return 1;
+}
diff --git a/core/src/xmake/utf8/offset.c b/core/src/xmake/utf8/offset.c
new file mode 100644
index 000000000..fcd155af7
--- /dev/null
+++ b/core/src/xmake/utf8/offset.c
@@ -0,0 +1,56 @@
+/*!A cross-platform build utility based on Lua
+ *
+ * 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 (C) 2015-present, Xmake Open Source Community.
+ *
+ * @author ruki
+ * @file offset.c
+ *
+ */
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * includes
+ */
+#include "utf8.h"
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * implementation
+ */
+
+/* offset(s, n, [i]) -> index where n-th character counting from
+ * position 'i' starts; 0 means character at 'i'.
+ */
+tb_int_t xm_utf8_offset(lua_State *lua) {
+ tb_assert_and_check_return_val(lua, 0);
+
+ size_t len;
+ tb_char_t const* s = luaL_checklstring(lua, 1, &len);
+ lua_Integer n = luaL_checkinteger(lua, 2);
+ lua_Integer posi = (n >= 0) ? 1 : len + 1;
+ posi = xm_utf8_posrelat((tb_long_t)luaL_optinteger(lua, 3, posi), len);
+
+ tb_long_t result = xm_utf8_offset_impl(s, len, (tb_long_t)n, (tb_long_t)posi);
+ if (result == -1) {
+ return luaL_argerror(lua, 3, "position out of bounds");
+ }
+ if (result == -2) {
+ return luaL_error(lua, "initial position is a continuation byte");
+ }
+ if (result == 0) {
+ lua_pushnil(lua);
+ return 1;
+ }
+ lua_pushinteger(lua, result);
+ return 1;
+}
diff --git a/core/src/xmake/utf8/prefix.h b/core/src/xmake/utf8/prefix.h
new file mode 100644
index 000000000..7a0881248
--- /dev/null
+++ b/core/src/xmake/utf8/prefix.h
@@ -0,0 +1,9 @@
+#ifndef XM_UTF8_PREFIX_H
+#define XM_UTF8_PREFIX_H
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * includes
+ */
+#include "../prefix.h"
+
+#endif
diff --git a/core/src/xmake/utf8/reverse.c b/core/src/xmake/utf8/reverse.c
new file mode 100644
index 000000000..0c0b7aaf7
--- /dev/null
+++ b/core/src/xmake/utf8/reverse.c
@@ -0,0 +1,57 @@
+/*!A cross-platform build utility based on Lua
+ *
+ * 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 (C) 2015-present, Xmake Open Source Community.
+ *
+ * @author ruki
+ * @file reverse.c
+ *
+ */
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * includes
+ */
+#include "utf8.h"
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * implementation
+ */
+
+/* utf8.reverse(s)
+ */
+tb_int_t xm_utf8_reverse(lua_State *lua) {
+ size_t len;
+ tb_char_t const* s = luaL_checklstring(lua, 1, &len);
+ if (len == 0) {
+ lua_pushliteral(lua, "");
+ return 1;
+ }
+
+ // do reverse
+ if (len < 1024) {
+ tb_char_t buf[1024 + 1];
+ xm_utf8_reverse_impl(s, len, buf);
+ lua_pushlstring(lua, buf, len);
+ } else {
+ tb_char_t* buf = (tb_char_t*)tb_malloc_bytes(len + 1);
+ if (buf) {
+ xm_utf8_reverse_impl(s, len, buf);
+ lua_pushlstring(lua, buf, len);
+ tb_free(buf);
+ } else {
+ lua_pushnil(lua);
+ }
+ }
+ return 1;
+}
diff --git a/core/src/xmake/utf8/sub.c b/core/src/xmake/utf8/sub.c
new file mode 100644
index 000000000..e1561f0f7
--- /dev/null
+++ b/core/src/xmake/utf8/sub.c
@@ -0,0 +1,47 @@
+/*!A cross-platform build utility based on Lua
+ *
+ * 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 (C) 2015-present, Xmake Open Source Community.
+ *
+ * @author ruki
+ * @file sub.c
+ *
+ */
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * includes
+ */
+#include "utf8.h"
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * implementation
+ */
+
+/* utf8.sub(s, i [, j])
+ */
+tb_int_t xm_utf8_sub(lua_State *lua) {
+ size_t len;
+ tb_char_t const* s = luaL_checklstring(lua, 1, &len);
+ lua_Integer i = luaL_checkinteger(lua, 2);
+ lua_Integer j = luaL_optinteger(lua, 3, -1);
+
+ tb_size_t sublen = 0;
+ tb_char_t const* sub = xm_utf8_sub_impl(s, len, (tb_long_t)i, (tb_long_t)j, &sublen);
+ if (sub) {
+ lua_pushlstring(lua, sub, sublen);
+ } else {
+ lua_pushliteral(lua, "");
+ }
+ return 1;
+}
diff --git a/core/src/xmake/utf8/utf8.c b/core/src/xmake/utf8/utf8.c
new file mode 100644
index 000000000..4deba3224
--- /dev/null
+++ b/core/src/xmake/utf8/utf8.c
@@ -0,0 +1,512 @@
+/*!A cross-platform build utility based on Lua
+ *
+ * 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 (C) 2015-present, Xmake Open Source Community.
+ *
+ * @author ruki
+ * @file utf8.c
+ *
+ */
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * includes
+ */
+#include "utf8.h"
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * implementation
+ */
+
+tb_char_t const* xm_utf8_decode(tb_char_t const* s, xm_utf8_int_t* val, tb_bool_t strict) {
+ static const xm_utf8_int_t limits[] = {~(xm_utf8_int_t)0, 0x80, 0x800, 0x10000u, 0x200000u, 0x4000000u};
+ tb_uint32_t c = (tb_byte_t)s[0];
+ xm_utf8_int_t res = 0;
+ if (c < 0x80) {
+ res = c;
+ } else {
+ if (xm_utf8_iscont(c)) {
+ return tb_null;
+ }
+ tb_int_t count = 0;
+ for (; c & 0x40; c <<= 1) {
+ tb_uint32_t cc = (tb_byte_t)s[++count];
+ if (!xm_utf8_iscont(cc)) {
+ return tb_null;
+ }
+ res = (res << 6) | (cc & 0x3F);
+ }
+ res |= ((xm_utf8_int_t)(c & 0x7F) << (count * 5));
+ if (count > 5 || res > XM_UTF8_MAXUTF || res < limits[count]) {
+ return tb_null;
+ }
+ s += count;
+ }
+ if (strict) {
+ if (res > XM_UTF8_MAXUNICODE || (0xD800u <= res && res <= 0xDFFFu)) {
+ return tb_null;
+ }
+ }
+ if (val) {
+ *val = res;
+ }
+ return s + 1;
+}
+
+tb_size_t xm_utf8_encode(tb_char_t* s, xm_utf8_int_t val) {
+ tb_assert_and_check_return_val(s, 0);
+
+ if (val < 0x80) {
+ s[0] = (tb_char_t)val;
+ return 1;
+ }
+ if (val < 0x800) {
+ s[0] = (tb_char_t)(0xc0 | ((val >> 6) & 0x1f));
+ s[1] = (tb_char_t)(0x80 | (val & 0x3f));
+ return 2;
+ }
+ if (val < 0x10000) {
+ s[0] = (tb_char_t)(0xe0 | ((val >> 12) & 0x0f));
+ s[1] = (tb_char_t)(0x80 | ((val >> 6) & 0x3f));
+ s[2] = (tb_char_t)(0x80 | (val & 0x3f));
+ return 3;
+ }
+ if (val <= 0x10FFFF) {
+ s[0] = (tb_char_t)(0xf0 | ((val >> 18) & 0x07));
+ s[1] = (tb_char_t)(0x80 | ((val >> 12) & 0x3f));
+ s[2] = (tb_char_t)(0x80 | ((val >> 6) & 0x3f));
+ s[3] = (tb_char_t)(0x80 | (val & 0x3f));
+ return 4;
+ }
+ if (val <= 0x3FFFFFF) {
+ s[0] = (tb_char_t)(0xf8 | ((val >> 24) & 0x03));
+ s[1] = (tb_char_t)(0x80 | ((val >> 18) & 0x3f));
+ s[2] = (tb_char_t)(0x80 | ((val >> 12) & 0x3f));
+ s[3] = (tb_char_t)(0x80 | ((val >> 6) & 0x3f));
+ s[4] = (tb_char_t)(0x80 | (val & 0x3f));
+ return 5;
+ }
+ if (val <= 0x7FFFFFFF) {
+ s[0] = (tb_char_t)(0xfc | ((val >> 30) & 0x01));
+ s[1] = (tb_char_t)(0x80 | ((val >> 24) & 0x3f));
+ s[2] = (tb_char_t)(0x80 | ((val >> 18) & 0x3f));
+ s[3] = (tb_char_t)(0x80 | ((val >> 12) & 0x3f));
+ s[4] = (tb_char_t)(0x80 | ((val >> 6) & 0x3f));
+ s[5] = (tb_char_t)(0x80 | (val & 0x3f));
+ return 6;
+ }
+ return 0;
+}
+
+tb_long_t xm_utf8_charpos(tb_char_t const* s, tb_size_t len, tb_long_t byte_pos) {
+ if (byte_pos <= 0) return 0;
+ if (byte_pos > len + 1) byte_pos = len + 1;
+
+ // adjust byte_pos to the start of the character
+ //
+ // performance:
+ // 0(1) complexity, because utf8 sequence is max 4 bytes
+ while (byte_pos > 1 && xm_utf8_iscont(s[byte_pos - 1])) {
+ byte_pos--;
+ }
+
+ // get character position
+ tb_long_t count = xm_utf8_len_impl(s, len, 1, byte_pos - 1, tb_true, tb_null);
+ return count >= 0? count + 1 : -1;
+}
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * implementation interfaces
+ */
+static struct { xm_utf8_int_t first; xm_utf8_int_t last; } const g_non_spacing[] =
+{
+ {0x0300, 0x036F}, {0x0483, 0x0486}, {0x0488, 0x0489},
+ {0x0591, 0x05BD}, {0x05BF, 0x05BF}, {0x05C1, 0x05C2},
+ {0x05C4, 0x05C5}, {0x05C7, 0x05C7}, {0x0600, 0x0603},
+ {0x0610, 0x0615}, {0x064B, 0x065E}, {0x0670, 0x0670},
+ {0x06D6, 0x06E4}, {0x06E7, 0x06E8}, {0x06EA, 0x06ED},
+ {0x070F, 0x070F}, {0x0711, 0x0711}, {0x0730, 0x074A},
+ {0x07A6, 0x07B0}, {0x07EB, 0x07F3}, {0x0901, 0x0902},
+ {0x093C, 0x093C}, {0x0941, 0x0948}, {0x094D, 0x094D},
+ {0x0951, 0x0954}, {0x0962, 0x0963}, {0x0981, 0x0981},
+ {0x09BC, 0x09BC}, {0x09C1, 0x09C4}, {0x09CD, 0x09CD},
+ {0x09E2, 0x09E3}, {0x0A01, 0x0A02}, {0x0A3C, 0x0A3C},
+ {0x0A41, 0x0A42}, {0x0A47, 0x0A48}, {0x0A4B, 0x0A4D},
+ {0x0A70, 0x0A71}, {0x0A81, 0x0A82}, {0x0ABC, 0x0ABC},
+ {0x0AC1, 0x0AC5}, {0x0AC7, 0x0AC8}, {0x0ACD, 0x0ACD},
+ {0x0AE2, 0x0AE3}, {0x0B01, 0x0B01}, {0x0B3C, 0x0B3C},
+ {0x0B3F, 0x0B3F}, {0x0B41, 0x0B43}, {0x0B4D, 0x0B4D},
+ {0x0B56, 0x0B56}, {0x0B82, 0x0B82}, {0x0BC0, 0x0BC0},
+ {0x0BCD, 0x0BCD}, {0x0C3E, 0x0C40}, {0x0C46, 0x0C48},
+ {0x0C4A, 0x0C4D}, {0x0C55, 0x0C56}, {0x0CBC, 0x0CBC},
+ {0x0CBF, 0x0CBF}, {0x0CC6, 0x0CC6}, {0x0CCC, 0x0CCD},
+ {0x0CE2, 0x0CE3}, {0x0D41, 0x0D43}, {0x0D4D, 0x0D4D},
+ {0x0DCA, 0x0DCA}, {0x0DD2, 0x0DD4}, {0x0DD6, 0x0DD6},
+ {0x0E31, 0x0E31}, {0x0E34, 0x0E3A}, {0x0E47, 0x0E4E},
+ {0x0EB1, 0x0EB1}, {0x0EB4, 0x0EB9}, {0x0EBB, 0x0EBC},
+ {0x0EC8, 0x0ECD}, {0x0F18, 0x0F19}, {0x0F35, 0x0F35},
+ {0x0F37, 0x0F37}, {0x0F39, 0x0F39}, {0x0F71, 0x0F7E},
+ {0x0F80, 0x0F84}, {0x0F86, 0x0F87}, {0x0F90, 0x0F97},
+ {0x0F99, 0x0FBC}, {0x0FC6, 0x0FC6}, {0x102D, 0x1030},
+ {0x1032, 0x1032}, {0x1036, 0x1037}, {0x1039, 0x1039},
+ {0x1058, 0x1059}, {0x1160, 0x11FF}, {0x135F, 0x135F},
+ {0x1712, 0x1714}, {0x1732, 0x1734}, {0x1752, 0x1753},
+ {0x1772, 0x1773}, {0x17B4, 0x17B5}, {0x17B7, 0x17BD},
+ {0x17C6, 0x17C6}, {0x17C9, 0x17D3}, {0x17DD, 0x17DD},
+ {0x180B, 0x180D}, {0x18A9, 0x18A9}, {0x1920, 0x1922},
+ {0x1927, 0x1928}, {0x1932, 0x1932}, {0x1939, 0x193B},
+ {0x1A17, 0x1A18}, {0x1B00, 0x1B03}, {0x1B34, 0x1B34},
+ {0x1B36, 0x1B3A}, {0x1B3C, 0x1B3C}, {0x1B42, 0x1B42},
+ {0x1B6B, 0x1B73}, {0x1DC0, 0x1DCA}, {0x1DFE, 0x1DFF},
+ {0x200B, 0x200F}, {0x202A, 0x202E}, {0x2060, 0x2063},
+ {0x206A, 0x206F}, {0x20D0, 0x20EF}, {0x302A, 0x302F},
+ {0x3099, 0x309A}, {0xA806, 0xA806}, {0xA80B, 0xA80B},
+ {0xA825, 0xA826}, {0xFB1E, 0xFB1E}, {0xFE00, 0xFE0F},
+ {0xFE20, 0xFE23}, {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB},
+ {0x10A01, 0x10A03}, {0x10A05, 0x10A06}, {0x10A0C, 0x10A0F},
+ {0x10A38, 0x10A3A}, {0x10A3F, 0x10A3F}, {0x1D167, 0x1D169},
+ {0x1D173, 0x1D182}, {0x1D185, 0x1D18B}, {0x1D1AA, 0x1D1AD},
+ {0x1D242, 0x1D244}, {0xE0001, 0xE0001}, {0xE0020, 0xE007F},
+ {0xE0100, 0xE01EF}
+};
+
+tb_long_t xm_utf8_charwidth(xm_utf8_int_t val) {
+
+ // test for 8-bit control characters
+ if (val == 0) return 0;
+ if (val < 32 || (val >= 0x7f && val < 0xa0)) {
+ if (val == 0x09) return 4; // TAB
+ if (val == 0x08) return -1; // BS
+ return 0; // other control chars
+ }
+
+ // binary search in table of non-spacing characters
+ tb_long_t min = 0;
+ tb_long_t max = tb_arrayn(g_non_spacing) - 1;
+ if (val >= g_non_spacing[0].first && val <= g_non_spacing[max].last) {
+ while (max >= min) {
+ tb_long_t mid = (min + max) / 2;
+ if (val > g_non_spacing[mid].last) {
+ min = mid + 1;
+ } else if (val < g_non_spacing[mid].first) {
+ max = mid - 1;
+ } else {
+ return 0;
+ }
+ }
+ }
+
+ if (val >= 0x1100 && (val <= 0x115f || // Hangul Jamo init. consonants
+ val == 0x2329 || val == 0x232a ||
+ (val >= 0x2e80 && val <= 0xa4cf &&
+ val != 0x303f) || // CJK ... Yi
+ (val >= 0xac00 && val <= 0xd7a3) || // Hangul Syllables
+ (val >= 0xf900 && val <= 0xfaff) || // CJK Compatibility Ideographs
+ (val >= 0xfe10 && val <= 0xfe19) || // Vertical forms
+ (val >= 0xfe30 && val <= 0xfe6f) || // CJK Compatibility Forms
+ (val >= 0xff00 && val <= 0xff60) || // Fullwidth Forms
+ (val >= 0xffe0 && val <= 0xffe6) ||
+ (val >= 0x20000 && val <= 0x2fffd) ||
+ (val >= 0x30000 && val <= 0x3fffd))) {
+ return 2;
+ }
+
+ return 1;
+}
+
+tb_long_t xm_utf8_strwidth(tb_char_t const* s, tb_size_t len) {
+ tb_assert_and_check_return_val(s, -1);
+
+ tb_long_t width = 0;
+ tb_char_t const* p = s;
+ tb_char_t const* e = s + len;
+ while (p < e) {
+ xm_utf8_int_t val;
+ tb_char_t const* next = xm_utf8_decode(p, &val, tb_true);
+ if (next) {
+ tb_long_t w = xm_utf8_charwidth(val);
+ if (w < 0) return -1;
+ width += w;
+ p = next;
+ } else {
+ p++; // invalid byte, skip
+ }
+ }
+ return width;
+}
+
+tb_long_t xm_utf8_len_impl(tb_char_t const* s, tb_size_t len, tb_long_t posi, tb_long_t posj, tb_bool_t strict, tb_size_t* errpos) {
+ tb_assert_and_check_return_val(s, -1);
+
+ tb_long_t n = 0;
+ while (posi <= posj) {
+ tb_char_t const* s1 = xm_utf8_decode(s + posi - 1, tb_null, strict);
+ if (s1 == tb_null) {
+ if (errpos) {
+ *errpos = posi;
+ }
+ return -1;
+ }
+ posi = s1 - s + 1;
+ n++;
+ }
+ return n;
+}
+
+tb_long_t xm_utf8_offset_impl(tb_char_t const* s, tb_size_t len, tb_long_t n, tb_long_t posi) {
+ tb_assert_and_check_return_val(s, -1);
+
+ // check
+ if (1 > posi || --posi > (tb_long_t)len) {
+ return -1; // error: position out of bounds
+ }
+
+ if (n == 0) {
+ // find beginning of current byte sequence
+ while (posi > 0 && xm_utf8_iscontp(s + posi)) {
+ posi--;
+ }
+ } else {
+ if (xm_utf8_iscontp(s + posi)) {
+ return -2; // error: initial position is a continuation byte
+ }
+
+ if (n < 0) {
+ while (n < 0 && posi > 0) {
+ do {
+ posi--;
+ } while (posi > 0 && xm_utf8_iscontp(s + posi));
+ n++;
+ }
+ } else {
+ n--;
+ while (n > 0 && posi < (tb_long_t)len) {
+ do {
+ posi++;
+ } while (xm_utf8_iscontp(s + posi));
+ n--;
+ }
+ }
+ }
+
+ if (n == 0) {
+ return posi + 1;
+ }
+ return 0; // nil
+}
+
+tb_bool_t xm_utf8_codepoint_impl(tb_char_t const* s, tb_size_t len, tb_long_t posi, tb_long_t posj, tb_bool_t strict, xm_utf8_codepoint_func_t func, tb_cpointer_t udata) {
+ tb_assert_and_check_return_val(s, tb_false);
+
+ if (posi > posj) {
+ return tb_true;
+ }
+
+ tb_char_t const* se = s + posj;
+ for (s += posi - 1; s < se;) {
+ xm_utf8_int_t code;
+ s = xm_utf8_decode(s, &code, strict);
+ if (s == tb_null) {
+ return tb_false;
+ }
+ if (func && !func(code, udata)) {
+ return tb_false;
+ }
+ }
+ return tb_true;
+}
+
+tb_long_t xm_utf8_find_impl(tb_char_t const* s, tb_size_t len, tb_char_t const* sub, tb_size_t sublen, tb_long_t init, tb_long_t* pchar_end) {
+ tb_assert_and_check_return_val(s && sub, 0);
+
+ if (sublen == 0) {
+ if (init > (tb_long_t)len + 1) init = len + 1;
+
+ tb_long_t start_byte = 1;
+ if (init > 0) {
+ start_byte = xm_utf8_offset_impl(s, len, init, 1);
+ } else if (init < 0) {
+ start_byte = xm_utf8_offset_impl(s, len, init, len + 1);
+ }
+ if (start_byte <= 0) start_byte = 1;
+
+ tb_long_t char_pos = 1;
+ if (start_byte > 1) {
+ tb_long_t c = xm_utf8_len_impl(s, len, 1, start_byte - 1, tb_true, tb_null);
+ if (c >= 0) char_pos = c + 1;
+ }
+
+ if (pchar_end) *pchar_end = char_pos - 1;
+ return char_pos;
+ }
+
+ tb_long_t start_byte = 1;
+ if (init > 0) {
+ start_byte = xm_utf8_offset_impl(s, len, init, 1);
+ } else if (init < 0) {
+ start_byte = xm_utf8_offset_impl(s, len, init, len + 1);
+ }
+ if (start_byte <= 0) return 0;
+
+ tb_char_t const* p = tb_strstr(s + start_byte - 1, sub);
+ if (!p) return 0;
+
+ tb_long_t found_byte_start = p - s + 1;
+
+ tb_long_t char_start = 1;
+ if (found_byte_start > 1) {
+ tb_long_t c = xm_utf8_len_impl(s, len, 1, found_byte_start - 1, tb_true, tb_null);
+ if (c < 0) return 0;
+ char_start = c + 1;
+ }
+
+ if (pchar_end) {
+ tb_long_t match_len = xm_utf8_len_impl(s, len, found_byte_start, found_byte_start + sublen - 1, tb_true, tb_null);
+ if (match_len < 0) return 0;
+ *pchar_end = char_start + match_len - 1;
+ }
+
+ return char_start;
+}
+
+tb_long_t xm_utf8_lastof_impl(tb_char_t const* s, tb_size_t len, tb_char_t const* sub, tb_size_t sublen) {
+ tb_assert_and_check_return_val(s && sub, 0);
+ tb_check_return_val(sublen, 0);
+
+ // optimize for single character search
+ if (sublen == 1) {
+ tb_char_t const* p = tb_strrchr(s, sub[0]);
+ return p ? (tb_long_t)(p - s + 1) : 0;
+ }
+
+ tb_char_t const* p = s;
+ tb_char_t const* last = tb_null;
+
+ while (1) {
+ p = tb_strstr(p, sub);
+ if (!p) break;
+ last = p;
+ p += 1;
+ }
+
+ if (last) {
+ return (tb_long_t)(last - s + 1);
+ }
+ return 0;
+}
+
+tb_long_t xm_utf8_byte_impl(tb_char_t const* s, tb_size_t len, tb_long_t i, tb_long_t j, xm_utf8_codepoint_func_t func, tb_cpointer_t udata) {
+ tb_size_t sublen = 0;
+ tb_char_t const* sub = xm_utf8_sub_impl(s, len, i, j, &sublen);
+ if (sub && sublen > 0) {
+
+ // decode and push codepoints
+ tb_long_t n = 0;
+ tb_char_t const* p = sub;
+ tb_char_t const* e = sub + sublen;
+ while (p < e) {
+ xm_utf8_int_t val;
+ tb_char_t const* next = xm_utf8_decode(p, &val, tb_true);
+ if (next) {
+ if (func && !func(val, udata)) break;
+ n++;
+ p = next;
+ } else {
+ p++;
+ }
+ }
+ return n;
+ }
+ return 0;
+}
+
+tb_char_t const* xm_utf8_sub_impl(tb_char_t const* s, tb_size_t len, tb_long_t i, tb_long_t j, tb_size_t* psublen) {
+ tb_assert_and_check_return_val(s && psublen, tb_null);
+ *psublen = 0;
+
+ // map i (char index) to byte offset
+ tb_long_t start_byte = 0;
+ if (i > 0) {
+ start_byte = xm_utf8_offset_impl(s, len, i, 1);
+ } else if (i < 0) {
+ start_byte = xm_utf8_offset_impl(s, len, i, len + 1);
+ } else {
+ start_byte = 1;
+ }
+
+ if (start_byte == -1) {
+ if (i > 0) {
+ return "";
+ } else {
+ start_byte = 1;
+ }
+ } else if (start_byte == 0) {
+ if (i < 0) {
+ start_byte = 1;
+ } else {
+ return "";
+ }
+ }
+
+ // map j (char index) to byte offset (end)
+ tb_long_t end_byte = 0;
+ if (j >= 0) {
+ end_byte = xm_utf8_offset_impl(s, len, j + 1, 1);
+ } else {
+ end_byte = xm_utf8_offset_impl(s, len, j + 1, len + 1);
+ }
+
+ if (end_byte == -1) {
+ if (j >= 0) end_byte = len + 1;
+ else end_byte = 1;
+ } else if (end_byte == 0) {
+ if (j >= 0) end_byte = len + 1;
+ else end_byte = 1;
+ }
+
+ if (end_byte <= start_byte) {
+ return "";
+ }
+
+ *psublen = end_byte - start_byte;
+ return s + start_byte - 1;
+}
+
+tb_char_t* xm_utf8_reverse_impl(tb_char_t const* s, tb_size_t len, tb_char_t* buf) {
+ tb_assert_and_check_return_val(s && len && buf, tb_null);
+
+ tb_char_t const* p = s;
+ tb_char_t const* e = s + len;
+ tb_char_t* d = buf + len;
+
+ while (p < e) {
+ xm_utf8_int_t code;
+ tb_char_t const* next = xm_utf8_decode(p, &code, tb_false);
+
+ // invalid utf8? treat as 1 byte
+ tb_size_t n = 1;
+ if (next) {
+ n = next - p;
+ }
+
+ // safety check
+ if (p + n > e) n = e - p;
+
+ d -= n;
+ tb_memcpy(d, p, n);
+ p += n;
+ }
+ buf[len] = '\0';
+ return buf;
+}
+
diff --git a/core/src/xmake/utf8/utf8.h b/core/src/xmake/utf8/utf8.h
new file mode 100644
index 000000000..fe83e8e86
--- /dev/null
+++ b/core/src/xmake/utf8/utf8.h
@@ -0,0 +1,60 @@
+#ifndef XM_UTF8_UTF8_H
+#define XM_UTF8_UTF8_H
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * includes
+ */
+#include "prefix.h"
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * macros
+ */
+#define xm_utf8_iscont(c) (((c) & 0xC0) == 0x80)
+#define xm_utf8_iscontp(p) xm_utf8_iscont(*(p))
+
+#define XM_UTF8_MAXUNICODE 0x10FFFFu
+#define XM_UTF8_MAXUTF 0x7FFFFFFFu
+#define XM_UTF8_MSGInvalid "invalid UTF-8 code"
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * types
+ */
+typedef tb_uint32_t xm_utf8_int_t;
+typedef tb_bool_t (*xm_utf8_codepoint_func_t)(xm_utf8_int_t code, tb_cpointer_t udata);
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * inline interfaces
+ */
+static __tb_inline__ tb_long_t xm_utf8_posrelat(tb_long_t pos, tb_size_t len) {
+ if (pos >= 0) {
+ return pos;
+ } else if (0u - (tb_size_t)pos > len) {
+ return 0;
+ } else {
+ return (tb_long_t)len + pos + 1;
+ }
+}
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * interfaces
+ */
+
+tb_char_t const* xm_utf8_decode(tb_char_t const* s, xm_utf8_int_t* val, tb_bool_t strict);
+tb_size_t xm_utf8_encode(tb_char_t* s, xm_utf8_int_t val);
+tb_long_t xm_utf8_charpos(tb_char_t const* s, tb_size_t len, tb_long_t byte_pos);
+tb_long_t xm_utf8_charwidth(xm_utf8_int_t val);
+tb_long_t xm_utf8_strwidth(tb_char_t const* s, tb_size_t len);
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * implementation interfaces
+ */
+tb_long_t xm_utf8_len_impl(tb_char_t const* s, tb_size_t len, tb_long_t posi, tb_long_t posj, tb_bool_t strict, tb_size_t* errpos);
+tb_long_t xm_utf8_offset_impl(tb_char_t const* s, tb_size_t len, tb_long_t n, tb_long_t posi);
+tb_bool_t xm_utf8_codepoint_impl(tb_char_t const* s, tb_size_t len, tb_long_t posi, tb_long_t posj, tb_bool_t strict, xm_utf8_codepoint_func_t func, tb_cpointer_t udata);
+tb_long_t xm_utf8_find_impl(tb_char_t const* s, tb_size_t len, tb_char_t const* sub, tb_size_t sublen, tb_long_t init, tb_long_t* pchar_end);
+tb_long_t xm_utf8_lastof_impl(tb_char_t const* s, tb_size_t len, tb_char_t const* sub, tb_size_t sublen);
+tb_long_t xm_utf8_byte_impl(tb_char_t const* s, tb_size_t len, tb_long_t i, tb_long_t j, xm_utf8_codepoint_func_t func, tb_cpointer_t udata);
+tb_char_t const* xm_utf8_sub_impl(tb_char_t const* s, tb_size_t len, tb_long_t i, tb_long_t j, tb_size_t* psublen);
+tb_char_t* xm_utf8_reverse_impl(tb_char_t const* s, tb_size_t len, tb_char_t* buf);
+
+#endif
diff --git a/core/src/xmake/utf8/width.c b/core/src/xmake/utf8/width.c
new file mode 100644
index 000000000..615572462
--- /dev/null
+++ b/core/src/xmake/utf8/width.c
@@ -0,0 +1,45 @@
+/*!A cross-platform build utility based on Lua
+ *
+ * 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 (C) 2015-present, Xmake Open Source Community.
+ *
+ * @author ruki
+ * @file width.c
+ *
+ */
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * includes
+ */
+#include "utf8.h"
+
+/* //////////////////////////////////////////////////////////////////////////////////////
+ * interfaces
+ */
+
+/* utf8.width(char)
+ * utf8.width(str)
+ * utf8.width(codepoint)
+ */
+tb_int_t xm_utf8_width(lua_State* lua) {
+ if (lua_isnumber(lua, 1)) {
+ xm_utf8_int_t val = (xm_utf8_int_t)lua_tointeger(lua, 1);
+ lua_pushinteger(lua, xm_utf8_charwidth(val));
+ } else {
+ size_t len = 0;
+ tb_char_t const* s = luaL_checklstring(lua, 1, &len);
+ lua_pushinteger(lua, xm_utf8_strwidth(s, len));
+ }
+ return 1;
+}
diff --git a/core/src/xmake/xmake.sh b/core/src/xmake/xmake.sh
index 291001306..dfe533aa3 100755
--- a/core/src/xmake/xmake.sh
+++ b/core/src/xmake/xmake.sh
@@ -70,6 +70,7 @@ target "xmake"
add_files "sandbox/*.c"
add_files "semver/*.c"
add_files "string/*.c"
+ add_files "utf8/*.c"
add_files "utils/*.c"
add_files "tty/*.c"
add_files "binutils/*.c"
diff --git a/tests/modules/path/test.lua b/tests/modules/path/test.lua
index f68a57e5f..57f7d06bc 100644
--- a/tests/modules/path/test.lua
+++ b/tests/modules/path/test.lua
@@ -34,6 +34,23 @@ function test_extension(t)
t:are_equal(path.extension("\\home\\foo.so"), ".so")
end
+function test_filename(t)
+ t:are_equal(path.filename("foo"), "foo")
+ t:are_equal(path.filename("foo.so"), "foo.so")
+ t:are_equal(path.filename("/tmp/foo.so"), "foo.so")
+ t:are_equal(path.filename("c:\\tmp\\foo.so"), "foo.so")
+ t:are_equal(path.filename("/tmp/.."), "..")
+ t:are_equal(path.filename("/tmp/."), ".")
+ t:are_equal(path.filename("/"), "")
+ t:are_equal(path.filename(""), "")
+
+ -- unicode
+ t:are_equal(path.filename("Unicode 测试/test.lua"), "test.lua")
+ t:are_equal(path.filename("Unicode 测试/foo/test.lua"), "test.lua")
+ t:are_equal(path.filename("测试/test.lua"), "test.lua")
+ t:are_equal(path.filename("测试\\test.lua"), "test.lua")
+end
+
function test_directory(t)
t:are_equal(path.directory(""), nil)
t:are_equal(path.directory("."), nil)
diff --git a/tests/modules/string/test.lua b/tests/modules/string/test.lua
index 94b3c1a04..1ec4b52a1 100644
--- a/tests/modules/string/test.lua
+++ b/tests/modules/string/test.lua
@@ -94,6 +94,13 @@ function test_lastof(t)
t:are_equal(("/home/file.txt"):lastof('/', true), 6)
t:are_equal(("/home/file.txt"):lastof('/home', true), 1)
t:are_equal(("/home/file.txt"):lastof('[/\\]home'), 1)
+
+ -- long string
+ local longstr = ("a"):rep(1000) .. "b"
+ t:are_equal(longstr:lastof("b"), 1001)
+ t:are_equal(longstr:lastof("a"), 1000)
+ t:are_equal(longstr:lastof("b", true), 1001)
+ t:are_equal(longstr:lastof("a", true), 1000)
end
function test_replace(t)
diff --git a/tests/modules/utf8/test.lua b/tests/modules/utf8/test.lua
new file mode 100644
index 000000000..e1fced3a0
--- /dev/null
+++ b/tests/modules/utf8/test.lua
@@ -0,0 +1,197 @@
+
+function test_len(t)
+ t:are_equal(utf8.len("A"), 1)
+ t:are_equal(utf8.len("¢"), 1)
+ t:are_equal(utf8.len("€"), 1)
+ t:are_equal(utf8.len("𐍈"), 1)
+ t:are_equal(utf8.len("ab"), 2)
+ t:are_equal(utf8.len("A€B"), 3)
+ t:are_equal(utf8.len("你好"), 2)
+end
+
+function test_char(t)
+ t:are_equal(utf8.char(65), "A")
+ t:are_equal(utf8.char(0x20AC), "€")
+ t:are_equal(utf8.char(65, 66, 67), "ABC")
+end
+
+function test_codepoint(t)
+ t:are_equal(utf8.codepoint("A"), 65)
+ t:are_equal(utf8.codepoint("€"), 0x20AC)
+ local c1, c2, c3 = utf8.codepoint("ABC", 1, 3)
+ t:are_equal(c1, 65)
+ t:are_equal(c2, 66)
+ t:are_equal(c3, 67)
+
+ -- test range
+ t:are_equal(utf8.codepoint("ABC", 2), 66)
+ t:are_equal(utf8.codepoint("ABC", 2, 2), 66)
+end
+
+function test_offset(t)
+ t:are_equal(utf8.offset("ABC", 1), 1)
+ t:are_equal(utf8.offset("ABC", 2), 2)
+ t:are_equal(utf8.offset("ABC", 4), 4)
+ t:are_equal(utf8.offset("ABC", 5), nil)
+
+ -- "€" is 3 bytes (0xE2 0x82 0xAC)
+ t:are_equal(utf8.offset("€BC", 1), 1)
+ t:are_equal(utf8.offset("€BC", 2), 4)
+ t:are_equal(utf8.offset("€BC", 3), 5)
+
+ t:are_equal(utf8.offset("你好", 1), 1)
+ t:are_equal(utf8.offset("你好", 2), 4)
+ t:are_equal(utf8.offset("你好", 3), 7)
+end
+
+function test_codes(t)
+ local s = "A€"
+ local codes = {}
+ for p, c in utf8.codes(s) do
+ table.insert(codes, {p, c})
+ end
+ t:are_equal(#codes, 2)
+ t:are_equal(codes[1][1], 1)
+ t:are_equal(codes[1][2], 65)
+ -- "€" starts at 2? No, byte offset.
+ -- "A" is 1 byte. "€" starts at 2.
+ t:are_equal(codes[2][1], 2)
+ t:are_equal(codes[2][2], 0x20AC)
+end
+
+function test_charpattern(t)
+ t:require(utf8.charpattern)
+end
+
+function test_sub(t)
+ t:are_equal(utf8.sub("ABC", 1, 1), "A")
+ t:are_equal(utf8.sub("ABC", 2, 2), "B")
+ t:are_equal(utf8.sub("ABC", 1, 2), "AB")
+ t:are_equal(utf8.sub("你好", 1, 1), "你")
+ t:are_equal(utf8.sub("你好", 2, 2), "好")
+ t:are_equal(utf8.sub("你好", 1, 2), "你好")
+
+ -- mixed
+ t:are_equal(utf8.sub("A你好B", 2, 3), "你好")
+ t:are_equal(utf8.sub("A你好B", 1, 3), "A你好")
+ t:are_equal(utf8.sub("A你好B", 2, 4), "你好B")
+
+ -- negative
+ t:are_equal(utf8.sub("ABC", -1), "C")
+ t:are_equal(utf8.sub("ABC", -2), "BC")
+ t:are_equal(utf8.sub("你好", -1), "好")
+ t:are_equal(utf8.sub("你好", -2), "你好")
+ t:are_equal(utf8.sub("你好", 1, -1), "你好")
+ t:are_equal(utf8.sub("你好", 1, -2), "你")
+
+ -- out of bounds
+ t:are_equal(utf8.sub("ABC", 4), "")
+ t:are_equal(utf8.sub("ABC", 1, 5), "ABC")
+ t:are_equal(utf8.sub("ABC", 0), "ABC")
+ t:are_equal(utf8.sub("ABC", -10), "ABC")
+end
+
+function test_lastof(t)
+ t:are_equal(utf8.lastof("ABC", "A"), 1)
+ t:are_equal(utf8.lastof("ABC", "B"), 2)
+ t:are_equal(utf8.lastof("ABC", "C"), 3)
+ t:are_equal(utf8.lastof("ABCA", "A"), 4)
+
+ t:are_equal(utf8.lastof("你好", "你"), 1)
+ t:are_equal(utf8.lastof("你好", "好"), 2)
+ t:are_equal(utf8.lastof("你好你", "你"), 3)
+
+ t:are_equal(utf8.lastof("A你好A", "A"), 4)
+ t:are_equal(utf8.lastof("A你好A", "好"), 3)
+
+ t:are_equal(utf8.lastof("ABC", "D"), nil)
+
+ -- plain
+ t:are_equal(utf8.lastof("ABC", "A", true), 1)
+ t:are_equal(utf8.lastof("ABC", "B", true), 2)
+ t:are_equal(utf8.lastof("ABC", ".", true), nil)
+ t:are_equal(utf8.lastof("你好", "好", true), 2)
+ t:are_equal(utf8.lastof("C你好D", "好", true), 3)
+ t:are_equal(utf8.lastof("C你好D", "D", true), 4)
+
+ -- long string
+ local longstr = ("你"):rep(1000) .. "好"
+ t:are_equal(utf8.lastof(longstr, "好"), 1001)
+ t:are_equal(utf8.lastof(longstr, "你"), 1000)
+ t:are_equal(utf8.lastof(longstr, "好", true), 1001)
+ t:are_equal(utf8.lastof(longstr, "你", true), 1000)
+
+ -- pattern
+ t:are_equal(utf8.lastof("ABC", "."), 3)
+ t:are_equal(utf8.lastof("你好", "."), 2)
+end
+
+function test_find(t)
+ -- plain
+ t:are_equal({utf8.find("你好", "你", 1, true)}, {1, 1})
+ t:are_equal({utf8.find("你好你", "你", 2, true)}, {3, 3})
+ t:are_equal({utf8.find("A你好A", "A", 2, true)}, {4, 4})
+ t:are_equal(utf8.find("ABC", "D", 1, true), nil)
+ t:are_equal({utf8.find("ABC", "", 1, true)}, {1, 0})
+
+ -- pattern matching (default)
+ t:are_equal({utf8.find("ABC", "B")}, {2, 2})
+ t:are_equal({utf8.find("ABC", "([BC])")}, {2, 2, "B"}) -- Capture
+ t:are_equal({utf8.find("ABC", "(.)(.)")}, {1, 2, "A", "B"})
+
+ -- UTF-8 pattern matching (byte-based)
+ -- "你" is 3 bytes. "." matches first byte.
+ t:are_equal({utf8.find("你好", ".")}, {1, 1})
+
+ -- "你好", "好" -> bytes 4-6.
+ t:are_equal({utf8.find("你好", "好")}, {2, 2})
+
+ -- "你好", "..." (3 dots) -> matches 3 bytes (whole "你").
+ t:are_equal({utf8.find("你好", "...")}, {1, 1})
+
+ -- "A你好", "%w" -> matches "A".
+ t:are_equal({utf8.find("A你好", "%w")}, {1, 1})
+end
+
+function test_width(t)
+ -- char/codepoint width
+ t:are_equal(utf8.width(string.byte("A")), 1)
+ t:are_equal(utf8.width(utf8.codepoint("€")), 1)
+ t:are_equal(utf8.width(utf8.codepoint("你")), 2)
+ t:are_equal(utf8.width(0), 0)
+ t:are_equal(utf8.width(0x09), 4) -- TAB
+
+ -- string width
+ t:are_equal(utf8.width("A"), 1)
+ t:are_equal(utf8.width("ABC"), 3)
+ t:are_equal(utf8.width("你好"), 4)
+ t:are_equal(utf8.width("A你好"), 5)
+ t:are_equal(utf8.width("A\tB"), 6) -- 1 + 4 + 1
+end
+
+function test_byte(t)
+ t:are_equal({utf8.byte("A")}, {65})
+ t:are_equal({utf8.byte("€")}, {0x20AC})
+ t:are_equal({utf8.byte("ABC")}, {65})
+ t:are_equal({utf8.byte("ABC", 2)}, {66})
+ t:are_equal({utf8.byte("ABC", 2, 2)}, {66})
+ t:are_equal({utf8.byte("ABC", 1, 3)}, {65, 66, 67})
+ t:are_equal({utf8.byte("你好")}, {utf8.codepoint("你好", 1, 1)})
+ t:are_equal({utf8.byte("你好", 1, 2)}, {20320, 22909})
+
+ -- negative indices
+ t:are_equal({utf8.byte("ABC", -1)}, {67})
+ t:are_equal({utf8.byte("ABC", -2)}, {66})
+ t:are_equal({utf8.byte("ABC", -2, -1)}, {66, 67})
+ t:are_equal({utf8.byte("ABC", 1, -1)}, {65, 66, 67})
+
+ -- out of bounds
+ t:are_equal({utf8.byte("ABC", 4)}, {})
+ t:are_equal({utf8.byte("ABC", 1, 0)}, {})
+end
+
+function test_reverse(t)
+ t:are_equal(utf8.reverse("hello"), "olleh")
+ t:are_equal(utf8.reverse("你好"), "好你")
+ t:are_equal(utf8.reverse("hello 你好"), "好你 olleh")
+end
diff --git a/tests/runner.lua b/tests/runner.lua
index 5d154674d..f60bf37d8 100644
--- a/tests/runner.lua
+++ b/tests/runner.lua
@@ -8,7 +8,7 @@ function main(script, opt)
script = path.join(script, "test.lua")
end
script = path.absolute(script)
- assert(path.filename(script) == "test.lua", "file should named `test.lua`")
+ assert(path.filename(script) == "test.lua", "file(%s) should named `test.lua`", script)
assert(os.isfile(script), "should be a file")
-- disable statistics
diff --git a/xmake/core/base/string.lua b/xmake/core/base/string.lua
index 2c7069cb9..21166cde7 100644
--- a/xmake/core/base/string.lua
+++ b/xmake/core/base/string.lua
@@ -131,16 +131,6 @@ function string:rtrim(trimchars)
return string._trim(self, trimchars, 1)
end
--- encode: ' ', '=', '\"', '<'
-function string:encode()
- return (self:gsub("[%s=\"<]", function (w) return string.format("%%%x", w:byte()) end))
-end
-
--- decode: ' ', '=', '\"'
-function string:decode()
- return (self:gsub("%%(%x%x)", function (w) return string.char(tonumber(w, 16)) end))
-end
-
-- replace text
function string:replace(old, new, opt)
if opt and opt.plain then
@@ -259,129 +249,6 @@ function string:deserialize()
return serialize.load(self)
end
--- unicode character width in the given index
-function string:wcwidth(idx)
-
- -- based on Markus Kuhn's implementation of wcswidth()
- -- https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c
- local non_spacing =
- {
- {0x0300, 0x036F}, {0x0483, 0x0486}, {0x0488, 0x0489},
- {0x0591, 0x05BD}, {0x05BF, 0x05BF}, {0x05C1, 0x05C2},
- {0x05C4, 0x05C5}, {0x05C7, 0x05C7}, {0x0600, 0x0603},
- {0x0610, 0x0615}, {0x064B, 0x065E}, {0x0670, 0x0670},
- {0x06D6, 0x06E4}, {0x06E7, 0x06E8}, {0x06EA, 0x06ED},
- {0x070F, 0x070F}, {0x0711, 0x0711}, {0x0730, 0x074A},
- {0x07A6, 0x07B0}, {0x07EB, 0x07F3}, {0x0901, 0x0902},
- {0x093C, 0x093C}, {0x0941, 0x0948}, {0x094D, 0x094D},
- {0x0951, 0x0954}, {0x0962, 0x0963}, {0x0981, 0x0981},
- {0x09BC, 0x09BC}, {0x09C1, 0x09C4}, {0x09CD, 0x09CD},
- {0x09E2, 0x09E3}, {0x0A01, 0x0A02}, {0x0A3C, 0x0A3C},
- {0x0A41, 0x0A42}, {0x0A47, 0x0A48}, {0x0A4B, 0x0A4D},
- {0x0A70, 0x0A71}, {0x0A81, 0x0A82}, {0x0ABC, 0x0ABC},
- {0x0AC1, 0x0AC5}, {0x0AC7, 0x0AC8}, {0x0ACD, 0x0ACD},
- {0x0AE2, 0x0AE3}, {0x0B01, 0x0B01}, {0x0B3C, 0x0B3C},
- {0x0B3F, 0x0B3F}, {0x0B41, 0x0B43}, {0x0B4D, 0x0B4D},
- {0x0B56, 0x0B56}, {0x0B82, 0x0B82}, {0x0BC0, 0x0BC0},
- {0x0BCD, 0x0BCD}, {0x0C3E, 0x0C40}, {0x0C46, 0x0C48},
- {0x0C4A, 0x0C4D}, {0x0C55, 0x0C56}, {0x0CBC, 0x0CBC},
- {0x0CBF, 0x0CBF}, {0x0CC6, 0x0CC6}, {0x0CCC, 0x0CCD},
- {0x0CE2, 0x0CE3}, {0x0D41, 0x0D43}, {0x0D4D, 0x0D4D},
- {0x0DCA, 0x0DCA}, {0x0DD2, 0x0DD4}, {0x0DD6, 0x0DD6},
- {0x0E31, 0x0E31}, {0x0E34, 0x0E3A}, {0x0E47, 0x0E4E},
- {0x0EB1, 0x0EB1}, {0x0EB4, 0x0EB9}, {0x0EBB, 0x0EBC},
- {0x0EC8, 0x0ECD}, {0x0F18, 0x0F19}, {0x0F35, 0x0F35},
- {0x0F37, 0x0F37}, {0x0F39, 0x0F39}, {0x0F71, 0x0F7E},
- {0x0F80, 0x0F84}, {0x0F86, 0x0F87}, {0x0F90, 0x0F97},
- {0x0F99, 0x0FBC}, {0x0FC6, 0x0FC6}, {0x102D, 0x1030},
- {0x1032, 0x1032}, {0x1036, 0x1037}, {0x1039, 0x1039},
- {0x1058, 0x1059}, {0x1160, 0x11FF}, {0x135F, 0x135F},
- {0x1712, 0x1714}, {0x1732, 0x1734}, {0x1752, 0x1753},
- {0x1772, 0x1773}, {0x17B4, 0x17B5}, {0x17B7, 0x17BD},
- {0x17C6, 0x17C6}, {0x17C9, 0x17D3}, {0x17DD, 0x17DD},
- {0x180B, 0x180D}, {0x18A9, 0x18A9}, {0x1920, 0x1922},
- {0x1927, 0x1928}, {0x1932, 0x1932}, {0x1939, 0x193B},
- {0x1A17, 0x1A18}, {0x1B00, 0x1B03}, {0x1B34, 0x1B34},
- {0x1B36, 0x1B3A}, {0x1B3C, 0x1B3C}, {0x1B42, 0x1B42},
- {0x1B6B, 0x1B73}, {0x1DC0, 0x1DCA}, {0x1DFE, 0x1DFF},
- {0x200B, 0x200F}, {0x202A, 0x202E}, {0x2060, 0x2063},
- {0x206A, 0x206F}, {0x20D0, 0x20EF}, {0x302A, 0x302F},
- {0x3099, 0x309A}, {0xA806, 0xA806}, {0xA80B, 0xA80B},
- {0xA825, 0xA826}, {0xFB1E, 0xFB1E}, {0xFE00, 0xFE0F},
- {0xFE20, 0xFE23}, {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB},
- {0x10A01, 0x10A03}, {0x10A05, 0x10A06}, {0x10A0C, 0x10A0F},
- {0x10A38, 0x10A3A}, {0x10A3F, 0x10A3F}, {0x1D167, 0x1D169},
- {0x1D173, 0x1D182}, {0x1D185, 0x1D18B}, {0x1D1AA, 0x1D1AD},
- {0x1D242, 0x1D244}, {0xE0001, 0xE0001}, {0xE0020, 0xE007F},
- {0xE0100, 0xE01EF},
- }
-
- idx = idx or 1
-
- -- turn codepoint into unicode
- local c = self:byte(idx)
- local seq = c < 0x80 and 1 or c < 0xE0 and 2 or c < 0xF0 and 3 or
- c < 0xF8 and 4 or error("invalid UTF-8 sequence")
- local val = seq == 1 and c or bit.band(c, (2^(8 - seq) - 1))
-
- for aux = 2, seq do
- c = self:byte(idx + aux - 1)
- val = val * 2 ^ 6 + bit.band(c, 0x3F)
- end
-
- -- test for 8-bit control characters
- if val == 0 then return 0 end
-
- if val < 32 or (val >= 0x7f and val < 0xa0) then
- return -1
- end
-
- -- binary search in table of non-spacing characters
- local min, max = 1, #non_spacing
- if val >= non_spacing[1][1] and val <= non_spacing[max][2] then
- while max >= min do
- local mid = math.floor((min + max) / 2)
- if val > non_spacing[mid][2] then
- min = mid + 1
- elseif val < non_spacing[mid][1] then
- max = mid - 1
- else
- return 0
- end
- end
- end
-
- if val >= 0x1100 and (val <= 0x115f or -- Hangul Jamo init. consonants
- val == 0x2329 or val == 0x232a or
- (val >= 0x2e80 and val <= 0xa4cf and
- val ~= 0x303f) or -- CJK ... Yi
- (val >= 0xac00 and val <= 0xd7a3) or -- Hangul Syllables
- (val >= 0xf900 and val <= 0xfaff) or -- CJK Compatibility Ideographs
- (val >= 0xfe10 and val <= 0xfe19) or -- Vertical forms
- (val >= 0xfe30 and val <= 0xfe6f) or -- CJK Compatibility Forms
- (val >= 0xff00 and val <= 0xff60) or -- Fullwidth Forms
- (val >= 0xffe0 and val <= 0xffe6) or
- (val >= 0x20000 and val <= 0x2fffd) or
- (val >= 0x30000 and val <= 0x3fffd)) then
- return 2
- end
-
- return 1
-end
-
--- unicode string width in given start index
-function string:wcswidth(idx)
- local width = 0
- idx = idx or 1
- while idx <= #self do
- if bit.band(self:byte(idx), 0xc0) ~= 0x80 then
- width = width + self:wcwidth(idx)
- end
- idx = idx + 1
- end
- return width
-end
-
-- compute the Levenshtein distance between two strings
--
-- @param str2 the string to compare against
@@ -425,5 +292,44 @@ function string:levenshtein(str2, opt)
return row1[len2 + 1]
end
+-- encode: ' ', '=', '\"', '<' (deprecated)
+function string:encode()
+ deprecated.add(nil, "string:encode()")
+ return (self:gsub("[%s=\"<]", function (w) return string.format("%%%x", w:byte()) end))
+end
+
+-- decode: ' ', '=', '\"' (deprecated)
+function string:decode()
+ deprecated.add(nil, "string:decode()")
+ return (self:gsub("%%(%x%x)", function (w) return string.char(tonumber(w, 16)) end))
+end
+
+-- unicode character width in the given index (deprecated)
+function string:wcwidth(idx)
+
+ -- deprecated
+ deprecated.add("utf8.width(char)", "string:wcwidth(idx)")
+
+ -- get codepoint and width
+ local utf8 = utf8 or require("base/utf8")
+ local code = utf8.codepoint(self, idx)
+ return utf8.width(code)
+end
+
+-- unicode string width in given start index (deprecated)
+function string:wcswidth(idx)
+
+ -- deprecated
+ deprecated.add("utf8.width(str)", "string:wcswidth(idx)")
+
+ -- get width
+ local utf8 = utf8 or require("base/utf8")
+ if idx and idx > 1 then
+ return utf8.width(self:sub(idx))
+ else
+ return utf8.width(self)
+ end
+end
+
-- return module: string
return string
diff --git a/xmake/core/base/utf8.lua b/xmake/core/base/utf8.lua
new file mode 100644
index 000000000..67b52e7ec
--- /dev/null
+++ b/xmake/core/base/utf8.lua
@@ -0,0 +1,46 @@
+--!A cross-platform build utility based on Lua
+--
+-- 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 (C) 2015-present, Xmake Open Source Community.
+--
+-- @author ruki
+-- @file utf8.lua
+
+-- define module: utf8
+local utf8 = utf8 or {}
+
+-- @desc The utf8 module
+-- It provides basic support for UTF-8 encoding.
+-- It is compatible with Lua 5.3+ utf8 library.
+--
+-- @interface utf8.len(s [, i [, j [, lax]]])
+-- @interface utf8.char(...)
+-- @interface utf8.codepoint(s [, i [, j]])
+-- @interface utf8.offset(s, n [, i])
+-- @interface utf8.codes(s [, lax])
+-- @interface utf8.sub(s, i [, j])
+-- @interface utf8.reverse(s)
+-- @interface utf8.lastof(s, pattern [, plain])
+-- @interface utf8.find(s, pattern [, init [, plain]])
+-- @interface utf8.width(s)
+-- @interface utf8.byte(s [, i [, j]])
+--
+
+-- the char pattern
+if not utf8.charpattern then
+ utf8.charpattern = "[\0-\x7F\xC2-\xFD][\x80-\xBF]*"
+end
+
+-- return module: utf8
+return utf8
diff --git a/xmake/core/sandbox/modules/interpreter/utf8.lua b/xmake/core/sandbox/modules/interpreter/utf8.lua
new file mode 100644
index 000000000..0b8ef72a4
--- /dev/null
+++ b/xmake/core/sandbox/modules/interpreter/utf8.lua
@@ -0,0 +1,22 @@
+--!A cross-platform build utility based on Lua
+--
+-- 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 (C) 2015-present, Xmake Open Source Community.
+--
+-- @author ruki
+-- @file utf8.lua
+--
+
+-- load module
+return require("sandbox/modules/utf8")
diff --git a/xmake/core/sandbox/modules/utf8.lua b/xmake/core/sandbox/modules/utf8.lua
new file mode 100644
index 000000000..a8450dc87
--- /dev/null
+++ b/xmake/core/sandbox/modules/utf8.lua
@@ -0,0 +1,21 @@
+--!A cross-platform build utility based on Lua
+--
+-- 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 (C) 2015-present, Xmake Open Source Community.
+--
+-- @author ruki
+-- @file utf8.lua
+--
+
+return require("base/utf8")
diff --git a/xmake/core/ui/label.lua b/xmake/core/ui/label.lua
index 33a35b16f..45330c80c 100644
--- a/xmake/core/ui/label.lua
+++ b/xmake/core/ui/label.lua
@@ -129,17 +129,20 @@ function label:splitext(text, width)
local line = lines[idx]
while #line > width do
local size = 0
- for i = 1, #line do
- if bit.band(line:byte(i), 0xc0) ~= 0x80 then
- size = size + line:wcwidth(i)
- if size > width then
- table.insert(result, line:sub(1, i - 1))
- line = line:sub(i)
- break
- end
+ local split_idx = 0
+ for p, c in utf8.codes(line) do
+ local w = utf8.width(c)
+ size = size + w
+ if size > width then
+ split_idx = p
+ break
end
end
- if size <= width then
+
+ if split_idx > 0 then
+ table.insert(result, line:sub(1, split_idx - 1))
+ line = line:sub(split_idx)
+ else
break
end
end