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,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slideck
4
+ # Responsible for parsing metadata in YAML format
5
+ #
6
+ # @api private
7
+ class MetadataParser
8
+ # The symbolize names parameter
9
+ #
10
+ # @return [Symbol]
11
+ #
12
+ # @api private
13
+ SYMBOLIZE_NAMES_PARAMETER = :symbolize_names
14
+ private_constant :SYMBOLIZE_NAMES_PARAMETER
15
+
16
+ # The permitted classes parameter
17
+ #
18
+ # @return [Symbol]
19
+ #
20
+ # @api private
21
+ PERMITTED_CLASSES_PARAMETER = :permitted_classes
22
+ private_constant :PERMITTED_CLASSES_PARAMETER
23
+
24
+ # The whitelist classes parameter
25
+ #
26
+ # @return [Symbol]
27
+ #
28
+ # @api private
29
+ WHITELIST_CLASSES_PARAMETER = :whitelist_classes
30
+ private_constant :WHITELIST_CLASSES_PARAMETER
31
+
32
+ # Create a MetadataParser instance
33
+ #
34
+ # @example
35
+ # MetadataParser.new(YAML, symbolize_names: true, permitted_classes: [])
36
+ #
37
+ # @param [YAML] yaml_parser
38
+ # the YAML parser
39
+ # @param [Boolean] symbolize_names
40
+ # whether or not to symobolize names
41
+ # @param [Array<Object>] permitted_classes
42
+ # the classes allowed to be deserialized
43
+ #
44
+ # @api public
45
+ def initialize(yaml_parser, symbolize_names: nil, permitted_classes: nil)
46
+ @yaml_parser = yaml_parser
47
+ @symbolize_names = symbolize_names
48
+ @permitted_classes = permitted_classes
49
+ end
50
+
51
+ # Parse metadata from content
52
+ #
53
+ # @example
54
+ # parser.parse("align: center\nfooter: footer content")
55
+ #
56
+ # @param [String] content
57
+ # the content to parse metadata from
58
+ #
59
+ # @return [Hash{String, Symbol => Object}]
60
+ # the deserialized metadata
61
+ #
62
+ # @api public
63
+ def parse(content)
64
+ parse_method = select_parse_method
65
+ parse_params = parse_method_params(parse_method)
66
+ arguments = parser_arguments(parse_params)
67
+ options = parser_options(parse_params)
68
+ metadata = @yaml_parser.send(parse_method, content, *arguments, **options)
69
+
70
+ return metadata if symbolize_names?(options)
71
+
72
+ @symbolize_names ? symbolize_keys(metadata) : metadata
73
+ end
74
+
75
+ private
76
+
77
+ # Select metadata parse method
78
+ #
79
+ # @return [Symbol]
80
+ #
81
+ # @api private
82
+ def select_parse_method
83
+ @yaml_parser.respond_to?(:safe_load) ? :safe_load : :load
84
+ end
85
+
86
+ # Parse method parameters
87
+ #
88
+ # @param [Symbol] parse_method
89
+ # the parse method name
90
+ #
91
+ # @return [Array<Symbol>]
92
+ #
93
+ # @api private
94
+ def parse_method_params(parse_method)
95
+ @yaml_parser.method(parse_method).parameters.map(&:last)
96
+ end
97
+
98
+ # Generate parser arguments
99
+ #
100
+ # @param [Array<Symbol>] parse_method_params
101
+ # the parse method parameters
102
+ #
103
+ # @return [Array<Object>]
104
+ #
105
+ # @api private
106
+ def parser_arguments(parse_method_params)
107
+ return [] unless parse_method_params.include?(WHITELIST_CLASSES_PARAMETER)
108
+
109
+ [@permitted_classes]
110
+ end
111
+
112
+ # Generate parser options
113
+ #
114
+ # @param [Array<Symbol>] parse_method_params
115
+ # the parse method parameters
116
+ #
117
+ # @return [Hash{Symbol => Object}]
118
+ #
119
+ # @api private
120
+ def parser_options(parse_method_params)
121
+ {}.tap do |opts|
122
+ if parse_method_params.include?(PERMITTED_CLASSES_PARAMETER)
123
+ opts[:permitted_classes] = @permitted_classes
124
+ end
125
+
126
+ if parse_method_params.include?(SYMBOLIZE_NAMES_PARAMETER)
127
+ opts[:symbolize_names] = @symbolize_names
128
+ end
129
+ end
130
+ end
131
+
132
+ # Check whether the YAML parser can symbolize names or not
133
+ #
134
+ # @param [Hash{Symbol => Object}] parse_options
135
+ # the parse method options
136
+ #
137
+ # @return [Boolean]
138
+ #
139
+ # @api private
140
+ def symbolize_names?(parse_options)
141
+ parse_options.key?(:symbolize_names)
142
+ end
143
+
144
+ # Symbolize metadata keys
145
+ #
146
+ # @param [Object] object
147
+ # the object with keys to symbolize
148
+ #
149
+ # @return [Hash{Symbol => Object}]
150
+ #
151
+ # @api private
152
+ def symbolize_keys(object)
153
+ case object
154
+ when Hash then symbolize_hash_keys(object)
155
+ when Array then symbolize_array_hashes(object)
156
+ else object
157
+ end
158
+ end
159
+
160
+ # Symbolize hash keys
161
+ #
162
+ # @param [Hash] object
163
+ # the hash object with keys to symbolize
164
+ #
165
+ # @return [Hash{Symbol => Object}]
166
+ #
167
+ # @api private
168
+ def symbolize_hash_keys(object)
169
+ object.each_with_object({}) do |(key, val), new_hash|
170
+ new_hash[key.to_sym] = symbolize_keys(val)
171
+ end
172
+ end
173
+
174
+ # Symbolize array hash values
175
+ #
176
+ # @param [Array] object
177
+ # the array object with hash values to symbolize
178
+ #
179
+ # @return [Array<Object>]
180
+ #
181
+ # @api private
182
+ def symbolize_array_hashes(object)
183
+ object.each_with_object([]) do |val, new_array|
184
+ new_array << symbolize_keys(val)
185
+ end
186
+ end
187
+ end # MetadataParser
188
+ end # Slideck
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slideck
4
+ # Responsible for wrapping parsed global and slide metadata
5
+ #
6
+ # @api private
7
+ class MetadataWrapper
8
+ # Create a MetadataWrapper instance
9
+ #
10
+ # @example
11
+ # MetadataWrapper.new(metadata, metadata_converter, metadata_defaults)
12
+ #
13
+ # @param [Slideck::Metadata] metadata
14
+ # the metadata initialiser
15
+ # @param [Slideck::MetadataConverter] metadata_converter
16
+ # the metadata converter
17
+ # @param [Slideck::MetadataDefaults] metadata_defaults
18
+ # the metadata defaults
19
+ #
20
+ # @api public
21
+ def initialize(metadata, metadata_converter, metadata_defaults)
22
+ @metadata = metadata
23
+ @metadata_converter = metadata_converter
24
+ @metadata_defaults = metadata_defaults
25
+ end
26
+
27
+ # Wrap parsed global and slide metadata
28
+ #
29
+ # @example
30
+ # metadata_wrapper.wrap({metadata: {}, slides: []})
31
+ #
32
+ # @param [Hash{Symbol => Hash, String}] deck
33
+ # the deck of parsed metadata and slides
34
+ #
35
+ # @return [Array<Slideck::Metadata, Hash>]
36
+ #
37
+ # @api public
38
+ def wrap(deck)
39
+ [
40
+ build_metadata(deck[:metadata], @metadata_defaults),
41
+ deck[:slides].map do |slide|
42
+ {
43
+ content: slide[:content],
44
+ metadata: build_metadata(slide[:metadata], {})
45
+ }
46
+ end
47
+ ]
48
+ end
49
+
50
+ private
51
+
52
+ # Build metadata
53
+ #
54
+ # @param [Hash{Symbol => Object}] custom_metadata
55
+ # the custom metadata
56
+ # @param [#merge] defaults
57
+ # the defaults to merge with
58
+ #
59
+ # @return [Slideck::Metadata]
60
+ #
61
+ # @api private
62
+ def build_metadata(custom_metadata, defaults)
63
+ @metadata.from(@metadata_converter, custom_metadata, defaults)
64
+ end
65
+ end # MetadataWrapper
66
+ end # Slideck
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slideck
4
+ # Responsible for extracting metadata and slides from content
5
+ #
6
+ # @api private
7
+ class Parser
8
+ # The pattern to detect metadata configuration
9
+ #
10
+ # @return [Regexp]
11
+ #
12
+ # @api private
13
+ METADATA_PATTERN = /^\s*:?[^:]+:[^:]+/.freeze
14
+ private_constant :METADATA_PATTERN
15
+
16
+ # The pattern to detect slide separator
17
+ #
18
+ # @return [Regexp]
19
+ #
20
+ # @api private
21
+ SLIDE_SEPARATOR = /\n?-{3,}([^\n]*)\n/.freeze
22
+ private_constant :SLIDE_SEPARATOR
23
+
24
+ # The pattern to match entire lines
25
+ #
26
+ # @return [Regexp]
27
+ #
28
+ # @api private
29
+ LINE_PATTERN = /^[^\n]+$/.freeze
30
+ private_constant :LINE_PATTERN
31
+
32
+ # Create a Parser instance
33
+ #
34
+ # @example
35
+ # Parser.new(StringScanner, Slideck::MetadataParser)
36
+ #
37
+ # @param [StringScanner] string_scanner
38
+ # the content scanner
39
+ # @param [Slideck::MetadataParser] metadata_parser
40
+ # the metadata parser
41
+ #
42
+ # @api public
43
+ def initialize(string_scanner, metadata_parser)
44
+ @string_scanner = string_scanner
45
+ @metadata_parser = metadata_parser
46
+ end
47
+
48
+ # Parse metadata and slides from content
49
+ #
50
+ # @example
51
+ # parser.parse("align: center\n---\nSlide1\n---\nSlide2\n---")
52
+ #
53
+ # @param [String] content
54
+ # the content to parse slides from
55
+ #
56
+ # @return [Hash{Symbol => Hash, Array<String>}]
57
+ # the metadata and slides content
58
+ #
59
+ # @api public
60
+ def parse(content)
61
+ scanner = @string_scanner.new(content)
62
+ slides = split_into_slides(scanner)
63
+ metadata = extract_metadata(slides.first && slides.first[:content])
64
+
65
+ {metadata: metadata, slides: metadata.empty? ? slides : slides[1..-1]}
66
+ end
67
+
68
+ private
69
+
70
+ # Split content into slides
71
+ #
72
+ # @param [StringScanner] scanner
73
+ # the slides content scanner
74
+ #
75
+ # @return [Array<String>]
76
+ #
77
+ # @api private
78
+ def split_into_slides(scanner)
79
+ slides, slide, slide_metadata = [], [], {}
80
+
81
+ until scanner.eos?
82
+ if scanner.scan(SLIDE_SEPARATOR)
83
+ slides = add_slide(slides, slide.join, slide_metadata)
84
+ slide_metadata = extract_metadata(scanner[1])
85
+ slide.clear
86
+ elsif scanner.scan(LINE_PATTERN)
87
+ slide << scanner.matched
88
+ else
89
+ slide << scanner.getch
90
+ end
91
+ end
92
+
93
+ add_slide(slides, slide.join.chomp, slide_metadata)
94
+ end
95
+
96
+ # Add a slide to slides
97
+ #
98
+ # @param [Array<String>] slides
99
+ # the slides array
100
+ # @param [String] slide
101
+ # the slide to add to slides
102
+ # @param [Hash{String, Symbol => Object}] slide_metadata
103
+ # the slide metadata
104
+ #
105
+ # @return [Array<Hash{Symbol => Hash, String}>]
106
+ #
107
+ # @api private
108
+ def add_slide(slides, slide, slide_metadata)
109
+ return slides if slide.empty?
110
+
111
+ slides + [{content: slide, metadata: slide_metadata}]
112
+ end
113
+
114
+ # Extract metadata from a slide
115
+ #
116
+ # @param [String, nil] slide
117
+ #
118
+ # @return [Hash]
119
+ #
120
+ # @api private
121
+ def extract_metadata(slide)
122
+ return {} if slide.nil? || !metadata_given?(slide)
123
+
124
+ @metadata_parser.parse(slide)
125
+ end
126
+
127
+ # Check whether or not metadata is given
128
+ #
129
+ # @param [String] content
130
+ # the slide content to check
131
+ #
132
+ # @return [Boolean]
133
+ #
134
+ # @api private
135
+ def metadata_given?(content)
136
+ !(content.lines.first =~ METADATA_PATTERN).nil?
137
+ end
138
+ end # Parser
139
+ end # Slideck
@@ -0,0 +1,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slideck
4
+ # Responsible for presenting slides
5
+ #
6
+ # @api private
7
+ class Presenter
8
+ # Terminal screen size change signal
9
+ #
10
+ # @return [String]
11
+ #
12
+ # @api private
13
+ TERM_SCREEN_SIZE_CHANGE_SIG = "WINCH"
14
+ private_constant :TERM_SCREEN_SIZE_CHANGE_SIG
15
+
16
+ # Create a Presenter
17
+ #
18
+ # @param [TTY::Reader] reader
19
+ # the keyboard input reader
20
+ # @param [Slideck::Renderer] renderer
21
+ # the slides renderer
22
+ # @param [Slideck::Tracker] tracker
23
+ # the tracker for slides
24
+ # @param [TTY::Screen] screen
25
+ # the terminal screen size
26
+ # @param [IO] output
27
+ # the output stream for the slides
28
+ # @param [Proc] reloader
29
+ # the metadata and slides reloader
30
+ #
31
+ # @api public
32
+ def initialize(reader, renderer, tracker, screen, output, &reloader)
33
+ @reader = reader
34
+ @renderer = renderer
35
+ @tracker = tracker
36
+ @screen = screen
37
+ @output = output
38
+ @reloader = reloader
39
+ @stop = false
40
+ @buffer = []
41
+ end
42
+
43
+ # Reload presentation
44
+ #
45
+ # @example
46
+ # presenter.reload
47
+ #
48
+ # @return [Slideck::Presenter]
49
+ #
50
+ # @api public
51
+ def reload
52
+ @metadata, @slides = *@reloader.()
53
+ @tracker = @tracker.resize(@slides.size)
54
+ self
55
+ end
56
+
57
+ # Start presentation
58
+ #
59
+ # @example
60
+ # presenter.start
61
+ #
62
+ # @return [void]
63
+ #
64
+ # @api public
65
+ def start
66
+ reload
67
+ @reader.subscribe(self)
68
+ hide_cursor
69
+ subscribe_to_screen_resize { resize.render }
70
+
71
+ until @stop
72
+ render
73
+ @reader.read_keypress
74
+ end
75
+ ensure
76
+ show_cursor
77
+ end
78
+
79
+ # Stop presentation
80
+ #
81
+ # @example
82
+ # presenter.stop
83
+ #
84
+ # @return [Slideck::Presenter]
85
+ #
86
+ # @api public
87
+ def stop
88
+ @stop = true
89
+ self
90
+ end
91
+
92
+ # Render presentation on cleared screen
93
+ #
94
+ # @example
95
+ # presenter.render
96
+ #
97
+ # @return [void]
98
+ #
99
+ # @api public
100
+ def render
101
+ clear_screen
102
+ render_slide
103
+ end
104
+
105
+ # Clear terminal screen
106
+ #
107
+ # @return [void]
108
+ #
109
+ # @api private
110
+ def clear_screen
111
+ @output.print @renderer.clear
112
+ end
113
+
114
+ # Render the current slide
115
+ #
116
+ # @return [void]
117
+ #
118
+ # @api private
119
+ def render_slide
120
+ @output.print @renderer.render(
121
+ @metadata,
122
+ @slides[@tracker.current],
123
+ @tracker.current + 1,
124
+ @tracker.total)
125
+ end
126
+
127
+ # Hide cursor
128
+ #
129
+ # @return [void]
130
+ #
131
+ # @api private
132
+ def hide_cursor
133
+ @output.print @renderer.cursor.hide
134
+ end
135
+
136
+ # Show cursor
137
+ #
138
+ # @return [void]
139
+ #
140
+ # @api private
141
+ def show_cursor
142
+ @output.print @renderer.cursor.show
143
+ end
144
+
145
+ # Subscribe to the terminal screen size change signal
146
+ #
147
+ # @param [Proc] resizer
148
+ # the presentation resizer
149
+ #
150
+ # @return [void]
151
+ #
152
+ # @api private
153
+ def subscribe_to_screen_resize(&resizer)
154
+ return if @screen.windows?
155
+
156
+ Signal.trap(TERM_SCREEN_SIZE_CHANGE_SIG, &resizer)
157
+ end
158
+
159
+ # Resize presentation
160
+ #
161
+ # @return [Slideck::Presenter]
162
+ #
163
+ # @api private
164
+ def resize
165
+ @renderer = @renderer.resize(@screen.width, @screen.height)
166
+ self
167
+ end
168
+
169
+ # Handle a keypress event
170
+ #
171
+ # @param [TTY::Reader::KeyEvent] event
172
+ # the key event
173
+ #
174
+ # @return [void]
175
+ #
176
+ # @api private
177
+ def keypress(event)
178
+ case event.value
179
+ when "n", "l" then keyright
180
+ when "p", "h" then keyleft
181
+ when "f", "^" then go_to_first
182
+ when "t", "$" then go_to_last
183
+ when "g" then go_to_slide
184
+ when /\d/ then add_to_buffer(event.value)
185
+ when "r" then keyctrl_l
186
+ when "q" then keyctrl_x
187
+ end
188
+ end
189
+
190
+ # Navigate to the next slide
191
+ #
192
+ # @return [void]
193
+ #
194
+ # @api private
195
+ def keyright(*)
196
+ @tracker = @tracker.next
197
+ end
198
+ alias keyspace keyright
199
+ alias keypage_down keyright
200
+
201
+ # Navigate to the previous slide
202
+ #
203
+ # @return [void]
204
+ #
205
+ # @api private
206
+ def keyleft(*)
207
+ @tracker = @tracker.previous
208
+ end
209
+ alias keybackspace keyleft
210
+ alias keypage_up keyleft
211
+
212
+ # Reload presentation
213
+ #
214
+ # @return [void]
215
+ #
216
+ # @api private
217
+ def keyctrl_l(*)
218
+ reload
219
+ end
220
+
221
+ # Exit presentation
222
+ #
223
+ # @return [void]
224
+ #
225
+ # @api private
226
+ def keyctrl_x(*)
227
+ clear_screen
228
+ stop
229
+ end
230
+ alias keyescape keyctrl_x
231
+
232
+ # Navigate to the fist slide
233
+ #
234
+ # @return [void]
235
+ #
236
+ # @api private
237
+ def go_to_first
238
+ @tracker = @tracker.first
239
+ end
240
+
241
+ # Navigate to the last slide
242
+ #
243
+ # @return [void]
244
+ #
245
+ # @api private
246
+ def go_to_last
247
+ @tracker = @tracker.last
248
+ end
249
+
250
+ # Navigate to a given slide
251
+ #
252
+ # @return [void]
253
+ #
254
+ # @api private
255
+ def go_to_slide
256
+ @tracker = @tracker.go_to(@buffer.join.to_i - 1)
257
+ @buffer.clear
258
+ end
259
+
260
+ # Add to the input buffer
261
+ #
262
+ # @param [String] input_key
263
+ # the input key
264
+ #
265
+ # @return [void]
266
+ #
267
+ # @api private
268
+ def add_to_buffer(input_key)
269
+ @buffer += [input_key]
270
+ end
271
+ end # Presenter
272
+ end # Slideck