Skip to content

Commit

Permalink
Markdown templates
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelHatherly committed Dec 19, 2023
1 parent 5269da3 commit 35fce30
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 34 deletions.
4 changes: 4 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -32,4 +35,5 @@ MacroTools = "0.5"
Match = "2"
PackageExtensionCompat = "1"
Random = "1.6"
TOML = "1"
julia = "1.6"
38 changes: 38 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,44 @@ julia> render(Templates.var"edit-contact-link"; id=123)
"<a href=\"/contact/123/edit\">Edit</a>"
```

## 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"]
+++

<large-custom-template prop="foo" />

# My Template

This is a template that takes two props: `foo` and `bar`. Their values are
<julia value="foo" /> and <julia value="bar" /> respectively.
```


## Development vs. Production Usage

`HypertextTemplates` supports integration with `Revise`-based development
Expand Down
37 changes: 37 additions & 0 deletions ext/HypertextTemplatesCommonMarkExt.jl
Original file line number Diff line number Diff line change
@@ -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 = "<function $name$props>$(CommonMark.html(ast))</function>"
return HypertextTemplates._components_from_str(str, file, mod)
end

function format_prop(k) end

end
59 changes: 40 additions & 19 deletions src/macro.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

"""
Expand All @@ -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
Expand Down
35 changes: 20 additions & 15 deletions src/nodes/function.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<function>' or '<html>' 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 '<function>' or '<html>' found in file: $file.")
else
return TemplateFunction.(roots, Ref(file), Ref(mod))
end
end
end

struct PropsAccessor{N<:NamedTuple}
nt::N
end
Expand Down
1 change: 1 addition & 0 deletions test/Project.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
11 changes: 11 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using CommonMark
using HypertextTemplates
using Test
using ReferenceTests
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions test/templates.jl
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,11 @@ using HypertextTemplates
template"templates/keywords/keywords.html"

end

module Markdown

using HypertextTemplates

template"templates/markdown/markdown.md"

end
3 changes: 3 additions & 0 deletions test/templates/markdown/markdown.1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<h1 id="markdown-template"><a href="#markdown-template" class="anchor"></a>Markdown Template</h1><p>prop-value<em>content</em><strong>here</strong>.</p><pre><code class="language-julia">function test()
return "test"
end </code></pre>
16 changes: 16 additions & 0 deletions test/templates/markdown/markdown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
+++
name = "custom-markdown-name"
props = [
"prop"
]
+++

# Markdown Template

<julia value=prop /> *content* **here**.

```julia
function test()
return "test"
end
```

0 comments on commit 35fce30

Please sign in to comment.