asciidoctor-pdf 1.5.0.alpha.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.adoc +22 -0
- data/NOTICE.adoc +76 -0
- data/README.adoc +263 -0
- data/Rakefile +78 -0
- data/bin/asciidoctor-pdf +15 -0
- data/bin/optimize-pdf +63 -0
- data/data/fonts/LICENSE-liberation-fonts-2.00.1 +102 -0
- data/data/fonts/LICENSE-mplus-testflight-58 +16 -0
- data/data/fonts/LICENSE-noto-fonts-2014-01-30 +201 -0
- data/data/fonts/liberationmono-bold-latin.ttf +0 -0
- data/data/fonts/liberationmono-bolditalic-latin.ttf +0 -0
- data/data/fonts/liberationmono-italic-latin.ttf +0 -0
- data/data/fonts/liberationmono-regular-latin.ttf +0 -0
- data/data/fonts/mplus1mn-bold-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-bolditalic-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-italic-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-regular-ascii-conums.ttf +0 -0
- data/data/fonts/mplus1p-bold-latin.ttf +0 -0
- data/data/fonts/mplus1p-light-latin.ttf +0 -0
- data/data/fonts/mplus1p-regular-latin.ttf +0 -0
- data/data/fonts/mplus1p-regular-multilingual.ttf +0 -0
- data/data/fonts/notoserif-bold-latin.ttf +0 -0
- data/data/fonts/notoserif-bolditalic-latin.ttf +0 -0
- data/data/fonts/notoserif-italic-latin.ttf +0 -0
- data/data/fonts/notoserif-regular-latin.ttf +0 -0
- data/data/themes/asciidoctor-theme.yml +174 -0
- data/data/themes/default-theme.yml +182 -0
- data/examples/chronicles.adoc +429 -0
- data/examples/chronicles.pdf +0 -0
- data/examples/example-pdf-screenshot.png +0 -0
- data/examples/example.adoc +27 -0
- data/examples/example.pdf +0 -0
- data/examples/sample-title-logo.jpg +0 -0
- data/examples/wolpertinger.jpg +0 -0
- data/lib/asciidoctor-pdf.rb +3 -0
- data/lib/asciidoctor-pdf/asciidoctor_ext.rb +1 -0
- data/lib/asciidoctor-pdf/asciidoctor_ext/section.rb +26 -0
- data/lib/asciidoctor-pdf/converter.rb +1365 -0
- data/lib/asciidoctor-pdf/core_ext/array.rb +5 -0
- data/lib/asciidoctor-pdf/core_ext/ostruct.rb +9 -0
- data/lib/asciidoctor-pdf/implicit_header_processor.rb +59 -0
- data/lib/asciidoctor-pdf/pdfmarks.rb +30 -0
- data/lib/asciidoctor-pdf/prawn_ext.rb +3 -0
- data/lib/asciidoctor-pdf/prawn_ext/coderay_encoder.rb +94 -0
- data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +529 -0
- data/lib/asciidoctor-pdf/prawn_ext/formatted_text/formatter.rb +29 -0
- data/lib/asciidoctor-pdf/prawn_ext/formatted_text/parser.rb +1012 -0
- data/lib/asciidoctor-pdf/prawn_ext/formatted_text/parser.treetop +115 -0
- data/lib/asciidoctor-pdf/prawn_ext/formatted_text/transform.rb +178 -0
- data/lib/asciidoctor-pdf/roman_numeral.rb +107 -0
- data/lib/asciidoctor-pdf/theme_loader.rb +103 -0
- data/lib/asciidoctor-pdf/version.rb +5 -0
- metadata +248 -0
@@ -0,0 +1,115 @@
|
|
1
|
+
module Asciidoctor
|
2
|
+
module Prawn
|
3
|
+
grammar FormattedText
|
4
|
+
rule text
|
5
|
+
complex
|
6
|
+
end
|
7
|
+
|
8
|
+
rule complex
|
9
|
+
(cdata / element / entity)* {
|
10
|
+
def content
|
11
|
+
elements.map {|e| e.content }
|
12
|
+
end
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
rule element
|
17
|
+
# strict tag matching (costs a minor toll)
|
18
|
+
# empty_element / start_tag complex end_tag &{|seq| seq[0].name == seq[2].name } {
|
19
|
+
|
20
|
+
empty_element / start_tag complex end_tag {
|
21
|
+
# NOTE content only applies to non-empty element
|
22
|
+
def content
|
23
|
+
{ type: :element, name: (tag_element = elements[0]).name.to_sym, attributes: tag_element.attributes, pcdata: elements[1].content }
|
24
|
+
end
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
rule empty_element
|
29
|
+
'<br' (spaces? '/')? '>' {
|
30
|
+
def content
|
31
|
+
{ type: :element, name: :br, attributes: {} }
|
32
|
+
end
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
rule start_tag
|
37
|
+
'<' tag_name attributes '>' {
|
38
|
+
def name
|
39
|
+
elements[1].text_value
|
40
|
+
end
|
41
|
+
|
42
|
+
def attributes
|
43
|
+
elements[2].content
|
44
|
+
end
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
rule tag_name
|
49
|
+
# QUESTION faster to do regex?
|
50
|
+
'a' / 'b' / 'code' / 'color' / 'del' / 'em' / 'font' / 'i' / 'link' / 'span' / 'strikethrough' / 'strong' / 'sub' / 'sup' / 'u'
|
51
|
+
end
|
52
|
+
|
53
|
+
rule attributes
|
54
|
+
attribute* {
|
55
|
+
def content
|
56
|
+
attrs = {}
|
57
|
+
elements.each {|e|
|
58
|
+
attr_name, attr_val = e.content
|
59
|
+
attrs[attr_name.to_sym] = attr_val
|
60
|
+
}
|
61
|
+
attrs
|
62
|
+
end
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
rule attribute
|
67
|
+
spaces [a-z_]+ '=' '"' [^"]* '"' {
|
68
|
+
def content
|
69
|
+
[elements[1].text_value, elements[4].text_value]
|
70
|
+
end
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
rule end_tag
|
75
|
+
'</' tag_name '>' {
|
76
|
+
def name
|
77
|
+
elements[1].text_value
|
78
|
+
end
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
rule cdata
|
83
|
+
[^<&]+ {
|
84
|
+
def content
|
85
|
+
{ type: :text, value: text_value }
|
86
|
+
end
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
rule entity
|
91
|
+
'&' ('#' entity_number / entity_name) ';' {
|
92
|
+
def content
|
93
|
+
if (entity_value = elements[1]).terminal?
|
94
|
+
{ type: :entity, name: entity_value.text_value.to_sym }
|
95
|
+
else
|
96
|
+
{ type: :entity, number: entity_value.elements[1].text_value.to_i }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
rule entity_number
|
103
|
+
[0-9] 2..4
|
104
|
+
end
|
105
|
+
|
106
|
+
rule entity_name
|
107
|
+
'amp' / 'apos' / 'gt' / 'lt' / 'quot'
|
108
|
+
end
|
109
|
+
|
110
|
+
rule spaces
|
111
|
+
' '+
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
module Asciidoctor
|
2
|
+
module Prawn
|
3
|
+
class FormattedTextTransform
|
4
|
+
def initialize(options = {})
|
5
|
+
@merge_adjacent_text_nodes = options.fetch(:merge_adjacent_text_nodes, false)
|
6
|
+
if (theme = options[:theme])
|
7
|
+
@link_font_color = theme.link_font_color
|
8
|
+
@monospaced_font_color = theme.literal_font_color
|
9
|
+
@monospaced_font_family = theme.literal_font_family
|
10
|
+
@monospaced_font_size = theme.literal_font_size
|
11
|
+
#@monospaced_letter_spacing = theme.literal_letter_spacing
|
12
|
+
else
|
13
|
+
@link_font_color = '0000FF'
|
14
|
+
@monospaced_font_color = nil
|
15
|
+
@monospaced_font_family = 'Courier'
|
16
|
+
@monospaced_font_size = 0.9
|
17
|
+
#@monospaced_letter_spacing = -0.1
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# FIXME might want to pass styles downwards rather than decorating on way up
|
22
|
+
def apply(parsed)
|
23
|
+
fragments = []
|
24
|
+
previous_fragment_is_text = false
|
25
|
+
# NOTE using inject is slower than a manual loop
|
26
|
+
parsed.each {|node|
|
27
|
+
case (node_type = node[:type])
|
28
|
+
when :element
|
29
|
+
if (tag_name = node[:name]) == :br
|
30
|
+
if @merge_adjacent_text_nodes && previous_fragment_is_text
|
31
|
+
fragments << { text: %(#{fragments.pop[:text]}\n) }
|
32
|
+
else
|
33
|
+
fragments << { text: "\n" }
|
34
|
+
end
|
35
|
+
previous_fragment_is_text = true
|
36
|
+
else
|
37
|
+
pcdata = node[:pcdata] || []
|
38
|
+
attributes = node[:attributes]
|
39
|
+
fragments << apply(pcdata).map {|fragment|
|
40
|
+
# decorate child fragments with styles from this element
|
41
|
+
build_fragment(fragment, tag_name, attributes)
|
42
|
+
} unless pcdata.size == 0
|
43
|
+
previous_fragment_is_text = false
|
44
|
+
end
|
45
|
+
when :text, :entity
|
46
|
+
node_text = if node_type == :text
|
47
|
+
node[:value]
|
48
|
+
elsif node_type == :entity
|
49
|
+
if (entity_name = node[:name])
|
50
|
+
case entity_name
|
51
|
+
when :lt
|
52
|
+
'<'
|
53
|
+
when :gt
|
54
|
+
'>'
|
55
|
+
when :amp
|
56
|
+
'&'
|
57
|
+
when :quot
|
58
|
+
'"'
|
59
|
+
when :apos
|
60
|
+
'\''
|
61
|
+
end
|
62
|
+
else
|
63
|
+
[node[:number]].pack('U*')
|
64
|
+
# afm fonts do not include a thin space glyph
|
65
|
+
# set fallback_fonts to allow glyph to be resolved
|
66
|
+
#if (node_number = node[:number]) == 8201
|
67
|
+
# ' '
|
68
|
+
#else
|
69
|
+
# [node_number].pack('U*')
|
70
|
+
#end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
if @merge_adjacent_text_nodes && previous_fragment_is_text
|
74
|
+
fragments << { text: %(#{fragments.pop[:text]}#{node_text}) }
|
75
|
+
else
|
76
|
+
fragments << { text: node_text }
|
77
|
+
end
|
78
|
+
previous_fragment_is_text = true
|
79
|
+
end
|
80
|
+
}
|
81
|
+
fragments.flatten
|
82
|
+
end
|
83
|
+
|
84
|
+
def build_fragment(fragment, tag_name = nil, attrs = {})
|
85
|
+
#return { text: fragment } if tag_name.nil?
|
86
|
+
styles = (fragment[:styles] ||= ::Set.new)
|
87
|
+
case tag_name
|
88
|
+
when :b, :strong
|
89
|
+
styles << :bold
|
90
|
+
when :i, :em
|
91
|
+
styles << :italic
|
92
|
+
when :code
|
93
|
+
fragment[:font] ||= @monospaced_font_family
|
94
|
+
if @monospaced_font_size
|
95
|
+
fragment[:size] ||= @monospaced_font_size
|
96
|
+
end
|
97
|
+
#if @monospaced_letter_spacing
|
98
|
+
# fragment[:character_spacing] ||= @monospaced_letter_spacing
|
99
|
+
#end
|
100
|
+
if @monospaced_font_color
|
101
|
+
fragment[:color] ||= @monospaced_font_color
|
102
|
+
end
|
103
|
+
when :color
|
104
|
+
if !fragment[:color]
|
105
|
+
if (rgb = attrs[:rgb])
|
106
|
+
if rgb[0] == '#'
|
107
|
+
rgb = rgb[1..-1]
|
108
|
+
end
|
109
|
+
fragment[:color] = rgb
|
110
|
+
elsif (r = attrs[:r]) && (g = attrs[:g]) && (b = attrs[:b])
|
111
|
+
fragment[:color] = [r, g, b].map {|e| '%02x' % e.to_i }.join
|
112
|
+
elsif (c = attrs[:c]) && (m = attrs[:m]) && (y = attrs[:y]) && (k = attrs[:k])
|
113
|
+
fragment[:color] = [c.to_i, m.to_i, y.to_i, k.to_i]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
when :font
|
117
|
+
if !fragment[:font] && (value = attrs[:name])
|
118
|
+
fragment[:font] = value
|
119
|
+
end
|
120
|
+
if !fragment[:size] && (value = attrs[:size])
|
121
|
+
fragment[:size] = value.to_f
|
122
|
+
end
|
123
|
+
#if !fragment[:character_spacing] && (value = attrs[:character_spacing])
|
124
|
+
# fragment[:character_spacing] = value.to_f
|
125
|
+
#end
|
126
|
+
when :a, :link
|
127
|
+
if !fragment[:anchor] && (value = attrs[:anchor])
|
128
|
+
fragment[:anchor] = value
|
129
|
+
end
|
130
|
+
if !fragment[:link] && (value = attrs[:href])
|
131
|
+
fragment[:link] = value
|
132
|
+
end
|
133
|
+
if !fragment[:local] && (value = attrs[:local])
|
134
|
+
fragment[:local] = value
|
135
|
+
end
|
136
|
+
fragment[:color] ||= @link_font_color
|
137
|
+
when :sub
|
138
|
+
styles << :subscript
|
139
|
+
when :sup
|
140
|
+
styles << :superscript
|
141
|
+
when :u
|
142
|
+
styles << :underline
|
143
|
+
when :del, :strikethrough
|
144
|
+
styles << :strikethrough
|
145
|
+
when :span
|
146
|
+
# span logic with normal style parsing
|
147
|
+
if (inline_styles = attrs[:style])
|
148
|
+
inline_styles.rstrip.chomp(';').split(';').each do |style|
|
149
|
+
pname, pvalue = style.split(':', 2)
|
150
|
+
case pname
|
151
|
+
when 'color'
|
152
|
+
fragment[:color] = pvalue.tr(' #', '') unless fragment[:color]
|
153
|
+
when 'font-weight'
|
154
|
+
if pvalue.lstrip == 'bold'
|
155
|
+
styles << :bold
|
156
|
+
end
|
157
|
+
when 'font-style'
|
158
|
+
if pvalue.lstrip == 'italic'
|
159
|
+
styles << :italic
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# quicker span logic that only honors font color
|
166
|
+
#if !fragment[:color] && (value = attrs[:style]) && value.start_with?('color:')
|
167
|
+
# if value.include?(';')
|
168
|
+
# value = value.split(';').first
|
169
|
+
# end
|
170
|
+
# fragment[:color] = value[6..-1].tr(' #', '')
|
171
|
+
#end
|
172
|
+
end
|
173
|
+
#fragment.delete(:styles) if styles.empty?
|
174
|
+
fragment
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
########################################################################
|
2
|
+
#
|
3
|
+
# This file was copied from roman-numerals and modified for use with
|
4
|
+
# Asciidoctor PDF.
|
5
|
+
#
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
7
|
+
# a copy of this software and associated documentation files (the
|
8
|
+
# "Software"), to deal in the Software without restriction, including
|
9
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
10
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
11
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
12
|
+
# the following conditions:
|
13
|
+
#
|
14
|
+
# The above copyright notice and this permission notice shall be
|
15
|
+
# included in all copies or substantial portions of the Software.
|
16
|
+
#
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
19
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
20
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
21
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
22
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
23
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
24
|
+
#
|
25
|
+
# Copyright (c) 2011 Andrew Vos
|
26
|
+
# Copyright (c) 2014 OpenDevice, Inc.
|
27
|
+
#
|
28
|
+
########################################################################
|
29
|
+
|
30
|
+
module Asciidoctor
|
31
|
+
module Pdf
|
32
|
+
class RomanNumeral
|
33
|
+
BaseDigits = {
|
34
|
+
1 => 'I',
|
35
|
+
4 => 'IV',
|
36
|
+
5 => 'V',
|
37
|
+
9 => 'IX',
|
38
|
+
10 => 'X',
|
39
|
+
40 => 'XL',
|
40
|
+
50 => 'L',
|
41
|
+
90 => 'XC',
|
42
|
+
100 => 'C',
|
43
|
+
400 => 'CD',
|
44
|
+
500 => 'D',
|
45
|
+
900 => 'CM',
|
46
|
+
1000 => 'M'
|
47
|
+
}
|
48
|
+
|
49
|
+
def initialize initial_value, letter_case = nil
|
50
|
+
initial_value ||= 1
|
51
|
+
if initial_value.is_a? ::Integer
|
52
|
+
@integer_value = initial_value
|
53
|
+
else
|
54
|
+
@integer_value = RomanNumeral.roman_to_int initial_value
|
55
|
+
letter_case = :lower if letter_case.nil? && initial_value.upcase != initial_value
|
56
|
+
end
|
57
|
+
@letter_case = letter_case.nil? ? :upper : letter_case
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_s
|
61
|
+
to_r
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_r
|
65
|
+
roman = RomanNumeral.int_to_roman @integer_value
|
66
|
+
@letter_case == :lower ? roman.downcase : roman
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_i
|
70
|
+
@integer_value
|
71
|
+
end
|
72
|
+
|
73
|
+
def next
|
74
|
+
RomanNumeral.new @integer_value + 1, @letter_case
|
75
|
+
end
|
76
|
+
|
77
|
+
def next!
|
78
|
+
@integer_value += 1
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.int_to_roman value
|
83
|
+
result = ''
|
84
|
+
BaseDigits.keys.reverse.each do |ival|
|
85
|
+
while value >= ival
|
86
|
+
value -= ival
|
87
|
+
result += BaseDigits[ival]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
result
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.roman_to_int value
|
94
|
+
value = value.upcase
|
95
|
+
result = 0
|
96
|
+
BaseDigits.values.reverse.each do |rval|
|
97
|
+
while value.start_with? rval
|
98
|
+
offset = rval.length
|
99
|
+
value = value[offset..offset]
|
100
|
+
result += BaseDigits.key rval
|
101
|
+
end
|
102
|
+
end
|
103
|
+
result
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'ostruct'
|
3
|
+
require_relative 'core_ext/ostruct'
|
4
|
+
|
5
|
+
module Asciidoctor
|
6
|
+
module Pdf
|
7
|
+
class ThemeLoader
|
8
|
+
DataDir = ::File.expand_path ::File.join(::File.dirname(__FILE__), '..', '..', 'data')
|
9
|
+
ThemesDir = ::File.join DataDir, 'themes'
|
10
|
+
FontsDir = ::File.join DataDir, 'fonts'
|
11
|
+
|
12
|
+
def self.resolve_theme_file theme_name = nil, theme_path = nil
|
13
|
+
theme_name ||= 'default'
|
14
|
+
# if .yml extension is given, assume it's a full file name
|
15
|
+
if theme_name.end_with? '.yml'
|
16
|
+
# FIXME restrict to jail!
|
17
|
+
theme_path ? (::File.join theme_path, theme_name) : theme_name
|
18
|
+
else
|
19
|
+
# QUESTION should we append '-theme.yml' or just '.yml'?
|
20
|
+
::File.expand_path %(#{theme_name}-theme.yml), (theme_path || ThemesDir)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.load_theme theme_name = nil, theme_path = nil
|
25
|
+
load_file (resolve_theme_file theme_name, theme_path)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.load_file filename
|
29
|
+
theme_hash = YAML.load_file filename
|
30
|
+
self.new.load theme_hash
|
31
|
+
end
|
32
|
+
|
33
|
+
def load hash
|
34
|
+
hash.inject(OpenStruct.new) do |s, (k, v)|
|
35
|
+
if v.kind_of? Hash
|
36
|
+
v.each do |k2, v2|
|
37
|
+
s[%(#{k}_#{k2})] = (k2.end_with? '_color') ? evaluate(v2, s).to_s : evaluate(v2, s)
|
38
|
+
end
|
39
|
+
else
|
40
|
+
s[k] = (k.end_with? '_color') ? evaluate(v, s).to_s : evaluate(v, s)
|
41
|
+
end
|
42
|
+
s
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def evaluate expr, vars
|
49
|
+
case expr
|
50
|
+
when String
|
51
|
+
evaluate_math(expand_vars(expr, vars))
|
52
|
+
when Array
|
53
|
+
expr.map {|e| evaluate(e, vars) }
|
54
|
+
else
|
55
|
+
expr
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def expand_vars expr, vars
|
60
|
+
if expr.include? '$'
|
61
|
+
if (expr.start_with? '$') && (expr.match /^\$([a-z0-9_]+)$/)
|
62
|
+
vars[$1]
|
63
|
+
else
|
64
|
+
expr.gsub(/\$([a-z0-9_]+)/) { vars[$1] }
|
65
|
+
end
|
66
|
+
else
|
67
|
+
expr
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def evaluate_math expr
|
72
|
+
return expr unless expr.kind_of? String
|
73
|
+
original = expr
|
74
|
+
while true
|
75
|
+
result = expr.gsub(/(-?\d+(?:\.\d+)?) *([*\/]) *(-?\d+(?:\.\d+)?)/) { $1.to_f.send($2.to_sym, $3.to_f) }
|
76
|
+
unchanged = (result == expr)
|
77
|
+
expr = result
|
78
|
+
break if unchanged
|
79
|
+
end
|
80
|
+
while true
|
81
|
+
result = expr.gsub(/(-?\d+(?:\.\d+)?) *([+\-]) *(-?\d+(?:\.\d+)?)/) { $1.to_f.send($2.to_sym, $3.to_f) }
|
82
|
+
unchanged = (result == expr)
|
83
|
+
expr = result
|
84
|
+
break if unchanged
|
85
|
+
end
|
86
|
+
if (expr.end_with? ')') && (expr.match /^(round|floor|ceil)\(/)
|
87
|
+
op = $1
|
88
|
+
offset = op.length + 1
|
89
|
+
expr = expr[offset...-1].to_f.send(op.to_sym)
|
90
|
+
end
|
91
|
+
if original == expr
|
92
|
+
expr
|
93
|
+
else
|
94
|
+
if ((int_val = expr.to_i) == (float_val = expr.to_f))
|
95
|
+
int_val
|
96
|
+
else
|
97
|
+
float_val
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|