showoff 0.19.4 → 0.20.3
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/keymap.rb +19 -5
- data/lib/showoff.rb +96 -44
- 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 +27 -21
- data/public/css/font-awesome-5.6.1/css/all.min.css +5 -0
- data/public/css/font-awesome-5.6.1/webfonts/fa-brands-400.eot +0 -0
- data/public/css/font-awesome-5.6.1/webfonts/fa-brands-400.svg +1260 -0
- data/public/css/font-awesome-5.6.1/webfonts/fa-brands-400.ttf +0 -0
- data/public/css/font-awesome-5.6.1/webfonts/fa-brands-400.woff +0 -0
- data/public/css/font-awesome-5.6.1/webfonts/fa-brands-400.woff2 +0 -0
- data/public/css/font-awesome-5.6.1/webfonts/fa-regular-400.eot +0 -0
- data/public/css/font-awesome-5.6.1/webfonts/fa-regular-400.svg +471 -0
- data/public/css/font-awesome-5.6.1/webfonts/fa-regular-400.ttf +0 -0
- data/public/css/font-awesome-5.6.1/webfonts/fa-regular-400.woff +0 -0
- data/public/css/font-awesome-5.6.1/webfonts/fa-regular-400.woff2 +0 -0
- data/public/css/font-awesome-5.6.1/webfonts/fa-solid-900.eot +0 -0
- data/public/css/font-awesome-5.6.1/webfonts/fa-solid-900.svg +2760 -0
- data/public/css/font-awesome-5.6.1/webfonts/fa-solid-900.ttf +0 -0
- data/public/css/font-awesome-5.6.1/webfonts/fa-solid-900.woff +0 -0
- data/public/css/font-awesome-5.6.1/webfonts/fa-solid-900.woff2 +0 -0
- data/public/css/presenter.css +1 -0
- data/public/css/showoff.css +42 -10
- data/public/js/highlight.pack-9.15.10.js +22614 -0
- data/public/js/highlightjs-line-numbers.min.js +1 -0
- data/public/js/presenter.js +13 -11
- data/public/js/showoff.js +39 -31
- data/views/download.erb +2 -2
- data/views/header.erb +27 -26
- data/views/header_mini.erb +8 -8
- data/views/index.erb +56 -56
- data/views/onepage.erb +10 -16
- data/views/presenter.erb +124 -123
- data/views/slide.erb +29 -0
- data/views/stats.erb +1 -1
- metadata +113 -100
- data/locales/id.yml +0 -2
- data/public/css/font-awesome-4.4.0/css/font-awesome.min.css +0 -4
- data/public/css/font-awesome-4.4.0/fonts/FontAwesome.otf +0 -0
- data/public/css/font-awesome-4.4.0/fonts/fontawesome-webfont.eot +0 -0
- data/public/css/font-awesome-4.4.0/fonts/fontawesome-webfont.svg +0 -640
- data/public/css/font-awesome-4.4.0/fonts/fontawesome-webfont.ttf +0 -0
- data/public/css/font-awesome-4.4.0/fonts/fontawesome-webfont.woff +0 -0
- data/public/css/font-awesome-4.4.0/fonts/fontawesome-webfont.woff2 +0 -0
- data/public/js/highlight.pack-9.2.0.js +0 -15448
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'tilt'
|
2
|
+
require 'tilt/erb'
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
class Showoff::Compiler
|
6
|
+
require 'showoff/compiler/form'
|
7
|
+
require 'showoff/compiler/variables'
|
8
|
+
require 'showoff/compiler/fixups'
|
9
|
+
require 'showoff/compiler/i18n'
|
10
|
+
require 'showoff/compiler/notes'
|
11
|
+
require 'showoff/compiler/glossary'
|
12
|
+
require 'showoff/compiler/downloads'
|
13
|
+
require 'showoff/compiler/table_of_contents'
|
14
|
+
|
15
|
+
def initialize(options)
|
16
|
+
@options = options
|
17
|
+
@profile = profile
|
18
|
+
end
|
19
|
+
|
20
|
+
# Configures Tilt with the selected engine and options.
|
21
|
+
#
|
22
|
+
# Returns render options profile hash
|
23
|
+
#
|
24
|
+
# Source:
|
25
|
+
# https://github.com/puppetlabs/showoff/blob/3f43754c84f97be4284bb34f9bc7c42175d45226/lib/showoff_utils.rb#L671-L720
|
26
|
+
# TODO: per slide profiles of render options
|
27
|
+
def profile
|
28
|
+
renderer = Showoff::Config.get('markdown')
|
29
|
+
profile = Showoff::Config.get(renderer)
|
30
|
+
|
31
|
+
begin
|
32
|
+
# Load markdown configuration
|
33
|
+
case renderer
|
34
|
+
when 'rdiscount'
|
35
|
+
Tilt.prefer Tilt::RDiscountTemplate, "markdown"
|
36
|
+
|
37
|
+
when 'maruku'
|
38
|
+
Tilt.prefer Tilt::MarukuTemplate, "markdown"
|
39
|
+
# Now check if we can go for latex mode
|
40
|
+
require 'maruku'
|
41
|
+
require 'maruku/ext/math'
|
42
|
+
|
43
|
+
if profile[:use_tex]
|
44
|
+
MaRuKu::Globals[:html_math_output_mathml] = false
|
45
|
+
MaRuKu::Globals[:html_math_output_png] = true
|
46
|
+
MaRuKu::Globals[:html_math_engine] = 'none'
|
47
|
+
MaRuKu::Globals[:html_png_engine] = 'blahtex'
|
48
|
+
MaRuKu::Globals[:html_png_dir] = profile[:png_dir]
|
49
|
+
MaRuKu::Globals[:html_png_url] = profile[:html_png_url]
|
50
|
+
end
|
51
|
+
|
52
|
+
when 'bluecloth'
|
53
|
+
Tilt.prefer Tilt::BlueClothTemplate, "markdown"
|
54
|
+
|
55
|
+
when 'kramdown'
|
56
|
+
Tilt.prefer Tilt::KramdownTemplate, "markdown"
|
57
|
+
|
58
|
+
when 'commonmarker', 'commonmark'
|
59
|
+
Tilt.prefer Tilt::CommonMarkerTemplate, "markdown"
|
60
|
+
|
61
|
+
when 'redcarpet', :default
|
62
|
+
Tilt.prefer Tilt::RedcarpetTemplate, "markdown"
|
63
|
+
|
64
|
+
else
|
65
|
+
raise 'Unsupported markdown renderer'
|
66
|
+
|
67
|
+
end
|
68
|
+
rescue LoadError
|
69
|
+
puts "ERROR: The #{renderer} markdown rendering engine does not appear to be installed correctly."
|
70
|
+
exit! 1
|
71
|
+
end
|
72
|
+
|
73
|
+
profile
|
74
|
+
end
|
75
|
+
|
76
|
+
# Compiles markdown and all Showoff extensions into the final HTML output and notes.
|
77
|
+
#
|
78
|
+
# @param content [String] markdown content.
|
79
|
+
# @return [[String, Array<String>]] A tuple of (html content, array of notes contents)
|
80
|
+
#
|
81
|
+
# @todo I think the update_image_paths() malarky is redundant. Verify that.
|
82
|
+
def render(content)
|
83
|
+
Variables::interpolate!(content)
|
84
|
+
I18n.selectLanguage!(content)
|
85
|
+
|
86
|
+
html = Tilt[:markdown].new(nil, nil, @profile) { content }.render
|
87
|
+
doc = Nokogiri::HTML::DocumentFragment.parse(html)
|
88
|
+
|
89
|
+
Form.render!(doc, @options)
|
90
|
+
Fixups.updateClasses!(doc)
|
91
|
+
Fixups.updateLinks!(doc)
|
92
|
+
Fixups.updateSyntaxHighlighting!(doc)
|
93
|
+
Fixups.updateCommandlineBlocks!(doc)
|
94
|
+
Fixups.updateImagePaths!(doc, @options)
|
95
|
+
Glossary.render!(doc)
|
96
|
+
Downloads.scanForFiles!(doc, @options)
|
97
|
+
|
98
|
+
# This call must be last in the chain because it separates notes from the
|
99
|
+
# content and returns them separately. If it's not last, then the notes
|
100
|
+
# won't have all the compilation steps applied to them.
|
101
|
+
#
|
102
|
+
# must pass in extra context because this will render markdown itself
|
103
|
+
Notes.render!(doc, @profile, @options)
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
@@ -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
|