{"id":23372920,"url":"https://github.com/rthrfrd/webxx","last_synced_at":"2025-09-19T17:20:26.181Z","repository":{"id":102979530,"uuid":"568791844","full_name":"rthrfrd/webxx","owner":"rthrfrd","description":"Declarative, composable, concise \u0026 fast HTML \u0026 CSS components in C++","archived":false,"fork":false,"pushed_at":"2024-05-08T10:24:59.000Z","size":159,"stargazers_count":29,"open_issues_count":0,"forks_count":2,"subscribers_count":5,"default_branch":"main","last_synced_at":"2024-05-08T11:34:08.659Z","etag":null,"topics":["cpp","css","cxx","cxx17","cxx20","html"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rthrfrd.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-11-21T12:21:49.000Z","updated_at":"2024-03-13T03:53:56.000Z","dependencies_parsed_at":"2023-12-12T22:48:08.393Z","dependency_job_id":null,"html_url":"https://github.com/rthrfrd/webxx","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rthrfrd%2Fwebxx","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rthrfrd%2Fwebxx/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rthrfrd%2Fwebxx/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rthrfrd%2Fwebxx/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rthrfrd","download_url":"https://codeload.github.com/rthrfrd/webxx/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230744453,"owners_count":18273997,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cpp","css","cxx","cxx17","cxx20","html"],"created_at":"2024-12-21T16:51:32.135Z","updated_at":"2025-09-19T17:20:21.147Z","avatar_url":"https://github.com/rthrfrd.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# `webxx`\n\n\u003e Declarative, composable, concise \u0026 fast HTML \u0026 CSS in C++.\n\n[![linux](https://github.com/rthrfrd/webxx/actions/workflows/linux.yml/badge.svg)](https://github.com/rthrfrd/webxx/actions/workflows/linux.yml) [![codecov](https://codecov.io/gh/rthrfrd/webxx/branch/main/graph/badge.svg)](https://codecov.io/gh/rthrfrd/webxx)\n\n```c++\n#include \"webxx.h\"\nusing namespace Webxx;\nstd::string html = render(h1{\"Hello \", i{\"world!\"}});\n// \u003ch1\u003eHello \u003ci\u003eworld\u003c/i\u003e\u003c/h1\u003e\n```\n\n## 🎛 Features\n\n- No templating language: Use native C++ features for variables and composition.\n- No external files or string interpolation: Your templates are compiled in.\n- Component system provides easy modularity, re-use and automatic CSS scoping.\n- Small, simple, single-file, header-only library.\n- Compatible with C++17 and above (to support `std::string_view`), minimally stdlib dependent.\n\n## 🏃 Getting started\n\n### Installation\n\nYou can simply download and include [`include/webxx.h`](include/webxx.h) into your project, or clone this repository (e.g. using CMake [`FetchContent`](https://cmake.org/cmake/help/latest/module/FetchContent.html)).\n\n#### Cmake integration\n\n```cmake\n# Define webxx as an interface library target:\nadd_library(webxx INTERFACE)\ntarget_sources(webxx INTERFACE your/path/to/webxx.h)\n\n# Link and include it in your target:\nadd_executable(yourApp main.cpp)\ntarget_link_libraries(yourApp PRIVATE webxx)\ntarget_include_directories(yourApp PRIVATE your/path/to/webxx)\n```\n\n### Demo\n\n```c++\n// main.cpp\n#include \u003ciostream\u003e\n#include \u003clist\u003e\n#include \u003cstring\u003e\n#include \"webxx.h\"\n\nint main () {\n    using namespace Webxx;\n\n    // Bring your own data model:\n    bool is1MVisit = true;\n    std::list\u003cstd::string\u003e toDoItems {\n        \"Water plants\",\n        \"Plug (memory) leaks\",\n        \"Get back to that other project\",\n    };\n\n    // Create modular components which include scoped CSS:\n    struct ToDoItem : component\u003cToDoItem\u003e {\n        ToDoItem (const std::string \u0026toDoText) : component\u003cToDoItem\u003e {\n            li { toDoText },\n        } {}\n    };\n\n    struct ToDoList : component\u003cToDoList\u003e {\n        ToDoList (std::list\u003cstd::string\u003e \u0026\u0026toDoItems) : component\u003cToDoList\u003e{\n            // Styles apply only to the component's elements:\n            {\n                {\"ul\",\n                    // Hyphenated properties are camelCased:\n                    listStyle{ \"none\" },\n                },\n            },\n            // Component HTML:\n            dv {\n                h1 { \"To-do:\" },\n                // Iterate over iterators:\n                ul {\n                    each\u003cToDoItem\u003e(std::move(toDoItems))\n                },\n            },\n        } {}\n    };\n\n    // Compose a full page:\n    doc page {\n        html {\n            head {\n                title { \"Hello world!\" },\n                script { \"alert('Howdy!');\" },\n                // Define global (unscoped) styles:\n                style {\n                    {\"a\",\n                        textDecoration{\"none\"},\n                    },\n                },\n                // Styles from components are gathered up here:\n                styleTarget {},\n            },\n            body {\n                // Set attributes:\n                {_class{\"dark\", is1MVisit ? \"party\" : \"\"}},\n                // Combine components, elements and text:\n                ToDoList{std::move(toDoItems)},\n                hr {},\n                // Optionally include fragments:\n                maybe(is1MVisit, [] () {\n                    return fragment {\n                        h1 {\n                            \"Congratulations you are the 1 millionth visitor!\",\n                        },\n                        a { { _href{\"/prize\" } },\n                            \"Click here to claim your prize\",\n                        },\n                    };\n                }),\n                \"© Me 2022\",\n            },\n        },\n    };\n\n    // Render to a string:\n    std::cout \u003c\u003c render(page) \u003c\u003c std::endl;\n}\n```\n\n## ⚠️ Beware - notes \u0026 gotchas\n\n### Things `webxx` won't do\n\n- __Parse:__ This library is just for constructing HTML \u0026 friends.\n- __Validate:__ You can construct all the malformed HTML you like.\n- __Escape:__ Strings are rendered raw - you must escape unsafe data to prevent XSS attacks.\n\n### Quirks \u0026 inconsistencies\n\n- The order in which CSS styles belonging to different components are rendered cannot be relied upon, due to the undefined order in which components may be initialised.\n- Over 700 symbols are exposed in the `Webxx` namespace - use it considerately.\n- Symbols are lowercased to mimic their typical appearance in HTML \u0026 CSS.\n- HTML attributes are all prefixed with `_` (e.g. `href` -\u003e `_href`).\n- All `kebab-case` tags, properties and attributes are translated to `camelCase` (e.g. `line-height` -\u003e `lineHeight`).\n- All CSS `@*` rules are renamed to `at*` (e.g. `@import` -\u003e `atImport`).\n- The following terms are specially aliased to avoid C++ keyword clashes:\n  - __HTML Elements:__\n    - `div` -\u003e `dv`\n    - `template` -\u003e `template_`\n  - __CSS Properties:__\n    - `continue` -\u003e `continue_`\n    - `float` -\u003e `float_`\n\n### Memory safety \u0026 efficiency\n\n- The type of value you provide as content to webxx elements/attributes/etc generally determines whether webxx will make a copy of the value or not:\n  - __Will not result in a copy:__\n    - `[const] char*`\n    - `[const] std::string_view`\n    - `[const] std::string\u0026\u0026`\n  - __Will result in a copy:__\n    - `[const] std::string`\n- As it is possible to render elements at a different time from constructing them, __you must make sure that the objects you reference in your document have not been destroyed before you render__.\n- It is encouraged to use `std::move` to move variables into the components where they are needed, both for performance and to ensure their lifetimes are extended to that of the webxx document.\n- Alternatively you can pass in variables by value, so that the document retains its own copy of the data it needs to render, which cannot fall out of scope.\n- Additional care must be taken when providing `std::string_view`s to the document. While performant, you must ensure the underlying string has not been destroyed.\n\n## 📖 User guide\n\n### 1. Components (a.k.a scope, reusability \u0026 composition)\n\nA component is any C++ struct/class that inherits from `Webxx::component`. It is made\nup of HTML, along with optional parameters \u0026 CSS styles. The CSS is \"scoped\": Any CSS styles defined in a component apply only to the HTML elements that belong to that component:\n\n```c++\nusing namespace Webxx;\n\n// Components can work with whatever data model you want:\nstruct TodoItemData {\n    std::string description;\n    bool isCompleted;\n};\n\nstruct TodoItem : component\u003cTodoItem\u003e {\n    // Paramters are defined in the constructor:\n    TodoItem (TodoItemData \u0026\u0026todoItem) : component\u003cTodoItem\u003e {\n        // CSS (optional, can be omitted):\n        {\n            {\"li.completed\",\n                textDecoration{\"line-through\"},\n            },\n        },\n        // HTML:\n        li {\n            // Element attributes appear first...\n            {_class{todoItem.isCompleted ? \"completed\" : \"\"}},\n            // ...followed by content:\n            todoItem.description,\n        },\n        // 'Head elements' - to add to \u003chead\u003e (optional, can be omitted):\n        {\n            // Useful for e.g. preloading assets that this component might use:\n            link{{_rel{\"preload\"}, _href{\"/background.gif\"}, _as{\"image\"}, _type{\"image/gif\"}}},\n        }\n    } {}\n};\n```\n\nIt is encouraged to move variables into the components where they are needed, to avoid any risk of them falling out of scope:\n\n```c++\nTodoItem generateTodoItem () {\n    TodoItemData item{\"Thing to do!\", false};\n    // If we did not use std::move, description would fall\n    // out of scope and be destroyed before being rendered:\n    return TodoItem{std::move(item)};\n}\n\nauto todoItem = generateTodoItem();\nauto html = render(todoItem); // \u003cli\u003eThing to do!\u003c/li\u003e\n```\n\nIt is straightforwards to repeat components using the `each` helper function, or optionally include them using `maybe`:\n\n```c++\nstruct TodoList : component\u003cTodoList\u003e {\n    TodoList (std::list\u003cTodoItemData\u003e \u0026\u0026todoItems) : component\u003cTodoList\u003e {\n        ul {\n            // Show each item in the list:\n            each\u003cTodoItem\u003e(std::move(todoItems)),\n            // Show a message if the list is empty:\n            maybe(todoItems.empty(), [] () {\n                return li{\"You're all done!\"};\n            }),\n        },\n    } {}\n};\n```\n\nComponents and other nodes can be composed arbitrarily. For example this allows you to create structural components with slots into which other components can be inserted:\n\n```c++\nstruct TodoPage : component\u003cTodoPage\u003e {\n    TodoPage (node \u0026\u0026titleEl, node \u0026\u0026mainEl) : component\u003cTodoPage\u003e {\n        doc { // Creates the \u003cdoctype\u003e\n            html{ // Creates the \u003chtml\u003e\n                head {\n                    title{\"Todo\"},\n                    // Special element to collect all component CSS:\n                    styleTarget{},\n                    // Special element to collect all component head elements:\n                    headTarget{},\n                },\n                body{\n                    std::move(titleEl),\n                    main {\n                        std::move(mainEl),\n                    }\n                },\n            },\n        },\n    } {}\n};\n\nauto pageHtml = render(TodoPage{\n    h1{\"My todo list\"},\n    TodoList{{\n        {\"Clean the car\", false},\n        {\"Clean the dog\", false},\n        {\"Clean the browser history\", true},\n    }},\n});\n```\n\n__The `styleTarget` element must appear somewhere in the HTML, in order for the CSS defined in each component to work. Likewise for the `headTarget` and component 'head elements'.__\n\n### 2. Loops, Conditionals \u0026 Fragments\n\nThe `each` function can be used to generate elements, and supports two approaches that can produce equivalent outputs:\n\n```c++\nstd::vector\u003cstd::string\u003e letters{\"a\", \"b\", \"c\"};\n\n// Using a lambda (or other callable) allows arbitrary complexity:\nfragment byLambda = each(letters, [] (std::string letter) {\n    return li { letter };\n});\n\n// Using the template approach is best for concise simplicity:\nfragment byTemplate = each\u003cli\u003e(letters);\n\nauto isSame = render(byLambda) == render(byTemplate); // is true\n```\n\nThe `loop` function behaves the same as `each`, but additionally provides a second parameter to the callback with information about the loop:\n\n```c++\nloop(letters, [] (const std::string\u0026 letter, const Loop\u0026 loop) {\n    return li { std::to_string(loop.index), \": \", letter };\n});\n```\n\nA `fragment` contains all the generated elements for each item. A `fragment` is an \"invisible\" element; it will not show up in the rendered output (but its children will).\n\nThey can be used to pass around multiple elements without wrapping them in a containing `div` or similar. For example they let you produce multiple elements for each item in a loop:\n\n```c++\nauto html = render(each(letters, [] (std::string letter) {\n    return fragment {\n        p{letter},\n        hr{},\n    };\n}));\n// html = \"\u003cp\u003ea\u003c/p\u003e\u003chr/\u003e\u003cp\u003eb\u003c/p\u003e\u003chr/\u003e\u003cp\u003ec\u003c/p\u003e\u003chr/\u003e\"\n```\n\n### 3. Placeholders (a.k.a how to i18n)\n\nPlaceholders enable you to perform post-processing of the document at render time. This can be useful for tasks such as internationalization.\n\nYou can define a \"populator\" function, which is called for every placeholder that is encountered while rendering the document.\n\n```c++\nstd::unordered_map\u003cstd::string_view,std::string_view\u003e translations {\n    {\"Hello\", \"Hej\"},\n    {\"world\", \"värld\"},\n};\n\nh1 title {_{\"Hello\"}, _{\"world\"}, \"!\"};\n\nauto translatedHtml = render(title, {\n    false,\n    [\u0026translations] (\n        const std::string_view key,\n        const std::string_view\n    ) -\u003e const std::string_view {\n        return translations.at(key);\n    }\n});\n\n// translatedHtml = \"\u003ch1\u003eHey värld!\u003c/h1\u003e\"\n```\n\n### 4. Custom elements \u0026 attributes\n\nYou can define your own elements and attributes in the same way that webxx does internally:\n\n```c++\nconstexpr static char customElTag[] = \"custom-el\";\nusing customEl = Webxx::el\u003ccustomElTag\u003e;\n\nconstexpr static char customDataThingAttr[] = \"data-thing\";\nusing dataThing = Webxx::attr\u003ccustomDataThingAttr\u003e;\n\nrender(customElTag{\n    {\n        dataThing{\"value\"},\n    },\n    \"Hi\",\n}); // \u003ccustom-el data-thing=\"value\"\u003eHi\u003c/custom-el\u003e\n```\n\n### 5. Rendering\n\nBy default the `render` function appends everything to an internal string buffer, which it returns. However if you want to get that first byte out before rendering the whole doc, you can hook in with a function to stream the output while the rendering is still in progress:\n\n```c++\n#include \u003csstream\u003e\nstd::stringstream out;\nsize_t chunkSize{256};\n\n// Our hook function takes rendered data, and a reference to the internal buffer:\nvoid onRenderData (const std::string_view data, std::string \u0026buffer) {\n    // In this case we are going to still use the internal buffer...\n    buffer.append(data);\n    if (buffer.size() \u003e= chunkSize) {\n        // ...and then stream it out every 256 characters:\n        out \u003c\u003c buffer;\n        buffer.clear();\n    }\n}\n\ndoc myDoc {\n    head {},\n    body {\"Hello world\"},\n};\n\n// Start the render:\nstd::string leftovers = render(myDoc, {\n    nullptr, // We're not using a placeholder populator.\n    onRenderData, // Provide our render output receiver.\n    chunkSize // Preset the size of the internal buffer.\n});\n\n// Some data might be left in the buffer - this is returned so we can stream that too:\nout \u003c\u003c leftovers;\n```\n\nYou can also defer work until calling `render` by using `lazy`. However lazy blocks are still executed in a pass _before_ the first bytes are rendered.\n\n```c++\nstd::string text{\"Hello\"};\n\ndv myDiv{\n    // Evaluated now:\n    h1{std::string{text}},\n    // Lazy block takes a function:\n    lazy{[\u0026text] () {\n        // Only evaluated after render() is called below:\n        return p{text};\n    }},\n};\n\ntext = \"world\";\n\nrender(myDiv); // \u003cdiv\u003e\u003ch1\u003eHello\u003c/h1\u003e\u003cp\u003eworld\u003c/p\u003e\u003c/div\u003e\n```\n\n## 🔥 Performance\n\nSome basic [benchmarks](test/benchmark/benchmark.cpp) are built at `build/test/benchmark/webxx_benchmark` using [google-benchmark](https://github.com/google/benchmark.git). Webxx appears to be ~5-30x faster than using a template language like [inja](https://github.com/pantor/inja).\n\n```sh\n# clang-14 on macOS Ventura:\nRunning build/test/benchmark/webxx_benchmark\n--------------------------------------------------------------------\nBenchmark                          Time             CPU   Iterations\n--------------------------------------------------------------------\nsingleElementInja               6442 ns         6438 ns        85931\nsingleElementWebxx               228 ns          227 ns      2939620\nsingleElementSprintf            70.3 ns         70.2 ns      9009705\nsingleElementStringAppend       26.8 ns         26.8 ns     25017959\nmultiElementInja                9208 ns         9206 ns        65686\nmultiElementWebxx                990 ns          990 ns       640756\nmultiElementSprintf              177 ns          176 ns      3711972\nmultiElementStringAppend         224 ns          224 ns      2844603\nloop1kInja                   1456063 ns      1454982 ns          455\nloop1kWebxx                   871208 ns       870924 ns          656\nloop1kStringAppend            108399 ns       108362 ns         5607\n\n# gcc-13 on macOS Ventura:\nRunning build/test/benchmark/webxx_benchmark\n--------------------------------------------------------------------\nBenchmark                          Time             CPU   Iterations\n--------------------------------------------------------------------\nsingleElementInja               6804 ns         6787 ns        95385\nsingleElementWebxx               240 ns          239 ns      2579599\nsingleElementSprintf            74.8 ns         74.7 ns      8870416\nsingleElementStringAppend       27.6 ns         27.5 ns     25333039\nmultiElementInja                9630 ns         9616 ns        65712\nmultiElementWebxx               1015 ns         1013 ns       622698\nmultiElementSprintf              177 ns          177 ns      3706096\nmultiElementStringAppend         211 ns          210 ns      3207111\nloop1kInja                   1537252 ns      1519808 ns          426\nloop1kWebxx                   927711 ns       926574 ns          659\nloop1kStringAppend            113185 ns       113055 ns         5656\n```\n\n## 🛠 Development\n\n### Contributing\n\nContributions are super welcome, in the form of pull requests from Github forks. Please ensure you are able to make your contribution under the terms of the project license [(MIT)](LICENSE.md). New features may be rejected to limit scope creep.\n\n### Roadmap / To-do\n\n- Now\n    - Optimisations to avoid heap allocations where sizes may be known.\n    - Add Mac \u0026 Windows builds.\n    - More benchmarking/testing (memory, libmxl2, more usage variations).\n- Later\n    - WASM usage (with two-way DOM binding).\n    - Indented render output.\n    - Publishing to package managers.\n\n### Approach\n\n- Simplicity, safety and accessibility are prioritised.\n- Idiomatic C++ is used wherever possible.\n- Scope creep is treated with caution.\n\n### Orientation\n\nThe library is sectioned into several modules:\n\n- __CSS:__ Classes for constructing CSS stylesheets.\n- __HTML:__ Classes for constructing HTML elements \u0026 documents.\n- __Component:__ Abstraction for a modular combination of CSS \u0026 HTML.\n- __Rendering:__ Functions for rendering constructed Components, HTML \u0026 CSS into strings.\n- __Utility:__ Helper functions for dynamically generating content.\n- __Public:__ The interface users of this library can consume.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frthrfrd%2Fwebxx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frthrfrd%2Fwebxx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frthrfrd%2Fwebxx/lists"}