-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathutils.lua
288 lines (263 loc) · 6.71 KB
/
utils.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
local utils = {}
local getmt = getmetatable
local setmt = setmetatable
local pairs = pairs
local ipairs = ipairs
local s_gsub = string.gsub
local utf8 = utf8 or require('.utf8_polyfill')
---Shallow copy a table's sequence part using `ipairs`.
---@generic T
---@param from T[]
---@param into T[]?
---@return T[]
function utils.copy_ipairs(from, into)
into = into or {}
for i, v in ipairs(from) do
into[i] = v
end
return into
end
---Shallow copy a table using `pairs`.
---@param from table
---@param into table?
---@return table
function utils.copy_pairs(from, into)
into = into or {}
for k, v in pairs(from) do
into[k] = v
end
return into
end
---Shallow copy a table without calling `__pairs` metamethod.
---@param from table
---@param into table?
---@return table
function utils.raw_shallow_copy(from, into)
into = into or {}
for k, v in next, from do
into[k] = v
end
return into
end
---Apply `func` to each element of `...` and return a table.
---@generic TIn
---@generic TOut
---@param func fun(arg: TIn): TOut
---@vararg TIn
---@return TOut[]
function utils.map_varargs(func, ...)
local n = select('#', ...)
local t = {...}
for i = 1, n do
t[i] = func(t[i])
end
return t
end
---@generic T
---@param list T[]
---@return {[T]: true}
function utils.list_to_bool_dict(list)
local dict = {}
for _, v in ipairs(list) do
dict[v] = true
end
return dict
end
local NBSP = '\194\160'
local ENTITY_ENCODE_MAP = {
['&'] = '&',
[NBSP] = ' ',
['"'] = '"',
['<'] = '<',
['>'] = '>',
}
---Replace `&`, NBSP, `"`, `<`, `>` with entities.
---@param str string | number
---@return string
function utils.attr_encode(str)
return (s_gsub(str, '\194?[\160&"<>]', ENTITY_ENCODE_MAP))
end
---Replace `&`, NBSP, `<`, `>` with entities.
---@param str string | number
---@return string
function utils.html_encode(str)
return (s_gsub(str, '\194?[\160&<>]', ENTITY_ENCODE_MAP))
end
---Return truthy value when `name` is a valid XML name, otherwise falsy value.
---
---Defined at:
---- https://www.w3.org/TR/xml/#NT-Name
---- https://www.w3.org/TR/xml11/#NT-Name
---
---TODO: non-ASCII support
---@param name any
---@return any
function utils.is_xml_name(name)
return type(name) == 'string' and name:find('^[:%a_][:%w_%-%.]*$')
end
local NON_CUSTOM_NAMES = {
['annotation-xml'] = true,
['color-profile'] = true,
['font-face'] = true,
['font-face-src'] = true,
['font-face-uri'] = true,
['font-face-format'] = true,
['font-face-name'] = true,
['missing-glyph'] = true,
}
---defined at https://html.spec.whatwg.org/#prod-pcenchar
local PCEN_CHAR_RANGES = {
{0x2D, 0x2E}, -- '-', '.'
{0x30, 0x39}, -- 0-9
{0x5F, 0x5F}, -- '_'
{0x61, 0x7A}, -- a-z
{0xB7, 0xB7},
{0xC0, 0xD6},
{0xD8, 0xF6},
{0xF8, 0x37D},
{0x37F, 0x1FFF},
{0x200C, 0x200D},
{0x203F, 0x2040},
{0x2070, 0x218F},
{0x2C00, 0x2FEF},
{0x3001, 0xD7FF},
{0xF900, 0xFDCF},
{0xFDF0, 0xFFFD},
{0x10000, 0xEFFFF},
}
---@param code_point integer
---@return boolean
local function is_pcen_char_code(code_point)
for _, range in ipairs(PCEN_CHAR_RANGES) do
if code_point >= range[1] and code_point <= range[2] then
return true
end
end
return false
end
---Return truthy value when `name` is a valid HTML tag name, otherwise falsy value.
---
---Defined at:
---- https://html.spec.whatwg.org/#syntax-tag-name
---- https://html.spec.whatwg.org/#valid-custom-element-name
---@param name any
---@return any
function utils.is_html_tag_name(name)
if type(name) ~= 'string' then
return false
elseif name:find('^%w+$') then
return true
elseif NON_CUSTOM_NAMES[name:lower()] then
return true
end
---@cast name string
local subs1, subs2 = name:match('^%l(.*)%-(.*)$')
if not subs1 then
return false
end
---@param s string
---@return boolean
local function validate(s)
if s:find('^[%-%.%d_%l]*$') then
return true
end
for _, cp in utf8.codes(s) do
if not is_pcen_char_code(cp) then
return false
end
end
return true
end
return validate(subs1) and validate(subs2)
end
---Return truthy value when `name` is a valid HTML attribute name, otherwise falsy value.
---
---Defined at:
---- https://html.spec.whatwg.org/#syntax-attribute-name
---@param name any
---@return any
function utils.is_html_attr_name(name)
if type(name) ~= 'string' then
return false
elseif name:find('[%z\1-\31\127 "\'>/=]') then
return false
end
return true
end
---Return truthy value when `name` is reserved by Lua (e.g., '_G', '_PROMPT'),
---otherwise falsy value.
---@param name any
---@return any
function utils.is_lua_reserved_name(name)
return type(name) == 'string' and name:find('^_[%u%d]+$')
end
---@param str string
---@return table
function utils.parse_shorthand_attrs(str)
-- parse id
local id = nil
str = s_gsub(str, '#([^%s#]*)', function (s)
if s == '' then
error('empty id found in '..('%q'):format(str), 4)
end
if id then
error('two or more ids found in '..('%q'):format(str), 4)
end
id = s
return ''
end)
-- parse class
local class = str:gsub('^%s*(.-)%s*$', '%1'):gsub('%s+', ' ')
return {
id = id,
class = class ~= '' and class or nil,
}
end
---Return a new `__index` metamethod that extends `base_index` with `elem_entry`.
---@param base_index table | (fun(t: any, k: any): any) | nil
---@param elem_entry table `acandy.a`
---@return fun(t: any, k: any): any
local function to_extended_index(base_index, elem_entry)
local function fallback(_t, k)
if utils.is_lua_reserved_name(k) then
return nil -- avoid indexing `a.__PROMPT` in interactive mode
end
return elem_entry[k]
end
if not base_index then
return fallback
elseif type(base_index) == "function" then
return function (t, k)
local v = base_index(t, k)
if v ~= nil then return v end
return fallback(t, k)
end
end
return function (t, k)
local v = base_index[k]
if v ~= nil then return v end
return fallback(t, k)
end
end
---Extend the environment in place with `acandy.a` as `__index`.
---@param env table the environment to be extended, e.g. `_ENV`, `_G`
---@param elem_entry table `acandy.a`
function utils.extend_env_with_elem_entry(env, elem_entry)
local mt = getmt(env)
if mt then
rawset(mt, '__index', to_extended_index(rawget(mt, '__index'), elem_entry))
else
setmt(env, {__index = to_extended_index(nil, elem_entry)})
end
end
---Return a new environment based on `env` with `acandy.a` as `__index`.
---@param env table the environment on which the new environment is based, e.g. `_ENV`, `_G`
---@param elem_entry table `acandy.a`
---@return table
function utils.to_extended_env_with_elem_entry(env, elem_entry)
local base_mt = getmt(env)
local new_mt = base_mt and utils.raw_shallow_copy(base_mt) or {}
new_mt.__index = to_extended_index(new_mt.__index, elem_entry)
return setmt(utils.raw_shallow_copy(env), new_mt)
end
return utils