require 'uri' #### jsrb.rb require 'js' # JS を Ruby ぽく扱えるようにする class JSrb def self.global @global ||= JSrb.new(JS.global) end def self.window @window ||= global[:window] end def self.document @document ||= global[:document] end # @param sec [Numeric] seocnd def self.timeout(sec, &block) JS.global.setTimeout(->{Fiber.new{block.call}.transfer if block}, sec * 1000) end # @param v [JS::Object] # @return [Object] def self.convert(v) return nil if v == JS::Null || v == JS::Undefined case v.typeof when 'number' return v.to_s =~ /\./ ? v.to_f : v.to_i when 'bigint' return v.to_i when 'string' return v.to_s when 'boolean' return v == JS::True end if v[:constructor] == JS.global[:Array] v[:length].to_i.times.map{|i| JSrb.convert(v[i])} elsif v[:length].typeof == 'number' && v[:item].typeof == 'function' v = JSrb.new(v) v.extend JSrb::Enumerable v elsif v[:constructor] == JS.global[:Date] Time.new(v.toISOString.to_s) else JSrb.new(v) end end # @param obj [JS::Object] def initialize(obj) @obj = obj end # hoge_fuga を hogeFuga に変換して JavaScript を呼び出し、 # 値を JS::Object から Ruby に変換して返す def method_missing(sym, *args, &block) jssym = sym.to_s.gsub(/_([a-z])/){$1.upcase}.intern jsargs = args.map{|a| a.is_a?(JSrb) ? a.js_object : a} jsblock = block ? proc{|*v| block.call(*v.map{JSrb.convert(_1)})} : nil if jssym.end_with? '=' return @obj.__send__(jssym, *jsargs, &jsblock) if @obj.respond_to? jssym return @obj.__send__(:[]=, jssym.to_s.chop.intern, *jsargs, &jsblock) end v = @obj[jssym] if v.typeof == 'function' JSrb.convert(@obj.call(jssym, *jsargs, &jsblock)) elsif v == JS::Undefined && @obj.respond_to?(jssym) JSrb.convert(@obj.__send__(jssym, *jsargs, &jsblock)) elsif v != JS::Undefined && args.empty? JSrb.convert(v) else super end end def respond_to_missing?(sym, include_private) return true if super return true if @obj.respond_to? sym jssym = sym.to_s.sub(/=$/, '').gsub(/_([a-z])/){$1.upcase}.intern @obj[sym] != JS::Undefined || @obj[jssym] != JS::Undefined end def to_s @obj.to_s end def to_i @obj.to_i end def to_h JSrb.window[:Object].entries(@obj).to_h end def inspect "#" end # @param sym [Symbol] # @return [Object] def [](sym) JSrb.convert(@obj[sym]) end # @return [JS::Object] def js_object @obj end private module Enumerable include ::Enumerable def each i = 0 while i < length yield self.item(i) i += 1 end end def size length end def empty? length == 0 end def last self[length - 1] end end end #### def create_element(tag, **attrs) e = @document.create_element(tag) attrs.each do |n, v| e.set_attribute(n.to_s, v) end e end def draw unless @start_time if current == 0 @document.cookie = "rabbit_start_time=0;max-age=0" end if @document.cookie =~ /\Arabbit_start_time=(\d+)/ @start_time = $1.to_i else @start_time = Time.now.to_i @document.cookie = "rabbit_start_time=#{@start_time};max-age=3600" end end parent.append_child(@img_turtle) parent.append_child(@img_rabbit) parent_width = @body.client_width width = parent_width - @img_rabbit.width left = width * current @img_rabbit.style.left = "#{left}px" @img_rabbit.style.transform = nil if @allotted_time == 0 @img_turtle.style.visibility = 'hidden' return end width = parent_width - @img_turtle.width left = width * (Time.now.to_f - @start_time) / (@allotted_time * 60) @img_turtle.style.transform = nil if left < parent_width + 200 @img_turtle.style.left = "#{left}px" @img_turtle.style.visibility = 'visible' else @img_turtle.style.visibility = 'hidden' end end def create_dialog @dialog = create_element( 'div', id: 'dialog', style: 'position:fixed;; top:0; left:0; display:none; padding:10px; border:solid 1px #000; z-index:10000; background-color:#fff' ) @dialog.innerHTML = <<~HTML HTML @body.append_child(@dialog) @document.query_selector('#allotted_time').add_event_listener('change') do |ev| @allotted_time = ev.target.value.to_i end @document.query_selector('#close_dialog').add_event_listener('click') do @dialog.style.display = 'none' end @document.query_selector('#reset_turtle-button').add_event_listener('click') do @start_time = Time.now.to_i @document.cookie = "rabbit_start_time=#{@start_time};max-age=86400" end end def start unless @started uri = URI.parse(JSrb.window.location.to_s) if uri.host == 'speakerdeck.com' include SpeakerDeck elsif JSrb.document.query_selector('div.pdfViewer') include PDFViewer else include RevealJS end q = URI.decode_www_form(uri.query.to_s).to_h @allotted_time ||= q['allotted_time'].to_i @document = document @body = @document.query_selector('body') create_dialog @img_rabbit = create_element( 'img', id: 'rabbit-rabbit', style: 'position:fixed; bottom:0px; left:0px; width:5%; margin:2px', src: 'https://tmtms.net/rabbit/rabbit.png' ) @img_rabbit.style.visibility = 'hidden' if @document.query_selector('.print-pdf') @img_rabbit.add_event_listener('click') do @dialog.style.display = @dialog.style[:display] == 'block' ? 'none' : 'block' end @img_turtle = create_element( 'img', id: 'rabbit-turtle', style: 'position:fixed; bottom:0px; left:0px; width:5%; margin:2px; visibility:hidden', src: 'https://tmtms.net/rabbit/turtle.png' ) @started = true end if current == 0 && @start_time @start_time = Time.now.to_i @document.cookie = "rabbit_start_time=#{@start_time};max-age=86400" end JSrb.window.clear_interval(@interval_id) if @interval_id @interval_id = JSrb.window.set_interval(->{draw}, 300) end module RevealJS def parent @body end def current slides = @document.query_selector_all('div.slides section').to_a current_page = slides.index{|v| v.get_attribute('class') == 'present'} || 0 current_page.to_f / (slides.size - 1) end def document JSrb.document end end module SpeakerDeck def parent @body.query_selector('div.is-fullscreen') ? @body.query_selector('div.js-sd-player-current-slide') : @body end def current @body.query_selector('div.sd-player-scrubber-progress').style.width.to_f / 100.0 end def document JSrb.document.query_selector('iframe.speakerdeck-iframe').content_document end end module PDFViewer def parent document.get_element_by_id('viewerContainer') end def current page = @document.get_element_by_id('pageNumber') cur = page.value.to_i - 1 max = page.max.to_i - 1 cur.to_f / max end def document JSrb.document end end start