Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add StreamingRender iterator to support streaming template renders #29

Merged
merged 1 commit into from
Dec 6, 2024
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
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -28,5 +29,6 @@ CodeTracking = "1.3"
InteractiveUtils = "1.6"
MacroTools = "0.5"
PackageExtensionCompat = "1"
SimpleBufferStream = "1"
TOML = "1"
julia = "1.6"
20 changes: 19 additions & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/HypertextTemplates.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module HypertextTemplates
import CodeTracking
import PackageExtensionCompat
import MacroTools
import SimpleBufferStream

# Exports:

Expand All @@ -18,6 +19,7 @@ export @esc_str
export @render
export @text
export SafeString
export StreamingRender

# Includes:

Expand All @@ -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:
Expand Down
40 changes: 40 additions & 0 deletions src/stream.jl
Original file line number Diff line number Diff line change
@@ -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()

21 changes: 21 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Loading