Skip to content

Commit d30296b

Browse files
committed
Merge remote-tracking branch 'myk002/quickfort_xlsx' into quickfort
2 parents 822790f + 1843d7c commit d30296b

File tree

4 files changed

+176
-67
lines changed

4 files changed

+176
-67
lines changed

internal/quickfort/command.lua

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ local valid_command_args = utils.invert({
2727
'-quiet',
2828
'v',
2929
'-verbose',
30-
's',
31-
'-sheet',
30+
'n',
31+
'-name',
3232
})
3333

3434
function do_command(in_args)
@@ -42,14 +42,15 @@ function do_command(in_args)
4242
qerror("expected <list_num> or <blueprint_name> parameter")
4343
end
4444
local list_num = tonumber(blueprint_name)
45+
local sheet_name = nil
4546
if list_num then
46-
blueprint_name = quickfort_list.get_blueprint_by_number(list_num)
47+
blueprint_name, sheet_name =
48+
quickfort_list.get_blueprint_by_number(list_num)
4749
end
48-
4950
local args = utils.processArgs(in_args, valid_command_args)
5051
local quiet = args['q'] ~= nil or args['-quiet'] ~= nil
5152
local verbose = args['v'] ~= nil or args['-verbose'] ~= nil
52-
local sheet = tonumber(args['s']) or tonumber(args['-sheet'])
53+
sheet_name = sheet_name or args['n'] or args['-name']
5354

5455
local cursor = guidm.getCursorPos()
5556
if command ~= 'orders' and not cursor then
@@ -60,7 +61,7 @@ function do_command(in_args)
6061
quickfort_common.verbose = verbose
6162

6263
local filepath = quickfort_common.get_blueprint_filepath(blueprint_name)
63-
local data = quickfort_parse.process_file(filepath, cursor)
64+
local data = quickfort_parse.process_file(filepath, sheet_name, cursor)
6465
for zlevel, section_data_list in pairs(data) do
6566
for _, section_data in ipairs(section_data_list) do
6667
local modeline = section_data.modeline

internal/quickfort/list.lua

Lines changed: 85 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ if not dfhack_flags.module then
66
end
77

88
local utils = require('utils')
9+
local xlsxreader = require('plugins.xlsxreader')
910
local quickfort_common = reqscript('internal/quickfort/common')
1011
local quickfort_parse = reqscript('internal/quickfort/parse')
1112

@@ -20,64 +21,109 @@ end
2021

2122
local blueprint_cache = {}
2223

23-
local function scan_blueprint(path)
24+
local function scan_csv_blueprint(path)
2425
local filepath = quickfort_common.get_blueprint_filepath(path)
2526
local mtime = dfhack.filesystem.mtime(filepath)
2627
if not blueprint_cache[path] or blueprint_cache[path].mtime ~= mtime then
2728
blueprint_cache[path] = {modeline=get_modeline(filepath), mtime=mtime}
2829
end
30+
if not blueprint_cache[path].modeline then
31+
print(string.format('skipping "%s": no #mode marker detected', path))
32+
end
2933
return blueprint_cache[path].modeline
3034
end
3135

32-
local blueprint_files = {}
36+
local function get_xlsx_sheet_modeline(xlsx_file, sheet_name)
37+
local xlsx_sheet = xlsxreader.open_sheet(xlsx_file, sheet_name)
38+
return dfhack.with_finalize(
39+
function() xlsxreader.close_sheet(xlsx_sheet) end,
40+
function()
41+
local row_cells = xlsxreader.get_row(xlsx_sheet)
42+
if not row_cells or #row_cells == 0 then return nil end
43+
return quickfort_parse.parse_modeline(row_cells[1])
44+
end
45+
)
46+
end
47+
48+
local function get_xlsx_file_sheet_infos(filepath)
49+
local sheet_infos = {}
50+
local xlsx_file = xlsxreader.open_xlsx_file(filepath)
51+
if not xlsx_file then return sheet_infos end
52+
return dfhack.with_finalize(
53+
function() xlsxreader.close_xlsx_file(xlsx_file) end,
54+
function()
55+
for _, sheet_name in ipairs(xlsxreader.list_sheets(xlsx_file)) do
56+
local modeline = get_xlsx_sheet_modeline(xlsx_file, sheet_name)
57+
if modeline then
58+
table.insert(sheet_infos,
59+
{name=sheet_name, modeline=modeline})
60+
end
61+
end
62+
return sheet_infos
63+
end
64+
)
65+
end
66+
67+
local function scan_xlsx_blueprint(path)
68+
local filepath = quickfort_common.get_blueprint_filepath(path)
69+
local mtime = dfhack.filesystem.mtime(filepath)
70+
if blueprint_cache[path] and blueprint_cache[path].mtime == mtime then
71+
return blueprint_cache[path].sheet_infos
72+
end
73+
local sheet_infos = get_xlsx_file_sheet_infos(filepath)
74+
if #sheet_infos == 0 then
75+
print(string.format(
76+
'skipping "%s": no sheet with #mode markers detected', path))
77+
end
78+
blueprint_cache[path] = {sheet_infos=sheet_infos, mtime=mtime}
79+
return sheet_infos
80+
end
81+
82+
local blueprints = {}
3383

