From b35f79dff01ed159098c544aae8d98662ad245e0 Mon Sep 17 00:00:00 2001 From: Michael Hatherly Date: Wed, 1 Nov 2023 22:38:04 +0000 Subject: [PATCH] Support exact line numbers in template "goto" feature 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. --- docs/src/index.md | 30 ++++++++++++++++++ ext/HypertextTemplatesHTTPReviseExt.jl | 42 +++++++++++++++++--------- ext/HypertextTemplatesReviseExt.jl | 5 +-- src/HypertextTemplates.jl | 18 +++++++++++ src/nodes/abstract.jl | 7 +++++ src/nodes/element.jl | 20 ++++++------ test/runtests.jl | 21 ++++++++++--- 7 files changed, 113 insertions(+), 30 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index 9598f0c..4861079 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -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 the 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 diff --git a/ext/HypertextTemplatesHTTPReviseExt.jl b/ext/HypertextTemplatesHTTPReviseExt.jl index bd6337e..41315b3 100644 --- a/ext/HypertextTemplatesHTTPReviseExt.jl +++ b/ext/HypertextTemplatesHTTPReviseExt.jl @@ -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( @@ -46,7 +60,7 @@ 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) { @@ -54,8 +68,8 @@ function _insert_template_file_lookup_listener( 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 { diff --git a/ext/HypertextTemplatesReviseExt.jl b/ext/HypertextTemplatesReviseExt.jl index cd0fa16..22c8dd6 100644 --- a/ext/HypertextTemplatesReviseExt.jl +++ b/ext/HypertextTemplatesReviseExt.jl @@ -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 diff --git a/src/HypertextTemplates.jl b/src/HypertextTemplates.jl index 5e9b2e5..61f0ee2 100644 --- a/src/HypertextTemplates.jl +++ b/src/HypertextTemplates.jl @@ -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 diff --git a/src/nodes/abstract.jl b/src/nodes/abstract.jl index 44063f2..921484a 100644 --- a/src/nodes/abstract.jl +++ b/src/nodes/abstract.jl @@ -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() diff --git a/src/nodes/element.jl b/src/nodes/element.jl index c580e65..7247842 100644 --- a/src/nodes/element.jl +++ b/src/nodes/element.jl @@ -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 @@ -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) @@ -62,7 +63,8 @@ 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 @@ -70,7 +72,7 @@ function Element(n::EzXML.Node) 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 @@ -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), "/>") @@ -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), ">") @@ -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 diff --git a/test/runtests.jl b/test/runtests.jl index 3dfdba2..783efad 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -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