Skip to content

Commit

Permalink
Implement utilities to safely create HTML content
Browse files Browse the repository at this point in the history
Signed-off-by: Juan Cruz Viotti <[email protected]>
  • Loading branch information
jviotti committed Jan 8, 2025
1 parent fa92900 commit 93751aa
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 131 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -march=native")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -mtune=native")

if(REGISTRY_INDEX)
add_subdirectory(src/html)
add_subdirectory(src/index)
endif()

Expand Down
270 changes: 156 additions & 114 deletions src/enterprise/enterprise_html.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,146 +2,188 @@
#define SOURCEMETA_REGISTRY_ENTERPRISE_HTML_H_

#include <sourcemeta/jsontoolkit/json.h>
#include <sourcemeta/registry/html.h>

#include <filesystem> // std::filesystem
#include <optional> // std::optional
#include <string> // std::string
#include <optional> // std::optional
#include <string> // std::string

#include "configure.h"

namespace sourcemeta::registry::enterprise {

template <typename T>
auto html_start(T &html, const sourcemeta::jsontoolkit::JSON &configuration,
auto html_navigation(T &output,
const sourcemeta::jsontoolkit::JSON &configuration)
-> void {
output.open("nav", {{"class", "navbar navbar-expand border-bottom bg-body"}});
output.open("div", {{"class", "container-fluid px-4 py-1 align-items-center "
"flex-column flex-sm-row"}});

output.open("a", {{"class", "navbar-brand"},
{"href", configuration.at("url").to_string()}});
sourcemeta::registry::html::partials::image(
output, "/static/icon.svg", 30, configuration.at("title").to_string(),
"me-2");
output.open("span", {{"class", "align-middle fw-bold"}})
.text(configuration.at("title").to_string())
.close("span")
.open("span", {{"class", "align-middle fw-lighter"}})
.text(" Schemas")
.close("span");
output.close("a");

output.open("div", {{"class", "mt-3 mt-sm-0 flex-grow-1 position-relative"}})
.open("div", {{"class", "input-group"}})
.open("span", {{"class", "input-group-text"}})
.open("i", {{"class", "bi bi-search"}})
.close("i")
.close("span")
.open("input", {{"class", "form-control"},
{"type", "search"},
{"id", "search"},
{"placeholder", "Search"},
{"aria-label", "Search"},
{"autocomplete", "off"}})
.close("div")
.open("ul", {{"class",
"d-none list-group position-absolute w-100 mt-2 shadow-sm"},
{"id", "search-result"}})
.close("ul")
.close("div");

if (configuration.defines("action")) {
output.open("a",
{{"class", "ms-3 btn btn-dark"},
{"role", "button"},
{"href", configuration.at("action").at("url").to_string()}});

if (configuration.at("action").defines("icon")) {
output
.open("i", {{"class",
"me-2 bi bi-" +
configuration.at("action").at("icon").to_string()}})
.close("i");
}

output.text(configuration.at("action").at("title").to_string());
output.close("a");
}

output.close("div");
output.close("nav");
}

template <typename T> auto html_footer(T &output) -> void {
output.open("footer",
{{"class", "border-top text-secondary py-3 d-flex "
"align-items-center justify-content-between"}});
output.open("small")
.open("a", {{"href", "https://github.com/sourcemeta/registry"},
{"class", "text-secondary"},
{"target", "_blank"}})
.text("Registry")
.close("a")
.text(" v")
.text(sourcemeta::registry::PROJECT_VERSION)
#ifdef SOURCEMETA_REGISTRY_ENTERPRISE
.text(" (Enterprise Edition)")
#else
.text(" (Community Edition)")
#endif
.text(" © 2025 ")
.open("a", {{"href", "https://www.sourcemeta.com"},
{"class", "text-secondary"},
{"target", "_blank"}})
.text("Sourcemeta, Ltd.")
.close("a")
.close("small");
output.open("small")
.open("a",
{{"href", "https://github.com/sourcemeta/registry/discussions"},
{"class", "text-secondary"},
{"target", "_blank"}})
.text("Need Help?")
.close("a")
.close("small");
output.close("footer");
}

template <typename T>
auto html_start(T &output, const sourcemeta::jsontoolkit::JSON &configuration,
const std::string &title, const std::string &description,
const std::optional<std::string> &path) -> void {
const auto &base_url{configuration.at("url").to_string()};

html << "<!DOCTYPE html>";
html << "<html class=\"h-100\" lang=\"en\">";
html << "<head>";
output.doctype();
output.open("html", {{"class", "h-100"}, {"lang", "en"}});
output.open("head");

// Meta headers
html << "<meta charset=\"utf-8\">";
html << "<meta name=\"referrer\" content=\"no-referrer\">";
html << "<meta name=\"viewport\" content=\"width=device-width, "
"initial-scale=1.0\">";
html << "<meta http-equiv=\"x-ua-compatible\" content=\"ie=edge\">";
output.open("meta", {{"charset", "utf-8"}})
.open("meta", {{"name", "referrer"}, {"content", "no-referrer"}})
.open("meta", {{"name", "viewport"},
{"content", "width=device-width, initial-scale=1.0"}})
.open("meta",
{{"http-equiv", "x-ua-compatible"}, {"content", "ie=edge"}});

// Site metadata
html << "<title>" << title << "</title>";
html << "<meta name=\"description\" content=\"" << description << "\">";
output.open("title").text(title).close("title");
output.open("meta", {{"name", "description"}, {"content", description}});
if (path.has_value()) {
html << "<link rel=\"canonical\" href=\"" << base_url << path.value()
<< "\">";
output.open("link",
{{"rel", "canonical"},
{"href", configuration.at("url").to_string() + path.value()}});
}
html << "<link rel=\"stylesheet\" href=\"/static/style.min.css\">";

sourcemeta::registry::html::partials::css(output, "/static/style.min.css");

// Application icons
// TODO: Allow changing these by supporing an object in the
// configuration manifest to select static files to override
html << "<link rel=\"icon\" href=\"/static/favicon.ico\" sizes=\"any\">";
html
<< "<link rel=\"icon\" href=\"/static/icon.svg\" type=\"image/svg+xml\">";
html << "<link rel=\"shortcut icon\" type=\"image/png\" "
"href=\"/static/apple-touch-icon.png\">";
html << "<link rel=\"apple-touch-icon\" sizes=\"180x180\" "
"href=\"/static/apple-touch-icon.png\">";
html << "<link rel=\"manifest\" href=\"/static/manifest.webmanifest\">";

if (configuration.defines("analytics")) {
if (configuration.at("analytics").defines("plausible")) {
html << "<script defer data-domain=\"";
html << configuration.at("analytics").at("plausible").to_string();
html << "\" src=\"https://plausible.io/js/script.js\"></script>";
}
output
.open(
"link",
{{"rel", "icon"}, {"href", "/static/favicon.ico"}, {"sizes", "any"}})
.open("link", {{"rel", "icon"},
{"href", "/static/icon.svg"},
{"type", "image/svg+xml"}})
.open("link", {{"rel", "shortcut icon"},
{"href", "/static/apple-touch-icon.png"},
{"type", "image/png"}})
.open("link", {{"rel", "apple-touch-icon"},
{"href", "/static/apple-touch-icon.png"},
{"sizes", "180x180"}})
.open("link",
{{"rel", "manifest"}, {"href", "/static/manifest.webmanifest"}});

if (configuration.defines("analytics") &&
configuration.at("analytics").defines("plausible")) {
output
.open("script",
{{"defer", ""},
{"data-domain",
configuration.at("analytics").at("plausible").to_string()},
{"src", "https://plausible.io/js/script.js"}})
.close("script");
}

html << "</head>";
html << "<body class=\"h-100 d-flex flex-column\">";

const auto &site_name{configuration.at("title").to_string()};

html << "<nav class=\"navbar navbar-expand border-bottom bg-body\">";
html << "<div class=\"container-fluid px-4 py-1 align-items-center "
"flex-column flex-sm-row\">";
html << "<a class=\"navbar-brand\" href=\"" << base_url << "\">";
html << "<img src=\"/static/icon.svg\" alt=\"" << site_name
<< "\" height=\"30\" width=\"30\" class=\"me-2\">";
html << "<span class=\"align-middle fw-bold\">" << site_name << "</span>";
html << "<span class=\"align-middle fw-lighter\"> Schemas</span>";
html << "</a>";

html << "<div class=\"mt-3 mt-sm-0 flex-grow-1 position-relative\">";
html << "<div class=\"input-group\">";
html << "<span class=\"input-group-text\">";
html << "<i class=\"bi bi-search\"></i>";
html << "</span>";
html << "<input class=\"form-control\" type=\"search\" id=\"search\" "
"placeholder=\"Search\" aria-label=\"Search\" autocomplete=\"off\">";
html << "</div>";
html << "<ul class=\"d-none list-group position-absolute w-100 mt-2 "
"shadow-sm\" id=\"search-result\"></ul>";
html << "</div>";
output.close("head");
output.open("body", {{"class", "h-100 d-flex flex-column"}});

if (configuration.defines("action")) {
html << "<a class=\"ms-3 btn btn-dark\" role=\"button\" href=\"";
html << configuration.at("action").at("url").to_string();
html << "\">";
if (configuration.at("action").defines("icon")) {
html << "<i class=\"me-2 bi bi-";
html << configuration.at("action").at("icon").to_string();
html << "\"></i>";
}

html << configuration.at("action").at("title").to_string();
html << "</a>";
}

html << "</div>";
html << "</nav>";
html_navigation(output, configuration);
}

template <typename T> auto html_end(T &html) -> void {
html << "<script async defer src=\"/static/main.js\"></script>";

html << "<div class=\"container-fluid px-4 mb-2\">";
html << "<footer class=\"border-top text-secondary py-3 d-flex "
"align-items-center justify-content-between\">";
html << "<small>";
html << "<a href=\"https://github.com/sourcemeta/registry\" "
"class=\"text-secondary\" target=\"_blank\">";
html << "Registry";
html << "</a>";
html << " v";
html << sourcemeta::registry::PROJECT_VERSION;
#ifdef SOURCEMETA_REGISTRY_ENTERPRISE
html << " (Enterprise Edition)";
#else
html << " (Community Edition)";
#endif
html << " © 2025 ";
html << "<a class=\"text-secondary\" "
"href=\"https://www.sourcemeta.com\" target=\"_blank\">";
html << "Sourcemeta, Ltd.";
html << "</a>";
html << "</small>";

html << "<small>";
html << "<a class=\"text-secondary\" "
"href=\"https://github.com/sourcemeta/registry/discussions\" "
"target=\"_blank\">";
html << "Need Help?";
html << "</a>";
html << "</small>";

html << "</footer>";
html << "</div>";

html << "</body>";
html << "</html>";
template <typename T> auto html_end(T &output) -> void {
output
.open("script",
{{"async", ""}, {"defer", ""}, {"src", "/static/main.js"}})
.close("script");
output.open("div", {{"class", "container-fluid px-4 mb-2"}});
html_footer(output);
output.close("div");
output.close("body");
output.close("html");
}

// TODO: Refactor this function to use new HTML utilities
template <typename T>
auto html_file_manager(T &html, const sourcemeta::jsontoolkit::JSON &meta)
-> void {
Expand Down
39 changes: 22 additions & 17 deletions src/enterprise/enterprise_index.h
Original file line number Diff line number Diff line change
Expand Up @@ -217,28 +217,31 @@ auto generate_toc(
std::cerr << "Generating HTML index page\n";
std::ofstream html{index_path.parent_path() / "index.html"};
assert(!html.fail());
sourcemeta::registry::html::SafeOutput output_html{html};
sourcemeta::registry::enterprise::html_start(
html, configuration, configuration.at("title").to_string() + " Schemas",
output_html, configuration,
configuration.at("title").to_string() + " Schemas",
configuration.at("description").to_string(), "");
sourcemeta::registry::enterprise::html_file_manager(html, meta);
sourcemeta::registry::enterprise::html_end(html);
sourcemeta::registry::enterprise::html_end(output_html);
html << "\n";
html.close();
} else {
std::cerr << "Generating HTML directory page\n";
const auto page_relative_path{std::string{'/'} + relative_path.string()};
std::ofstream html{index_path.parent_path() / "index.html"};
assert(!html.fail());
sourcemeta::registry::html::SafeOutput output_html{html};
sourcemeta::registry::enterprise::html_start(
html, configuration,
output_html, configuration,
meta.defines("title") ? meta.at("title").to_string()
: page_relative_path,
meta.defines("description")
? meta.at("description").to_string()
: ("Schemas located at " + page_relative_path),
page_relative_path);
sourcemeta::registry::enterprise::html_file_manager(html, meta);
sourcemeta::registry::enterprise::html_end(html);
sourcemeta::registry::enterprise::html_end(output_html);
html << "\n";
html.close();
}
Expand Down Expand Up @@ -281,21 +284,23 @@ auto attach(const sourcemeta::jsontoolkit::FlatFileSchemaResolver &resolver,
// Not found page
std::ofstream stream_not_found{output / "generated" / "404.html"};
assert(!stream_not_found.fail());
sourcemeta::registry::html::SafeOutput output_html{stream_not_found};
sourcemeta::registry::enterprise::html_start(
stream_not_found, configuration, "Not Found",
output_html, configuration, "Not Found",
"What you are looking for is not here", std::nullopt);
stream_not_found << "<div class=\"container-fluid p-4\">";
stream_not_found << "<h2 class=\"fw-bold\">";
stream_not_found << "Oops! What you are looking for is not here";
stream_not_found << "</h2>";
stream_not_found << "<p class=\"lead\">";
stream_not_found << "Are you sure the link you got is correct?";
stream_not_found << "</p>";
stream_not_found << "<a href=\"/\">";
stream_not_found << "Get back to the home page";
stream_not_found << "</a>";
stream_not_found << "</div>";
sourcemeta::registry::enterprise::html_end(stream_not_found);
output_html.open("div", {{"class", "container-fluid p-4"}})
.open("h2", {{"class", "fw-bold"}})
.text("Oops! What you are looking for is not here")
.close("h2")
.open("p", {{"class", "lead"}})
.text("Are you sure the link you got is correct?")
.close("p")
.open("a", {{"href", "/"}})
.text("Get back to the home page")
.close("a")
.close("div")
.close("div");
sourcemeta::registry::enterprise::html_end(output_html);
stream_not_found << "\n";
stream_not_found.close();

Expand Down
2 changes: 2 additions & 0 deletions src/enterprise/index.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ target_sources(schema_registry_index PRIVATE
"${ENTERPRISE_SOURCE_DIR}/enterprise_index.h"
"${ENTERPRISE_SOURCE_DIR}/enterprise_html.h")
target_include_directories(schema_registry_index PRIVATE "${ENTERPRISE_SOURCE_DIR}")

target_link_libraries(schema_registry_index PRIVATE sourcemeta::registry::html)
3 changes: 3 additions & 0 deletions src/html/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
noa_library(NAMESPACE sourcemeta PROJECT registry NAME html
FOLDER "Registry/HTML"
PRIVATE_HEADERS safe.h partials.h)
2 changes: 2 additions & 0 deletions src/html/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
All rights reserved. You can only use this software under a commercial license,
as set out in <https://www.sourcemeta.com/licensing/>.
Loading

0 comments on commit 93751aa

Please sign in to comment.