diff --git a/Project.toml b/Project.toml index df379bd..b078770 100644 --- a/Project.toml +++ b/Project.toml @@ -9,6 +9,7 @@ InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" PackageExtensionCompat = "65ce6f38-6b18-4e1d-a461-8949797d7930" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +SimpleBufferStream = "777ac1f9-54b0-4bf8-805c-2214025038e7" TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" [weakdeps] @@ -28,5 +29,6 @@ CodeTracking = "1.3" InteractiveUtils = "1.6" MacroTools = "0.5" PackageExtensionCompat = "1" +SimpleBufferStream = "1" TOML = "1" julia = "1.6" diff --git a/docs/src/index.md b/docs/src/index.md index 334676c..905d9ce 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -5,7 +5,8 @@ _Hypertext templating DSL for Julia_ This package provides a collection of macros for building and rendering HTML from Julia code using all the normal control flow syntax of the language, such as loops and conditional. No intermediate "virtual" DOM is constructed during -rendering process, which reduces memory allocations. +rendering process, which reduces memory allocations. Supports streaming renders +of templates via a `StreamingRender` iterator. When rendered in "development" mode source locations of all elements within the rendered within the DOM are preserved, which allows for lookup from the browser @@ -173,6 +174,23 @@ means that any keyword props provided in the component definition, such as `prop` above can be interpolated into the markdown file and will be rendered into the final HTML output that the component generates. +## `StreamingRender` + +A `StreamingRender` is an iterator that handles asynchronous execution of +`@render` calls. This is useful if your `@component` potentially takes a long +time to render completely and you wish to begin streaming the HTML to the +browser as it becomes available. + +```julia +for bytes in StreamingRender(io -> @render io @slow_component {args...}) + write(http_stream, bytes) +end +``` + +`do`-block syntax is also, naturally, supported by the `StreamingRender` +constructor. All `@component` definitions support streaming out-of-the-box. Be +aware that rendering happens in a `Threads.@spawn`ed task. + ## Docstrings ```@autodocs diff --git a/src/HypertextTemplates.jl b/src/HypertextTemplates.jl index fb31a59..a754c5a 100644 --- a/src/HypertextTemplates.jl +++ b/src/HypertextTemplates.jl @@ -5,6 +5,7 @@ module HypertextTemplates import CodeTracking import PackageExtensionCompat import MacroTools +import SimpleBufferStream # Exports: @@ -18,6 +19,7 @@ export @esc_str export @render export @text export SafeString +export StreamingRender # Includes: @@ -36,6 +38,7 @@ include("deftag.jl") include("Elements.jl") include("template-source-lookup.jl") include("render.jl") +include("stream.jl") include("cmfile.jl") # Initialization: diff --git a/src/stream.jl b/src/stream.jl new file mode 100644 index 0000000..22aca85 --- /dev/null +++ b/src/stream.jl @@ -0,0 +1,40 @@ +""" + StreamingRender(func) + +An iterable that will run the render function `func`, which takes a single `io` +argument that must be passed to the `@render` macro call. + +```julia +for bytes in StreamingRender(io -> @render io @component {args...}) + write(http_stream, bytes) +end +``` + +Or use a `do` block rather than `->` syntax. +""" +struct StreamingRender + io::SimpleBufferStream.BufferStream + task::Task + + function StreamingRender(f) + io = SimpleBufferStream.BufferStream() + task = Threads.@spawn begin + f(io) + close(io) + end + return new(io, task) + end +end + +function Base.iterate(s::StreamingRender, state = nothing) + bytes = readavailable(s.io) + if isempty(bytes) + wait(s.task) + return nothing + else + return bytes, nothing + end +end +Base.eltype(::Type{StreamingRender}) = Vector{UInt8} +Base.IteratorSize(::Type{StreamingRender}) = Base.SizeUnknown() + diff --git a/test/runtests.jl b/test/runtests.jl index 2162013..fd338e0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -68,6 +68,17 @@ end @cm_component markdown_component(; x) = joinpath(@__DIR__, "markdown.md") @deftag macro markdown_component end +@component function streaming(; n::Integer) + @div {class = "streamed"} begin + @ul begin + for id = 1:n + @li {id} "This is item $id." + end + end + end +end +@deftag macro streaming end + @testset "HypertestTemplates" begin @testset "Basics" begin render_test("references/basics/html-elements.txt") do io @@ -184,4 +195,14 @@ end result = @render @p "content" @test contains(result, "data-htloc=\"$file:$(line + 2)\"") end + @testset "Streaming" begin + func(io = Vector{UInt8}) = @render io @streaming {n = 10000} + output = UInt8[] + for bytes in StreamingRender(func) + @assert !isempty(bytes) + append!(output, bytes) + end + @test length(output) > 1 + @test output == func() + end end