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,132 @@
1
+ require 'i18n'
2
+ require 'i18n/backend/fallbacks'
3
+ require 'iso-639'
4
+
5
+ I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
6
+ I18n.load_path += Dir[File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'locales', '*.yml'))]
7
+ I18n.backend.load_translations
8
+ I18n.enforce_available_locales = false
9
+
10
+ class Showoff::Locale
11
+ @@contentLocale = nil
12
+
13
+ # Set the minimized canonical version of the specified content locale, selecting
14
+ # the nearest match to whatever exists in the presentation's locales directory.
15
+ # If the locale doesn't exist on disk, it will just default to no translation
16
+ #
17
+ # @todo: I don't think this is right at all -- it doesn't autoselect content
18
+ # languages, just built in Showoff languages. It only worked by accident before
19
+ #
20
+ # @param user_locale [String, Symbol] The locale to select.
21
+ #
22
+ # @returns [Symbol] The selected and saved locale.
23
+ def self.setContentLocale(user_locale = nil)
24
+ if [nil, '', 'auto'].include? user_locale
25
+ languages = I18n.available_locales
26
+ @@contentLocale = I18n.fallbacks[I18n.locale].select { |f| languages.include? f }.first
27
+ else
28
+ locales = Dir.glob("#{Showoff::Config.root}/locales/*").map {|e| File.basename e }
29
+ locales.delete 'strings.json'
30
+
31
+ @@contentLocale = with_locale(user_locale) do |str|
32
+ str.to_sym if locales.include? str
33
+ end
34
+ end
35
+ end
36
+
37
+ def self.contentLocale
38
+ @@contentLocale
39
+ end
40
+
41
+ # Find the closest match to current locale in an array of possibilities
42
+ #
43
+ # @param items [Array] An array of possibilities to check
44
+ # @return [Symbol] The closest match to the current locale.
45
+ def self.resolve(items)
46
+ with_locale(contentLocale) do |str|
47
+ str.to_sym if items.include? str
48
+ end
49
+ end
50
+
51
+ # Turns a locale code into a string name
52
+ #
53
+ # @param locale [String, Symbol] The code of the locale to translate
54
+ # @returns [String] The name of the locale.
55
+ def self.languageName(locale = contentLocale)
56
+ with_locale(locale) do |str|
57
+ result = ISO_639.find(str)
58
+ result[3] unless result.nil?
59
+ end
60
+ end
61
+
62
+ # This function returns the directory containing translated *content*, defaulting
63
+ # to the presentation root. This works similarly to I18n fallback, but we cannot
64
+ # reuse that as it's a different translation mechanism.
65
+
66
+ # @returns [String] Path to the translated content.
67
+ def self.contentPath
68
+ root = Showoff::Config.root
69
+
70
+ with_locale(contentLocale) do |str|
71
+ path = "#{root}/locales/#{str}"
72
+ return path if File.directory?(path)
73
+ end || root
74
+ end
75
+
76
+ # Generates a hash of all language codes available and the long name description of each
77
+ #
78
+ # @returns [Hash] The language code/name hash.
79
+ def self.contentLanguages
80
+ root = Showoff::Config.root
81
+
82
+ strings = JSON.parse(File.read("#{root}/locales/strings.json")) rescue {}
83
+ locales = Dir.glob("#{root}/locales/*")
84
+ .select {|f| File.directory?(f) }
85
+ .map {|f| File.basename(f) }
86
+
87
+ (strings.keys + locales).inject({}) do |memo, locale|
88
+ memo.update(locale => languageName(locale))
89
+ end
90
+ end
91
+
92
+
93
+ # Generates a hash of all translations for the current language. This is used
94
+ # for the javascript half of the UI translations
95
+ #
96
+ # @returns [Hash] The locale code/strings hash.
97
+ def self.translations
98
+ languages = I18n.backend.send(:translations)
99
+ fallback = I18n.fallbacks[I18n.locale].select { |f| languages.keys.include? f }.first
100
+ languages[fallback]
101
+ end
102
+
103
+ # Finds the language key from strings.json and returns the strings hash. This is
104
+ # used for user translations in the presentation content, e.g. SVG translations.
105
+ #
106
+ # @returns [Hash] The user translation code/strings hash.
107
+ def self.userTranslations
108
+ path = "#{Showoff::Config.root}/locales/strings.json"
109
+ return {} unless File.file? path
110
+ strings = JSON.parse(File.read(path)) rescue {}
111
+
112
+ with_locale(contentLocale) do |key|
113
+ return strings[key] if strings.include? key
114
+ end
115
+ {}
116
+ end
117
+
118
+ # This is just a unified lookup method that takes a full locale name
119
+ # and then resolves it to an available version of the name
120
+ def self.with_locale(locale)
121
+ locale = locale.to_s
122
+ until (locale.empty?) do
123
+ result = yield(locale)
124
+ return result unless result.nil?
125
+
126
+ # if not found, chop off a section and try again
127
+ locale = locale.rpartition(/[-_]/).first
128
+ end
129
+ end
130
+ private_class_method :with_locale
131
+
132
+ end
@@ -0,0 +1,15 @@
1
+ require 'logger'
2
+ class Showoff::Logger
3
+ @@logger = Logger.new(STDERR)
4
+ @@logger.progname = 'Showoff'
5
+ @@logger.formatter = proc { |severity,datetime,progname,msg| "(#{progname}) #{severity}: #{msg}\n" }
6
+ @@logger.level = Showoff::State.get(:verbose) ? Logger::DEBUG : Logger::WARN
7
+ @@logger.level = Logger::WARN
8
+
9
+ [:debug, :info, :warn, :error, :fatal].each do |meth|
10
+ define_singleton_method(meth) do |msg|
11
+ @@logger.send(meth, msg)
12
+ end
13
+ end
14
+
15
+ end
@@ -0,0 +1,28 @@
1
+ # Yay for Ruby 2.0!
2
+ class Hash
3
+ unless Hash.method_defined? :dig
4
+ def dig(*args)
5
+ args.reduce(self) do |iter, arg|
6
+ break nil unless iter.is_a? Enumerable
7
+ break nil unless iter.include? arg
8
+ iter[arg]
9
+ end
10
+ end
11
+ end
12
+
13
+ end
14
+
15
+ class Nokogiri::XML::Element
16
+ unless Nokogiri::XML::Element.method_defined? :add_class
17
+ def add_class(classlist)
18
+ self[:class] = [self[:class], classlist].join(' ')
19
+ end
20
+ end
21
+
22
+ unless Nokogiri::XML::Element.method_defined? :classes
23
+ def classes
24
+ self[:class] ? self[:class].split(' ') : []
25
+ end
26
+ end
27
+
28
+ end
@@ -0,0 +1,181 @@
1
+ class Showoff::Presentation
2
+ require 'showoff/presentation/section'
3
+ require 'showoff/presentation/slide'
4
+ require 'showoff/compiler'
5
+ require 'keymap'
6
+
7
+ attr_reader :sections
8
+
9
+ def initialize(options)
10
+ @options = options
11
+ @sections = Showoff::Config.sections.map do |name, files|
12
+ Showoff::Presentation::Section.new(name, files)
13
+ end
14
+
15
+ # weird magic variables the presentation expects
16
+ @baseurl = nil # this doesn't appear to have ever been used
17
+ @title = Showoff::Config.get('name') || I18n.t('name')
18
+ @favicon = Showoff::Config.get('favicon') || 'favicon.ico'
19
+ @feedback = Showoff::Config.get('feedback') # note: the params check is obsolete
20
+ @pause_msg = Showoff::Config.get('pause_msg')
21
+ @language = Showoff::Locale.translations
22
+ @edit = Showoff::Config.get('edit') if options[:review]
23
+
24
+ # invert the logic to maintain backwards compatibility of interactivity on by default
25
+ @interactive = ! options[:standalone]
26
+
27
+ # Load up the default keymap, then merge in any customizations
28
+ keymapfile = File.expand_path(File.join('~', '.showoff', 'keymap.json'))
29
+ @keymap = Keymap.default
30
+ @keymap.merge! JSON.parse(File.read(keymapfile)) rescue {}
31
+
32
+ # map keys to the labels we're using
33
+ @keycode_dictionary = Keymap.keycodeDictionary
34
+ @keycode_shifted_keys = Keymap.shiftedKeyDictionary
35
+
36
+ @highlightStyle = Showoff::Config.get('highlight') || 'default'
37
+
38
+ if Showoff::State.get(:supplemental)
39
+ @wrapper_classes = ['supplemental']
40
+ end
41
+ end
42
+
43
+ def compile
44
+ Showoff::State.reset([:slide_count, :section_major, :section_minor])
45
+
46
+ # @todo For now, we reparse the html so that we can generate content via slide
47
+ # templates. This adds a bit of extra time, but not too much. Perhaps
48
+ # we'll change that at some point.
49
+ html = @sections.map(&:render).join("\n")
50
+ doc = Nokogiri::HTML::DocumentFragment.parse(html)
51
+
52
+ Showoff::Compiler::TableOfContents.generate!(doc)
53
+ Showoff::Compiler::Glossary.generatePage!(doc)
54
+
55
+ doc
56
+ end
57
+
58
+ # The index page does not contain content; just a placeholder div that's
59
+ # dynamically loaded after the page is displayed. This increases perceived
60
+ # responsiveness.
61
+ def index
62
+ ERB.new(File.read(File.join(Showoff::GEMROOT, 'views','index.erb')), nil, '-').result(binding)
63
+ end
64
+
65
+ def slides
66
+ compile.to_html
67
+ end
68
+
69
+ def static
70
+ # This singleton guard removes ordering coupling between assets() & static()
71
+ @doc ||= compile
72
+ @slides = @doc.to_html
73
+
74
+ # All static snapshots should be non-interactive by definition
75
+ @interactive = false
76
+
77
+ case Showoff::State.get(:format)
78
+ when 'web'
79
+ template = 'index.erb'
80
+ when 'print', 'supplemental', 'pdf'
81
+ template = 'onepage.erb'
82
+ end
83
+
84
+ ERB.new(File.read(File.join(Showoff::GEMROOT, 'views', template)), nil, '-').result(binding)
85
+ end
86
+
87
+ # Generates a list of all image/font/etc files used by the presentation. This
88
+ # will only identify the sources of <img> tags and files referenced by the
89
+ # CSS url() function.
90
+ #
91
+ # @see
92
+ # https://github.com/puppetlabs/showoff/blob/220d6eef4c5942eda625dd6edc5370c7490eced7/lib/showoff.rb#L1509-L1573
93
+ # @returns [Array]
94
+ # List of assets, such as images or fonts, used by the presentation.
95
+ def assets
96
+ # This singleton guard removes ordering coupling between assets() & static()
97
+ @doc ||= compile
98
+
99
+ # matches url(<path>) and returns the path as a capture group
100
+ urlsrc = /url\([\"\']?(.*?)(?:[#\?].*)?[\"\']?\)/
101
+
102
+ # get all image and url() sources
103
+ files = @doc.search('img').map {|img| img[:src] }
104
+ @doc.search('*').each do |node|
105
+ next unless node[:style]
106
+ next unless matches = node[:style].match(urlsrc)
107
+ files << matches[1]
108
+ end
109
+
110
+ # add in images from css files too
111
+ css_files.each do |css_path|
112
+ data = File.read(File.join(Showoff::Config.root, css_path))
113
+
114
+ # @todo: This isn't perfect. It will match commented out styles. But its
115
+ # worst case behavior is displaying a warning message, so that's ok for now.
116
+ data.scan(urlsrc).flatten.each do |path|
117
+ # resolve relative paths in the stylesheet
118
+ path = File.join(File.dirname(css_path), path) unless path.start_with? '/'
119
+ files << path
120
+ end
121
+ end
122
+
123
+ # also all user-defined styles and javascript files
124
+ files.concat css_files
125
+ files.concat js_files
126
+ files.uniq
127
+ end
128
+
129
+ def erb(template)
130
+ ERB.new(File.read(File.join(Showoff::GEMROOT, 'views', "#{template}.erb")), nil, '-').result(binding)
131
+ end
132
+
133
+ def css_files
134
+ base = Dir.glob("#{Showoff::Config.root}/*.css").map { |path| File.basename(path) }
135
+ extra = Array(Showoff::Config.get('styles'))
136
+ base + extra
137
+ end
138
+
139
+ def js_files
140
+ base = Dir.glob("#{Showoff::Config.root}/*.js").map { |path| File.basename(path) }
141
+ extra = Array(Showoff::Config.get('scripts'))
142
+ base + extra
143
+ end
144
+
145
+ # return a list of keys associated with a given action in the keymap
146
+ def mapped_keys(action, klass='key')
147
+ list = @keymap.select { |key,value| value == action }.keys
148
+
149
+ if klass
150
+ list.map { |val| "<span class=\"#{klass}\">#{val}</span>" }.join
151
+ else
152
+ list.join ', '
153
+ end
154
+ end
155
+
156
+
157
+
158
+
159
+ # @todo: backwards compatibility shim
160
+ def user_translations
161
+ Showoff::Locale.userTranslations
162
+ end
163
+
164
+ # @todo: backwards compatibility shim
165
+ def language_names
166
+ Showoff::Locale.contentLanguages
167
+ end
168
+
169
+
170
+ # @todo: this should be part of the server. Move there with the least disruption.
171
+ def master_presenter?
172
+ false
173
+ end
174
+
175
+ # @todo: this should be part of the server. Move there with the least disruption.
176
+ def valid_presenter_cookie?
177
+ false
178
+ end
179
+
180
+
181
+ end
@@ -0,0 +1,70 @@
1
+ class Showoff::Presentation::Section
2
+ attr_reader :slides, :name
3
+
4
+ def initialize(name, files)
5
+ @name = name
6
+ @slides = []
7
+ files.each { |filename| loadSlides(filename) }
8
+
9
+ # merged output means that we just want to generate *everything*. This is used by internal,
10
+ # methods such as content validation, where we want all content represented.
11
+ # https://github.com/puppetlabs/showoff/blob/220d6eef4c5942eda625dd6edc5370c7490eced7/lib/showoff.rb#L429-L453
12
+ unless Showoff::State.get(:merged)
13
+ if Showoff::State.get(:supplemental)
14
+ # if we're looking for supplemental material, only include the content we want
15
+ @slides.select! {|slide| slide.classes.include? 'supplemental' }
16
+ @slides.select! {|slide| slide.classes.include? Showoff::State.get(:supplemental) }
17
+ else
18
+ # otherwise just skip all supplemental material completely
19
+ @slides.reject! {|slide| slide.classes.include? 'supplemental' }
20
+ end
21
+
22
+ case Showoff::State.get(:format)
23
+ when 'web'
24
+ @slides.reject! {|slide| slide.classes.include? 'toc' }
25
+ @slides.reject! {|slide| slide.classes.include? 'printonly' }
26
+ when 'print', 'supplemental'
27
+ @slides.reject! {|slide| slide.classes.include? 'noprint' }
28
+ end
29
+ end
30
+
31
+ end
32
+
33
+ def render
34
+ @slides.map(&:render).join("\n")
35
+ end
36
+
37
+ # Gets the raw file content from disk and partitions it by slide markers into
38
+ # content for each slide.
39
+ #
40
+ # Returns an array of Slide objects
41
+ #
42
+ # Source:
43
+ # https://github.com/puppetlabs/showoff/blob/3f43754c84f97be4284bb34f9bc7c42175d45226/lib/showoff.rb#L396-L414
44
+ def loadSlides(filename)
45
+ return unless filename.end_with? '.md'
46
+
47
+ content = File.read(File.join(Showoff::Locale.contentPath, filename))
48
+
49
+ # if there are no !SLIDE markers, then make every H1 define a new slide
50
+ unless content =~ /^\<?!SLIDE/m
51
+ content = content.gsub(/^# /m, "<!SLIDE>\n# ")
52
+ end
53
+
54
+ slides = content.split(/^<?!SLIDE\s?([^>]*)>?/)
55
+ slides.shift # has an extra empty string because the regex matches the entire source string.
56
+
57
+ # this is a counter keeping track of how many slides came from the file.
58
+ # It kicks in at 2 because at this point, slides are a tuple of (options, content)
59
+ seq = slides.size > 2 ? 1 : nil
60
+
61
+ # iterate each slide tuple and add slide objects to the array
62
+ slides.each_slice(2) do |data|
63
+ options, content = data
64
+ @slides << Showoff::Presentation::Slide.new(options, content, :section => @name, :name => filename, :seq => seq)
65
+ seq +=1 if seq
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -0,0 +1,113 @@
1
+ require 'erb'
2
+
3
+ class Showoff::Presentation::Slide
4
+ attr_reader :section, :section_title, :name, :seq, :id, :ref, :background, :transition, :form, :markdown, :classes
5
+
6
+ def initialize(options, content, context={})
7
+ @markdown = content
8
+ @transition = 'none'
9
+ @classes = []
10
+ setOptions!(options)
11
+ setContext!(context)
12
+ end
13
+
14
+ def render
15
+ Showoff::State.increment(:slide_count)
16
+ options = { :form => @form,
17
+ :name => @name,
18
+ :seq => @seq,
19
+ }
20
+
21
+ content, notes = Showoff::Compiler.new(options).render(@markdown)
22
+
23
+ # if a template file has been specified for this slide, load from disk and render it
24
+ # @todo How many people are actually using these limited templates?!
25
+ if tpl_file = Showoff::Config.get('template', @template)
26
+ template = File.read(tpl_file)
27
+ content = template.gsub(/~~~CONTENT~~~/, content)
28
+ end
29
+
30
+ ERB.new(File.read(File.join(Showoff::GEMROOT, 'views','slide.erb')), nil, '-').result(binding)
31
+ end
32
+
33
+ # This is a list of classes that we want applied *only* to content, and not to the slide,
34
+ # typically so that overly aggressive selectors don't match more than they should.
35
+ #
36
+ # @see
37
+ # https://github.com/puppetlabs/showoff/blob/3f43754c84f97be4284bb34f9bc7c42175d45226/lib/showoff.rb#L734-L737
38
+ def slideClasses
39
+ blacklist = ['bigtext']
40
+ @classes.reject { |klass| blacklist.include? klass }
41
+ end
42
+
43
+ # options are key=value elements within the [] brackets
44
+ def setOptions!(options)
45
+ return unless options
46
+ return unless matches = options.match(/(\[(.*?)\])?(.*)/)
47
+
48
+ if matches[2]
49
+ matches[2].split(",").each do |element|
50
+ key, val = element.split("=")
51
+ case key
52
+ when 'tpl', 'template'
53
+ @template = val
54
+ when 'bg', 'background'
55
+ @background = val
56
+ # For legacy reasons, the options below may also be specified in classes.
57
+ # Currently that takes priority.
58
+ # @todo: better define the difference between options and classes.
59
+ when 'form'
60
+ @form = val
61
+ when 'id'
62
+ @id = val
63
+ when 'transition'
64
+ @transition = val
65
+ else
66
+ Showoff::Logger.warn "Unknown slide option: #{key}=#{val}"
67
+ end
68
+ end
69
+ end
70
+
71
+ if matches[3]
72
+ @classes = matches[3].split
73
+ end
74
+ end
75
+
76
+ # currently a mishmash of passed in context and calculated valued extracted from classes
77
+ def setContext!(context)
78
+ @section = context[:section] || 'main'
79
+ @name = context[:name].chomp('.md')
80
+ @seq = context[:seq]
81
+
82
+ #TODO: this should be in options
83
+ # extract id from classes if set, or default to the HTML sanitized name
84
+ @classes.delete_if { |x| x =~ /^#([\w-]+)/ && @id = $1 }
85
+ @id ||= @name.dup.gsub(/[^-A-Za-z0-9_]/, '_')
86
+ @id << seq.to_s if @seq
87
+
88
+ # provide an href for the slide. If we've got multiple slides in this file, we'll have a sequence number
89
+ # include that sequence number to index directly into that content
90
+ @ref = @seq ? "#{@name}:#{@seq.to_s}" : @name
91
+
92
+ #TODO: this should be in options
93
+ # extract transition from classes, or default to 'none'
94
+ @classes.delete_if { |x| x =~ /^transition=(.+)/ && @transition = $1 }
95
+
96
+ #TODO: this should be in options
97
+ # extract form id from classes, or default to nil
98
+ @classes.delete_if { |x| x =~ /^form=(.+)/ && @form = $1 }
99
+
100
+ # Extract a section title from subsection slides and add it to state so that it
101
+ # can be carried forward to subsequent slides until a new section title is discovered.
102
+ # @see
103
+ # https://github.com/puppetlabs/showoff/blob/3f43754c84f97be4284bb34f9bc7c42175d45226/lib/showoff.rb#L499-L508
104
+ if @classes.include? 'subsection'
105
+ matches = @markdown.match(/#+ *(.*?)#*$/)
106
+ @section_title = matches[1] || @section
107
+ Showoff::State.set(:section_title, @section_title)
108
+ else
109
+ @section_title = Showoff::State.get(:section_title) || @section
110
+ end
111
+ end
112
+
113
+ end