showoff 0.20.1 → 0.20.2

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