ruby-latex 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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/ruby-latex.rb +411 -0
  3. metadata +64 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c77fd912c5a1fd4c97cd6c3df883bb8e0241df501f1a18cc7dfb1d4029ee161d
4
+ data.tar.gz: e4b334a03b8ea96a2af93d09196732c09e5f4ded4971a5070ad2a4b27224dea3
5
+ SHA512:
6
+ metadata.gz: 1ca80bf37a1866eb4ee26555d209f2cbeb79600bc9a146b41f7edfbc9bacc0deb1eab09b15c310c23a57febea4a59bea680673f6d48049f32e95f5955bd32f6a
7
+ data.tar.gz: 86cb4a25cfb183d97ee1f0b3c1318c05d66cbfb588242da37bddb2c7956fdcda0ab40777cb92ab1f836f3605d54a0b0978d80057028bfbbd4cec8a3db41fc72a
data/lib/ruby-latex.rb ADDED
@@ -0,0 +1,411 @@
1
+ require 'code-assertions'
2
+
3
+ module Latex
4
+ class Table
5
+ attr_accessor :caption
6
+ attr_accessor :label
7
+
8
+ def initialize(caption = nil, label = nil)
9
+ @caption = caption
10
+ @label = label
11
+ @tabulars = []
12
+ end
13
+
14
+ def auto_resize
15
+ @resize = true
16
+
17
+ return self
18
+ end
19
+
20
+ def add_tabular(tabular)
21
+ raise TypeError, "No implicit conversion from #{tabular.class} to Latex::Tabular" unless tabular.is_a?(Tabular)
22
+
23
+ @tabulars << tabular
24
+
25
+ return self
26
+ end
27
+
28
+ def tabular=(tabular)
29
+ @tabulars.clear
30
+ add_tabular(tabular)
31
+
32
+ return nil
33
+ end
34
+
35
+ def to_tex
36
+ out = []
37
+ out << "\\begin{table}"
38
+ out << self.caption_string
39
+ @tabulars.each do |tabular|
40
+ out << tabular.to_tex
41
+ end
42
+ out << "\\end{table}"
43
+
44
+ return out.join("\n")
45
+ end
46
+
47
+ private
48
+ def caption_string
49
+ out = []
50
+ out << " \\caption{#@caption}" if @caption
51
+ out << " \\label{#@label}" if @caption && @label
52
+
53
+ return out.join("\n")
54
+ end
55
+ end
56
+
57
+ class Tabular
58
+ def initialize(cols)
59
+ @header = nil
60
+
61
+ if cols.is_a? Array
62
+ @columns = cols.map { |e| e.to_sym }
63
+ @columns_info = cols.map { |e| [e, Column.new] }.to_h
64
+
65
+ elsif cols.is_a? Hash
66
+ keys = cols.keys
67
+ @columns_info = cols.clone
68
+ @columns = keys.map { |k| k.to_sym }
69
+
70
+ if @columns_info.values.any? { |c| c.header }
71
+ headers_row = @columns.map { |c| [c, @columns_info[c].header.to_s] }.to_h
72
+ @header = HeaderRowFamily.new(@columns_info, Row.new(headers_row))
73
+ end
74
+
75
+ else
76
+ raise "Invalid argument: expected Array or Hash"
77
+ end
78
+
79
+ @rows = RowFamily.new(@columns_info)
80
+ end
81
+
82
+ def header_style(&block)
83
+ raise "Block expected" unless block_given?
84
+ raise "No header to style" unless @header
85
+ @header.styler = block
86
+
87
+ return self
88
+ end
89
+
90
+ def content_style(&block)
91
+ raise "Block expected" unless block_given?
92
+ @rows.styler = block
93
+
94
+ return self
95
+ end
96
+
97
+ def header=(row)
98
+ @header = HeaderRowFamily.new(@columns_info, Row.new(row))
99
+ end
100
+
101
+ def add_row(row)
102
+ @rows << Row.new(row)
103
+ end
104
+ alias :<< :add_row
105
+
106
+ def add_separator(type="\\midrule")
107
+ @rows << RowSeparator.new(type)
108
+ end
109
+
110
+ def to_tex
111
+ sizes = self.column_sizes
112
+
113
+ out = []
114
+ out << " \\begin{tabular}{#{self.align_string}}"
115
+ out << " \\toprule"
116
+ if @header
117
+ out << " " + @header.row.to_tex(sizes)
118
+ out << " \\midrule"
119
+ end
120
+ out += @rows.rows.map { |row| " " + row.to_tex(sizes) }
121
+ out << " \\bottomrule"
122
+ out << " \\end{tabular}"
123
+
124
+ return out.join("\n")
125
+ end
126
+
127
+ def column_sizes
128
+ header_sizes = @header ? [ @header.row.sizes ] : []
129
+ sizes = @rows.rows.map { |row| row.sizes } + header_sizes
130
+ @columns.map { |col| [col, (sizes.map { |e| e[col] }).max] }.to_h
131
+ end
132
+
133
+ def align_string
134
+ @columns.map { |col| @columns_info[col].is_a?(Column) ? @columns_info[col].align : auto_align(col) }.join(" ")
135
+ end
136
+
137
+ def auto_align(col)
138
+ @rows.rows.map { |row| row[col] }.all? { |e| e.is_a?(Numeric) } ? 'r' : 'l'
139
+ end
140
+ end
141
+
142
+ class RowFamily
143
+ attr_reader :rows
144
+ attr_reader :columns
145
+
146
+ def initialize(columns)
147
+ @columns = columns
148
+ @rows = []
149
+
150
+ @styler = proc { |e| e }
151
+ end
152
+
153
+ def columns
154
+ @columns.keys
155
+ end
156
+
157
+ def format(element, col)
158
+ @columns[col].formatter.call(element)
159
+ end
160
+
161
+ def style(element)
162
+ @styler.call(element)
163
+ end
164
+
165
+ def <<(row)
166
+ row.family = self
167
+
168
+ @rows << row
169
+ end
170
+
171
+ def formatter=(formatter)
172
+ @formatter = formatter
173
+ end
174
+
175
+ def styler=(styler)
176
+ @styler = styler
177
+ end
178
+ end
179
+
180
+ class SingleRowFamily < RowFamily
181
+ def initialize(columns, row)
182
+ super(columns)
183
+
184
+ row.family = self
185
+ @rows = [row]
186
+ end
187
+
188
+ def row
189
+ @rows.first
190
+ end
191
+
192
+ def <<(row)
193
+ raise "Unable to add new rows to a SingleRowFamily"
194
+ end
195
+ end
196
+
197
+ class HeaderRowFamily < SingleRowFamily
198
+ def initialize(columns, row)
199
+ super(columns, row)
200
+
201
+ @formatter = Formatters::StringFormatter.new
202
+ end
203
+
204
+ def format(element, col)
205
+ @formatter.call(element)
206
+ end
207
+ end
208
+
209
+ class Row
210
+ attr_accessor :family
211
+
212
+ def initialize(hash)
213
+ raise(WrongRowException, "Hash expected") unless hash.is_a?(Hash)
214
+
215
+ @row = hash
216
+ end
217
+
218
+ def family=(family)
219
+ raise(WrongRowException, "The row must have the following headers: #{family.columns}") unless @row.keys.sort == family.columns.sort
220
+
221
+ @family = family
222
+ end
223
+
224
+ def row
225
+ @row
226
+ end
227
+
228
+ def [](col)
229
+ @row[col]
230
+ end
231
+
232
+ def sizes
233
+ styled = self.styled
234
+ @family.columns.map { |col| [col, styled[col].length] }.to_h
235
+ end
236
+
237
+ def formatted
238
+ @family.columns.map { |col| [col, @family.format(row[col], col) ] }.to_h
239
+ end
240
+
241
+ def styled
242
+ self.formatted.map { |col, content| [col, @family.style(content) ] }.to_h
243
+ end
244
+
245
+ def to_tex(column_sizes)
246
+ self.styled.map { |col, content| (" " * (column_sizes[col] - content.length)) + content }.join(" & ") + " \\\\"
247
+ end
248
+
249
+ class WrongRowException < Exception
250
+ end
251
+ end
252
+
253
+ class Column
254
+ attr_reader :align
255
+ attr_reader :formatter
256
+ attr_reader :header
257
+
258
+ def initialize
259
+ @formatter = Formatters::StringFormatter.new
260
+
261
+ self.with_left_align
262
+ end
263
+
264
+ def with_left_align
265
+ self.with_align('l')
266
+ end
267
+
268
+ def with_right_align
269
+ self.with_align('r')
270
+ end
271
+
272
+ def with_center_align
273
+ self.with_align('c')
274
+ end
275
+
276
+ def with_integer_formatter(comma=true)
277
+ self.with_formatter(Formatters::IntegerFormatter.new(comma))
278
+ end
279
+
280
+ def with_decimal_formatter(n=2, comma=true)
281
+ self.with_formatter(Formatters::DecimalFormatter.new(n, comma))
282
+ end
283
+
284
+ def with_align(align)
285
+ @align = align
286
+ return self
287
+ end
288
+
289
+ def with_header(header)
290
+ @header = header
291
+ return self
292
+ end
293
+
294
+ def with_formatter(formatter)
295
+ @formatter = formatter
296
+ return self
297
+ end
298
+ end
299
+
300
+ class RowSeparator
301
+ def initialize(type)
302
+ @content = type
303
+ end
304
+
305
+ def family=(family)
306
+ @columns = family.columns
307
+ end
308
+
309
+ def row
310
+ nil
311
+ end
312
+
313
+ def sizes
314
+ @columns.map { |e| [e, 0] }.to_h
315
+ end
316
+
317
+ def formatted
318
+ @content
319
+ end
320
+
321
+ def styled
322
+ @content
323
+ end
324
+
325
+ def to_tex(*args)
326
+ @content
327
+ end
328
+ end
329
+
330
+ module Parsers
331
+ class TableParser
332
+ def self.parse(text)
333
+ table_space = ['\\begin{table}', '\\end{table}'].map { |s| text.index(s) }
334
+ table_space[0] += 13 # length of '\begin{table}'
335
+
336
+ table_text = text[*table_space]
337
+
338
+ caption = table_text.scan(/\\caption\{([^}]*)\}/).flatten[0]
339
+ label = table_text.scan(/\\label\{([^}]*)\}/).flatten[0]
340
+
341
+ self.parse_tabular(table_text)
342
+ end
343
+
344
+ def self.parse_tabular(text)
345
+ tabular_space = ['\\begin{tabular}', '\\end{tabular}'].map { |s| text.index(s) }
346
+ tabular_space[0] += 15 # length of '\begin{tabular}'
347
+
348
+ tabular_text = text[*tabular_space]
349
+
350
+ align_string = tabular_text.scan(/\{([^}]*)\}/).flatten[0]
351
+ align = self.parse_align_string(align_string)
352
+
353
+ content_text = tabular_text[tabular_text.index('}') + 1, content_text.length]
354
+ content_text.lines.each do |line|
355
+
356
+ end
357
+ end
358
+
359
+ def self.parse_align_string(text)
360
+ # TODO implement
361
+ warn "Ignoring align string #{text}"
362
+ return nil
363
+ end
364
+ end
365
+ end
366
+
367
+ module Formatters
368
+ class StringFormatter
369
+ def call(object)
370
+ object.to_s
371
+ end
372
+ end
373
+
374
+ class IntegerFormatter
375
+ def initialize(comma=true, nums=nil)
376
+ @format = "%#{nums}d"
377
+ @comma = comma
378
+ end
379
+
380
+ def call(object)
381
+ result = @format % [object.to_i]
382
+ result = result.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,") if @comma
383
+ return result
384
+ end
385
+ end
386
+
387
+ class DecimalFormatter
388
+ def initialize(decimals=2, comma=true, nums=nil)
389
+ @format = "%#{nums}.#{decimals}f"
390
+ @comma = comma
391
+ end
392
+
393
+ def call(object)
394
+ result = @format % [object.to_f]
395
+ result = result.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,") if @comma
396
+ return result
397
+ end
398
+ end
399
+
400
+ class CustomFormatter
401
+ def initialize(&block)
402
+ raise "Block required" unless block_given?
403
+ @block = block
404
+ end
405
+
406
+ def call(object)
407
+ @block.call(object)
408
+ end
409
+ end
410
+ end
411
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-latex
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Simone Scalabrino
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-08-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: code-assertions
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.1.2
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.1.2
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: 1.1.2
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.1.2
33
+ description: This library allows to easily generate LaTeX content (e.g., tables) directly
34
+ from Ruby.
35
+ email: s.scalabrino9@gmail.com
36
+ executables: []
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - lib/ruby-latex.rb
41
+ homepage: https://github.com/intersimone999/ruby-latex
42
+ licenses:
43
+ - GPL-3.0-only
44
+ metadata: {}
45
+ post_install_message:
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.1.4
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: Generate LaTeX content from Ruby
64
+ test_files: []