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.
- checksums.yaml +7 -0
- data/lib/ruby-latex.rb +411 -0
- 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: []
|