squib 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +162 -133
  3. data/Gemfile +4 -4
  4. data/README.md +630 -550
  5. data/RELEASE TODO.md +18 -18
  6. data/Rakefile +99 -99
  7. data/lib/squib.rb +32 -32
  8. data/lib/squib/api/background.rb +20 -19
  9. data/lib/squib/api/data.rb +100 -99
  10. data/lib/squib/api/image.rb +90 -76
  11. data/lib/squib/api/save.rb +149 -103
  12. data/lib/squib/api/settings.rb +35 -37
  13. data/lib/squib/api/shapes.rb +230 -228
  14. data/lib/squib/api/text.rb +65 -66
  15. data/lib/squib/api/text_embed.rb +96 -66
  16. data/lib/squib/args/arg_loader.rb +138 -0
  17. data/lib/squib/args/box.rb +55 -0
  18. data/lib/squib/args/card_range.rb +32 -0
  19. data/lib/squib/args/color_validator.rb +12 -0
  20. data/lib/squib/args/coords.rb +33 -0
  21. data/lib/squib/args/dir_validator.rb +16 -0
  22. data/lib/squib/args/draw.rb +92 -0
  23. data/lib/squib/args/embed_adjust.rb +25 -0
  24. data/lib/squib/args/embed_key.rb +17 -0
  25. data/lib/squib/args/hand_special.rb +37 -0
  26. data/lib/squib/args/input_file.rb +37 -0
  27. data/lib/squib/args/paint.rb +44 -0
  28. data/lib/squib/args/paragraph.rb +115 -0
  29. data/lib/squib/args/save_batch.rb +60 -0
  30. data/lib/squib/args/scale_box.rb +53 -0
  31. data/lib/squib/args/sheet.rb +72 -0
  32. data/lib/squib/args/showcase_special.rb +38 -0
  33. data/lib/squib/args/svg_special.rb +37 -0
  34. data/lib/squib/args/transform.rb +25 -0
  35. data/lib/squib/args/typographer.rb +117 -117
  36. data/lib/squib/card.rb +67 -67
  37. data/lib/squib/conf.rb +117 -111
  38. data/lib/squib/constants.rb +178 -178
  39. data/lib/squib/deck.rb +113 -111
  40. data/lib/squib/graphics/cairo_context_wrapper.rb +99 -53
  41. data/lib/squib/graphics/gradient_regex.rb +46 -46
  42. data/lib/squib/graphics/hand.rb +42 -43
  43. data/lib/squib/graphics/image.rb +76 -73
  44. data/lib/squib/graphics/save_doc.rb +103 -137
  45. data/lib/squib/graphics/save_images.rb +33 -33
  46. data/lib/squib/graphics/shapes.rb +119 -152
  47. data/lib/squib/graphics/showcase.rb +85 -88
  48. data/lib/squib/graphics/text.rb +176 -216
  49. data/lib/squib/layout_parser.rb +91 -89
  50. data/lib/squib/layouts/economy.yml +85 -0
  51. data/lib/squib/layouts/fantasy.yml +101 -0
  52. data/lib/squib/layouts/hand.yml +62 -46
  53. data/lib/squib/layouts/playing-card.yml +35 -18
  54. data/lib/squib/project_template/config.yml +45 -40
  55. data/lib/squib/version.rb +10 -10
  56. data/samples/color_shortcuts.rb +6 -0
  57. data/samples/csv_import.rb +18 -18
  58. data/samples/custom-config.yml +5 -5
  59. data/samples/custom_config.rb +18 -18
  60. data/samples/draw_shapes.rb +45 -35
  61. data/samples/embed_text.rb +88 -90
  62. data/samples/hand.rb +24 -24
  63. data/samples/layouts.rb +62 -61
  64. data/samples/layouts_builtin.rb +51 -0
  65. data/samples/load_images.rb +78 -64
  66. data/samples/ranges.rb +64 -53
  67. data/samples/sample.csv +2 -2
  68. data/samples/text_options.rb +102 -94
  69. data/spec/api/api_data_spec.rb +57 -50
  70. data/spec/api/api_settings_spec.rb +37 -17
  71. data/spec/args/box_spec.rb +127 -0
  72. data/spec/args/draw_spec.rb +95 -0
  73. data/spec/args/embed_key_spec.rb +13 -0
  74. data/spec/args/input_file_spec.rb +21 -0
  75. data/spec/args/paint_spec.rb +22 -0
  76. data/spec/args/paragraph_spec.rb +153 -0
  77. data/spec/args/range_spec.rb +36 -0
  78. data/spec/args/save_batch_spec.rb +51 -0
  79. data/spec/args/scale_box_spec.rb +71 -0
  80. data/spec/args/sheet_spec.rb +58 -0
  81. data/spec/args/showcase_special_spec.rb +15 -0
  82. data/spec/data/samples/autoscale_font.rb.txt +84 -87
  83. data/spec/data/samples/basic.rb.txt +209 -203
  84. data/spec/data/samples/cairo_access.rb.txt +2 -2
  85. data/spec/data/samples/config_text_markup.rb.txt +72 -75
  86. data/spec/data/samples/csv_import.rb.txt +76 -80
  87. data/spec/data/samples/custom_config.rb.txt +48 -49
  88. data/spec/data/samples/draw_shapes.rb.txt +100 -42
  89. data/spec/data/samples/embed_text.rb.txt +283 -295
  90. data/spec/data/samples/excel.rb.txt +162 -171
  91. data/spec/data/samples/gradients.rb.txt +79 -67
  92. data/spec/data/samples/hand.rb.txt +538 -514
  93. data/spec/data/samples/hello_world.rb.txt +36 -38
  94. data/spec/data/samples/load_images.rb.txt +41 -5
  95. data/spec/data/samples/portrait-landscape.rb.txt +49 -51
  96. data/spec/data/samples/ranges.rb.txt +460 -429
  97. data/spec/data/samples/saves.rb.txt +801 -785
  98. data/spec/data/samples/showcase.rb.txt +5910 -5906
  99. data/spec/data/samples/text_options.rb.txt +1125 -981
  100. data/spec/data/samples/tgc_proofs.rb.txt +81 -79
  101. data/spec/data/samples/units.rb.txt +18 -12
  102. data/spec/data/xlsx/with_macros.xlsm +0 -0
  103. data/spec/graphics/cairo_context_wrapper_spec.rb +84 -75
  104. data/spec/graphics/graphics_images_spec.rb +94 -85
  105. data/spec/graphics/graphics_save_doc_spec.rb +67 -65
  106. data/spec/samples/expected/hand.png +0 -0
  107. data/spec/samples/expected/hand_pretty.png +0 -0
  108. data/spec/samples/expected/layout_00.png +0 -0
  109. data/spec/samples/expected/load_images_00.png +0 -0
  110. data/spec/samples/expected/ranges_00.png +0 -0
  111. data/spec/samples/expected/shape_00.png +0 -0
  112. data/spec/samples/expected/showcase.png +0 -0
  113. data/spec/samples/expected/showcase2.png +0 -0
  114. data/spec/samples/expected/showcase_individual_00.png +0 -0
  115. data/spec/samples/expected/showcase_individual_01.png +0 -0
  116. data/spec/samples/expected/showcase_individual_02.png +0 -0
  117. data/spec/samples/expected/showcase_individual_03.png +0 -0
  118. data/spec/samples/expected/text_00.png +0 -0
  119. data/spec/samples/expected/text_01.png +0 -0
  120. data/spec/samples/expected/text_02.png +0 -0
  121. data/spec/samples/samples_regression_spec.rb +82 -82
  122. data/spec/spec_helper.rb +3 -2
  123. data/squib.gemspec +48 -48
  124. data/squib.sublime-project +42 -36
  125. metadata +61 -33
  126. data/lib/squib/input_helpers.rb +0 -238
  127. data/spec/api/api_image_spec.rb +0 -38
  128. data/spec/api/api_text_spec.rb +0 -37
  129. data/spec/graphics/graphics_shapes_spec.rb +0 -85
  130. data/spec/graphics/graphics_text_spec.rb +0 -164
  131. data/spec/input_helpers_spec.rb +0 -238
  132. data/spec/samples/expected/embed_multi_00.png +0 -0
  133. data/spec/samples/expected/embed_multi_01.png +0 -0
  134. data/spec/samples/expected/embed_multi_02.png +0 -0
  135. data/spec/samples/expected/ranges_01.png +0 -0
  136. data/spec/samples/expected/ranges_02.png +0 -0
