From 35fce3074a6041172e9369be03be567ee638accf Mon Sep 17 00:00:00 2001 From: MichaelHatherly Date: Tue, 19 Dec 2023 21:13:22 +0000 Subject: [PATCH] Markdown templates --- Project.toml | 4 ++ docs/src/index.md | 38 +++++++++++++++++ ext/HypertextTemplatesCommonMarkExt.jl | 37 ++++++++++++++++ src/macro.jl | 59 +++++++++++++++++--------- src/nodes/function.jl | 35 ++++++++------- test/Project.toml | 1 + test/runtests.jl | 11 +++++ test/templates.jl | 8 ++++ test/templates/markdown/markdown.1.txt | 3 ++ test/templates/markdown/markdown.md | 16 +++++++ 10 files changed, 178 insertions(+), 34 deletions(-) create mode 100644 ext/HypertextTemplatesCommonMarkExt.jl create mode 100644 test/templates/markdown/markdown.1.txt create mode 100644 test/templates/markdown/markdown.md diff --git a/Project.toml b/Project.toml index 8d2e3a7..a2a9f84 100644 --- a/Project.toml +++ b/Project.toml @@ -13,12 +13,15 @@ MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" Match = "7eb4fadd-790c-5f42-8a69-bfa0b872bfbf" PackageExtensionCompat = "65ce6f38-6b18-4e1d-a461-8949797d7930" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" [weakdeps] +CommonMark = "a80b9123-70ca-4bc0-993e-6e3bcb318db6" HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" [extensions] +HypertextTemplatesCommonMarkExt = "CommonMark" HypertextTemplatesHTTPReviseExt = ["HTTP", "Revise"] HypertextTemplatesReviseExt = "Revise" @@ -32,4 +35,5 @@ MacroTools = "0.5" Match = "2" PackageExtensionCompat = "1" Random = "1.6" +TOML = "1" julia = "1.6" diff --git a/docs/src/index.md b/docs/src/index.md index 4861079..437c87b 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -313,6 +313,44 @@ julia> render(Templates.var"edit-contact-link"; id=123) "Edit" ``` +## Markdown Template Files + +*Experimental Feature* + +You can also use Markdown files as templates. This requires the user to have +imported the `CommonMark` package prior to using a markdown file in a template +macro. Internally the markdown syntax is rendered to HTML using +`CommonMark.jl`'s `html` function and then parsed into a template function using +`HypertextTemplates.jl`. + +Extensions are enabled by default in the CommonMark parser, such as tables, +admonitions, and footnotes. Since CommonMark supports embedding *some* basic +HTML tags you can also use those in your markdown templates to call other HTML +or markdown templates for composition. Don't expect complete HTML support to +work though, and if you find yourself embedding large amounts of HTML in your +markdown templates then you should probably just use HTML templates instead and +then embed that custom template tag in your markdown template. + +TOML frontmatter syntax is used to allow the template name and props to be +declared. When no `name` is provided the template name will be derived from the +file name. When no `props` are provided the template will have no props. +Frontmatter is optional and declared with `+++` fences at the top of the file. + +```markdown ++++ +name = "my-template" +props = ["foo", "bar"] ++++ + + + +# My Template + +This is a template that takes two props: `foo` and `bar`. Their values are + and respectively. +``` + + ## Development vs. Production Usage `HypertextTemplates` supports integration with `Revise`-based development diff --git a/ext/HypertextTemplatesCommonMarkExt.jl b/ext/HypertextTemplatesCommonMarkExt.jl new file mode 100644 index 0000000..a9afc0f --- /dev/null +++ b/ext/HypertextTemplatesCommonMarkExt.jl @@ -0,0 +1,37 @@ +module HypertextTemplatesCommonMarkExt + +import CommonMark +import HypertextTemplates +import TOML + +function HypertextTemplates._handle_commonmark_ext( + ::HypertextTemplates.CommonMarkExtensionType, + file, + mod, + suffix, +) + if !isfile(file) + error("markdown template file does not exist: $file") + end + + parser = CommonMark._init_parser(mod, suffix) + rule = CommonMark.FrontMatterRule(; toml = TOML.parse) + CommonMark.enable!(parser, rule) + + ast = open(parser, file) + + frontmatter = CommonMark.frontmatter(ast) + name = basename(file) + name, _ = splitext(name) + name = get(frontmatter, "name", name) + + props = join(get(Vector{String}, frontmatter, "props"), " ") + props = isempty(props) ? props : " $props" + + str = "$(CommonMark.html(ast))" + return HypertextTemplates._components_from_str(str, file, mod) +end + +function format_prop(k) end + +end diff --git a/src/macro.jl b/src/macro.jl index 1afbb3d..6a1e2c6 100644 --- a/src/macro.jl +++ b/src/macro.jl @@ -3,8 +3,8 @@ Define an HTML template. The string must be a path to a valid HTML file. """ -macro template_str(file::AbstractString) - return _template_str(file, __module__, __source__, create = false) +macro template_str(file::AbstractString, suffix = "jmd") + return _template_str(file, __module__, __source__, suffix; create = false) end """ @@ -13,32 +13,53 @@ end A helper macro to create an empty template file. The string must be a path to a non-existant HTML file. """ -macro create_template_str(file::AbstractString) - return _template_str(file, __module__, __source__, create = true) +macro create_template_str(file::AbstractString, suffix = "jmd") + return _template_str(file, __module__, __source__, suffix, create = true) end -function _template_str(file::AbstractString, __module__, __source__; create::Bool) +function _template_str(file::AbstractString, __module__, __source__, suffix; create::Bool) file = _template_file_path(file, __source__) - if endswith(file, ".html") + _, ext = splitext(file) + if ext in (".html", ".md") if create - if isfile(file) - @error "template file exists, replace `create_template` with the `template` string macro and implement the template function definition" file - else - mkpath(dirname(file)) - touch(file) - @info "created empty template file" file - return nothing - end - else - defs = components(file, __module__) - exprs = expression.(defs) - return esc(Expr(:toplevel, :(include_dependency($file)), exprs...)) + return _create_template(file) + end + if ext == ".html" + return _template_expr(file, components(file, __module__)) end + if ext == ".md" + return _template_expr(file, _markdown_components(file, __module__, suffix)) + end + end + error("template file must have an '.html' or '.md' extension: $file") +end + +function _template_expr(file, defs) + exprs = expression.(defs) + return esc(Expr(:toplevel, :(include_dependency($file)), exprs...)) +end + +function _create_template(file::AbstractString) + if isfile(file) + @error "template file exists, replace `create_template` with the `template` string macro and implement the template function definition" file else - error("template file must have an '.html' extension: $file") + mkpath(dirname(file)) + touch(file) + @info "created empty template file" file + return nothing end end +function _markdown_components(file, mod, suffix) + return _handle_commonmark_ext(CommonMarkExtensionType(), file, mod, suffix) +end + +struct CommonMarkExtensionType end + +function _handle_commonmark_ext(::Any, file, mod, suffix) + error("markdown template files require you to import the `CommonMark` package first.") +end + function _template_file_path(file, __source__) if isabspath(file) && isfile(file) return file diff --git a/src/nodes/function.jl b/src/nodes/function.jl index 329ddbd..d9be72f 100644 --- a/src/nodes/function.jl +++ b/src/nodes/function.jl @@ -312,26 +312,31 @@ AbstractTrees.children(c::TemplateFunction) = c.body function components(file::String, mod::Module)::Vector{TemplateFunction} if endswith(file, ".html") - content = _swap_special_symbols(read(file, String)) - if isempty(content) - error("template file is empty: $file") - else - html = _with_filtered_logging() do - return EzXML.parsehtml(content) - end - roots = findall("//$TEMPLATE_FUNCTION_TAG", html) - roots = isempty(roots) ? findall("//html", html) : roots - if isempty(roots) - error("no '' or '' found in file: $file.") - else - return TemplateFunction.(roots, Ref(file), Ref(mod)) - end - end + str = read(file, String) + return _components_from_str(str, file, mod) else error("template file must have an '.html' extension: $file") end end +function _components_from_str(str::AbstractString, file::String, mod::Module) + content = _swap_special_symbols(str) + if isempty(content) + error("template file is empty: $file") + else + html = _with_filtered_logging() do + return EzXML.parsehtml(content) + end + roots = findall("//$TEMPLATE_FUNCTION_TAG", html) + roots = isempty(roots) ? findall("//html", html) : roots + if isempty(roots) + error("no '' or '' found in file: $file.") + else + return TemplateFunction.(roots, Ref(file), Ref(mod)) + end + end +end + struct PropsAccessor{N<:NamedTuple} nt::N end diff --git a/test/Project.toml b/test/Project.toml index b4e6eb5..24eba67 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,4 +1,5 @@ [deps] +CommonMark = "a80b9123-70ca-4bc0-993e-6e3bcb318db6" HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" ReferenceTests = "324d217c-45ce-50fc-942e-d289b448e8cf" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" diff --git a/test/runtests.jl b/test/runtests.jl index 06d70d2..60f571c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,3 +1,4 @@ +using CommonMark using HypertextTemplates using Test using ReferenceTests @@ -303,6 +304,16 @@ end @test_throws TypeError render(TK.var"typed-props"; props = "string") end + @testset "markdown" begin + markdown = joinpath(templates, "markdown") + TM = Templates.Markdown + + @test_reference joinpath(markdown, "markdown.1.txt") render( + TM.var"custom-markdown-name"; + prop = "prop-value", + ) + end + @testset "data-htloc" begin HypertextTemplates._DATA_FILENAME_ATTR[] = true html = render(Templates.Complex.app) diff --git a/test/templates.jl b/test/templates.jl index c9f0d29..471196b 100644 --- a/test/templates.jl +++ b/test/templates.jl @@ -45,3 +45,11 @@ using HypertextTemplates template"templates/keywords/keywords.html" end + +module Markdown + +using HypertextTemplates + +template"templates/markdown/markdown.md" + +end diff --git a/test/templates/markdown/markdown.1.txt b/test/templates/markdown/markdown.1.txt new file mode 100644 index 0000000..3f52f6f --- /dev/null +++ b/test/templates/markdown/markdown.1.txt @@ -0,0 +1,3 @@ +

Markdown Template

prop-valuecontenthere.

function test()
+    return "test"
+end 
\ No newline at end of file diff --git a/test/templates/markdown/markdown.md b/test/templates/markdown/markdown.md new file mode 100644 index 0000000..097f6c1 --- /dev/null +++ b/test/templates/markdown/markdown.md @@ -0,0 +1,16 @@ ++++ +name = "custom-markdown-name" +props = [ + "prop" +] ++++ + +# Markdown Template + + *content* **here**. + +```julia +function test() + return "test" +end +```