showoff 0.20.1 → 0.20.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/Rakefile +24 -12
- data/bin/showoff +47 -24
- data/lib/showoff.rb +43 -20
- data/lib/showoff/compiler.rb +106 -0
- data/lib/showoff/compiler/downloads.rb +91 -0
- data/lib/showoff/compiler/fixups.rb +142 -0
- data/lib/showoff/compiler/form.rb +236 -0
- data/lib/showoff/compiler/glossary.rb +164 -0
- data/lib/showoff/compiler/i18n.rb +24 -0
- data/lib/showoff/compiler/notes.rb +73 -0
- data/lib/showoff/compiler/table_of_contents.rb +51 -0
- data/lib/showoff/compiler/variables.rb +71 -0
- data/lib/showoff/config.rb +218 -0
- data/lib/showoff/locale.rb +132 -0
- data/lib/showoff/logger.rb +15 -0
- data/lib/showoff/monkeypatches.rb +28 -0
- data/lib/showoff/presentation.rb +181 -0
- data/lib/showoff/presentation/section.rb +70 -0
- data/lib/showoff/presentation/slide.rb +113 -0
- data/lib/showoff/state.rb +89 -0
- data/lib/showoff/version.rb +2 -2
- data/lib/showoff_ng.rb +99 -0
- data/lib/showoff_utils.rb +21 -19
- data/public/css/showoff.css +14 -1
- data/public/js/highlight.pack-9.15.10.js +22614 -0
- data/public/js/showoff.js +3 -3
- data/views/header.erb +3 -3
- data/views/header_mini.erb +2 -2
- data/views/onepage.erb +4 -10
- data/views/presenter.erb +5 -5
- data/views/slide.erb +29 -0
- metadata +24 -21
- data/locales/id.yml +0 -2
- data/public/js/highlight.pack-9.2.0.js +0 -15448
@@ -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
|