Skip to content

Commit

Permalink
Thread correct source file information through in markdown files
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelHatherly committed Dec 20, 2023
1 parent 35fce30 commit 7b6af80
Show file tree
Hide file tree
Showing 13 changed files with 139 additions and 32 deletions.
13 changes: 11 additions & 2 deletions ext/HypertextTemplatesCommonMarkExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,19 @@ function HypertextTemplates._handle_commonmark_ext(
props = join(get(Vector{String}, frontmatter, "props"), " ")
props = isempty(props) ? props : " $props"

str = "<function $name$props>$(CommonMark.html(ast))</function>"
str = "<function $name$props>$(html_str(ast))</function>"
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
5 changes: 4 additions & 1 deletion ext/HypertextTemplatesReviseExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/HypertextTemplates.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
18 changes: 9 additions & 9 deletions src/nodes/abstract.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
48 changes: 41 additions & 7 deletions src/nodes/element.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions src/nodes/for.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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 '<for>' tag, found: $tag")
end
Expand Down
17 changes: 15 additions & 2 deletions src/nodes/function.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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),
Expand All @@ -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),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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
2 changes: 1 addition & 1 deletion src/nodes/julia.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/nodes/match.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.")
Expand Down
6 changes: 3 additions & 3 deletions src/nodes/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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
Expand Down
26 changes: 25 additions & 1 deletion src/nodes/slot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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)
2 changes: 1 addition & 1 deletion src/nodes/text.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
23 changes: 23 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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, "<!DOCTYPE")
@test contains(html, "language-julia")
end
end

0 comments on commit 7b6af80

Please sign in to comment.