diff --git a/Project.toml b/Project.toml index 11428fc02..e0f11ec95 100755 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Genie" uuid = "c43c736e-a2d1-11e8-161f-af95117fbd1e" authors = ["Adrian Salceanu "] -version = "0.21.0" +version = "0.22.0" [deps] ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63" diff --git a/docs/guides/Working_With_Genie_Apps.md b/docs/guides/Working_With_Genie_Apps.md index 513a000e4..6db983516 100644 --- a/docs/guides/Working_With_Genie_Apps.md +++ b/docs/guides/Working_With_Genie_Apps.md @@ -1,7 +1,7 @@ # Working with Genie apps (projects) Working with Genie in an interactive environment can be useful – but usually we want to persist the application and reuse it between sessions. -One way to achieve that is to save it as an IJulia notebook and rerun the cells. However, you can get the best of Genie by working with Genie apps. +One way to achieve this is to save it as an IJulia notebook and rerun the cells. However, you can get the best of Genie by working with Genie apps. A Genie app is an MVC web application which promotes the convention-over-configuration principle. By working with a few predefined files, within the Genie app structure, the framework can lift a lot of weight and massively improve development productivity. But following Genie's workflow, one instantly gets, out of the box, features like automatic module loading and reloading, dedicated configuration files, logging, support for environments, code generators, caching, support for Genie plugins, and more. In order to create a new Genie app, all we need to do is run `Genie.newapp($app_name)`: diff --git a/src/Configuration.jl b/src/Configuration.jl index 054d73da5..a15c1b90d 100755 --- a/src/Configuration.jl +++ b/src/Configuration.jl @@ -3,7 +3,7 @@ Core genie configuration / settings functionality. """ module Configuration -const GENIE_VERSION = v"0.21.0" +const GENIE_VERSION = v"0.22.0" import Logging import Genie diff --git a/src/Flax.jl b/src/Flax.jl index 9b906b9cc..d73fac261 100755 --- a/src/Flax.jl +++ b/src/Flax.jl @@ -23,6 +23,9 @@ using .HTMLRenderer include("JSONRenderer.jl") using .JSONRenderer +include("JSRenderer.jl") +using .JSRenderer + const Html = @__MODULE__ const BUILD_NAME = "FlaxViews" @@ -53,30 +56,6 @@ function partial(path::String; context::Module = @__MODULE__, vars...) :: String end -""" - parseview(data::String; partial = false, context::Module = @__MODULE__) :: Function - -Parses a view file, returning a rendering function. If necessary, the function is JIT-compiled, persisted and loaded into memory. -""" -function parseview(data::String; partial = false, context::Module = @__MODULE__) :: Function - data_hash = hash(data) - path = "Flax_" * string(data_hash) - - func_name = function_name(string(data_hash, partial)) |> Symbol - mod_name = m_name(string(path, partial)) * ".jl" - f_path = joinpath(Genie.config.path_build, BUILD_NAME, mod_name) - f_stale = build_is_stale(f_path, f_path) - - if f_stale || ! isdefined(context, func_name) - f_stale && build_module(string_to_flax(data, partial = partial), path, mod_name) - - return Base.include(context, joinpath(Genie.config.path_build, BUILD_NAME, mod_name)) - end - - getfield(context, func_name) -end - - """ template(path::String; partial::Bool = true, context::Module = @__MODULE__) :: String @@ -442,4 +421,27 @@ function changebuilds(subfolder = BUILD_NAME) :: Bool preparebuilds() end + +""" + view_file_info(path::String, supported_extensions = SUPPORTED_HTML_OUTPUT_FILE_FORMATS) :: Tuple{String,String} + +Extracts path and extension info about a file +""" +function view_file_info(path::String, supported_extensions::Vector{String} = HTMLRenderer.SUPPORTED_HTML_OUTPUT_FILE_FORMATS) :: Tuple{String,String} + _path, _extension = "", "" + + if isfile(path) + _path, _extension = relpath(path), "." * split(path, ".", limit = 2)[end] + else + for file_extension in supported_extensions + if isfile(path * file_extension) + _path, _extension = path * file_extension, file_extension + break + end + end + end + + _path, _extension +end + end \ No newline at end of file diff --git a/src/HTMLRenderer.jl b/src/HTMLRenderer.jl index 06fc7a546..130f1bd40 100644 --- a/src/HTMLRenderer.jl +++ b/src/HTMLRenderer.jl @@ -106,10 +106,6 @@ function attributes(attrs::Vector{Pair{Symbol,Any}} = Vector{Pair{Symbol,Any}}() a = IOBuffer() for (k,v) in attrs - sk = string(k) - # startswith(sk, "_") && (k = sk = sk[2:end]) - # k = replace(sk, "_"=>"-") - print(a, "$(k)=\"$(v)\" ") end @@ -192,7 +188,7 @@ Resolves the inclusion and rendering of a template file function get_template(path::String; partial::Bool = true, context::Module = @__MODULE__) :: Function orig_path = path - path, extension = view_file_info(path) + path, extension = Flax.view_file_info(path) isfile(path) || error("Template file \"$orig_path\" with extensions $SUPPORTED_HTML_OUTPUT_FILE_FORMATS does not exist") @@ -240,25 +236,26 @@ end """ - view_file_info(path::String) :: Tuple{String,String} + parseview(data::String; partial = false, context::Module = @__MODULE__) :: Function -Extracts path and extension info about a file +Parses a view file, returning a rendering function. If necessary, the function is JIT-compiled, persisted and loaded into memory. """ -function view_file_info(path::String) :: Tuple{String,String} - _path, _extension = "", "" +function parseview(data::String; partial = false, context::Module = @__MODULE__) :: Function + data_hash = hash(data) + path = "Flax_" * string(data_hash) - if isfile(path) - _path, _extension = relpath(path), "." * split(path, ".", limit = 2)[end] - else - for file_extension in SUPPORTED_HTML_OUTPUT_FILE_FORMATS - if isfile(path * file_extension) - _path, _extension = path * file_extension, file_extension - break - end - end + func_name = Flax.function_name(string(data_hash, partial)) |> Symbol + mod_name = Flax.m_name(string(path, partial)) * ".jl" + f_path = joinpath(Genie.config.path_build, Flax.BUILD_NAME, mod_name) + f_stale = Flax.build_is_stale(f_path, f_path) + + if f_stale || ! isdefined(context, func_name) + f_stale && Flax.build_module(Flax.string_to_flax(data, partial = partial), path, mod_name) + + return Base.include(context, joinpath(Genie.config.path_build, Flax.BUILD_NAME, mod_name)) end - _path, _extension + getfield(context, func_name) end @@ -268,10 +265,10 @@ function render(data::String; context::Module = @__MODULE__, layout::Union{Strin Flax.registervars(vars...) if layout !== nothing - task_local_storage(:__yield, Flax.parseview(data, partial = true, context = context)) - Flax.parseview(layout, partial = false, context = context) + task_local_storage(:__yield, parseview(data, partial = true, context = context)) + parseview(layout, partial = false, context = context) else - Flax.parseview(data, partial = false, context = context) + parseview(data, partial = false, context = context) end end diff --git a/src/JSRenderer.jl b/src/JSRenderer.jl new file mode 100644 index 000000000..177eda077 --- /dev/null +++ b/src/JSRenderer.jl @@ -0,0 +1,89 @@ +module JSRenderer + + +import Revise +import Logging, FilePaths +using Genie, Genie.Flax + + +const JS_FILE_EXT = ["js.jl"] +const TEMPLATE_EXT = [".flax.js", ".jl.js"] + +const SUPPORTED_JS_OUTPUT_FILE_FORMATS = TEMPLATE_EXT + +const JSString = String + +const NBSP_REPLACEMENT = (" "=>"!!nbsp;;") + +export JSString + + +""" +""" +function get_template(path::String; context::Module = @__MODULE__) :: Function + orig_path = path + + path, extension = Flax.view_file_info(path, SUPPORTED_JS_OUTPUT_FILE_FORMATS) + + isfile(path) || error("JS file \"$orig_path\" with extensions $SUPPORTED_JS_OUTPUT_FILE_FORMATS does not exist") + + extension in JS_FILE_EXT && return (() -> Base.include(context, path)) + + f_name = Flax.function_name(path) |> Symbol + mod_name = Flax.m_name(path) * ".jl" + f_path = joinpath(Genie.config.path_build, Flax.BUILD_NAME, mod_name) + f_stale = Flax.build_is_stale(path, f_path) + + if f_stale || ! isdefined(context, func_name) + f_stale && Flax.build_module(Flax.to_js(data), path, mod_name) + + return Base.include(context, joinpath(Genie.config.path_build, Flax.BUILD_NAME, mod_name)) + end + + getfield(context, f_name) +end + + +""" +""" +@inline function to_js(data::String; prepend = "\n") :: String + string("function $(Flax.function_name(data))() \n", + Flax.injectvars(), + prepend, + "\"\"\"$data\"\"\"", + "\nend \n") +end + + +""" +""" +function render(data::String; context::Module = @__MODULE__, vars...) :: Function + Flax.registervars(vars...) + + data_hash = hash(data) + path = "Flax_" * string(data_hash) + + func_name = Flax.function_name(string(data_hash)) |> Symbol + mod_name = Flax.m_name(path) * ".jl" + f_path = joinpath(Genie.config.path_build, Flax.BUILD_NAME, mod_name) + f_stale = Flax.build_is_stale(f_path, f_path) + + if f_stale || ! isdefined(context, func_name) + f_stale && Flax.build_module(to_js(data), path, mod_name) + + return Base.include(context, joinpath(Genie.config.path_build, Flax.BUILD_NAME, mod_name)) + end + + getfield(context, func_name) +end + + +""" +""" +function render(viewfile::FilePaths.PosixPath; context::Module = @__MODULE__, vars...) :: Function + Flax.registervars(vars...) + + get_template(string(viewfile), partial = false, context = context) +end + +end \ No newline at end of file diff --git a/src/Renderer.jl b/src/Renderer.jl index ba51bae51..c63873a98 100755 --- a/src/Renderer.jl +++ b/src/Renderer.jl @@ -1,6 +1,6 @@ module Renderer -export respond, html, json, redirect +export respond, html, json, redirect, js import Revise import HTTP, Reexport, Markdown, Logging, FilePaths @@ -42,8 +42,9 @@ Collection of renderers associated to each supported mime time. Other mime-rende or current ones can be replaced by custom ones, to be used by Genie. """ const RENDERERS = Dict( - MIME"text/html" => Flax.HTMLRenderer, - MIME"application/json" => Flax.JSONRenderer + MIME"text/html" => Flax.HTMLRenderer, + MIME"application/json" => Flax.JSONRenderer, + MIME"application/javascript" => Flax.JSRenderer ) const ResourcePath = Union{String,Symbol} @@ -135,6 +136,15 @@ function WebRenderable(wr::WebRenderable, status::Int, headers::HTTPHeaders) end +function WebRenderable(wr::WebRenderable, content_type::Symbol, status::Int, headers::HTTPHeaders) + wr.content_type = content_type + wr.status = status + wr.headers = headers + + wr +end + + function render(::Type{MIME"text/html"}, data::String; context::Module = @__MODULE__, layout::Union{String,Nothing} = nothing, vars...) :: WebRenderable try @@ -144,6 +154,8 @@ function render(::Type{MIME"text/html"}, data::String; rethrow(ex) end end + + function render(::Type{MIME"text/html"}, viewfile::FilePath; layout::Union{Nothing,FilePath} = nothing, context::Module = @__MODULE__, vars...) :: WebRenderable try @@ -242,7 +254,7 @@ end """ function json(datafile::FilePath; context::Module = @__MODULE__, status::Int = 200, headers::HTTPHeaders = HTTPHeaders(), vars...) :: HTTP.Response - WebRenderable(render(MIME"application/json", datafile; context = context, vars...), status, headers) |> respond + WebRenderable(render(MIME"application/json", datafile; context = context, vars...), :json, status, headers) |> respond end @@ -250,14 +262,51 @@ end """ function json(data::String; context::Module = @__MODULE__, status::Int = 200, headers::HTTPHeaders = HTTPHeaders(), vars...) :: HTTP.Response - WebRenderable(render(MIME"application/json", data; context = context, vars...), status, headers) |> respond + WebRenderable(render(MIME"application/json", data; context = context, vars...), :json, status, headers) |> respond end """ """ function json(data; status::Int = 200, headers::HTTPHeaders = HTTPHeaders()) :: HTTP.Response - WebRenderable(render(MIME"application/json", data), status, headers) |> respond + WebRenderable(render(MIME"application/json", data), :json, status, headers) |> respond +end + + +### JS RENDERING + + +function render(::Type{MIME"application/javascript"}, data::String; context::Module = @__MODULE__, vars...) :: WebRenderable + try + WebRenderable(RENDERERS[MIME"application/javascript"].render(data; context = context, vars...) |> Base.invokelatest, :js) + catch ex + isa(ex, KeyError) && Flax.changebuilds() # it's a view error so don't reuse them + rethrow(ex) + end +end + + +function render(::Type{MIME"application/javascript"}, viewfile::FilePath; context::Module = @__MODULE__, vars...) :: WebRenderable + try + WebRenderable(RENDERERS[MIME"application/javascript"].render(viewfile; context = context, vars...) |> Base.invokelatest, :js) + catch ex + isa(ex, KeyError) && Flax.changebuilds() # it's a view error so don't reuse them + rethrow(ex) + end +end + + +""" +""" +function js(data::String; context::Module = @__MODULE__, status::Int = 200, headers::HTTPHeaders = HTTPHeaders(), vars...) :: HTTP.Response + WebRenderable(render(MIME"application/javascript", data; context = context, vars...), :js, status, headers) |> respond +end + + +""" +""" +function js(viewfile::FilePath; context::Module = @__MODULE__, status::Int = 200, headers::HTTPHeaders = HTTPHeaders(), vars...) :: HTTP.Response + WebRenderable(render(MIME"application/javascript", viewfile; context = context, vars...), :js, status, headers) |> respond end ### REDIRECT RESPONSES ### diff --git a/test/tests_advanced_html_rendering.jl b/test/tests_advanced_html_rendering.jl index 2bc2487e0..c4be3afa0 100644 --- a/test/tests_advanced_html_rendering.jl +++ b/test/tests_advanced_html_rendering.jl @@ -1,8 +1,3 @@ -# TODO - rendering of html elements with properties, slashes, quotes, no quotes, empty, etc -# TODO - multiple tags in if/else -# TODO @foreach, if, etc - - @safetestset "Advanced rendering" begin @safetestset "@foreach macro renders local variables" begin diff --git a/test/tests_flax_views_rendering.jl b/test/tests_flax_views_rendering.jl index e69de29bb..68c7e733b 100644 --- a/test/tests_flax_views_rendering.jl +++ b/test/tests_flax_views_rendering.jl @@ -0,0 +1,21 @@ +@safetestset "Flax rendering" begin + + @safetestset "Simple tag rendering" begin + using Genie + using Genie.Renderer + + copy = "Hello Genie" + + @test Html.p(copy) == "

