diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d359871..fef46d2f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,7 +60,6 @@ if(ONE_INDEX) include(commands/esbuild) add_subdirectory(src/configuration) add_subdirectory(src/resolver) - add_subdirectory(src/html) add_subdirectory(src/web) add_subdirectory(src/index) add_subdirectory(collections) @@ -103,7 +102,6 @@ if(ONE_TESTS) if(ONE_INDEX) add_subdirectory(test/unit/configuration) add_subdirectory(test/unit/resolver) - add_subdirectory(test/unit/html) endif() add_subdirectory(test/cli) endif() diff --git a/DEPENDENCIES b/DEPENDENCIES index 97ab9a8a..c1383ab5 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,6 +1,6 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 uwebsockets https://github.com/uNetworking/uWebSockets v20.74.0 -core https://github.com/sourcemeta/core 6866e0e65979ced65cb5d37bde8cffd26eae9b86 +core https://github.com/sourcemeta/core c0180e5d87753e562f7b742e054ff71de3830348 blaze https://github.com/sourcemeta/blaze 93342104a85814bc0fd11792d305c4e83de259c0 jsonbinpack https://github.com/sourcemeta/jsonbinpack 0c2340990bf31c630155991a93306990d9d94fd4 hydra https://github.com/sourcemeta/hydra c86d2165a2f27f838837af1a5af24b1055a35317 diff --git a/src/html/CMakeLists.txt b/src/html/CMakeLists.txt deleted file mode 100644 index e97f0035..00000000 --- a/src/html/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -sourcemeta_library(NAMESPACE sourcemeta PROJECT one NAME html - PRIVATE_HEADERS escape.h elements.h encoder.h - SOURCES escape.cc encoder.cc) diff --git a/src/html/encoder.cc b/src/html/encoder.cc deleted file mode 100644 index 5ac4f084..00000000 --- a/src/html/encoder.cc +++ /dev/null @@ -1,78 +0,0 @@ -#include - -#include // std::ostream -#include // std::ostringstream -#include // std::string - -namespace sourcemeta::one::html { - -auto HTML::render() const -> std::string { - std::ostringstream output_stream; - output_stream << "<" << tag_name; - - // Render attributes - for (const auto &[attribute_name, attribute_value] : attributes) { - std::string escaped_value{attribute_value}; - escape(escaped_value); - output_stream << " " << attribute_name << "=\"" << escaped_value << "\""; - } - - if (self_closing) { - output_stream << " />"; - return output_stream.str(); - } - - output_stream << ">"; - - // Render children - if (child_elements.empty()) { - output_stream << ""; - } else if (child_elements.size() == 1 && - std::get_if(&child_elements[0])) { - // Inline single text node - output_stream << render(child_elements[0]); - output_stream << ""; - } else { - // Block level children - for (const auto &child_element : child_elements) { - output_stream << render(child_element); - } - output_stream << ""; - } - - return output_stream.str(); -} - -auto HTML::render(const Node &child_element) const -> std::string { - if (const auto *text = std::get_if(&child_element)) { - std::string escaped_text{*text}; - escape(escaped_text); - return escaped_text; - } else if (const auto *raw_html = std::get_if(&child_element)) { - return raw_html->content; - } else if (const auto *html_element = std::get_if(&child_element)) { - return html_element->render(); - } - return ""; -} - -auto HTML::push_back(const Node &child) -> HTML & { - child_elements.push_back(child); - return *this; -} - -auto HTML::push_back(Node &&child) -> HTML & { - child_elements.push_back(std::move(child)); - return *this; -} - -auto operator<<(std::ostream &output_stream, const HTML &html_element) - -> std::ostream & { - return output_stream << html_element.render(); -} - -auto raw(std::string html_content) -> RawHTML { - return RawHTML{std::move(html_content)}; -} - -} // namespace sourcemeta::one::html diff --git a/src/html/include/sourcemeta/one/html.h b/src/html/include/sourcemeta/one/html.h deleted file mode 100644 index f66e4833..00000000 --- a/src/html/include/sourcemeta/one/html.h +++ /dev/null @@ -1,7 +0,0 @@ -#ifndef SOURCEMETA_ONE_HTML_H_ -#define SOURCEMETA_ONE_HTML_H_ - -#include -#include - -#endif diff --git a/src/html/include/sourcemeta/one/html_elements.h b/src/html/include/sourcemeta/one/html_elements.h deleted file mode 100644 index f446d3c1..00000000 --- a/src/html/include/sourcemeta/one/html_elements.h +++ /dev/null @@ -1,434 +0,0 @@ -#ifndef SOURCEMETA_ONE_HTML_ELEMENTS_H_ -#define SOURCEMETA_ONE_HTML_ELEMENTS_H_ - -#include - -namespace sourcemeta::one::html { - -#define HTML_VOID_ELEMENT(name) \ - inline auto name() -> HTML { return HTML(#name, true); } \ - inline auto name(Attributes attributes) -> HTML { \ - return HTML(#name, std::move(attributes), true); \ - } - -#define HTML_CONTAINER_ELEMENT(name) \ - inline auto name(Attributes attributes) -> HTML { \ - return HTML(#name, std::move(attributes)); \ - } \ - template \ - inline auto name(Attributes attributes, Children &&...children) -> HTML { \ - return HTML(#name, std::move(attributes), \ - std::forward(children)...); \ - } \ - template \ - inline auto name(Children &&...children) -> HTML { \ - return HTML(#name, std::forward(children)...); \ - } - -#define HTML_COMPACT_ELEMENT(name) \ - inline auto name(Attributes attributes) -> HTML { \ - return HTML(#name, std::move(attributes)); \ - } \ - template \ - inline auto name(Attributes attributes, Children &&...children) -> HTML { \ - return HTML(#name, std::move(attributes), \ - std::forward(children)...); \ - } \ - template \ - inline auto name(Children &&...children) -> HTML { \ - return HTML(#name, std::forward(children)...); \ - } - -#define HTML_VOID_ATTR_ELEMENT(name) \ - inline auto name(Attributes attributes) -> HTML { \ - return HTML(#name, std::move(attributes), true); \ - } - -// ============================================================================= -// Document Structure Elements -// ============================================================================= - -// The Root Element -// https://html.spec.whatwg.org/multipage/semantics.html#the-html-element -HTML_CONTAINER_ELEMENT(html) - -// Document Metadata -// https://html.spec.whatwg.org/multipage/semantics.html#the-base-element -HTML_VOID_ATTR_ELEMENT(base) - -// https://html.spec.whatwg.org/multipage/semantics.html#the-head-element -HTML_CONTAINER_ELEMENT(head) - -// https://html.spec.whatwg.org/multipage/semantics.html#the-link-element -HTML_VOID_ATTR_ELEMENT(link) - -// https://html.spec.whatwg.org/multipage/semantics.html#the-meta-element -HTML_VOID_ATTR_ELEMENT(meta) - -// https://html.spec.whatwg.org/multipage/semantics.html#the-style-element -HTML_CONTAINER_ELEMENT(style) - -// https://html.spec.whatwg.org/multipage/semantics.html#the-title-element -HTML_CONTAINER_ELEMENT(title) - -// Sectioning Root -// https://html.spec.whatwg.org/multipage/sections.html#the-body-element -HTML_CONTAINER_ELEMENT(body) - -// ============================================================================= -// Content Sectioning Elements -// ============================================================================= - -// https://html.spec.whatwg.org/multipage/sections.html#the-address-element -HTML_CONTAINER_ELEMENT(address) - -// https://html.spec.whatwg.org/multipage/sections.html#the-article-element -HTML_CONTAINER_ELEMENT(article) - -// https://html.spec.whatwg.org/multipage/sections.html#the-aside-element -HTML_CONTAINER_ELEMENT(aside) - -// https://html.spec.whatwg.org/multipage/sections.html#the-footer-element -HTML_CONTAINER_ELEMENT(footer) - -// https://html.spec.whatwg.org/multipage/sections.html#the-header-element -HTML_CONTAINER_ELEMENT(header) - -// https://html.spec.whatwg.org/multipage/sections.html#the-h1-h2-h3-h4-h5-and-h6-elements -HTML_COMPACT_ELEMENT(h1) -HTML_COMPACT_ELEMENT(h2) -HTML_COMPACT_ELEMENT(h3) -HTML_COMPACT_ELEMENT(h4) -HTML_COMPACT_ELEMENT(h5) -HTML_COMPACT_ELEMENT(h6) - -// https://html.spec.whatwg.org/multipage/sections.html#the-hgroup-element -HTML_CONTAINER_ELEMENT(hgroup) - -// https://html.spec.whatwg.org/multipage/grouping-content.html#the-main-element -HTML_CONTAINER_ELEMENT(main) - -// https://html.spec.whatwg.org/multipage/sections.html#the-nav-element -HTML_CONTAINER_ELEMENT(nav) - -// https://html.spec.whatwg.org/multipage/sections.html#the-section-element -HTML_CONTAINER_ELEMENT(section) - -// https://html.spec.whatwg.org/multipage/sections.html#the-search-element -HTML_CONTAINER_ELEMENT(search) - -// ============================================================================= -// Text Content Elements -// ============================================================================= - -// https://html.spec.whatwg.org/multipage/grouping-content.html#the-blockquote-element -HTML_CONTAINER_ELEMENT(blockquote) - -// https://html.spec.whatwg.org/multipage/grouping-content.html#the-dd-element -HTML_COMPACT_ELEMENT(dd) - -// https://html.spec.whatwg.org/multipage/grouping-content.html#the-div-element -HTML_CONTAINER_ELEMENT(div) - -// https://html.spec.whatwg.org/multipage/grouping-content.html#the-dl-element -HTML_COMPACT_ELEMENT(dl) - -// https://html.spec.whatwg.org/multipage/grouping-content.html#the-dt-element -HTML_COMPACT_ELEMENT(dt) - -// https://html.spec.whatwg.org/multipage/grouping-content.html#the-figcaption-element -HTML_CONTAINER_ELEMENT(figcaption) - -// https://html.spec.whatwg.org/multipage/grouping-content.html#the-figure-element -HTML_CONTAINER_ELEMENT(figure) - -// https://html.spec.whatwg.org/multipage/grouping-content.html#the-hr-element -HTML_VOID_ELEMENT(hr) - -// https://html.spec.whatwg.org/multipage/grouping-content.html#the-li-element -HTML_COMPACT_ELEMENT(li) - -// https://html.spec.whatwg.org/multipage/grouping-content.html#the-menu-element -HTML_CONTAINER_ELEMENT(menu) - -// https://html.spec.whatwg.org/multipage/grouping-content.html#the-ol-element -HTML_COMPACT_ELEMENT(ol) - -// https://html.spec.whatwg.org/multipage/grouping-content.html#the-p-element -HTML_COMPACT_ELEMENT(p) - -// https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element -HTML_CONTAINER_ELEMENT(pre) - -// https://html.spec.whatwg.org/multipage/grouping-content.html#the-ul-element -HTML_CONTAINER_ELEMENT(ul) - -// ============================================================================= -// Inline Text Semantics Elements -// ============================================================================= - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-a-element -HTML_COMPACT_ELEMENT(a) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-abbr-element -HTML_CONTAINER_ELEMENT(abbr) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-b-element -HTML_COMPACT_ELEMENT(b) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-bdi-element -HTML_CONTAINER_ELEMENT(bdi) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-bdo-element -HTML_CONTAINER_ELEMENT(bdo) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-br-element -HTML_VOID_ELEMENT(br) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-cite-element -HTML_CONTAINER_ELEMENT(cite) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-code-element -HTML_CONTAINER_ELEMENT(code) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-data-element -HTML_CONTAINER_ELEMENT(data) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-dfn-element -HTML_CONTAINER_ELEMENT(dfn) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-em-element -HTML_COMPACT_ELEMENT(em) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-i-element -HTML_COMPACT_ELEMENT(i) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-kbd-element -HTML_CONTAINER_ELEMENT(kbd) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-mark-element -HTML_CONTAINER_ELEMENT(mark) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-q-element -HTML_COMPACT_ELEMENT(q) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-rp-element -HTML_COMPACT_ELEMENT(rp) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-rt-element -HTML_COMPACT_ELEMENT(rt) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-ruby-element -HTML_CONTAINER_ELEMENT(ruby) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-s-element -HTML_COMPACT_ELEMENT(s) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-samp-element -HTML_CONTAINER_ELEMENT(samp) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-small-element -HTML_CONTAINER_ELEMENT(small) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-span-element -HTML_CONTAINER_ELEMENT(span) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-strong-element -HTML_CONTAINER_ELEMENT(strong) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-sub-and-sup-elements -HTML_CONTAINER_ELEMENT(sub) - -HTML_CONTAINER_ELEMENT(sup) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-time-element -HTML_CONTAINER_ELEMENT(time) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-u-element -HTML_COMPACT_ELEMENT(u) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-var-element -HTML_CONTAINER_ELEMENT(var) - -// https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-wbr-element -HTML_VOID_ELEMENT(wbr) - -// ============================================================================= -// Image and Multimedia Elements -// ============================================================================= - -// https://html.spec.whatwg.org/multipage/image-maps.html#the-area-element -HTML_VOID_ATTR_ELEMENT(area) - -// https://html.spec.whatwg.org/multipage/media.html#the-audio-element -HTML_CONTAINER_ELEMENT(audio) - -// https://html.spec.whatwg.org/multipage/embedded-content.html#the-img-element -HTML_VOID_ATTR_ELEMENT(img) - -// https://html.spec.whatwg.org/multipage/image-maps.html#the-map-element -HTML_CONTAINER_ELEMENT(map) - -// https://html.spec.whatwg.org/multipage/media.html#the-track-element -HTML_VOID_ATTR_ELEMENT(track) - -// https://html.spec.whatwg.org/multipage/media.html#the-video-element -HTML_CONTAINER_ELEMENT(video) - -// ============================================================================= -// Embedded Content Elements -// ============================================================================= - -// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-embed-element -HTML_VOID_ATTR_ELEMENT(embed) - -// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-iframe-element -HTML_CONTAINER_ELEMENT(iframe) - -// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-object-element -HTML_CONTAINER_ELEMENT(object) - -// https://html.spec.whatwg.org/multipage/embedded-content.html#the-picture-element -HTML_CONTAINER_ELEMENT(picture) - -// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-portal-element -HTML_CONTAINER_ELEMENT(portal) - -// https://html.spec.whatwg.org/multipage/embedded-content.html#the-source-element -HTML_VOID_ATTR_ELEMENT(source) - -// ============================================================================= -// Scripting Elements -// ============================================================================= - -// https://html.spec.whatwg.org/multipage/canvas.html#the-canvas-element -HTML_CONTAINER_ELEMENT(canvas) - -// https://html.spec.whatwg.org/multipage/scripting.html#the-noscript-element -HTML_CONTAINER_ELEMENT(noscript) - -// https://html.spec.whatwg.org/multipage/scripting.html#the-script-element -HTML_CONTAINER_ELEMENT(script) - -// ============================================================================= -// Demarcating Edits Elements -// ============================================================================= - -// https://html.spec.whatwg.org/multipage/edits.html#the-del-element -HTML_CONTAINER_ELEMENT(del) - -// https://html.spec.whatwg.org/multipage/edits.html#the-ins-element -HTML_CONTAINER_ELEMENT(ins) - -// ============================================================================= -// Table Content Elements -// ============================================================================= - -// https://html.spec.whatwg.org/multipage/tables.html#the-caption-element -HTML_CONTAINER_ELEMENT(caption) - -// https://html.spec.whatwg.org/multipage/tables.html#the-col-element -HTML_VOID_ATTR_ELEMENT(col) - -// https://html.spec.whatwg.org/multipage/tables.html#the-colgroup-element -HTML_CONTAINER_ELEMENT(colgroup) - -// https://html.spec.whatwg.org/multipage/tables.html#the-table-element -HTML_CONTAINER_ELEMENT(table) - -// https://html.spec.whatwg.org/multipage/tables.html#the-tbody-element -HTML_CONTAINER_ELEMENT(tbody) - -// https://html.spec.whatwg.org/multipage/tables.html#the-td-element -HTML_COMPACT_ELEMENT(td) - -// https://html.spec.whatwg.org/multipage/tables.html#the-tfoot-element -HTML_CONTAINER_ELEMENT(tfoot) - -// https://html.spec.whatwg.org/multipage/tables.html#the-th-element -HTML_COMPACT_ELEMENT(th) - -// https://html.spec.whatwg.org/multipage/tables.html#the-thead-element -HTML_CONTAINER_ELEMENT(thead) - -// https://html.spec.whatwg.org/multipage/tables.html#the-tr-element -HTML_CONTAINER_ELEMENT(tr) - -// ============================================================================= -// Forms Elements -// ============================================================================= - -// https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element -HTML_CONTAINER_ELEMENT(button) - -// https://html.spec.whatwg.org/multipage/form-elements.html#the-datalist-element -HTML_CONTAINER_ELEMENT(datalist) - -// https://html.spec.whatwg.org/multipage/form-elements.html#the-fieldset-element -HTML_CONTAINER_ELEMENT(fieldset) - -// https://html.spec.whatwg.org/multipage/forms.html#the-form-element -HTML_CONTAINER_ELEMENT(form) - -// https://html.spec.whatwg.org/multipage/input.html#the-input-element -HTML_VOID_ATTR_ELEMENT(input) - -// https://html.spec.whatwg.org/multipage/forms.html#the-label-element -HTML_CONTAINER_ELEMENT(label) - -// https://html.spec.whatwg.org/multipage/form-elements.html#the-legend-element -HTML_CONTAINER_ELEMENT(legend) - -// https://html.spec.whatwg.org/multipage/form-elements.html#the-meter-element -HTML_CONTAINER_ELEMENT(meter) - -// https://html.spec.whatwg.org/multipage/form-elements.html#the-optgroup-element -HTML_CONTAINER_ELEMENT(optgroup) - -// https://html.spec.whatwg.org/multipage/form-elements.html#the-option-element -HTML_CONTAINER_ELEMENT(option) - -// https://html.spec.whatwg.org/multipage/form-elements.html#the-output-element -HTML_CONTAINER_ELEMENT(output) - -// https://html.spec.whatwg.org/multipage/form-elements.html#the-progress-element -HTML_CONTAINER_ELEMENT(progress) - -// https://html.spec.whatwg.org/multipage/form-elements.html#the-select-element -HTML_CONTAINER_ELEMENT(select) - -// https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element -HTML_CONTAINER_ELEMENT(textarea) - -// ============================================================================= -// Interactive Elements -// ============================================================================= - -// https://html.spec.whatwg.org/multipage/interactive-elements.html#the-details-element -HTML_CONTAINER_ELEMENT(details) - -// https://html.spec.whatwg.org/multipage/interactive-elements.html#the-dialog-element -HTML_CONTAINER_ELEMENT(dialog) - -// https://html.spec.whatwg.org/multipage/interactive-elements.html#the-summary-element -HTML_CONTAINER_ELEMENT(summary) - -// ============================================================================= -// Web Components Elements -// ============================================================================= - -// https://html.spec.whatwg.org/multipage/scripting.html#the-slot-element -HTML_CONTAINER_ELEMENT(slot) - -// https://html.spec.whatwg.org/multipage/scripting.html#the-template-element -HTML_CONTAINER_ELEMENT(template_) - -// Clean up macros to avoid polluting the global namespace -#undef HTML_VOID_ELEMENT -#undef HTML_CONTAINER_ELEMENT -#undef HTML_COMPACT_ELEMENT -#undef HTML_VOID_ATTR_ELEMENT - -} // namespace sourcemeta::one::html - -#endif diff --git a/src/html/include/sourcemeta/one/html_encoder.h b/src/html/include/sourcemeta/one/html_encoder.h deleted file mode 100644 index 2d5d9af4..00000000 --- a/src/html/include/sourcemeta/one/html_encoder.h +++ /dev/null @@ -1,102 +0,0 @@ -#ifndef SOURCEMETA_ONE_HTML_ENCODER_H_ -#define SOURCEMETA_ONE_HTML_ENCODER_H_ - -#include - -#include // std::ostream -#include // std::map -#include // std::string -#include // std::variant, std::holds_alternative, std::get -#include // std::vector - -namespace sourcemeta::one::html { - -using Attributes = std::map; - -// Forward declaration -class HTML; - -// Raw HTML content wrapper for unescaped content -struct RawHTML { - std::string content; - explicit RawHTML(std::string html_content) - : content{std::move(html_content)} {} -}; - -// A node can be either a string (text node), raw HTML content, or another HTML -// element -using Node = std::variant; - -class HTML { -public: - HTML(std::string tag, bool self_closing_tag = false) - : tag_name(std::move(tag)), self_closing(self_closing_tag) {} - - HTML(std::string tag, Attributes tag_attributes, - bool self_closing_tag = false) - : tag_name(std::move(tag)), attributes(std::move(tag_attributes)), - self_closing(self_closing_tag) {} - - HTML(std::string tag, Attributes tag_attributes, std::vector children) - : tag_name(std::move(tag)), attributes(std::move(tag_attributes)), - child_elements(std::move(children)), self_closing(false) {} - - HTML(std::string tag, Attributes tag_attributes, std::vector children) - : tag_name(std::move(tag)), attributes(std::move(tag_attributes)), - self_closing(false) { - child_elements.reserve(children.size()); - for (auto &child_element : children) { - child_elements.emplace_back(std::move(child_element)); - } - } - - HTML(std::string tag, std::vector children) - : tag_name(std::move(tag)), child_elements(std::move(children)), - self_closing(false) {} - - HTML(std::string tag, std::vector children) - : tag_name(std::move(tag)), self_closing(false) { - child_elements.reserve(children.size()); - for (auto &child_element : children) { - child_elements.emplace_back(std::move(child_element)); - } - } - - template - HTML(std::string tag, Attributes tag_attributes, Children &&...children) - : tag_name(std::move(tag)), attributes(std::move(tag_attributes)), - self_closing(false) { - (child_elements.push_back(std::forward(children)), ...); - } - - template - HTML(std::string tag, Children &&...children) - : tag_name(std::move(tag)), self_closing(false) { - (child_elements.push_back(std::forward(children)), ...); - } - - [[nodiscard]] auto render() const -> std::string; - - auto push_back(const Node &child) -> HTML &; - auto push_back(Node &&child) -> HTML &; - - // Stream operator declaration - friend auto operator<<(std::ostream &output_stream, const HTML &html_element) - -> std::ostream &; - -private: - std::string tag_name; - Attributes attributes; - std::vector child_elements; - bool self_closing; - - [[nodiscard]] auto render(const Node &child_element) const -> std::string; -}; - -// Raw HTML content wrapper - DANGER: Content is NOT escaped! -// Use with extreme caution and only with trusted content -auto raw(std::string html_content) -> RawHTML; - -} // namespace sourcemeta::one::html - -#endif diff --git a/src/html/include/sourcemeta/one/html_escape.h b/src/html/include/sourcemeta/one/html_escape.h deleted file mode 100644 index 927bf848..00000000 --- a/src/html/include/sourcemeta/one/html_escape.h +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef SOURCEMETA_ONE_HTML_ESCAPE_H_ -#define SOURCEMETA_ONE_HTML_ESCAPE_H_ - -#include // std::string - -namespace sourcemeta::one::html { - -// HTML character escaping implementation per HTML Living Standard -// See: https://html.spec.whatwg.org/multipage/parsing.html#escapingString -auto escape(std::string &text) -> void; - -} // namespace sourcemeta::one::html - -#endif diff --git a/src/web/CMakeLists.txt b/src/web/CMakeLists.txt index d080e63b..9fb6e1a4 100644 --- a/src/web/CMakeLists.txt +++ b/src/web/CMakeLists.txt @@ -7,9 +7,9 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT one NAME web target_link_libraries(sourcemeta_one_web PUBLIC sourcemeta::core::build) target_link_libraries(sourcemeta_one_web PRIVATE sourcemeta::core::json) +target_link_libraries(sourcemeta_one_web PRIVATE sourcemeta::core::html) target_link_libraries(sourcemeta_one_web PUBLIC sourcemeta::one::configuration) target_link_libraries(sourcemeta_one_web PRIVATE sourcemeta::one::shared) -target_link_libraries(sourcemeta_one_web PRIVATE sourcemeta::one::html) sourcemeta_esbuild_bundle( ENTRYPOINT "${CMAKE_CURRENT_SOURCE_DIR}/scripts/main.js" diff --git a/src/web/helpers.h b/src/web/helpers.h index 2d438bfb..68d33f8f 100644 --- a/src/web/helpers.h +++ b/src/web/helpers.h @@ -1,8 +1,8 @@ #ifndef SOURCEMETA_ONE_WEB_HELPERS_H_ #define SOURCEMETA_ONE_WEB_HELPERS_H_ +#include #include -#include #include #include // assert @@ -14,7 +14,10 @@ namespace sourcemeta::one::html { -inline auto make_breadcrumb(const sourcemeta::core::JSON &breadcrumb) -> HTML { +using namespace sourcemeta::core::html; + +inline auto make_breadcrumb(const sourcemeta::core::JSON &breadcrumb) + -> sourcemeta::core::HTML { assert(breadcrumb.is_array()); assert(!breadcrumb.empty()); auto entries = ol({{"class", "breadcrumb mb-0"}}, @@ -42,7 +45,7 @@ inline auto make_breadcrumb(const sourcemeta::core::JSON &breadcrumb) -> HTML { inline auto make_schema_health_progress_bar(const sourcemeta::core::JSON::Integer health) - -> HTML { + -> sourcemeta::core::HTML { const auto [progress_class, progress_style] = [health]() -> std::pair { if (health > 90) { @@ -60,9 +63,9 @@ make_schema_health_progress_bar(const sourcemeta::core::JSON::Integer health) } }(); - Attributes attributes{{"class", progress_class}}; + sourcemeta::core::HTMLAttributes attributes{{"class", progress_class}}; if (!progress_style.empty()) { - attributes["style"] = progress_style; + attributes.emplace_back("style", progress_style); } return div({{"class", "progress"}, @@ -76,7 +79,7 @@ make_schema_health_progress_bar(const sourcemeta::core::JSON::Integer health) inline auto make_dialect_badge(const sourcemeta::core::JSON::String &base_dialect_uri) - -> HTML { + -> sourcemeta::core::HTML { const auto [short_name, is_current] = [&base_dialect_uri]() -> std::pair { if (base_dialect_uri == "https://json-schema.org/draft/2020-12/schema") { @@ -110,12 +113,12 @@ make_dialect_badge(const sourcemeta::core::JSON::String &base_dialect_uri) } inline auto make_directory_header(const sourcemeta::core::JSON &directory) - -> HTML { + -> sourcemeta::core::HTML { if (!directory.defines("title")) { return div(); } - std::vector children; + std::vector children; if (directory.defines("github") && !directory.at("github").contains('/')) { children.emplace_back( @@ -126,7 +129,7 @@ inline auto make_directory_header(const sourcemeta::core::JSON &directory) {"class", "img-thumbnail me-4"}})); } - std::vector title_section_children; + std::vector title_section_children; title_section_children.emplace_back( h2({{"class", "fw-bold h4"}}, directory.at("title").to_string())); @@ -138,7 +141,7 @@ inline auto make_directory_header(const sourcemeta::core::JSON &directory) if (directory.defines("email") || directory.defines("github") || directory.defines("website")) { - std::vector contact_children; + std::vector contact_children; if (directory.defines("github")) { contact_children.emplace_back( @@ -178,8 +181,9 @@ inline auto make_directory_header(const sourcemeta::core::JSON &directory) std::move(children)); } -inline auto make_file_manager_row(const sourcemeta::core::JSON &entry) -> HTML { - auto type_content = [&entry]() -> HTML { +inline auto make_file_manager_row(const sourcemeta::core::JSON &entry) + -> sourcemeta::core::HTML { + auto type_content = [&entry]() -> sourcemeta::core::HTML { if (entry.at("type").to_string() == "directory") { if (entry.defines("github") && !entry.at("github").contains('/')) { return img( @@ -211,7 +215,7 @@ inline auto make_file_manager_row(const sourcemeta::core::JSON &entry) -> HTML { make_schema_health_progress_bar(entry.at("health").to_integer()))); } -inline auto make_file_manager_table_header() -> HTML { +inline auto make_file_manager_table_header() -> sourcemeta::core::HTML { return thead(tr(th({{"scope", "col"}, {"style", "width: 50px"}}), th({{"scope", "col"}}, "Name"), th({{"scope", "col"}}, "Title"), @@ -220,7 +224,8 @@ inline auto make_file_manager_table_header() -> HTML { th({{"scope", "col"}, {"style", "width: 150px"}}, "Health"))); } -inline auto make_file_manager(const sourcemeta::core::JSON &directory) -> HTML { +inline auto make_file_manager(const sourcemeta::core::JSON &directory) + -> sourcemeta::core::HTML { if (directory.at("entries").empty()) { return div( {{"class", "container-fluid p-4 flex-grow-1"}}, @@ -244,7 +249,7 @@ inline auto make_file_manager(const sourcemeta::core::JSON &directory) -> HTML { } } - std::vector container_children; + std::vector container_children; if (has_regular_entries) { container_children.emplace_back(table( diff --git a/src/web/page.h b/src/web/page.h index bb6f22ba..5f4f5e9d 100644 --- a/src/web/page.h +++ b/src/web/page.h @@ -1,8 +1,8 @@ #ifndef SOURCEMETA_ONE_WEB_PAGE_H_ #define SOURCEMETA_ONE_WEB_PAGE_H_ +#include #include -#include #include #include // std::optional @@ -13,7 +13,10 @@ namespace sourcemeta::one::html { -inline auto make_navigation(const Configuration &configuration) -> HTML { +using namespace sourcemeta::core::html; + +inline auto make_navigation(const Configuration &configuration) + -> sourcemeta::core::HTML { auto container = div({{"class", "container-fluid px-4 py-1 align-items-center " "flex-column flex-md-row"}}, @@ -50,7 +53,7 @@ inline auto make_navigation(const Configuration &configuration) -> HTML { std::move(container)); } -inline auto make_footer() -> HTML { +inline auto make_footer() -> sourcemeta::core::HTML { std::ostringstream information; information << " v" << version() << " © 2025 "; @@ -81,10 +84,10 @@ inline auto make_footer() -> HTML { "Need Help?")))); } -inline auto make_head(const Configuration &configuration, - const std::string &canonical, - const std::string &page_title, - const std::string &description) -> HTML { +inline auto +make_head(const Configuration &configuration, const std::string &canonical, + const std::string &page_title, const std::string &description) + -> sourcemeta::core::HTML { return head(meta({{"charset", "utf-8"}}), meta({{"name", "referrer"}, {"content", "no-referrer"}}), meta({{"name", "viewport"}, @@ -119,8 +122,8 @@ template inline auto make_page(const Configuration &configuration, const std::string &canonical, const std::string &title, const std::string &description, Children &&...children) - -> HTML { - std::vector nodes; + -> sourcemeta::core::HTML { + std::vector nodes; nodes.emplace_back(make_navigation(configuration)); (nodes.emplace_back(std::forward(children)), ...); nodes.emplace_back(make_footer()); @@ -130,9 +133,10 @@ inline auto make_page(const Configuration &configuration, {"src", // For cache busting, to force browsers to refresh styles on any update "/self/static/main.min.js?v=" + std::string{stamp()}}})); - return html({{"class", "h-100"}, {"lang", "en"}}, - make_head(configuration, canonical, title, description), - body({{"class", "h-100 d-flex flex-column"}}, nodes)); + return sourcemeta::core::html::html( + {{"class", "h-100"}, {"lang", "en"}}, + make_head(configuration, canonical, title, description), + body({{"class", "h-100 d-flex flex-column"}}, nodes)); } } // namespace sourcemeta::one::html diff --git a/src/web/pages/directory.cc b/src/web/pages/directory.cc index ea40f377..969b9fdc 100644 --- a/src/web/pages/directory.cc +++ b/src/web/pages/directory.cc @@ -3,7 +3,7 @@ #include "../helpers.h" #include "../page.h" -#include +#include #include #include // std::chrono diff --git a/src/web/pages/index.cc b/src/web/pages/index.cc index 533c87e8..4cc59431 100644 --- a/src/web/pages/index.cc +++ b/src/web/pages/index.cc @@ -3,7 +3,7 @@ #include "../helpers.h" #include "../page.h" -#include +#include #include #include // std::chrono @@ -13,8 +13,8 @@ namespace { auto make_hero(const sourcemeta::one::Configuration &configuration) - -> sourcemeta::one::html::HTML { - using namespace sourcemeta::one::html; + -> sourcemeta::core::HTML { + using namespace sourcemeta::core::html; if (configuration.html->hero.has_value()) { return div({{"class", "container-fluid px-4"}}, div({{"class", "bg-light border border-light-subtle mt-4 " diff --git a/src/web/pages/not_found.cc b/src/web/pages/not_found.cc index a349adea..533e4410 100644 --- a/src/web/pages/not_found.cc +++ b/src/web/pages/not_found.cc @@ -3,7 +3,7 @@ #include "../helpers.h" #include "../page.h" -#include +#include #include #include // std::chrono diff --git a/src/web/pages/schema.cc b/src/web/pages/schema.cc index 5478e5cb..cd350888 100644 --- a/src/web/pages/schema.cc +++ b/src/web/pages/schema.cc @@ -3,7 +3,7 @@ #include "../helpers.h" #include "../page.h" -#include +#include #include #include // assert @@ -32,11 +32,11 @@ auto GENERATE_WEB_SCHEMA::handler( ? meta.at("description").to_string() : ("Schemas located at " + meta.at("path").to_string())}; - using namespace sourcemeta::one::html; + using namespace sourcemeta::core::html; - std::vector container_children; - std::vector content_children; - std::vector header_children; + std::vector container_children; + std::vector content_children; + std::vector header_children; // Title and description if (meta.defines("title")) { @@ -63,7 +63,7 @@ auto GENERATE_WEB_SCHEMA::handler( content_children.emplace_back(div(header_children)); // Information table - std::vector table_rows; + std::vector table_rows; // Identifier row table_rows.emplace_back( @@ -124,7 +124,7 @@ auto GENERATE_WEB_SCHEMA::handler( assert(health.defines("errors")); // Tab navigation - std::vector nav_items; + std::vector nav_items; nav_items.emplace_back(li( {{"class", "nav-item"}}, button( @@ -163,11 +163,11 @@ auto GENERATE_WEB_SCHEMA::handler( ul({{"class", "nav nav-tabs mt-4 mb-3"}}, nav_items)); // Examples tab - std::vector examples_content; + std::vector examples_content; if (meta.at("examples").empty()) { examples_content.emplace_back(p("This schema declares 0 examples.")); } else { - std::vector example_items; + std::vector example_items; for (const auto &example : meta.at("examples").as_array()) { std::ostringstream pretty; sourcemeta::core::prettify(example, pretty); @@ -199,13 +199,13 @@ auto GENERATE_WEB_SCHEMA::handler( << (indirect.size() == 1 ? "dependency" : "dependencies") << "."; - std::vector dependencies_content; + std::vector dependencies_content; dependencies_content.emplace_back(p(dependency_summary.str())); if (direct.size() + indirect.size() > 0) { - std::vector dep_table_rows; + std::vector dep_table_rows; for (const auto &dependency : dependencies_json.as_array()) { - std::vector row_cells; + std::vector row_cells; if (dependency.at("from") == meta.at("identifier")) { std::ostringstream dependency_attribute; @@ -250,7 +250,7 @@ auto GENERATE_WEB_SCHEMA::handler( // Health tab const auto errors_count{health.at("errors").size()}; - std::vector health_content; + std::vector health_content; if (errors_count == 1) { health_content.emplace_back(p( "This schema has " + std::to_string(errors_count) + " quality error.")); @@ -261,13 +261,13 @@ auto GENERATE_WEB_SCHEMA::handler( } if (errors_count > 0) { - std::vector error_items; + std::vector error_items; for (const auto &error : health.at("errors").as_array()) { assert(error.at("pointers").size() >= 1); std::ostringstream pointers; sourcemeta::core::stringify(error.at("pointers"), pointers); - std::vector error_children; + std::vector error_children; error_children.emplace_back( code({{"class", "d-block text-primary"}}, error.at("pointers").front().to_string())); diff --git a/test/unit/html/CMakeLists.txt b/test/unit/html/CMakeLists.txt deleted file mode 100644 index 2aea35d2..00000000 --- a/test/unit/html/CMakeLists.txt +++ /dev/null @@ -1,4 +0,0 @@ -sourcemeta_googletest(NAMESPACE sourcemeta PROJECT one NAME html - SOURCES html_test.cc html_escape_test.cc) - -target_link_libraries(sourcemeta_one_html_unit PRIVATE sourcemeta::one::html) diff --git a/test/unit/html/html_escape_test.cc b/test/unit/html/html_escape_test.cc deleted file mode 100644 index f964e737..00000000 --- a/test/unit/html/html_escape_test.cc +++ /dev/null @@ -1,586 +0,0 @@ -#include - -#include - -// Basic escaping tests -TEST(HTML_escape, empty_string) { - using namespace sourcemeta::one::html; - - std::string text = ""; - escape(text); - EXPECT_EQ(text, ""); -} - -TEST(HTML_escape, no_escape_needed_letters) { - using namespace sourcemeta::one::html; - - std::string text = "hello"; - escape(text); - EXPECT_EQ(text, "hello"); -} - -TEST(HTML_escape, no_escape_needed_alphanumeric) { - using namespace sourcemeta::one::html; - - std::string text = "test123"; - escape(text); - EXPECT_EQ(text, "test123"); -} - -TEST(HTML_escape, no_escape_needed_spaces_only) { - using namespace sourcemeta::one::html; - - std::string text = " "; - escape(text); - EXPECT_EQ(text, " "); -} - -// Ampersand escaping tests -TEST(HTML_escape, ampersand_single) { - using namespace sourcemeta::one::html; - - std::string text = "&"; - escape(text); - EXPECT_EQ(text, "&"); -} - -TEST(HTML_escape, ampersand_in_text) { - using namespace sourcemeta::one::html; - - std::string text = "Tom & Jerry"; - escape(text); - EXPECT_EQ(text, "Tom & Jerry"); -} - -TEST(HTML_escape, ampersand_multiple) { - using namespace sourcemeta::one::html; - - std::string text = "A&B&C"; - escape(text); - EXPECT_EQ(text, "A&B&C"); -} - -TEST(HTML_escape, ampersand_already_escaped) { - using namespace sourcemeta::one::html; - - std::string text = "&"; - escape(text); - EXPECT_EQ(text, "&amp;"); -} - -TEST(HTML_escape, ampersand_business_context) { - using namespace sourcemeta::one::html; - - std::string text = "R&D"; - escape(text); - EXPECT_EQ(text, "R&D"); -} - -// Less-than escaping tests -TEST(HTML_escape, less_than_single) { - using namespace sourcemeta::one::html; - - std::string text = "<"; - escape(text); - EXPECT_EQ(text, "<"); -} - -TEST(HTML_escape, less_than_in_comparison) { - using namespace sourcemeta::one::html; - - std::string text = "x < y"; - escape(text); - EXPECT_EQ(text, "x < y"); -} - -TEST(HTML_escape, less_than_tag_like) { - using namespace sourcemeta::one::html; - - std::string text = ""; - escape(text); - EXPECT_EQ(text, "<script>alert('xss')</script>"); -} - -TEST(HTML_escape, img_onerror_xss) { - using namespace sourcemeta::one::html; - - std::string text = ""; - escape(text); - EXPECT_EQ(text, "<img src='x' onerror='alert(1)'>"); -} - -TEST(HTML_escape, svg_onload_xss) { - using namespace sourcemeta::one::html; - - std::string text = ""; - escape(text); - EXPECT_EQ(text, "<svg onload=alert(1)>"); -} - -TEST(HTML_escape, javascript_url) { - using namespace sourcemeta::one::html; - - std::string text = "javascript:alert('test')"; - escape(text); - EXPECT_EQ(text, "javascript:alert('test')"); -} - -// Mutation XSS prevention (2025 spec changes) -TEST(HTML_escape, mxss_img_in_attribute) { - using namespace sourcemeta::one::html; - - std::string text = "attr=''"; - escape(text); - EXPECT_EQ(text, "attr='<img src=x onerror=alert(1)>'"); -} - -TEST(HTML_escape, mxss_svg_in_attribute) { - using namespace sourcemeta::one::html; - - std::string text = "data=\"\""; - escape(text); - EXPECT_EQ(text, "data="<svg onload=alert('xss')>""); -} - -TEST(HTML_escape, mxss_iframe_javascript) { - using namespace sourcemeta::one::html; - - std::string text = "
"; - escape(text); - EXPECT_EQ(text, "<div class="container">"); -} - -TEST(HTML_escape, input_with_mixed_quotes) { - using namespace sourcemeta::one::html; - - std::string text = ""; - escape(text); - EXPECT_EQ(text, "<input type='text' value="test">"); -} - -TEST(HTML_escape, html_document_end) { - using namespace sourcemeta::one::html; - - std::string text = ""; - escape(text); - EXPECT_EQ(text, "</body></html>"); -} - -TEST(HTML_escape, meta_charset) { - using namespace sourcemeta::one::html; - - std::string text = ""; - escape(text); - EXPECT_EQ(text, "<meta charset="utf-8">"); -} - -// HTML comments -TEST(HTML_escape, basic_html_comment) { - using namespace sourcemeta::one::html; - - std::string text = ""; - escape(text); - EXPECT_EQ(text, "<!-- comment -->"); -} - -TEST(HTML_escape, comment_with_entities) { - using namespace sourcemeta::one::html; - - std::string text = ""; - escape(text); - EXPECT_EQ(text, "<!-- <script> & 'test' -->"); -} - -// CSS and JavaScript content -TEST(HTML_escape, css_with_quotes) { - using namespace sourcemeta::one::html; - - std::string text = "body { color: 'red'; }"; - escape(text); - EXPECT_EQ(text, "body { color: 'red'; }"); -} - -TEST(HTML_escape, css_selector) { - using namespace sourcemeta::one::html; - - std::string text = "div > p"; - escape(text); - EXPECT_EQ(text, "div > p"); -} - -TEST(HTML_escape, javascript_condition) { - using namespace sourcemeta::one::html; - - std::string text = "if (x < y && z > 'test') { alert(\"hello\"); }"; - escape(text); - EXPECT_EQ(text, "if (x < y && z > 'test') { " - "alert("hello"); }"); -} - -// CDATA sections -TEST(HTML_escape, cdata_basic) { - using namespace sourcemeta::one::html; - - std::string text = ""; - escape(text); - EXPECT_EQ(text, "<![CDATA[content]]>"); -} - -TEST(HTML_escape, cdata_with_entities) { - using namespace sourcemeta::one::html; - - std::string text = " & 'test']]>"; - escape(text); - EXPECT_EQ(text, "<![CDATA[<tag> & 'test']]>"); -} - -// URL scenarios -TEST(HTML_escape, url_with_query_params) { - using namespace sourcemeta::one::html; - - std::string text = "http://example.com?param='value'&other=\"test\""; - escape(text); - EXPECT_EQ( - text, - "http://example.com?param='value'&other="test""); -} - -TEST(HTML_escape, search_url_with_script) { - using namespace sourcemeta::one::html; - - std::string text = "search?q=")); - - EXPECT_EQ(result.str(), "
"); -} - -TEST(HTML, raw_html_mixed_with_escaped) { - using namespace sourcemeta::one::html; - - std::ostringstream result; - result << div("Safe text: "}}, - "Content"); - - EXPECT_EQ( - result.str(), - "
Content
"); -} - -TEST(HTML, empty_attribute_values) { - using namespace sourcemeta::one::html; - - std::ostringstream result; - result << input( - {{"type", "text"}, {"value", ""}, {"placeholder", "Enter text"}}); - - EXPECT_EQ(result.str(), - ""); -} - -TEST(HTML, nested_html_elements) { - using namespace sourcemeta::one::html; - - std::ostringstream result; - result << div( - p("First paragraph"), - div({{"class", "nested"}}, span("Nested content"), strong("Important")), - p("Last paragraph")); - - EXPECT_EQ(result.str(), - "

First paragraph

" - "Nested contentImportant
" - "

Last paragraph

"); -} - -TEST(HTML, complex_table_structure) { - using namespace sourcemeta::one::html; - - std::ostringstream result; - result << table(thead(tr(th("Name"), th("Age"), th("City"))), - tbody(tr(td("John"), td("25"), td("NYC")), - tr(td("Jane"), td("30"), td("LA"))), - tfoot(tr(td({{"colspan", "3"}}, "2 rows total")))); - - std::string expected = - "" - "" - "" - "
NameAgeCity
John25NYC
Jane30LA
2 rows " - "total
"; - - EXPECT_EQ(result.str(), expected); -} - -TEST(HTML, form_elements_combination) { - using namespace sourcemeta::one::html; - - std::ostringstream result; - result << form( - {{"action", "/submit"}, {"method", "post"}}, - fieldset(legend("Personal Info"), label({{"for", "name"}}, "Name:"), - input({{"type", "text"}, {"id", "name"}, {"name", "name"}}), - label({{"for", "age"}}, "Age:"), - select({{"id", "age"}, {"name", "age"}}, - option({{"value", ""}}, "Select age"), - option({{"value", "18-25"}}, "18-25"), - option({{"value", "26-35"}}, "26-35"))), - button({{"type", "submit"}}, "Submit")); - - std::string expected = "
" - "
Personal Info" - "" - "" - "" - "
" - "
"; - - EXPECT_EQ(result.str(), expected); -} - -TEST(HTML, unicode_and_special_characters) { - using namespace sourcemeta::one::html; - - std::ostringstream result; - result << p("Unicode: 你好世界 🌍 ñáéíóú"); - - EXPECT_EQ(result.str(), "

Unicode: 你好世界 🌍 ñáéíóú

"); -} - -TEST(HTML, whitespace_handling) { - using namespace sourcemeta::one::html; - - std::ostringstream result; - result << pre(" Whitespace\n should be\n preserved "); - - EXPECT_EQ(result.str(), - "
  Whitespace\n  should be\n    preserved  
"); -} - -TEST(HTML, mixed_raw_and_escaped_complex) { - using namespace sourcemeta::one::html; - - std::ostringstream result; - result << article(h2("Article Title & "), - p("Normal text with ", em("emphasis"), " and ", - raw("highlighted"), " parts."), - raw(""), - p("More content & special chars")); - - std::string expected = "
" - "

Article Title & <subtitle>

" - "

Normal text with emphasis and " - "highlighted parts.

" - "" - "

More content & special chars

" - "
"; - - EXPECT_EQ(result.str(), expected); -} - -TEST(HTML, semantic_html5_elements) { - using namespace sourcemeta::one::html; - - std::ostringstream result; - result << main({{"role", "main"}}, - header(nav(ul(li(a({{"href", "/"}}, "Home")), - li(a({{"href", "/about"}}, "About"))))), - section(article(h1("Article Title"), p("Article content")), - aside("Sidebar content")), - footer("Copyright 2024")); - - std::string expected = "
" - "
" - "
" - "

Article Title

" - "

Article content

" - "
" - "
" - "
Copyright 2024
" - "
"; - - EXPECT_EQ(result.str(), expected); -} - -TEST(HTML, attribute_order_consistency) { - using namespace sourcemeta::one::html; - - // Since std::map orders keys lexicographically, attributes should be - // consistent - std::ostringstream result; - result << div({{"z-index", "1"}, {"class", "test"}, {"id", "main"}}, - "Content"); - - EXPECT_EQ(result.str(), - "
Content
"); -} - -TEST(HTML, zero_length_strings) { - using namespace sourcemeta::one::html; - - EXPECT_EQ(p("").render(), "

"); - EXPECT_EQ(span({{"class", ""}}, "").render(), ""); -} - -TEST(HTML, push_back_string) { - using namespace sourcemeta::one::html; - - auto element = div(); - element.push_back(std::string("Hello World")); - - EXPECT_EQ(element.render(), "
Hello World
"); -} - -TEST(HTML, push_back_string_chaining) { - using namespace sourcemeta::one::html; - - auto element = div() - .push_back(std::string("First")) - .push_back(std::string(" ")) - .push_back(std::string("Second")); - - EXPECT_EQ(element.render(), "
First Second
"); -} - -TEST(HTML, push_back_html_element) { - using namespace sourcemeta::one::html; - - auto element = div(); - element.push_back(span("Nested span")); - - EXPECT_EQ(element.render(), "
Nested span
"); -} - -TEST(HTML, push_back_html_element_chaining) { - using namespace sourcemeta::one::html; - - auto element = div() - .push_back(h1("Title")) - .push_back(p("Paragraph")) - .push_back(span("Footer")); - - EXPECT_EQ(element.render(), - "

Title

Paragraph

Footer
"); -} - -TEST(HTML, push_back_raw_html) { - using namespace sourcemeta::one::html; - - auto element = div(); - element.push_back(raw("Bold text")); - - EXPECT_EQ(element.render(), "
Bold text
"); -} - -TEST(HTML, push_back_raw_html_chaining) { - using namespace sourcemeta::one::html; - - auto element = div() - .push_back(raw("Italic")) - .push_back(std::string(" and ")) - .push_back(raw("Bold")); - - EXPECT_EQ(element.render(), - "
Italic and Bold
"); -} - -TEST(HTML, push_back_mixed_content) { - using namespace sourcemeta::one::html; - - auto element = div(); - element.push_back(std::string("Text: ")) - .push_back(span("Nested")) - .push_back(std::string(" & ")) - .push_back(raw("Raw HTML")); - - EXPECT_EQ(element.render(), - "
Text: Nested & Raw HTML
"); -} - -TEST(HTML, push_back_with_attributes) { - using namespace sourcemeta::one::html; - - auto element = div({{"class", "container"}, {"id", "main"}}); - element.push_back(std::string("Content")).push_back(p("Paragraph")); - - EXPECT_EQ( - element.render(), - "
Content

Paragraph

"); -} - -TEST(HTML, push_back_to_existing_children) { - using namespace sourcemeta::one::html; - - auto element = div("Initial content"); - element.push_back(std::string(" ")).push_back(span("Added span")); - - EXPECT_EQ(element.render(), - "
Initial content Added span
"); -} - -TEST(HTML, push_back_complex_nesting) { - using namespace sourcemeta::one::html; - - auto list = ul(); - list.push_back(li("Item 1")) - .push_back(li().push_back(std::string("Item 2 with ")) - .push_back(strong("emphasis"))) - .push_back(li().push_back(raw("Item 3"))); - - EXPECT_EQ(list.render(), - "
  • Item 1
  • Item 2 with emphasis
  • " - "
  • Item 3
"); -} - -TEST(HTML, push_back_escaped_content) { - using namespace sourcemeta::one::html; - - auto element = div(); - element.push_back(std::string("Safe: ")); - - EXPECT_EQ( - element.render(), - "
Safe: <script>alert('xss')</script>
"); -} - -TEST(HTML, push_back_return_reference) { - using namespace sourcemeta::one::html; - - auto element = div(); - auto &ref = element.push_back(std::string("test")); - - // Verify that push_back returns a reference to the same object - EXPECT_EQ(&ref, &element); -} diff --git a/vendor/core/CMakeLists.txt b/vendor/core/CMakeLists.txt index 4edf68f0..e6914a55 100644 --- a/vendor/core/CMakeLists.txt +++ b/vendor/core/CMakeLists.txt @@ -19,6 +19,7 @@ option(SOURCEMETA_CORE_JSONSCHEMA "Build the Sourcemeta Core JSON Schema library option(SOURCEMETA_CORE_JSONPOINTER "Build the Sourcemeta Core JSON Pointer library" ON) option(SOURCEMETA_CORE_JSONL "Build the Sourcemeta Core JSONL library" ON) option(SOURCEMETA_CORE_YAML "Build the Sourcemeta Core YAML library" ON) +option(SOURCEMETA_CORE_HTML "Build the Sourcemeta Core HTML library" ON) option(SOURCEMETA_CORE_EXTENSION_ALTERSCHEMA "Build the Sourcemeta Core AlterSchema library" ON) option(SOURCEMETA_CORE_EXTENSION_EDITORSCHEMA "Build the Sourcemeta Core EditorSchema library" ON) option(SOURCEMETA_CORE_EXTENSION_SCHEMACONFIG "Build the Sourcemeta Core SchemaConfig library" ON) @@ -126,6 +127,10 @@ if(SOURCEMETA_CORE_YAML) add_subdirectory(src/core/yaml) endif() +if(SOURCEMETA_CORE_HTML) + add_subdirectory(src/core/html) +endif() + if(SOURCEMETA_CORE_EXTENSION_ALTERSCHEMA) add_subdirectory(src/extension/alterschema) endif() @@ -241,6 +246,10 @@ if(SOURCEMETA_CORE_TESTS) add_subdirectory(test/yaml) endif() + if(SOURCEMETA_CORE_HTML) + add_subdirectory(test/html) + endif() + if(SOURCEMETA_CORE_EXTENSION_ALTERSCHEMA) add_subdirectory(test/alterschema) endif() diff --git a/vendor/core/config.cmake.in b/vendor/core/config.cmake.in index c3d107df..2e23e907 100644 --- a/vendor/core/config.cmake.in +++ b/vendor/core/config.cmake.in @@ -20,6 +20,7 @@ if(NOT SOURCEMETA_CORE_COMPONENTS) list(APPEND SOURCEMETA_CORE_COMPONENTS jsonpointer) list(APPEND SOURCEMETA_CORE_COMPONENTS jsonschema) list(APPEND SOURCEMETA_CORE_COMPONENTS yaml) + list(APPEND SOURCEMETA_CORE_COMPONENTS html) list(APPEND SOURCEMETA_CORE_COMPONENTS alterschema) list(APPEND SOURCEMETA_CORE_COMPONENTS editorschema) list(APPEND SOURCEMETA_CORE_COMPONENTS schemaconfig) @@ -91,6 +92,8 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) find_dependency(yaml CONFIG) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_yaml.cmake") + elseif(component STREQUAL "html") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_html.cmake") elseif(component STREQUAL "alterschema") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_uri.cmake") find_dependency(mpdecimal CONFIG) diff --git a/vendor/core/src/core/html/CMakeLists.txt b/vendor/core/src/core/html/CMakeLists.txt new file mode 100644 index 00000000..6b35797f --- /dev/null +++ b/vendor/core/src/core/html/CMakeLists.txt @@ -0,0 +1,7 @@ +sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME html + PRIVATE_HEADERS escape.h encoder.h elements.h + SOURCES escape.cc encoder.cc) + +if(SOURCEMETA_CORE_INSTALL) + sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME html) +endif() diff --git a/vendor/core/src/core/html/encoder.cc b/vendor/core/src/core/html/encoder.cc new file mode 100644 index 00000000..ffddbc5e --- /dev/null +++ b/vendor/core/src/core/html/encoder.cc @@ -0,0 +1,74 @@ +#include + +#include // std::ostream +#include // std::ostringstream +#include // std::string + +namespace sourcemeta::core { + +auto HTML::render() const -> std::string { + std::ostringstream output_stream; + output_stream << "<" << this->tag_name; + + // Render attributes + for (const auto &[attribute_name, attribute_value] : this->attributes) { + std::string escaped_value{attribute_value}; + html_escape(escaped_value); + output_stream << " " << attribute_name << "=\"" << escaped_value << "\""; + } + + if (this->self_closing) { + output_stream << " />"; + return output_stream.str(); + } + + output_stream << ">"; + + // Render children + if (this->child_elements.empty()) { + output_stream << "tag_name << ">"; + } else if (this->child_elements.size() == 1 && + std::get_if(&this->child_elements[0])) { + // Inline single text node + output_stream << this->render(this->child_elements[0]); + output_stream << "tag_name << ">"; + } else { + // Block level children + for (const auto &child_element : this->child_elements) { + output_stream << this->render(child_element); + } + output_stream << "tag_name << ">"; + } + + return output_stream.str(); +} + +auto HTML::render(const HTMLNode &child_element) const -> std::string { + if (const auto *text = std::get_if(&child_element)) { + std::string escaped_text{*text}; + html_escape(escaped_text); + return escaped_text; + } else if (const auto *raw_html = std::get_if(&child_element)) { + return raw_html->content; + } else if (const auto *html_element = std::get_if(&child_element)) { + return html_element->render(); + } + return ""; +} + +auto HTML::push_back(const HTMLNode &child) -> HTML & { + this->child_elements.push_back(child); + return *this; +} + +auto HTML::push_back(HTMLNode &&child) -> HTML & { + this->child_elements.push_back(std::move(child)); + return *this; +} + +auto operator<<(std::ostream &output_stream, const HTML &html_element) + -> std::ostream & { + return output_stream << html_element.render(); +} + +} // namespace sourcemeta::core diff --git a/src/html/escape.cc b/vendor/core/src/core/html/escape.cc similarity index 93% rename from src/html/escape.cc rename to vendor/core/src/core/html/escape.cc index 1b9cd5b9..6070d729 100644 --- a/src/html/escape.cc +++ b/vendor/core/src/core/html/escape.cc @@ -1,10 +1,10 @@ -#include +#include #include // std::string -namespace sourcemeta::one::html { +namespace sourcemeta::core { -auto escape(std::string &text) -> void { +auto html_escape(std::string &text) -> void { std::size_t write_position{0}; std::size_t original_size{text.size()}; @@ -93,4 +93,4 @@ auto escape(std::string &text) -> void { } } -} // namespace sourcemeta::one::html +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/html/include/sourcemeta/core/html.h b/vendor/core/src/core/html/include/sourcemeta/core/html.h new file mode 100644 index 00000000..b2a09289 --- /dev/null +++ b/vendor/core/src/core/html/include/sourcemeta/core/html.h @@ -0,0 +1,17 @@ +#ifndef SOURCEMETA_CORE_HTML_H_ +#define SOURCEMETA_CORE_HTML_H_ + +/// @defgroup html HTML +/// @brief A growing implementation of HTML generation utilities per the HTML +/// Living Standard. +/// +/// This functionality is included as follows: +/// +/// ```cpp +/// #include +/// ``` + +#include +#include + +#endif diff --git a/vendor/core/src/core/html/include/sourcemeta/core/html_elements.h b/vendor/core/src/core/html/include/sourcemeta/core/html_elements.h new file mode 100644 index 00000000..7eacddfa --- /dev/null +++ b/vendor/core/src/core/html/include/sourcemeta/core/html_elements.h @@ -0,0 +1,447 @@ +#ifndef SOURCEMETA_CORE_HTML_ELEMENTS_H_ +#define SOURCEMETA_CORE_HTML_ELEMENTS_H_ + +#include + +namespace sourcemeta::core::html { + +#ifndef DOXYGEN +#define HTML_VOID_ELEMENT(name) \ + inline auto name() -> HTML { return HTML(#name, true); } \ + inline auto name(HTMLAttributes attributes) -> HTML { \ + return HTML(#name, std::move(attributes), true); \ + } + +#define HTML_CONTAINER_ELEMENT(name) \ + inline auto name(HTMLAttributes attributes) -> HTML { \ + return HTML(#name, std::move(attributes)); \ + } \ + template \ + inline auto name(HTMLAttributes attributes, Children &&...children) \ + -> HTML { \ + return HTML(#name, std::move(attributes), \ + std::forward(children)...); \ + } \ + template \ + inline auto name(Children &&...children) -> HTML { \ + return HTML(#name, std::forward(children)...); \ + } + +#define HTML_COMPACT_ELEMENT(name) \ + inline auto name(HTMLAttributes attributes) -> HTML { \ + return HTML(#name, std::move(attributes)); \ + } \ + template \ + inline auto name(HTMLAttributes attributes, Children &&...children) \ + -> HTML { \ + return HTML(#name, std::move(attributes), \ + std::forward(children)...); \ + } \ + template \ + inline auto name(Children &&...children) -> HTML { \ + return HTML(#name, std::forward(children)...); \ + } + +#define HTML_VOID_ATTR_ELEMENT(name) \ + inline auto name(HTMLAttributes attributes) -> HTML { \ + return HTML(#name, std::move(attributes), true); \ + } +#endif + +/// @ingroup html +inline auto raw(std::string html_content) -> HTMLRaw { + return HTMLRaw{std::move(html_content)}; +} + +// ============================================================================= +// Document Structure Elements +// ============================================================================= + +/// @ingroup html +HTML_CONTAINER_ELEMENT(html) + +/// @ingroup html +HTML_VOID_ATTR_ELEMENT(base) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(head) + +/// @ingroup html +HTML_VOID_ATTR_ELEMENT(link) + +/// @ingroup html +HTML_VOID_ATTR_ELEMENT(meta) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(style) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(title) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(body) + +// ============================================================================= +// Content Sectioning Elements +// ============================================================================= + +/// @ingroup html +HTML_CONTAINER_ELEMENT(address) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(article) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(aside) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(footer) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(header) + +/// @ingroup html +HTML_COMPACT_ELEMENT(h1) +/// @ingroup html +HTML_COMPACT_ELEMENT(h2) +/// @ingroup html +HTML_COMPACT_ELEMENT(h3) +/// @ingroup html +HTML_COMPACT_ELEMENT(h4) +/// @ingroup html +HTML_COMPACT_ELEMENT(h5) +/// @ingroup html +HTML_COMPACT_ELEMENT(h6) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(hgroup) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(main) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(nav) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(section) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(search) + +// ============================================================================= +// Text Content Elements +// ============================================================================= + +/// @ingroup html +HTML_CONTAINER_ELEMENT(blockquote) + +/// @ingroup html +HTML_COMPACT_ELEMENT(dd) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(div) + +/// @ingroup html +HTML_COMPACT_ELEMENT(dl) + +/// @ingroup html +HTML_COMPACT_ELEMENT(dt) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(figcaption) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(figure) + +/// @ingroup html +HTML_VOID_ELEMENT(hr) + +/// @ingroup html +HTML_COMPACT_ELEMENT(li) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(menu) + +/// @ingroup html +HTML_COMPACT_ELEMENT(ol) + +/// @ingroup html +HTML_COMPACT_ELEMENT(p) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(pre) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(ul) + +// ============================================================================= +// Inline Text Semantics Elements +// ============================================================================= + +/// @ingroup html +HTML_COMPACT_ELEMENT(a) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(abbr) + +/// @ingroup html +HTML_COMPACT_ELEMENT(b) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(bdi) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(bdo) + +/// @ingroup html +HTML_VOID_ELEMENT(br) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(cite) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(code) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(data) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(dfn) + +/// @ingroup html +HTML_COMPACT_ELEMENT(em) + +/// @ingroup html +HTML_COMPACT_ELEMENT(i) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(kbd) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(mark) + +/// @ingroup html +HTML_COMPACT_ELEMENT(q) + +/// @ingroup html +HTML_COMPACT_ELEMENT(rp) + +/// @ingroup html +HTML_COMPACT_ELEMENT(rt) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(ruby) + +/// @ingroup html +HTML_COMPACT_ELEMENT(s) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(samp) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(small) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(span) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(strong) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(sub) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(sup) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(time) + +/// @ingroup html +HTML_COMPACT_ELEMENT(u) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(var) + +/// @ingroup html +HTML_VOID_ELEMENT(wbr) + +// ============================================================================= +// Image and Multimedia Elements +// ============================================================================= + +/// @ingroup html +HTML_VOID_ATTR_ELEMENT(area) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(audio) + +/// @ingroup html +HTML_VOID_ATTR_ELEMENT(img) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(map) + +/// @ingroup html +HTML_VOID_ATTR_ELEMENT(track) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(video) + +// ============================================================================= +// Embedded Content Elements +// ============================================================================= + +/// @ingroup html +HTML_VOID_ATTR_ELEMENT(embed) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(iframe) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(object) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(picture) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(portal) + +/// @ingroup html +HTML_VOID_ATTR_ELEMENT(source) + +// ============================================================================= +// Scripting Elements +// ============================================================================= + +/// @ingroup html +HTML_CONTAINER_ELEMENT(canvas) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(noscript) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(script) + +// ============================================================================= +// Demarcating Edits Elements +// ============================================================================= + +/// @ingroup html +HTML_CONTAINER_ELEMENT(del) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(ins) + +// ============================================================================= +// Table Content Elements +// ============================================================================= + +/// @ingroup html +HTML_CONTAINER_ELEMENT(caption) + +/// @ingroup html +HTML_VOID_ATTR_ELEMENT(col) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(colgroup) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(table) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(tbody) + +/// @ingroup html +HTML_COMPACT_ELEMENT(td) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(tfoot) + +/// @ingroup html +HTML_COMPACT_ELEMENT(th) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(thead) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(tr) + +// ============================================================================= +// Forms Elements +// ============================================================================= + +/// @ingroup html +HTML_CONTAINER_ELEMENT(button) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(datalist) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(fieldset) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(form) + +/// @ingroup html +HTML_VOID_ATTR_ELEMENT(input) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(label) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(legend) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(meter) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(optgroup) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(option) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(output) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(progress) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(select) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(textarea) + +// ============================================================================= +// Interactive Elements +// ============================================================================= + +/// @ingroup html +HTML_CONTAINER_ELEMENT(details) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(dialog) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(summary) + +// ============================================================================= +// Web Components Elements +// ============================================================================= + +/// @ingroup html +HTML_CONTAINER_ELEMENT(slot) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(template_) + +#ifndef DOXYGEN +#undef HTML_VOID_ELEMENT +#undef HTML_CONTAINER_ELEMENT +#undef HTML_COMPACT_ELEMENT +#undef HTML_VOID_ATTR_ELEMENT +#endif + +} // namespace sourcemeta::core::html + +#endif diff --git a/vendor/core/src/core/html/include/sourcemeta/core/html_encoder.h b/vendor/core/src/core/html/include/sourcemeta/core/html_encoder.h new file mode 100644 index 00000000..7be0433e --- /dev/null +++ b/vendor/core/src/core/html/include/sourcemeta/core/html_encoder.h @@ -0,0 +1,145 @@ +#ifndef SOURCEMETA_CORE_HTML_ENCODER_H_ +#define SOURCEMETA_CORE_HTML_ENCODER_H_ + +#ifndef SOURCEMETA_CORE_HTML_EXPORT +#include +#endif + +#include + +#include // std::ostream +#include // std::string +#include // std::pair +#include // std::variant, std::holds_alternative, std::get +#include // std::vector + +namespace sourcemeta::core { + +/// @ingroup html +using HTMLAttributes = std::vector>; + +#ifndef DOXYGEN +// Forward declaration +class HTML; +#endif + +/// @ingroup html +/// Raw HTML content wrapper for unescaped content +struct SOURCEMETA_CORE_HTML_EXPORT HTMLRaw { +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251) +#endif + std::string content; +#if defined(_MSC_VER) +#pragma warning(default : 4251) +#endif + explicit HTMLRaw(std::string html_content) + : content{std::move(html_content)} {} +}; + +/// @ingroup html +/// A node can be either a string (text node), raw HTML content, or another HTML +/// element +using HTMLNode = std::variant; + +/// @ingroup html +/// An HTML element that can be rendered to a string. Elements can contain +/// attributes and child nodes. +/// +/// For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// using namespace sourcemeta::core::html; +/// +/// std::ostringstream result; +/// result << div(h1("Title"), p("Content")); +/// assert(result.str() == "

