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