diff --git a/ext/HypertextTemplatesCommonMarkExt.jl b/ext/HypertextTemplatesCommonMarkExt.jl
index a9afc0f..df50de9 100644
--- a/ext/HypertextTemplatesCommonMarkExt.jl
+++ b/ext/HypertextTemplatesCommonMarkExt.jl
@@ -28,10 +28,19 @@ function HypertextTemplates._handle_commonmark_ext(
props = join(get(Vector{String}, frontmatter, "props"), " ")
props = isempty(props) ? props : " $props"
- str = "$(CommonMark.html(ast))"
+ str = "$(html_str(ast))"
return HypertextTemplates._components_from_str(str, file, mod)
end
-function format_prop(k) end
+# Custom wrapper around CommonMark's HTML writer so that we can enable sourcepos
+# which allows us to use goto definition in browsers when dev mode is enabled.
+function html_str(ast::CommonMark.Node)
+ html = CommonMark.HTML(; sourcepos = true)
+ io = IOBuffer()
+ env = Dict{String,Any}()
+ w = CommonMark.Writer(html, io, env)
+ CommonMark.write_html(w, ast)
+ return String(take!(io))
+end
end
diff --git a/ext/HypertextTemplatesReviseExt.jl b/ext/HypertextTemplatesReviseExt.jl
index 22c8dd6..faa9570 100644
--- a/ext/HypertextTemplatesReviseExt.jl
+++ b/ext/HypertextTemplatesReviseExt.jl
@@ -24,7 +24,10 @@ function HypertextTemplates.is_stale_template(file::AbstractString, previous_mti
end
function HypertextTemplates._data_filename_attr(file::String, line::Int)
- if HypertextTemplates._DATA_FILENAME_ATTR[]
+ # Skip the `data-htloc` attribute if the line is invalid. The way that the
+ # browser lookup handles this is that it will look to the parent element for
+ # the `data-htloc` attribute if it doesn't exist on the current element.
+ if HypertextTemplates._DATA_FILENAME_ATTR[] && line > 0
filekey = HypertextTemplates._register_filename_mapping!(file)
return [Symbol("data-htloc") => "$filekey:$line"]
else
diff --git a/src/HypertextTemplates.jl b/src/HypertextTemplates.jl
index 74124e3..c59afdc 100644
--- a/src/HypertextTemplates.jl
+++ b/src/HypertextTemplates.jl
@@ -16,7 +16,8 @@ end
# Exports.
-export @template_str, @custom_element, @create_template_str, render, TemplateFileLookup
+export @template_str,
+ @custom_element, @create_template_str, render, TemplateFileLookup, slots
# Constants.
diff --git a/src/nodes/abstract.jl b/src/nodes/abstract.jl
index f2a832e..59d1b1f 100644
--- a/src/nodes/abstract.jl
+++ b/src/nodes/abstract.jl
@@ -2,26 +2,26 @@ const FALLBACK_TAG = "fallback"
abstract type AbstractNode end
-transform(ns::Vector{EzXML.Node}) = [transform(n) for n in ns]
+transform(ctx, ns::Vector{EzXML.Node}) = [transform(ctx, n) for n in ns]
-function transform(n::EzXML.Node)
+function transform(ctx, n::EzXML.Node)
if EzXML.istext(n)
- return Text(n)
+ return Text(ctx, n)
else
if EzXML.iselement(n)
tag = EzXML.nodename(n)
if tag == FOR_TAG
- return For(n)
+ return For(ctx, n)
elseif tag == JULIA_TAG
- return Julia(n)
+ return Julia(ctx, n)
elseif tag == SHOW_TAG
- return Show(n)
+ return Show(ctx, n)
elseif tag == MATCH_TAG
- return Match(n)
+ return Match(ctx, n)
elseif tag == SLOT_TAG
- return Slot(n)
+ return Slot(ctx, n)
else
- return Element(n)
+ return Element(ctx, n)
end
else
return Text(cdata(n), 0)
diff --git a/src/nodes/element.jl b/src/nodes/element.jl
index e62bb31..4620bcc 100644
--- a/src/nodes/element.jl
+++ b/src/nodes/element.jl
@@ -43,20 +43,47 @@ struct Element <: AbstractNode
slots::Vector{Pair{String,Vector{AbstractNode}}}
line::Int
- function Element(name, attributes, body, slots, line)
+ function Element(ctx, name, attributes, body, slots, line)
+ attributes, line = _translate_data_sourcepos(ctx, attributes, line)
return new(_restore_special_symbols(name), attributes, body, slots, line)
end
end
-function Element(n::EzXML.Node)
+# CommonMark HTML output stores a `data-sourcepos` attribute on each element
+# to track source position. We use a slightly different attribute name in
+# HypertextTemplates and don't care about column information.
+function _translate_data_sourcepos(ctx, attributes::Vector, line::Integer)
+ attrs = Attribute[]
+ sourcepos = nothing
+ for each in attributes
+ if each.name == "data-sourcepos"
+ sourcepos = each.value
+ else
+ push!(attrs, each)
+ end
+ end
+ if isnothing(sourcepos)
+ return attrs, ctx.markdown ? 0 : line
+ else
+ m = match(r"(\d+):(\d+)-(\d+):(\d+)", sourcepos)
+ if isnothing(m)
+ return attrs, 0
+ else
+ line = something(tryparse(Int, m.captures[1]), 0)
+ return attrs, line
+ end
+ end
+end
+
+function Element(ctx, n::EzXML.Node)
name = EzXML.nodename(n)
if name in RESERVED_ELEMENT_NAMES
error("elements cannot be named after reserved node names: $name")
end
attrs = attributes(n)
if name in VALID_HTML_ELEMENTS
- body = transform(EzXML.nodes(n))
- return Element(name, Attribute.(attrs), body, [], nodeline(n))
+ body = transform(ctx, EzXML.nodes(n))
+ return Element(ctx, name, Attribute.(attrs), body, [], nodeline(n))
else
slots = []
nodes = EzXML.nodes(n)
@@ -66,15 +93,22 @@ function Element(n::EzXML.Node)
if contains(tag, ':')
tag, slot = split(tag, ':'; limit = 2)
child =
- Element(tag, [], transform(EzXML.nodes(each)), [], nodeline(each))
+ Element(
+ ctx,
+ tag,
+ [],
+ transform(ctx, EzXML.nodes(each)),
+ [],
+ nodeline(each),
+ )
push!(slots, slot => [child])
end
end
end
if isempty(slots)
- push!(slots, UNNAMED_SLOT => transform(nodes))
+ push!(slots, UNNAMED_SLOT => transform(ctx, nodes))
end
- return Element(name, Attribute.(attrs), [], slots, nodeline(n))
+ return Element(ctx, name, Attribute.(attrs), [], slots, nodeline(n))
end
end
diff --git a/src/nodes/for.jl b/src/nodes/for.jl
index 49768ec..2e38b20 100644
--- a/src/nodes/for.jl
+++ b/src/nodes/for.jl
@@ -18,7 +18,7 @@ struct For <: AbstractNode
end
end
-function For(n::EzXML.Node)
+function For(ctx, n::EzXML.Node)
tag = EzXML.nodename(n)
if tag == FOR_TAG
attrs = Dict(attributes(n))
@@ -27,7 +27,7 @@ function For(n::EzXML.Node)
iter = key_default(attrs, "iter")
item = key_default(attrs, "item")
index = key_default(attrs, "index")
- return For(iter, item, index, transform(EzXML.nodes(n)), nodeline(n))
+ return For(iter, item, index, transform(ctx, EzXML.nodes(n)), nodeline(n))
else
error("expected a '' tag, found: $tag")
end
diff --git a/src/nodes/function.jl b/src/nodes/function.jl
index d9be72f..dda89ea 100644
--- a/src/nodes/function.jl
+++ b/src/nodes/function.jl
@@ -259,6 +259,7 @@ struct TemplateFunction
end
function TemplateFunction(n::EzXML.Node, file::String, mod::Module)
+ ctx = (; markdown = endswith(file, ".md"))
if isabspath(file)
if EzXML.iselement(n)
tag = EzXML.nodename(n)
@@ -271,7 +272,7 @@ function TemplateFunction(n::EzXML.Node, file::String, mod::Module)
name,
false,
Prop.(props),
- transform(EzXML.nodes(n)),
+ transform(ctx, EzXML.nodes(n)),
file,
mod,
nodeline(n),
@@ -290,7 +291,7 @@ function TemplateFunction(n::EzXML.Node, file::String, mod::Module)
name,
true,
[],
- transform(EzXML.nodes(n)),
+ transform(ctx, EzXML.nodes(n)),
file,
mod,
nodeline(n),
@@ -374,6 +375,9 @@ function expression(c::TemplateFunction)::Expr
end
return nothing
end
+ function $(name)(default_slots::NamedTuple = (;); default_props...)
+ return $(delay_io)($(name), default_slots, default_props)
+ end
end
else
props = expression(context, c.props)
@@ -402,6 +406,9 @@ function expression(c::TemplateFunction)::Expr
end
return nothing
end
+ function $(name)(default_slots::NamedTuple = (;); default_props...)
+ return $(delay_io)($(name), default_slots, default_props)
+ end
end
end
return expr |> lln_replacer(c.file, c.line)
@@ -417,3 +424,9 @@ function _recompile_template(mod::Module, file::String, mtime::Float64)
return false
end
end
+
+function delay_io(name, default_slots, default_props)
+ return function (io::IO, slots::NamedTuple = (;); props...)
+ name(io, merge(default_slots, slots); default_props..., props...)
+ end
+end
diff --git a/src/nodes/julia.jl b/src/nodes/julia.jl
index 30b7f9c..e06af84 100644
--- a/src/nodes/julia.jl
+++ b/src/nodes/julia.jl
@@ -9,7 +9,7 @@ struct Julia <: AbstractNode
end
end
-function Julia(n::EzXML.Node)
+function Julia(ctx, n::EzXML.Node)
attrs = attributes(n)
if length(attrs) == 1
(name, value), = attrs
diff --git a/src/nodes/match.jl b/src/nodes/match.jl
index b05974d..34b2cf7 100644
--- a/src/nodes/match.jl
+++ b/src/nodes/match.jl
@@ -20,7 +20,7 @@ struct Match <: AbstractNode
cases::Vector{Case}
line::Int
- function Match(n::EzXML.Node)
+ function Match(ctx, n::EzXML.Node)
tag = EzXML.nodename(n)
if tag == MATCH_TAG
nodes, fallback = split_fallback(n)
@@ -32,7 +32,7 @@ struct Match <: AbstractNode
attrs = Dict(attributes(each))
if length(attrs) == 1
when = key_default(attrs, "when")
- body = transform(EzXML.nodes(each))
+ body = transform(ctx, EzXML.nodes(each))
push!(cases, Case(when, body, nodeline(each)))
else
error("'match' nodes require a single attribute.")
diff --git a/src/nodes/show.jl b/src/nodes/show.jl
index ee72953..06f5095 100644
--- a/src/nodes/show.jl
+++ b/src/nodes/show.jl
@@ -11,7 +11,7 @@ struct Show <: AbstractNode
end
end
-function Show(n::EzXML.Node)
+function Show(ctx, n::EzXML.Node)
tag = EzXML.nodename(n)
if tag == SHOW_TAG
attrs = Dict(attributes(n))
@@ -20,8 +20,8 @@ function Show(n::EzXML.Node)
nodes, fallback = split_fallback(n)
return Show(
when,
- transform(nodes),
- isnothing(fallback) ? [] : transform(EzXML.nodes(fallback)),
+ transform(ctx, nodes),
+ isnothing(fallback) ? [] : transform(ctx, EzXML.nodes(fallback)),
nodeline(n),
)
else
diff --git a/src/nodes/slot.jl b/src/nodes/slot.jl
index 7823c60..de1d0de 100644
--- a/src/nodes/slot.jl
+++ b/src/nodes/slot.jl
@@ -10,7 +10,7 @@ struct Slot <: AbstractNode
end
end
-function Slot(n::EzXML.Node)
+function Slot(ctx, n::EzXML.Node)
attrs = attributes(n)
if isempty(attrs)
return Slot(nothing, nodeline(n))
@@ -36,3 +36,27 @@ function expression(c::BuilderContext, s::Slot)
:($(c.slots).$(name)($(c.io)))
end |> lln_replacer(c.file, s.line)
end
+
+"""
+ slots([unnamed]; named...)
+
+Helper function to construct `NamedTuple`s to pass to templates as their slots.
+
+This function is useful when composing template functions within Julia code
+rather than within template files templates. For example when rendering a
+markdown template with a specific wrapper layout.
+
+```julia
+base_layout(stdout, slots(markdown(; title = "My Title")))
+```
+
+The above does several things:
+
+ - Delays the rendering of the `markdown` template until the `base_layout`
+ template requests rendering within it's unnamed slot.
+ - Creates the `slots` object to assign the delayed `markdown` template to the
+ unnamed slot.
+ - Renders the `base_layout` template to `stdout` with the `slots` object, which
+ will render the `markdown` template within the unnamed slot.
+"""
+slots(unnamed = (io) -> nothing; named...) = (; named..., Symbol(UNNAMED_SLOT) => unnamed)
diff --git a/src/nodes/text.jl b/src/nodes/text.jl
index b601942..c22992e 100644
--- a/src/nodes/text.jl
+++ b/src/nodes/text.jl
@@ -7,7 +7,7 @@ struct Text <: AbstractNode
end
end
-function Text(n::EzXML.Node)
+function Text(ctx, n::EzXML.Node)
if EzXML.istext(n)
content = EzXML.nodecontent(n)
diff --git a/test/runtests.jl b/test/runtests.jl
index 60f571c..7a617a9 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -332,5 +332,28 @@ end
@test contains(html, "$(mapping["button.html"]):2")
@test contains(html, "$(mapping["dropdown.html"]):2")
@test contains(html, "$(mapping["dropdown.html"]):3")
+
+ html = render(Templates.Markdown.var"custom-markdown-name"; prop = "prop-value")
+ @test contains(html, "data-htloc")
+
+ mapping = Dict(
+ basename(file) => line for
+ (file, line) in HypertextTemplates.DATA_FILENAME_MAPPING
+ )
+ @test contains(html, "$(mapping["markdown.md"]):8")
+ @test contains(html, "$(mapping["markdown.md"]):10")
+ @test contains(html, "$(mapping["markdown.md"]):12")
+
+ HypertextTemplates._DATA_FILENAME_ATTR[] = false
+ end
+
+ @testset "composed templates" begin
+ template = Templates.Complex.var"base-layout"(
+ slots(Templates.Markdown.var"custom-markdown-name"(; prop = "prop-value"));
+ title = "title",
+ )
+ html = render(template)
+ @test contains(html, "