slideck 0.1.0

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.
@@ -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