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 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
- @my_thingies = MyItem.all
17
+ @my_items = MyItem.all
18
18
  respond_to do |format|
19
- format.html {}
20
- format.xlsx { render xlsx: :index, filename: "thingies_index" }
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::Axlsx::Renderer.to_package(html).serialize(path)
21
+ ToSpreadsheet::Renderer.to_package(html).serialize(path)
21
22
  puts "Written to #{path}"
22
23
  end
@@ -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.context = ToSpreadsheet::Context.new.apply ToSpreadsheet.theme(:default)
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/axlsx/renderer'
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.context.derive do
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/formats'
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
- def initialize(wb_options = nil)
9
- @formats = []
10
- @current_format = Formats.new
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
- # Check if selector matches a given sheet / cell / row
25
- def selects?(selector, entity)
26
- return true if !selector
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
- # current format, used internally
49
- attr_accessor :current_format
50
-
51
- # format_xls 'table.zebra' do
52
- # format 'td', lambda { |cell| {b: true} if cell.row.even? }
53
- # end
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
- add_format(selector, &theme) if theme
57
- add_format(selector, &block) if block
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 = extract_selector!(selector, options)
69
- add selector[0], selector, options
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 = extract_selector!(selector, options)
76
- add :sheet, selector, options
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
- options = {default_value: value}
82
- selector = extract_selector!(selector, options)
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 apply(theme = nil, &block)
95
- add_format &theme if theme
96
- add_format &block if block
97
- self
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
- def derive
101
- derived = dup
102
- derived.current_format = derived.current_format.derive
103
- derived
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
- def add_format(sheet_sel = nil, &block)
109
- format_was = @current_format
110
- @current_format = @current_format.derive
111
- instance_eval &block
112
- @formats << [extract_selector!(sheet_sel), @current_format]
113
- @current_format = format_was
114
- end
115
-
116
- def extract_selector!(selector, options = {})
117
- if selector
118
- if selector =~ /:/ && selector[0].upcase == selector[0]
119
- return [:range, selector]
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, selector]
107
+ return [:css, text]
122
108
  end
123
109
  end
124
- [:column, :row, :range].each do |key|
125
- return [key, options.delete(key)] if options.key?(key)
126
- end
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
- # Associating Axlsx entities and Nokogiri nodes
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 entity_from_node(node)
12
+ def to_xls_entity(node)
13
13
  @node_to_entity[node]
14
14
  end
15
15
 
16
- def node_from_entity(entity)
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.context
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,10 @@
1
+ module ToSpreadsheet
2
+ module Rule
3
+ class Workbook < Base
4
+ def apply(context, sheet)
5
+ workbook = sheet.workbook
6
+ options.each { |k, v| workbook.send :"#{k}=", v }
7
+ end
8
+ end
9
+ end
10
+ 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 node_from_entity(cell)[:class]
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
@@ -1,3 +1,3 @@
1
1
  module ToSpreadsheet
2
- VERSION = '1.0.0.rc2'
2
+ VERSION = '1.0.0.rc3'
3
3
  end
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
 
4
4
 
5
- describe ToSpreadsheet::Axlsx::Formatter do
5
+ describe ToSpreadsheet::Rule::DefaultValue do
6
6
  let :spreadsheet do
7
7
  build_spreadsheet(haml: <<HAML)
8
8
  - format_xls 'table' do
data/spec/format_spec.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe ToSpreadsheet::Axlsx::Formatter do
3
+ describe ToSpreadsheet::Rule::Format do
4
4
  let :spreadsheet do
5
5
  build_spreadsheet haml: <<-HAML
6
6
  :ruby
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe ToSpreadsheet::Axlsx::Renderer do
3
+ describe ToSpreadsheet::Renderer do
4
4
  let :spreadsheet do
5
5
  build_spreadsheet haml: <<-HAML
6
6
  %table
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.rc2
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-16 00:00:00.000000000 Z
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: Adds various html -> spreadsheet (xls, odt, etc) renderers to Rails.
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
File without changes