changed
README.md
|
@@ -23,7 23,7 @@ Via hex, in mix.exs:
|
23
23
|
|
24
24
|
```Elixir
|
25
25
|
defp deps do
|
26
|
- [{:elixlsx, "~> 0.4.1"}]
|
26
|
[{:elixlsx, "~> 0.4.2"}]
|
27
27
|
end
|
28
28
|
```
|
changed
hex_metadata.config
|
@@ -18,10 18,11 @@
|
18
18
|
<<"lib/elixlsx/style/cell_style.ex">>,<<"lib/elixlsx/style/fill.ex">>,
|
19
19
|
<<"lib/elixlsx/style/font.ex">>,<<"lib/elixlsx/style/num_fmt.ex">>,
|
20
20
|
<<"lib/elixlsx/util.ex">>,<<"lib/elixlsx/workbook.ex">>,
|
21
|
- <<"lib/elixlsx/writer.ex">>,<<"lib/elixlsx/xml_templates.ex">>,
|
22
|
- <<"mix.exs">>,<<"README.md">>,<<"LICENSE">>]}.
|
21
|
<<"lib/elixlsx/writer.ex">>,<<"lib/elixlsx/xml.ex">>,
|
22
|
<<"lib/elixlsx/xml_templates.ex">>,<<"mix.exs">>,<<"README.md">>,
|
23
|
<<"LICENSE">>]}.
|
23
24
|
{<<"licenses">>,[<<"MIT">>]}.
|
24
25
|
{<<"links">>,[{<<"GitHub">>,<<"https://github.com/xou/elixlsx">>}]}.
|
25
26
|
{<<"name">>,<<"elixlsx">>}.
|
26
27
|
{<<"requirements">>,[]}.
|
27
|
- {<<"version">>,<<"0.4.1">>}.
|
28
|
{<<"version">>,<<"0.4.2">>}.
|
changed
lib/elixlsx/compiler.ex
|
@@ -25,7 25,7 @@ defmodule Elixlsx.Compiler do
|
25
25
|
|
26
26
|
def compinfo_cell_pass_value wci, value do
|
27
27
|
cond do
|
28
|
- is_binary(value) && String.valid?(value)
|
28
|
is_binary(value) && XML.valid?(value)
|
29
29
|
-> update_in wci.stringdb, &StringDB.register_string(&1, value)
|
30
30
|
true -> wci
|
31
31
|
end
|
changed
lib/elixlsx/compiler/cell_style_db.ex
|
@@ -46,29 46,29 @@ defmodule Elixlsx.Compiler.CellStyleDB do
|
46
46
|
@spec register_all(WorkbookCompInfo.t) :: WorkbookCompInfo.t
|
47
47
|
def register_all(wci) do
|
48
48
|
Enum.reduce wci.cellstyledb.cellstyles, wci, fn ({style, _}, wci) ->
|
49
|
- wci = if is_nil(style.font) do
|
50
|
- wci
|
51
|
- else
|
52
|
- update_in(wci.fontdb, &(FontDB.register_font &1, style.font))
|
53
|
- end
|
54
|
- wci = if is_nil(style.fill) do
|
55
|
- wci
|
56
|
- else
|
57
|
- update_in(wci.filldb, &(FillDB.register_fill &1, style.fill))
|
58
|
- end
|
59
|
- wci = if is_nil(style.numfmt) do
|
60
|
- wci
|
61
|
- else
|
62
|
- update_in(wci.numfmtdb, &(NumFmtDB.register_numfmt &1, style.numfmt))
|
63
|
- end
|
64
|
- wci = if is_nil(style.border) do
|
65
|
- wci
|
66
|
- else
|
67
|
- update_in(wci.borderstyledb, &(BorderStyleDB.register_border &1, style.border))
|
68
|
- end
|
69
|
-
|
70
49
|
wci
|
50
|
|> update_font(style.font)
|
51
|
|> update_fill(style.fill)
|
52
|
|> update_numfmt(style.numfmt)
|
53
|
|> update_border(style.border)
|
71
54
|
# TODO: update_in wci.borderstyledb ...; wci.fillstyledb...
|
72
55
|
end
|
73
56
|
end
|
57
|
|
58
|
defp update_border(wci, nil), do: wci
|
59
|
|
60
|
defp update_border(wci, border), do: update_in(wci.borderstyledb, &(BorderStyleDB.register_border &1, border))
|
61
|
|
62
|
defp update_fill(wci, nil), do: wci
|
63
|
|
64
|
defp update_fill(wci, fill), do: update_in(wci.filldb, &(FillDB.register_fill &1, fill))
|
65
|
|
66
|
defp update_font(wci, nil), do: wci
|
67
|
|
68
|
defp update_font(wci, font), do: update_in(wci.fontdb, &(FontDB.register_font &1, font))
|
69
|
|
70
|
defp update_numfmt(wci, nil), do: wci
|
71
|
|
72
|
defp update_numfmt(wci, numfmt), do: update_in(wci.numfmtdb, &(NumFmtDB.register_numfmt &1, numfmt))
|
73
|
|
74
74
|
end
|
changed
lib/elixlsx/compiler/string_db.ex
|
@@ -23,8 23,14 @@ defmodule Elixlsx.Compiler.StringDB do
|
23
23
|
def get_id(stringdb, s) do
|
24
24
|
case Map.fetch(stringdb.strings, s) do
|
25
25
|
:error ->
|
26
|
- raise %ArgumentError{
|
27
|
- message: "Invalid key provided for StringDB.get_id: " <> inspect(s)}
|
26
|
if XML.valid?(s) do
|
27
|
raise %ArgumentError{
|
28
|
message: "Invalid key provided for StringDB.get_id: " <> inspect(s)}
|
29
|
else
|
30
|
# if the xml is invalid, then we never wanted it in the stringdb to
|
31
|
# begin with
|
32
|
-1
|
33
|
end
|
28
34
|
{:ok, id} ->
|
29
35
|
id
|
30
36
|
end
|
changed
lib/elixlsx/sheet.ex
|
@@ -16,16 16,19 @@ defmodule Elixlsx.Sheet do
|
16
16
|
The property list describes formatting options for that
|
17
17
|
cell. See Font.from_props/1 for a list of options.
|
18
18
|
"""
|
19
|
- defstruct name: "", rows: [], col_widths: %{}, row_heights: %{}, merge_cells: [], pane_freeze: nil, show_grid_lines: true
|
19
|
defstruct name: "", rows: [], col_widths: %{}, row_heights: %{}, group_cols: [], group_rows: [], merge_cells: [], pane_freeze: nil, show_grid_lines: true
|
20
20
|
@type t :: %Sheet {
|
21
21
|
name: String.t,
|
22
22
|
rows: list(list(any())),
|
23
23
|
col_widths: %{pos_integer => number},
|
24
24
|
row_heights: %{pos_integer => number},
|
25
|
- merge_cells: [],
|
25
|
group_cols: list(rowcol_group),
|
26
|
group_rows: list(rowcol_group),
|
27
|
merge_cells: [{String.t, String.t}],
|
26
28
|
pane_freeze: {number, number} | nil,
|
27
29
|
show_grid_lines: boolean()
|
28
30
|
}
|
31
|
@type rowcol_group :: Range.t | {Range.t, opts :: keyword}
|
29
32
|
|
30
33
|
@doc ~S"""
|
31
34
|
Create a sheet with a sheet name.
|
|
@@ -140,6 143,36 @@ defmodule Elixlsx.Sheet do
|
140
143
|
&(Map.put &1, row_idx, height)
|
141
144
|
end
|
142
145
|
|
146
|
@spec group_cols(Sheet.t, String.t, String.t) :: Sheet.t
|
147
|
@doc ~S"""
|
148
|
Group given column range. (i.e. increase outline level by one)
|
149
|
Column is indexed by name ("A", ...)
|
150
|
|
151
|
## Options
|
152
|
|
153
|
- `collapsed`: if true, collapse this group.
|
154
|
"""
|
155
|
def group_cols(sheet, first_col, last_col, opts \\ []) do
|
156
|
col_range = Range.new(Util.decode_col(first_col), Util.decode_col(last_col))
|
157
|
new_group = if opts === [], do: col_range, else: {col_range, opts}
|
158
|
update_in(sheet.group_cols, fn groups -> groups [new_group] end)
|
159
|
end
|
160
|
|
161
|
@spec group_rows(Sheet.t, pos_integer, pos_integer) :: Sheet.t
|
162
|
@doc ~S"""
|
163
|
Group given row range. (i.e. increase outline level by one)
|
164
|
Row is indexed starting from 1.
|
165
|
|
166
|
## Options
|
167
|
|
168
|
- `collapsed`: if true, collapse this group.
|
169
|
"""
|
170
|
def group_rows(sheet, first_row_idx, last_row_idx, opts \\ []) do
|
171
|
row_range = Range.new(first_row_idx, last_row_idx)
|
172
|
new_group = if opts === [], do: row_range, else: {row_range, opts}
|
173
|
update_in(sheet.group_rows, fn groups -> groups [new_group] end)
|
174
|
end
|
175
|
|
143
176
|
@spec set_pane_freeze(Sheet.t, number, number) :: Sheet.t
|
144
177
|
@doc ~S"""
|
145
178
|
Set the pane freeze at the given row and column. Row and column are indexed starting from 1.
|
changed
lib/elixlsx/style/num_fmt.ex
|
@@ -12,7 12,7 @@ defmodule Elixlsx.Style.NumFmt do
|
12
12
|
props[:yyyymmdd] -> date_yyyy_mm_dd()
|
13
13
|
props[:datetime] -> date_datetime()
|
14
14
|
props[:num_format] ->
|
15
|
- if String.valid? props[:num_format] do
|
15
|
if XML.valid? props[:num_format] do
|
16
16
|
%NumFmt{format: props[:num_format]}
|
17
17
|
else
|
18
18
|
raise %ArgumentError{
|
changed
lib/elixlsx/util.ex
|
@@ -1,5 1,5 @@
|
1
1
|
defmodule Elixlsx.Util do
|
2
|
- @col_alphabet to_string(Enum.to_list(?A..?Z))
|
2
|
@col_alphabet Enum.to_list(?A..?Z)
|
3
3
|
|
4
4
|
@doc ~S"""
|
5
5
|
returns the column letter(s) associated with a column index. Col idx starts at 1.
|
|
@@ -16,14 16,17 @@ defmodule Elixlsx.Util do
|
16
16
|
@spec encode_col(non_neg_integer) :: String.t
|
17
17
|
def encode_col(0), do: ""
|
18
18
|
def encode_col(num) when num <= 26, do: <<num 64>>
|
19
|
- def encode_col(num) do
|
19
|
|
20
|
def encode_col(num, suffix \\ "")
|
21
|
def encode_col(num, suffix) when num <= 26, do: <<num 64>> <> suffix
|
22
|
def encode_col(num, suffix) do
|
20
23
|
mod = div(num, 26)
|
21
24
|
rem = rem(num, 26)
|
22
25
|
|
23
26
|
if rem == 0 do
|
24
|
- encode_col(mod - 1) <> encode_col(26)
|
27
|
encode_col(mod - 1, "Z" <> suffix)
|
25
28
|
else
|
26
|
- encode_col(mod) <> encode_col(rem)
|
29
|
encode_col(mod, <<rem 64>> <> suffix)
|
27
30
|
end
|
28
31
|
end
|
29
32
|
|
|
@@ -40,29 43,22 @@ defmodule Elixlsx.Util do
|
40
43
|
|
41
44
|
"""
|
42
45
|
@spec decode_col(list(char()) | String.t) :: non_neg_integer
|
43
|
- def decode_col s do
|
44
|
- cond do
|
45
|
- is_list s -> decode_col(to_string s)
|
46
|
- String.valid? s -> decode_col_ s
|
47
|
- true -> raise %ArgumentError{message: "decode_col expects string or charlist, got "
|
48
|
- <> inspect s}
|
46
|
def decode_col(s) when is_list(s), do: decode_col(to_string s)
|
47
|
def decode_col(""), do: 0
|
48
|
def decode_col(s) when is_binary(s) do
|
49
|
case String.match? s, ~r/^[A-Z]*$/ do
|
50
|
false ->
|
51
|
raise %ArgumentError{message: "Invalid column string: " <> inspect s}
|
52
|
|
53
|
true ->
|
54
|
# translate list of strings to the base-26 value they represent
|
55
|
Enum.map(String.to_charlist(s), (fn x -> :string.chr(@col_alphabet, x) end)) |>
|
56
|
# multiply and aggregate them
|
57
|
List.foldl(0, (fn (x, acc) -> x 26 * acc end))
|
49
58
|
end
|
50
59
|
end
|
51
|
-
|
52
|
-
|
53
|
- @spec decode_col_(String.t) :: non_neg_integer
|
54
|
- defp decode_col_("") do 0 end
|
55
|
- defp decode_col_(s) do
|
56
|
- alphabet_list = String.to_charlist @col_alphabet
|
57
|
-
|
58
|
- if !String.match? s, ~r/^[A-Z]*$/ do
|
59
|
- raise %ArgumentError{message: "Invalid column string: " <> inspect s}
|
60
|
- end
|
61
|
-
|
62
|
- # translate list of strings to the base-26 value they represent
|
63
|
- Enum.map(String.to_charlist(s), (fn x -> :string.chr(alphabet_list, x) end)) |>
|
64
|
- # multiply and aggregate them
|
65
|
- List.foldl(0, (fn (x, acc) -> x 26 * acc end))
|
60
|
def decode_col(s) do
|
61
|
raise %ArgumentError{message: "decode_col expects string or charlist, got " <> inspect s}
|
66
62
|
end
|
67
63
|
|
68
64
|
|
|
@@ -167,7 163,7 @@ defmodule Elixlsx.Util do
|
167
163
|
iso_from_datetime(:calendar.now_to_universal_time({div(input, 1000000), rem(input, 1000000), 0}))
|
168
164
|
# TODO this case should parse the string i guess
|
169
165
|
# TODO also prominently absent: [char].
|
170
|
- String.valid? input ->
|
166
|
XML.valid? input ->
|
171
167
|
input
|
172
168
|
true -> raise "Invalid input to iso_timestamp." <> (inspect input)
|
173
169
|
end
|
|
@@ -252,4 248,3 @@ defmodule Elixlsx.Util do
|
252
248
|
String.replace(@version, ~r/(\d )\.(\d )\.(\d )/, "\\1.\\2\\3")
|
253
249
|
end
|
254
250
|
end
|
255
|
-
|
added
lib/elixlsx/xml.ex
|
@@ -0,0 1,19 @@
|
1
|
defmodule XML do
|
2
|
@xml_chars_block_1 [9, 10, 13]
|
3
|
@xml_chars_block_2 32..55_295
|
4
|
@xml_chars_block_3 57_344..65_533
|
5
|
@xml_chars_block_4 65_536..1_114_111
|
6
|
|
7
|
# From the xml spec 1.0: https://www.w3.org/TR/REC-xml/#charsets
|
8
|
# Character Range
|
9
|
# any Unicode character, excluding the surrogate blocks, FFFE, and FFFF.
|
10
|
# Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
|
11
|
def valid?(<<h::utf8, t::binary>>)
|
12
|
when h in @xml_chars_block_1 or h in @xml_chars_block_2
|
13
|
or h in @xml_chars_block_3 or h in @xml_chars_block_4 do
|
14
|
valid?(t)
|
15
|
end
|
16
|
|
17
|
def valid?(<<>>), do: true
|
18
|
def valid?(_), do: false
|
19
|
end
|
changed
lib/elixlsx/xml_templates.ex
|
@@ -142,24 142,18 @@ defmodule Elixlsx.XMLTemplates do
|
142
142
|
### xl/worksheet/sheet*.xml
|
143
143
|
###
|
144
144
|
|
145
|
- defp split_into_content_style(cell, wci) do
|
146
|
- cond do
|
147
|
- is_list(cell) ->
|
148
|
- cellstyle = CellStyle.from_props (tl cell)
|
149
|
- {
|
150
|
- hd(cell),
|
151
|
- CellStyleDB.get_id(wci.cellstyledb, cellstyle),
|
152
|
- cellstyle
|
153
|
- }
|
154
|
- true ->
|
155
|
- {
|
156
|
- cell,
|
157
|
- 0,
|
158
|
- nil
|
159
|
- }
|
160
|
- end
|
145
|
defp split_into_content_style([h | t], wci) do
|
146
|
cellstyle = CellStyle.from_props(t)
|
147
|
|
148
|
{
|
149
|
h,
|
150
|
CellStyleDB.get_id(wci.cellstyledb, cellstyle),
|
151
|
cellstyle
|
152
|
}
|
161
153
|
end
|
162
154
|
|
155
|
defp split_into_content_style(cell, _wci), do: {cell, 0, nil}
|
156
|
|
163
157
|
defp get_content_type_value(content, wci) do
|
164
158
|
case content do
|
165
159
|
{:excelts, num} ->
|
|
@@ -171,7 165,13 @@ defmodule Elixlsx.XMLTemplates do
|
171
165
|
x when is_number(x) ->
|
172
166
|
{"n", to_string(x)}
|
173
167
|
x when is_binary(x) ->
|
174
|
- {"s", to_string(StringDB.get_id wci.stringdb, x)}
|
168
|
id = StringDB.get_id(wci.stringdb, x)
|
169
|
|
170
|
if id == -1 do
|
171
|
{:empty, :empty}
|
172
|
else
|
173
|
{"s", to_string(id)}
|
174
|
end
|
175
175
|
x when is_boolean(x) ->
|
176
176
|
{"b", if x do "1" else "0" end}
|
177
177
|
:empty ->
|
|
@@ -184,59 184,76 @@ defmodule Elixlsx.XMLTemplates do
|
184
184
|
|
185
185
|
# TODO i know now about string interpolation, i should probably clean this up. ;)
|
186
186
|
defp xl_sheet_cols(row, rowidx, wci) do
|
187
|
- Enum.zip(row, 1 .. length row) |>
|
188
|
- Enum.map(
|
189
|
- fn {cell, colidx} ->
|
187
|
{updated_row, _id} =
|
188
|
row
|
189
|
|> List.foldl({"", 1}, fn cell, {acc, colidx} ->
|
190
190
|
{content, styleID, cellstyle} = split_into_content_style(cell, wci)
|
191
|
|
191
192
|
if is_nil(content) do
|
192
|
- ""
|
193
|
{acc, colidx 1}
|
193
194
|
else
|
194
|
- content = if CellStyle.is_date? cellstyle do
|
195
|
- U.to_excel_datetime content
|
196
|
- else
|
197
|
- content
|
198
|
- end
|
195
|
content =
|
196
|
if CellStyle.is_date?(cellstyle) do
|
197
|
U.to_excel_datetime(content)
|
198
|
else
|
199
|
content
|
200
|
end
|
199
201
|
|
200
202
|
cv = get_content_type_value(content, wci)
|
203
|
|
201
204
|
{content_type, content_value, content_opts} =
|
202
|
- case cv do
|
203
|
- {t, v} -> {t, v, []}
|
204
|
- {t, v, opts} -> {t, v, opts}
|
205
|
- :error -> raise %ArgumentError{
|
206
|
- message: "Invalid column content at " <>
|
207
|
- U.to_excel_coords(rowidx, colidx) <> ": "
|
208
|
- <> (inspect content)
|
209
|
- }
|
210
|
- end
|
205
|
case cv do
|
206
|
{t, v} ->
|
207
|
{t, v, []}
|
211
208
|
|
212
|
- case content_type do
|
213
|
- :formula ->
|
214
|
- value = if not is_nil(content_opts[:value]), do: "<v>#{content_opts[:value]}</v>", else: ""
|
209
|
{t, v, opts} ->
|
210
|
{t, v, opts}
|
215
211
|
|
216
|
- """
|
217
|
- <c r="#{U.to_excel_coords(rowidx, colidx)}"
|
218
|
- s="#{styleID}">
|
219
|
- <f>#{content_value}</f>
|
220
|
- #{value}
|
221
|
- </c>
|
222
|
- """
|
223
|
- :empty ->
|
224
|
- """
|
225
|
- <c r="#{U.to_excel_coords(rowidx, colidx)}"
|
226
|
- s="#{styleID}">
|
227
|
- </c>
|
228
|
- """
|
229
|
- type ->
|
230
|
- """
|
231
|
- <c r="#{U.to_excel_coords(rowidx, colidx)}"
|
232
|
- s="#{styleID}" t="#{type}">
|
233
|
- <v>#{content_value}</v>
|
234
|
- </c>
|
235
|
- """
|
236
|
- end
|
237
|
- end
|
238
|
- end) |>
|
239
|
- List.foldr("", &<>/2)
|
212
|
:error ->
|
213
|
raise %ArgumentError{
|
214
|
message:
|
215
|
"Invalid column content at " <>
|
216
|
U.to_excel_coords(rowidx, colidx) <> ": " <> inspect(content)
|
217
|
}
|
218
|
end
|
219
|
|
220
|
cell_xml =
|
221
|
case content_type do
|
222
|
:formula ->
|
223
|
value =
|
224
|
if not is_nil(content_opts[:value]),
|
225
|
do: "<v>#{content_opts[:value]}</v>",
|
226
|
else: ""
|
227
|
|
228
|
"""
|
229
|
<c r="#{U.to_excel_coords(rowidx, colidx)}"
|
230
|
s="#{styleID}">
|
231
|
<f>#{content_value}</f>
|
232
|
#{value}
|
233
|
</c>
|
234
|
"""
|
235
|
|
236
|
:empty ->
|
237
|
"""
|
238
|
<c r="#{U.to_excel_coords(rowidx, colidx)}"
|
239
|
s="#{styleID}">
|
240
|
</c>
|
241
|
"""
|
242
|
|
243
|
type ->
|
244
|
"""
|
245
|
<c r="#{U.to_excel_coords(rowidx, colidx)}"
|
246
|
s="#{styleID}" t="#{type}">
|
247
|
<v>#{content_value}</v>
|
248
|
</c>
|
249
|
"""
|
250
|
end
|
251
|
|
252
|
{acc <> cell_xml, colidx 1}
|
253
|
end
|
254
|
end)
|
255
|
|
256
|
updated_row
|
240
257
|
end
|
241
258
|
|
242
259
|
defp xl_merge_cells([]) do
|
|
@@ -253,14 270,23 @@ defmodule Elixlsx.XMLTemplates do
|
253
270
|
"""
|
254
271
|
end
|
255
272
|
|
256
|
- defp xl_sheet_rows(data, row_heights, wci) do
|
257
|
- Enum.zip(data, 1 .. length data) |>
|
258
|
- Enum.map_join(fn {row, rowidx} ->
|
273
|
defp xl_sheet_rows(data, row_heights, grouping_info, wci) do
|
274
|
rows =
|
275
|
Enum.zip(data, 1 .. length data) |>
|
276
|
Enum.map_join(fn {row, rowidx} ->
|
259
277
|
"""
|
260
|
- <row r="#{rowidx}" #{get_row_height_attr(row_heights, rowidx)}>
|
278
|
<row r="#{rowidx}" #{get_row_height_attr(row_heights, rowidx)}#{get_row_grouping_attr(grouping_info, rowidx)}>
|
261
279
|
#{xl_sheet_cols(row, rowidx, wci)}
|
262
280
|
</row>
|
263
281
|
""" end)
|
282
|
|
283
|
if (length(data) 1) in grouping_info.collapsed_idxs do
|
284
|
rows <> """
|
285
|
<row r="#{length(data) 1}" collapsed="1"></row>
|
286
|
"""
|
287
|
else
|
288
|
rows
|
289
|
end
|
264
290
|
end
|
265
291
|
|
266
292
|
defp get_row_height_attr(row_heights, rowidx) do
|
|
@@ -272,15 298,85 @@ defmodule Elixlsx.XMLTemplates do
|
272
298
|
end
|
273
299
|
end
|
274
300
|
|
275
|
- defp make_col_width({k, v}) do
|
276
|
- '<col min="#{k}" max="#{k}" width="#{v}" customWidth="1" />'
|
301
|
defp get_row_grouping_attr(gr_info, rowidx) do
|
302
|
outline_level = Map.get(gr_info.outline_lvs, rowidx)
|
303
|
(if outline_level, do: " outlineLevel=\"#{outline_level}\"", else: "")
|
304
|
<>
|
305
|
(if rowidx in gr_info.hidden_idxs, do: " hidden=\"1\"", else: "")
|
306
|
<>
|
307
|
(if rowidx in gr_info.collapsed_idxs, do: " collapsed=\"1\"", else: "")
|
277
308
|
end
|
278
309
|
|
279
|
- defp make_col_widths(sheet) do
|
280
|
- if Kernel.map_size(sheet.col_widths) != 0 do
|
281
|
- cols = Map.to_list(sheet.col_widths)
|
282
|
- |> Enum.sort
|
283
|
- |> Enum.map_join(&make_col_width/1)
|
310
|
@typep grouping_info :: %{
|
311
|
outline_lvs: %{optional(idx :: pos_integer) => lv :: pos_integer},
|
312
|
hidden_idxs: MapSet.t(pos_integer),
|
313
|
collapsed_idxs: MapSet.t(pos_integer)
|
314
|
}
|
315
|
@spec get_grouping_info([Sheet.rowcol_group]) :: grouping_info
|
316
|
defp get_grouping_info(groups) do
|
317
|
ranges =
|
318
|
Enum.map(groups, fn
|
319
|
{%Range{} = range, _opts} -> range
|
320
|
%Range{} = range -> range
|
321
|
end)
|
322
|
|
323
|
collapsed_ranges =
|
324
|
groups
|
325
|
|> Enum.filter(fn
|
326
|
{%Range{} = _range, opts} -> opts[:collapsed]
|
327
|
%Range{} = _range -> false
|
328
|
end)
|
329
|
|> Enum.map(fn {range, _opts} -> range end)
|
330
|
|
331
|
# see ECMA Office Open XML Part1, 18.3.1.73 Row -> attributes -> collapsed for examples
|
332
|
%{
|
333
|
outline_lvs:
|
334
|
ranges
|
335
|
|> Stream.concat()
|
336
|
|> Enum.group_by(& &1)
|
337
|
|> Map.new(fn {k, v} -> {k, length(v)} end),
|
338
|
hidden_idxs:
|
339
|
collapsed_ranges |> Stream.concat() |> MapSet.new(),
|
340
|
collapsed_idxs:
|
341
|
collapsed_ranges |> Enum.map(& &1.last 1) |> MapSet.new()
|
342
|
}
|
343
|
end
|
344
|
|
345
|
defp make_col({k, width, outline_level, hidden, collapsed}) do
|
346
|
width_attr =
|
347
|
if width, do: " width=\"#{width}\" customWidth=\"1\"", else: ""
|
348
|
hidden_attr = if hidden, do: " hidden=\"1\"", else: ""
|
349
|
outline_level_attr =
|
350
|
if outline_level, do: " outlineLevel=\"#{outline_level}\"", else: ""
|
351
|
collapsed_attr =
|
352
|
if collapsed, do: " collapsed=\"1\"", else: ""
|
353
|
|
354
|
'<col min="#{k}" max="#{k}"#{width_attr}#{hidden_attr}#{outline_level_attr}#{collapsed_attr} />'
|
355
|
end
|
356
|
|
357
|
defp make_cols(sheet) do
|
358
|
grouping_info = get_grouping_info(sheet.group_cols)
|
359
|
col_indices =
|
360
|
Stream.concat([
|
361
|
Map.keys(sheet.col_widths),
|
362
|
Map.keys(grouping_info.outline_lvs),
|
363
|
grouping_info.hidden_idxs,
|
364
|
grouping_info.collapsed_idxs
|
365
|
])
|
366
|
|> Enum.sort()
|
367
|
|> Enum.dedup()
|
368
|
|
369
|
unless Enum.empty?(col_indices) do
|
370
|
cols =
|
371
|
col_indices
|
372
|
|> Stream.map(&({
|
373
|
&1,
|
374
|
Map.get(sheet.col_widths, &1),
|
375
|
Map.get(grouping_info.outline_lvs, &1),
|
376
|
&1 in grouping_info.hidden_idxs,
|
377
|
&1 in grouping_info.collapsed_idxs
|
378
|
}))
|
379
|
|> Enum.map_join(&make_col/1)
|
284
380
|
|
285
381
|
"<cols>#{cols}</cols>"
|
286
382
|
else
|
|
@@ -288,11 384,25 @@ defmodule Elixlsx.XMLTemplates do
|
288
384
|
end
|
289
385
|
end
|
290
386
|
|
387
|
defp make_max_outline_level_row(row_outline_levels) do
|
388
|
unless row_outline_levels === %{} do
|
389
|
max_outline_level_row =
|
390
|
Map.values(row_outline_levels)
|
391
|
|> Enum.max()
|
392
|
|
393
|
" outlineLevelRow=\"#{max_outline_level_row}\""
|
394
|
else
|
395
|
""
|
396
|
end
|
397
|
end
|
398
|
|
291
399
|
@spec make_sheet(Sheet.t, WorkbookCompInfo.t) :: String.t
|
292
400
|
@doc ~S"""
|
293
401
|
Returns the XML content for single sheet.
|
294
402
|
"""
|
295
403
|
def make_sheet(sheet, wci) do
|
404
|
grouping_info = get_grouping_info(sheet.group_rows)
|
405
|
|
296
406
|
~S"""
|
297
407
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
298
408
|
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
|
@@ -311,14 421,18 @@ defmodule Elixlsx.XMLTemplates do
|
311
421
|
"""
|
312
422
|
</sheetView>
|
313
423
|
</sheetViews>
|
314
|
- <sheetFormatPr defaultRowHeight="12.8"/>
|
424
|
<sheetFormatPr defaultRowHeight="12.8"
|
315
425
|
"""
|
316
|
- <> make_col_widths(sheet) <>
|
426
|
<> make_max_outline_level_row(grouping_info.outline_lvs) <>
|
427
|
"""
|
428
|
/>
|
429
|
"""
|
430
|
<> make_cols(sheet) <>
|
317
431
|
"""
|
318
432
|
<sheetData>
|
319
433
|
"""
|
320
434
|
<>
|
321
|
- xl_sheet_rows(sheet.rows, sheet.row_heights, wci)
|
435
|
xl_sheet_rows(sheet.rows, sheet.row_heights, grouping_info, wci)
|
322
436
|
<>
|
323
437
|
~S"""
|
324
438
|
</sheetData>
|
changed
mix.exs
|
@@ -3,7 3,7 @@ defmodule Elixlsx.Mixfile do
|
3
3
|
|
4
4
|
def project do
|
5
5
|
[app: :elixlsx,
|
6
|
- version: "0.4.1",
|
6
|
version: "0.4.2",
|
7
7
|
elixir: "~> 1.3",
|
8
8
|
package: package(),
|
9
9
|
description: "a writer for XLSX spreadsheet files",
|
|
@@ -19,7 19,7 @@ defmodule Elixlsx.Mixfile do
|
19
19
|
defp deps do
|
20
20
|
[
|
21
21
|
{:excheck, "~> 0.5", only: :test},
|
22
|
- {:triq, git: "https://gitlab.com/triq/triq.git", ref: "2c497398e020e06db8496f1d89f12481cc5adab9", only: :test},
|
22
|
{:triq, "~> 1.0", only: :test},
|
23
23
|
{:credo, "~> 0.5", only: [:dev, :test]},
|
24
24
|
{:ex_doc, "~> 0.19", only: [:dev]},
|
25
25
|
{:dialyxir, "~> 0.5", only: [:dev], runtime: false}
|