slideck 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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