Skip to content

Commit

Permalink
Support exact line numbers in template "goto" feature
Browse files Browse the repository at this point in the history
Also encode the file name as an integer counter rather than rendering the entire filename in each attribute to reduce the clutter in rendered HTML.
  • Loading branch information
MichaelHatherly committed Nov 1, 2023
1 parent 5956ca0 commit 00e22c9
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 30 deletions.
30 changes: 30 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,36 @@ Note that this does add a small amount of overhead. For production builds of
your code you should ensure that `Revise` is not part of your build. If you do
this then all templates will be run without checking for updated file contents.

## Template Lookup

During development you'll likely need to find to source file and line number of
particular parts of the rendered HTML. This is usually done by searching the
codebase for matching tag or attribute names with an editor's search feature.
This becomes tedious though, so `HypertextTemplates` provides a "template
lookup" feature that allows you to jump to the source file and line number of
any part of the rendered HTML by hovering over it in the browser and pressing
`Ctrl+Shift`.

To set this up you'll need to have `Revise` loaded in your session, as you
should if you're in a development setting. Doing this will automatically
annotate all rendered HTML with special `data-htloc` attributes that contain the
source file and line number of the template that rendered that part of the HTML.
Secondly, you'll need to add the `TemplateFileLookup` middleware to the `HTTP`
server that is serving your rendered HTML.

```julia
using HypertextTemplates
using HTTP

HTTP.serve(router |> TemplateFileLookup, host, port)
```

Now just hover your mouse over any part of your browser and press `Ctrl+Shift`.
This will open your editor at the correct file and line number. You'll need to
have set the `"EDITOR"` environment variable to your editor command in your
`ENV` in your `~/.julia/config/startup.jl` file for this to work. See the Julia
manual for more details on setting environment variables.

## Public Interface

