/cheap

C++ HTML Element Apparator

Primary LanguageC++MIT LicenseMIT

cheap (C++ HTML Element Apparator) 💲💲💲

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”

Options

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 indenttion
  • indent_with_tab: use tab instead of spaces for indentation
  • initial_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 always level * indentation.
  • escaping: HTML escaping, i.e. &&amp;, <&lt; and >&gt;. On by default
  • end_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

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:

First interface: Convenient template interface

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>

Second interface: element type

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).

Parallel elements

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" />

Performance; string ref output

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;

Error handling

The HTML spec constraints certain attributes

  • There are enum attributes which have a set of allowed values. For example, dir must be one of ltr, rtl or auto
  • There are boolean attributes which can't have a value (autofocus, hidden and itemscope)
  • 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.

Compatibility with inja, mustache, Handlebars etc

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.