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