```@autodocs
Expand Down
42 changes: 28 additions & 14 deletions ext/HypertextTemplatesHTTPReviseExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,37 @@ function HypertextTemplates._template_file_lookup(
# page that uses the same target, as unlikely as that may be.
target = "/template-file-lookup-$(Random.randstring())"
function (request::HTTP.Request)
if request.method == "POST" && request.target == target
filename = String(request.body)
if isfile(filename)
@debug "Opening $filename for editing."
InteractiveUtils.edit(filename)
return HTTP.Response(200, "Opened file in default editor.")
else
error("filename does not exist: $filename")
end
return _template_file_lookup_impl(handler, request, target)
end
end

function _template_file_lookup_impl(handler, request::HTTP.Request, target::String)
if request.method == "POST" && request.target == target
location = String(request.body)
file, line = split(location, ':'; limit = 2)

filekey = Base.parse(Int, file)
filename = get(HypertextTemplates.DATA_FILENAME_MAPPING_REVERSE, filekey, nothing)
isnothing(filename) && error("filename not found for key: $filekey")

linenumber = Base.parse(Int, line)
if isfile(filename)
@debug "Opening $filename at line $linenumber for editing."
InteractiveUtils.edit(filename, linenumber)
return HTTP.Response(
200,
"Opened file '$filename' at line '$linenumber' in default editor.",
)
else
return _insert_template_file_lookup_listener(handler(request), target)
error("filename does not exist: $filename")
end
else
return _insert_template_file_lookup_listener(handler(request), target)
end
end

# Injects a script into the response that listens for clicks on elements with
# the `data-filename` attribute and sends a POST request to the given address
# the `data-htloc` attribute and sends a POST request to the given address
# with the filename as the body. This is used to open the template file in the
# default editor when the user clicks on the rendered template.
function _insert_template_file_lookup_listener(
Expand All @@ -46,16 +60,16 @@ function _insert_template_file_lookup_listener(
window.addEventListener("mousemove", async function (event) {
window._lastMousePosition = { x: event.clientX, y: event.clientY };
});
// Listen for Ctrl+Shift clicks on elements with the `data-filename`
// Listen for Ctrl+Shift clicks on elements with the `data-htloc`
// attribute and send a POST request to the given address with the
// filename as the body.
window.addEventListener("keydown", async function (event) {
if (event.ctrlKey && event.shiftKey) {
const target = document.elementFromPoint(window._lastMousePosition.x, window._lastMousePosition.y);
// When we can't find the attribute on the clicked element, we
// look for it on the body element.
const node = target.closest("[data-filename]") || target.querySelector("body");
const filename = node.attributes["data-filename"].value;
const node = target.closest("[data-htloc]") || target.querySelector("body");
const filename = node.attributes["data-htloc"].value;
if (filename) {
fetch("$address", { method: "POST", body: filename });
} else {
Expand Down
5 changes: 3 additions & 2 deletions ext/HypertextTemplatesReviseExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ function HypertextTemplates.is_stale_template(file::AbstractString, previous_mti
end
end

function HypertextTemplates._data_filename_attr(file::String)
function HypertextTemplates._data_filename_attr(file::String, line::Int)
if HypertextTemplates._DATA_FILENAME_ATTR[]
return [Symbol("data-filename") => file]
filekey = HypertextTemplates._register_filename_mapping!(file)
return [Symbol("data-htloc") => "$filekey:$line"]
else
return Pair{Symbol,String}[]
end
Expand Down
18 changes: 18 additions & 0 deletions src/HypertextTemplates.jl
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,26 @@ const RESERVED_ELEMENT_NAMES = Set([
JULIA_TAG,
])

const DATA_FILENAME_MAPPING = Dict{String,Int}()
const DATA_FILENAME_MAPPING_REVERSE = Dict{Int,String}()

function _register_filename_mapping!(file::String)
key = get!(DATA_FILENAME_MAPPING, file) do
return length(DATA_FILENAME_MAPPING) + 1
end
if haskey(DATA_FILENAME_MAPPING_REVERSE, key) &&
DATA_FILENAME_MAPPING_REVERSE[key] != file
error("filename key collision: $key")
else
DATA_FILENAME_MAPPING_REVERSE[key] = file
end
return key
end

function __init__()
PackageExtensionCompat.@require_extensions
empty!(DATA_FILENAME_MAPPING)
empty!(DATA_FILENAME_MAPPING_REVERSE)
return nothing
end

Expand Down
7 changes: 7 additions & 0 deletions src/nodes/abstract.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ function transform(n::EzXML.Node)
end
end

function nodeline(node::EzXML.Node)
node_ptr = node.ptr
@assert node_ptr != C_NULL
@assert unsafe_load(node_ptr).typ == EzXML.ELEMENT_NODE
return unsafe_load(convert(Ptr{EzXML._Element}, node_ptr)).line
end

# TODO: is this hack sufficient, or required?
function cdata(n::EzXML.Node)
buffer = IOBuffer()
Expand Down
20 changes: 11 additions & 9 deletions src/nodes/element.jl
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ struct Element <: AbstractNode
attributes::Vector{Attribute}
body::Vector{AbstractNode}
slots::Vector{Pair{String,Vector{AbstractNode}}}
line::Int

function Element(name, attributes, body, slots)
return new(_restore_special_symbols(name), attributes, body, slots)
function Element(name, attributes, body, slots, line)
return new(_restore_special_symbols(name), attributes, body, slots, line)
end
end

Expand All @@ -53,7 +54,7 @@ function Element(n::EzXML.Node)
attrs = attributes(n)
if name in VALID_HTML_ELEMENTS
body = transform(EzXML.nodes(n))
return Element(name, Attribute.(attrs), body, [])
return Element(name, Attribute.(attrs), body, [], nodeline(n))
else
slots = []
nodes = EzXML.nodes(n)
Expand All @@ -62,15 +63,16 @@ function Element(n::EzXML.Node)
tag = EzXML.nodename(each)
if contains(tag, ':')
tag, slot = split(tag, ':'; limit = 2)
child = Element(tag, [], transform(EzXML.nodes(each)), [])
child =
Element(tag, [], transform(EzXML.nodes(each)), [], nodeline(each))
push!(slots, slot => [child])
end
end
end
if isempty(slots)
push!(slots, UNNAMED_SLOT => transform(nodes))
end
return Element(name, Attribute.(attrs), [], slots)
return Element(name, Attribute.(attrs), [], slots, nodeline(n))
end
end

Expand Down Expand Up @@ -102,7 +104,7 @@ function expression(c::BuilderContext, e::Element)
print($(c.io), "<", $(e.name))
$(print_attributes)(
$(c.io);
$(_data_filename_attr)($(c.file))...,
$(_data_filename_attr)($(c.file), $(e.line))...,
$(attrs...),
)
print($(c.io), "/>")
Expand All @@ -114,7 +116,7 @@ function expression(c::BuilderContext, e::Element)
print($(c.io), "<", $(e.name))
$(print_attributes)(
$(c.io);
$(_data_filename_attr)($(c.file))...,
$(_data_filename_attr)($(c.file), $(e.line))...,
$(attrs...),
)
print($(c.io), ">")
Expand Down Expand Up @@ -143,10 +145,10 @@ function expression(c::BuilderContext, e::Element)
end
end

# Used in `HypertextTemplatesReviseExt` to toggle the `data-filename` attribute
# Used in `HypertextTemplatesReviseExt` to toggle the `data-htloc` attribute
# on and off during tests. Not a public API, do not rely on this.
const _DATA_FILENAME_ATTR = Ref(true)
_data_filename_attr(::Any) = (;)
_data_filename_attr(::Any, line) = (;)

function print_attributes(io::IO; attrs...)
for (k, v) in attrs
Expand Down
21 changes: 16 additions & 5 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,23 @@ end
@test_throws TypeError render(TK.var"typed-props"; props = "string")
end

@testset "data-filename" begin
@testset "data-htloc" begin
HypertextTemplates._DATA_FILENAME_ATTR[] = true
html = render(Templates.Complex.app)
@test contains(html, "data-filename")
@test contains(html, "base-layout.html")
@test contains(html, "sidebar.html")
@test contains(html, "app.html")
@test contains(html, "data-htloc")

# Since the filenames are encoded as an integer counter in the order
# they are encountered, we need to map them back to the original
# filename for the test to be easily readable.
mapping = Dict(
basename(file) => line for
(file, line) in HypertextTemplates.DATA_FILENAME_MAPPING
)
@test contains(html, "$(mapping["base-layout.html"]):8")
@test contains(html, "$(mapping["sidebar.html"]):2")
@test contains(html, "$(mapping["app.html"]):4")
@test contains(html, "$(mapping["button.html"]):2")
@test contains(html, "$(mapping["dropdown.html"]):2")
@test contains(html, "$(mapping["dropdown.html"]):3")
end
end

0 comments on commit 00e22c9

Please sign in to comment.