to_spreadsheet 1.0.0.rc2 → 1.0.0.rc3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.textile +8 -15
- data/Rakefile +2 -1
- data/lib/to_spreadsheet.rb +1 -3
- data/lib/to_spreadsheet/action_pack_renderers.rb +6 -2
- data/lib/to_spreadsheet/context.rb +69 -84
- data/lib/to_spreadsheet/context/pairing.rb +7 -3
- data/lib/to_spreadsheet/helpers.rb +1 -6
- data/lib/to_spreadsheet/renderer.rb +58 -0
- data/lib/to_spreadsheet/rule.rb +9 -0
- data/lib/to_spreadsheet/rule/base.rb +38 -0
- data/lib/to_spreadsheet/rule/container.rb +25 -0
- data/lib/to_spreadsheet/rule/default_value.rb +19 -0
- data/lib/to_spreadsheet/rule/format.rb +64 -0
- data/lib/to_spreadsheet/rule/sheet.rb +18 -0
- data/lib/to_spreadsheet/rule/workbook.rb +10 -0
- data/lib/to_spreadsheet/selectors.rb +89 -0
- data/lib/to_spreadsheet/themes/default.rb +3 -2
- data/lib/to_spreadsheet/type_from_value.rb +19 -0
- data/lib/to_spreadsheet/version.rb +1 -1
- data/spec/defaults_spec.rb +1 -1
- data/spec/format_spec.rb +1 -1
- data/spec/worksheets_spec.rb +1 -1
- metadata +29 -7
- data/lib/to_spreadsheet/axlsx/formatter.rb +0 -86
- data/lib/to_spreadsheet/axlsx/renderer.rb +0 -52
- data/lib/to_spreadsheet/formats.rb +0 -56
- data/lib/to_spreadsheet/xlsx.rb +0 -0
data/README.textile
CHANGED
@@ -14,10 +14,10 @@ bc. # my_thingies_controller.rb
|
|
14
14
|
class MyThingiesController < ApplicationController
|
15
15
|
respond_to :xls, :html
|
16
16
|
def index
|
17
|
-
@
|
17
|
+
@my_items = MyItem.all
|
18
18
|
respond_to do |format|
|
19
|
-
format.html
|
20
|
-
format.xlsx { render xlsx: :index, filename: "
|
19
|
+
format.html
|
20
|
+
format.xlsx { render xlsx: :index, filename: "my_items_doc" }
|
21
21
|
end
|
22
22
|
end
|
23
23
|
end
|
@@ -51,6 +51,11 @@ bc. # index.html.haml
|
|
51
51
|
= link_to 'Download spreadsheet', my_items_url(format: :xlsx)
|
52
52
|
= render 'my_items', my_items: @my_items
|
53
53
|
|
54
|
+
h3. Worksheets
|
55
|
+
|
56
|
+
Every table in the view will be converted to a separate sheet.
|
57
|
+
The sheet title will be assigned to the value of the table's caption element if it exists.
|
58
|
+
|
54
59
|
h3. Formatting
|
55
60
|
|
56
61
|
You can define formats in your view file (local to the view) or in the initializer
|
@@ -91,15 +96,3 @@ Here is the list of class to type mapping:
|
|
91
96
|
| datetime | DateTime (Chronic.parse) |
|
92
97
|
| date | Date (Date.parse) |
|
93
98
|
| time | Time (Chronic.parse) |
|
94
|
-
|
95
|
-
h3. Styling
|
96
|
-
|
97
|
-
|
98
|
-
h3. Default values
|
99
|
-
|
100
|
-
Add a `data-default="default value"` attribute to a cell to use the value as a default if the model value is nil.
|
101
|
-
|
102
|
-
h3. Worksheets
|
103
|
-
|
104
|
-
Every table in the view will be converted to a separate sheet.
|
105
|
-
The sheet title will be assigned to the value of the table's <caption> element if it exists.
|
data/Rakefile
CHANGED
@@ -13,10 +13,11 @@ task :env do
|
|
13
13
|
include ToSpreadsheet::Helpers
|
14
14
|
end
|
15
15
|
|
16
|
+
desc 'Generate a simple xlsx file'
|
16
17
|
task :write_test_xlsx => :env do
|
17
18
|
require 'haml'
|
18
19
|
path = '/tmp/spreadsheet.xlsx'
|
19
20
|
html = Haml::Engine.new(File.read('spec/support/table.html.haml')).render
|
20
|
-
ToSpreadsheet::
|
21
|
+
ToSpreadsheet::Renderer.to_package(html).serialize(path)
|
21
22
|
puts "Written to #{path}"
|
22
23
|
end
|
data/lib/to_spreadsheet.rb
CHANGED
@@ -7,8 +7,6 @@ require 'to_spreadsheet/context'
|
|
7
7
|
|
8
8
|
module ToSpreadsheet
|
9
9
|
class << self
|
10
|
-
attr_accessor :context
|
11
|
-
|
12
10
|
def theme(name, &formats)
|
13
11
|
@themes ||= {}
|
14
12
|
if formats
|
@@ -21,4 +19,4 @@ module ToSpreadsheet
|
|
21
19
|
end
|
22
20
|
|
23
21
|
require 'to_spreadsheet/themes/default'
|
24
|
-
ToSpreadsheet
|
22
|
+
ToSpreadsheet::Context.global.format_xls ToSpreadsheet.theme(:default)
|
@@ -2,14 +2,18 @@ require 'active_support'
|
|
2
2
|
require 'action_controller/metal/renderers'
|
3
3
|
require 'action_controller/metal/responder'
|
4
4
|
|
5
|
-
require 'to_spreadsheet/
|
5
|
+
require 'to_spreadsheet/renderer'
|
6
6
|
|
7
7
|
# This will let us do thing like `render :xlsx => 'index'`
|
8
8
|
# This is similar to how Rails internally implements its :json and :xml renderers
|
9
9
|
ActionController::Renderers.add :xlsx do |template, options|
|
10
10
|
filename = options[:filename] || options[:template] || 'data'
|
11
11
|
|
12
|
-
html = with_context ToSpreadsheet.
|
12
|
+
html = with_context ToSpreadsheet::Context.global.merge(ToSpreadsheet::Context.new) do
|
13
|
+
# local context
|
14
|
+
@local_formats.each do |selector, &block|
|
15
|
+
context.process_dsl selector, &block
|
16
|
+
end if @local_formats
|
13
17
|
render_to_string(options[:template], options)
|
14
18
|
end
|
15
19
|
|
@@ -1,63 +1,53 @@
|
|
1
1
|
require 'to_spreadsheet/context/pairing'
|
2
|
-
require 'to_spreadsheet/
|
2
|
+
require 'to_spreadsheet/rule'
|
3
|
+
require 'to_spreadsheet/rule/base'
|
4
|
+
require 'to_spreadsheet/rule/container'
|
5
|
+
require 'to_spreadsheet/rule/format'
|
6
|
+
require 'to_spreadsheet/rule/default_value'
|
7
|
+
require 'to_spreadsheet/rule/sheet'
|
8
|
+
require 'to_spreadsheet/rule/workbook'
|
3
9
|
|
4
10
|
module ToSpreadsheet
|
11
|
+
# This is the DSL context for `format_xls`
|
12
|
+
# It maintains the current formats set to enable for local and nested `format_xls` blocks
|
5
13
|
class Context
|
6
14
|
include Pairing
|
15
|
+
attr_accessor :rules
|
7
16
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
workbook wb_options if wb_options
|
12
|
-
end
|
13
|
-
|
14
|
-
# Returns a new formats jar for a given sheet
|
15
|
-
def formats(sheet)
|
16
|
-
format = Formats.new
|
17
|
-
@formats.each do |v|
|
18
|
-
sel, fmt = v[0], v[1]
|
19
|
-
format.merge!(fmt) if selects?(sel, sheet)
|
17
|
+
class << self
|
18
|
+
def global
|
19
|
+
@global ||= new
|
20
20
|
end
|
21
|
-
format
|
22
21
|
end
|
23
22
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
type, val = selector[0], selector[1]
|
28
|
-
sheet = entity.is_a?(::Axlsx::Workbook) ? entity : (entity.respond_to?(:workbook) ? entity.workbook : entity.worksheet.workbook)
|
29
|
-
doc = node_from_entity(sheet)
|
30
|
-
case type
|
31
|
-
when :css
|
32
|
-
doc.css(val).include?(node_from_entity(entity))
|
33
|
-
when :column
|
34
|
-
return false if entity.is_a?(Axlsx::Row)
|
35
|
-
entity.index == val if entity.is_a?(Axlsx::Cell)
|
36
|
-
when :row
|
37
|
-
return entity.index == val if entity.is_a?(Axlsx::Row)
|
38
|
-
entity.row.index == val if entity.is_a?(Axlsx::Cell)
|
39
|
-
when :range
|
40
|
-
if entity.is_a?(Axlsx::Cell)
|
41
|
-
pos = entity.pos
|
42
|
-
top_left, bot_right = val.split(':').map { |s| Axlsx.name_to_indices(s) }
|
43
|
-
pos[0] >= top_left[0] && pos[0] <= bot_right[0] && pos[1] >= top_left[1] && pos[1] <= bot_right[1]
|
44
|
-
end
|
45
|
-
end
|
23
|
+
def initialize(wb_options = nil)
|
24
|
+
@rules = []
|
25
|
+
workbook wb_options if wb_options
|
46
26
|
end
|
47
27
|
|
48
|
-
#
|
49
|
-
|
50
|
-
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
28
|
+
# Examples:
|
29
|
+
# format_xls 'table.zebra' do
|
30
|
+
# format 'td', lambda { |cell| {b: true} if cell.row.even? }
|
31
|
+
# end
|
32
|
+
# format_xls ToSpreadsheet.theme(:a_theme)
|
33
|
+
# format_xls 'table.zebra', ToSpreadsheet.theme(:zebra)
|
54
34
|
def format_xls(selector = nil, theme = nil, &block)
|
55
35
|
selector, theme = nil, selector if selector.is_a?(Proc) && !theme
|
56
|
-
|
57
|
-
|
36
|
+
process_dsl(selector, &theme) if theme
|
37
|
+
process_dsl(selector, &block) if block
|
58
38
|
self
|
59
39
|
end
|
60
40
|
|
41
|
+
def process_dsl(selector, &block)
|
42
|
+
@rule_container = add_rule :container, *selector_query(selector)
|
43
|
+
instance_eval(&block)
|
44
|
+
@rule_container = nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def workbook(selector = nil, value)
|
48
|
+
add_rule :workbook, *selector_query(selector), value
|
49
|
+
end
|
50
|
+
|
61
51
|
# format 'td.b', b: true # bold
|
62
52
|
# format column: 0, width: 50
|
63
53
|
# format 'A1:C30', b: true
|
@@ -65,66 +55,61 @@ module ToSpreadsheet
|
|
65
55
|
# column format also accepts Axlsx columnInfo settings
|
66
56
|
def format(selector = nil, options)
|
67
57
|
options = options.dup
|
68
|
-
selector =
|
69
|
-
|
58
|
+
selector = selector_query(selector, options)
|
59
|
+
add_rule :format, *selector, options
|
70
60
|
end
|
71
61
|
|
72
62
|
# sheet 'table.landscape', page_setup: { orientation: landscape }
|
73
63
|
def sheet(selector = nil, options)
|
74
64
|
options = options.dup
|
75
|
-
selector =
|
76
|
-
|
65
|
+
selector = selector_query(selector, options)
|
66
|
+
add_rule :sheet, *selector, options
|
77
67
|
end
|
78
68
|
|
79
69
|
# default 'td.c', 5
|
80
70
|
def default(selector, value)
|
81
|
-
|
82
|
-
|
83
|
-
add selector[0], selector, options
|
84
|
-
end
|
85
|
-
|
86
|
-
def add(setting, selector, value)
|
87
|
-
@current_format[setting] << [selector.try(:[], 1), value] if selector || value
|
88
|
-
end
|
89
|
-
|
90
|
-
def workbook(selector = nil, value)
|
91
|
-
add :package, selector, value
|
71
|
+
selector = selector_query(selector)
|
72
|
+
add_rule :default_value, *selector, value
|
92
73
|
end
|
93
74
|
|
94
|
-
def
|
95
|
-
|
96
|
-
|
97
|
-
|
75
|
+
def add_rule(rule_type, selector_type, selector_value, options = {})
|
76
|
+
rule = ToSpreadsheet::Rule.make(rule_type, selector_type, selector_value, options)
|
77
|
+
if @rule_container
|
78
|
+
@rule_container.children << rule
|
79
|
+
else
|
80
|
+
@rules << rule
|
81
|
+
end
|
82
|
+
rule
|
98
83
|
end
|
99
84
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
85
|
+
# A new context
|
86
|
+
def merge(other_context)
|
87
|
+
ctx = Context.new()
|
88
|
+
ctx.rules = rules + other_context.rules
|
89
|
+
ctx
|
104
90
|
end
|
105
91
|
|
106
92
|
private
|
107
93
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
def
|
117
|
-
if
|
118
|
-
if
|
119
|
-
return [:range,
|
94
|
+
# Extract selector query from DSL arguments
|
95
|
+
#
|
96
|
+
# Figures out text type:
|
97
|
+
# selector_query('td.num') # [:css, "td.num"]
|
98
|
+
# selector_query('A0:B5') # [:range, "A0:B5"]
|
99
|
+
#
|
100
|
+
# If text is nil, extracts first of row, range, and css keys
|
101
|
+
# selector_query(nil, {column: 0}] # [:column, 0]
|
102
|
+
def selector_query(text, opts = {})
|
103
|
+
if text
|
104
|
+
if text =~ /:/ && text[0].upcase == text[0]
|
105
|
+
return [:range, text]
|
120
106
|
else
|
121
|
-
return [:css,
|
107
|
+
return [:css, text]
|
122
108
|
end
|
123
109
|
end
|
124
|
-
[:column, :row, :range].
|
125
|
-
|
126
|
-
|
127
|
-
selector
|
110
|
+
key = [:column, :row, :range].detect { |key| opts.key?(key) }
|
111
|
+
return [key, opts.delete(key)] if key
|
112
|
+
[nil, nil]
|
128
113
|
end
|
129
114
|
end
|
130
115
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module ToSpreadsheet
|
2
2
|
class Context
|
3
|
-
#
|
3
|
+
# Axlsx classes <-> Nokogiri table nodes round-tripping
|
4
4
|
module Pairing
|
5
5
|
def assoc!(entity, node)
|
6
6
|
@entity_to_node ||= {}
|
@@ -9,14 +9,18 @@ module ToSpreadsheet
|
|
9
9
|
@node_to_entity[node] = entity
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
12
|
+
def to_xls_entity(node)
|
13
13
|
@node_to_entity[node]
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
16
|
+
def to_xml_node(entity)
|
17
17
|
@entity_to_node[entity]
|
18
18
|
end
|
19
19
|
|
20
|
+
def xml_node_and_xls_entity(entity)
|
21
|
+
[@entity_to_node[entity], entity, @node_to_entity[entity]].compact
|
22
|
+
end
|
23
|
+
|
20
24
|
def clear_assoc!
|
21
25
|
@entity_to_node = {}
|
22
26
|
@node_to_entity = {}
|
@@ -1,16 +1,11 @@
|
|
1
1
|
module ToSpreadsheet
|
2
2
|
module Helpers
|
3
|
-
|
4
|
-
def to_spreadsheet(selector, &block)
|
5
|
-
context.apply(block)
|
6
|
-
end
|
7
|
-
|
8
3
|
def format_xls(selector = nil, &block)
|
9
4
|
context.format_xls selector, &block
|
10
5
|
end
|
11
6
|
|
12
7
|
def context
|
13
|
-
@context || ToSpreadsheet.
|
8
|
+
@context || ToSpreadsheet::Context.global
|
14
9
|
end
|
15
10
|
|
16
11
|
def with_context(context, &block)
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'axlsx'
|
2
|
+
module ToSpreadsheet
|
3
|
+
module Renderer
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def to_stream(html, local_context = nil)
|
7
|
+
to_package(html, local_context).to_stream
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_data(html, local_context = nil)
|
11
|
+
to_package(html, local_context).to_stream.read
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_package(html, local_context = nil)
|
15
|
+
with_context init_context(local_context) do
|
16
|
+
package = build_package(html, context)
|
17
|
+
context.rules.each do |rule|
|
18
|
+
puts "Applying #{rule}"
|
19
|
+
rule.apply(context, package)
|
20
|
+
end
|
21
|
+
package
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def init_context(local_context)
|
28
|
+
local_context ||= ToSpreadsheet::Context.new
|
29
|
+
ToSpreadsheet::Context.global.merge local_context
|
30
|
+
end
|
31
|
+
|
32
|
+
def build_package(html, context)
|
33
|
+
package = ::Axlsx::Package.new
|
34
|
+
spreadsheet = package.workbook
|
35
|
+
doc = Nokogiri::HTML::Document.parse(html)
|
36
|
+
# Workbook <-> %document association
|
37
|
+
context.assoc! spreadsheet, doc
|
38
|
+
doc.css('table').each_with_index do |xml_table, i|
|
39
|
+
sheet = spreadsheet.add_worksheet(
|
40
|
+
name: xml_table.css('caption').inner_text.presence || xml_table['name'] || "Sheet #{i + 1}"
|
41
|
+
)
|
42
|
+
# Sheet <-> %table association
|
43
|
+
context.assoc! sheet, xml_table
|
44
|
+
xml_table.css('tr').each do |row_node|
|
45
|
+
xls_row = sheet.add_row
|
46
|
+
# Row <-> %tr association
|
47
|
+
context.assoc! xls_row, row_node
|
48
|
+
row_node.css('th,td').each do |cell_node|
|
49
|
+
xls_col = xls_row.add_cell cell_node.inner_text
|
50
|
+
# Cell <-> th or td association
|
51
|
+
context.assoc! xls_col, cell_node
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
package
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'active_support/core_ext'
|
2
|
+
module ToSpreadsheet
|
3
|
+
module Rule
|
4
|
+
def self.make(rule_type, selector_type, selector_value, options)
|
5
|
+
klass = "ToSpreadsheet::Rule::#{rule_type.to_s.camelize}".constantize
|
6
|
+
klass.new(selector_type, selector_value, options)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'to_spreadsheet/selectors'
|
2
|
+
module ToSpreadsheet
|
3
|
+
module Rule
|
4
|
+
class Base
|
5
|
+
include ::ToSpreadsheet::Selectors
|
6
|
+
attr_reader :selector_type, :selector_query, :options
|
7
|
+
|
8
|
+
def initialize(selector_type, selector_query, options)
|
9
|
+
@selector_type = selector_type
|
10
|
+
@selector_query = selector_query
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def applies_to?(context, xml_or_xls_node)
|
15
|
+
return true if !selector_type
|
16
|
+
node, entity = context.xml_node_and_xls_entity(xml_or_xls_node)
|
17
|
+
sheet = entity.is_a?(::Axlsx::Workbook) ? entity : (entity.respond_to?(:workbook) ? entity.workbook : entity.worksheet.workbook)
|
18
|
+
doc = context.to_xml_node(sheet)
|
19
|
+
query_match?(
|
20
|
+
selector_type: selector_type,
|
21
|
+
selector_query: selector_query,
|
22
|
+
xml_document: doc,
|
23
|
+
xml_node: node,
|
24
|
+
xls_worksheet: sheet,
|
25
|
+
xls_entity: entity
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def type
|
30
|
+
self.class.name.demodulize.underscore.to_sym
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
"Rule [#{type}, #{selector_type}, #{selector_query}, #{options}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ToSpreadsheet
|
2
|
+
module Rule
|
3
|
+
# Applies children rules to all the matching tables
|
4
|
+
class Container < Base
|
5
|
+
attr_reader :children
|
6
|
+
def initialize(*args)
|
7
|
+
super
|
8
|
+
@children = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def apply(context, package)
|
12
|
+
package.workbook.worksheets.each do |sheet|
|
13
|
+
table = context.to_xml_node(sheet)
|
14
|
+
if applies_to?(context, table)
|
15
|
+
children.each { |c| c.apply(context, sheet) }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
"Rules(#{selector_type}, #{selector_query}) [#{children.map(&:to_s)}]"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'to_spreadsheet/type_from_value'
|
2
|
+
module ToSpreadsheet
|
3
|
+
module Rule
|
4
|
+
class DefaultValue < Base
|
5
|
+
include ::ToSpreadsheet::TypeFromValue
|
6
|
+
|
7
|
+
def apply(context, sheet)
|
8
|
+
default = options
|
9
|
+
each_cell context, sheet, selector_type, selector_query do |cell|
|
10
|
+
unless cell.value.present? &&
|
11
|
+
!([:integer, :float].include?(cell.type) && cell.value.zero?)
|
12
|
+
cell.type = cell_type_from_value(default)
|
13
|
+
cell.value = default
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'to_spreadsheet/type_from_value'
|
2
|
+
require 'set'
|
3
|
+
module ToSpreadsheet
|
4
|
+
module Rule
|
5
|
+
class Format < Base
|
6
|
+
include ::ToSpreadsheet::TypeFromValue
|
7
|
+
def apply(context, sheet)
|
8
|
+
case selector_type
|
9
|
+
when :css
|
10
|
+
css_match selector_query, context.to_xml_node(sheet) do |xml_node|
|
11
|
+
apply_inline_styles context, context.to_xls_entity(xml_node)
|
12
|
+
end
|
13
|
+
when :row
|
14
|
+
sheet.row_style selector_query, options if options.present?
|
15
|
+
when :column
|
16
|
+
inline_styles = options.except(*COL_INFO_PROPS)
|
17
|
+
sheet.col_style selector_query, inline_styles if inline_styles.present?
|
18
|
+
apply_col_info sheet.column_info[selector_query]
|
19
|
+
when :range
|
20
|
+
apply_inline_styles range_match(selector_type, sheet), context
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
COL_INFO_PROPS = %w(bestFit collapsed customWidth hidden phonetic width).map(&:to_sym).to_set
|
26
|
+
def apply_col_info(col_info)
|
27
|
+
return if col_info.nil?
|
28
|
+
options.each do |k, v|
|
29
|
+
if COL_INFO_PROPS.include?(k)
|
30
|
+
col_info.send :"#{k}=", v
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def apply_inline_styles(context, xls_ent)
|
36
|
+
# Custom format rule
|
37
|
+
# format 'td.sel', lambda { |node| ...}
|
38
|
+
if self.options.is_a?(Proc)
|
39
|
+
context.instance_exec(xls_ent, &self.options)
|
40
|
+
return
|
41
|
+
end
|
42
|
+
|
43
|
+
options = self.options.dup
|
44
|
+
# Compute Proc rules
|
45
|
+
# format 'td.sel', color: lambda {|node| ...}
|
46
|
+
options.each do |k, v|
|
47
|
+
options[k] = context.instance_exec(xls_ent, &v) if v.is_a?(Proc)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Apply inline styles
|
51
|
+
options.each do |k, v|
|
52
|
+
next if v.nil?
|
53
|
+
setter = :"#{k}="
|
54
|
+
xls_ent.send setter, v if xls_ent.respond_to?(setter)
|
55
|
+
if xls_ent.respond_to?(:cells)
|
56
|
+
xls_ent.cells.each do |cell|
|
57
|
+
cell.send setter, v if cell.respond_to?(setter)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module ToSpreadsheet
|
2
|
+
module Rule
|
3
|
+
class Sheet < Base
|
4
|
+
def apply(context, sheet)
|
5
|
+
options.each { |k, v|
|
6
|
+
if v.is_a?(Hash)
|
7
|
+
sub = sheet.send(k)
|
8
|
+
v.each do |sub_k, sub_v|
|
9
|
+
sub.send :"#{sub_k}=", sub_v
|
10
|
+
end
|
11
|
+
else
|
12
|
+
sheet.send :"#{k}=", v
|
13
|
+
end
|
14
|
+
}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module ToSpreadsheet
|
2
|
+
# This is the DSL context for `format_xls`
|
3
|
+
# Query types: :css, :column, :row or :range
|
4
|
+
# Query values:
|
5
|
+
# For css: [String] css selector
|
6
|
+
# For column and row: [Fixnum] column/row number
|
7
|
+
# For range: [String] table range, e.g. A4:B5
|
8
|
+
module Selectors
|
9
|
+
# Flexible API query match
|
10
|
+
# Options (all optional):
|
11
|
+
# xls_worksheet
|
12
|
+
# xls_entity
|
13
|
+
# xml_document
|
14
|
+
# xml_node
|
15
|
+
# selector_type :css, :column, :row or :range
|
16
|
+
# selector_query
|
17
|
+
def query_match?(options)
|
18
|
+
return true if !options[:selector_query]
|
19
|
+
case options[:selector_type]
|
20
|
+
when :css
|
21
|
+
css_match? options[:selector_query], options[:xml_document], options[:xml_node]
|
22
|
+
when :column
|
23
|
+
return false unless [Axlsx::Row, Axlsx::Cell].include?(options[:xml_node].class)
|
24
|
+
column_number_match? options[:selector_query], options[:xml_node]
|
25
|
+
when :row
|
26
|
+
return false unless Axlsx::Cell == options[:xml_node].class
|
27
|
+
row_number_match? options[:selector_query], options[:xml_node]
|
28
|
+
when :range
|
29
|
+
return false if entity.is_a?(Axlsx::Cell)
|
30
|
+
range_contains? options[:selector_query], options[:xml_node]
|
31
|
+
else
|
32
|
+
raise "Unsupported type #{options[:selector_type].inspect} (:css, :column, :row or :range expected)"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def each_cell(context, sheet, selector_type, selector_query, &block)
|
37
|
+
if !selector_type
|
38
|
+
sheet.rows.each do |row|
|
39
|
+
sheet.cells.each do |cell|
|
40
|
+
block.(cell)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
return
|
44
|
+
end
|
45
|
+
case selector_type
|
46
|
+
when :css
|
47
|
+
css_match selector_query, context.to_xml_node(sheet) do |xml_node|
|
48
|
+
block.(context.to_xls_entity(xml_node))
|
49
|
+
end
|
50
|
+
when :column
|
51
|
+
sheet.cols[selector_query].cells.each(&block)
|
52
|
+
when :row
|
53
|
+
sheet.cols[selector_query].cells.each(&block)
|
54
|
+
when :range
|
55
|
+
sheet[range].each(&block)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def css_match(css_selector, xml_node, &block)
|
60
|
+
xml_node.css(css_selector).each(&block)
|
61
|
+
end
|
62
|
+
|
63
|
+
def css_match?(css_selector, xml_document, xml_node)
|
64
|
+
xml_document.css(css_selector).include?(xml_node)
|
65
|
+
end
|
66
|
+
|
67
|
+
def row_number_match?(row_number, xls_row_or_cell)
|
68
|
+
if xls_row_or_cell.is_a? Axlsx::Row
|
69
|
+
row_number == xls_row_or_cell.index
|
70
|
+
elsif xls_row_or_cell.is_a? Axlsx::Cell
|
71
|
+
row_number == xls_row_or_cell.row.index
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def column_number_match?(column_number, xls_cell)
|
76
|
+
xls_cell.index == column_number if xls_cell.is_a?(Axlsx::Cell)
|
77
|
+
end
|
78
|
+
|
79
|
+
def range_match(range, xls_sheet)
|
80
|
+
xls_sheet[range]
|
81
|
+
end
|
82
|
+
|
83
|
+
def range_contains?(range, xls_cell)
|
84
|
+
pos = xls_cell.pos
|
85
|
+
top_left, bot_right = range.split(':').map { |s| Axlsx.name_to_indices(s) }
|
86
|
+
pos[0] >= top_left[0] && pos[0] <= bot_right[0] && pos[1] >= top_left[1] && pos[1] <= bot_right[1]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -1,7 +1,8 @@
|
|
1
1
|
module ToSpreadsheet::Themes
|
2
2
|
module Default
|
3
3
|
::ToSpreadsheet.theme :default do
|
4
|
-
workbook use_autowidth: true
|
4
|
+
workbook use_autowidth: true,
|
5
|
+
use_shared_strings: true
|
5
6
|
sheet page_setup: {
|
6
7
|
fit_to_height: 1,
|
7
8
|
fit_to_width: 1,
|
@@ -10,7 +11,7 @@ module ToSpreadsheet::Themes
|
|
10
11
|
# Set value type based on CSS class
|
11
12
|
format 'td,th', lambda { |cell|
|
12
13
|
val = cell.value
|
13
|
-
case
|
14
|
+
case to_xml_node(cell)[:class]
|
14
15
|
when /decimal|float/
|
15
16
|
cell.type = :float
|
16
17
|
when /num|int/
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ToSpreadsheet
|
2
|
+
module TypeFromValue
|
3
|
+
def cell_type_from_value(v)
|
4
|
+
if v.is_a?(Date)
|
5
|
+
:date
|
6
|
+
elsif v.is_a?(Time)
|
7
|
+
:time
|
8
|
+
elsif v.is_a?(TrueClass) || v.is_a?(FalseClass)
|
9
|
+
:boolean
|
10
|
+
elsif v.to_s.match(/\A[+-]?\d+?\Z/) #numeric
|
11
|
+
:integer
|
12
|
+
elsif v.to_s.match(/\A[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?\Z/) #float
|
13
|
+
:float
|
14
|
+
else
|
15
|
+
:string
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/spec/defaults_spec.rb
CHANGED
data/spec/format_spec.rb
CHANGED
data/spec/worksheets_spec.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: to_spreadsheet
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.
|
4
|
+
version: 1.0.0.rc3
|
5
5
|
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-11-
|
12
|
+
date: 2012-11-17 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -59,6 +59,22 @@ dependencies:
|
|
59
59
|
- - ! '>='
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: chronic
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
62
78
|
- !ruby/object:Gem::Dependency
|
63
79
|
name: mocha
|
64
80
|
requirement: !ruby/object:Gem::Requirement
|
@@ -102,16 +118,22 @@ files:
|
|
102
118
|
- LICENSE
|
103
119
|
- Rakefile
|
104
120
|
- lib/to_spreadsheet/action_pack_renderers.rb
|
105
|
-
- lib/to_spreadsheet/axlsx/formatter.rb
|
106
|
-
- lib/to_spreadsheet/axlsx/renderer.rb
|
107
121
|
- lib/to_spreadsheet/context/pairing.rb
|
108
122
|
- lib/to_spreadsheet/context.rb
|
109
|
-
- lib/to_spreadsheet/formats.rb
|
110
123
|
- lib/to_spreadsheet/helpers.rb
|
111
124
|
- lib/to_spreadsheet/mime_types.rb
|
125
|
+
- lib/to_spreadsheet/renderer.rb
|
126
|
+
- lib/to_spreadsheet/rule/base.rb
|
127
|
+
- lib/to_spreadsheet/rule/container.rb
|
128
|
+
- lib/to_spreadsheet/rule/default_value.rb
|
129
|
+
- lib/to_spreadsheet/rule/format.rb
|
130
|
+
- lib/to_spreadsheet/rule/sheet.rb
|
131
|
+
- lib/to_spreadsheet/rule/workbook.rb
|
132
|
+
- lib/to_spreadsheet/rule.rb
|
133
|
+
- lib/to_spreadsheet/selectors.rb
|
112
134
|
- lib/to_spreadsheet/themes/default.rb
|
135
|
+
- lib/to_spreadsheet/type_from_value.rb
|
113
136
|
- lib/to_spreadsheet/version.rb
|
114
|
-
- lib/to_spreadsheet/xlsx.rb
|
115
137
|
- lib/to_spreadsheet.rb
|
116
138
|
- spec/defaults_spec.rb
|
117
139
|
- spec/format_spec.rb
|
@@ -140,7 +162,7 @@ rubyforge_project:
|
|
140
162
|
rubygems_version: 1.8.24
|
141
163
|
signing_key:
|
142
164
|
specification_version: 3
|
143
|
-
summary:
|
165
|
+
summary: Render existing views as Excel documents
|
144
166
|
test_files:
|
145
167
|
- spec/defaults_spec.rb
|
146
168
|
- spec/format_spec.rb
|
@@ -1,86 +0,0 @@
|
|
1
|
-
module ToSpreadsheet
|
2
|
-
module Axlsx
|
3
|
-
module Formatter
|
4
|
-
COL_INFO_PROPS = %w(bestFit collapsed customWidth hidden phonetic width).map(&:to_sym)
|
5
|
-
|
6
|
-
def apply_formats(package, context)
|
7
|
-
package.workbook.worksheets.each do |sheet|
|
8
|
-
fmt = context.formats(sheet)
|
9
|
-
fmt.workbook_props.each do |prop, value|
|
10
|
-
package.send(:"#{prop}=", value)
|
11
|
-
end
|
12
|
-
fmt.sheet_props.each do |set, props|
|
13
|
-
apply_props sheet.send(set), props, context
|
14
|
-
end
|
15
|
-
fmt.column_props.each do |v|
|
16
|
-
idx, props = v[0], v[1]
|
17
|
-
apply_props sheet.column_info[0], props.slice(*COL_INFO_PROPS), context
|
18
|
-
props = props.except(*COL_INFO_PROPS)
|
19
|
-
sheet.col_style idx, props if props.present?
|
20
|
-
end
|
21
|
-
fmt.row_props.each do |v|
|
22
|
-
idx, props = v[0], v[1]
|
23
|
-
sheet.row_style idx, props
|
24
|
-
end
|
25
|
-
fmt.range_props.each do |v|
|
26
|
-
range, props = v[0], v[1]
|
27
|
-
apply_props sheet[range], props, context
|
28
|
-
end
|
29
|
-
fmt.css_props.each do |v|
|
30
|
-
css_sel, props = v[0], v[1]
|
31
|
-
context.node_from_entity(sheet).css(css_sel).each do |node|
|
32
|
-
apply_props context.entity_from_node(node), props, context
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
private
|
39
|
-
def apply_props(obj, props, context)
|
40
|
-
if props.is_a?(Proc)
|
41
|
-
context.instance_exec(obj, &props)
|
42
|
-
return
|
43
|
-
end
|
44
|
-
|
45
|
-
props = props.dup
|
46
|
-
props.each do |k, v|
|
47
|
-
props[k] = context.instance_exec(obj, &v) if v.is_a?(Proc)
|
48
|
-
end
|
49
|
-
|
50
|
-
props.each do |k, v|
|
51
|
-
next if v.nil?
|
52
|
-
if k == :default_value
|
53
|
-
unless obj.value.present? && !([:integer, :float].include?(obj.type) && obj.value.zero?)
|
54
|
-
obj.type = cell_type_from_value(v)
|
55
|
-
obj.value = v
|
56
|
-
end
|
57
|
-
else
|
58
|
-
setter = :"#{k}="
|
59
|
-
obj.send setter, v if obj.respond_to?(setter)
|
60
|
-
if obj.respond_to?(:cells)
|
61
|
-
obj.cells.each do |cell|
|
62
|
-
cell.send setter, v if cell.respond_to?(setter)
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
def cell_type_from_value(v)
|
70
|
-
if v.is_a?(Date)
|
71
|
-
:date
|
72
|
-
elsif v.is_a?(Time)
|
73
|
-
:time
|
74
|
-
elsif v.is_a?(TrueClass) || v.is_a?(FalseClass)
|
75
|
-
:boolean
|
76
|
-
elsif v.to_s.match(/\A[+-]?\d+?\Z/) #numeric
|
77
|
-
:integer
|
78
|
-
elsif v.to_s.match(/\A[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?\Z/) #float
|
79
|
-
:float
|
80
|
-
else
|
81
|
-
:string
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
@@ -1,52 +0,0 @@
|
|
1
|
-
require 'axlsx'
|
2
|
-
require 'to_spreadsheet/axlsx/formatter'
|
3
|
-
module ToSpreadsheet
|
4
|
-
module Axlsx
|
5
|
-
module Renderer
|
6
|
-
include Formatter
|
7
|
-
extend self
|
8
|
-
|
9
|
-
def to_stream(html, context = ToSpreadsheet.context)
|
10
|
-
to_package(html, context).to_stream
|
11
|
-
end
|
12
|
-
|
13
|
-
def to_data(html, context = ToSpreadsheet.context)
|
14
|
-
to_package(html, context).to_stream.read
|
15
|
-
end
|
16
|
-
|
17
|
-
def to_package(html, context = ToSpreadsheet.context)
|
18
|
-
package = build_package(html, context)
|
19
|
-
apply_formats(package, context)
|
20
|
-
# Don't leak memory: clear all dom <-> axslsx associations
|
21
|
-
context.clear_assoc!
|
22
|
-
# Numbers compat
|
23
|
-
package.use_shared_strings = true
|
24
|
-
package
|
25
|
-
end
|
26
|
-
|
27
|
-
private
|
28
|
-
|
29
|
-
def build_package(html, context)
|
30
|
-
package = ::Axlsx::Package.new
|
31
|
-
spreadsheet = package.workbook
|
32
|
-
doc = Nokogiri::HTML::Document.parse(html)
|
33
|
-
context.assoc! spreadsheet, doc
|
34
|
-
doc.css('table').each_with_index do |xml_table, i|
|
35
|
-
sheet = spreadsheet.add_worksheet(
|
36
|
-
name: xml_table.css('caption').inner_text.presence || xml_table['name'] || "Sheet #{i + 1}"
|
37
|
-
)
|
38
|
-
context.assoc! sheet, xml_table
|
39
|
-
xml_table.css('tr').each do |row_node|
|
40
|
-
xls_row = sheet.add_row
|
41
|
-
context.assoc! xls_row, row_node
|
42
|
-
row_node.css('th,td').each do |cell_node|
|
43
|
-
xls_col = xls_row.add_cell cell_node.inner_text
|
44
|
-
context.assoc! xls_col, cell_node
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
package
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
@@ -1,56 +0,0 @@
|
|
1
|
-
module ToSpreadsheet
|
2
|
-
class Formats
|
3
|
-
attr_accessor :styles_by_type
|
4
|
-
attr_writer :sheet_props
|
5
|
-
|
6
|
-
def [](type)
|
7
|
-
(@styles_by_type ||= {})[type.to_sym] ||= []
|
8
|
-
end
|
9
|
-
|
10
|
-
def each
|
11
|
-
@styles_by_type.each do |k, v|
|
12
|
-
yield(k, v)
|
13
|
-
end if @styles_by_type
|
14
|
-
end
|
15
|
-
|
16
|
-
# Sheet props without selectors
|
17
|
-
def sheet_props
|
18
|
-
self[:sheet].map(&:last).inject({}, &:merge)
|
19
|
-
end
|
20
|
-
|
21
|
-
# Workbook props without selectors
|
22
|
-
def workbook_props
|
23
|
-
self[:workbook].map(&:last).inject({}, &:merge)
|
24
|
-
end
|
25
|
-
|
26
|
-
def range_props
|
27
|
-
self[:range]
|
28
|
-
end
|
29
|
-
|
30
|
-
def column_props
|
31
|
-
self[:column]
|
32
|
-
end
|
33
|
-
|
34
|
-
def row_props
|
35
|
-
self[:row]
|
36
|
-
end
|
37
|
-
|
38
|
-
def css_props
|
39
|
-
self[:css]
|
40
|
-
end
|
41
|
-
|
42
|
-
def derive
|
43
|
-
derived = Formats.new
|
44
|
-
each { |type, styles| derived[type].concat(styles) }
|
45
|
-
derived
|
46
|
-
end
|
47
|
-
|
48
|
-
def merge!(other_fmt)
|
49
|
-
other_fmt.each { |type, styles| self[type].concat(styles) }
|
50
|
-
end
|
51
|
-
|
52
|
-
def inspect
|
53
|
-
"Formats(sheet: #@sheet_props, styles: #@styles_by_type)"
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
data/lib/to_spreadsheet/xlsx.rb
DELETED
File without changes
|