-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathluaParser.lua
More file actions
270 lines (243 loc) · 9.4 KB
/
luaParser.lua
File metadata and controls
270 lines (243 loc) · 9.4 KB
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
-- luaParser.lua -- Text-based gear extraction for ClosetCleaner v2
-- Reads GearSwap Lua files as plain text and extracts gear item names
-- from known equipment slot assignments using pattern matching.
local parser = {}
local gear_slots = {
main=true, sub=true, range=true, ranged=true, ammo=true,
head=true, neck=true, body=true, hands=true,
back=true, waist=true, legs=true, feet=true,
left_ear=true, right_ear=true, ear1=true, ear2=true, lear=true, rear=true,
left_ring=true, right_ring=true, ring1=true, ring2=true, lring=true, rring=true,
}
-- Strip single-line comments (--) while preserving strings.
-- Handles the common case; does not attempt full Lua lexing.
local function strip_comments(text)
local lines = {}
for line in (text .. '\n'):gmatch('(.-)\n') do
local stripped = line
local i = 1
while i <= #stripped do
local c = stripped:sub(i, i)
if c == '"' or c == "'" then
local close = stripped:find(c, i + 1, true)
if close then
i = close + 1
else
break
end
elseif c == '-' and stripped:sub(i + 1, i + 1) == '-' then
stripped = stripped:sub(1, i - 1)
break
else
i = i + 1
end
end
lines[#lines + 1] = stripped
end
return table.concat(lines, '\n')
end
-- Strip block comments --[[ ... ]] or --[[ ... ]]--
-- Lua's . doesn't match newlines, so we search with string.find instead.
local function strip_block_comments(text)
local result = text
while true do
local open = result:find('%-%-%[%[', 1, false)
if not open then break end
local close = result:find('%]%]', open + 4, false)
if not close then
result = result:sub(1, open - 1)
break
end
local end_pos = close + 1
if result:sub(end_pos + 1, end_pos + 2) == '--' then
end_pos = end_pos + 2
end
result = result:sub(1, open - 1) .. result:sub(end_pos + 1)
end
return result
end
-- Collect variable-to-string assignments for resolution of indirect gear
-- references like head = EMPY.Head where EMPY.Head was set to a string.
-- Returns { ["VAR.field"] = "value", ["simple_var"] = "value", ... }
local function collect_variables(clean)
local vars = {}
-- TABLE.field = "string" / TABLE.field = 'string'
for tbl, field, val in clean:gmatch('(%a[%w_]*)%.(%a[%w_]*)%s*=%s*"([^"]+)"') do
vars[tbl .. '.' .. field] = val:match('^%s*(.-)%s*$')
end
for tbl, field, val in clean:gmatch("(%a[%w_]*)%.(%a[%w_]*)%s*=%s*'([^']+)'") do
vars[tbl .. '.' .. field] = val:match('^%s*(.-)%s*$')
end
-- TABLE = { field = "string", ... } (table constructors via balanced braces)
for tbl_name, body in clean:gmatch('(%a[%w_]*)%s*=%s*(%b{})') do
for field, val in body:gmatch('(%a[%w_]*)%s*=%s*"([^"]+)"') do
local key = tbl_name .. '.' .. field
if not vars[key] then
vars[key] = val:match('^%s*(.-)%s*$')
end
end
for field, val in body:gmatch("(%a[%w_]*)%s*=%s*'([^']+)'") do
local key = tbl_name .. '.' .. field
if not vars[key] then
vars[key] = val:match('^%s*(.-)%s*$')
end
end
end
-- simple_var = "string" (not a gear slot or 'name', to avoid duplicating
-- direct slot matches handled by extract_gear_names)
for key, val in clean:gmatch('(%a[%w_]*)%s*=%s*"([^"]+)"') do
if not gear_slots[key:lower()] and key ~= 'name' then
vars[key] = val:match('^%s*(.-)%s*$')
end
end
for key, val in clean:gmatch("(%a[%w_]*)%s*=%s*'([^']+)'") do
if not gear_slots[key:lower()] and key ~= 'name' then
vars[key] = val:match('^%s*(.-)%s*$')
end
end
return vars
end
-- Extract all gear item names from a single Lua source string.
-- Returns a table { ["item name lowercase"] = true, ... }
function parser.extract_gear_names(source)
local items = {}
local clean = strip_comments(strip_block_comments(source))
local vars = collect_variables(clean)
-- Pattern 1: slot_key = "Item Name" or slot_key = 'Item Name'
-- We match word = "string" then check if word is a known slot.
-- Slot check is case-insensitive so that TABLE.Head = "Item" is also
-- recognised (the gmatch captures "Head" as the key after the dot).
for key, val in clean:gmatch('(%a[%w_]*)%s*=%s*"([^"]+)"') do
if gear_slots[key:lower()] then
local name = val:match('^%s*(.-)%s*$')
if name ~= '' and name ~= 'empty' then
items[name:lower()] = true
end
end
end
for key, val in clean:gmatch("(%a[%w_]*)%s*=%s*'([^']+)'") do
if gear_slots[key:lower()] then
local name = val:match('^%s*(.-)%s*$')
if name ~= '' and name ~= 'empty' then
items[name:lower()] = true
end
end
end
-- Pattern 2: name = "Item Name" inside augmented gear tables.
for val in clean:gmatch('name%s*=%s*"([^"]+)"') do
local name = val:match('^%s*(.-)%s*$')
if name ~= '' and name ~= 'empty' then
items[name:lower()] = true
end
end
for val in clean:gmatch("name%s*=%s*'([^']+)'") do
local name = val:match('^%s*(.-)%s*$')
if name ~= '' and name ~= 'empty' then
items[name:lower()] = true
end
end
-- Pattern 3: slot_key = { "Item Name", augments={...} }
-- Augmented gear from //gs export uses the item name as the first
-- positional element in the table, with no name= key.
for key, val in clean:gmatch('(%a[%w_]*)%s*=%s*{%s*"([^"]+)"') do
if gear_slots[key:lower()] then
local name = val:match('^%s*(.-)%s*$')
if name ~= '' and name ~= 'empty' then
items[name:lower()] = true
end
end
end
for key, val in clean:gmatch("(%a[%w_]*)%s*=%s*{%s*'([^']+)'") do
if gear_slots[key:lower()] then
local name = val:match('^%s*(.-)%s*$')
if name ~= '' and name ~= 'empty' then
items[name:lower()] = true
end
end
end
-- Pattern 4: slot_key = TABLE.field (variable reference to a table field)
for key, tbl, field in clean:gmatch('(%a[%w_]*)%s*=%s*(%a[%w_]*)%.(%a[%w_]*)') do
if gear_slots[key:lower()] then
local val = vars[tbl .. '.' .. field]
if val and val ~= '' and val:lower() ~= 'empty' then
items[val:lower()] = true
end
end
end
-- Pattern 5: slot_key = simple_var (reference to a plain string variable)
for key, var_ref in clean:gmatch('(%a[%w_]*)%s*=%s*(%a[%w_]*)') do
if gear_slots[key:lower()] and vars[var_ref] then
local val = vars[var_ref]
if val ~= '' and val:lower() ~= 'empty' then
items[val:lower()] = true
end
end
end
return items
end
-- Extract include directives from source text.
-- Matches: include('Filename') include("Filename") include 'Filename' include "Filename"
-- Returns a list of raw include strings (e.g. "Mote-Include").
function parser.extract_includes(source)
local includes = {}
local seen = {}
local clean = strip_comments(strip_block_comments(source))
for name in clean:gmatch("include%s*%(?%s*['\"]([^'\"]+)['\"]") do
local key = name:lower()
if not seen[key] then
seen[key] = true
includes[#includes + 1] = name
end
end
return includes
end
-- Read a file's full contents. Returns the string, or nil on failure.
function parser.read_file(path)
local f = io.open(path, 'r')
if not f then return nil end
local text = f:read('*a')
f:close()
return text
end
-- Parse a Lua file and all its includes recursively.
-- search_dirs: ordered list of directories to search for includes.
-- Returns { ["item name lowercase"] = true, ... } (merged from file + includes).
function parser.parse_file_recursive(filepath, search_dirs)
local visited = {}
local all_items = {}
local function process(path)
local norm = path:lower():gsub('\\', '/')
if visited[norm] then return end
visited[norm] = true
local source = parser.read_file(path)
if not source then return end
local items = parser.extract_gear_names(source)
for k in pairs(items) do
all_items[k] = true
end
local includes = parser.extract_includes(source)
for _, inc_name in ipairs(includes) do
local inc_file = inc_name
if not inc_file:match('%.lua$') then
inc_file = inc_file .. '.lua'
end
for _, dir in ipairs(search_dirs) do
local try_path = dir .. inc_file
if windower and windower.file_exists and windower.file_exists(try_path) then
process(try_path)
break
elseif not windower then
local f = io.open(try_path, 'r')
if f then
f:close()
process(try_path)
break
end
end
end
end
end
process(filepath)
return all_items
end
return parser