Title

Content

"); +/// ``` +class SOURCEMETA_CORE_HTML_EXPORT HTML { +public: + HTML(std::string tag, bool self_closing_tag = false) + : tag_name(std::move(tag)), self_closing(self_closing_tag) {} + + HTML(std::string tag, HTMLAttributes tag_attributes, + bool self_closing_tag = false) + : tag_name(std::move(tag)), attributes(std::move(tag_attributes)), + self_closing(self_closing_tag) {} + + HTML(std::string tag, HTMLAttributes tag_attributes, + std::vector children) + : tag_name(std::move(tag)), attributes(std::move(tag_attributes)), + child_elements(std::move(children)), self_closing(false) {} + + HTML(std::string tag, HTMLAttributes tag_attributes, + std::vector children) + : tag_name(std::move(tag)), attributes(std::move(tag_attributes)), + self_closing(false) { + this->child_elements.reserve(children.size()); + for (auto &child_element : children) { + this->child_elements.emplace_back(std::move(child_element)); + } + } + + HTML(std::string tag, std::vector children) + : tag_name(std::move(tag)), child_elements(std::move(children)), + self_closing(false) {} + + HTML(std::string tag, std::vector children) + : tag_name(std::move(tag)), self_closing(false) { + this->child_elements.reserve(children.size()); + for (auto &child_element : children) { + this->child_elements.emplace_back(std::move(child_element)); + } + } + + template + HTML(std::string tag, HTMLAttributes tag_attributes, Children &&...children) + : tag_name(std::move(tag)), attributes(std::move(tag_attributes)), + self_closing(false) { + (this->child_elements.push_back(std::forward(children)), ...); + } + + template + HTML(std::string tag, Children &&...children) + : tag_name(std::move(tag)), self_closing(false) { + (this->child_elements.push_back(std::forward(children)), ...); + } + + [[nodiscard]] auto render() const -> std::string; + + auto push_back(const HTMLNode &child) -> HTML &; + auto push_back(HTMLNode &&child) -> HTML &; + + // Stream operator declaration + friend SOURCEMETA_CORE_HTML_EXPORT auto + operator<<(std::ostream &output_stream, const HTML &html_element) + -> std::ostream &; + +private: +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251) +#endif + std::string tag_name; + HTMLAttributes attributes; + std::vector child_elements; +#if defined(_MSC_VER) +#pragma warning(default : 4251) +#endif + bool self_closing; + + [[nodiscard]] auto render(const HTMLNode &child_element) const -> std::string; +}; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/html/include/sourcemeta/core/html_escape.h b/vendor/core/src/core/html/include/sourcemeta/core/html_escape.h new file mode 100644 index 00000000..2d5d11e8 --- /dev/null +++ b/vendor/core/src/core/html/include/sourcemeta/core/html_escape.h @@ -0,0 +1,38 @@ +#ifndef SOURCEMETA_CORE_HTML_ESCAPE_H_ +#define SOURCEMETA_CORE_HTML_ESCAPE_H_ + +#ifndef SOURCEMETA_CORE_HTML_EXPORT +#include +#endif + +#include // std::string + +namespace sourcemeta::core { + +/// @ingroup html +/// HTML character escaping implementation per HTML Living Standard. +/// See: https://html.spec.whatwg.org/multipage/parsing.html#escapingString +/// +/// This function escapes the five HTML special characters in-place: +/// - `&` becomes `&` +/// - `<` becomes `<` +/// - `>` becomes `>` +/// - `"` becomes `"` +/// - `'` becomes `'` +/// +/// For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// std::string text{""}; +/// sourcemeta::core::html_escape(text); +/// assert(text == "<script>alert('xss')</script>"); +/// ``` +SOURCEMETA_CORE_HTML_EXPORT +auto html_escape(std::string &text) -> void; + +} // namespace sourcemeta::core + +#endif