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.
- 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
|