Class: Showoff
- Inherits:
-
Sinatra::Application
- Object
- Sinatra::Application
- Showoff
- Defined in:
- lib/showoff.rb,
lib/showoff_ng.rb
Defined Under Namespace
Classes: Compiler, Config, Locale, Logger, Presentation, State
Constant Summary collapse
- GEMROOT =
File.(File.join(File.dirname(__FILE__), '..'))
Instance Attribute Summary collapse
-
#cached_image_size ⇒ Object
readonly
Returns the value of attribute cached_image_size.
Class Method Summary collapse
- .do_static(args, options) ⇒ Object
-
.flush ⇒ Object
save stats to disk.
-
.generatePDF ⇒ Object
Generate a PDF version of the presentation in the current directory.
-
.makeSnapshot(presentation) ⇒ Object
Generate a static HTML snapshot of the presentation in the ‘static` directory.
- .pres_dir_current ⇒ Object
Instance Method Summary collapse
- #authenticate(credentials) ⇒ Object
- #authorized? ⇒ Boolean
-
#get_code_from_slide(path, index, executable = true) ⇒ Object
Load a slide file from disk, parse it and return the text of a code block by index.
- #guid ⇒ Object
-
#initialize(app = nil) ⇒ Showoff
constructor
A new instance of Showoff.
- #localhost? ⇒ Boolean
- #locked! ⇒ Object
- #manage_client_cookies(presenter = false) ⇒ Object
- #master_presenter? ⇒ Boolean
-
#protected! ⇒ Object
Basic auth boilerplate.
- #require_ruby_files ⇒ Object
- #unlocked? ⇒ Boolean
- #valid_presenter_cookie? ⇒ Boolean
Constructor Details
#initialize(app = nil) ⇒ Showoff
Returns a new instance of Showoff.
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 |
# File 'lib/showoff.rb', line 72 def initialize(app=nil) super(app) @logger = Logger.new(STDERR) @logger.formatter = proc { |severity,datetime,progname,msg| "#{progname} #{msg}\n" } @logger.level = settings.verbose ? Logger::DEBUG : Logger::WARN @review = settings.review @execute = settings.execute settings.pres_dir ||= Dir.pwd @root_path = "." # Load up the default keymap, then merge in any customizations keymapfile = File.(File.join('~', '.showoff', 'keymap.json')) rescue nil @keymap = Keymap.default @keymap.merge! JSON.parse(File.read(keymapfile)) rescue {} # map keys to the labels we're using @keycode_dictionary = Keymap.keycodeDictionary @keycode_shifted_keys = Keymap.shiftedKeyDictionary settings.pres_dir = File.(settings.pres_dir) if (settings.pres_file and settings.pres_file != 'showoff.json') ShowoffUtils.presentation_config_file = settings.pres_file end # Load configuration for page size and template from the # configuration JSON file if File.exist?(ShowoffUtils.presentation_config_file) showoff_json = JSON.parse(File.read(ShowoffUtils.presentation_config_file)) settings.showoff_config = showoff_json # Set options for encoding, template and page size settings.encoding = showoff_json["encoding"] || 'UTF-8' settings.page_size = showoff_json["page-size"] || "Letter" settings.pres_template = showoff_json["templates"] end # if no sections are provided, we'll just start from cwd settings.showoff_config['sections'] ||= ['.'] # code execution timeout settings.showoff_config['timeout'] ||= 15 # If favicon in presentation root, use it by default if File.exist? 'favicon.ico' settings.showoff_config['favicon'] ||= 'file/favicon.ico' end # default protection levels if settings.showoff_config.has_key? 'password' settings.showoff_config['protected'] ||= ["presenter", "onepage", "print"] else settings.showoff_config['protected'] ||= Array.new end if settings.showoff_config.has_key? 'key' settings.showoff_config['locked'] ||= ["slides"] else settings.showoff_config['locked'] ||= Array.new end # default code parsers (for executable code blocks) settings.showoff_config['parsers'] ||= {} settings.showoff_config['parsers']['perl'] ||= 'perl' settings.showoff_config['parsers']['puppet'] ||= 'puppet apply --color=false' settings.showoff_config['parsers']['python'] ||= 'python' settings.showoff_config['parsers']['ruby'] ||= 'ruby' settings.showoff_config['parsers']['shell'] ||= 'sh' # default code validators settings.showoff_config['validators'] ||= {} settings.showoff_config['validators']['perl'] ||= 'perl -cw' settings.showoff_config['validators']['puppet'] ||= 'puppet parser validate' settings.showoff_config['validators']['python'] ||= 'python -m py_compile' settings.showoff_config['validators']['ruby'] ||= 'ruby -c' settings.showoff_config['validators']['shell'] ||= 'sh -n' # highlightjs syntax style @highlightStyle = settings.showoff_config['highlight'] || 'default' # variables used for building section numbering and title @slide_count = 0 @section_major = 0 @section_minor = 0 @section_title = settings.showoff_config['name'] rescue I18n.t('name') @@slide_titles = [] # a list of generated slide names, used for cross references later. @logger.debug settings.pres_template @cached_image_size = {} @logger.debug settings.pres_dir @pres_name = settings.pres_dir.split('/').pop require_ruby_files # invert the logic to maintain backwards compatibility of interactivity on by default @interactive = ! settings.standalone rescue false # Create stats directory FileUtils.mkdir settings.statsdir unless File.directory? settings.statsdir if @interactive # Page view time accumulator. Tracks how often slides are viewed by the audience begin @@counter = JSON.parse(File.read("#{settings.statsdir}/#{settings.viewstats}")) # TODO: remove this logic 4/15/2017: port old format stats unless @@counter.has_key? 'user_agents' @@counter['pageviews'] = @@counter end @@counter['current'] ||= {} @@counter['pageviews'] ||= {} @@counter['user_agents'] ||= {} rescue @@counter = { 'user_agents' => {}, 'pageviews' => {}, 'current' => {} } end # keeps track of form responses. In memory to avoid concurrence issues. begin @@forms = JSON.parse(File.read("#{settings.statsdir}/#{settings.forms}")) rescue @@forms = Hash.new end @@downloads = Hash.new # Track downloadable files @@cookie = nil # presenter cookie. Identifies the presenter for control messages @@master = nil # this holds the @client_id of the master presenter, for the cases in which multiple presenters are loaded @@current = Hash.new # The current slide that the presenter is viewing @@cache = Hash.new # Cache slide content for subsequent hits @@activity = [] # keep track of completion for activity slides if @interactive # flush stats to disk periodically Thread.new do loop do sleep 30 Showoff.flush end end end # Initialize Markdown Configuration MarkdownConfig::setup(settings.pres_dir) # Process renderer config options @engine_options = ShowoffUtils.(settings.pres_dir) end |
Instance Attribute Details
#cached_image_size ⇒ Object (readonly)
Returns the value of attribute cached_image_size.
33 34 35 |
# File 'lib/showoff.rb', line 33 def cached_image_size @cached_image_size end |
Class Method Details
.do_static(args, options) ⇒ Object
1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 |
# File 'lib/showoff.rb', line 1476 def self.do_static(args, opts = {}) args ||= [] # handle nil arguments what = args[0] || "index" opt = args[1] ShowoffUtils.presentation_config_file = opts[:f] # Sinatra now aliases new to new! # https://github.com/sinatra/sinatra/blob/v1.3.3/lib/sinatra/base.rb#L1369 showoff = Showoff.new! name = showoff.instance_variable_get(:@pres_name) path = showoff.instance_variable_get(:@root_path) logger = showoff.instance_variable_get(:@logger) I18n.locale = opts[:language] case what when 'supplemental' data = showoff.send(what, opt, true) when 'pdf' opt ||= "#{name}.pdf" data = showoff.send(what, opt) when 'print' opt ||= 'handouts' data = showoff.send(what, opt, true) else data = showoff.send(what, true) end if data.is_a?(File) logger.warn "Generated PDF as #{opt}" else out = File.("#{path}/static") # First make a directory FileUtils.makedirs(out) # Then write the html file = File.new("#{out}/index.html", "w") file.puts(data) file.close # Now copy all the js and css my_path = File.join( File.dirname(__FILE__), '..', 'public') ["js", "css"].each { |dir| FileUtils.copy_entry("#{my_path}/#{dir}", "#{out}/#{dir}", false, false, true) } # @todo: uh. I don't know how this ever worked. my_path is showoff and name is presentation. # And copy the directory Dir.glob("#{my_path}/#{name}/*").each { |subpath| base = File.basename(subpath) next if "static" == base next unless File.directory?(subpath) || base.match(/\.(css|js)$/) FileUtils.copy_entry(subpath, "#{out}/#{base}") } # Set up file dir file_dir = File.join(out, 'file') FileUtils.makedirs(file_dir) pres_dir = showoff.settings.pres_dir # ..., copy all user-defined styles and javascript files showoff.css_files.each { |path| dest = File.join(out, path) FileUtils.mkdir_p(File.dirname(dest)) FileUtils.copy(path, dest) } showoff.js_files.each { |path| dest = File.join(out, path) FileUtils.mkdir_p(File.dirname(dest)) FileUtils.copy(path, dest) } # ... and copy all needed image files [/img src=[\"\'].\/file\/(.*?)[\"\']/, /style=[\"\']background(?:-image): url\(\'file\/(.*?)'/].each do |regex| data.scan(regex).flatten.each do |path| dir = File.dirname(path) FileUtils.makedirs(File.join(file_dir, dir)) begin FileUtils.copy(File.join(pres_dir, path), File.join(file_dir, path)) rescue Errno::ENOENT => e puts "Missing source file: #{path}" end end end # copy images from css too showoff.css_files.each do |css_path| File.open(css_path) do |file| data = file.read data.scan(/url\([\"\']?(?!https?:\/\/)(.*?)[\"\']?\)/).flatten.each do |path| path.gsub!(/(\#.*)$/, '') # get rid of the anchor path.gsub!(/(\?.*)$/, '') # get rid of the query # resolve relative paths in the stylesheet path = "#{File.dirname(css_path)}/#{path}" unless path.start_with? '/' logger.debug path dir = File.dirname(path) FileUtils.makedirs(File.join(file_dir, dir)) begin FileUtils.copy(File.join(pres_dir, path), File.join(file_dir, path)) rescue Errno::ENOENT => e puts "Missing source file: #{path}" end end end end end end |
.flush ⇒ Object
save stats to disk
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 |
# File 'lib/showoff.rb', line 221 def self.flush begin if defined?(@@counter) and not @@counter.empty? File.open("#{settings.statsdir}/#{settings.viewstats}", 'w') do |f| if settings.verbose then f.write(JSON.pretty_generate(@@counter)) else f.write(@@counter.to_json) end end end if defined?(@@forms) and not @@forms.empty? File.open("#{settings.statsdir}/#{settings.forms}", 'w') do |f| if settings.verbose then f.write(JSON.pretty_generate(@@forms)) else f.write(@@forms.to_json) end end end rescue Errno::ENOENT => e end end |
.generatePDF ⇒ Object
wkhtmltopdf is terrible and will often report hard failures even after successfully building a PDF. Therefore, we check file existence and display different error messaging.
Generate a PDF version of the presentation in the current directory. This requires that the HTML snaphot exists, and it will remove that snapshot if the PDF generation is successful.
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/showoff_ng.rb', line 75 def self.generatePDF begin require 'pdfkit' output = Showoff::Config.get('name')+'.pdf' kit = PDFKit.new(File.new('static/index.html'), Showoff::Config.get('pdf_options')) kit.to_file(output) FileUtils.rm_rf('static') rescue RuntimeError => e if File.exist? output Showoff::Logger.warn "Your PDF was generated, but PDFkit reported an error. Inspect the file #{output} for suitability." Showoff::Logger.warn "You might try loading `static/index.html` in a web browser and checking the developer console for 404 errors." else Showoff::Logger.error "Generating your PDF with wkhtmltopdf was not successful." Showoff::Logger.error "Try running the following command manually to see what it's failing on." Showoff::Logger.error e..sub('--quiet', '') end rescue LoadError Showoff::Logger.error 'Generating a PDF version of your presentation requires the `pdfkit` gem.' end end |
.makeSnapshot(presentation) ⇒ Object
Generate a static HTML snapshot of the presentation in the ‘static` directory. Note that the `Showoff::Presentation` determines the format of the generated presentation based on the content requested.
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/showoff_ng.rb', line 40 def self.makeSnapshot(presentation) FileUtils.mkdir_p 'static' File.write(File.join('static', 'index.html'), presentation.static) ['js', 'css'].each { |dir| src = File.join(GEMROOT, 'public', dir) dest = File.join('static', dir) FileUtils.copy_entry(src, dest, false, false, true) } # now copy all the files we care about presentation.assets.each do |path| src = File.join(Showoff::Config.root, path) dest = File.join('static', path) FileUtils.mkdir_p(File.dirname(dest)) begin FileUtils.copy(src, dest) rescue Errno::ENOENT => e Showoff::Logger.warn "Missing source file: #{path}" end end end |
.pres_dir_current ⇒ Object
246 247 248 249 |
# File 'lib/showoff.rb', line 246 def self.pres_dir_current opt = {:pres_dir => Dir.pwd} Showoff.set opt end |
Instance Method Details
#authenticate(credentials) ⇒ Object
1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 |
# File 'lib/showoff.rb', line 1662 def authenticate(credentials) auth = Rack::Auth::Basic::Request.new(request.env) return false unless auth.provided? && auth.basic? && auth.credentials case credentials when Array auth.credentials == credentials when String auth.credentials.last == credentials else false end end |
#authorized? ⇒ Boolean
1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 |
# File 'lib/showoff.rb', line 1638 def # allow localhost if we have no password if not settings.showoff_config.has_key? 'password' localhost? else user = settings.showoff_config['user'] || '' password = settings.showoff_config['password'] authenticate([user, password]) end end |
#get_code_from_slide(path, index, executable = true) ⇒ Object
Load a slide file from disk, parse it and return the text of a code block by index
1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 |
# File 'lib/showoff.rb', line 1586 def (path, index, executable=true) if path =~ /^(.*)(?::)(\d+)$/ path = $1 num = $2.to_i else num = 1 end classes = executable ? 'code.execute' : 'code' = "#{path}.md" return [] unless File.exist? content = File.read() return [] if content.nil? return [] if content.empty? if defined? num content = content.split(/^\<?!SLIDE/m).reject { |sl| sl.empty? }[num-1] end html = process_markdown(, '', content, {}) doc = Nokogiri::HTML::DocumentFragment.parse(html) if index == 'all' doc.css(classes).collect do |code| classes = code.attr('class').split rescue [] lang = classes.shift =~ /language-(\S*)/ ? $1 : nil [lang, code.text.gsub(/^\* /, ' '), classes] end else doc.css(classes)[index.to_i].text.gsub(/^\* /, ' ') rescue 'Invalid code block index' end end |
#guid ⇒ Object
1677 1678 1679 1680 |
# File 'lib/showoff.rb', line 1677 def guid # this is a terrifyingly simple GUID generator (0..15).to_a.map{|a| rand(16).to_s(16)}.join end |
#localhost? ⇒ Boolean
1658 1659 1660 |
# File 'lib/showoff.rb', line 1658 def localhost? request.env['REMOTE_HOST'] == 'localhost' or request.ip == '127.0.0.1' end |
#locked! ⇒ Object
1630 1631 1632 1633 1634 1635 1636 |
# File 'lib/showoff.rb', line 1630 def locked! # check auth first, because if the presenter has logged in with a password, we don't want to prompt again unless or unlocked? response['WWW-Authenticate'] = %(Basic realm="#{@title}: Locked Area. A presentation key is required to view.") throw(:halt, [401, "Not authorized."]) end end |
#manage_client_cookies(presenter = false) ⇒ Object
1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 |
# File 'lib/showoff.rb', line 1691 def (presenter=false) # store a cookie to tell clients apart. More reliable than using IP due to proxies, etc. if request.nil? # when running showoff static @client_id = guid() else if request.['client_id'] @client_id = request.['client_id'] else @client_id = guid() response.('client_id', @client_id) end # if we have no content translations then remove the cookie response.('locale') if language_names.empty? end if presenter @@master ||= @client_id @@cookie ||= guid() response.('presenter', @@cookie) end end |
#master_presenter? ⇒ Boolean
1687 1688 1689 |
# File 'lib/showoff.rb', line 1687 def master_presenter? @@master == @client_id end |
#protected! ⇒ Object
Basic auth boilerplate
1623 1624 1625 1626 1627 1628 |
# File 'lib/showoff.rb', line 1623 def protected! unless response['WWW-Authenticate'] = %(Basic realm="#{@title}: Protected Area. Please log in.") throw(:halt, [401, "Not authorized."]) end end |
#require_ruby_files ⇒ Object
251 252 253 |
# File 'lib/showoff.rb', line 251 def require_ruby_files Dir.glob("#{settings.pres_dir}/*.rb").map { |path| require path } end |
#unlocked? ⇒ Boolean
1649 1650 1651 1652 1653 1654 1655 1656 |
# File 'lib/showoff.rb', line 1649 def unlocked? # allow localhost if we have no key if not settings.showoff_config.has_key? 'key' localhost? else authenticate(settings.showoff_config['key']) end end |
#valid_presenter_cookie? ⇒ Boolean
1682 1683 1684 1685 |
# File 'lib/showoff.rb', line 1682 def return false if @@cookie.nil? (request.['presenter'] == @@cookie) end |