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