3484
local function scan_blueprints()
3585
local paths = dfhack.filesystem.listdir_recursive(
3686
quickfort_common.settings['blueprints_dir'].value, nil, false)
37-
blueprint_files = {}
38-
local library_files = {}
87+
blueprints = {}
88+
local library_blueprints = {}
3989
for _, v in ipairs(paths) do
40-
if not v.isdir and
41-
(string.find(v.path, '[.]csv$') or
42-
string.find(v.path, '[.]xlsx$')) then
43-
if string.find(v.path, '[.]xlsx$') then
44-
print(string.format(
45-
'skipping "%s": .xlsx files not supported yet', v.path))
46-
goto skip
90+
local is_library = string.find(v.path, '^library/') ~= nil
91+
local target_list = blueprints
92+
if is_library then target_list = library_blueprints end
93+
if not v.isdir and string.find(v.path:lower(), '[.]csv$') then
94+
local modeline = scan_csv_blueprint(v.path)
95+
if modeline then
96+
table.insert(target_list,
97+
{path=v.path, modeline=modeline, is_library=is_library})
4798
end
48-
local modeline = scan_blueprint(v.path)
49-
if not modeline then
50-
print(string.format(
51-
'skipping "%s": no #mode marker detected', v.path))
52-
goto skip
99+
elseif not v.isdir and string.find(v.path:lower(), '[.]xlsx$') then
100+
local sheet_infos = scan_xlsx_blueprint(v.path)
101+
if #sheet_infos > 0 then
102+
for _, sheet_info in ipairs(sheet_infos) do
103+
table.insert(target_list,
104+
{path=v.path,
105+
sheet_name=sheet_info.name,
106+
modeline=sheet_info.modeline,
107+
is_library=is_library})
108+
end
53109
end
54-
if string.find(v.path, '^library/') ~= nil then
55-
table.insert(
56-
library_files,
57-
{path=v.path, modeline=modeline, is_library=true})
58-
else
59-
table.insert(
60-
blueprint_files,
61-
{path=v.path, modeline=modeline, is_library=false})
62-
end
63-
::skip::
64110
end
65111
end
66112
-- tack library files on to the end so user files are contiguous
67-
for i=1, #library_files do
68-
blueprint_files[#blueprint_files + 1] = library_files[i]
113+
for i=1, #library_blueprints do
114+
blueprints[#blueprints + 1] = library_blueprints[i]
69115
end
70116
end
71117

72118
function get_blueprint_by_number(list_num)
73-
if #blueprint_files == 0 then
119+
if #blueprints == 0 then
74120
scan_blueprints()
75121
end
76-
local blueprint_file = blueprint_files[list_num]
77-
if not blueprint_file then
122+
local blueprint = blueprints[list_num]
123+
if not blueprint then
78124
qerror(string.format('invalid list index: %d', list_num))
79125
end
80-
return blueprint_file.path
126+
return blueprint.path, blueprint.sheet_name
81127
end
82128

83129
local valid_list_args = utils.invert({
@@ -89,8 +135,12 @@ function do_list(in_args)
89135
local args = utils.processArgs(in_args, valid_list_args)
90136
local show_library = args['l'] ~= nil or args['-library'] ~= nil
91137
scan_blueprints()
92-
for i, v in ipairs(blueprint_files) do
138+
for i, v in ipairs(blueprints) do
93139
if show_library or not v.is_library then
140+
local sheet_spec = ''
141+
if v.sheet_name then
142+
sheet_spec = string.format(' -n "%s"', v.sheet_name)
143+
end
94144
local comment = ')'
95145
if #v.modeline.comment > 0 then
96146
comment = string.format(': %s)', v.modeline.comment)
@@ -100,8 +150,8 @@ function do_list(in_args)
100150
start_comment = string.format('; cursor start: %s',
101151
v.modeline.start_comment)
102152
end
103-
print(string.format('%d) "%s" (%s%s%s',
104-
i, v.path, v.modeline.mode, comment,
153+
print(string.format('%d) "%s"%s (%s%s%s',
154+
i, v.path, sheet_spec, v.modeline.mode, comment,
105155
start_comment))
106156
end
107157
end

internal/quickfort/parse.lua

Lines changed: 81 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ if not dfhack_flags.module then
55
qerror('this script cannot be called directly')
66
end
77

8+
local xlsxreader = require('plugins.xlsxreader')
89
local quickfort_common = reqscript('internal/quickfort/common')
910

1011
local function trim_and_insert(tokens, token)
@@ -13,6 +14,7 @@ local function trim_and_insert(tokens, token)
1314
end
1415

1516
-- adapted from example on http://lua-users.org/wiki/LuaCsv
17+
-- returns a list of strings corresponding to the text in the cells in the row
1618
function tokenize_csv_line(line)
1719
line = string.gsub(line, '[\r\n]*$', '')
1820
local tokens = {}
@@ -108,16 +110,62 @@ local function make_cell_label(col_num, row_num)
108110
return get_col_name(col_num) .. tostring(math.floor(row_num))
109111
end
110112

113+
local function read_csv_line(ctx)
114+
local line = ctx.csv_file:read()
115+
if not line then return nil end
116+
return tokenize_csv_line(line)
117+
end
118+
119+
local function cleanup_csv_ctx(ctx)
120+
ctx.csv_file:close()
121+
end
122+
123+
local function read_xlsx_line(ctx)
124+
return xlsxreader.get_row(ctx.xlsx_sheet)
125+
end
126+
127+
local function cleanup_xslx_ctx(ctx)
128+
xlsxreader.close_sheet(ctx.xlsx_sheet)
129+
xlsxreader.close_xlsx_file(ctx.xlsx_file)
130+
end
131+
132+
local function init_reader_ctx(filepath, sheet_name)
133+
local reader_ctx = {}
134+
if string.find(filepath:lower(), '[.]csv$') then
135+
local file = io.open(filepath)
136+
if not file then
137+
qerror(string.format('failed to open blueprint file: "%s"',
138+
filepath))
139+
end
140+
reader_ctx.csv_file = file
141+
reader_ctx.get_row_tokens = read_csv_line
142+
reader_ctx.cleanup = cleanup_csv_ctx
143+
else
144+
reader_ctx.xlsx_file = xlsxreader.open_xlsx_file(filepath)
145+
if not reader_ctx.xlsx_file then
146+
qerror(string.format('failed to open blueprint file: "%s"',
147+
filepath))
148+
end
149+
-- open_sheet succeeds even if the sheet cannot be found; we need to
150+
-- check that when we try to read
151+
reader_ctx.xlsx_sheet =
152+
xlsxreader.open_sheet(reader_ctx.xlsx_file, sheet_name)
153+
reader_ctx.get_row_tokens = read_xlsx_line
154+
reader_ctx.cleanup = cleanup_xslx_ctx
155+
end
156+
return reader_ctx
157+
end
158+
111159
-- returns a grid representation of the current section, the number of lines
112160
-- read from the input, and the next z-level modifier, if any. See process_file
113161
-- for grid format.
114-
local function process_section(file, start_line_num, start_coord)
162+
local function process_section(reader_ctx, start_line_num, start_coord)
115163
local grid = {}
116164
local y = start_coord.y
117165
while true do
118-
local line = file:read()
119-
if not line then return grid, y-start_coord.y end
120-
for i, v in ipairs(tokenize_csv_line(line)) do
166+
local row_tokens = reader_ctx.get_row_tokens(reader_ctx)
167+
if not row_tokens then return grid, y-start_coord.y end
168+
for i, v in ipairs(row_tokens) do
121169
if i == 1 then
122170
if v == '#<' then return grid, y-start_coord.y, 1 end
123171
if v == '#>' then return grid, y-start_coord.y, -1 end
@@ -135,32 +183,22 @@ local function process_section(file, start_line_num, start_coord)
135183
end
136184
end
137185

138-
--[[
139-
returns the following logical structure:
140-
map of target map z coordinate ->
141-
list of {modeline, grid} tables
142-
Where the structure of modeline is defined as per parse_modeline and grid is a:
143-
map of target y coordinate ->
144-
map of target map x coordinate ->
145-
{cell=spreadsheet cell, text=text from spreadsheet cell}
146-
Map keys are numbers, and the keyspace is sparse -- only elements that have
147-
contents are non-nil.
148-
]]
149-
function process_file(filepath, start_cursor_coord)
150-
local file = io.open(filepath)
151-
if not file then
152-
qerror(string.format('failed to open blueprint file: "%s"', filepath))
186+
function process_sections(reader_ctx, filepath, sheet_name, start_cursor_coord)
187+
local row_tokens = reader_ctx.get_row_tokens(reader_ctx)
188+
if not row_tokens then
189+
qerror(string.format(
190+
'sheet with name: "%s" in file "%s" empty or not found',
191+
sheet_name, filepath))
153192
end
154-
local line = file:read()
155-
local modeline = parse_modeline(tokenize_csv_line(line)[1])
193+
local modeline = parse_modeline(row_tokens[1])
156194
local cur_line_num = 2
157195
local x = start_cursor_coord.x - modeline.startx + 1
158196
local y = start_cursor_coord.y - modeline.starty + 1
159197
local z = start_cursor_coord.z
160198
local zlevels = {}
161199
while true do
162200
local grid, num_section_rows, zmod =
163-
process_section(file, cur_line_num, xyz2pos(x, y, z))
201+
process_section(reader_ctx, cur_line_num, xyz2pos(x, y, z))
164202
for _, _ in pairs(grid) do
165203
-- apparently, the only way to tell if a sparse array is not empty
166204
if not zlevels[z] then zlevels[z] = {} end
@@ -171,7 +209,27 @@ function process_file(filepath, start_cursor_coord)
171209
cur_line_num = cur_line_num + num_section_rows + 1
172210
z = z + zmod
173211
end
174-
file:close()
175212
return zlevels
176213
end
177214

215+
--[[
216+
returns the following logical structure:
217+
map of target map z coordinate ->
218+
list of {modeline, grid} tables
219+
Where the structure of modeline is defined as per parse_modeline and grid is a:
220+
map of target y coordinate ->
221+
map of target map x coordinate ->
222+
{cell=spreadsheet cell, text=text from spreadsheet cell}
223+
Map keys are numbers, and the keyspace is sparse -- only elements that have
224+
contents are non-nil.
225+
]]
226+
function process_file(filepath, sheet_name, start_cursor_coord)
227+
local reader_ctx = init_reader_ctx(filepath, sheet_name)
228+
return dfhack.with_finalize(
229+
function() reader_ctx.cleanup(reader_ctx) end,
230+
function()
231+
return process_sections(reader_ctx, filepath, sheet_name,
232+
start_cursor_coord)
233+
end
234+
)
235+
end

quickfort.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ Usage:
3939
subfolder are not shown. Specify ``-l`` to include library blueprints.
4040
**quickfort <command> <list_num> [<options>]**
4141
Applies the blueprint with the number from the list command.
42-
**quickfort <command> <filename> [-s|--sheet <sheet_num>] [<options>]**
42+
**quickfort <command> <filename> [-n|--name <sheet_name>] [<options>]**
4343
Applies the blueprint from the named file. If it is an ``.xlsx`` file,
44-
the ``-s`` (or ``--sheet``) parameter is required to identify the sheet
45-
number. The first sheet is ``-s 1``.
44+
the ``-n`` (or ``--name``) parameter can identify the sheet name. If the
45+
sheet name is not specified, the first sheet is used.
4646
4747
**<command>** can be one of:
4848

0 commit comments

Comments
 (0)