From 6e00449177fd2daeb9359db413744d0ead0fd197 Mon Sep 17 00:00:00 2001 From: Gleb Date: Sat, 17 Nov 2012 16:09:14 +0100 Subject: [PATCH] Refactoring --- Rakefile | 3 +- lib/to_spreadsheet.rb | 4 +- lib/to_spreadsheet/action_pack_renderers.rb | 8 +- lib/to_spreadsheet/axlsx/formatter.rb | 86 ----------- lib/to_spreadsheet/axlsx/renderer.rb | 52 ------- lib/to_spreadsheet/context.rb | 153 +++++++++----------- lib/to_spreadsheet/context/pairing.rb | 8 +- lib/to_spreadsheet/formats.rb | 59 -------- lib/to_spreadsheet/helpers.rb | 7 +- lib/to_spreadsheet/renderer.rb | 58 ++++++++ lib/to_spreadsheet/rule.rb | 9 ++ lib/to_spreadsheet/rule/base.rb | 38 +++++ lib/to_spreadsheet/rule/container.rb | 25 ++++ lib/to_spreadsheet/rule/default_value.rb | 19 +++ lib/to_spreadsheet/rule/format.rb | 64 ++++++++ lib/to_spreadsheet/rule/sheet.rb | 18 +++ lib/to_spreadsheet/rule/workbook.rb | 10 ++ lib/to_spreadsheet/selectors.rb | 89 ++++++++++++ lib/to_spreadsheet/themes/default.rb | 5 +- lib/to_spreadsheet/type_from_value.rb | 19 +++ lib/to_spreadsheet/xlsx.rb | 0 spec/defaults_spec.rb | 2 +- spec/format_spec.rb | 2 +- spec/spec_helper.rb | 2 +- spec/worksheets_spec.rb | 2 +- 25 files changed, 439 insertions(+), 303 deletions(-) delete mode 100644 lib/to_spreadsheet/axlsx/formatter.rb delete mode 100644 lib/to_spreadsheet/axlsx/renderer.rb delete mode 100644 lib/to_spreadsheet/formats.rb create mode 100644 lib/to_spreadsheet/renderer.rb create mode 100644 lib/to_spreadsheet/rule.rb create mode 100644 lib/to_spreadsheet/rule/base.rb create mode 100644 lib/to_spreadsheet/rule/container.rb create mode 100644 lib/to_spreadsheet/rule/default_value.rb create mode 100644 lib/to_spreadsheet/rule/format.rb create mode 100644 lib/to_spreadsheet/rule/sheet.rb create mode 100644 lib/to_spreadsheet/rule/workbook.rb create mode 100644 lib/to_spreadsheet/selectors.rb create mode 100644 lib/to_spreadsheet/type_from_value.rb delete mode 100644 lib/to_spreadsheet/xlsx.rb diff --git a/Rakefile b/Rakefile index 8c035ae..f5caf8b 100644 --- a/Rakefile +++ b/Rakefile @@ -13,10 +13,11 @@ task :env do include ToSpreadsheet::Helpers end +desc 'Generate a simple xlsx file' task :write_test_xlsx => :env do require 'haml' path = '/tmp/spreadsheet.xlsx' html = Haml::Engine.new(File.read('spec/support/table.html.haml')).render - ToSpreadsheet::Axlsx::Renderer.to_package(html).serialize(path) + ToSpreadsheet::Renderer.to_package(html).serialize(path) puts "Written to #{path}" end \ No newline at end of file diff --git a/lib/to_spreadsheet.rb b/lib/to_spreadsheet.rb index f0606ef..fb79668 100644 --- a/lib/to_spreadsheet.rb +++ b/lib/to_spreadsheet.rb @@ -7,8 +7,6 @@ module ToSpreadsheet class << self - attr_accessor :context - def theme(name, &formats) @themes ||= {} if formats @@ -21,4 +19,4 @@ def theme(name, &formats) end require 'to_spreadsheet/themes/default' -ToSpreadsheet.context = ToSpreadsheet::Context.new.apply ToSpreadsheet.theme(:default) \ No newline at end of file +ToSpreadsheet::Context.global.format_xls ToSpreadsheet.theme(:default) \ No newline at end of file diff --git a/lib/to_spreadsheet/action_pack_renderers.rb b/lib/to_spreadsheet/action_pack_renderers.rb index f51bbf7..a51fddf 100644 --- a/lib/to_spreadsheet/action_pack_renderers.rb +++ b/lib/to_spreadsheet/action_pack_renderers.rb @@ -2,14 +2,18 @@ require 'action_controller/metal/renderers' require 'action_controller/metal/responder' -require 'to_spreadsheet/axlsx/renderer' +require 'to_spreadsheet/renderer' # This will let us do thing like `render :xlsx => 'index'` # This is similar to how Rails internally implements its :json and :xml renderers ActionController::Renderers.add :xlsx do |template, options| filename = options[:filename] || options[:template] || 'data' - html = with_context ToSpreadsheet.context.derive do + html = with_context ToSpreadsheet::Context.global.merge(ToSpreadsheet::Context.new) do + # local context + @local_formats.each do |selector, &block| + context.process_dsl selector, &block + end if @local_formats render_to_string(options[:template], options) end diff --git a/lib/to_spreadsheet/axlsx/formatter.rb b/lib/to_spreadsheet/axlsx/formatter.rb deleted file mode 100644 index 6b88d2b..0000000 --- a/lib/to_spreadsheet/axlsx/formatter.rb +++ /dev/null @@ -1,86 +0,0 @@ -module ToSpreadsheet - module Axlsx - module Formatter - COL_INFO_PROPS = %w(bestFit collapsed customWidth hidden phonetic width).map(&:to_sym) - - def apply_formats(package, context) - package.workbook.worksheets.each do |sheet| - fmt = context.formats(sheet) - fmt.workbook_props.each do |prop, value| - package.send(:"#{prop}=", value) - end - fmt.sheet_props.each do |set, props| - apply_props sheet.send(set), props, context - end - fmt.column_props.each do |v| - idx, props = v[0], v[1] - apply_props sheet.column_info[0], props.slice(*COL_INFO_PROPS), context - props = props.except(*COL_INFO_PROPS) - sheet.col_style idx, props if props.present? - end - fmt.row_props.each do |v| - idx, props = v[0], v[1] - sheet.row_style idx, props - end - fmt.range_props.each do |v| - range, props = v[0], v[1] - apply_props sheet[range], props, context - end - fmt.css_props.each do |v| - css_sel, props = v[0], v[1] - context.node_from_entity(sheet).css(css_sel).each do |node| - apply_props context.entity_from_node(node), props, context - end - end - end - end - - private - def apply_props(obj, props, context) - if props.is_a?(Proc) - context.instance_exec(obj, &props) - return - end - - props = props.dup - props.each do |k, v| - props[k] = context.instance_exec(obj, &v) if v.is_a?(Proc) - end - - props.each do |k, v| - next if v.nil? - if k == :default_value - unless obj.value.present? && !([:integer, :float].include?(obj.type) && obj.value.zero?) - obj.type = cell_type_from_value(v) - obj.value = v - end - else - setter = :"#{k}=" - obj.send setter, v if obj.respond_to?(setter) - if obj.respond_to?(:cells) - obj.cells.each do |cell| - cell.send setter, v if cell.respond_to?(setter) - end - end - end - end - end - - def cell_type_from_value(v) - if v.is_a?(Date) - :date - elsif v.is_a?(Time) - :time - elsif v.is_a?(TrueClass) || v.is_a?(FalseClass) - :boolean - elsif v.to_s.match(/\A[+-]?\d+?\Z/) #numeric - :integer - elsif v.to_s.match(/\A[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?\Z/) #float - :float - else - :string - end - end - end - end -end \ No newline at end of file diff --git a/lib/to_spreadsheet/axlsx/renderer.rb b/lib/to_spreadsheet/axlsx/renderer.rb deleted file mode 100644 index e0d0720..0000000 --- a/lib/to_spreadsheet/axlsx/renderer.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'axlsx' -require 'to_spreadsheet/axlsx/formatter' -module ToSpreadsheet - module Axlsx - module Renderer - include Formatter - extend self - - def to_stream(html, context = ToSpreadsheet.context) - to_package(html, context).to_stream - end - - def to_data(html, context = ToSpreadsheet.context) - to_package(html, context).to_stream.read - end - - def to_package(html, context = ToSpreadsheet.context) - package = build_package(html, context) - apply_formats(package, context) - # Don't leak memory: clear all dom <-> axslsx associations - context.clear_assoc! - # Numbers compat - package.use_shared_strings = true - package - end - - private - - def build_package(html, context) - package = ::Axlsx::Package.new - spreadsheet = package.workbook - doc = Nokogiri::HTML::Document.parse(html) - context.assoc! spreadsheet, doc - doc.css('table').each_with_index do |xml_table, i| - sheet = spreadsheet.add_worksheet( - name: xml_table.css('caption').inner_text.presence || xml_table['name'] || "Sheet #{i + 1}" - ) - context.assoc! sheet, xml_table - xml_table.css('tr').each do |row_node| - xls_row = sheet.add_row - context.assoc! xls_row, row_node - row_node.css('th,td').each do |cell_node| - xls_col = xls_row.add_cell cell_node.inner_text - context.assoc! xls_col, cell_node - end - end - end - package - end - end - end -end diff --git a/lib/to_spreadsheet/context.rb b/lib/to_spreadsheet/context.rb index b5092f1..19d9d7b 100644 --- a/lib/to_spreadsheet/context.rb +++ b/lib/to_spreadsheet/context.rb @@ -1,67 +1,53 @@ require 'to_spreadsheet/context/pairing' -require 'to_spreadsheet/formats' +require 'to_spreadsheet/rule' +require 'to_spreadsheet/rule/base' +require 'to_spreadsheet/rule/container' +require 'to_spreadsheet/rule/format' +require 'to_spreadsheet/rule/default_value' +require 'to_spreadsheet/rule/sheet' +require 'to_spreadsheet/rule/workbook' module ToSpreadsheet # This is the DSL context for `format_xls` # It maintains the current formats set to enable for local and nested `format_xls` blocks class Context - # todo (cleaner code): split features further into modules - # todo (extensibility): add processing callbacks (internal API) include Pairing + attr_accessor :rules - def initialize(wb_options = nil) - @formats = [] - @current_format = Formats.new - workbook wb_options if wb_options - end - - # Returns a new formats jar for a given sheet - def formats(sheet) - format = Formats.new - @formats.each do |v| - sel, fmt = v[0], v[1] - format.merge!(fmt) if selects?(sel, sheet) + class << self + def global + @global ||= new end - format end - # Check if selector matches a given sheet / cell / row - def selects?(selector, entity) - return true if !selector - type, val = selector[0], selector[1] - sheet = entity.is_a?(::Axlsx::Workbook) ? entity : (entity.respond_to?(:workbook) ? entity.workbook : entity.worksheet.workbook) - doc = node_from_entity(sheet) - case type - when :css - doc.css(val).include?(node_from_entity(entity)) - when :column - return false if entity.is_a?(Axlsx::Row) - entity.index == val if entity.is_a?(Axlsx::Cell) - when :row - return entity.index == val if entity.is_a?(Axlsx::Row) - entity.row.index == val if entity.is_a?(Axlsx::Cell) - when :range - if entity.is_a?(Axlsx::Cell) - pos = entity.pos - top_left, bot_right = val.split(':').map { |s| Axlsx.name_to_indices(s) } - pos[0] >= top_left[0] && pos[0] <= bot_right[0] && pos[1] >= top_left[1] && pos[1] <= bot_right[1] - end - end + def initialize(wb_options = nil) + @rules = [] + workbook wb_options if wb_options end - # current format, used internally - attr_accessor :current_format - - # format_xls 'table.zebra' do - # format 'td', lambda { |cell| {b: true} if cell.row.even? } - # end + # Examples: + # format_xls 'table.zebra' do + # format 'td', lambda { |cell| {b: true} if cell.row.even? } + # end + # format_xls ToSpreadsheet.theme(:a_theme) + # format_xls 'table.zebra', ToSpreadsheet.theme(:zebra) def format_xls(selector = nil, theme = nil, &block) selector, theme = nil, selector if selector.is_a?(Proc) && !theme - add_format(selector, &theme) if theme - add_format(selector, &block) if block + process_dsl(selector, &theme) if theme + process_dsl(selector, &block) if block self end + def process_dsl(selector, &block) + @rule_container = add_rule :container, *selector_query(selector) + instance_eval(&block) + @rule_container = nil + end + + def workbook(selector = nil, value) + add_rule :workbook, *selector_query(selector), value + end + # format 'td.b', b: true # bold # format column: 0, width: 50 # format 'A1:C30', b: true @@ -69,66 +55,61 @@ def format_xls(selector = nil, theme = nil, &block) # column format also accepts Axlsx columnInfo settings def format(selector = nil, options) options = options.dup - selector = extract_selector!(selector, options) - add selector[0], selector, options + selector = selector_query(selector, options) + add_rule :format, *selector, options end # sheet 'table.landscape', page_setup: { orientation: landscape } def sheet(selector = nil, options) options = options.dup - selector = extract_selector!(selector, options) - add :sheet, selector, options + selector = selector_query(selector, options) + add_rule :sheet, *selector, options end # default 'td.c', 5 def default(selector, value) - options = {default_value: value} - selector = extract_selector!(selector, options) - add selector[0], selector, options - end - - def add(setting, selector, value) - @current_format[setting] << [selector.try(:[], 1), value] if selector || value - end - - def workbook(selector = nil, value) - add :package, selector, value + selector = selector_query(selector) + add_rule :default_value, *selector, value end - def apply(theme = nil, &block) - add_format &theme if theme - add_format &block if block - self + def add_rule(rule_type, selector_type, selector_value, options = {}) + rule = ToSpreadsheet::Rule.make(rule_type, selector_type, selector_value, options) + if @rule_container + @rule_container.children << rule + else + @rules << rule + end + rule end - def derive - derived = dup - derived.current_format = derived.current_format.derive - derived + # A new context + def merge(other_context) + ctx = Context.new() + ctx.rules = rules + other_context.rules + ctx end private - def add_format(sheet_sel = nil, &block) - format_was = @current_format - @current_format = @current_format.derive - instance_eval &block - @formats << [extract_selector!(sheet_sel), @current_format] - @current_format = format_was - end - - def extract_selector!(selector, options = {}) - if selector - if selector =~ /:/ && selector[0].upcase == selector[0] - return [:range, selector] + # Extract selector query from DSL arguments + # + # Figures out text type: + # selector_query('td.num') # [:css, "td.num"] + # selector_query('A0:B5') # [:range, "A0:B5"] + # + # If text is nil, extracts first of row, range, and css keys + # selector_query(nil, {column: 0}] # [:column, 0] + def selector_query(text, opts = {}) + if text + if text =~ /:/ && text[0].upcase == text[0] + return [:range, text] else - return [:css, selector] + return [:css, text] end end - [:column, :row, :range].each do |key| - return [key, options.delete(key)] if options.key?(key) - end - selector + key = [:column, :row, :range].detect { |key| opts.key?(key) } + return [key, opts.delete(key)] if key + [nil, nil] end end end \ No newline at end of file diff --git a/lib/to_spreadsheet/context/pairing.rb b/lib/to_spreadsheet/context/pairing.rb index 474d1e7..007706a 100644 --- a/lib/to_spreadsheet/context/pairing.rb +++ b/lib/to_spreadsheet/context/pairing.rb @@ -9,14 +9,18 @@ def assoc!(entity, node) @node_to_entity[node] = entity end - def entity_from_node(node) + def to_xls_entity(node) @node_to_entity[node] end - def node_from_entity(entity) + def to_xml_node(entity) @entity_to_node[entity] end + def xml_node_and_xls_entity(entity) + [@entity_to_node[entity], entity, @node_to_entity[entity]].compact + end + def clear_assoc! @entity_to_node = {} @node_to_entity = {} diff --git a/lib/to_spreadsheet/formats.rb b/lib/to_spreadsheet/formats.rb deleted file mode 100644 index 49d489a..0000000 --- a/lib/to_spreadsheet/formats.rb +++ /dev/null @@ -1,59 +0,0 @@ -module ToSpreadsheet - # An intrnal container of formats - class Formats - attr_accessor :styles_by_type - attr_writer :sheet_props - - # A list of [format_selector, formatting block] by `type` - # `type` is one of :sheet, :workbook, :range, :column, :row, :css - def [](type) - (@styles_by_type ||= {})[type.to_sym] ||= [] - end - - def each - @styles_by_type.each do |k, v| - yield(k, v) - end if @styles_by_type - end - - # A list of all formatting blocks with type :sheet - def sheet_props - self[:sheet].map(&:last).inject({}, &:merge) - end - - # Workbook props without selectors - def workbook_props - self[:workbook].map(&:last).inject({}, &:merge) - end - - def range_props - self[:range] - end - - def column_props - self[:column] - end - - def row_props - self[:row] - end - - def css_props - self[:css] - end - - def derive - derived = Formats.new - each { |type, styles| derived[type].concat(styles) } - derived - end - - def merge!(other_fmt) - other_fmt.each { |type, styles| self[type].concat(styles) } - end - - def inspect - "Formats(sheet: #@sheet_props, styles: #@styles_by_type)" - end - end -end \ No newline at end of file diff --git a/lib/to_spreadsheet/helpers.rb b/lib/to_spreadsheet/helpers.rb index 2cbe1ee..3df195e 100644 --- a/lib/to_spreadsheet/helpers.rb +++ b/lib/to_spreadsheet/helpers.rb @@ -1,16 +1,11 @@ module ToSpreadsheet module Helpers - - def to_spreadsheet(selector, &block) - context.apply(block) - end - def format_xls(selector = nil, &block) context.format_xls selector, &block end def context - @context || ToSpreadsheet.context + @context || ToSpreadsheet::Context.global end def with_context(context, &block) diff --git a/lib/to_spreadsheet/renderer.rb b/lib/to_spreadsheet/renderer.rb new file mode 100644 index 0000000..a0d8c58 --- /dev/null +++ b/lib/to_spreadsheet/renderer.rb @@ -0,0 +1,58 @@ +require 'axlsx' +module ToSpreadsheet + module Renderer + extend self + + def to_stream(html, local_context = nil) + to_package(html, local_context).to_stream + end + + def to_data(html, local_context = nil) + to_package(html, local_context).to_stream.read + end + + def to_package(html, local_context = nil) + with_context init_context(local_context) do + package = build_package(html, context) + context.rules.each do |rule| + puts "Applying #{rule}" + rule.apply(context, package) + end + package + end + end + + private + + def init_context(local_context) + local_context ||= ToSpreadsheet::Context.new + ToSpreadsheet::Context.global.merge local_context + end + + def build_package(html, context) + package = ::Axlsx::Package.new + spreadsheet = package.workbook + doc = Nokogiri::HTML::Document.parse(html) + # Workbook <-> %document association + context.assoc! spreadsheet, doc + doc.css('table').each_with_index do |xml_table, i| + sheet = spreadsheet.add_worksheet( + name: xml_table.css('caption').inner_text.presence || xml_table['name'] || "Sheet #{i + 1}" + ) + # Sheet <-> %table association + context.assoc! sheet, xml_table + xml_table.css('tr').each do |row_node| + xls_row = sheet.add_row + # Row <-> %tr association + context.assoc! xls_row, row_node + row_node.css('th,td').each do |cell_node| + xls_col = xls_row.add_cell cell_node.inner_text + # Cell <-> th or td association + context.assoc! xls_col, cell_node + end + end + end + package + end + end +end diff --git a/lib/to_spreadsheet/rule.rb b/lib/to_spreadsheet/rule.rb new file mode 100644 index 0000000..8574b08 --- /dev/null +++ b/lib/to_spreadsheet/rule.rb @@ -0,0 +1,9 @@ +require 'active_support/core_ext' +module ToSpreadsheet + module Rule + def self.make(rule_type, selector_type, selector_value, options) + klass = "ToSpreadsheet::Rule::#{rule_type.to_s.camelize}".constantize + klass.new(selector_type, selector_value, options) + end + end +end \ No newline at end of file diff --git a/lib/to_spreadsheet/rule/base.rb b/lib/to_spreadsheet/rule/base.rb new file mode 100644 index 0000000..f313709 --- /dev/null +++ b/lib/to_spreadsheet/rule/base.rb @@ -0,0 +1,38 @@ +require 'to_spreadsheet/selectors' +module ToSpreadsheet + module Rule + class Base + include ::ToSpreadsheet::Selectors + attr_reader :selector_type, :selector_query, :options + + def initialize(selector_type, selector_query, options) + @selector_type = selector_type + @selector_query = selector_query + @options = options + end + + def applies_to?(context, xml_or_xls_node) + return true if !selector_type + node, entity = context.xml_node_and_xls_entity(xml_or_xls_node) + sheet = entity.is_a?(::Axlsx::Workbook) ? entity : (entity.respond_to?(:workbook) ? entity.workbook : entity.worksheet.workbook) + doc = context.to_xml_node(sheet) + query_match?( + selector_type: selector_type, + selector_query: selector_query, + xml_document: doc, + xml_node: node, + xls_worksheet: sheet, + xls_entity: entity + ) + end + + def type + self.class.name.demodulize.underscore.to_sym + end + + def to_s + "Rule [#{type}, #{selector_type}, #{selector_query}, #{options}" + end + end + end +end \ No newline at end of file diff --git a/lib/to_spreadsheet/rule/container.rb b/lib/to_spreadsheet/rule/container.rb new file mode 100644 index 0000000..4664e62 --- /dev/null +++ b/lib/to_spreadsheet/rule/container.rb @@ -0,0 +1,25 @@ +module ToSpreadsheet + module Rule + # Applies children rules to all the matching tables + class Container < Base + attr_reader :children + def initialize(*args) + super + @children = [] + end + + def apply(context, package) + package.workbook.worksheets.each do |sheet| + table = context.to_xml_node(sheet) + if applies_to?(context, table) + children.each { |c| c.apply(context, sheet) } + end + end + end + + def to_s + "Rules(#{selector_type}, #{selector_query}) [#{children.map(&:to_s)}]" + end + end + end +end diff --git a/lib/to_spreadsheet/rule/default_value.rb b/lib/to_spreadsheet/rule/default_value.rb new file mode 100644 index 0000000..a8dbd8d --- /dev/null +++ b/lib/to_spreadsheet/rule/default_value.rb @@ -0,0 +1,19 @@ +require 'to_spreadsheet/type_from_value' +module ToSpreadsheet + module Rule + class DefaultValue < Base + include ::ToSpreadsheet::TypeFromValue + + def apply(context, sheet) + default = options + each_cell context, sheet, selector_type, selector_query do |cell| + unless cell.value.present? && + !([:integer, :float].include?(cell.type) && cell.value.zero?) + cell.type = cell_type_from_value(default) + cell.value = default + end + end + end + end + end +end diff --git a/lib/to_spreadsheet/rule/format.rb b/lib/to_spreadsheet/rule/format.rb new file mode 100644 index 0000000..dcfbaea --- /dev/null +++ b/lib/to_spreadsheet/rule/format.rb @@ -0,0 +1,64 @@ +require 'to_spreadsheet/type_from_value' +require 'set' +module ToSpreadsheet + module Rule + class Format < Base + include ::ToSpreadsheet::TypeFromValue + def apply(context, sheet) + case selector_type + when :css + css_match selector_query, context.to_xml_node(sheet) do |xml_node| + apply_inline_styles context, context.to_xls_entity(xml_node) + end + when :row + sheet.row_style selector_query, options if options.present? + when :column + inline_styles = options.except(*COL_INFO_PROPS) + sheet.col_style selector_query, inline_styles if inline_styles.present? + apply_col_info sheet.column_info[selector_query] + when :range + apply_inline_styles range_match(selector_type, sheet), context + end + end + + private + COL_INFO_PROPS = %w(bestFit collapsed customWidth hidden phonetic width).map(&:to_sym).to_set + def apply_col_info(col_info) + return if col_info.nil? + options.each do |k, v| + if COL_INFO_PROPS.include?(k) + col_info.send :"#{k}=", v + end + end + end + + def apply_inline_styles(context, xls_ent) + # Custom format rule + # format 'td.sel', lambda { |node| ...} + if self.options.is_a?(Proc) + context.instance_exec(xls_ent, &self.options) + return + end + + options = self.options.dup + # Compute Proc rules + # format 'td.sel', color: lambda {|node| ...} + options.each do |k, v| + options[k] = context.instance_exec(xls_ent, &v) if v.is_a?(Proc) + end + + # Apply inline styles + options.each do |k, v| + next if v.nil? + setter = :"#{k}=" + xls_ent.send setter, v if xls_ent.respond_to?(setter) + if xls_ent.respond_to?(:cells) + xls_ent.cells.each do |cell| + cell.send setter, v if cell.respond_to?(setter) + end + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/to_spreadsheet/rule/sheet.rb b/lib/to_spreadsheet/rule/sheet.rb new file mode 100644 index 0000000..1e382a2 --- /dev/null +++ b/lib/to_spreadsheet/rule/sheet.rb @@ -0,0 +1,18 @@ +module ToSpreadsheet + module Rule + class Sheet < Base + def apply(context, sheet) + options.each { |k, v| + if v.is_a?(Hash) + sub = sheet.send(k) + v.each do |sub_k, sub_v| + sub.send :"#{sub_k}=", sub_v + end + else + sheet.send :"#{k}=", v + end + } + end + end + end +end diff --git a/lib/to_spreadsheet/rule/workbook.rb b/lib/to_spreadsheet/rule/workbook.rb new file mode 100644 index 0000000..165fbb5 --- /dev/null +++ b/lib/to_spreadsheet/rule/workbook.rb @@ -0,0 +1,10 @@ +module ToSpreadsheet + module Rule + class Workbook < Base + def apply(context, sheet) + workbook = sheet.workbook + options.each { |k, v| workbook.send :"#{k}=", v } + end + end + end +end diff --git a/lib/to_spreadsheet/selectors.rb b/lib/to_spreadsheet/selectors.rb new file mode 100644 index 0000000..06d5b82 --- /dev/null +++ b/lib/to_spreadsheet/selectors.rb @@ -0,0 +1,89 @@ +module ToSpreadsheet + # This is the DSL context for `format_xls` + # Query types: :css, :column, :row or :range + # Query values: + # For css: [String] css selector + # For column and row: [Fixnum] column/row number + # For range: [String] table range, e.g. A4:B5 + module Selectors + # Flexible API query match + # Options (all optional): + # xls_worksheet + # xls_entity + # xml_document + # xml_node + # selector_type :css, :column, :row or :range + # selector_query + def query_match?(options) + return true if !options[:selector_query] + case options[:selector_type] + when :css + css_match? options[:selector_query], options[:xml_document], options[:xml_node] + when :column + return false unless [Axlsx::Row, Axlsx::Cell].include?(options[:xml_node].class) + column_number_match? options[:selector_query], options[:xml_node] + when :row + return false unless Axlsx::Cell == options[:xml_node].class + row_number_match? options[:selector_query], options[:xml_node] + when :range + return false if entity.is_a?(Axlsx::Cell) + range_contains? options[:selector_query], options[:xml_node] + else + raise "Unsupported type #{options[:selector_type].inspect} (:css, :column, :row or :range expected)" + end + end + + def each_cell(context, sheet, selector_type, selector_query, &block) + if !selector_type + sheet.rows.each do |row| + sheet.cells.each do |cell| + block.(cell) + end + end + return + end + case selector_type + when :css + css_match selector_query, context.to_xml_node(sheet) do |xml_node| + block.(context.to_xls_entity(xml_node)) + end + when :column + sheet.cols[selector_query].cells.each(&block) + when :row + sheet.cols[selector_query].cells.each(&block) + when :range + sheet[range].each(&block) + end + end + + def css_match(css_selector, xml_node, &block) + xml_node.css(css_selector).each(&block) + end + + def css_match?(css_selector, xml_document, xml_node) + xml_document.css(css_selector).include?(xml_node) + end + + def row_number_match?(row_number, xls_row_or_cell) + if xls_row_or_cell.is_a? Axlsx::Row + row_number == xls_row_or_cell.index + elsif xls_row_or_cell.is_a? Axlsx::Cell + row_number == xls_row_or_cell.row.index + end + end + + def column_number_match?(column_number, xls_cell) + xls_cell.index == column_number if xls_cell.is_a?(Axlsx::Cell) + end + + def range_match(range, xls_sheet) + xls_sheet[range] + end + + def range_contains?(range, xls_cell) + pos = xls_cell.pos + top_left, bot_right = range.split(':').map { |s| Axlsx.name_to_indices(s) } + pos[0] >= top_left[0] && pos[0] <= bot_right[0] && pos[1] >= top_left[1] && pos[1] <= bot_right[1] + end + end +end \ No newline at end of file diff --git a/lib/to_spreadsheet/themes/default.rb b/lib/to_spreadsheet/themes/default.rb index 9c3b99e..5ec0411 100644 --- a/lib/to_spreadsheet/themes/default.rb +++ b/lib/to_spreadsheet/themes/default.rb @@ -1,7 +1,8 @@ module ToSpreadsheet::Themes module Default ::ToSpreadsheet.theme :default do - workbook use_autowidth: true + workbook use_autowidth: true, + use_shared_strings: true sheet page_setup: { fit_to_height: 1, fit_to_width: 1, @@ -10,7 +11,7 @@ module Default # Set value type based on CSS class format 'td,th', lambda { |cell| val = cell.value - case node_from_entity(cell)[:class] + case to_xml_node(cell)[:class] when /decimal|float/ cell.type = :float when /num|int/ diff --git a/lib/to_spreadsheet/type_from_value.rb b/lib/to_spreadsheet/type_from_value.rb new file mode 100644 index 0000000..d3d47be --- /dev/null +++ b/lib/to_spreadsheet/type_from_value.rb @@ -0,0 +1,19 @@ +module ToSpreadsheet + module TypeFromValue + def cell_type_from_value(v) + if v.is_a?(Date) + :date + elsif v.is_a?(Time) + :time + elsif v.is_a?(TrueClass) || v.is_a?(FalseClass) + :boolean + elsif v.to_s.match(/\A[+-]?\d+?\Z/) #numeric + :integer + elsif v.to_s.match(/\A[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?\Z/) #float + :float + else + :string + end + end + end +end \ No newline at end of file diff --git a/lib/to_spreadsheet/xlsx.rb b/lib/to_spreadsheet/xlsx.rb deleted file mode 100644 index e69de29..0000000 diff --git a/spec/defaults_spec.rb b/spec/defaults_spec.rb index 8a0c0f0..3c3249c 100644 --- a/spec/defaults_spec.rb +++ b/spec/defaults_spec.rb @@ -2,7 +2,7 @@ -describe ToSpreadsheet::Axlsx::Formatter do +describe ToSpreadsheet::Rule::DefaultValue do let :spreadsheet do build_spreadsheet(haml: <