$copy

" + @test Html.div() == "
" + @test Html.br() == "
" + + message = "Important message" + @test Html.span(message, class = "focus") == "$message" + @test Html.span(message, class = "focus"; NamedTuple{(Symbol("data-process"),)}(("pre-process",))...) == "Important message" + @test Html.span("Important message", class = "focus"; NamedTuple{(Symbol("data-process"),)}(("pre-process",))...) do + Html.a("Click here to read message") + end == "Click here to read message" + end; + +end; \ No newline at end of file diff --git a/test/tests_js_rendering.jl b/test/tests_js_rendering.jl new file mode 100644 index 000000000..fc4c03061 --- /dev/null +++ b/test/tests_js_rendering.jl @@ -0,0 +1,31 @@ +@safetestset "JS rendering" begin + + @safetestset "Plain JS rendering" begin + using Genie + using Genie.Renderer + + script = raw"var app = new Vue({el: '#app', data: { message: 'Hello Vue!' }})" + + r = js(script) + + @test String(r.body) == "var app = new Vue({el: '#app', data: { message: 'Hello Vue!' }})" + @test r.headers[1]["Content-Type"] == "application/javascript; charset=utf-8" + end; + + + @safetestset "Vars JS rendering" begin + using Genie + using Genie.Renderer + using Genie.Renderer.Flax.JSONRenderer.JSONParser + + data = JSON.json(("message" => "Hi Vue")) + + script = raw"var app = new Vue({el: '#app', data: $data})" + + r = js(script, data = data) + + @test String(r.body) == "var app = new Vue({el: '#app', data: {\"message\":\"Hi Vue\"}})" + @test r.headers[1]["Content-Type"] == "application/javascript; charset=utf-8" + end; + +end; \ No newline at end of file