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,164 @@
1
+ # adds glossary processing to the compiler
2
+ class Showoff::Compiler::Glossary
3
+
4
+ # Scan for glossary links and add definitions. This does not create the
5
+ # glossary page at the end.
6
+ #
7
+ # @param doc [Nokogiri::HTML::DocumentFragment]
8
+ # The slide document
9
+ #
10
+ # @return [Nokogiri::HTML::DocumentFragment]
11
+ # The slide DOM with all glossary entries rendered.
12
+ #
13
+ # @see
14
+ # https://github.com/puppetlabs/showoff/blob/3f43754c84f97be4284bb34f9bc7c42175d45226/lib/showoff.rb#L650-L706
15
+ def self.render!(doc)
16
+
17
+ # Find all callout style definitions on the slide and add links to the glossary page
18
+ doc.search('.callout.glossary').each do |item|
19
+ next unless item.content =~ /^([^|]+)\|([^:]+):(.*)$/
20
+ item['data-term'] = $1
21
+ item['data-target'] = $2
22
+ item['data-text'] = $3.strip
23
+ item.content = $3.strip
24
+
25
+ glossary = (item.attr('class').split - ['callout', 'glossary']).first
26
+ address = glossary ? "#{glossary}/#{$2}" : $2
27
+
28
+ link = Nokogiri::XML::Node.new('a', doc)
29
+ link.add_class('processed label')
30
+ link.set_attribute('href', "glossary://#{address}")
31
+ link.content = $1
32
+
33
+ item.prepend_child(link)
34
+ end
35
+
36
+ # Find glossary links and add definitions to the notes
37
+ doc.search('a').each do |link|
38
+ next unless link['href']
39
+ next unless link['href'].start_with? 'glossary://'
40
+ next if link.classes.include? 'processed'
41
+
42
+ link.add_class('term')
43
+
44
+ term = link.content
45
+ text = link['title']
46
+ href = link['href']
47
+
48
+ parts = href.split('/')
49
+ target = parts.pop
50
+ name = parts.pop # either the glossary name or nil
51
+
52
+ label = link.clone
53
+ label.add_class('label processed')
54
+
55
+ definition = Nokogiri::XML::Node.new('p', doc)
56
+ definition.add_class("callout glossary #{name}")
57
+ definition.set_attribute('data-term', term)
58
+ definition.set_attribute('data-text', text)
59
+ definition.set_attribute('data-target', target)
60
+ definition.content = text
61
+ definition.prepend_child(label)
62
+
63
+ # @todo this duplication is annoying but it makes it less order dependent
64
+ doc.add_child '<div class="notes-section notes"></div>' if doc.search('div.notes-section.notes').empty?
65
+ doc.add_child '<div class="notes-section handouts"></div>' if doc.search('div.notes-section.handouts').empty?
66
+
67
+ [doc.css('div.notes-section.notes'), doc.css('div.notes-section.handouts')].each do |section|
68
+ section.first.add_child(definition.clone)
69
+ end
70
+
71
+ end
72
+
73
+ doc
74
+ end
75
+
76
+ # Generate and add the glossary page
77
+ #
78
+ # @param doc [Nokogiri::HTML::DocumentFragment]
79
+ # The presentation document
80
+ #
81
+ # @return [Nokogiri::HTML::DocumentFragment]
82
+ # The presentation DOM with the glossary page rendered.
83
+ #
84
+ # @see
85
+ # https://github.com/puppetlabs/showoff/blob/3f43754c84f97be4284bb34f9bc7c42175d45226/lib/showoff.rb#L770-L810
86
+ def self.generatePage!(doc)
87
+ doc.search('.slide.glossary .content').each do |glossary|
88
+ name = (glossary.attr('class').split - ['content', 'glossary']).first
89
+ list = Nokogiri::XML::Node.new('ul', doc)
90
+ list.add_class('glossary terms')
91
+ seen = []
92
+
93
+ doc.search('.callout.glossary').each do |item|
94
+ target = (item.attr('class').split - ['callout', 'glossary']).first
95
+
96
+ # if the name matches or if we didn't name it to begin with.
97
+ next unless target == name
98
+
99
+ # the definition can exist in multiple places, so de-dup it here
100
+ term = item.attr('data-term')
101
+ next if seen.include? term
102
+ seen << term
103
+
104
+ # excrutiatingly find the parent slide content and grab the ref
105
+ # in a library less shitty, this would be something like
106
+ # $(this).parent().siblings('.content').attr('ref')
107
+ href = nil
108
+ item.ancestors('.slide').first.traverse do |element|
109
+ next if element['class'].nil?
110
+ next unless element['class'].split.include? 'content'
111
+
112
+ href = element.attr('ref').gsub('/', '_')
113
+ end
114
+
115
+ text = item.attr('data-text')
116
+ link = item.attr('data-target')
117
+ page = glossary.attr('ref')
118
+ anchor = "#{page}+#{link}"
119
+ next if href.nil? or text.nil? or link.nil?
120
+
121
+ entry = Nokogiri::XML::Node.new('li', doc)
122
+
123
+ label = Nokogiri::XML::Node.new('a', doc)
124
+ label.add_class('label')
125
+ label.set_attribute('id', anchor)
126
+ label.content = term
127
+
128
+ link = Nokogiri::XML::Node.new('a', doc)
129
+ label.add_class('return')
130
+ link.set_attribute('href', "##{href}")
131
+ link.content = '↩'
132
+
133
+ entry.add_child(label)
134
+ entry.add_child(Nokogiri::XML::Text.new(text, doc))
135
+ entry.add_child(link)
136
+
137
+ list.add_child(entry)
138
+ end
139
+
140
+ glossary.add_child(list)
141
+ end
142
+
143
+ # now fix all the links to point to the glossary page
144
+ doc.search('a').each do |link|
145
+ next if link['href'].nil?
146
+ next unless link['href'].start_with? 'glossary://'
147
+
148
+ href = link['href']
149
+ href.slice!('glossary://')
150
+
151
+ parts = href.split('/')
152
+ target = parts.pop
153
+ name = parts.pop # either the glossary name or nil
154
+
155
+ classes = name.nil? ? ".slide.glossary" : ".slide.glossary.#{name}"
156
+ href = doc.at("#{classes} .content").attr('ref') rescue nil
157
+
158
+ link['href'] = "##{href}+#{target}"
159
+ end
160
+
161
+ doc
162
+ end
163
+
164
+ end
@@ -0,0 +1,24 @@
1
+ # Adds slide language selection to the compiler
2
+ class Showoff::Compiler::I18n
3
+
4
+ def self.selectLanguage!(content)
5
+ translations = {}
6
+ content.scan(/^((~~~LANG:([\w-]+)~~~\n)(.+?)(\n~~~ENDLANG~~~))/m).each do |match|
7
+ markup, opentag, code, text, closetag = match
8
+ translations[code] = {:markup => markup, :content => text}
9
+ end
10
+
11
+ lang = Showoff::Locale.resolve(translations.keys).to_s
12
+
13
+ translations.each do |code, translation|
14
+ if code == lang
15
+ content.sub!(translation[:markup], translation[:content])
16
+ else
17
+ content.sub!(translation[:markup], "\n")
18
+ end
19
+ end
20
+
21
+ content
22
+ end
23
+
24
+ end
@@ -0,0 +1,73 @@
1
+ # adds presenter notes processing to the compiler
2
+ class Showoff::Compiler::Notes
3
+
4
+ # Generate the presenter notes sections, including personal notes
5
+ #
6
+ # @param doc [Nokogiri::HTML::DocumentFragment]
7
+ # The slide document
8
+ #
9
+ # @param profile [String]
10
+ # The markdown engine profile to use when rendering
11
+ #
12
+ # @param options [Hash] Options used for rendering any embedded markdown
13
+ # @option options [String] :name The markdown slide name
14
+ # @option options [String] :seq The sequence number for multiple slides in one file
15
+ #
16
+ # @return [Nokogiri::HTML::DocumentFragment]
17
+ # The slide DOM with all notes sections rendered.
18
+ #
19
+ # @see
20
+ # https://github.com/puppetlabs/showoff/blob/3f43754c84f97be4284bb34f9bc7c42175d45226/lib/showoff.rb#L616-L716
21
+ # @note
22
+ # A ton of the functionality in the original method got refactored to its logical location
23
+ def self.render!(doc, profile, options = {})
24
+ # Turn tags into classed divs.
25
+ doc.search('p').select {|p| p.text.start_with?('~~~SECTION:') }.each do |p|
26
+ klass = p.text.match(/~~~SECTION:([^~]*)~~~/)[1]
27
+
28
+ # Don't bother creating this if we don't want to use it
29
+ next unless Showoff::Config.includeNotes?(klass)
30
+
31
+ notes = Nokogiri::XML::Node.new('div', doc)
32
+ notes.add_class("notes-section #{klass}")
33
+ nodes = []
34
+ iter = p.next_sibling
35
+ until iter.text == '~~~ENDSECTION~~~' do
36
+ nodes << iter
37
+ iter = iter.next_sibling
38
+
39
+ # if the author forgot the closing tag, let's not crash, eh?
40
+ break unless iter
41
+ end
42
+ iter.remove if iter # remove the extraneous closing ~~~ENDSECTION~~~ tag
43
+
44
+ # We need to collect the list before moving or the iteration crashes since the iterator no longer has a sibling
45
+ nodes.each {|n| n.parent = notes }
46
+
47
+ p.replace(notes)
48
+ end
49
+
50
+ filename = [
51
+ File.join(Showoff::Config.root, '_notes', "#{options[:name]}.#{options[:seq]}.md"),
52
+ File.join(Showoff::Config.root, '_notes', "#{options[:name]}.md"),
53
+ ].find {|path| File.file?(path) }
54
+
55
+ if filename and Showoff::Config.includeNotes?('notes')
56
+ # Make sure we've got a notes div to hang personal notes from
57
+ doc.add_child '<div class="notes-section notes"></div>' if doc.search('div.notes-section.notes').empty?
58
+ doc.search('div.notes-section.notes').each do |section|
59
+ text = Tilt[:markdown].new(nil, nil, options[:profile]) { File.read(filename) }.render
60
+ frag = "<div class=\"personal\"><h1>#{I18n.t('presenter.notes.personal')}</h1>#{text}</div>"
61
+ section.prepend_child(frag)
62
+ end
63
+ end
64
+
65
+ # return notes separately from content so that it can be rendered outside the slide
66
+ # @see https://github.com/puppetlabs/showoff/blob/3f43754c84f97be4284bb34f9bc7c42175d45226/lib/showoff.rb#L726-L732
67
+ notes = doc.search('div.notes-section')
68
+ doc.search('div.notes-section').each {|n| n.remove }
69
+
70
+ [doc, notes]
71
+ end
72
+
73
+ end
@@ -0,0 +1,51 @@
1
+ # adds table of content generation to the compiler
2
+ class Showoff::Compiler::TableOfContents
3
+
4
+ # Render a table of contents
5
+ #
6
+ # @param doc [Nokogiri::HTML::DocumentFragment]
7
+ # The presentation document
8
+ #
9
+ # @return [Nokogiri::HTML::DocumentFragment]
10
+ # The presentation DOM with the table of contents rendered.
11
+ #
12
+ # @see
13
+ # https://github.com/puppetlabs/showoff/blob/3f43754c84f97be4284bb34f9bc7c42175d45226/lib/showoff.rb#L747-L768
14
+ def self.generate!(doc)
15
+ container = doc.search('p').find {|p| p.text == '~~~TOC~~~' }
16
+ return doc unless container
17
+
18
+ section = nil
19
+ toc = Nokogiri::XML::Node.new('ol', doc)
20
+ toc.set_attribute('id', 'toc')
21
+
22
+ doc.search('div.slide:not(.toc)').each do |slide|
23
+ next if slide.search('.content').first.classes.include? 'cover'
24
+
25
+ heads = slide.search('div.content h1:not(.section_title)')
26
+ title = heads.empty? ? slide['data-title'] : heads.first.text
27
+ href = "##{slide['id']}"
28
+
29
+ entry = Nokogiri::XML::Node.new('li', doc)
30
+ entry.add_class('tocentry')
31
+ link = Nokogiri::XML::Node.new('a', doc)
32
+ link.set_attribute('href', href)
33
+ link.content = title
34
+ entry.add_child(link)
35
+
36
+ if (section and slide['data-section'] == section['data-section'])
37
+ section.add_child(entry)
38
+ else
39
+ section = Nokogiri::XML::Node.new('ol', doc)
40
+ section.add_class('major')
41
+ section.set_attribute('data-section', slide['data-section'])
42
+ entry.add_child(section)
43
+ toc.add_child(entry)
44
+ end
45
+
46
+ end
47
+ container.replace(toc)
48
+
49
+ doc
50
+ end
51
+ end
@@ -0,0 +1,71 @@
1
+ # Adds variable interpolation to the compiler
2
+ class Showoff::Compiler::Variables
3
+ #
4
+ #
5
+ # @param content [String]
6
+ # A string of Markdown content which may contain Showoff variables.
7
+ # @return [String]
8
+ # The content with variables interpolated.
9
+ # @note
10
+ # Had side effects of altering state datastore.
11
+ # @see
12
+ # https://github.com/puppetlabs/showoff/blob/3f43754c84f97be4284bb34f9bc7c42175d45226/lib/showoff.rb#L557-L614
13
+ def self.interpolate!(content)
14
+ # update counters, incrementing section:minor if needed
15
+ content.gsub!("~~~CURRENT_SLIDE~~~", Showoff::State.get(:slide_count).to_s)
16
+ content.gsub!("~~~SECTION:MAJOR~~~", Showoff::State.get(:section_major).to_s)
17
+ if content.include? "~~~SECTION:MINOR~~~"
18
+ Showoff::State.increment(:section_minor)
19
+ content.gsub!("~~~SECTION:MINOR~~~", Showoff::State.get(:section_minor).to_s)
20
+ end
21
+
22
+ # scan for pagebreak tags. Should really only be used for handout notes or supplemental materials
23
+ content.gsub!("~~~PAGEBREAK~~~", '<div class="pagebreak">continued...</div>')
24
+
25
+ # replace with form rendering placeholder
26
+ content.gsub!(/~~~FORM:([^~]*)~~~/, '<div class="form wrapper" title="\1"></div>')
27
+
28
+ # Now check for any kind of options
29
+ content.scan(/(~~~CONFIG:(.*?)~~~)/).each do |match|
30
+ parts = match[1].split('.') # Use dots ('.') to separate Hash keys
31
+ value = Showoff::Config.get(*parts)
32
+
33
+ unless value.is_a?(String)
34
+ msg = "#{match[0]} refers to a non-String data type (#{value.class})"
35
+ msg = "#{match[0]}: not found in settings data" if value.nil?
36
+ Showoff::Logger.warn(msg)
37
+ next
38
+ end
39
+
40
+ content.gsub!(match[0], value)
41
+ end
42
+
43
+ # Load and replace any file tags
44
+ content.scan(/(~~~FILE:([^:~]*):?(.*)?~~~)/).each do |match|
45
+ # make a list of code highlighting classes to include
46
+ css = match[2].split.collect {|i| "language-#{i.downcase}" }.join(' ')
47
+
48
+ # get the file content and parse out html entities
49
+ name = match[1]
50
+ file = File.read(File.join(Showoff::Config.root, '_files', name)) rescue "Nonexistent file: #{name}"
51
+ file = "Empty file: #{name}" if file.empty?
52
+ file = HTMLEntities.new.encode(file) rescue "HTML encoding of #{name} failed"
53
+
54
+ content.gsub!(match[0], "<pre class=\"highlight\"><code class=\"#{css}\">#{file}</code></pre>")
55
+ end
56
+
57
+ # insert font awesome icons
58
+ content.gsub!(/\[(fa\w?)-(\S*) ?(.*)\]/, '<i class="\1 fa-\2 \3"></i>')
59
+
60
+ # For fenced code blocks, translate the space separated classes into one
61
+ # colon separated string so Commonmarker doesn't ignore the rest
62
+ content.gsub!(/^`{3} *(.+)$/) {|s| "``` #{$1.split.join(':')}"}
63
+
64
+ # escape any tags left and ensure they're in distinctly separate p tags so
65
+ # that renderers that accept a string of tildes for fenced code blocks don't blow up.
66
+ # @todo This is terrible and we need to design a better tag syntax.
67
+ content.gsub!(/^~~~(.*?)~~~/, "\n\\~~~\\1~~~\n")
68
+
69
+ content
70
+ end
71
+ end
@@ -0,0 +1,218 @@
1
+ require 'json'
2
+
3
+ class Showoff::Config
4
+
5
+ def self.keys
6
+ @@config.keys
7
+ end
8
+
9
+ # Retrieve settings from the config hash.
10
+ # If multiple arguments are given then it will dig down through data
11
+ # structures argument by argument.
12
+ #
13
+ # Returns the data type & value requested, nil on error.
14
+ def self.get(*setting)
15
+ @@config.dig(*setting) rescue nil
16
+ end
17
+
18
+ def self.sections
19
+ @@sections
20
+ end
21
+
22
+ # Absolute root of presentation
23
+ def self.root
24
+ @@root
25
+ end
26
+
27
+ # Relative path to an item in the presentation directory structure
28
+ def self.path(path)
29
+ File.expand_path(File.join(@@root, path)).sub(/^#{@@root}\//, '')
30
+ end
31
+
32
+ # Identifies whether we're including a given notes section
33
+ #
34
+ # @param section [String] The name of the notes section of interest.
35
+ # @return [Boolean] Whether to include this section in the output
36
+ def self.includeNotes?(section)
37
+ return true # todo make this work
38
+ end
39
+
40
+ def self.load(path = 'showoff.json')
41
+ raise 'Presentation file does not exist at the specified path' unless File.exist? path
42
+
43
+ @@root = File.dirname(path)
44
+ @@config = JSON.parse(File.read(path))
45
+ @@sections = self.expand_sections
46
+
47
+ self.load_defaults!
48
+ end
49
+
50
+ # Expand and normalize all the different variations that the sections structure
51
+ # can exist in. When finished, this should return an ordered hash of one or more
52
+ # section titles pointing to an array of filenames, for example:
53
+ #
54
+ # {
55
+ # "Section name": [ "array.md, "of.md, "files.md"],
56
+ # "Another Section": [ "two/array.md, "two/of.md, "two/files.md"],
57
+ # }
58
+ #
59
+ # See valid input forms at
60
+ # https://puppetlabs.github.io/showoff/documentation/PRESENTATION_rdoc.html#label-Defining+slides+using+the+sections+setting.
61
+ # Source:
62
+ # https://github.com/puppetlabs/showoff/blob/3f43754c84f97be4284bb34f9bc7c42175d45226/lib/showoff_utils.rb#L427-L475
63
+ def self.expand_sections
64
+ begin
65
+ if @@config.is_a?(Hash)
66
+ # dup so we don't overwrite the original data structure and make it impossible to re-localize
67
+ sections = @@config['sections'].dup
68
+ else
69
+ sections = @@config.dup
70
+ end
71
+
72
+ if sections.is_a? Array
73
+ sections = self.legacy_sections(sections)
74
+ elsif sections.is_a? Hash
75
+ raise "Named sections are unsupported on Ruby versions less than 1.9." if RUBY_VERSION.start_with? '1.8'
76
+ sections.each do |key, value|
77
+ next if value.is_a? Array
78
+ path = File.dirname(value)
79
+ data = JSON.parse(File.read(File.join(@@root, value)))
80
+ raise "The section file #{value} must contain an array of filenames." unless data.is_a? Array
81
+
82
+ # get relative paths to each slide in the array
83
+ sections[key] = data.map do |filename|
84
+ Pathname.new("#{path}/#{filename}").cleanpath.to_path
85
+ end
86
+ end
87
+ else
88
+ raise "The `sections` key must be an Array or Hash, not a #{sections.class}."
89
+ end
90
+
91
+ rescue => e
92
+ Showoff::Logger.error "There was a problem with the presentation file #{index}"
93
+ Showoff::Logger.error e.message
94
+ Showoff::Logger.debug e.backtrace
95
+ sections = {}
96
+ end
97
+
98
+ sections
99
+ end
100
+
101
+ # Source:
102
+ # https://github.com/puppetlabs/showoff/blob/3f43754c84f97be4284bb34f9bc7c42175d45226/lib/showoff_utils.rb#L477-L545
103
+ def self.legacy_sections(data)
104
+ # each entry in sections can be:
105
+ # - "filename.md"
106
+ # - "directory"
107
+ # - { "section": "filename.md" }
108
+ # - { "section": "directory" }
109
+ # - { "section": [ "array.md, "of.md, "files.md"] }
110
+ # - { "include": "sections.json" }
111
+ sections = {}
112
+ counters = {}
113
+ lastpath = nil
114
+
115
+ data.map do |entry|
116
+ next entry if entry.is_a? String
117
+ next nil unless entry.is_a? Hash
118
+ next entry['section'] if entry.include? 'section'
119
+
120
+ section = nil
121
+ if entry.include? 'include'
122
+ file = entry['include']
123
+ path = File.dirname(file)
124
+ data = JSON.parse(File.read(File.join(@@root, file)))
125
+ if data.is_a? Array
126
+ if path == '.'
127
+ section = data
128
+ else
129
+ section = data.map do |source|
130
+ "#{path}/#{source}"
131
+ end
132
+ end
133
+ end
134
+ end
135
+ section
136
+ end.flatten.compact.each do |entry|
137
+ # We do this in two passes simply because most of it was already done
138
+ # and I don't want to waste time on legacy functionality.
139
+
140
+ # Normalize to a proper path from presentation root
141
+ if File.directory? File.join(@@root, entry)
142
+ sections[entry] = Dir.glob("#{@@root}/#{entry}/**/*.md").map {|e| e.sub(/^#{@@root}\//, '') }
143
+ lastpath = entry
144
+ else
145
+ path = File.dirname(entry)
146
+
147
+ # this lastpath business allows us to reference files in a directory that aren't
148
+ # necessarily contiguous.
149
+ if path != lastpath
150
+ counters[path] ||= 0
151
+ counters[path] += 1
152
+ end
153
+
154
+ # now record the last path we've seen
155
+ lastpath = path
156
+
157
+ # and if there are more than one disparate occurences of path, add a counter to this string
158
+ path = "#{path} (#{counters[path]})" unless counters[path] == 1
159
+
160
+ sections[path] ||= []
161
+ sections[path] << entry
162
+ end
163
+ end
164
+
165
+ sections
166
+ end
167
+
168
+ def self.load_defaults!
169
+ # use a symbol which cannot clash with a string key loaded from json
170
+ @@config['markdown'] ||= :default
171
+ renderer = @@config['markdown']
172
+ defaults = case renderer
173
+ when 'rdiscount'
174
+ {
175
+ :autolink => true,
176
+ }
177
+ when 'maruku'
178
+ {
179
+ :use_tex => false,
180
+ :png_dir => 'images',
181
+ :html_png_url => '/file/images/',
182
+ }
183
+ when 'bluecloth'
184
+ {
185
+ :auto_links => true,
186
+ :definition_lists => true,
187
+ :superscript => true,
188
+ :tables => true,
189
+ }
190
+ when 'kramdown'
191
+ {}
192
+ else
193
+ {
194
+ :autolink => true,
195
+ :no_intra_emphasis => true,
196
+ :superscript => true,
197
+ :tables => true,
198
+ :underline => true,
199
+ :escape_html => false,
200
+ }
201
+ end
202
+
203
+ @@config[renderer] ||= {}
204
+ @@config[renderer] = defaults.merge!(@@config[renderer])
205
+
206
+ # run `wkhtmltopdf --extended-help` for a full list of valid options here
207
+ pdf_defaults = {
208
+ :page_size => 'Letter',
209
+ :orientation => 'Portrait',
210
+ :print_media_type => true,
211
+ :quiet => false}
212
+ pdf_options = @@config['pdf_options'] || {}
213
+ pdf_options = Hash[pdf_options.map {|k, v| [k.to_sym, v]}]
214
+
215
+ @@config['pdf_options'] = pdf_defaults.merge!(pdf_options)
216
+ end
217
+
218
+ end