README
¶
funtemplates
Funtemplates is a templating library for go that uses the C preprocessor.
-
Calls the
cpp
binary on your system. -
Can load templates from an
fs.FS
, or you can pass them in as structs. -
Supports passing in variables when rendering the template, which will be inserted using
#define
.
Example
#define BODY(content) <html><body>content</body></html>
#define LINK(url, text) <a href=url>text</a>
BODY( \
<h1>Rendered with the C preprocessor!</h1> \
\
<!-- a variable set dynamically in go --> \
<p>the current time is _V_time</p> \
<p>unicode works!! 「ガー ガー」との🪿 </p> \
<p>it works in variables too: _V_mouse</p> \
\
<!-- make sure to put things with two slashes in them in \
quotes. otherwise bad things will happen - because \
it looks like a C comment. --> \
<p>LINK("https://git.sr.ht/~bbbb/funtemplates", source)</p> \
)
...
// you can load templates from a fs.FS. (or you can just
// pass in their content and name as strings).
t = ft.Must(t.ParseFSRecursive(templates, "templates"))
h := func(w http.ResponseWriter, req *http.Request) {
now := time.Now().UTC().Format(time.RFC1123)
err := t.ExecuteTemplate(w, "example.html", ft.FunData{
"time": now,
// unicode works in these variables too.
"mouse": "🐁",
})
}
...
(full code is at examples/simple)
This example is with html, but funtemplates isn't specific to html. it can be used with all text files.
(formats that have significant whitespace can be tricky though.)
Gotchas
To prevent it from evaulating something, you can put it in "quotes". The quotes will be in the output though; it won't remove them.
Another way is by putting a unicode codepoint literal instead of the actual character(s). It will be turned into the actual character in the output.
-
instead of "\U", you need to use "|U". this is so the preprocessor won't try to interpret the literal itself.
-
funtemplates only supports 8-character codepoint literals right now. instead of "|u002f", you'd need to write "|U0000002f" (this is a '/')
Special characters you should be aware of:
The C preprocessor has some special characters which you should escape if you're not using them for what they do.
-
In the body of macro functions,
#
must be followed by a parameter of the function and will put it in quotes. To escape it, the codepoint is|U00000023
. -
In function bodies,
##
will concentrate the things to its left and to its right.
e.g #define A() 123 456 ## 789
would result in 123 456789
, when A()
is
called.
- Things that look like C comments will be treated like C comments.
/* */
and//
./
's codepoint is|U0000002f
.
Be aware of that with URLs. The //
in the protocol can get treated like a
comment. e.g. https://example.com
could turn into https:
. You can use the
codepoint literal instead of a "/" to avoid this.
https:|U0000002f|U0000002fexample.com
would turn into "https://example.com".
Debugging
You can set FtOptions.Debug_print
to true
(passed into
ft.NewFunTemplate()
), and it will print out each of the files it generates
to stderr
.
ExecCpp
(what calls the cpp
binary on your system) also has a debugging
option.
Performance
Currently the only C preprocessor implementation we have is ExecCpp
, which
uses the cpp
present on your system. It writes all the generated template
files to the disk in a (configurable) tmp directory and calls cpp
with them
(the files are deleted after). I'm not sure how big a difference it would make,
but if you want to use this in production (😰), you could set the tmp directory
to something in-memory, like docker's tmpfs
.
Security
I would not be surprised if this had some kind of RCE. The C compiler wasn't designed to be used as a templating engine for a web server (or whatever else), so it might have some unexpected behaviors.
funtemplates
doesn't pass any user-generated input into the arguments when it
calls cpp
. but, you can pass in variables when you execute a template (which
get turned into #define
statements). so it still may deal with user-generated
input.
Escaping text
When you pass variables into the templaes it does some escaping by default. Currently it replaces all '#'s and '/'s with the codepoint literal (so the preprocessor won't treat them like a special character), and replaces the "i" in "include" with the codepoint literal.
(This is to make it harder for someone to get it to print a file on your system
by #includ
ing it. It could still be possible though.)
It doesn't do any escaping of html in the input, since this isn't specific to html. If you're putting user input into the templates, you should probably escape it.
(It could be a good idea to run it in a sandbox/vm if you're passing untrusted input into templates. A TODO is to compile a c preprocessor to webassembly and embed it, which would reduce the security risk a lot and improve portability & probably performance too.)
Documentation
¶
Index ¶
- Variables
- func EscapeCpp(text string) string
- func Url(url string) string
- type Cpp
- type CppFile
- type CppOptions
- type ExecCpp
- type ExecOptions
- type FtOptions
- type FunData
- type FunTemplate
- func (e *FunTemplate) ExecuteTemplate(wr io.Writer, name string, data FunData) error
- func (e *FunTemplate) ExecuteTemplateWithoutEscaping(wr io.Writer, name string, data FunData) error
- func (e *FunTemplate) Parse(text string, name string) (*FunTemplate, error)
- func (e *FunTemplate) ParseFSRecursive(_fs fs.FS, base_path string) (*FunTemplate, error)
Constants ¶
This section is empty.
Variables ¶
var ErrInvalidIdentifier = errors.New("invalid identifier in FunData")
Functions ¶
func EscapeCpp ¶
currently the escaping is very rudimentary.
escape user input, making it suitable to be passed into a template with FunData. `ExecuteTemplate` automatically calls this on all input. currently we just remove all '#'s and the word "include" from anywhere in `text`. They are replaced with a sad face emoji, to shame your users for trying to leak your private files.
if the value contains any non-unicode text, it will wrap it in quotes. "value".
TODO: log a warning when something is escaped? we'd probably want to pass in a logger though...
Types ¶
type Cpp ¶
type Cpp interface { // TODO: should file_to_run be put in as a CppFile instead of just the name to // the one in `files`? idk. Run(files []CppFile, file_to_run string, options CppOptions) (string, error) }
TODO: rename to Cpp or something
func NewExecCpp ¶
func NewExecCpp(_options *ExecOptions) Cpp
_options as nil for the default values.
type CppFile ¶
type CppOptions ¶
type CppOptions struct{}
TODO: for now we're just using some kind of hardcoded default thing i guess..
type ExecCpp ¶
type ExecCpp struct {
// contains filtered or unexported fields
}
a Cpp implementation that uses the OS `cpp` command.
type ExecOptions ¶
type ExecOptions struct { // whether it logs debug messages to the console. // (it logs at debug logging level.) // default false. Debug bool // will use the default slog logger if unset. Logger *slog.Logger // the name of the `cpp` command in the os. // default "cpp". Cpp_cmd string // the arguments that are always passed to `cpp` // default "-P" (no line numbers in output). Args []string // the root directory to store temporary files in. // e.g. /tmp. // this should ideally be loaded in memory and not an // actual directory in the disk. (e.g. docker tmpfs). // defaults to the the OS default. Tmp_root string }
Options for ExecCpp
type FtOptions ¶
type FtOptions struct { // if true, will log all the files that it generates // in stderr. // defaults to false. Debug_print bool // if unset, will use the default slog.Logger. Logger *slog.Logger }
options for a FunTemplate.
type FunTemplate ¶
type FunTemplate struct {
// contains filtered or unexported fields
}
func Must ¶
func Must(e *FunTemplate, err error) *FunTemplate
wraps a call to a function returning (*FunTemplate, error) and panics if the error isn't nil.
func NewFunTemplate ¶
func NewFunTemplate(cpp Cpp, _options *FtOptions) *FunTemplate
if `debug` is true, will log all the files it generates in stderr.
func (*FunTemplate) ExecuteTemplate ¶
execute the template `name`, returning the output.
this escapes all the values in `data` (not the variable names though). the escaping is very rudimentary, so don't rely on it. and it's just C preprocessor escaping, not html escaping.
func (*FunTemplate) ExecuteTemplateWithoutEscaping ¶
func (*FunTemplate) Parse ¶
func (e *FunTemplate) Parse(text string, name string) (*FunTemplate, error)
func (*FunTemplate) ParseFSRecursive ¶
func (e *FunTemplate) ParseFSRecursive(_fs fs.FS, base_path string) (*FunTemplate, error)
recursively parse all of the files in `base_path`