diff --git a/.clang-tidy b/.clang-tidy index 87194fa..ed1d7a1 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -7,6 +7,7 @@ Checks: > modernize-*, -modernize-use-trailing-return-type, -modernize-use-integer-sign-comparison, + -modernize-avoid-c-arrays, portability-*, readability-*, -readability-identifier-length, @@ -16,6 +17,7 @@ Checks: > -readability-avoid-return-with-void-value, -readability-named-parameter, -readability-magic-numbers, + -clang-analyzer-optin.core.EnumCastOutOfRange # Turn all the warnings from the checks above into errors. @@ -23,6 +25,7 @@ WarningsAsErrors: "*" CheckOptions: - { key: readability-function-cognitive-complexity.IgnoreMacros, value: true } + - { key: clang-analyzer-optin.IgnoreMacros, value: true } - { key: readability-identifier-naming.ConstantCase, value: UPPER_CASE } - { key: readability-identifier-naming.LocalConstantCase, value: lower_case } - { key: readability-identifier-naming.NamespaceCase, value: lower_case } diff --git a/.github/workflows/Codestyle.yaml b/.github/workflows/Codestyle.yaml new file mode 100644 index 0000000..4dc04b0 --- /dev/null +++ b/.github/workflows/Codestyle.yaml @@ -0,0 +1,29 @@ +name: "Codestyle" + +on: + push: + +jobs: + check-codestyle: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@v26 + - uses: cachix/cachix-action@v14 + with: + name: devenv + + - name: devenv.sh install + run: nix profile install nixpkgs#devenv + + - name: devenv.sh evaluation + run: devenv shell echo + + - name: clang-format checks + run: devenv shell "xmake format -ne" + + - name: generate compile-commands + run: devenv shell "xmake project -k compile_commands -y" + + - name: clang-tidy checks + run: devenv shell "xmake check clang.tidy" diff --git a/.github/workflows/TestUbuntu.yaml b/.github/workflows/TestUbuntu.yaml new file mode 100644 index 0000000..50581a9 --- /dev/null +++ b/.github/workflows/TestUbuntu.yaml @@ -0,0 +1,29 @@ +name: "TestUbuntu" + +on: + push: + +jobs: + ubuntu-build-and-test: + runs-on: ubuntu-latest + strategy: + matrix: + config: [asan, tsan, fast, small] + toolchain: [clang, gcc] + steps: + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@v26 + - uses: cachix/cachix-action@v14 + with: + name: devenv + + - name: devenv.sh install + run: nix profile install nixpkgs#devenv + + - name: devenv.sh evaluation + run: devenv shell echo + + - run: devenv shell "xmake config --toolchain=${{ matrix.toolchain }} --mode=${{ matrix.config }} --root -y" + - run: devenv shell "xmake build -y" + - run: devenv shell "xmake test -y" + - run: devenv shell "xmake install -y -o installdir" diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..4370a3b --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,7 @@ +Copyright (c) 2025 Dmitry Malakhov + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/include/cppwebdoc/CompString.hpp b/include/cppwebdoc/CompString.hpp index e368f5d..6b29813 100644 --- a/include/cppwebdoc/CompString.hpp +++ b/include/cppwebdoc/CompString.hpp @@ -11,11 +11,11 @@ namespace cppwebdoc { template struct CompString { - consteval CompString(const char (&s)[N]) { // NOLINT + consteval CompString(const char (&s)[N]) { std::ranges::copy(s, std::begin(data)); } - consteval CompString(std::string_view s) { // NOLINT + consteval CompString(std::string_view s) { std::ranges::copy(s, std::begin(data)); } @@ -44,7 +44,7 @@ struct CompString { return std::string_view{s} <=> std::string_view{*this}; } - char data[N]{}; // NOLINT + char data[N]{}; }; namespace detail { diff --git a/include/cppwebdoc/Html5Minimizer.hpp b/include/cppwebdoc/Html5Minimizer.hpp index 5001c65..c12400f 100644 --- a/include/cppwebdoc/Html5Minimizer.hpp +++ b/include/cppwebdoc/Html5Minimizer.hpp @@ -5,13 +5,14 @@ #include "cppwebdoc/Html5Tokenizer.hpp" #include +#include namespace cppwebdoc { -constexpr std::string html5_minimize(std::string_view html) { - std::string result; +namespace detail { - auto match_start_tag = [&](StartTag const &tag) -> void { +struct Html5MinimizeMatcher { + constexpr void operator()(StartTag const &tag) { result += '<'; result += tag.name; for (Attribute attr : tag.attributes()) { @@ -37,33 +38,56 @@ constexpr std::string html5_minimize(std::string_view html) { result += '/'; } result += '>'; - }; + } - auto match_end_tag = [&](EndTag const &tag) -> void { + constexpr void operator()(EndTag const &tag) { result += "'; - }; + } - auto match_special_text = [&](SpeciallyTaggedText const &text) -> void { result += text.value; }; + constexpr void operator()(Text const &text) { + std::string_view value = text.value; + trim_html5_spaces(value); + for (auto line_subr : value | std::views::split('\n')) { - auto match_comment = [](Comment const &) -> void {}; + std::string_view line = {line_subr.begin(), line_subr.size()}; + bool had_newline = *line.end() == '\n'; + trim_html5_spaces(line); + result += line; + if (had_newline) { + result += '\n'; + } + } + } - auto match_text = [&](Text const &text) -> void { - std::string_view value = text.value; - while (!value.empty() && detail::html5_is_space(value.front())) { - value.remove_prefix(1); + constexpr void operator()(SpeciallyTaggedText const &text) { + result += text.value; + } + + constexpr void operator()(Comment const &) {}; + + std::string result; + +private: + constexpr static void trim_html5_spaces(std::string_view &s) noexcept { + while (!s.empty() && detail::html5_is_space(s.front())) { + s.remove_prefix(1); } - while (!value.empty() && detail::html5_is_space(value.back())) { - value.remove_suffix(1); + while (!s.empty() && detail::html5_is_space(s.back())) { + s.remove_suffix(1); } - result += value; - }; + } +}; +} // namespace detail + +constexpr std::string html5_minimize(std::string_view html) { + detail::Html5MinimizeMatcher matcher; for (Html5TokenNoEof token : Html5Tokenizer{html}) { - token.match(match_start_tag, match_end_tag, match_text, match_special_text, match_comment); + token.visit(matcher); } - return result; + return matcher.result; } template diff --git a/include/cppwebdoc/Html5Tokenizer.hpp b/include/cppwebdoc/Html5Tokenizer.hpp index cfdadff..ba843f9 100644 --- a/include/cppwebdoc/Html5Tokenizer.hpp +++ b/include/cppwebdoc/Html5Tokenizer.hpp @@ -6,7 +6,6 @@ #include #include -#include #include #include #include @@ -31,7 +30,7 @@ struct Overloaded : TYPES... { template struct BetterVariant : std::variant { - using std::variant::variant; // NOLINT + using std::variant::variant; template [[nodiscard]] constexpr T &as() noexcept { @@ -131,7 +130,7 @@ struct Eof {}; struct Html5Token : detail::BetterVariant { - using BetterVariant::BetterVariant; // NOLINT + using BetterVariant::BetterVariant; }; struct Html5TokenNoEof diff --git a/packages/c/cppwebdoc/xmake.lua b/packages/c/cppwebdoc/xmake.lua new file mode 100644 index 0000000..6922f35 --- /dev/null +++ b/packages/c/cppwebdoc/xmake.lua @@ -0,0 +1,16 @@ +package("cppwebdoc") + set_homepage("https://github.com/bugsnotabunny/cppwebdoc") + set_description("A collection of c++ assets for manipulating html, css and js at compile-time") + set_license("MIT") + + add_urls("https://github.com/bugsnotabunny/cppwebdoc/archive/refs/tags/v$(version).tar.gz") + + on_install(function (package) + import("package.tools.xmake").install(package, { + kind = package:config("shared") and "shared" or "static" + }) + end): + + on_test(function (package) + assert(package:has_cxxfuncs("cppwebdoc::html5_minimize", {includes = "cppwebdoc/Html5Tokenizer.hpp"})) + end) diff --git a/test/cases/Html5Minimizer.cpp b/test/cases/Html5Minimizer.cpp index 53fe162..d938bb0 100644 --- a/test/cases/Html5Minimizer.cpp +++ b/test/cases/Html5Minimizer.cpp @@ -9,16 +9,15 @@ namespace { using namespace cppwebdoc; - } TEST_CASE("html5_minimize - basic behavior") { SECTION("Empty input") { - STATIC_REQUIRE(html5_minimize("") == ""); + STATIC_REQUIRE(html5_minimize("").empty()); } SECTION("Whitespace-only input") { - STATIC_REQUIRE(html5_minimize(" \n\t ") == ""); + STATIC_REQUIRE(html5_minimize(" \n\t ").empty()); } SECTION("Already minimal HTML remains unchanged") { @@ -72,8 +71,8 @@ TEST_CASE("html5_minimize - robustness") { } } -TEST_CASE("bebebe") { - constexpr static CompString DATA = R"( +TEST_CASE("some real'ish usage. only check that this compiles fine") { + constexpr static std::string_view DATA = R"( @@ -141,5 +140,5 @@ TEST_CASE("bebebe") { )"; - STATIC_REQUIRE_FALSE(html5_minimize_comptime().empty()); + STATIC_REQUIRE_FALSE(html5_minimize(DATA).empty()); } diff --git a/test/cases/parse/StringCursor.cpp b/test/cases/parse/StringCursor.cpp index f8178d5..97f0277 100644 --- a/test/cases/parse/StringCursor.cpp +++ b/test/cases/parse/StringCursor.cpp @@ -27,7 +27,7 @@ TEST_CASE("StringCursor basic peek operations") { SECTION("peek_try(n) succeeds when enough characters") { auto r = cur.peek(3); REQUIRE(r.has_value()); - REQUIRE(*r == "abc"); + REQUIRE(r == "abc"); } } @@ -42,7 +42,7 @@ TEST_CASE("StringCursor consume operations") { SECTION("consume1_try consumes one character") { auto r = cur.consume1(); REQUIRE(r.has_value()); - REQUIRE(*r == 'a'); + REQUIRE(r == 'a'); REQUIRE(cur.remaining() == "bc"); } @@ -54,7 +54,7 @@ TEST_CASE("StringCursor consume operations") { SECTION("consume_try(n) succeeds and consumes") { auto r = cur.consume(2); REQUIRE(r.has_value()); - REQUIRE(*r == "ab"); + REQUIRE(r == "ab"); REQUIRE(cur.remaining() == "c"); } @@ -82,7 +82,7 @@ TEST_CASE("StringCursor peek_until operations") { SECTION("peek_until_try returns optional when found") { auto r = cur.peek_until([](char c) { return c == 'c'; }); REQUIRE(r.has_value()); - REQUIRE(*r == "ab"); + REQUIRE(r == "ab"); } SECTION("peek_until_try returns nullopt when not found") { @@ -107,7 +107,7 @@ TEST_CASE("StringCursor consume_until operations") { SECTION("consume_until_try consumes when successful") { auto r = cur.consume_until([](char c) { return c == 'b'; }); REQUIRE(r.has_value()); - REQUIRE(*r == "a"); + REQUIRE(r == "a"); REQUIRE(cur.remaining() == "bc123"); } @@ -139,7 +139,7 @@ TEST_CASE("StringCursor *_until_not operations") { StringCursor cur2{" abc"}; auto r = cur2.consume_until_not(is_space); REQUIRE(r.has_value()); - REQUIRE(*r == " "); + REQUIRE(r == " "); REQUIRE(cur2.remaining() == "abc"); } } @@ -152,7 +152,7 @@ TEST_CASE("StringCursor edge cases") { REQUIRE_FALSE(cur.consume1().has_value()); REQUIRE(cur.peek(0).has_value()); - REQUIRE(*cur.peek(0) == ""); + REQUIRE(cur.peek(0) == ""); REQUIRE(cur.remaining().empty()); } @@ -161,6 +161,6 @@ TEST_CASE("StringCursor edge cases") { StringCursor cur{""}; auto r = cur.peek_until([](char) { return true; }); REQUIRE(r.has_value()); - REQUIRE(r->empty()); + REQUIRE(r == ""); } } diff --git a/xmake.lua b/xmake.lua index 74225ec..73e878b 100644 --- a/xmake.lua +++ b/xmake.lua @@ -29,8 +29,9 @@ end add_requires("catch2 v3.10.0", { configs = { lto = false, main = true, gmock = false } }) target("cppwebdoc") - set_kind("static") + set_kind("headeronly") add_includedirs("include", { public = true }) + add_headerfiles("include/(**.hpp)") set_default(true) target_end() @@ -43,7 +44,7 @@ target_end() for _, file in ipairs(os.files("test/cases/**.cpp")) do - local name = "test_" .. path.basename(file) + local name = "Test" .. path.basename(file) target(name) set_kind("binary") add_deps("cppwebdoc-testing")