to_spreadsheet 1.0.0.rc2 → 1.0.0.rc3

Sign up to get free protection for your applications and to get access to all the features.
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