spreadsheet_builder 0.0.1

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.
@@ -0,0 +1,163 @@
1
+ module SpreadsheetBuilder
2
+ class CssParser
3
+
4
+ def self.px_from_input(val, base = :width)
5
+ case val
6
+ when /%$/
7
+ val.to_f / 100 * { width: 1024, height: 16 }[base]
8
+ when /px$/
9
+ val.to_f
10
+ when /cm$/
11
+ val.to_f * 37.795275591
12
+ when /em$/
13
+ val.to_f * 16
14
+ when /pt$/
15
+ val.to_f * 16 / 12
16
+ else
17
+ nil
18
+ end
19
+ end
20
+
21
+ def self.pt_from_input(val, base = :width)
22
+ px_from_input(val, base) * 12 / 16
23
+ end
24
+
25
+ attr_reader :cache, :rules
26
+
27
+ def initialize
28
+ reset
29
+ end
30
+
31
+ def reset(level = :parser)
32
+ method = "reset_#{level}"
33
+
34
+ __send__(method)
35
+ end
36
+
37
+ def format_from_node(node)
38
+ format_from_klass_tree(klass_tree_from_node(node), node)
39
+ end
40
+
41
+ private
42
+ def accepted_keys
43
+ # TODO Keep these in a config
44
+ keys = %w{ color background-color font-size font-weight vertical-align text-align border border-width border-style border-color height width }
45
+ dirs = %w{ top bottom left right }
46
+ types = %w{ width style color }
47
+ dirs.each do |dir|
48
+ keys << "border-#{dir}"
49
+ types.each { |type| keys << "border-#{dir}-#{type}" }
50
+ end
51
+ keys
52
+ end
53
+
54
+ def klass_tree_from_node(base)
55
+ tree = (base.ancestors.reverse.drop(1) << base)
56
+ tree.map { |n, t| klass_node_from_node(n) }
57
+ end
58
+
59
+ def format_from_klass_tree(klass, node)
60
+ # klass is uniq to each node (because of first-child, nth-child, etc)
61
+ # so caching with the class is useless
62
+ # TODO find a better way to cache that works
63
+ if @cache[klass]
64
+ format = @cache[klass]
65
+ else
66
+ declarations = declarations_from_klass_tree(klass)
67
+ format = format_from_declarations(declarations, node)
68
+ @cache[klass] = format
69
+ end
70
+
71
+ format || {}
72
+ end
73
+
74
+ def klass_node_from_node(node)
75
+ name = node.name
76
+ root = node.ancestors.last
77
+ siblings = node.parent.element_children
78
+
79
+ index = siblings.index(node) + 1
80
+ first_of_kind = root.css(name).first == node
81
+ last_of_kind = root.css(name).last == node
82
+
83
+ klasses = node.attributes["class"] && node.attributes["class"].value
84
+ klasses = klasses.to_s.split(/ /).map { |k| "." + k }
85
+ klasses << name
86
+ klasses << "#{name}:nth-child(#{index})"
87
+ klasses << "#{name}:nth-child(odd)" if index.odd?
88
+ klasses << "#{name}:nth-child(even)" if index.even?
89
+ klasses << "#{name}:first-child)" if index == 1
90
+ klasses << "#{name}:last-child)" if index == siblings.length
91
+ klasses << "#{name}:first-of-kind)" if first_of_kind
92
+ klasses << "#{name}:last-of-kind)" if last_of_kind
93
+ klasses
94
+ end
95
+
96
+ def find_rules(tree)
97
+ @rules.each_with_object(Hash.new {|h,k| h[k] = []}) do |rule,rules_found|
98
+ found = rule.find_selector(tree)
99
+ rules_found[found.length] << rule if found
100
+ end
101
+ end
102
+
103
+ def reset_none
104
+ end
105
+
106
+ def reset_rules(parser = @parser)
107
+ @rules = []
108
+ parser.each_rule_set(:all) { |r|
109
+ @rules << SpreadsheetBuilder::CssRule.new(r)
110
+ }
111
+ @rules.delete_if { |rule|
112
+ rule.each_declaration do |key,_|
113
+ rule.remove_declaration!(key) unless accepted_keys.include?(key)
114
+ end
115
+ rule.declarations.empty?
116
+ }
117
+ end
118
+
119
+ def translate_declaration(key, val)
120
+ f = TRANSLATIONS[key].call(val) if key && val
121
+ f || {}
122
+ end
123
+
124
+ def format_from_declarations(declarations, node)
125
+ denied = Hash.new { |h,k| h[k] = [] }.merge(
126
+ "table" => %w{ width height },
127
+ "tr" => %w{ width }
128
+ )
129
+ declarations.delete_if { |k,v|
130
+ v.nil? || v.empty? || denied[node.name].include?(k)
131
+ }
132
+
133
+ declarations.each_with_object({}) { |(k,v), format|
134
+ format.merge!(translate_declaration(k,v[:value]))
135
+ }
136
+ end
137
+
138
+ def declarations_from_klass_tree(klass)
139
+ rules = find_rules(klass).sort_by { |specificity,_| specificity }
140
+ declarations = rules.map { |_,rules|
141
+ rules.inject({}) { |dec, r| dec.merge(r.declarations) }
142
+ }.inject({},&:merge)
143
+ end
144
+
145
+ def reset_cache
146
+ @cache = {}
147
+ end
148
+
149
+ def reset_parser
150
+ parser = CssParser::Parser.new
151
+ # TODO load these files from a config
152
+ parser.load_uri!("file://#{Dir.pwd}/test2.css")
153
+ # TODO or even better parse the html doc for spreadsheet links
154
+ # and load those
155
+ #parser.load_uri!("file://#{Dir.pwd}/test2.css")
156
+
157
+ # Explicity reset rules to avoid infinite loop or bad data
158
+ reset_rules(parser)
159
+ reset_cache
160
+ @parser = parser
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,42 @@
1
+ module SpreadsheetBuilder
2
+ class CssRule
3
+ def initialize(rule)
4
+ @rule = rule
5
+ end
6
+
7
+ def declarations
8
+ @rule.instance_variable_get(:@declarations)
9
+ end
10
+
11
+ def selectors
12
+ @rule.selectors.map { |s|
13
+ s = s.split(/[\s>]/).map { |node| node.split('.') }
14
+ s.each do |node|
15
+ n = node.length
16
+ node[1...n] = node[1...n].map { |k| '.' + k }
17
+ node.delete_if { |k| k.empty? || k == "." }
18
+ end
19
+ }.sort_by(&:length)
20
+ end
21
+
22
+ def find_selector(tree)
23
+ selectors.find { |s|
24
+ next unless tree.last & s.last == s.last
25
+
26
+ s[0..-2].reverse.inject(tree.length - 2) do |i, s_node|
27
+ break false unless i
28
+ tree[0..i].index { |kt_node| kt_node & s_node == s_node }
29
+ end
30
+ }
31
+ end
32
+
33
+ private
34
+ def method_missing(method, *attrs, &block)
35
+ if @rule.respond_to?(method)
36
+ @rule.__send__(method, *attrs, &block)
37
+ else
38
+ super
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,31 @@
1
+ module SpreadsheetBuilder
2
+ def self.from_data(data)
3
+ builder = Builder.new
4
+ [:merges, :row_heights, :col_widths].each do |d|
5
+ if data[d]
6
+ builder.instance_variable_get("@#{d}".to_sym).merge!(data[d])
7
+ end
8
+ end
9
+ if data[:sheets]
10
+ builder.instance_variable_set(:@sheets, data[:sheets])
11
+ end
12
+ if data[:cells]
13
+ data[:cells].group_by { |c| c[:sheet] }.each do |sheet, cells|
14
+ sheet ||= 0
15
+ if sheet.respond_to?(:to_str)
16
+ builder.set_sheet_by_name(sheet)
17
+ else
18
+ builder.set_sheet(sheet)
19
+ end
20
+ cells.each do |cell|
21
+ # row and col required
22
+ builder.set_cell_value(cell[:row], cell[:col], cell[:value])
23
+ if format = cell[:format]
24
+ builder.add_format_to_cell(cell[:row], cell[:col], format)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ builder
30
+ end
31
+ end
@@ -0,0 +1,96 @@
1
+ module SpreadsheetBuilder
2
+ class HtmlParser
3
+ def self.from_slim(file, options = {}, context = self, &block)
4
+ html = Slim::Template.new(file, options).render(context, &block)
5
+ new(html)
6
+ end
7
+
8
+ def self.from_erb(file)
9
+ html = File.read(file)
10
+ template = ERB.new(html)
11
+ html = template.result
12
+
13
+ new(html)
14
+ end
15
+
16
+ def initialize(html)
17
+ @html = html
18
+ @css = SpreadsheetBuilder::CssParser.new
19
+ end
20
+
21
+ def build(force_level = :none)
22
+ SpreadsheetBuilder.from_data(to_data(force_level))
23
+ end
24
+
25
+ # TODO clean this up
26
+ def to_data(force_level = :none)
27
+ @css.reset(force_level)
28
+
29
+ # need to check for attributes colspan and row span
30
+ cells = []
31
+ merges = []
32
+ col_widths = {}
33
+ row_heights = {}
34
+
35
+ doc = Nokogiri::HTML(@html)
36
+ tb = doc.css('table').first
37
+
38
+ # ignoring specified formats for anything other than table tr td/th
39
+ tb_format = @css.format_from_node(tb)
40
+
41
+ doc.css('tr').each_with_index do |tr, row|
42
+ tr_format = tb_format.merge(@css.format_from_node(tr))
43
+
44
+ tr.css('td, th').each_with_index do |td, col|
45
+
46
+ rowheight = td.attributes["rowheight"]
47
+ colwidth = td.attributes["colwidth"]
48
+ rowspan = td.attributes["rowspan"]
49
+ colspan = td.attributes["colspan"]
50
+
51
+ rowheight &&= rowheight.value.to_i
52
+ colwidth &&= colwidth.value.to_i
53
+ rowspan &&= rowspan.value.to_i
54
+ colspan &&= colspan.value.to_i
55
+
56
+ add_td_to_cells(row, col, td, tr_format, cells)
57
+ if colspan
58
+ (1..colspan-1).each {|t|
59
+ add_td_to_cells(row, col+t, td, tr_format, cells)
60
+ }
61
+ end
62
+ if rowspan
63
+ (1..rowspan-1).each {|t|
64
+ add_td_to_cells(row+t, col, td, tr_format, cells)
65
+ }
66
+ end
67
+ if colspan || rowspan
68
+ merges << [
69
+ row, col, row + (rowspan || 1)-1, col + (colspan || 1)-1
70
+ ]
71
+ end
72
+ end
73
+ end
74
+
75
+ { cells: cells, merges: { 0 => merges } }
76
+ end
77
+
78
+ private
79
+ # TODO Document
80
+ def add_td_to_cells(row, col, td, tr_format, cells)
81
+ found = cells.find { |cell| cell[:row] == row && cell[:col] == col}
82
+ unless found
83
+ td_format = tr_format.merge(@css.format_from_node(td))
84
+ cells << {
85
+ row: row,
86
+ col: col,
87
+ value: td.text.strip,
88
+ format: td_format,
89
+ path: td.css_path
90
+ }
91
+ else
92
+ add_td_to_cells(row, col + 1, td, tr_format, cells)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,32 @@
1
+ module SpreadsheetBuilder
2
+ Rgb = Spreadsheet::Excel::Rgb
3
+
4
+ CUSTOM_PALETTE = {
5
+ :xls_color_0 => Rgb.new(0,0,0),
6
+ :xls_color_1 => Rgb.new(255,255,255),
7
+ :xls_color_2 => Rgb.new(204,204,204),
8
+ :xls_color_3 => Rgb.new(249,249,249)
9
+ }
10
+
11
+ PALETTE = Shade::Palette.new do |p|
12
+ Rgb.class_variable_get(:@@RGB_MAP).merge(CUSTOM_PALETTE).each do
13
+ |name, value|
14
+ p.add("##{value.to_i.to_s(16).ljust(6, "0")}", name.to_s)
15
+ end
16
+ end
17
+
18
+ module Palette
19
+ def self._color_from_input(input)
20
+ input = input.to_s
21
+ if input =~ /^rgb/i
22
+ _, r, g, b = input.match(/^rgba*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)[^\)]*/)
23
+ r, g, b = [r, g, b].map(&:to_i)
24
+ input = "##{Spreadsheet::Excel::Rgb.new(r, g, b).as_hex.ljust(6, "0")}"
25
+ end
26
+
27
+ # Assume a color is always found
28
+ color = PALETTE.nearest_value(input).name.to_sym
29
+ color
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,37 @@
1
+ module SpreadsheetBuilder
2
+ TRANSLATIONS = {}
3
+ TRANSLATIONS["text-align"] = Proc.new { |val|
4
+ allowed = %w{ left center right justify }
5
+ { horizontal_align: val.to_sym } if allowed.include?(val)
6
+ }
7
+ TRANSLATIONS["vertical-align"] = Proc.new { |val|
8
+ allowed = %{ top middle bottom }
9
+ { vertical_align: val.to_sym } if allowed.include?(val)
10
+ }
11
+ TRANSLATIONS["color"] = Proc.new { |val|
12
+ { color: Palette._color_from_input(val) }
13
+ }
14
+ TRANSLATIONS["background-color"] = Proc.new { |val|
15
+ { pattern_fg_color: Palette._color_from_input(val), pattern: 1 }
16
+ }
17
+ TRANSLATIONS["font-size"] = Proc.new { |val|
18
+ { size: SpreadsheetBuilder::CssParser.pt_from_input(val, :height) }
19
+ }
20
+ TRANSLATIONS["font-weight"] = Proc.new { |val|
21
+ accepted = %{ bold normal }
22
+ if accepted.inlcude?(val)
23
+ { weight: val }
24
+ end
25
+ }
26
+ %w{ border border-top border-bottom border-left border-right border-width border-top-width border-bottom-width border-left-width border-right-width }.each do
27
+ |key|
28
+ TRANSLATIONS[key] = Proc.new { |val| Border.new(key, val).format }
29
+ end
30
+ # TODO Prove these ratios
31
+ TRANSLATIONS["height"] = Proc.new { |val|
32
+ { height: SpreadsheetBuilder::CssParser.pt_from_input(val) }
33
+ }
34
+ TRANSLATIONS["width"] = Proc.new { |val|
35
+ { width: SpreadsheetBuilder::CssParser.px_from_input(val) / 7.5 }
36
+ }
37
+ end
data/pass_height.xls ADDED
Binary file
data/pass_size.xls ADDED
Binary file
data/pass_size2.xls ADDED
Binary file
data/pass_size3.xls ADDED
Binary file
Binary file
data/pass_width.xls ADDED
Binary file
@@ -0,0 +1,24 @@
1
+ current_dir = File.expand_path('..', __FILE__)
2
+ #extensions = %w{ rb yml haml erb slim html js json jbuilder }
3
+ #files = Dir.glob(current_dir + "/**/*.{#{extensions.join(',')}}")
4
+ files = Dir.glob(current_dir + '/**/*')
5
+ files.collect! {|file| file.sub(current_dir + '/', '')}
6
+ files.push('LICENSE')
7
+
8
+ Gem::Specification.new do |s|
9
+ s.name = 'spreadsheet_builder'
10
+ s.version = '0.0.1'
11
+ s.date = "#{Time.now.strftime("%Y-%m-%d")}"
12
+ s.homepage = 'https://github.com/jphager2/spreadsheet_builder'
13
+ s.summary = 'build xls spreadsheets'
14
+ s.description = 'A nice extension for building xls spreadsheets'
15
+ s.authors = ['jphager2']
16
+ s.email = 'jphager2@gmail.com'
17
+ s.files = files
18
+ s.license = 'MIT'
19
+
20
+ s.add_runtime_dependency 'nokogiri', '~> 1.6'
21
+ s.add_runtime_dependency 'spreadsheet', '~> 1.0'
22
+ s.add_runtime_dependency 'css_parser', '~> 1.3 '
23
+ s.add_runtime_dependency 'shade', '~> 0.0'
24
+ end