@@ -1,216 +1,176 @@
1
- require 'pango'
2
- require 'squib/args/typographer'
3
-
4
- module Squib
5
- class Card
6
-
7
- # :nodoc:
8
- # @api private
9
- def draw_text_hint(cc, x, y, layout, color)
10
- color = @deck.text_hint if color.to_s.eql? 'off' and not @deck.text_hint.to_s.eql? 'off'
11
- return if color.to_s.eql? 'off' or color.nil?
12
- # when w,h < 0, it was never set. extents[1] are ink extents
13
- w = layout.width / Pango::SCALE
14
- w = layout.extents[1].width / Pango::SCALE if w < 0
15
- h = layout.height / Pango::SCALE
16
- h = layout.extents[1].height / Pango::SCALE if h < 0
17
- cc.rounded_rectangle(0, 0, w, h, 0, 0)
18
- cc.set_source_color(color)
19
- cc.set_line_width(2.0)
20
- cc.stroke
21
- end
22
-
23
- # :nodoc:
24
- # @api private
25
- def set_ellipsize!(layout, ellipsize)
26
- case ellipsize.to_s.downcase
27
- when 'none', 'false'
28
- layout.ellipsize = Pango::Layout::ELLIPSIZE_NONE
29
- when 'start'
30
- layout.ellipsize = Pango::Layout::ELLIPSIZE_START
31
- when 'middle'
32
- layout.ellipsize = Pango::Layout::ELLIPSIZE_MIDDLE
33
- when 'end', 'true'
34
- layout.ellipsize = Pango::Layout::ELLIPSIZE_END
35
- end
36
- end
37
-
38
- # :nodoc:
39
- # @api private
40
- def set_wrap!(layout, wrap)
41
- case wrap.to_s.downcase
42
- when 'word'
43
- layout.wrap = Pango::Layout::WRAP_WORD
44
- when 'char'
45
- layout.wrap = Pango::Layout::WRAP_CHAR
46
- when 'word_char', 'true'
47
- layout.wrap = Pango::Layout::WRAP_WORD_CHAR
48
- end
49
- end
50
-
51
- # :nodoc:
52
- # @api private
53
- def set_align!(layout, align)
54
- case align.to_s.downcase
55
- when 'left'
56
- layout.alignment = Pango::ALIGN_LEFT
57
- when 'right'
58
- layout.alignment = Pango::ALIGN_RIGHT
59
- when 'center'
60
- layout.alignment = Pango::ALIGN_CENTER
61
- end
62
- end
63
-
64
- # :nodoc:
65
- # @api private
66
- def compute_valign(layout, valign)
67
- return 0 unless layout.height > 0
68
- ink_extents = layout.extents[1]
69
- case valign.to_s.downcase
70
- when 'middle'
71
- Pango.pixels( (layout.height - ink_extents.height) / 2)
72
- when 'bottom'
73
- Pango.pixels(layout.height - ink_extents.height)
74
- else
75
- 0
76
- end
77
- end
78
-
79
- def set_font_rendering_opts!(layout)
80
- font_options = Cairo::FontOptions.new
81
- font_options.antialias = Conf::ANTIALIAS_OPTS[(@deck.antialias || 'gray').downcase]
82
- font_options.hint_metrics = 'on' # TODO make this configurable
83
- font_options.hint_style = 'full' # TODO make this configurable
84
- layout.context.font_options = font_options
85
- end
86
-
87
- # :nodoc:
88
- # @api private
89
- def set_wh!(layout, width, height)
90
- layout.width = width * Pango::SCALE unless width.nil? || width == :native
91
- layout.height = height * Pango::SCALE unless height.nil? || height == :native
92
- end
93
-
94
- # :nodoc:
95
- # @api private
96
- def next_embed(keys, str)
97
- ret = nil
98
- ret_key = nil
99
- keys.each do |key|
100
- i = str.index(key)
101
- ret ||= i
102
- unless i.nil? || i > ret
103
- ret = i
104
- ret_key = key
105
- end
106
- end
107
- ret_key
108
- end
109
-
110
- # When we do embedded icons, we create a 3-character string that has a font
111
- # size of zero and a letter-spacing that fits the icon we need. That gives
112
- # us the wrapping behavior we need but no clutter beneath. On most
113
- # platforms, this works fine. On Linux, this creates
114
- # a Cairo transformation matrix that
115
- ZERO_WIDTH_CHAR_SIZE = 0 # this works on most platforms
116
- # some platforms make this Pango pixels (1/1024), others a 1 pt font
117
- ZERO_WIDTH_CHAR_SIZE = 1 if RbConfig::CONFIG['host_os'] === 'linux-gnu'
118
-
119
- # :nodoc:
120
- # @api private
121
- def process_embeds(embed, str, layout)
122
- return [] unless embed.rules.any?
123
- layout.markup = str
124
- clean_str = layout.text
125
- draw_calls = []
126
- searches = []
127
- while (key = next_embed(embed.rules.keys, clean_str)) != nil
128
- rule = embed.rules[key]
129
- spacing = rule[:width] * Pango::SCALE
130
- index = clean_str.index(key)
131
- index = clean_str[0..index].bytesize #convert to byte index (bug #57)
132
- str = str.sub(key, "<span size=\"#{ZERO_WIDTH_CHAR_SIZE}\">a<span letter_spacing=\"#{spacing.to_i}\">a</span>a</span>")
133
- layout.markup = str
134
- clean_str = layout.text
135
- searches << { index: index, rule: rule }
136
- end
137
- searches.each do |search|
138
- rect = layout.index_to_pos(search[:index])
139
- x = Pango.pixels(rect.x) + search[:rule][:dx]
140
- y = Pango.pixels(rect.y) + search[:rule][:dy]
141
- draw_calls << {x: x, y: y, draw: search[:rule][:draw]} # defer drawing until we've valigned
142
- end
143
- return draw_calls
144
- end
145
-
146
- def stroke_outline!(cc, layout, stroke_width, stroke_color)
147
- if stroke_width > 0
148
- cc.set_source_squibcolor(stroke_color)
149
- cc.set_line_width(stroke_width)
150
- cc.pango_layout_path(layout)
151
- cc.stroke
152
- end
153
- end
154
-
155
- # :nodoc:
156
- # @api private
157
- def text(embed,str, font, font_size, color,
158
- x, y, width, height,
159
- markup, justify, wrap, ellipsize,
160
- spacing, align, valign, hint, angle,
161
- stroke_color, stroke_width)
162
- Squib.logger.debug {"Placing '#{str}'' with font '#{font}' @ #{x}, #{y}, color: #{color}, angle: #{angle} etc."}
163
- extents = nil
164
- str = str.to_s
165
- use_cairo do |cc|
166
- cc.set_source_squibcolor(color)
167
- cc.translate(x,y)
168
- cc.rotate(angle)
169
- cc.move_to(0, 0)
170
-
171
- font_desc = Pango::FontDescription.new(font)
172
- font_desc.size = font_size * Pango::SCALE unless font_size.nil?
173
- layout = cc.create_pango_layout
174
- layout.font_description = font_desc
175
- layout.text = str
176
- if markup
177
- str = @deck.typographer.process(layout.text)
178
- layout.markup = str
179
- end
180
-
181
- set_font_rendering_opts!(layout)
182
- set_wh!(layout, width, height)
183
- set_wrap!(layout, wrap)
184
- set_ellipsize!(layout, ellipsize)
185
- set_align!(layout, align)
186
-
187
- layout.justify = justify unless justify.nil?
188
- layout.spacing = spacing * Pango::SCALE unless spacing.nil?
189
- cc.update_pango_layout(layout)
190
-
191
- embed_draws = process_embeds(embed, str, layout)
192
-
193
- vertical_start = compute_valign(layout, valign)
194
- cc.move_to(0, vertical_start)
195
-
196
- cc.update_pango_layout(layout)
197
- cc.show_pango_layout(layout)
198
- stroke_outline!(cc, layout, stroke_width, stroke_color)
199
- begin
200
- embed_draws.each { |ed| ed[:draw].call(self, ed[:x], ed[:y] + vertical_start) }
201
- rescue Exception => e
202
- puts "====EXCEPTION!===="
203
- puts e
204
- puts "If this was a non-invertible matrix error, this is a known issue with a potential workaround. Please report it at: https://github.com/andymeneely/squib/issues/55"
205
- puts "=================="
206
- raise e
207
- end
208
- draw_text_hint(cc, x, y, layout, hint)
209
- extents = { width: layout.extents[1].width / Pango::SCALE,
210
- height: layout.extents[1].height / Pango::SCALE }
211
- end
212
- return extents
213
- end
214
-
215
- end
216
- end
1
+ require 'pango'
2
+ require 'squib/args/typographer'
3
+
4
+ module Squib
5
+ class Card
6
+
7
+ # :nodoc:
8
+ # @api private
9
+ def draw_text_hint(cc, x, y, layout, color)
10
+ color = @deck.text_hint if color.to_s.eql? 'off' and not @deck.text_hint.to_s.eql? 'off'
11
+ return if color.to_s.eql? 'off' or color.nil?
12
+ # when w,h < 0, it was never set. extents[1] are ink extents
13
+ w = layout.width / Pango::SCALE
14
+ w = layout.extents[1].width / Pango::SCALE if w < 0
15
+ h = layout.height / Pango::SCALE
16
+ h = layout.extents[1].height / Pango::SCALE if h < 0
17
+ cc.rounded_rectangle(0, 0, w, h, 0, 0)
18
+ cc.set_source_color(color)
19
+ cc.set_line_width(2.0)
20
+ cc.stroke
21
+ end
22
+
23
+ # :nodoc:
24
+ # @api private
25
+ def compute_valign(layout, valign)
26
+ return 0 unless layout.height > 0
27
+ ink_extents = layout.extents[1]
28
+ case valign.to_s.downcase
29
+ when 'middle'
30
+ Pango.pixels( (layout.height - ink_extents.height) / 2)
31
+ when 'bottom'
32
+ Pango.pixels(layout.height - ink_extents.height)
33
+ else
34
+ 0
35
+ end
36
+ end
37
+
38
+ def set_font_rendering_opts!(layout)
39
+ font_options = Cairo::FontOptions.new
40
+ font_options.antialias = Conf::ANTIALIAS_OPTS[(@deck.antialias || 'gray').downcase]
41
+ font_options.hint_metrics = 'on' # TODO make this configurable
42
+ font_options.hint_style = 'full' # TODO make this configurable
43
+ layout.context.font_options = font_options
44
+ end
45
+
46
+ # :nodoc:
47
+ # @api private
48
+ def set_wh!(layout, width, height)
49
+ layout.width = width * Pango::SCALE unless width.nil? || width == :auto
50
+ layout.height = height * Pango::SCALE unless height.nil? || height == :auto
51
+ end
52
+
53
+ # :nodoc:
54
+ # @api private
55
+ def next_embed(keys, str)
56
+ ret = nil
57
+ ret_key = nil
58
+ keys.each do |key|
59
+ i = str.index(key)
60
+ ret ||= i
61
+ unless i.nil? || i > ret
62
+ ret = i
63
+ ret_key = key
64
+ end
65
+ end
66
+ ret_key
67
+ end
68
+
69
+ # When we do embedded icons, we create a 3-character string that has a font
70
+ # size of zero and a letter-spacing that fits the icon we need. That gives
71
+ # us the wrapping behavior we need but no clutter beneath. On most
72
+ # platforms, this works fine. On Linux, this creates
73
+ # a Cairo transformation matrix that
74
+ ZERO_WIDTH_CHAR_SIZE = 0 # this works on most platforms
75
+ # some platforms make this Pango pixels (1/1024), others a 1 pt font
76
+ ZERO_WIDTH_CHAR_SIZE = 1 if RbConfig::CONFIG['host_os'] === 'linux-gnu'
77
+
78
+ # :nodoc:
79
+ # @api private
80
+ def process_embeds(embed, str, layout)
81
+ return [] unless embed.rules.any?
82
+ layout.markup = str
83
+ clean_str = layout.text
84
+ draw_calls = []
85
+ searches = []
86
+ while (key = next_embed(embed.rules.keys, clean_str)) != nil
87
+ rule = embed.rules[key]
88
+ spacing = rule[:box].width[@index] * Pango::SCALE
89
+ kindex = clean_str.index(key)
90
+ kindex = clean_str[0..kindex].bytesize #convert to byte index (bug #57)
91
+ str = str.sub(key, "<span size=\"#{ZERO_WIDTH_CHAR_SIZE}\">a<span letter_spacing=\"#{spacing.to_i}\">a</span>a</span>")
92
+ layout.markup = str
93
+ clean_str = layout.text
94
+ searches << { index: kindex, rule: rule }
95
+ end
96
+ searches.each do |search|
97
+ rect = layout.index_to_pos(search[:index])
98
+ x = Pango.pixels(rect.x) + search[:rule][:adjust].dx[@index]
99
+ y = Pango.pixels(rect.y) + search[:rule][:adjust].dy[@index]
100
+ draw_calls << {x: x, y: y, draw: search[:rule][:draw]} # defer drawing until we've valigned
101
+ end
102
+ return draw_calls
103
+ end
104
+
105
+ def stroke_outline!(cc, layout, draw)
106
+ if draw.stroke_width > 0
107
+ cc.pango_layout_path(layout)
108
+ cc.fancy_stroke draw
109
+ cc.set_source_squibcolor(draw.color)
110
+ end
111
+ end
112
+
113
+ def warn_if_ellipsized(layout)
114
+ if @deck.conf.warn_ellipsize? && layout.ellipsized?
115
+ Squib.logger.warn { "Ellipsized (too much text). Card \##{@index}. Text: \"#{layout.text}\". \n (To disable this warning, set warn_ellipsize: false in config.yml)" }
116
+ end
117
+ end
118
+
119
+ # :nodoc:
120
+ # @api private
121
+ def text(embed, para, box, trans, draw)
122
+ Squib.logger.debug {"Rendering text with: \n#{para} \nat:\n #{box} \ndraw:\n #{draw} \ntransform: #{trans}"}
123
+ extents = nil
124
+ use_cairo do |cc|
125
+ cc.set_source_squibcolor(draw.color)
126
+ cc.translate(box.x, box.y)
127
+ cc.rotate(trans.angle)
128
+ cc.move_to(0, 0)
129
+
130
+ font_desc = Pango::FontDescription.new(para.font)
131
+ font_desc.size = para.font_size * Pango::SCALE unless para.font_size.nil?
132
+ layout = cc.create_pango_layout
133
+ layout.font_description = font_desc
134
+ layout.text = para.str
135
+ if para.markup
136
+ para.str = @deck.typographer.process(layout.text)
137
+ layout.markup = para.str
138
+ end
139
+
140
+ set_font_rendering_opts!(layout)
141
+ set_wh!(layout, box.width, box.height)
142
+ layout.wrap = para.wrap
143
+ layout.ellipsize = para.ellipsize
144
+ layout.alignment = para.align
145
+
146
+ layout.justify = para.justify unless para.justify.nil?
147
+ layout.spacing = para.spacing unless para.spacing.nil?
148
+
149
+ embed_draws = process_embeds(embed, para.str, layout)
150
+
151
+ vertical_start = compute_valign(layout, para.valign)
152
+ cc.move_to(0, vertical_start) #TODO clean this up a bit
153
+
154
+ stroke_outline!(cc, layout, draw) if draw.stroke_strategy == :stroke_first
155
+ cc.move_to(0, vertical_start)
156
+ cc.show_pango_layout(layout)
157
+ stroke_outline!(cc, layout, draw) if draw.stroke_strategy == :fill_first
158
+ begin
159
+ embed_draws.each { |ed| ed[:draw].call(self, ed[:x], ed[:y] + vertical_start) }
160
+ rescue Exception => e
161
+ puts "====EXCEPTION!===="
162
+ puts e
163
+ puts "If this was a non-invertible matrix error, this is a known issue with a potential workaround. Please report it at: https://github.com/andymeneely/squib/issues/55"
164
+ puts "=================="
165
+ raise e
166
+ end
167
+ draw_text_hint(cc, box.x, box.y, layout, para.hint)
168
+ extents = { width: layout.extents[1].width / Pango::SCALE,
169
+ height: layout.extents[1].height / Pango::SCALE }
170
+ warn_if_ellipsized layout
171
+ end
172
+ return extents
173
+ end
174
+
175
+ end
176
+ end
@@ -1,89 +1,91 @@
1
- module Squib
2
- # Internal class for handling layouts
3
- #@api private
4
- class LayoutParser
5
-
6
- # Load the layout file(s), if exists
7
- # @api private
8
- def self.load_layout(files)
9
- layout = {}
10
- Squib::logger.info { " using layout(s): #{files}" }
11
- Array(files).each do |file|
12
- thefile = file
13
- thefile = builtin(file) unless File.exists?(file)
14
- if File.exists? thefile
15
- yml = layout.merge(YAML.load_file(thefile) || {}) #load_file returns false on empty file
16
- yml.each do |key, value|
17
- layout[key] = recurse_extends(yml, key, {})
18
- end
19
- else
20
- Squib::logger.error { "Layout file not found: #{file}. Skipping..." }
21
- end
22
- end
23
- return layout
24
- end
25
-
26
- # Determine the file path of the built-in layout file
27
- def self.builtin(file)
28
- "#{File.dirname(__FILE__)}/layouts/#{file}"
29
- end
30
-
31
- # Process the extends recursively
32
- # :nodoc:
33
- # @api private
34
- def self.recurse_extends(yml, key, visited )
35
- assert_not_visited(key, visited)
36
- return yml[key] unless has_extends?(yml, key)
37
- return yml[key] unless parents_exist?(yml, key)
38
- visited[key] = key
39
- parent_keys = [yml[key]['extends']].flatten
40
- h = {}
41
- parent_keys.each do |parent_key|
42
- from_extends = yml[key].merge(recurse_extends(yml, parent_key, visited)) do |key, child_val, parent_val|
43
- if child_val.to_s.strip.start_with?('+=')
44
- parent_val + child_val.sub('+=','').strip.to_f
45
- elsif child_val.to_s.strip.start_with?('-=')
46
- parent_val - child_val.sub('-=','').strip.to_f
47
- else
48
- child_val #child overrides parent when merging, no +=
49
- end
50
- end
51
- h = h.merge(from_extends) do |key, older_sibling, younger_sibling|
52
- younger_sibling #when two siblings have the same entry, the "younger" (lower one) overrides
53
- end
54
- end
55
- return h
56
- end
57
-
58
- # Does this layout entry have an extends field?
59
- # i.e. is it a base-case or will it need recursion?
60
- # :nodoc:
61
- # @api private
62
- def self.has_extends?(yml, key)
63
- !!yml[key] && yml[key].key?('extends')
64
- end
65
-
66
- # Checks if we have any absentee parents
67
- # @api private
68
- def self.parents_exist?(yml, key)
69
- exists = true
70
- Array(yml[key]['extends']).each do |parent|
71
- unless yml.key?(parent)
72
- exists = false unless
73
- Squib.logger.error "Processing layout: '#{key}' attempts to extend a missing '#{yml[key]['extends']}'"
74
- end
75
- end
76
- return exists
77
- end
78
-
79
- # Safeguard against malformed circular extends
80
- # :nodoc:
81
- # @api private
82
- def self.assert_not_visited(key, visited)
83
- if visited.key? key
84
- raise "Invalid layout: circular extends with '#{key}'"
85
- end
86
- end
87
-
88
- end
89
- end
1
+ require 'yaml'
2
+
3
+ module Squib
4
+ # Internal class for handling layouts
5
+ #@api private
6
+ class LayoutParser
7
+
8
+ # Load the layout file(s), if exists
9
+ # @api private
10
+ def self.load_layout(files)
11
+ layout = {}
12
+ Squib::logger.info { " using layout(s): #{files}" }
13
+ Array(files).each do |file|
14
+ thefile = file
15
+ thefile = builtin(file) unless File.exists?(file)
16
+ if File.exists? thefile
17
+ yml = layout.merge(YAML.load_file(thefile) || {}) #load_file returns false on empty file
18
+ yml.each do |key, value|
19
+ layout[key] = recurse_extends(yml, key, {})
20
+ end
21
+ else
22
+ Squib::logger.error { "Layout file not found: #{file}. Skipping..." }
23
+ end
24
+ end
25
+ return layout
26
+ end
27
+
28
+ # Determine the file path of the built-in layout file
29
+ def self.builtin(file)
30
+ "#{File.dirname(__FILE__)}/layouts/#{file}"
31
+ end
32
+
33
+ # Process the extends recursively
34
+ # :nodoc:
35
+ # @api private
36
+ def self.recurse_extends(yml, key, visited )
37
+ assert_not_visited(key, visited)
38
+ return yml[key] unless has_extends?(yml, key)
39
+ return yml[key] unless parents_exist?(yml, key)
40
+ visited[key] = key
41
+ parent_keys = [yml[key]['extends']].flatten
42
+ h = {}
43
+ parent_keys.each do |parent_key|
44
+ from_extends = yml[key].merge(recurse_extends(yml, parent_key, visited)) do |key, child_val, parent_val|
45
+ if child_val.to_s.strip.start_with?('+=')
46
+ parent_val + child_val.sub('+=','').strip.to_f
47
+ elsif child_val.to_s.strip.start_with?('-=')
48
+ parent_val - child_val.sub('-=','').strip.to_f
49
+ else
50
+ child_val #child overrides parent when merging, no +=
51
+ end
52
+ end
53
+ h = h.merge(from_extends) do |key, older_sibling, younger_sibling|
54
+ younger_sibling #when two siblings have the same entry, the "younger" (lower one) overrides
55
+ end
56
+ end
57
+ return h
58
+ end
59
+
60
+ # Does this layout entry have an extends field?
61
+ # i.e. is it a base-case or will it need recursion?
62
+ # :nodoc:
63
+ # @api private
64
+ def self.has_extends?(yml, key)
65
+ !!yml[key] && yml[key].key?('extends')
66
+ end
67
+
68
+ # Checks if we have any absentee parents
69
+ # @api private
70
+ def self.parents_exist?(yml, key)
71
+ exists = true
72
+ Array(yml[key]['extends']).each do |parent|
73
+ unless yml.key?(parent)
74
+ exists = false unless
75
+ Squib.logger.error "Processing layout: '#{key}' attempts to extend a missing '#{yml[key]['extends']}'"
76
+ end
77
+ end
78
+ return exists
79
+ end
80
+
81
+ # Safeguard against malformed circular extends
82
+ # :nodoc:
83
+ # @api private
84
+ def self.assert_not_visited(key, visited)
85
+ if visited.key? key
86
+ raise "Invalid layout: circular extends with '#{key}'"
87
+ end
88
+ end
89
+
90
+ end
91
+ end