-- Copyright 2025 David Vazgenovich Shakaryan local mp_utils = require('mp.utils') local util = {} function util.copy_table(t) local u = {} for k, v in pairs(t) do u[k] = v end return u end function util.reverse(t) for i = 1, #t/2 do t[i], t[#t-i+1] = t[#t-i+1], t[i] end end function util.strip(str) return (str:gsub('^%s*(.-)%s*$', '%1')) end function util.strip_ne(str) if str == nil then return nil end local str = util.strip(tostring(str)) return str ~= '' and str or nil end -- skips over utf8 continuation bytes (10xxxxxx) -- valid positions range from 1 to #str + 1 (*after* the last byte) function util.utf8_seek(str, pos, n) local step = n > 0 and 1 or -1 local test = n > 0 and function() return pos > #str end or function() return pos <= 1 end while n ~= 0 and not test() do repeat pos = pos + step until test() or bit.band(str:byte(pos), 0xc0) ~= 0x80 n = n - step end return pos end -- wraps string into a table of strings. spaces are not removed, resulting in -- width-1 visible chars; newlines and end of string are handled similarly for -- consistency. words longer than width are not broken. optional cont_width can -- be specified to use a different width for continuation lines. function util.wrap(str, width, cont_width) local t = {} local start, stop = 0, 0 while stop < #str do local i = str:find('[ \n]', stop + 1) or #str + 1 if i - start >= width then t[#t+1] = str:sub(start, stop) start = stop + 1 if cont_width then width = cont_width end end stop = i if str:byte(stop) == 10 or stop >= #str then t[#t+1] = str:sub(start, stop - 1) .. ' ' start = stop + 1 end end return t end function util.read_json_file(path) local f = io.open(path, 'r') if not f then return {} end local json = f:read('*all') f:close() return mp_utils.parse_json(json) end function util.write_json_file(path, data) local f = io.open(path, 'w') f:write(mp_utils.format_json(data), '\n') f:close() end -- merges tables `t' and `u', using key `k' for identity. the order from `u' is -- always respected, preserving the relative order from `t' when possible. function util.stable_kmerge(t, u, k) local pos = {} for i, v in ipairs(u) do pos[v[k]] = i end local res = {} local seen = {} local function append(v) if not seen[v[k]] then res[#res+1] = v seen[v[k]] = true end end local ind = 1 for _, v in ipairs(t) do if pos[v[k]] then while ind <= pos[v[k]] do append(u[ind]) ind = ind + 1 end end append(v) end for i = ind, #u do append(u[i]) end return res end function util.flatten_table(src, dst, prefix) local dst = dst or {} for k, v in pairs(src) do local k = prefix and prefix .. '.' .. k or k if type(v) == 'table' then util.flatten_table(v, dst, k) else dst[k] = v end end return dst end function util.unflatten_table(src, dst) local dst = dst or {} for k, v in pairs(src) do local t = dst local prev for curr in (k .. '.'):gmatch('(.-)%.') do if prev then t[prev] = t[prev] or {} t = t[prev] end prev = curr end t[prev] = v end return dst end function util.find_matches(str, substr, case_sensitive) if not str then return end if not case_sensitive then str = str:lower() end local matches local i, j = 0, 0 while true do i, j = str:find(substr, j + 1, true) -- j < i avoids infinite loop on empty substr if not i or j < i then break end matches = matches or {} matches[#matches+1] = {start = i, stop = j} end return matches or i and {} end function util.str_seek_prev_char(str, pos) return util.utf8_seek(str, pos, -1) end function util.str_seek_next_char(str, pos) return util.utf8_seek(str, pos, 1) end function util.str_seek_prev_word(str, pos) return str:sub(1, pos - 1):match('()%S*%s*$') end function util.str_seek_next_word(str, pos) return str:match('%s*%S*()', pos) end function util.str_insert_char(str, pos, ch) return str:sub(1, pos - 1) .. ch .. str:sub(pos), pos + #ch end function util.str_del_prev_char(str, pos) if pos <= 1 then return end local npos = util.utf8_seek(str, pos, -1) return str:sub(1, npos - 1) .. str:sub(pos), npos end function util.str_del_next_char(str, pos) if pos > #str then return end return str:sub(1, pos - 1) .. str:sub(util.utf8_seek(str, pos, 1)), pos end function util.str_del_prev_word(str, pos) if pos <= 1 then return end local npos = str:sub(1, pos - 1):match('()%S*%s*$') return str:sub(1, npos - 1) .. str:sub(pos), npos end function util.str_del_next_word(str, pos) if pos > #str then return end return str:sub(1, pos - 1) .. str:sub(str:match('%s*%S*()', pos)), pos end function util.str_del_to_start(str, pos) if pos <= 1 then return end return str:sub(pos), 1 end function util.str_del_to_end(str, pos) if pos > #str then return end return str:sub(1, pos - 1), pos end function util.str_transpose_chars(str, pos) if pos <= 1 then return end local npos = util.utf8_seek(str, pos, 1) local cp2 = util.utf8_seek(str, npos, -1) if cp2 <= 1 then return end local cp1 = util.utf8_seek(str, cp2, -1) return str:sub(1, cp1 - 1) .. str:sub(cp2, npos - 1) .. str:sub(cp1, cp2 - 1) .. str:sub(npos), npos end function util.str_transpose_words(str, pos) if pos <= 1 then return end local npos = str:match('%s*%S*()', pos) local pre, w1, sp, w2 = str:sub(1, npos - 1):match( '^(.-)(%S+)(%s+)(%S+%s*)$') if not pre then return end return pre .. w2 .. sp .. w1 .. str:sub(npos), npos end return util