cheap is a C++ library to create html strings. TL;DR:
const std::string elem_str = get_element_str(div(span("first"), img("src=test.jpg"_att)));
⏬ output: ⏬
<div>
<span>first</span>
<img src="test.jpg" />
</div>
I needed this for a project, maybe others have use for some server-side rendering or god knows what.
- It requires C++20, runs on MSVC, clang and gcc (godbolt)
- It's a single header with no external includes. The implementation hidden behind the
CHEAP_IMPL
macro. Define that in one translation unit before you include it, so
#define CHEAP_IMPL
#include <cheap.h>
- to apparate: originates from latin and means “to appear”
The stringification functions have an optional options
parameter:
struct options
{
int indentation = 4;
bool indent_with_tab = false;
int initial_level = 0;
bool escaping = true;
bool end_with_newline = true;
};
indentation
: number of spaces to use for indenttionindent_with_tab
: use tab instead of spaces for indentationinitial_level
: initial indentation level. Might be useful to set >0 if the generated html will be inserted into a bigger HTML. Note that this is the indentation level. The number of spaces is alwayslevel * indentation
.escaping
: HTML escaping, i.e.&
→&
,<
→<
and>
→>
. On by defaultend_with_newline
: By default, the resulting string always ends with a newline, as is often useful with text files. This can be disabled. this doesn't affect newlines in the middle
Attributes can be created with the _att
literal operator. For boolean attributes, just enter the name ("hidden"_att
). For string attributes, write with equation sign ("id=container"_att
). You can also just create bool_attribute
or string_attribute
objects. They're straightforward aggregates:
struct bool_attribute {
std::string m_name;
bool m_value = true;
};
struct string_attribute {
std::string m_name;
std::string m_value;
};
The literal operator is defined in an inline namespace cheap::literals
, so you have the choice of either using namespace cheap
or a more restrictive using namespace cheap::literals;
.
There are two interfaces of creating the elements:
This involves heavily templated functions. That's convenient, just be aware that this might impact compile times depending on use.
create_element(<element name>, [<attributes>], [<conents>])
accepts the element name as the first parameter. The function is variadic, you can shovel attributes and sub-elements into it at will. The sub-elements can be other elements, or a plain std::string
.
For all HTML spec elements (from here), there is an equally named function. So div(...)
is just a shortcut to create_element("div, ...)
. Note that due to C++ limitations, the function for the template
element is called template_()
and the small
creator is called small_()
.
auto elem = div("Hello world");
// <div>Hello world</div>
auto elem = div("data-cool=true"_att, "awesome"_att, "Attributes!");
// <div data-cool="true" awesome>Attributes!</div>
auto elem = div("class=flex"_att, span("nested"), span("content"));
// <div class="flex">
// <span>nested</span>
// <span>content</span>
// </div>
// Since you can nowerdays write your own elements
auto elem = create_element("my_elem", "oof"); // <my_elem>oof</my_elem>
An element
is basically:
struct element
{
std::string m_name;
std::vector<attribute> m_attributes;
std::vector<content> m_inner_html;
element(const std::string_view name, std::vector<attribute> attributes, std::vector<content> inner_html)
element(const std::string_view name, std::vector<content> inner_html)
element(const std::string_view name)
}
With using content = std::variant<element, std::string>
. This interface is a little less magic and easier to use of you use code to generate your hierarchy.
Usage:
element elem{ "div", {"cool=true"_att},
{
element{"span", {"first"}},
element{"span", {"second"}}
}
};
Also feel free to just set the members yourself (everything is public).
There's also an overload that accepts a vector of elements. It gets rendered just as you would expect.
auto get_element_str(const element& elem, const options& opt = options{}) -> std::string;
auto get_element_str(const std::vector<element>& elements, const options& opt = options{}) -> std::string;
const std::string elem_str = get_element_str(
{ img("src=a.jpg"_att), img("src=b.jpg"_att) }
);
// <img src="a.jpg" />
// <img src="b.jpg" />
I did some rudimentary profiling and things are still fast with a million elements. The first pain points are allocations of the vectors etc.
To alleviate memory allocation worries at least to some degree, there's an alternative set of stringification functions that write into a std::string&
. That target string can be preallocated by the user, or re-used between changes to avoid most string allocations.
auto write_element_str(const element& elem, std::string& output, const options& opt = options{}) -> void;
auto write_element_str(const std::vector<element>& elements, std::string& output, const options& opt = options{}) -> void;
The HTML spec constraints certain attributes
- There are enum attributes which have a set of allowed values. For example,
dir
must be one ofltr
,rtl
orauto
- There are boolean attributes which can't have a value (
autofocus
,hidden
anditemscope
) - There are string attributes which must have a value (For example
id
)
Other checks:
- Self-closing tags (
<area>
,<base>
,<br>
,<col>
,<embed>
,<hr>
,<img>
,<input>
,<link>
,<meta>
,<source>
,<track>
and<wbr>
) can't have sub-elements create_element()
must get a name as the first parameter
If any of that is violated, a cheap_exception
is thrown with a meaningful error message.
There's a range of popular libraries (inja, mustache, handlebars) that fill strings that contain placeholders like {{ this }}
with structured content - often from json or other sources. Depending on your pipeline, cheap might replace the need for this.
But maybe it doesn't. I'm just here to tell you that such strings "survives" cheap. So you can use them for attributes, element names and string contents and they come out on the other side just fine - ready to be used by such libraries.