diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41ef7912..007c56c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: version: - '1.6' - '1.7' - - 'nightly' + - '1.10' os: - ubuntu-latest - macOS-latest diff --git a/Project.toml b/Project.toml index f2b9c9d2..f09f4dac 100644 --- a/Project.toml +++ b/Project.toml @@ -17,17 +17,28 @@ Observables = "510215fc-4207-5dde-b226-833fc4488ee2" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" Requires = "ae029012-a4dd-5104-9daa-d747884805df" StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" +[weakdeps] +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" + +[extensions] +StippleDataFramesExt = "DataFrames" +StippleJSONExt = "JSON" +StippleOffsetArraysExt = "OffsetArrays" + [compat] DataFrames = "1" Dates = "1.6" FilePathsBase = "0.9" -Genie = "5.23.8" +Genie = "5.24.4" GenieSession = "1" GenieSessionFileSession = "1" JSON = "0.20, 0.21" @@ -40,6 +51,7 @@ OffsetArrays = "1" OrderedCollections = "1" Parameters = "0.12" Pkg = "1.6" +PrecompileTools = "1.2" Random = "1.6" Reexport = "1" Requires = "1" @@ -47,11 +59,6 @@ StructTypes = "1.8" Tables = "1" julia = "1.6" -[extensions] -StippleDataFramesExt = "DataFrames" -StippleJSONExt = "JSON" -StippleOffsetArraysExt = "OffsetArrays" - [extras] DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" @@ -60,8 +67,3 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] test = ["Test", "DataFrames", "JSON", "OffsetArrays"] - -[weakdeps] -DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" -JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" diff --git a/assets/css/stipplecore.css b/assets/css/stipplecore.css index 76d69eb3..7226640b 100644 --- a/assets/css/stipplecore.css +++ b/assets/css/stipplecore.css @@ -41,6 +41,15 @@ border-radius: 5px; box-shadow: 0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1); } +.stipple-core header { + padding-top:5px; + padding-left: 5px; + } + +.stipple-core a { + color: steelblue; +} + .container { margin-bottom: 20px; } .stipple-core .st-module > h1, .stipple-core .st-module > h2, @@ -87,7 +96,9 @@ .stipple-core .text-h1, .stipple-core h1 { line-height: 2.5rem; + font-size: 4rem; } + .stipple-core .text-h2, .stipple-core h2 { line-height: 2rem; @@ -265,6 +276,10 @@ .stipple-core .q-field--standout .text-white .q-field__native { color: var(--q-color-white); } +.stipple-core .q-field__control-container .material-icons { + margin-top: auto; + margin-bottom: auto; +} .stipple-core .q-badge { font-weight: 700; padding: 4px 6px; @@ -540,4 +555,4 @@ body > .q-loading-bar { } } -[v-cloak] { display: none; } \ No newline at end of file +[v-cloak] { display: none; } diff --git a/ext/StippleJSONExt.jl b/ext/StippleJSONExt.jl index 5ff53c81..dc4e75a0 100644 --- a/ext/StippleJSONExt.jl +++ b/ext/StippleJSONExt.jl @@ -18,4 +18,5 @@ Stipple.JSONText(json::JSON.JSONText) = Stipple.JSONText(json.s) @inline StructTypes.construct(::Type{JSON.JSONText}, json::JSON3.RawValue) = JSON.JSONText(string(json)) @inline JSON3.rawbytes(json::JSON.JSONText) = codeunits(json.s) +Stipple.js_print(io::IO, js::JSON.JSONText) = print(io, js.s) end \ No newline at end of file diff --git a/src/Elements.jl b/src/Elements.jl index 4ba36d78..2ae22c79 100644 --- a/src/Elements.jl +++ b/src/Elements.jl @@ -443,8 +443,8 @@ julia> stylesheet("https://fonts.googleapis.com/css?family=Material+Icons") "" ``` """ -function stylesheet(href::String; args...) :: ParsedHTMLString - Genie.Renderer.Html.link(href=href, rel="stylesheet", args...) +function stylesheet(href::String; kwargs...) :: ParsedHTMLString + Genie.Renderer.Html.link(href=href, rel="stylesheet"; kwargs...) end end diff --git a/src/ReactiveTools.jl b/src/ReactiveTools.jl index f8c73270..412326bd 100644 --- a/src/ReactiveTools.jl +++ b/src/ReactiveTools.jl @@ -434,7 +434,7 @@ end macro add_vars(expr) init_storage(__module__) - REACTIVE_STORAGE[__module__] = Stipple.merge_storage(REACTIVE_STORAGE[__module__], @eval(__module__, Stipple.@var_storage($expr))) + REACTIVE_STORAGE[__module__] = Stipple.merge_storage(REACTIVE_STORAGE[__module__], @eval(__module__, Stipple.@var_storage($expr)); context = __module__) update_storage(__module__) end @@ -670,7 +670,7 @@ macro mixin(location, expr, prefix, postfix) M = $x isa DataType ? $x : typeof($x) # really needed? local mixin_storage = Stipple.model_to_storage(M, $(QuoteNode(prefix)), $postfix) - merge!(storage, Stipple.merge_storage(storage, mixin_storage)) + merge!(storage, Stipple.merge_storage(storage, mixin_storage; context = @__MODULE__)) location isa DataType || location isa Symbol ? eval(:(Stipple.@type($$loc, $storage))) : location mixin_storage end |> esc diff --git a/src/Stipple.jl b/src/Stipple.jl index 2a572f84..3e468bcf 100644 --- a/src/Stipple.jl +++ b/src/Stipple.jl @@ -15,6 +15,7 @@ existing Vue.js libraries. """ module Stipple +using PrecompileTools const PRECOMPILE = Ref(false) const ALWAYS_REGISTER_CHANNELS = Ref(true) const USE_MODEL_STORAGE = Ref(true) @@ -619,27 +620,25 @@ function init(t::Type{M}; end ch = "/$channel/events" - if ! Genie.Router.ischannel(Router.channelname(ch)) - Genie.Router.channel(ch, named = Router.channelname(ch)) do - # get event name - event = Genie.Requests.payload(:payload)["event"] - # form handler parameter & call event notifier - handler = Symbol(get(event, "name", nothing)) - event_info = get(event, "event", nothing) - - # add client id if requested - if event_info isa Dict && get(event_info, "_addclient", false) - client = transport == Genie.WebChannels ? Genie.WebChannels.id(Genie.Requests.wsclient()) : Genie.Requests.wtclient() - push!(event_info, "_client" => client) - end + Genie.Router.channel(ch, named = Router.channelname(ch)) do + # get event name + event = Genie.Requests.payload(:payload)["event"] + # form handler parameter & call event notifier + handler = Symbol(get(event, "name", nothing)) + event_info = get(event, "event", nothing) + + # add client id if requested + if event_info isa Dict && get(event_info, "_addclient", false) + client = transport == Genie.WebChannels ? Genie.WebChannels.id(Genie.Requests.wsclient()) : Genie.Requests.wtclient() + push!(event_info, "_client" => client) + end - isempty(methods(notify, (M, Val{handler}))) || notify(model, Val(handler)) - isempty(methods(notify, (M, Val{handler}, Any))) || notify(model, Val(handler), event_info) + isempty(methods(notify, (M, Val{handler}))) || notify(model, Val(handler)) + isempty(methods(notify, (M, Val{handler}, Any))) || notify(model, Val(handler), event_info) - LAST_ACTIVITY[Symbol(channel)] = now() + LAST_ACTIVITY[Symbol(channel)] = now() - ok_response - end + ok_response end end @@ -1287,4 +1286,45 @@ include("Layout.jl") @reexport using .Elements @reexport using .Layout +# precompilation ... + +using Stipple.ReactiveTools +@setup_workload begin + # Putting some things in `setup` can reduce the size of the + # precompile file and potentially make loading faster. + using Genie.HTTPUtils.HTTP + PRECOMPILE[] = true + @compile_workload begin + # all calls in this block will be precompiled, regardless of whether + # they belong to your package or not (on Julia 1.8 and higher) + ui() = [cell("hello"), row("world"), htmldiv("Hello World")] + + @app PrecompileApp begin + @in demo_i = 1 + @out demo_s = "Hi" + + @onchange demo_i begin + println(demo_i) + end + end + + route("/") do + model = Stipple.ReactiveTools.@init PrecompileApp + page(model, ui) |> html + end + port = tryparse(Int, get(ENV, "STIPPLE_PRECOMPILE_PORT", "")) + port === nothing && (port = rand(8081:8999)) + up(port) + + precompile_get = tryparse(Bool, get(ENV, "STIPPLE_PRECOMPILE_GET", "1")) + precompile_get === true && HTTP.get("http://localhost:$port") + # The following lines (still) produce an error although + # they pass at the repl. Not very important though. + # HTTP.get("http://localhost:$port$(Genie.Assets.asset_path(Genie.assets_config, :js, file = "channels"))") + # HTTP.get("http://localhost:$port$(Genie.Assets.asset_path(assets_config, :js, file = "stipplecore"))") + down() + end + PRECOMPILE[] = false +end + end diff --git a/src/stipple/jsintegration.jl b/src/stipple/jsintegration.jl index ae3559d8..a411afe8 100644 --- a/src/stipple/jsintegration.jl +++ b/src/stipple/jsintegration.jl @@ -5,14 +5,14 @@ Checks whether the string is a valid js function and returns a `Dict` from which in the backend can construct a function. """ function parse_jsfunction(s::AbstractString) - # look for classical function definition - m = match( r"function\s*\(([^)]*)\)\s*{(.*)}"s, s) + # look for classical function definition (not a full syntax check, though) + m = match( r"^\s*function\s*\(([^)]*)\)\s*{(.*)}\s*$"s, s) !isnothing(m) && length(m.captures) == 2 && return opts(arguments=m[1], body=m[2]) - - # look for pure function definition - m = match( r"\s*\(?([^=)]*?)\)?\s*=>\s*({*.*?}*)\s*$"s , s ) + + # look for pure function definition including unbracketed single parameter + m = match( r"^\s*\(?([^=<>:;.(){}\[\]]*?)\)?\s*=>\s*({*.*?}*)\s*$"s , s ) (isnothing(m) || length(m.captures) != 2) && return nothing - + # if pure function body is without curly brackets, add a `return`, otherwise strip the brackets # Note: for utf-8 strings m[2][2:end-1] will fail if the string ends with a wide character, e.g. ϕ body = startswith(m[2], "{") ? m[2][2:prevind(m[2], lastindex(m[2]))] : "return " * m[2] diff --git a/src/stipple/parsers.jl b/src/stipple/parsers.jl index e7bd5792..c460ff67 100644 --- a/src/stipple/parsers.jl +++ b/src/stipple/parsers.jl @@ -53,4 +53,14 @@ end #String to AbstractFloat function stipple_parse(::Type{T}, value::String) where T<:AbstractFloat Base.parse(T, value) +end + +# Union with Nothing +function stipple_parse(::Type{Union{Nothing, T}}, ::Nothing) where T + nothing +end + +# Union with Nothing +function stipple_parse(::Type{Union{Nothing, T}}, value) where T + stipple_parse(T, value) end \ No newline at end of file diff --git a/src/stipple/reactivity.jl b/src/stipple/reactivity.jl index 70f61e4b..6cb377f7 100644 --- a/src/stipple/reactivity.jl +++ b/src/stipple/reactivity.jl @@ -211,9 +211,9 @@ function model_to_storage(::Type{T}, prefix = "", postfix = "") where T# <: Reac storage end -function merge_storage(storage_1::AbstractDict, storage_2::AbstractDict; keep_channel = true) - m1 = eval(haskey(storage_1, :modes__) ? storage_1[:modes__].args[end] : LittleDict{Symbol, Int}()) - m2 = eval(haskey(storage_2, :modes__) ? storage_2[:modes__].args[end] : LittleDict{Symbol, Int}()) +function merge_storage(storage_1::AbstractDict, storage_2::AbstractDict; keep_channel = true, context::Module) + m1 = haskey(storage_1, :modes__) ? Core.eval(context, storage_1[:modes__].args[end]) : LittleDict{Symbol, Int}() + m2 = haskey(storage_2, :modes__) ? Core.eval(context, storage_2[:modes__].args[end]) : LittleDict{Symbol, Int}() modes = merge(m1, m2) keep_channel && haskey(storage_2, :channel__) && (storage_2 = delete!(copy(storage_2), :channel__)) @@ -375,7 +375,7 @@ macro var_storage(expr, new_inputmode = :auto) end mixin_storage = @eval __module__ Stipple.model_to_storage($(e.args[2]), $prefix, $postfix) - storage = merge_storage(storage, mixin_storage) + storage = merge_storage(storage, mixin_storage; context = __module__) end :modes__, e end @@ -482,7 +482,7 @@ macro add_vars(modelname, expr, new_inputmode = :auto) storage = @eval(__module__, Stipple.@var_storage($expr, $new_inputmode)) new_storage = if isdefined(__module__, modelname) old_storage = @eval(__module__, Stipple.model_to_storage($modelname)) - ReactiveTools.merge_storage(old_storage, storage) + ReactiveTools.merge_storage(old_storage, storage; context = __module__) else storage end diff --git a/src/stipple/rendering.jl b/src/stipple/rendering.jl index 9eedea3a..a5a31d34 100644 --- a/src/stipple/rendering.jl +++ b/src/stipple/rendering.jl @@ -1,3 +1,11 @@ +""" + js_print(io::IO, x) + +Stipple internal print routine for join_js. +Mainly used to allow for printing of JSON.JSONText +""" +js_print(io::IO, x) = print(io, x) + """ join_js(xx, delim = ""; skip_empty = true, pre::Function = identity, strip_delimiter = true, pre_delim::Union{Function,Nothing} = nothing) @@ -42,8 +50,10 @@ function join_js(xx, delim = ""; skip_empty = true, pre::Function = identity, st if x isa Union{AbstractDict, Pair, Base.Iterators.Pairs, Vector{<:Pair}} s = json(Dict(k => JSONText(v) for (k, v) in (x isa Pair ? [x] : x)))[2:end - 1] print(io2, s) + elseif x isa JSONText + print(io2, x.s) else - print(io2, x) + js_print(io2, x) end s = String(take!(io2)) hasdelimiter = strip_delimiter && endswith(s, delim)