slideck 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,345 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slideck
4
+ # Responsible for accessing margin configuration
5
+ #
6
+ # @api private
7
+ class Margin
8
+ # The pattern to validate input contains only integers
9
+ #
10
+ # @return [Regexp]
11
+ #
12
+ # @api private
13
+ INTEGERS_ONLY_PATTERN = /^[\d, ]+$/.freeze
14
+ private_constant :INTEGERS_ONLY_PATTERN
15
+
16
+ # The pattern to detect integers separator
17
+ #
18
+ # @return [Regexp]
19
+ #
20
+ # @api private
21
+ INTEGERS_SEPARATOR = /[ ,]+/.freeze
22
+ private_constant :INTEGERS_SEPARATOR
23
+
24
+ # The allowed margin side names
25
+ #
26
+ # @return [Array<Symbol>]
27
+ #
28
+ # @api private
29
+ SIDE_NAMES = %i[top right bottom left].freeze
30
+ private_constant :SIDE_NAMES
31
+
32
+ # Create a Margin instance from a value
33
+ #
34
+ # @example
35
+ # Slideck::Margin.from(1)
36
+ #
37
+ # @example
38
+ # Slideck::Margin.from([1, 2])
39
+ #
40
+ # @example
41
+ # Slideck::Margin.from({top: 1, left: 2})
42
+ #
43
+ # @example
44
+ # Slideck::Margin.from("1, 2")
45
+ #
46
+ # @param [Object] value
47
+ # the value to create a margin from
48
+ #
49
+ # @raise [Slideck::InvalidArgumentError]
50
+ #
51
+ # @return [Slideck::Margin]
52
+ #
53
+ # @api public
54
+ def self.from(value)
55
+ if value.is_a?(Hash)
56
+ from_hash(value)
57
+ elsif (converted = convert_to_array(value))
58
+ from_array(converted)
59
+ else
60
+ raise_invalid_margin_error(value)
61
+ end
62
+ end
63
+
64
+ # Create a Margin instance with an array-like initialiser
65
+ #
66
+ # @example
67
+ # Slideck::Margin[1, 2]
68
+ #
69
+ # @example
70
+ # Slideck::Margin[1, 2, 3, 4]
71
+ #
72
+ # @param [Array<Integer>] values
73
+ # the values to convert to a margin
74
+ #
75
+ # @return [Slideck::Margin]
76
+ #
77
+ # @api public
78
+ def self.[](*values)
79
+ from_array(values)
80
+ end
81
+
82
+ # Create a Margin instance from an array
83
+ #
84
+ # @example
85
+ # Slideck::Margin.from_array([1, 2])
86
+ #
87
+ # @param [Array] value
88
+ # the value to convert to a margin
89
+ #
90
+ # @raise [Slideck::InvalidArgumentError]
91
+ #
92
+ # @return [Slideck::Margin]
93
+ #
94
+ # @api public
95
+ def self.from_array(value)
96
+ case value.size
97
+ when 1 then new(*(value * 4))
98
+ when 2 then new(*(value * 2))
99
+ when 3 then new(*value, value[1])
100
+ when 4 then new(*value)
101
+ else raise_invalid_margin_as_array_error(value)
102
+ end
103
+ end
104
+
105
+ # Create a Margin instance from a hash
106
+ #
107
+ # @example
108
+ # Slideck::Margin.from_hash{top: 1, left: 2})
109
+ #
110
+ # @param [Hash] value
111
+ # the hash value to convert to a margin
112
+ #
113
+ # @raise [Slideck::InvalidArgumentError]
114
+ #
115
+ # @return [Slideck::Margin]
116
+ #
117
+ # @api public
118
+ def self.from_hash(value)
119
+ unless (invalid = (value.keys - SIDE_NAMES)).empty?
120
+ raise_invalid_margin_as_hash_error(invalid)
121
+ end
122
+
123
+ new(*value.values_at(*SIDE_NAMES).map { |val| val.nil? ? 0 : val })
124
+ end
125
+
126
+ # Convert a value into an array
127
+ #
128
+ # @param [Object] value
129
+ # the value to convert into an array
130
+ #
131
+ # @return [Array<Integer>, nil]
132
+ #
133
+ # @api private
134
+ def self.convert_to_array(value)
135
+ if value.is_a?(Numeric)
136
+ [value]
137
+ elsif value.is_a?(String) && value =~ INTEGERS_ONLY_PATTERN
138
+ value.split(INTEGERS_SEPARATOR).map(&:to_i)
139
+ elsif value.is_a?(Array)
140
+ value
141
+ end
142
+ end
143
+ private_class_method :convert_to_array
144
+
145
+ # Raise an error when the value is invalid
146
+ #
147
+ # @param [Object] value
148
+ # the invalid value
149
+ #
150
+ # @raise [Slideck::InvalidArgumentError]
151
+ #
152
+ # @return [void]
153
+ #
154
+ # @api private
155
+ def self.raise_invalid_margin_error(value)
156
+ raise InvalidArgumentError,
157
+ "invalid value for margin: #{value.inspect}.\n" \
158
+ "The margin needs to be an integer, a string of " \
159
+ "integers, an array of integers or " \
160
+ "a hash of side names and integer values."
161
+ end
162
+ private_class_method :raise_invalid_margin_error
163
+
164
+ # Raise an error when the array has a wrong number of values
165
+ #
166
+ # @param [Array<Integer>] values
167
+ # the margin values
168
+ #
169
+ # @raise [Slideck::InvalidArgumentError]
170
+ #
171
+ # @return [void]
172
+ #
173
+ # @api private
174
+ def self.raise_invalid_margin_as_array_error(values)
175
+ raise InvalidArgumentError,
176
+ "wrong number of integers for margin: " \
177
+ "#{values.join(", ").inspect}.\n" \
178
+ "The margin needs to be specified with one, two, three " \
179
+ "or four integers."
180
+ end
181
+ private_class_method :raise_invalid_margin_as_array_error
182
+
183
+ # Raise an error when the hash has an invalid side name
184
+ #
185
+ # @param [Array] invalid_sides
186
+ # the invalid side names
187
+ #
188
+ # @raise [Slideck::InvalidArgumentError]
189
+ #
190
+ # @return [void]
191
+ #
192
+ # @api private
193
+ def self.raise_invalid_margin_as_hash_error(invalid_sides)
194
+ raise InvalidArgumentError,
195
+ "unknown name#{"s" if invalid_sides.size > 1} for margin: " \
196
+ "#{invalid_sides.map(&:inspect).join(", ")}.\n" \
197
+ "Valid names are: top, left, right and bottom."
198
+ end
199
+ private_class_method :raise_invalid_margin_as_hash_error
200
+
201
+ # The bottom margin
202
+ #
203
+ # @example
204
+ # margin.bottom
205
+ #
206
+ # @return [Integer]
207
+ #
208
+ # @api public
209
+ attr_reader :bottom
210
+
211
+ # The left margin
212
+ #
213
+ # @example
214
+ # margin.left
215
+ #
216
+ # @return [Integer]
217
+ #
218
+ # @api public
219
+ attr_reader :left
220
+
221
+ # The right margin
222
+ #
223
+ # @example
224
+ # margin.right
225
+ #
226
+ # @return [Integer]
227
+ #
228
+ # @api public
229
+ attr_reader :right
230
+
231
+ # The top margin
232
+ #
233
+ # @example
234
+ # margin.top
235
+ #
236
+ # @return [Integer]
237
+ #
238
+ # @api public
239
+ attr_reader :top
240
+
241
+ # Create a Margin instance
242
+ #
243
+ # @param [Integer] top
244
+ # the top margin
245
+ # @param [Integer] right
246
+ # the right margin
247
+ # @param [Integer] bottom
248
+ # the bottom margin
249
+ # @param [Integer] left
250
+ # the left margin
251
+ #
252
+ # @api private
253
+ def initialize(top, right, bottom, left)
254
+ @top = validate_margin_side(:top, top)
255
+ @right = validate_margin_side(:right, right)
256
+ @bottom = validate_margin_side(:bottom, bottom)
257
+ @left = validate_margin_side(:left, left)
258
+
259
+ freeze
260
+ end
261
+ private_class_method :new
262
+
263
+ # Determine equivalence with another object
264
+ #
265
+ # @example
266
+ # margin == other
267
+ #
268
+ # @param [Object] other
269
+ # the other object to determine equivalence with
270
+ #
271
+ # @return [Boolean]
272
+ # true if this object is equivalent to the other, false otherwise
273
+ #
274
+ # @api public
275
+ def ==(other)
276
+ other.is_a?(self.class) &&
277
+ top == other.top && right == other.right &&
278
+ bottom == other.bottom && left == other.left
279
+ end
280
+
281
+ # Determine for equality with another object
282
+ #
283
+ # @example
284
+ # margin.eql?(other)
285
+ #
286
+ # @param [Object] other
287
+ # the other object to determine equality with
288
+ #
289
+ # @return [Boolean]
290
+ # true if this object is equal to the other, false otherwise
291
+ #
292
+ # @api public
293
+ def eql?(other)
294
+ instance_of?(other.class) &&
295
+ top.eql?(other.top) && right.eql?(other.right) &&
296
+ bottom.eql?(other.bottom) && left.eql?(other.left)
297
+ end
298
+
299
+ # Generate hash value of this margin
300
+ #
301
+ # @example
302
+ # margin.hash
303
+ #
304
+ # @return [Integer]
305
+ #
306
+ # @api public
307
+ def hash
308
+ [self.class, top, right, bottom, left].hash
309
+ end
310
+
311
+ # An array representation of all margin sides
312
+ #
313
+ # @example
314
+ # margin = Slideck::Margin[1, 2, 3, 4]
315
+ # margin.to_a # => [1, 2, 3, 4]
316
+ #
317
+ # @return [Array<Integer, Integer, Integer, Integer>]
318
+ #
319
+ # @api public
320
+ def to_a
321
+ [top, right, bottom, left]
322
+ end
323
+
324
+ private
325
+
326
+ # Validate margin side
327
+ #
328
+ # @param [Symbol] side
329
+ # the margin side
330
+ # @param [Object] value
331
+ # the value to validate
332
+ #
333
+ # @raise [Slideck::InvalidArgumentError]
334
+ #
335
+ # @return [Integer]
336
+ #
337
+ # @api private
338
+ def validate_margin_side(side, value)
339
+ return value if value.is_a?(Integer)
340
+
341
+ raise InvalidArgumentError,
342
+ "#{side} margin needs to be an integer, got: #{value.inspect}"
343
+ end
344
+ end # Margin
345
+ end # Slideck
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slideck
4
+ # Responsible for accessing metadata configuration
5
+ #
6
+ # @api private
7
+ class Metadata
8
+ # Create a Metadata instance from slides configuration
9
+ #
10
+ # @param [Slideck::MetadataConverter] metadata_converter
11
+ # the metadata converter
12
+ # @param [Hash{Symbol => Object}] custom_metadata
13
+ # the custom metadata
14
+ # @param [MetadataDefaults] metadata_defaults
15
+ # the metadata defaults
16
+ #
17
+ # @raise [Slideck::InvalidMetadataKeyError]
18
+ #
19
+ # @return [Slideck::Metadata]
20
+ #
21
+ # @api public
22
+ def self.from(metadata_converter, custom_metadata, metadata_defaults)
23
+ validate_keys(custom_metadata.keys)
24
+
25
+ new(metadata_defaults.merge(metadata_converter.convert(custom_metadata)))
26
+ end
27
+
28
+ # Check for unknown metadata keys
29
+ #
30
+ # @param [Array<Symbol>] custom_metadata_keys
31
+ # the custom metadata keys
32
+ #
33
+ # @raise [Slideck::InvalidMetadataKeyError]
34
+ #
35
+ # @return [nil]
36
+ #
37
+ # @api private
38
+ def self.validate_keys(custom_metadata_keys)
39
+ unknown_keys = custom_metadata_keys - @metadata_keys
40
+ return if unknown_keys.empty?
41
+
42
+ raise InvalidMetadataKeyError.new(@metadata_keys, unknown_keys)
43
+ end
44
+ private_class_method :validate_keys
45
+
46
+ # Define a method to access metadata
47
+ #
48
+ # @param [Symbol] key
49
+ # the metadata key name
50
+ #
51
+ # @return [void]
52
+ #
53
+ # @api private
54
+ def self.define_meta(key)
55
+ define_method(key) { @metadata[key] }
56
+ (@metadata_keys ||= []) << key
57
+ end
58
+ private_class_method :define_meta
59
+
60
+ # The alignment configuration
61
+ #
62
+ # @return [Slideck::Alignment]
63
+ define_meta :align
64
+
65
+ # The footer configuration
66
+ #
67
+ # @return [Hash{Symbol => Slideck::Alignment,String}]
68
+ define_meta :footer
69
+
70
+ # The margin configuration
71
+ #
72
+ # @return [Slideck::Margin]
73
+ define_meta :margin
74
+
75
+ # The pager configuration
76
+ #
77
+ # @return [Hash{Symbol => Slideck::Alignment,String}]
78
+ define_meta :pager
79
+
80
+ # The symbols configuration
81
+ #
82
+ # @return [Hash, String, Symbol]
83
+ define_meta :symbols
84
+
85
+ # The theme configuration
86
+ #
87
+ # @return [Hash{Symbol => Array, String, Symbol}]
88
+ define_meta :theme
89
+
90
+ # Create a Metadata instance
91
+ #
92
+ # @param [Hash{Symbol => Object}] metadata
93
+ # the metadata configuration
94
+ #
95
+ # @api private
96
+ def initialize(metadata)
97
+ @metadata = metadata
98
+
99
+ freeze
100
+ end
101
+ private_class_method :new
102
+
103
+ # Determine equivalence with another object
104
+ #
105
+ # @example
106
+ # metadata == other
107
+ #
108
+ # @param [Object] other
109
+ # the other object to determine equivalence with
110
+ #
111
+ # @return [Boolean]
112
+ # true if this object is equivalent to the other, false otherwise
113
+ #
114
+ # @api public
115
+ def ==(other)
116
+ other.is_a?(self.class) &&
117
+ @metadata.keys.all? do |name|
118
+ send(name) == other.send(name)
119
+ end
120
+ end
121
+
122
+ # Determine equality with another object
123
+ #
124
+ # @example
125
+ # metadata.eql?(other)
126
+ #
127
+ # @param [Object] other
128
+ # the other object to determine equality with
129
+ #
130
+ # @return [Boolean]
131
+ # true if this object is equal to the other, false otherwise
132
+ #
133
+ # @api public
134
+ def eql?(other)
135
+ instance_of?(other.class) &&
136
+ @metadata.keys.all? do |name|
137
+ send(name).eql?(other.send(name))
138
+ end
139
+ end
140
+
141
+ # Generate hash value of this metadata
142
+ #
143
+ # @example
144
+ # metadata.hash
145
+ #
146
+ # @return [Integer]
147
+ #
148
+ # @api public
149
+ def hash
150
+ [self.class, *@metadata.keys.map { |name| send(name) }].hash
151
+ end
152
+ end # Metadata
153
+ end # Slideck
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slideck
4
+ # Responsible for converting custom metadata
5
+ #
6
+ # @api private
7
+ class MetadataConverter
8
+ # Create a MetadataConverter instance
9
+ #
10
+ # @example
11
+ # Slideck::MetadataConverter.new(Slideck::Alignment)
12
+ #
13
+ # @param [Slideck::Alignment] alignment
14
+ # the alignment initialiser
15
+ # @param [Slideck::Margin] margin
16
+ # the margin initialiser
17
+ #
18
+ # @api public
19
+ def initialize(alignment, margin)
20
+ @alignment = alignment
21
+ @margin = margin
22
+ end
23
+
24
+ # Convert metadata values
25
+ #
26
+ # @example
27
+ # metadata_converter.convert({align: "center"})
28
+ #
29
+ # @param [Hash{Symbol => Object}] custom_metadata
30
+ # the custom metadata to convert
31
+ #
32
+ # @return [Hash{Symbol => Object}]
33
+ #
34
+ # @api public
35
+ def convert(custom_metadata)
36
+ custom_metadata.each_with_object({}) do |(key, val), new_metadata|
37
+ new_metadata[key] = convert_for(key, val)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # Convert a value for a metadata key
44
+ #
45
+ # @param [Symbol] key
46
+ # the metadata key
47
+ # @param [Object] value
48
+ # the metadata value
49
+ #
50
+ # @return [Hash, Slideck::Alignment, Slideck::Margin, String, Symbol]
51
+ #
52
+ # @api private
53
+ def convert_for(key, value)
54
+ case key
55
+ when :align
56
+ @alignment.from(value)
57
+ when :margin
58
+ @margin.from(value)
59
+ when :footer, :pager
60
+ convert_align_key(wrap_with_text_key(value), "bottom")
61
+ else
62
+ value
63
+ end
64
+ end
65
+
66
+ # Wrap value in Hash with text key
67
+ #
68
+ # @param [Hash, String] value
69
+ # the value to wrap with text key
70
+ #
71
+ # @return [Hash{Symbol => String}]
72
+ #
73
+ # @api private
74
+ def wrap_with_text_key(value)
75
+ value.is_a?(::Hash) ? value : {text: value || ""}
76
+ end
77
+
78
+ # Convert value for align key in Hash to Alignment
79
+ #
80
+ # @param [Hash] value
81
+ # the value with align key
82
+ # @param [String] default
83
+ # the default vertical alignment
84
+ #
85
+ # @return [Hash{Symbol => Slideck::Alignment,String}]
86
+ #
87
+ # @api private
88
+ def convert_align_key(value, default)
89
+ return value unless value.key?(:align)
90
+
91
+ alignment = @alignment.from(value[:align], default: default)
92
+ value.merge(align: alignment)
93
+ end
94
+ end # MetadataConverter
95
+ end # Slideck
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slideck
4
+ # Default metadata configuration
5
+ #
6
+ # @api private
7
+ class MetadataDefaults
8
+ # Create a MetadataDefaults instance
9
+ #
10
+ # @example
11
+ # Slideck::MetadataDefaults.new(Slideck::Alignment)
12
+ #
13
+ # @param [Slideck::Alignment] alignment
14
+ # the alignment initialiser
15
+ # @param [Slideck::Margin] margin
16
+ # the margin initialiser
17
+ #
18
+ # @api public
19
+ def initialize(alignment, margin)
20
+ @alignment = alignment
21
+ @margin = margin
22
+ @defaults = create_defaults
23
+ end
24
+
25
+ # Merge given custom metadata with defaults
26
+ #
27
+ # @example
28
+ # metadata_defaults.merge({align: "center"})
29
+ #
30
+ # @param [Hash{Symbol => Object}] custom_metadata
31
+ # the custom metadata to merge
32
+ #
33
+ # @return [Hash{Symbol => Object}]
34
+ #
35
+ # @api public
36
+ def merge(custom_metadata)
37
+ @defaults.merge(custom_metadata) do |_, def_val, val|
38
+ def_val.is_a?(::Hash) ? def_val.merge(val) : val
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ # The default metadata configuration
45
+ #
46
+ # @return [Hash{Symbol => Object}]
47
+ #
48
+ # @api private
49
+ def create_defaults
50
+ {
51
+ align: @alignment["left", "top"],
52
+ footer: default_footer,
53
+ margin: @margin[0, 0, 0, 0],
54
+ pager: default_pager,
55
+ symbols: :unicode,
56
+ theme: {}
57
+ }.freeze
58
+ end
59
+
60
+ # The default footer configuration
61
+ #
62
+ # @return [Hash{Symbol => Slideck::Alignment,String}]
63
+ #
64
+ # @api private
65
+ def default_footer
66
+ {
67
+ align: @alignment["left", "bottom"],
68
+ text: ""
69
+ }.freeze
70
+ end
71
+
72
+ # The default pager configuration
73
+ #
74
+ # @return [Hash{Symbol => Slideck::Alignment,String}]
75
+ #
76
+ # @api private
77
+ def default_pager
78
+ {
79
+ align: @alignment["right", "bottom"],
80
+ text: "%<page>d / %<total>d"
81
+ }.freeze
82
+ end
83
+ end # MetadataDefaults
84
+ end # Slideck