Open gryumov opened 7 months ago
You should probably look at https://mommawatasu.github.io/OteraEngine.jl/
using OteraEngine
function api_example_template_creation_v1()
template = Template(
"""
Hello, {{ name }} from {{ from }}!
...
""", path=false
)
return template
end
function api_example_rendering_with_dict()
template = api_example_template_creation_v1()
some_dict = Dict("name" => "BHFT", "from" => "FemtoTrader")
println(template(init = some_dict))
end
struct OpenSourceContributor
name::String
from::String
email::String
end
function api_example_rendering_with_struct()
template = api_example_template_creation_v1()
contributor = OpenSourceContributor("BHFT", "FemtoTrader", "AtGmailDotCom")
d_contributor = Dict(String(k) => v for (k,v) in zip(fieldnames(typeof(contributor)), getfield.(Ref(contributor), fieldnames(typeof(contributor)))))
println(template(init = d_contributor))
end
struct Book
title::String
year::Int64
price::Float64
end
function api_example_rendering_with_variables()
my_book = Book(
"Advanced Julia Programming",
2024,
49.99,
)
template = Template(
"""
<book>
<title>{{ title }}</title>
<authors>
<author lang="en">John Doe</author>
<author lang="es">Juan Pérez</author>
</authors>
<year>{{ year }}</year>
<price>{{ price }}</price>
</book>
""", path=false
)
d_book = Dict(String(k) => v for (k,v) in zip(fieldnames(typeof(my_book)), getfield.(Ref(my_book), fieldnames(typeof(my_book)))))
println(template(init = d_book))
end
function api_example_rendering_with_if()
cloud_server = Dict{String,Any}("status" => 1)
template = Template(
"""
name: cloud_server
{% if (status == 0) %}
status: offline
{% elseif (status == 1) %}
status: online
{% else %}
status: NA
{% end %}
""", path=false
)
println(template(init = cloud_server))
end
struct Student
id::Int64
name::String
grade::Float64
end
struct School
students::Vector{Student}
end
function api_example_rendering_with_for()
school = School([
Student(1, "Fred", 78.2),
Student(2, "Benny", 82.0),
])
template = Template(
"""
"id","name","grade"
{% for student in students %}
{{ student.id }},{{ student.name }},{{ student.grade }}
{% end %}
""", path=false
)
d_school = Dict("students" => school.students)
println(template(init = d_school))
# bug: the output is not (exactly) as expected - there are extra newlines
# it should be fixed upstream
# https://github.com/MommaWatasu/OteraEngine.jl/issues/39
end
struct Candle
type::String
o::Float64
h::Float64
l::Float64
c::Float64
v::Float64
end
function api_example_rendering_with_include()
candle = Candle(
"Binance",
12.4,
45.0,
10.7,
19.2,
3456.7,
)
template = Template(joinpath(homedir(), "Candle_data.json"))
"""
> cat Candle_data.json
{
"candle":
{% if type == "Binance" %}
{% include "Binance_candle.json" %}
{% elseif type == "Coinbase" %}
{% include "Coinbase_candle.json" %}
{% end %}
}
"""
d_candle = Dict(String(k) => v for (k,v) in zip(fieldnames(typeof(candle)), getfield.(Ref(candle), fieldnames(typeof(candle)))))
println(template(init = d_candle))
end
using UUIDs
struct Project
name::String
uuid::UUID
version::VersionNumber
deps::Vector{Pair{String,UUID}}
compat::Vector{Pair{String,VersionNumber}}
end
function api_example_rendering_complex_example()
# Initialise the template
template = Template(joinpath(homedir(), "Project.toml"))
"""
> cat Project.toml
name = "{{ name }}"
uuid = "{{ uuid }}"
version = "{{ version }}"
{% if !isempty(deps) %}
[deps]
{% for (dep, uuid) in deps %}
{{ dep }} = "{{ uuid }}"
{% end %}
{% include "Compat.toml" %}
{% end %}
> cat Compat.toml
[compat]
{% for (name, version) in compat %}
{{ name }} = "{{ version }}"
{% end %}
"""
# Create the object
cryptoapis = Project(
"CryptoAPIs",
UUID("5e3d4798-c815-4641-85e1-deed530626d3"),
v"0.13.0",
[
"Base64" => UUID("2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"),
"Dates" => UUID("ade2ca70-3891-5945-98fb-dc099432e06a"),
"JSONWebTokens" => UUID("9b8beb19-0777-58c6-920b-28f749fee4d3"),
"NanoDates" => UUID("46f1a544-deae-4307-8689-c12aa3c955c6"),
],
[
"JSONWebTokens" => v"1.1.1",
"NanoDates" => v"0.3.0",
],
)
# Create the dictionary
d_cryptoapis = Dict(String(k) => v for (k,v) in zip(fieldnames(typeof(cryptoapis)), getfield.(Ref(cryptoapis), fieldnames(typeof(cryptoapis)))))
# Render the template
println(template(init = d_cryptoapis))
end
api_example_rendering_with_dict()
api_example_rendering_with_struct()
api_example_rendering_with_variables()
api_example_rendering_with_if()
api_example_rendering_with_for()
api_example_rendering_with_include()
api_example_rendering_complex_example()
outputs
> julia .\1_basic.jl
Hello, BHFT from FemtoTrader!
...
Hello, BHFT from FemtoTrader!
...
<book>
<title>Advanced Julia Programming</title>
<authors>
<author lang="en">John Doe</author>
<author lang="es">Juan Pérez</author>
</authors>
<year>2024</year>
<price>49.99</price>
</book>
name: cloud_server
status: online
"id","name","grade"
1,Fred,78.2
2,Benny,82.0
{
"candle":
{
"openPrice": 12.4,
"highPrice": 45.0,
"lowPrice": 10.7,
"closePrice": 19.2,
"volume": 3456.7
}
}
name = "CryptoAPIs"
uuid = "5e3d4798-c815-4641-85e1-deed530626d3"
version = "0.13.0"
[deps]
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
JSONWebTokens = "9b8beb19-0777-58c6-920b-28f749fee4d3"
NanoDates = "46f1a544-deae-4307-8689-c12aa3c955c6"
[compat]
JSONWebTokens = "1.1.1"
NanoDates = "0.3.0"
Hi, @femtotrader!
How about wrapping Jinja2Cpp_jll.jl? This package seems promising for implementing templates using Jinja2 in Julia.
What do you think?
Hi @gryumov
Let's remember about the context...
In this issue you asked (with an example) for a Jinka-like templating engine for Julia.
My answer will be biased as I provided you code showing that OteraEngine.jl a pure Julia package can do what was requested in your examples (with very small template code adaptation).
OteraEngine is according its GH repo "Very very small! There are no dependency. Jinja-like syntax. Easy to use.".
I don't know exactly what features are missing in OteraEngine.jl for your use cases. So I'd first try, if I where you, to answer this question.
You should weight the pros and cons of wrapping Jinja2Cpp.
A more formal approach through a SWOT analysis could probably be considered. I asked to a LLM for such an analysis... Here are results... (but maybe it should be "manually" improved...)
Hope that helps.
Femto
PS : What LLM forgets here is that relying on Jinja2Cpp is not the same as relying on Jinja2.
Jinja2Cpp community is not Jinja2 community on the other side Python performances are not C++ performances !
PS2 : What is also important to consider is that building a wrapper for such a library can have educational sense. It can help to better understand how a wrapper can be done.
Create a Jinja-like templating engine
You need to create a Julia package that implements functionality of Jinja templating engine. You can either do a pure Julia implementation from scratch or wrap an existing C library.
In case, you want to implement a C wrapper, make sure to change the repo name according to Julia package naming conventions.
Tasks
The package must support the following types and expressions:
variables
for
if
include
Requirements
Note that templating engines are usually file-agnostic and work with any txt-like extension. For the purposes of this task, the package should support file formats that do not conflict with
{{ % ... % }}
syntax. Having all of the key-symbols specifiable and not hard-coded would be a boon.API
User interface (API) of the package should adhere to the following:
Creation of base
NinjaTemplate
object:Next, implement a
ninja_render
method to fill thetemplate
. The output of the function should be aString
:Syntax
Syntax should be similar to Jinja:
Variables
Template variables should look similar to:
After the
ninja_render
call every case of{{ ... }}
variable must be replaced with an according value. If the value is missing then it must be left blank.Example
Julia code:
Expected output:
If
Conditional
if
statement should look similar to:The line should be filled according to the result of
if-elif-else
statement. If the value does not match any of the conditions, the line must be left blank.Example
Julia code:
Expected output:
For
Loop operator
for
should look similar to:or
For every
<var>
in<iterable>
collection the inner block should be executed. An empty collection must be blank.Example
Julia code:
Expected output:
Include
The
include
statement should be similar to:Here
include
copies contents of another file. If the file is a template, the copied statements must be executed, too.Example
Template files
Binance_candle.json
,Coinbase_candle.json
andCandle_data.json
:Julia code:
Expected behavior:
Complex example
The following example should help you debug your code.
Julia uses
Compat.toml
andProject.toml
to track dependencies. They can be formalised as the following templates:Compat.toml
Project.toml
[deps] {% for (dep, uuid) in deps %} {{ dep }} = {{ uuid }} {% endfor %}
{% include "Compat.toml" %} {% endif %}
Expected output: