showoff 0.20.1 → 0.20.2

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,91 @@
1
+ # adds file download link processing
2
+ class Showoff::Compiler::Downloads
3
+
4
+ # Scan for file download links and move them to the state storage.
5
+ #
6
+ # @param doc [Nokogiri::HTML::DocumentFragment]
7
+ # The slide document
8
+ #
9
+ # @return [Nokogiri::HTML::DocumentFragment]
10
+ # The slide DOM with download links removed.
11
+ #
12
+ # @todo Should .download change meaning to 'make available on this slide'?
13
+ #
14
+ # @see
15
+ # https://github.com/puppetlabs/showoff/blob/3f43754c84f97be4284bb34f9bc7c42175d45226/lib/showoff.rb#L1056-L1073
16
+ def self.scanForFiles!(doc, options)
17
+ current = Showoff::State.get(:slide_count)
18
+ doc.search('p.download').each do |container|
19
+ links = container.text.gsub(/^\.download ?/, '')
20
+ links.split("\n").each do |line|
21
+ file, modifier = line.split
22
+ modifier ||= 'next' # @todo Is this still the proper default?
23
+
24
+ case modifier
25
+ when 'a', 'all', 'always', 'now'
26
+ self.pushFile(0, current, options[:name], file)
27
+ when 'p', 'prev', 'previous'
28
+ self.pushFile(current-1, current, options[:name], file)
29
+ when 'c', 'curr', 'current'
30
+ self.pushFile(current, current, options[:name], file)
31
+ when 'n', 'next'
32
+ self.pushFile(current+1, current, options[:name], file)
33
+ end
34
+ end
35
+
36
+ container.remove
37
+ end
38
+
39
+ doc
40
+ end
41
+
42
+
43
+ # Convention that index 0 represents files that are always available and every
44
+ # other index represents files whose visibility will be triggered on that slide.
45
+ #
46
+ # [
47
+ # {
48
+ # :enabled => false,
49
+ # :slides => [
50
+ # {:slidenum => num, :source => name, :file => file},
51
+ # {:slidenum => num, :source => name, :file => file},
52
+ # ],
53
+ # },
54
+ # {
55
+ # :enabled => false,
56
+ # :slides => [
57
+ # {:slidenum => num, :source => name, :file => file},
58
+ # {:slidenum => num, :source => name, :file => file},
59
+ # ],
60
+ # },
61
+ # ]
62
+
63
+
64
+ def self.pushFile(index, current, source, file)
65
+ record = Showoff::State.getAtIndex(:downloads, index) || {}
66
+ record[:enabled] ||= false
67
+ record[:slides] ||= []
68
+ record[:slides] << {:slidenum => current, :source => source, :file => file}
69
+
70
+ Showoff::State.setAtIndex(:downloads, index, record)
71
+ end
72
+
73
+ def self.enableFiles(index)
74
+ record = Showoff::State.getAtIndex(:downloads, index)
75
+ return unless record
76
+
77
+ record[:enabled] = true
78
+ Showoff::State.setAtIndex(:downloads, index, record)
79
+ end
80
+
81
+ def self.getFiles(index)
82
+ record = Showoff::State.getAtIndex(:downloads, index)
83
+
84
+ if (record and record[:enabled])
85
+ record[:slides]
86
+ else
87
+ []
88
+ end
89
+ end
90
+
91
+ end
@@ -0,0 +1,142 @@
1
+ require 'commandline_parser'
2
+
3
+ # adds misc fixup methods to the compiler
4
+ class Showoff::Compiler::Fixups
5
+
6
+ # Find any <p> or <img> tags with classes defined via the prefixed dot syntax.
7
+ # Remove .break and .comment paragraphs and apply classes/alt to the rest.
8
+ #
9
+ # @param doc [Nokogiri::HTML::DocumentFragment]
10
+ # The slide document
11
+ # @return [Nokogiri::HTML::DocumentFragment]
12
+ # The document with classes applied.
13
+ def self.updateClasses!(doc)
14
+ doc.search('p').select {|p| p.text.start_with? '.'}.each do |p|
15
+ # The first string of plain text in the paragraph
16
+ node = p.children.first
17
+ classes, sep, text = node.content.partition(' ')
18
+ classes = classes.split('.')
19
+ classes.shift
20
+
21
+ if ['break', 'comment'].include? classes.first
22
+ p.remove
23
+ else
24
+ p.add_class(classes.join(' '))
25
+ node.content = text
26
+ end
27
+ end
28
+
29
+ doc.search('img').select {|img| img.attr('alt').start_with? '.'}.each do |img|
30
+ classes, sep, text = img.attr('alt').partition(' ')
31
+ classes = classes.split('.')
32
+ classes.shift
33
+
34
+ img.add_class(classes.join(' '))
35
+ img.set_attribute('alt', text)
36
+ end
37
+
38
+ doc
39
+ end
40
+
41
+ # Ensure that all links open in a new window. Perhaps move some of this to glossary.rb
42
+ def self.updateLinks!(doc)
43
+ doc.search('a').each do |link|
44
+ next unless link['href']
45
+ next if link['href'].start_with? '#'
46
+ next if link['href'].start_with? 'glossary://'
47
+ # Add a target so we open all external links from notes in a new window
48
+ link.set_attribute('target', '_blank')
49
+ end
50
+
51
+ doc
52
+ end
53
+
54
+ # This munges code blocks to ensure the proper syntax highlighting
55
+ # @see
56
+ # https://github.com/puppetlabs/showoff/blob/3f43754c84f97be4284bb34f9bc7c42175d45226/lib/showoff.rb#L1105-L1133
57
+ def self.updateSyntaxHighlighting!(doc)
58
+ doc.search('pre').each do |pre|
59
+ pre.search('code').each do |code|
60
+ out = code.text
61
+ lang = code.get_attribute('class')
62
+
63
+ # Skip this if we've got an empty code block
64
+ next if out.empty?
65
+
66
+ # catch fenced code blocks from commonmarker
67
+ if (lang and lang.start_with? 'language-' )
68
+ pre.set_attribute('class', 'highlight')
69
+ # turn the colon separated name back into classes
70
+ code.set_attribute('class', lang.gsub(':', ' '))
71
+
72
+ # or we've started a code block with a Showoff language tag
73
+ elsif out.strip[0, 3] == '@@@'
74
+ lines = out.split("\n")
75
+ lang = lines.shift.gsub('@@@', '').strip
76
+ pre.set_attribute('class', 'highlight')
77
+ code.set_attribute('class', 'language-' + lang.downcase) if !lang.empty?
78
+ code.content = lines.join("\n")
79
+ end
80
+
81
+ end
82
+ end
83
+
84
+ doc
85
+ end
86
+
87
+ # This munges commandline code blocks for the proper classing
88
+ # @see
89
+ # https://github.com/puppetlabs/showoff/blob/3f43754c84f97be4284bb34f9bc7c42175d45226/lib/showoff.rb#L1107
90
+ # https://github.com/puppetlabs/showoff/blob/3f43754c84f97be4284bb34f9bc7c42175d45226/lib/showoff.rb#L1135-L1163
91
+ def self.updateCommandlineBlocks!(doc)
92
+ parser = CommandlineParser.new
93
+ doc.search('.commandline > pre > code').each do |code|
94
+ out = code.text
95
+ code.content = ''
96
+ tree = parser.parse(out)
97
+ transform = Parslet::Transform.new do
98
+ rule(:prompt => simple(:prompt), :input => simple(:input), :output => simple(:output)) do
99
+ command = Nokogiri::XML::Node.new('code', doc)
100
+ command.set_attribute('class', 'command')
101
+ command.content = "#{prompt} #{input}"
102
+ code << command
103
+
104
+ # Add newline after the input so that users can
105
+ # advance faster than the typewriter effect
106
+ # and still keep inputs on separate lines.
107
+ code << "\n"
108
+
109
+ unless output.to_s.empty?
110
+
111
+ result = Nokogiri::XML::Node.new('code', doc)
112
+ result.set_attribute('class', 'result')
113
+ result.content = output
114
+ code << result
115
+ end
116
+ end
117
+ end
118
+ transform.apply(tree)
119
+ end
120
+
121
+ doc
122
+ end
123
+
124
+ # Because source slide files can be nested in arbitrarily deep directories we
125
+ # need to simplify paths to images when we flatten it out to a single HTML file.
126
+ # @see
127
+ # https://github.com/puppetlabs/showoff/blob/220d6eef4c5942eda625dd6edc5370c7490eced7/lib/showoff.rb#L1076-L1103
128
+ def self.updateImagePaths!(doc, options={})
129
+ doc.search('img').each do |img|
130
+ slide_dir = File.dirname(options[:name])
131
+
132
+ # We need to turn all URLs into relative from the root. If it starts with '/'
133
+ # then we can assume the author meant to start the path at the presentation root.
134
+ if img[:src].start_with? '/'
135
+ img[:src] = img[:src][1..-1]
136
+ else
137
+ # clean up the path and remove some of the relative nonsense
138
+ img[:src] = Pathname.new(File.join(slide_dir, img[:src])).cleanpath.to_path
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,236 @@
1
+ # Adds form processing to the compiler
2
+ #
3
+ # @see https://github.com/puppetlabs/showoff/blob/3f43754c84f97be4284bb34f9bc7c42175d45226/lib/showoff.rb#L848-L1022
4
+ class Showoff::Compiler::Form
5
+
6
+ # Add the form markup to the slide and then render all elements
7
+ #
8
+ # @todo UI elements to translate once i18n is baked in.
9
+ # @todo Someday this should be rearchitected into the markdown renderer.
10
+ #
11
+ # @return [Nokogiri::HTML::DocumentFragment]
12
+ # The slide DOM with all form elements rendered.
13
+ #
14
+ # @see
15
+ # https://github.com/puppetlabs/showoff/blob/3f43754c84f97be4284bb34f9bc7c42175d45226/lib/showoff.rb#L849-L878
16
+ def self.render!(doc, options={})
17
+ title = options[:form]
18
+ return unless title
19
+
20
+ begin
21
+ tools = Nokogiri::XML::Node.new('div', doc)
22
+ tools.add_class('tools')
23
+ doc.add_child(tools)
24
+
25
+ button = Nokogiri::XML::Node.new('input', doc)
26
+ button.add_class('display')
27
+ button.set_attribute('type', 'button')
28
+ button.set_attribute('value', I18n.t('forms.display'))
29
+ tools.add_child(button)
30
+
31
+ submit = Nokogiri::XML::Node.new('input', doc)
32
+ submit.add_class('save')
33
+ submit.set_attribute('type', 'submit')
34
+ submit.set_attribute('value', I18n.t('forms.save'))
35
+ submit.set_attribute('disabled', 'disabled')
36
+ tools.add_child(submit)
37
+
38
+ form = Nokogiri::XML::Node.new('form', doc)
39
+ form.set_attribute('id', title)
40
+ form.set_attribute('action', "form/#{title}")
41
+ form.set_attribute('method', 'POST')
42
+ doc.add_child(form)
43
+
44
+ doc.children.each do |elem|
45
+ next if elem == form
46
+ elem.parent = form
47
+ end
48
+
49
+ doc.css('p').each do |p|
50
+ if p.text =~ /^(\w*) ?(?:->)? ?(.*)? (\*?)= ?(.*)?$/
51
+ code = $1
52
+ id = "#{title}_#{code}"
53
+ name = $2.empty? ? code : $2
54
+ required = ! $3.empty?
55
+ rhs = $4
56
+
57
+ p.replace self.form_element(id, code, name, required, rhs, p.text)
58
+ end
59
+ end
60
+
61
+ rescue Exception => e
62
+ Showoff::Logger.warn "Form parsing failed: #{e.message}"
63
+ Showoff::Logger.debug "Backtrace:\n\t#{e.backtrace.join("\n\t")}"
64
+ end
65
+
66
+ doc
67
+ end
68
+
69
+ # Generates markup for any supported form element type
70
+ #
71
+ # @param id [String]
72
+ # The HTML ID for the generated markup
73
+ # @param code [String]
74
+ # The question code; used for indexing
75
+ # @param name [String]
76
+ # The full text of the question
77
+ # @param required [Boolean]
78
+ # Whether the rendered element should be marked as required
79
+ # @param rhs [String]
80
+ # The right hand side of the question specification, if on one line.
81
+ # @param text [String]
82
+ # The full content of the content, used for recursive multiline calls
83
+ #
84
+ # @return [String]
85
+ # The HTML markup for all the HTML nodes that the full element renders to.
86
+ #
87
+ # @see
88
+ # https://github.com/puppetlabs/showoff/blob/3f43754c84f97be4284bb34f9bc7c42175d45226/lib/showoff.rb#L880-L903
89
+ def self.form_element(id, code, name, required, rhs, text)
90
+ required = required ? 'required' : ''
91
+ str = "<div class='form element #{required}' id='#{id}' data-name='#{code}'>"
92
+ str << "<label class='question' for='#{id}'>#{name}</label>"
93
+ case rhs
94
+ when /^\[\s+(\d*)\]$$/ # value = [ 5] (textarea)
95
+ str << self.form_element_textarea(id, code, $1)
96
+ when /^___+(?:\[(\d+)\])?$/ # value = ___[50] (text)
97
+ str << self.form_element_text(id, code, $1)
98
+ when /^\(.?\)/ # value = (x) option one (=) opt2 () opt3 -> option 3 (radio)
99
+ str << self.form_element_radio(id, code, rhs.scan(/\((.?)\)\s*([^()]+)\s*/))
100
+ when /^\[.?\]/ # value = [x] option one [=] opt2 [] opt3 -> option 3 (checkboxes)
101
+ str << self.form_element_checkboxes(id, code, rhs.scan(/\[(.?)\] ?([^\[\]]+)/))
102
+ when /^\{(.*)\}$/ # value = {BOS, [SFO], (NYC)} (select shorthand)
103
+ str << self.form_element_select(id, code, rhs.scan(/[(\[]?\w+[)\]]?/))
104
+ when /^\{$/ # value = { (select)
105
+ str << self.form_element_select_multiline(id, code, text)
106
+ when '' # value = (radio/checkbox list)
107
+ str << self.form_element_multiline(id, code, text)
108
+ else
109
+ Showoff::Logger.warn "Unmatched form element: #{rhs}"
110
+ end
111
+ str << '</div>'
112
+ end
113
+
114
+ def self.form_element_text(id, code, length)
115
+ "<input type='text' id='#{id}_response' name='#{code}' size='#{length}' />"
116
+ end
117
+
118
+ def self.form_element_textarea(id, code, rows)
119
+ rows = 3 if rows.empty?
120
+ "<textarea id='#{id}_response' name='#{code}' rows='#{rows}'></textarea>"
121
+ end
122
+
123
+ def self.form_element_radio(id, code, items)
124
+ self.form_element_check_or_radio_set('radio', id, code, items)
125
+ end
126
+
127
+ def self.form_element_checkboxes(id, code, items)
128
+ self.form_element_check_or_radio_set('checkbox', id, code, items)
129
+ end
130
+
131
+ def self.form_element_select(id, code, items)
132
+ str = "<select id='#{id}_response' name='#{code}'>"
133
+ str << '<option value="">----</option>'
134
+
135
+ items.each do |item|
136
+ selected = classes = ''
137
+ case item
138
+ when /\((\w+)\)/
139
+ item = $1
140
+ selected = 'selected'
141
+ when /\[(\w+)\]/
142
+ item = $1
143
+ classes = 'correct'
144
+ end
145
+ str << "<option value='#{item}' class='#{classes}' #{selected}>#{item}</option>"
146
+ end
147
+ str << '</select>'
148
+ end
149
+
150
+ def self.form_element_select_multiline(id, code, text)
151
+ str = "<select id='#{id}_response' name='#{code}'>"
152
+ str << '<option value="">----</option>'
153
+
154
+ text.split("\n")[1..-1].each do |item|
155
+ case item
156
+ when /^ +\((\w+) -> (.+)\),?$/ # (NYC -> New York City)
157
+ str << "<option value='#{$1}' selected>#{$2}</option>"
158
+ when /^ +\[(\w+) -> (.+)\],?$/ # [NYC -> New York City]
159
+ str << "<option value='#{$1}' class='correct'>#{$2}</option>"
160
+ when /^ +(\w+) -> (.+),?$/ # NYC -> New, York City
161
+ str << "<option value='#{$1}'>#{$2}</option>"
162
+ when /^ +\((.+)\)$/ # (Boston)
163
+ str << "<option value='#{$1}' selected>#{$1}</option>"
164
+ when /^ +\[(.+)\]$/ # [Boston]
165
+ str << "<option value='#{$1}' class='correct'>#{$1}</option>"
166
+ when /^ +([^\(].+[^\),]),?$/ # Boston
167
+ str << "<option value='#{$1}'>#{$1}</option>"
168
+ end
169
+ end
170
+ str << '</select>'
171
+ end
172
+
173
+ def self.form_element_multiline(id, code, text)
174
+ str = '<ul>'
175
+
176
+ text.split("\n")[1..-1].each do |item|
177
+ case item
178
+ when /\((.?)\)\s*(\w+)\s*(?:->\s*(.*)?)?/
179
+ modifier = $1
180
+ type = 'radio'
181
+ value = $2
182
+ label = $3 || $2
183
+ when /\[(.?)\]\s*(\w+)\s*(?:->\s*(.*)?)?/
184
+ modifier = $1
185
+ type = 'checkbox'
186
+ value = $2
187
+ label = $3 || $2
188
+ end
189
+
190
+ str << '<li>'
191
+ str << self.form_element_check_or_radio(type, id, code, value, label, modifier)
192
+ str << '</li>'
193
+ end
194
+ str << '</ul>'
195
+ end
196
+
197
+ def self.form_element_check_or_radio_set(type, id, code, items)
198
+ str = ''
199
+ items.each do |item|
200
+ modifier = item[0]
201
+
202
+ if item[1] =~ /^(\w*) -> (.*)$/
203
+ value = $1
204
+ label = $2
205
+ else
206
+ value = label = item[1].strip
207
+ end
208
+
209
+ str << self.form_element_check_or_radio(type, id, code, value, label, modifier)
210
+ end
211
+ str
212
+ end
213
+
214
+ def self.form_element_check_or_radio(type, id, code, value, label, modifier)
215
+ # yes, value and id are conflated, because this is the id of the parent widget
216
+ checked = self.form_checked?(modifier)
217
+ classes = self.form_classes(modifier)
218
+
219
+ name = (type == 'checkbox') ? "#{code}[]" : code
220
+ str = "<input type='#{type}' name='#{name}' id='#{id}_#{value}' value='#{value}' class='#{classes}' #{checked} />"
221
+ str << "<label for='#{id}_#{value}' class='#{classes}'>#{label}</label>"
222
+ end
223
+
224
+ def self.form_classes(modifier)
225
+ modifier.downcase!
226
+ classes = ['response']
227
+ classes << 'correct' if modifier.include?('=')
228
+
229
+ classes.join(' ')
230
+ end
231
+
232
+ def self.form_checked?(modifier)
233
+ modifier.downcase.include?('x') ? "checked='checked'" : ''
234
+ end
235
+
236
+ end