Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,19 +116,25 @@ end
### Browser API Wrappers

In strict mode, method calls without an explicit receiver are resolved against a default browser context object.
For now, this context exposes `console` with:
This context currently exposes:

- `log`
- `info`
- `warn`
- `error`
- `console.log`
- `console.info`
- `console.warn`
- `console.error`
- `window.setTimeout(callback, delay)` returning a typed timer handle
- `window.clearTimeout(handle)`
- `navigator.share(text:, title: nil, url: nil)`

Use regular-looking calls (instead of wrapper constants or `_literal_js`):

```crystal
class MyConsoleCode < JS::Code
def_to_js strict: true do
console.log("Hello", 7, true)
timer = window.setTimeout("tick", 1000)
window.clearTimeout(timer)
navigator.share(text: "Done", title: "Status")
end
end
```
Expand Down
2 changes: 1 addition & 1 deletion spec/js/code/strict_mode_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ module JS::Code::StrictModeSpec

exit_code, _stdout, stderr = crystal_eval(source)
exit_code.should_not eq(0)
stderr.should contain("undefined method")
stderr.should contain("Strict mode forbids `_literal_js(...)`.")
stderr.should contain("_literal_js")
end
end
Expand Down
65 changes: 65 additions & 0 deletions spec/js/context/browser_api_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
require "../../spec_helper"

module JS::Context::BrowserAPISpec
class StrictWindowTimersCode < JS::Code
def_to_js strict: true do
timer = window.setTimeout(-> {
console.log("tick")
}, 1000)
window.clearTimeout(timer)
end
end

class StrictNavigatorShareCode < JS::Code
def_to_js strict: true do
navigator.share(text: "Done", title: "Status", url: "https://example.com")
end
end

class StrictReceiverlessWindowTimersCode < JS::Code
def_to_js strict: true do
timer = setTimeout(-> {
console.log("tick")
}, 1000)
clearTimeout(timer)
end
end

describe "strict browser context timer calls" do
it "transpiles window.setTimeout and window.clearTimeout in strict mode" do
expected = <<-JS.squish
var timer;
timer = window.setTimeout(() => {
console.log("tick");
}, 1000);
window.clearTimeout(timer);
JS

StrictWindowTimersCode.to_js.should eq(expected)
end
end

describe "strict browser context forwarded window calls" do
it "transpiles receiverless timer calls by forwarding through window" do
expected = <<-JS.squish
var timer;
timer = setTimeout(() => {
console.log("tick");
}, 1000);
clearTimeout(timer);
JS

StrictReceiverlessWindowTimersCode.to_js.should eq(expected)
end
end

describe "strict browser context navigator calls" do
it "transpiles navigator.share with named args in strict mode" do
expected = <<-JS.squish
navigator.share({text: "Done", title: "Status", url: "https://example.com"});
JS

StrictNavigatorShareCode.to_js.should eq(expected)
end
end
end
11 changes: 6 additions & 5 deletions src/js/code.cr
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,12 @@ module JS
{% if !opts[:inline] %}
{{io}} << ";"
{% end %}
{% elsif exp.is_a?(Call) && exp.name.stringify == "_literal_js" && !opts[:strict] %}
{{io}} << {{exp.args.first}}
{% elsif exp.is_a?(Call) && exp.name.stringify == "_literal_js" %}
{% if opts[:strict] %}
{{exp.raise "Strict mode forbids `_literal_js(...)`."}}
{% else %}
{{io}} << {{exp.args.first}}
{% end %}
{% elsif exp.is_a?(Call) && exp.name.stringify == "to_js_call" %}
{{io}} << {{exp}}
{% if !opts[:inline] %}
Expand Down Expand Up @@ -207,9 +211,6 @@ module JS
{{exp.args.first}}
end
{% else %}
{% if opts[:strict] && !exp.receiver && !JS_ALIASES.has_key?(exp.name.stringify) && !scope_declared_vars.includes?(exp.name.stringify) %}
JS::Context.default.{{exp.name}}
{% end %}
{% emitted_from_strict_context = false %}
{% if exp.receiver %}
# TODO: Replace this whole `if` by a recursive call to this macro?
Expand Down
14 changes: 14 additions & 0 deletions src/js/context/browser.cr
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
require "./console"
require "./navigator"
require "./window"

module JS
module Context
class Browser
def console : JS::Context::Console
JS::Context::Console.new
end

def window : JS::Context::Window
JS::Context::Window.new
end

def navigator : JS::Context::Navigator
JS::Context::Navigator.new
end

macro method_missing(call)
window.{{call}}
end
end

def self.default : JS::Context::Browser
Expand Down
6 changes: 3 additions & 3 deletions src/js/context/context_object.cr
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ module JS
protected def initialize(@to_js_ref : String)
end

def initialize(preceding_call_chain : String, method_name : String, *args : JS::Context::CallArgument)
def initialize(preceding_call_chain : String, method_name : String, *args)
@to_js_ref = JS::Context.build_call_chain(preceding_call_chain, method_name, *args)
end
end

# Keep chain generation centralized so all browser context wrappers emit identical JS call syntax.
def self.build_call_chain(preceding_call_chain : String, method_name : String, *args : JS::Context::CallArgument) : String
def self.build_call_chain(preceding_call_chain : String, method_name : String, *args) : String
String.build do |io|
unless preceding_call_chain.empty?
io << preceding_call_chain
Expand All @@ -29,7 +29,7 @@ module JS
end
end

def self.serialize_args(io : IO, *args : JS::Context::CallArgument) : Nil
def self.serialize_args(io : IO, *args) : Nil
args.each_with_index do |arg, index|
io << ", " unless index.zero?
io << arg.to_js_ref
Expand Down
26 changes: 26 additions & 0 deletions src/js/context/navigator.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require "./context_object"
require "./undefined"

module JS
module Context
class Navigator < JS::Context::ContextObject
def initialize
super("navigator")
end

def share(*, text : String, title : String? = nil, url : String? = nil) : JS::Context::Undefined
# Omit nil keys so we emit a realistic Web Share payload object.
payload = if title && url
{text: text, title: title, url: url}
elsif title
{text: text, title: title}
elsif url
{text: text, url: url}
else
{text: text}
end
JS::Context::Undefined.new(to_js_ref, "share", payload)
end
end
end
end
8 changes: 8 additions & 0 deletions src/js/context/timer_handle.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require "./context_object"

module JS
module Context
class TimerHandle < JS::Context::ContextObject
end
end
end
24 changes: 24 additions & 0 deletions src/js/context/window.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
require "./context_object"
require "./timer_handle"
require "./undefined"

module JS
module Context
alias TimerDelay = Int::Primitive | Float32 | Float64
alias TimerCallback = String | JS::Context::ContextObject

class Window < JS::Context::ContextObject
def initialize
super("window")
end

def setTimeout(callback : JS::Context::TimerCallback, delay : JS::Context::TimerDelay) : JS::Context::TimerHandle
JS::Context::TimerHandle.new(to_js_ref, "setTimeout", callback, delay)
end

def clearTimeout(handle : JS::Context::TimerHandle) : JS::Context::Undefined
JS::Context::Undefined.new(to_js_ref, "clearTimeout", handle)
end
end
end
end