Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

1.0.0.rc1:

* formatting DSL
* stop using spreadsheet (c-ext, gplv3, .xls format) in favour of axlsx (pure ruby, .xslx format)
  • Loading branch information...
commit c58de8de669206a120791bb2ec745bdd7bfceb9c 1 parent 60f1303
@glebm authored
View
6 .gitignore
@@ -3,7 +3,5 @@
log/*.log
pkg/
-test/test_app/.bundle/
-test/test_app/db/*.sqlite3
-test/test_app/log/*.log
-test/test_app/tmp/
+Gemfile.lock
+.rvmrc
View
48 .rvmrc
@@ -1,48 +0,0 @@
-#!/usr/bin/env bash
-
-# This is an RVM Project .rvmrc file, used to automatically load the ruby
-# development environment upon cd'ing into the directory
-
-# First we specify our desired <ruby>[@<gemset>], the @gemset name is optional,
-# Only full ruby name is supported here, for short names use:
-# echo "rvm use 1.9.3" > .rvmrc
-environment_id="ruby-1.9.3-p286-perf@to_spreadsheet"
-
-# Uncomment the following lines if you want to verify rvm version per project
-# rvmrc_rvm_version="1.16.17 (stable)" # 1.10.1 seams as a safe start
-# eval "$(echo ${rvm_version}.${rvmrc_rvm_version} | awk -F. '{print "[[ "$1*65536+$2*256+$3" -ge "$4*65536+$5*256+$6" ]]"}' )" || {
-# echo "This .rvmrc file requires at least RVM ${rvmrc_rvm_version}, aborting loading."
-# return 1
-# }
-
-# First we attempt to load the desired environment directly from the environment
-# file. This is very fast and efficient compared to running through the entire
-# CLI and selector. If you want feedback on which environment was used then
-# insert the word 'use' after --create as this triggers verbose mode.
-if [[ -d "${rvm_path:-$HOME/.rvm}/environments"
- && -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
-then
- \. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
- [[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]] &&
- \. "${rvm_path:-$HOME/.rvm}/hooks/after_use" || true
-else
- # If the environment file has not yet been created, use the RVM CLI to select.
- rvm --create "$environment_id" || {
- echo "Failed to create RVM environment '${environment_id}'."
- return 1
- }
-fi
-
-# If you use bundler, this might be useful to you:
-# if [[ -s Gemfile ]] && {
-# ! builtin command -v bundle >/dev/null ||
-# builtin command -v bundle | GREP_OPTIONS= \grep $rvm_path/bin/bundle >/dev/null
-# }
-# then
-# printf "%b" "The rubygem 'bundler' is not installed. Installing it now.\n"
-# gem install bundler
-# fi
-# if [[ -s Gemfile ]] && builtin command -v bundle >/dev/null
-# then
-# bundle install | GREP_OPTIONS= \grep -vE '^Using|Your bundle is complete'
-# fi
View
3  .travis.yml
@@ -2,4 +2,7 @@ language: ruby
rvm:
- 1.9.2
- 1.9.3
+ - jruby-head
+
+
script: bundle exec rake spec
View
3  Gemfile
@@ -3,8 +3,11 @@ source "http://rubygems.org"
gem 'rails', '> 3'
gem 'axlsx'
gem 'nokogiri'
+gem 'chronic'
group :test, :development do
gem 'rspec'
gem 'haml'
+ gem 'RedCloth'
+ gem 'debugger', platform: :ruby
end
View
110 Gemfile.lock
@@ -1,110 +0,0 @@
-GEM
- remote: http://rubygems.org/
- specs:
- actionmailer (3.1.3)
- actionpack (= 3.1.3)
- mail (~> 2.3.0)
- actionpack (3.1.3)
- activemodel (= 3.1.3)
- activesupport (= 3.1.3)
- builder (~> 3.0.0)
- erubis (~> 2.7.0)
- i18n (~> 0.6)
- rack (~> 1.3.5)
- rack-cache (~> 1.1)
- rack-mount (~> 0.8.2)
- rack-test (~> 0.6.1)
- sprockets (~> 2.0.3)
- activemodel (3.1.3)
- activesupport (= 3.1.3)
- builder (~> 3.0.0)
- i18n (~> 0.6)
- activerecord (3.1.3)
- activemodel (= 3.1.3)
- activesupport (= 3.1.3)
- arel (~> 2.2.1)
- tzinfo (~> 0.3.29)
- activeresource (3.1.3)
- activemodel (= 3.1.3)
- activesupport (= 3.1.3)
- activesupport (3.1.3)
- multi_json (~> 1.0)
- arel (2.2.1)
- axlsx (1.3.3)
- htmlentities (~> 4.3.1)
- nokogiri (>= 1.4.1)
- rubyzip (>= 0.9.5)
- builder (3.0.0)
- diff-lcs (1.1.3)
- erubis (2.7.0)
- haml (3.1.4)
- hike (1.2.1)
- htmlentities (4.3.1)
- i18n (0.6.0)
- json (1.6.5)
- mail (2.3.0)
- i18n (>= 0.4.0)
- mime-types (~> 1.16)
- treetop (~> 1.4.8)
- mime-types (1.17.2)
- multi_json (1.0.4)
- nokogiri (1.5.0)
- nokogiri (1.5.0-x86-mingw32)
- polyglot (0.3.3)
- rack (1.3.6)
- rack-cache (1.1)
- rack (>= 0.4)
- rack-mount (0.8.3)
- rack (>= 1.0.0)
- rack-ssl (1.3.2)
- rack
- rack-test (0.6.1)
- rack (>= 1.0)
- rails (3.1.3)
- actionmailer (= 3.1.3)
- actionpack (= 3.1.3)
- activerecord (= 3.1.3)
- activeresource (= 3.1.3)
- activesupport (= 3.1.3)
- bundler (~> 1.0)
- railties (= 3.1.3)
- railties (3.1.3)
- actionpack (= 3.1.3)
- activesupport (= 3.1.3)
- rack-ssl (~> 1.3.2)
- rake (>= 0.8.7)
- rdoc (~> 3.4)
- thor (~> 0.14.6)
- rake (0.9.2.2)
- rdoc (3.12)
- json (~> 1.4)
- rspec (2.8.0)
- rspec-core (~> 2.8.0)
- rspec-expectations (~> 2.8.0)
- rspec-mocks (~> 2.8.0)
- rspec-core (2.8.0)
- rspec-expectations (2.8.0)
- diff-lcs (~> 1.1.2)
- rspec-mocks (2.8.0)
- rubyzip (0.9.9)
- sprockets (2.0.3)
- hike (~> 1.2)
- rack (~> 1.0)
- tilt (~> 1.1, != 1.3.0)
- thor (0.14.6)
- tilt (1.3.3)
- treetop (1.4.10)
- polyglot
- polyglot (>= 0.3.1)
- tzinfo (0.3.31)
-
-PLATFORMS
- ruby
- x86-mingw32
-
-DEPENDENCIES
- axlsx
- haml
- nokogiri
- rails (> 3)
- rspec
View
67 README.textile
@@ -14,55 +14,86 @@ bc. # my_thingies_controller.rb
class MyThingiesController < ApplicationController
respond_to :xls, :html
def index
- @my_thingies = MyThingie.all
+ @my_thingies = MyItem.all
respond_to do |format|
format.html {}
- format.xlsx { render :xlsx => :index, :filename => "thingies_index" }
+ format.xlsx { render xlsx: :index, filename: "thingies_index" }
end
end
end
In your view partial:
-bc. # _my_thingie.haml
+bc. # _my_items.haml
%table
- %caption My thingies
+ %caption My items
%thead
%tr
- %td{width: 5} ID
+ %td ID
%td Name
%tbody
- - my_thingies.each do |thingie|
+ - my_items.each do |my_item|
%tr
- %td.number{ xls_style: '{sz:14,format_code:"0000%"}' }= thingie.id
- %td= thingie.name
+ %td.number= my_item.id
+ %td= my_item.name
%tfoot
%tr
- %td(colspan="2") #{my_thingies.length}
-
-See axlsx's README for info and syntax on styles. If a TR has a style, it's merged with any TD styles under it.
+ %td(colspan="2") #{my_items.length}
In your index.xls.haml:
bc. # index.xls.haml
-= render 'my_thingies', :my_thingies => @my_thingies
+= render 'my_items', my_items: @my_items
In your index.html.haml:
bc. # index.html.haml
-= link_to 'Download XLS', my_thingies_url(:format => :xls)
-= render 'my_thingies', :my_thingies => @my_thingies
+= link_to 'Download spreadsheet', my_items_url(format: :xlsx)
+= render 'my_items', my_items: @my_items
h3. Formatting
-You can use class names on td/th for typed values. Here is the list of class to type mapping:
+You can define formats in your view file (local to the view) or in the initializer
+
+bc. format_xls 'table.my-table' do
+ workbook use_autowidth: true
+ sheet orientation: landscape
+ format 'th', b: true # bold
+ format 'tbody tr', color: lambda { |row| 'ddffdd' if row.index.odd? }
+ format 'A3:B10', i: true # italic
+ format column: 0, width: 35
+ format 'td.custom', lambda { |cell| modify cell somehow.}
+ # default value (fallback value when value is blank or 0 for integer / float)
+ default 'td.price', 10
+
+For the full list of supported properties head here: http://rubydoc.info/github/randym/axlsx/Axlsx/Cell
+In addition, for column formats, Axlsx columnInfo properties are also supported
+
+h3. Themes
+
+You can define "themes" - blocks of formatting code:
+
+bc. ToSpreadsheet.theme :zebra do
+ format 'tr', color: lambda { |row| 'ddffdd' if row.index.odd? }
+
+And then use them:
+
+bc. format_xls 'table.zebra', ToSpreadsheet.theme(:zebra)
+
+h3. Types
+
+The default theme uses class names on td/th to cast values.
+Here is the list of class to type mapping:
|_. ==CSS== class |_. Format |
| decimal or float | Decimal |
| num or int | Integer |
-| datetime | DateTime |
-| date | Date |
-| time | Time |
+| datetime | DateTime (Chronic.parse) |
+| date | Date (Date.parse) |
+| time | Time (Chronic.parse) |
+
+h3. Styling
+
h3. Default values
View
18 Rakefile
@@ -7,10 +7,16 @@ require 'rdoc/task'
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
-task :generate_pdf do
- Haml::Engine.new(File.read('spec/support/table.html.haml').render
- xls_io = ToSpreadsheet::XLS.to_io(html)
- f = File.open('/tmp/spreadsheet.xls', 'wb')
- Spreadsheet.writer(f).write_workbook(spreadsheet, f)
- f.close
+task :env do
+ $: << File.expand_path('lib', File.dirname(__FILE__))
+ require 'to_spreadsheet'
+ include ToSpreadsheet::Helpers
+end
+
+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)
+ puts "Written to #{path}"
end
View
18 lib/to_spreadsheet.rb
@@ -2,7 +2,23 @@
require 'to_spreadsheet/action_pack_renderers'
require 'to_spreadsheet/mime_types'
require 'to_spreadsheet/version'
+require 'to_spreadsheet/helpers'
+require 'to_spreadsheet/context'
module ToSpreadsheet
+ class << self
+ attr_accessor :context
-end
+ def theme(name, &formats)
+ @themes ||= {}
+ if formats
+ @themes[name] = formats
+ else
+ @themes[name]
+ end
+ end
+ end
+end
+
+require 'to_spreadsheet/themes/default'
+ToSpreadsheet.context = ToSpreadsheet::Context.new.apply ToSpreadsheet.theme(:default)
View
17 lib/to_spreadsheet/action_pack_renderers.rb
@@ -2,22 +2,27 @@
require 'action_controller/metal/renderers'
require 'action_controller/metal/responder'
-require 'to_spreadsheet/xlsx'
+require 'to_spreadsheet/axlsx/renderer'
# This will let us do thing like `render :xlsx => 'index'`
-# This is also how Rails internally implements its :json and :xml renderers
-# Rarely used, nevertheless public API
+# 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'
- send_data ToSpreadsheet::XLSX.generate(render_to_string(options[:template], options)).to_stream.read, :type => :xlsx, :disposition => "attachment; filename=\"#{filename}.xlsx\""
+
+ html = with_context ToSpreadsheet.context.derive do
+ render_to_string(options[:template], options)
+ end
+
+ data = ToSpreadsheet::Axlsx::Renderer.to_data(html)
+ send_data data, type: :xlsx, disposition: %(attachment; filename="#{filename}.xlsx")
end
class ActionController::Responder
# This sets up a default render call for when you do
# respond_to do |format|
- # format.xls
+ # format.xlsx
# end
def to_xlsx
- controller.render :xlsx => controller.action_name
+ controller.render xlsx: controller.action_name
end
end
View
86 lib/to_spreadsheet/axlsx/formatter.rb
@@ -0,0 +1,86 @@
+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
View
52 lib/to_spreadsheet/axlsx/renderer.rb
@@ -0,0 +1,52 @@
+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
View
130 lib/to_spreadsheet/context.rb
@@ -0,0 +1,130 @@
+require 'to_spreadsheet/context/pairing'
+require 'to_spreadsheet/formats'
+
+module ToSpreadsheet
+ class Context
+ include Pairing
+
+ 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)
+ 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
+ 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
+ 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
+ self
+ end
+
+ # format 'td.b', b: true # bold
+ # format column: 0, width: 50
+ # format 'A1:C30', b: true
+ # Accepted properties: http://rubydoc.info/github/randym/axlsx/Axlsx/Cell
+ # 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
+ 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
+ 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
+ end
+
+ def apply(theme = nil, &block)
+ add_format &theme if theme
+ add_format &block if block
+ self
+ end
+
+ def derive
+ derived = dup
+ derived.current_format = derived.current_format.derive
+ derived
+ 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]
+ else
+ return [:css, selector]
+ end
+ end
+ [:column, :row, :range].each do |key|
+ return [key, options.delete(key)] if options.key?(key)
+ end
+ selector
+ end
+ end
+end
View
26 lib/to_spreadsheet/context/pairing.rb
@@ -0,0 +1,26 @@
+module ToSpreadsheet
+ class Context
+ # Associating Axlsx entities and Nokogiri nodes
+ module Pairing
+ def assoc!(entity, node)
+ @entity_to_node ||= {}
+ @node_to_entity ||= {}
+ @entity_to_node[entity] = node
+ @node_to_entity[node] = entity
+ end
+
+ def entity_from_node(node)
+ @node_to_entity[node]
+ end
+
+ def node_from_entity(entity)
+ @entity_to_node[entity]
+ end
+
+ def clear_assoc!
+ @entity_to_node = {}
+ @node_to_entity = {}
+ end
+ end
+ end
+end
View
56 lib/to_spreadsheet/formats.rb
@@ -0,0 +1,56 @@
+module ToSpreadsheet
+ class Formats
+ attr_accessor :styles_by_type
+ attr_writer :sheet_props
+
+ 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
+
+ # Sheet props without selectors
+ 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
View
24 lib/to_spreadsheet/helpers.rb
@@ -0,0 +1,24 @@
+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
+ end
+
+ def with_context(context, &block)
+ context_was = self.context
+ @context = context
+ result = block.call
+ @context = context_was
+ result
+ end
+ end
+end
View
37 lib/to_spreadsheet/themes/default.rb
@@ -0,0 +1,37 @@
+module ToSpreadsheet::Themes
+ module Default
+ ::ToSpreadsheet.theme :default do
+ workbook use_autowidth: true
+ sheet page_setup: {
+ fit_to_height: 1,
+ fit_to_width: 1,
+ orientation: :landscape
+ }
+ # Set value type based on CSS class
+ format 'td,th', lambda { |cell|
+ val = cell.value
+ case node_from_entity(cell)[:class]
+ when /decimal|float/
+ cell.type = :float
+ when /num|int/
+ cell.type = :integer
+ when /bool/
+ cell.type = :boolean
+ # Parse (date)times and dates with Chronic and Date.parse
+ when /datetime|time/
+ val = Chronic.parse(val)
+ if val
+ cell.type = :time
+ cell.value = val
+ end
+ when /date/
+ val = (Date.parse(val) rescue val)
+ if val.present?
+ cell.type = :date
+ cell.value = val
+ end
+ end
+ }
+ end
+ end
+end
View
2  lib/to_spreadsheet/version.rb
@@ -1,3 +1,3 @@
module ToSpreadsheet
- VERSION = '0.9.3.1'
+ VERSION = '1.0.0.rc1'
end
View
115 lib/to_spreadsheet/xlsx.rb
@@ -1,115 +0,0 @@
-# Style attributes:
-# bg_color
-# fg_color
-# b = bold
-# i = italic
-# u = underline
-# strike
-# outline
-# shadow
-# charset
-# family
-# font_name
-# hidden
-# locked
-# alignment
-# border = { :style => ?, :color => ?, :edges => [:left, :right, :top, :bottom] }
-#
-module ToSpreadsheet
- require 'axlsx'
- module XLSX
- extend self
-
- # Generate an AXLSX Package for the passed HTML table
- def generate(html)
- package = Axlsx::Package.new
- package.use_autowidth = false
- spreadsheet = package.workbook
-
- sheet = nil
- row = nil
- colwidths = []
- Nokogiri::HTML::Document.parse(html).css('table').each_with_index do |xml_table, i|
- sheetname = xml_table.css('caption').inner_text.presence || xml_table['name'] || "Sheet #{i + 1}"
-
- page_setup_options = eval ( xml_table['page_setup'] ) rescue nil
- page_setup_options = { fit_to_height: 1, fit_to_width: 1, orientation: :landscape } if page_setup_options.nil?
-
- sheet = spreadsheet.add_worksheet(:name => sheetname, :page_setup => page_setup_options)
- row = 0
- colwidths = [] # cache column widths as we go through the table, only set it once at the end
-
- xml_table.css('tr').each do |row_node|
- xlsrow = sheet.add_row
-
- if ! row_node[:xls_style].nil? && ! row_node[:xls_style].empty?
- row_stylehash = eval( row_node[:xls_style] ) rescue {}
- else
- row_stylehash = {}
- end
-
- row_node.css('th,td').each_with_index do |col_node, col|
-
- if ( ! col_node[:xls_style].nil? && ! col_node[:xls_style].empty? ) || ! row_stylehash.empty?
- stylehash = eval( col_node[:xls_style] ) rescue {}
- merged = stylehash.merge(row_stylehash)
- style = spreadsheet.styles.add_style merged
- xlscol = xlsrow.add_cell typed_node_val(col_node), :style => style
- else
- xlscol = xlsrow.add_cell typed_node_val(col_node)
- end
-
- mywidth = xlscol.value.to_s.length
- mywidth += 3 if spreadsheet.styles.fonts[spreadsheet.styles.cellXfs[xlscol.index].fontId].b rescue false
-
- if colwidths[xlscol.index].nil?
- sheet.column_info[xlscol.index].width = col_node[:width].to_i if col_node[:width]
-
- # AB: Not quite sure what this is for?
- if colwidths[xlscol.index] && mywidth > colwidths[xlscol.index]
- colwidths[xlscol.index] = mywidth
- end
- end
-
- end
-
- row += 1
- end
- colwidths.each_with_index.map { |len, i| sheet.column_info[i].width = len }
- row += 1 # extra space between tables on same sheet
- end
- package
- end
-
- private
-
- def typed_node_val(node)
- val = val_or_default(node)
-
- case node[:class]
- when /decimal|float/
- val.to_f
- when /num|int/
- val.to_i
- when /datetime/
- DateTime.parse(val) rescue val
- when /date/
- Date.parse(val) rescue val
- when /time/
- Time.parse(val) rescue val
- else
- val
- end
- end
-
- def val_or_default(node)
- val = node.inner_text
- if val.blank?
- node['data-default']
- else
- val
- end
- end
-
- end
-end
View
29 spec/defaults_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+
+
+describe ToSpreadsheet::Axlsx::Formatter do
+ let :spreadsheet do
+ build_spreadsheet(haml: <<HAML)
+- format_xls 'table' do
+ - default 'td.price', 100
+%table
+ %tr
+ %td.price
+ %td.price 50
+HAML
+ end
+
+ let :row do
+ spreadsheet.workbook.worksheets[0].rows[0]
+ end
+
+ context 'default values' do
+ it 'get set when the cell is empty' do
+ row.cells[0].value.should == 100
+ end
+ it 'does not get set when the cell is not empty' do
+ row.cells[1].value.should == 50
+ end
+ end
+end
View
29 spec/format_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe ToSpreadsheet::Axlsx::Formatter do
+ let :spreadsheet do
+ build_spreadsheet haml: <<-HAML
+:ruby
+ format_xls do
+ format column: 0, width: 25
+ format 'tr', color: lambda { |row| 'cccccc' if row.index.odd? }
+ end
+%table
+ %tr
+ %th
+ %tr
+ %td
+ HAML
+ end
+
+ let(:sheet) { spreadsheet.workbook.worksheets[0] }
+
+ context 'local styles' do
+ it 'sets column width' do
+ sheet.column_info[0].width.should == 25
+ end
+ it 'runs lambdas' do
+ sheet.rows[1].cells[0].color.rgb.should == Axlsx::Color.new(rgb: 'cccccc').rgb
+ end
+ end
+end
View
15 spec/spec_helper.rb
@@ -2,4 +2,17 @@
require 'rspec/autorun'
$: << File.expand_path('../lib', __FILE__)
require 'to_spreadsheet'
-require 'haml'
+require 'haml'
+
+RSpec.configure do |config|
+ include ToSpreadsheet::Helpers
+end
+
+def build_spreadsheet(src = {})
+ haml = if src[:haml]
+ src[:haml]
+ elsif src[:file]
+ File.read(File.expand_path "support/#{src[:file]}", File.dirname(__FILE__))
+ end
+ ToSpreadsheet::Axlsx::Renderer.to_package(Haml::Engine.new(haml).render)
+end
View
14 spec/support/table.html.haml
@@ -1,3 +1,13 @@
+:ruby
+ format_xls do
+ format 'tr', color: lambda { |row| 'ddffdd' if row.index.even? }
+ format 'th', b: true
+ default 'td.num', 100
+ end
+ format_xls 'table#two' do
+ format 'td', color: Axlsx::Color.new(rgb: 'ffaaaa')
+ end
+
%table
%caption A worksheet
%thead
@@ -12,10 +22,10 @@
%td.date 27/05/1991
%tr
%td John
- %td.num{ data: { null: 100 } }
+ %td.num
%td.date
-%table
+%table#two
%caption Another worksheet
%thead
%tr
View
37 spec/types_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe ToSpreadsheet::Themes::Default do
+ let :spreadsheet do
+ build_spreadsheet haml: <<-HAML
+
+%table
+ %tr
+ %td.num 20
+ %td.float 1
+ %td.date 27/05/1991
+ %td.date
+HAML
+ end
+
+ let :row do
+ spreadsheet.workbook.worksheets[0].rows[0]
+ end
+
+ context 'data types' do
+ it 'num' do
+ row.cells[0].value.should == 20
+ end
+
+ it 'float' do
+ row.cells[1].type.should be :float
+ end
+
+ it 'date' do
+ row.cells[2].type.should be :date
+ end
+
+ it 'empty date' do
+ row.cells[3].type.should_not be :date
+ end
+ end
+end
View
16 spec/worksheets_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe ToSpreadsheet::Axlsx::Renderer do
+ let :spreadsheet do
+ build_spreadsheet haml: <<-HAML
+%table
+%table
+ HAML
+ end
+
+ context 'worksheets' do
+ it 'are created 1 per <table>' do
+ spreadsheet.workbook.should have(2).worksheets
+ end
+ end
+end
View
81 spec/xlsx_spec.rb
@@ -1,81 +0,0 @@
-require 'spec_helper'
-
-describe ToSpreadsheet::XLSX do
- let(:spreadsheet) {
- html = Haml::Engine.new(TEST_HAML).render
- package = ToSpreadsheet::XLSX.generate(html)
- }
-
- it 'creates multiple worksheets' do
- spreadsheet.workbook.should have(2).worksheets
- end
-
- it 'supports num format' do
- spreadsheet.workbook.worksheets[0].rows[1].cells[1].value.should == 20
- end
-
- it 'support float format' do
- spreadsheet.workbook.worksheets[1].rows[1].cells[1].type.should be(:float)
- end
-
- it 'supports date format' do
- spreadsheet.workbook.worksheets[0].rows[1].cells[2].type.should be(:date)
- end
-
- it 'parses null dates' do
- spreadsheet.workbook.worksheets[0].rows[2].cells[2].type.should_not be(:date)
- end
-
- it 'parses default values' do
- spreadsheet.workbook.worksheets[0].rows[2].cells[1].value.should == 100
- end
-
- it 'sets column width based on th width' do
- spreadsheet.workbook.worksheets[0].column_info[0].width.should == 25
- end
-
- it 'sets column width based on td width' do
- spreadsheet.workbook.worksheets[1].column_info[1].width.should == 35
- end
-
- # The test spreadsheet will be saved to /tmp/spreadsheet.xls
- it 'writes to disk' do
- spreadsheet.serialize('/tmp/spreadsheet.xlsx')
- File.exists?('/tmp/spreadsheet.xlsx').should == true
- end
-
-end
-
-TEST_HAML = <<-HAML
-
-%table
- %caption A worksheet
- %thead
- %tr
- %th{ width: 25 } Name
- %th Age
- %th Date
- %tbody
- %tr
- %td Gleb
- %td.num 20
- %td.date 27/05/1991
- %tr
- %td John
- %td.num{ data: { default: 100 } }
- %td.date
-
-%table
- %caption Another worksheet
- %thead
- %tr
- %th Name
- %th Age
- %th Date
- %tbody
- %tr
- %td Alice
- %td.float{ width: 35 } 19.5
- %td.date 10/05/1991
-
-HAML
View
2  to_spreadsheet.gemspec
@@ -24,7 +24,7 @@ Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.files = include_files - exclude_files
s.require_path = "lib"
- s.test_files = Dir["test/**/test_*.rb"]
+ s.test_files = Dir["spec/**/*_spec.rb"]
s.has_rdoc = true
s.extra_rdoc_files = Dir["README*"]
s.add_dependency 'rails'
Please sign in to comment.
Something went wrong with that request. Please try again.