From 4b574e21334d3aa12d958f24418b197328534ffc Mon Sep 17 00:00:00 2001 From: Tim Ebbeke Date: Wed, 11 Oct 2023 01:25:14 +0200 Subject: [PATCH 1/7] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2608be0..8f8d6f5 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,13 @@ Tested on: - Ubuntu 20 (using clang15 and libstdc++10) ![example workflow](https://github.com/NuiCpp/Nui/actions/workflows/ubuntu_20.yml/badge.svg) - Ubuntu 22 (using clang14 and libstdc++12) ![example workflow](https://github.com/NuiCpp/Nui/actions/workflows/ubuntu_22.yml/badge.svg) - Windows (using clang16 and current msys libstdc++) ![example workflow](https://github.com/NuiCpp/Nui/actions/workflows/windows.yml/badge.svg) + - MacOS: Currently In Progress: https://github.com/NuiCpp/Nui/tree/macos Not automatically tested but should work: - Arch Linux - Other Linux Distributions with new enough packages for C++20 Planed: - - Mac OS - Android if this gets more traction here: https://github.com/webview/webview/issues/871 ## Examples @@ -44,4 +44,4 @@ Nui uses the following dependencies: - 5cript/mplex https://github.com/5cript/mplex - [MIT LICENSE](https://github.com/5cript/mplex/blob/master/LICENSE) - nlohmann/json https://github.com/nlohmann/json - [MIT LICENSE](https://github.com/nlohmann/json/blob/develop/LICENSE.MIT) - portable-file-dialogs https://github.com/samhocevar/portable-file-dialogs - [WTFPL LICENSE](https://github.com/samhocevar/portable-file-dialogs/blob/main/README.md) -- 5cript/roar https://github.com/5cript/roar - [BSL-1.0 LICENSE](https://github.com/5cript/roar/blob/master/LICENSE) \ No newline at end of file +- 5cript/roar https://github.com/5cript/roar - [BSL-1.0 LICENSE](https://github.com/5cript/roar/blob/master/LICENSE) From d6affd2c4f643b4fe2455e9998da622ae0e25c71 Mon Sep 17 00:00:00 2001 From: Tim Ebbeke Date: Thu, 19 Oct 2023 01:28:13 +0200 Subject: [PATCH 2/7] Added inline preprocessing. --- CMakeLists.txt | 2 + cmake/backend/emscripten.cmake | 12 +- cmake/frontend/emscripten.cmake | 10 +- cmake/inline_extractor.cmake | 74 ++ nui/test/nui/CMakeLists.txt | 6 +- tools/inline_parser/CMakeLists.txt | 5 + .../include/inline_parser/constants.hpp | 9 + .../inline_parser/inline_extractor.hpp | 29 + .../include/inline_parser/parser.hpp | 101 +++ .../include/inline_parser/section_cache.hpp | 48 ++ .../src/inline_parser/CMakeLists.txt | 20 + .../src/inline_parser/inline_extractor.cpp | 194 +++++ .../inline_parser/src/inline_parser/main.cpp | 31 + .../src/inline_parser/section_cache.cpp | 153 ++++ tools/inline_parser/test/CMakeLists.txt | 19 + tools/inline_parser/test/temp_dir.cpp | 19 + tools/inline_parser/test/temp_dir.hpp | 15 + .../test/test_inline_extractor.hpp | 783 ++++++++++++++++++ tools/inline_parser/test/tests.cpp | 9 + tools/parcel_adapter/main.cpp | 1 + 20 files changed, 1529 insertions(+), 11 deletions(-) create mode 100644 cmake/inline_extractor.cmake create mode 100644 tools/inline_parser/CMakeLists.txt create mode 100644 tools/inline_parser/include/inline_parser/constants.hpp create mode 100644 tools/inline_parser/include/inline_parser/inline_extractor.hpp create mode 100644 tools/inline_parser/include/inline_parser/parser.hpp create mode 100644 tools/inline_parser/include/inline_parser/section_cache.hpp create mode 100644 tools/inline_parser/src/inline_parser/CMakeLists.txt create mode 100644 tools/inline_parser/src/inline_parser/inline_extractor.cpp create mode 100644 tools/inline_parser/src/inline_parser/main.cpp create mode 100644 tools/inline_parser/src/inline_parser/section_cache.cpp create mode 100644 tools/inline_parser/test/CMakeLists.txt create mode 100644 tools/inline_parser/test/temp_dir.cpp create mode 100644 tools/inline_parser/test/temp_dir.hpp create mode 100644 tools/inline_parser/test/test_inline_extractor.hpp create mode 100644 tools/inline_parser/test/tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index eadc581..09aa3fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,7 @@ set(NUI_SOURCE_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} CACHE INTERNAL "NUI source di include(${CMAKE_CURRENT_LIST_DIR}/cmake/common.cmake) include(${CMAKE_CURRENT_LIST_DIR}/cmake/options.cmake) include(${CMAKE_CURRENT_LIST_DIR}/cmake/static_analyzers.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/cmake/inline_extractor.cmake) include(${CMAKE_CURRENT_LIST_DIR}/cmake/dependencies/boostpp.cmake) include(${CMAKE_CURRENT_LIST_DIR}/cmake/dependencies/mplex.cmake) include(${CMAKE_CURRENT_LIST_DIR}/cmake/dependencies/interval_tree.cmake) @@ -30,6 +31,7 @@ else() add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/tools/patch_acorn) add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/tools/parcel_adapter) add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/tools/patch_emscripten_config) + add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/tools/inline_parser) include(${CMAKE_CURRENT_LIST_DIR}/cmake/backend/common.cmake) include(${CMAKE_CURRENT_LIST_DIR}/cmake/backend/emscripten.cmake) endif() diff --git a/cmake/backend/emscripten.cmake b/cmake/backend/emscripten.cmake index 69c0272..fe21a89 100644 --- a/cmake/backend/emscripten.cmake +++ b/cmake/backend/emscripten.cmake @@ -12,9 +12,9 @@ else() COMMAND "${CMAKE_BINARY_DIR}/_deps/emscripten-src/emsdk" install latest --build=Release COMMAND "${CMAKE_BINARY_DIR}/_deps/emscripten-src/upstream/emscripten/emcc" --generate-config COMMAND $ "${CMAKE_BINARY_DIR}/_deps/emscripten-src/upstream/emscripten/tools/acorn-optimizer.js" - COMMAND $ - "${CMAKE_BINARY_DIR}/_deps/emscripten-src/upstream/emscripten/.emscripten" - "${CMAKE_BINARY_DIR}/_deps/binaryen_release-src" + COMMAND $ + "${CMAKE_BINARY_DIR}/_deps/emscripten-src/upstream/emscripten/.emscripten" + "${CMAKE_BINARY_DIR}/_deps/binaryen_release-src" "${CMAKE_BINARY_DIR}/_deps/emscripten-src/java/bin/java.exe" # not setting node, because global installed node might be preferred # "${CMAKE_BINARY_DIR}/_deps/emscripten-src/node/bin/node.exe" @@ -106,9 +106,10 @@ function(nui_add_emscripten_target) "${NUI_ADD_EMSCRIPTEN_TARGET_ARGS_TARGET}-emscripten" SOURCE_DIR "${SOURCE_DIR}" # emscripten cmake with passed down Release/Debug build type - CONFIGURE_COMMAND + CONFIGURE_COMMAND ${EMCMAKE} cmake ${NUI_ADD_EMSCRIPTEN_TARGET_ARGS_CMAKE_OPTIONS} + -DNUI_INLINE_EXTRACTOR_TARGET_FILE=$ "${SOURCE_DIR}" # copy over package.json and fill parcel options that do not exist on it ${BUILD_COMMAND} @@ -120,6 +121,7 @@ function(nui_add_emscripten_target) BUILD_ALWAYS 1 BUILD_BYPRODUCTS "${CMAKE_BINARY_DIR}/module_${NUI_ADD_EMSCRIPTEN_TARGET_ARGS_TARGET}/bin/index.html" INSTALL_COMMAND "" + DEPENDS inline-parser ) add_dependencies(${NUI_ADD_EMSCRIPTEN_TARGET_ARGS_TARGET} ${NUI_ADD_EMSCRIPTEN_TARGET_ARGS_TARGET}-emscripten) add_dependencies(${NUI_ADD_EMSCRIPTEN_TARGET_ARGS_TARGET}-emscripten bin2hpp) @@ -128,7 +130,7 @@ function(nui_add_emscripten_target) DEPENDS ${NUI_ADD_EMSCRIPTEN_TARGET_ARGS_PREJS} ) add_custom_target( - ${NUI_ADD_EMSCRIPTEN_TARGET_ARGS_TARGET}-parcel-dep + ${NUI_ADD_EMSCRIPTEN_TARGET_ARGS_TARGET}-parcel-dep DEPENDS ${NUI_ADD_EMSCRIPTEN_TARGET_ARGS_TARGET}-emscripten ) add_dependencies( diff --git a/cmake/frontend/emscripten.cmake b/cmake/frontend/emscripten.cmake index e00962a..7bf884f 100644 --- a/cmake/frontend/emscripten.cmake +++ b/cmake/frontend/emscripten.cmake @@ -1,7 +1,7 @@ function(nui_prepare_emscripten_target) cmake_parse_arguments( NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS - "" + "NO_INLINE_JS" "TARGET;PREJS;STATIC;UNPACKED_MODE" "EMSCRIPTEN_LINK_OPTIONS;EMSCRIPTEN_COMPILE_OPTIONS;PARCEL_ARGS" ${ARGN} @@ -14,14 +14,18 @@ function(nui_prepare_emscripten_target) nui_set_target_output_directories(${NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS_TARGET}) + if (NOT NO_INLINE_JS) + nui_enable_inline(TARGET ${NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS_TARGET}) + endif() + add_custom_target( - ${NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS_TARGET}-npm-install + ${NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS_TARGET}-npm-install COMMAND npm install WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) add_custom_target( - ${NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS_TARGET}-parcel + ${NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS_TARGET}-parcel COMMAND ${CMAKE_COMMAND} -E copy_directory "${NUI_SOURCE_DIRECTORY}/nui/js" "${CMAKE_BINARY_DIR}/nui-js" COMMAND ${CMAKE_COMMAND} -E copy_directory ${NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS_STATIC} "${CMAKE_BINARY_DIR}/static" COMMAND "${CMAKE_BINARY_DIR}/node_modules/.bin/parcel" build --dist-dir "${CMAKE_BINARY_DIR}/bin" ${NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS_PARCEL_ARGS} diff --git a/cmake/inline_extractor.cmake b/cmake/inline_extractor.cmake new file mode 100644 index 0000000..55ed051 --- /dev/null +++ b/cmake/inline_extractor.cmake @@ -0,0 +1,74 @@ +function(nui_preprocess_inline_js) + set(one_value_args TARGET SOURCE OUTPUT DIRECTORY INLINE_CACHE DEPENDS IS_FIRST) + set(multi_value_args EXTRA_CXX_FLAGS) + cmake_parse_arguments(CPP "" "${one_value_args}" "${multi_value_args}" ${ARGN}) + + string(TOUPPER ${CMAKE_BUILD_TYPE} build_type) + string(REPLACE " " ";" c_flags "${CMAKE_CXX_FLAGS} ${CMAKE_CXX_FLAGS_${build_type}}") + + add_custom_command( + OUTPUT ${CPP_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E make_directory ${CPP_DIRECTORY} + COMMAND ${CMAKE_CXX_COMPILER} + #"-D$,;-D>" + "-I$,;-I>" + ${c_flags} + $ + ${CPP_EXTRA_CXX_FLAGS} + -E ${CPP_SOURCE} -o ${CPP_OUTPUT} + # COMMAND "${NUI_INLINE_EXTRACTOR_TARGET_FILE}" ${CPP_INLINE_CACHE} ${CPP_OUTPUT} ${CPP_IS_FIRST} + COMMAND_EXPAND_LISTS VERBATIM + IMPLICIT_DEPENDS C ${CPP_SOURCE} + DEPENDS ${CPP_SOURCE} ${CPP_DEPENDS}) +endfunction() + +function(nui_enable_inline) + set(one_value_args TARGET) + set(multi_value_args) + cmake_parse_arguments(nui_enable_inline_ARGS "" "${one_value_args}" "${multi_value_args}" ${ARGN}) + + get_target_property(INLINE_JS_SOURCES ${nui_enable_inline_ARGS_TARGET} SOURCES) + + set(INLINE_DIRECTORY_SUBDIR "nui-inline") + set(INLINE_DIRECTORY "${CMAKE_BINARY_DIR}/${INLINE_DIRECTORY_SUBDIR}") + set(INLINE_CACHE "${INLINE_DIRECTORY}/inline.cache") + set(INLINE_IMPORTS_SCRIPTS "${INLINE_DIRECTORY}/inline_imports.js") + set(INLINE_IMPORTS_STYLES "${INLINE_DIRECTORY}/inline_imports.css") + + # for each source file preprocess it: + set(IS_FIRST TRUE) + foreach(source_file ${INLINE_JS_SOURCES}) + get_filename_component(source_file_name "${source_file}" NAME) + set(preprocessed_source_file "${INLINE_DIRECTORY}/${source_file_name}.i") + nui_preprocess_inline_js( + TARGET ${nui_enable_inline_ARGS_TARGET} + DIRECTORY "${INLINE_DIRECTORY}" + INLINE_CACHE "${INLINE_CACHE}" + SOURCE "${source_file}" + OUTPUT "${preprocessed_source_file}" + EXTRA_CXX_FLAGS -P -CC -DNUI_INLINE -DNUI_MODULE_SOURCE_DIR="${CMAKE_SOURCE_DIR}" -DNUI_MODULE_CURRENT_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}" + IS_FIRST ${IS_FIRST} + ) + set(IS_FIRST FALSE) + message(STATUS "Preprocessing ${source_file} to ${preprocessed_source_file}") + list(APPEND preprocessed_sources "${preprocessed_source_file}") + endforeach() + + add_custom_command( + OUTPUT + ${INLINE_IMPORTS_SCRIPTS} + ${INLINE_IMPORTS_STYLES} + COMMAND ${CMAKE_COMMAND} -E make_directory ${INLINE_DIRECTORY} + COMMAND "${NUI_INLINE_EXTRACTOR_TARGET_FILE}" ${INLINE_CACHE} ${CMAKE_BINARY_DIR} ${INLINE_DIRECTORY_SUBDIR} ${preprocessed_sources} + COMMAND_EXPAND_LISTS VERBATIM + DEPENDS ${preprocessed_sources} + ) + + add_custom_target( + inline-js-${nui_enable_inline_ARGS_TARGET} + ALL + DEPENDS ${INLINE_IMPORTS_SCRIPTS} + DEPENDS ${INLINE_IMPORTS_STYLES} + ) + add_dependencies(${nui_enable_inline_ARGS_TARGET} inline-js-${nui_enable_inline_ARGS_TARGET}) +endfunction() \ No newline at end of file diff --git a/nui/test/nui/CMakeLists.txt b/nui/test/nui/CMakeLists.txt index a480482..f2156eb 100644 --- a/nui/test/nui/CMakeLists.txt +++ b/nui/test/nui/CMakeLists.txt @@ -27,9 +27,9 @@ add_executable(nui-tests engine/object.cpp engine/array.cpp ) -target_link_libraries(nui-tests PRIVATE - nui-frontend-mocked - gtest +target_link_libraries(nui-tests PRIVATE + nui-frontend-mocked + gtest ) gtest_discover_tests(nui-tests) diff --git a/tools/inline_parser/CMakeLists.txt b/tools/inline_parser/CMakeLists.txt new file mode 100644 index 0000000..62a6153 --- /dev/null +++ b/tools/inline_parser/CMakeLists.txt @@ -0,0 +1,5 @@ +add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/src/inline_parser") + +if (${NUI_ENABLE_TESTS}) + add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/test") +endif() \ No newline at end of file diff --git a/tools/inline_parser/include/inline_parser/constants.hpp b/tools/inline_parser/include/inline_parser/constants.hpp new file mode 100644 index 0000000..9264f50 --- /dev/null +++ b/tools/inline_parser/include/inline_parser/constants.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace Constants +{ + constexpr static std::string_view inlineDirective = "@inline"; + constexpr static std::string_view endInlineDirective = "@endinline"; +} \ No newline at end of file diff --git a/tools/inline_parser/include/inline_parser/inline_extractor.hpp b/tools/inline_parser/include/inline_parser/inline_extractor.hpp new file mode 100644 index 0000000..9ca7531 --- /dev/null +++ b/tools/inline_parser/include/inline_parser/inline_extractor.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +class InlineExtractor +{ + public: + InlineExtractor(std::function onWarning = [](std::string const&) {}); + void parseFileForSections(std::filesystem::path const& path); + void parallelParseFilesForSections(std::vector const& paths); + + SectionCache& sectionCache(); + SectionCache const& sectionCache() const; + + private: + bool parseInlineDirective(std::string const& line, Section& section) const; + + private: + SectionCache sectionCache_; + std::function onWarning_; + std::mutex sectionCacheMutex_; +}; \ No newline at end of file diff --git a/tools/inline_parser/include/inline_parser/parser.hpp b/tools/inline_parser/include/inline_parser/parser.hpp new file mode 100644 index 0000000..e309f1e --- /dev/null +++ b/tools/inline_parser/include/inline_parser/parser.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include +#include +#include + +template +class Parser +{ + public: + enum class State + { + ReachedEnd, + Success, + Failure + }; + + Parser(IteratorT begin, IteratorT end) + : begin_(begin) + , end_(end) + {} + Parser(std::string_view str) + : begin_(str.begin()) + , end_(str.end()) + {} + Parser(std::string const& str) + : begin_(str.begin()) + , end_(str.end()) + {} + + State skipWhitespace() + { + while (begin_ != end_ && std::isspace(*begin_)) + ++begin_; + return begin_ == end_ ? State::ReachedEnd : State::Success; + } + + State matchComment() + { + if (begin_ == end_ || *begin_ != '/') + return State::Failure; + ++begin_; + if (begin_ == end_ || *begin_ != '/') + return State::Failure; + ++begin_; + return State::Success; + } + + State match(std::string_view str) + { + auto it = begin_; + for (auto c : str) + { + if (it == end_ || *it != c) + return State::Failure; + ++it; + } + begin_ = it; + return State::Success; + } + + State match(char c) + { + if (begin_ == end_ || *begin_ != c) + return State::Failure; + ++begin_; + return State::Success; + } + + std::pair match(std::function const& predicate) + { + std::string result; + while (begin_ != end_ && predicate(*begin_)) + { + result += *begin_; + ++begin_; + } + if (result.empty()) + return {State::Failure, result}; + else + return {begin_ == end_ ? State::ReachedEnd : State::Success, result}; + } + + std::pair matchUntil(char endMarker) + { + std::string result; + while (begin_ != end_ && *begin_ != endMarker) + { + result += *begin_; + ++begin_; + } + if (begin_ == end_) + return {State::ReachedEnd, result}; + else + return {State::Success, result}; + } + + private: + IteratorT begin_; + IteratorT end_; +}; \ No newline at end of file diff --git a/tools/inline_parser/include/inline_parser/section_cache.hpp b/tools/inline_parser/include/inline_parser/section_cache.hpp new file mode 100644 index 0000000..252e880 --- /dev/null +++ b/tools/inline_parser/include/inline_parser/section_cache.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +struct Section +{ + std::string type; + std::string name; + std::string content; +}; + +class SectionCache +{ + public: + void addSection(Section const& section); + std::optional
findSection(std::string const& type, std::string const& name) const; + void fromFile(std::filesystem::path const& path); + void toFile(std::filesystem::path const& path) const; + + /// Deletes all sections that were not overwritten. + void removeColdSections(); + + void produceToDirectory( + std::filesystem::path const& basePath, + std::filesystem::path const& path, + bool emitComments = true) const; + + friend void to_json(nlohmann::json& j, SectionCache const& sc); + friend void from_json(nlohmann::json const& j, SectionCache& sc); + + std::unordered_map::const_iterator begin() const; + std::unordered_map::const_iterator end() const; + std::unordered_map::const_iterator + find(std::string const& type, std::string const& name) const; + + std::set::const_iterator coldBegin() const; + std::set::const_iterator coldEnd() const; + + private: + std::unordered_map sections; + std::set coldSections; +}; \ No newline at end of file diff --git a/tools/inline_parser/src/inline_parser/CMakeLists.txt b/tools/inline_parser/src/inline_parser/CMakeLists.txt new file mode 100644 index 0000000..4975591 --- /dev/null +++ b/tools/inline_parser/src/inline_parser/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.16) + +project(inline-parser VERSION 0.1.0) + +add_library(inline-parser-lib + STATIC + inline_extractor.cpp + section_cache.cpp +) +target_link_libraries(inline-parser-lib PUBLIC nlohmann_json roar) +target_compile_features(inline-parser-lib PUBLIC cxx_std_20) +target_include_directories(inline-parser-lib PUBLIC ${CMAKE_CURRENT_LIST_DIR}/../../include) + +add_executable(inline-parser main.cpp) +target_link_libraries(inline-parser PRIVATE inline-parser-lib) + +set_target_properties(inline-parser + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tools_bin" +) \ No newline at end of file diff --git a/tools/inline_parser/src/inline_parser/inline_extractor.cpp b/tools/inline_parser/src/inline_parser/inline_extractor.cpp new file mode 100644 index 0000000..ec321e7 --- /dev/null +++ b/tools/inline_parser/src/inline_parser/inline_extractor.cpp @@ -0,0 +1,194 @@ +#include +#include +#include + +#include +#include +#include + +#include + +namespace +{ + constexpr bool nameMatcher(char c) + { + return c != ')' && c != ',' && c != ' ' && c != '\t' && c != '\r' && c != '\n'; + } +} + +InlineExtractor::InlineExtractor(std::function onWarning) + : onWarning_{std::move(onWarning)} + , sectionCache_{} +{} + +bool InlineExtractor::parseInlineDirective(std::string const& line, Section& section) const +{ + if (line.empty()) + return false; + + using StringParser = Parser; + + StringParser parser{line}; + auto state = parser.skipWhitespace(); + if (state == StringParser::State::ReachedEnd) + return false; + + state = parser.matchComment(); + if (state != StringParser::State::Success) + return false; + + state = parser.skipWhitespace(); + if (state == StringParser::State::ReachedEnd) + return false; + + state = parser.match(Constants::inlineDirective); + if (state != StringParser::State::Success) + return false; + + // Warnings from here on out + + state = parser.skipWhitespace(); + if (state == StringParser::State::ReachedEnd) + { + onWarning_("Found start marker without parameters, skipping"); + return false; + } + + state = parser.match('('); + if (state != StringParser::State::Success) + { + onWarning_("Found start marker without parameters, skipping"); + return false; + } + + state = parser.skipWhitespace(); + if (state == StringParser::State::ReachedEnd) + { + onWarning_("Marker parameter clause not closed on same line, skipping"); + return false; + } + + auto [result, type] = parser.match(nameMatcher); + if (result != StringParser::State::Success) + { + onWarning_("Expected type, skipping"); + return false; + } + + state = parser.skipWhitespace(); + if (state == StringParser::State::ReachedEnd) + { + onWarning_("Unexpected end of line after marker type, skipping"); + return false; + } + + state = parser.match(','); + if (state != StringParser::State::Success) + { + onWarning_("Expected comma after marker type, skipping"); + return false; + } + + state = parser.skipWhitespace(); + if (state == StringParser::State::ReachedEnd) + { + onWarning_("Unexpected end of line after comma, skipping"); + return false; + } + + auto [result2, name] = parser.match(nameMatcher); + if (result2 != StringParser::State::Success) + { + onWarning_("Expected name, skipping"); + return false; + } + + state = parser.skipWhitespace(); + if (state == StringParser::State::ReachedEnd) + { + onWarning_("Unexpected end of line after marker name, skipping"); + return false; + } + + state = parser.match(')'); + if (state != StringParser::State::Success) + { + onWarning_("Expected closing parenthesis after marker name, skipping"); + return false; + } + + section.type = std::move(type); + section.name = std::move(name); + + return true; +} + +void InlineExtractor::parseFileForSections(std::filesystem::path const& path) +{ + std::ifstream reader{path, std::ios_base::binary}; + if (!reader.is_open()) + return; + + std::string line; + Section currentSection; + std::function currentParser; + std::function doParseInlineDirective; + + auto doSearchEndInlineDirective = [this, ¤tSection, &line, ¤tParser, &doParseInlineDirective]() { + if (line.find(Constants::endInlineDirective) != std::string::npos) + { + { + std::lock_guard guard{sectionCacheMutex_}; + sectionCache_.addSection(currentSection); + currentSection = {}; + } + currentParser = doParseInlineDirective; + } + else + { + line.resize(line.size() + 1); + line.back() = '\n'; + currentSection.content.append(std::move(line)); + } + }; + + doParseInlineDirective = [this, ¤tSection, &line, ¤tParser, &doSearchEndInlineDirective]() { + if (parseInlineDirective(line, currentSection)) + currentParser = doSearchEndInlineDirective; + }; + + currentParser = doParseInlineDirective; + + while (std::getline(reader, line)) + { + currentParser(); + } + if (!currentSection.type.empty()) + { + onWarning_("Found start marker without end marker, skipping"); + } +} + +void InlineExtractor::parallelParseFilesForSections(std::vector const& paths) +{ + boost::asio::thread_pool pool{ + std::min(paths.size(), static_cast(std::thread::hardware_concurrency()))}; + + for (auto const& path : paths) + { + boost::asio::post(pool, [this, path]() { + parseFileForSections(path); + }); + } + + pool.join(); +} + +SectionCache& InlineExtractor::sectionCache() +{ + return sectionCache_; +} +SectionCache const& InlineExtractor::sectionCache() const +{ + return sectionCache_; +} \ No newline at end of file diff --git a/tools/inline_parser/src/inline_parser/main.cpp b/tools/inline_parser/src/inline_parser/main.cpp new file mode 100644 index 0000000..241b5c3 --- /dev/null +++ b/tools/inline_parser/src/inline_parser/main.cpp @@ -0,0 +1,31 @@ +#include + +#include + +int main(int argc, char** argv) +{ + if (argc < 5) + { + std::cerr << "Usage: " << argv[0] << " ..." << std::endl; + return 1; + } + + std::filesystem::path cachePath{argv[1]}; + std::filesystem::path baseDir{argv[2]}; + std::filesystem::path unpackDir{argv[3]}; + + std::vector paths; + for (int i = 4; i < argc; ++i) + paths.emplace_back(argv[i]); + + InlineExtractor extractor{[](std::string const& warning) { + std::cerr << warning << std::endl; + }}; + extractor.sectionCache().fromFile(cachePath); + extractor.parallelParseFilesForSections(paths); + + // save: + extractor.sectionCache().removeColdSections(); + extractor.sectionCache().toFile(cachePath); + extractor.sectionCache().produceToDirectory(baseDir, unpackDir); +} \ No newline at end of file diff --git a/tools/inline_parser/src/inline_parser/section_cache.cpp b/tools/inline_parser/src/inline_parser/section_cache.cpp new file mode 100644 index 0000000..c842492 --- /dev/null +++ b/tools/inline_parser/src/inline_parser/section_cache.cpp @@ -0,0 +1,153 @@ +#include + +#include + +#include + +namespace +{ + bool isStyleSection(std::string const& type) + { + return type == "css" || type == "scss" || type == "sass" || type == "less"; + } + bool isScriptSection(std::string const& type) + { + return type == "js" || type == "ts" || type == "coffee"; + } +} + +void SectionCache::addSection(Section const& section) +{ + std::string key = section.type + "." + section.name; + coldSections.erase(key); + sections[key] = section; +} + +std::optional
SectionCache::findSection(std::string const& type, std::string const& name) const +{ + std::string key = type + "." + name; + + auto it = sections.find(key); + if (it != sections.end()) + return it->second; + else + return std::nullopt; +} + +void SectionCache::fromFile(std::filesystem::path const& path) +{ + std::ifstream reader{path, std::ios_base::binary}; + if (!reader.is_open()) + return; + + nlohmann::json j = nlohmann::json::parse(reader); + + *this = j.get(); + + for (auto const& [key, section] : sections) + coldSections.insert(key); +} + +void SectionCache::toFile(std::filesystem::path const& path) const +{ + nlohmann::json j = *this; + std::ofstream writer{path, std::ios_base::binary}; + writer << j.dump(4); +} + +void SectionCache::removeColdSections() +{ + for (auto const& key : coldSections) + sections.erase(key); + coldSections.clear(); +} + +std::unordered_map::const_iterator SectionCache::begin() const +{ + return sections.begin(); +} +std::unordered_map::const_iterator SectionCache::end() const +{ + return sections.end(); +} + +void SectionCache::produceToDirectory( + std::filesystem::path const& basePath, + std::filesystem::path const& path, + bool emitComments) const +{ + const auto fullPath = basePath / path; + + // Always create them! For cmake technical reasons, even if they are empty. + std::ofstream scriptImportsWriter{fullPath / "inline_imports.js", std::ios_base::binary}; + std::ofstream styleImportsWriter{fullPath / "inline_imports.css", std::ios_base::binary}; + + for (auto const& [key, section] : sections) + { + const auto sectionSubPath = std::filesystem::path{section.type} / (section.name + "." + section.type); + const auto sectionPath = fullPath / sectionSubPath; + std::filesystem::create_directories(sectionPath.parent_path()); + std::ofstream writer{sectionPath, std::ios_base::binary}; + if (emitComments) + { + writer << "/* This file was automatically generated by the nui inline parser. */\n"; + writer << "/* Do not edit this file, your changes will be overwritten. */\n\n"; + } + writer.write(section.content.data(), section.content.size()); + + if (isStyleSection(section.type)) + { + styleImportsWriter << "@import \"./" << sectionSubPath.generic_string() << "\";\n"; + } + else if (isScriptSection(section.type)) + { + scriptImportsWriter << "import './" << sectionSubPath.generic_string() << "';\n"; + } + else + { + scriptImportsWriter << "// Unknown section type: " << section.type << "\n"; + } + } +} + +std::unordered_map::const_iterator +SectionCache::find(std::string const& type, std::string const& name) const +{ + std::string key = type + "." + name; + return sections.find(key); +} + +std::set::const_iterator SectionCache::coldBegin() const +{ + return coldSections.begin(); +} +std::set::const_iterator SectionCache::coldEnd() const +{ + return coldSections.end(); +} + +void to_json(nlohmann::json& j, SectionCache const& sc) +{ + j = nlohmann::json::object(); + for (auto const& [key, section] : sc.sections) + { + nlohmann::json sectionJson; + sectionJson["type"] = section.type; + sectionJson["name"] = section.name; + sectionJson["content"] = Roar::base64Encode(section.content); + j[key] = sectionJson; + } +} + +void from_json(nlohmann::json const& j, SectionCache& sc) +{ + sc = {}; + for (auto const& [key, sectionJson] : j.items()) + { + Section section; + section.type = sectionJson["type"].get(); + section.name = sectionJson["name"].get(); + section.content = Roar::base64Decode(sectionJson["content"].get()); + sc.sections[key] = section; + } +} \ No newline at end of file diff --git a/tools/inline_parser/test/CMakeLists.txt b/tools/inline_parser/test/CMakeLists.txt new file mode 100644 index 0000000..0a4091e --- /dev/null +++ b/tools/inline_parser/test/CMakeLists.txt @@ -0,0 +1,19 @@ +include(GoogleTest) + +find_package(Boost REQUIRED) + +add_executable(inline_parser-tests + tests.cpp + temp_dir.cpp +) +target_link_libraries(inline_parser-tests + PRIVATE + inline-parser-lib + gtest +) +gtest_discover_tests(inline_parser-tests) + +set_target_properties(inline_parser-tests + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests" +) \ No newline at end of file diff --git a/tools/inline_parser/test/temp_dir.cpp b/tools/inline_parser/test/temp_dir.cpp new file mode 100644 index 0000000..98f980d --- /dev/null +++ b/tools/inline_parser/test/temp_dir.cpp @@ -0,0 +1,19 @@ +#include "temp_dir.hpp" + +#include + +TempDir::TempDir() + : path_{[]() { + auto path = std::tmpnam(nullptr); + std::filesystem::create_directory(path); + return path; + }()} +{} +TempDir::~TempDir() +{ + std::filesystem::remove_all(path_); +} +std::filesystem::path const& TempDir::path() const +{ + return path_; +} \ No newline at end of file diff --git a/tools/inline_parser/test/temp_dir.hpp b/tools/inline_parser/test/temp_dir.hpp new file mode 100644 index 0000000..f0dfe44 --- /dev/null +++ b/tools/inline_parser/test/temp_dir.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +class TempDir +{ + public: + TempDir(); + ~TempDir(); + + std::filesystem::path const& path() const; + + private: + std::filesystem::path path_; +}; \ No newline at end of file diff --git a/tools/inline_parser/test/test_inline_extractor.hpp b/tools/inline_parser/test/test_inline_extractor.hpp new file mode 100644 index 0000000..5ccc45a --- /dev/null +++ b/tools/inline_parser/test/test_inline_extractor.hpp @@ -0,0 +1,783 @@ +#include +#include "temp_dir.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace Nui::Tests +{ + class TestInlineExtractor : public ::testing::Test + { + protected: + void createFile(std::string const& name, std::string content) + { + std::stringstream ss{content}; + std::string line; + content.clear(); + while (std::getline(ss, line)) + { + if (!line.empty()) + { + line += '\n'; + } + // trim front whitespace: + line.erase(line.begin(), std::find_if(line.begin(), line.end(), [](int ch) { + return !std::isspace(ch); + })); + content += line; + } + + createdFiles_.push_back(tempDir_.path() / name); + std::ofstream{tempDir_.path() / name} << content; + } + + std::string toLinuxLineEndings(std::string const& str) + { + std::stringstream sstr{str}; + std::string line; + std::string result; + while (std::getline(sstr, line)) + { + if (line.back() == '\r') + line.pop_back(); + result += line + '\n'; + } + return result; + } + + void parallelParse() + { + extractor_.parallelParseFilesForSections(createdFiles_); + } + + SectionCache& cache() + { + return extractor_.sectionCache(); + } + + protected: + TempDir tempDir_; + std::vector createdFiles_; + InlineExtractor extractor_; + }; + + TEST_F(TestInlineExtractor, EmptyFileResultsEmptyCache) + { + createFile("empty.cpp", ""); + + parallelParse(); + + EXPECT_EQ(cache().begin(), cache().end()); + } + + TEST_F(TestInlineExtractor, MultipleEmptyFilesResultsEmptyCache) + { + createFile("empty1.cpp", ""); + createFile("empty2.cpp", ""); + createFile("empty3.cpp", ""); + + parallelParse(); + + EXPECT_EQ(cache().begin(), cache().end()); + } + + TEST_F(TestInlineExtractor, SourceFileWithoutSectionsResultsEmptyCache) + { + createFile("source.cpp", "int main() {}"); + + parallelParse(); + + EXPECT_EQ(cache().begin(), cache().end()); + } + + TEST_F(TestInlineExtractor, SingleSourceSectionIsParsed) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo) + let x = () => {}; + // @endinline + )"); + + parallelParse(); + + ASSERT_NE(cache().begin(), cache().end()); + EXPECT_EQ(cache().begin()->second.type, "js"); + EXPECT_EQ(cache().begin()->second.name, "foo"); + EXPECT_EQ(toLinuxLineEndings(cache().begin()->second.content), "let x = () => {};\n"); + } + + TEST_F(TestInlineExtractor, MultipleSourceSectionsAreParsed) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo) + let x = () => {}; + // @endinline + // @inline(js, bar) + let y = () => {}; + // @endinline + )"); + + parallelParse(); + + ASSERT_EQ(std::distance(cache().begin(), cache().end()), 2); + + auto jsfoo = cache().find("js", "foo"); + ASSERT_NE(jsfoo, cache().end()); + EXPECT_EQ(jsfoo->second.type, "js"); + EXPECT_EQ(jsfoo->second.name, "foo"); + EXPECT_EQ(toLinuxLineEndings(jsfoo->second.content), "let x = () => {};\n"); + + auto jsbar = cache().find("js", "bar"); + ASSERT_NE(jsbar, cache().end()); + EXPECT_EQ(jsbar->second.type, "js"); + EXPECT_EQ(jsbar->second.name, "bar"); + EXPECT_EQ(toLinuxLineEndings(jsbar->second.content), "let y = () => {};\n"); + } + + TEST_F(TestInlineExtractor, MissingEndMarkerSkipsEverything) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo) + let x = () => {}; + )"); + + parallelParse(); + + EXPECT_EQ(cache().begin(), cache().end()); + } + + TEST_F(TestInlineExtractor, MissingEndMarkerSkipsEverything2) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo) + let x = () => {}; + // @inline(js, foo2) + let x = () => {}; + )"); + + parallelParse(); + + EXPECT_EQ(cache().begin(), cache().end()); + } + + TEST_F(TestInlineExtractor, SpaceAfterCommentIsOptional) + { + createFile("source.cpp", R"( + int main() {} + //@inline(js, foo) + let x = () => {}; + //@endinline + )"); + + parallelParse(); + + ASSERT_NE(cache().begin(), cache().end()); + EXPECT_EQ(cache().begin()->second.type, "js"); + EXPECT_EQ(cache().begin()->second.name, "foo"); + EXPECT_EQ(toLinuxLineEndings(cache().begin()->second.content), "let x = () => {};\n"); + } + + TEST_F(TestInlineExtractor, MoreSpaceIsAllowedAfterComment) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo) + let x = () => {}; + // @endinline + )"); + + parallelParse(); + + ASSERT_NE(cache().begin(), cache().end()); + EXPECT_EQ(cache().begin()->second.type, "js"); + EXPECT_EQ(cache().begin()->second.name, "foo"); + EXPECT_EQ(toLinuxLineEndings(cache().begin()->second.content), "let x = () => {};\n"); + } + + TEST_F(TestInlineExtractor, SpaceIsAllowedBeforeParameters) + { + createFile("source.cpp", R"( + int main() {} + // @inline (js, foo) + let x = () => {}; + // @endinline + )"); + + parallelParse(); + + ASSERT_NE(cache().begin(), cache().end()); + EXPECT_EQ(cache().begin()->second.type, "js"); + EXPECT_EQ(cache().begin()->second.name, "foo"); + EXPECT_EQ(toLinuxLineEndings(cache().begin()->second.content), "let x = () => {};\n"); + } + + TEST_F(TestInlineExtractor, SpaceIsAllowedAfterParameters) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo) + let x = () => {}; + // @endinline + )"); + + parallelParse(); + + ASSERT_NE(cache().begin(), cache().end()); + EXPECT_EQ(cache().begin()->second.type, "js"); + EXPECT_EQ(cache().begin()->second.name, "foo"); + EXPECT_EQ(toLinuxLineEndings(cache().begin()->second.content), "let x = () => {};\n"); + } + + TEST_F(TestInlineExtractor, SpaceIsAllowedBeforeComma) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js , foo) + let x = () => {}; + // @endinline + )"); + + parallelParse(); + + ASSERT_NE(cache().begin(), cache().end()); + EXPECT_EQ(cache().begin()->second.type, "js"); + EXPECT_EQ(cache().begin()->second.name, "foo"); + EXPECT_EQ(toLinuxLineEndings(cache().begin()->second.content), "let x = () => {};\n"); + } + + TEST_F(TestInlineExtractor, SpaceIsOptionalAfterComma) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js,foo) + let x = () => {}; + // @endinline + )"); + + parallelParse(); + + ASSERT_NE(cache().begin(), cache().end()); + EXPECT_EQ(cache().begin()->second.type, "js"); + EXPECT_EQ(cache().begin()->second.name, "foo"); + EXPECT_EQ(toLinuxLineEndings(cache().begin()->second.content), "let x = () => {};\n"); + } + + TEST_F(TestInlineExtractor, SpaceIsAllowedBeforeClosingParen) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo ) + let x = () => {}; + // @endinline + )"); + + parallelParse(); + + ASSERT_NE(cache().begin(), cache().end()); + EXPECT_EQ(cache().begin()->second.type, "js"); + EXPECT_EQ(cache().begin()->second.name, "foo"); + EXPECT_EQ(toLinuxLineEndings(cache().begin()->second.content), "let x = () => {};\n"); + } + + TEST_F(TestInlineExtractor, ContentCanBeMultipleLines) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo) + let x = () => { + let y = 1; + let z = 2; + }; + // @endinline + )"); + + parallelParse(); + + ASSERT_NE(cache().begin(), cache().end()); + EXPECT_EQ(cache().begin()->second.type, "js"); + EXPECT_EQ(cache().begin()->second.name, "foo"); + EXPECT_EQ(toLinuxLineEndings(cache().begin()->second.content), "let x = () => {\nlet y = 1;\nlet z = 2;\n};\n"); + } + + TEST_F(TestInlineExtractor, ContentCanBeMultipleLines2) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo) + let x = () => { + let y = 1; + let z = 2; + }; + // @endinline + // @inline(js, bar) + let y = () => { + let y = 1; + let z = 2; + }; + // @endinline + )"); + + parallelParse(); + + ASSERT_EQ(std::distance(cache().begin(), cache().end()), 2); + + auto jsfoo = cache().find("js", "foo"); + ASSERT_NE(jsfoo, cache().end()); + EXPECT_EQ(jsfoo->second.type, "js"); + EXPECT_EQ(jsfoo->second.name, "foo"); + EXPECT_EQ(toLinuxLineEndings(jsfoo->second.content), "let x = () => {\nlet y = 1;\nlet z = 2;\n};\n"); + + auto jsbar = cache().find("js", "bar"); + ASSERT_NE(jsbar, cache().end()); + EXPECT_EQ(jsbar->second.type, "js"); + EXPECT_EQ(jsbar->second.name, "bar"); + EXPECT_EQ(toLinuxLineEndings(jsbar->second.content), "let y = () => {\nlet y = 1;\nlet z = 2;\n};\n"); + } + + TEST_F(TestInlineExtractor, MultipleContentOfMultipleTypesIsPossible) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo) + let x = () => { + let y = 1; + let z = 2; + }; + // @endinline + // @inline(css, bar) + .foo { + color: red; + } + // @endinline + )"); + + parallelParse(); + + ASSERT_EQ(std::distance(cache().begin(), cache().end()), 2); + + auto jsfoo = cache().find("js", "foo"); + ASSERT_NE(jsfoo, cache().end()); + EXPECT_EQ(jsfoo->second.type, "js"); + EXPECT_EQ(jsfoo->second.name, "foo"); + EXPECT_EQ(toLinuxLineEndings(jsfoo->second.content), "let x = () => {\nlet y = 1;\nlet z = 2;\n};\n"); + + auto cssbar = cache().find("css", "bar"); + ASSERT_NE(cssbar, cache().end()); + EXPECT_EQ(cssbar->second.type, "css"); + EXPECT_EQ(cssbar->second.name, "bar"); + EXPECT_EQ(toLinuxLineEndings(cssbar->second.content), ".foo {\ncolor: red;\n}\n"); + } + + TEST_F(TestInlineExtractor, DuplicateSectionNameOverwritesPrevious) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo) + let x = () => { + let y = 1; + let z = 2; + }; + // @endinline + // @inline(js, foo) + let y = () => { + let y = 3; + let z = 4; + }; + // @endinline + )"); + + parallelParse(); + + ASSERT_EQ(std::distance(cache().begin(), cache().end()), 1); + + auto jsfoo = cache().find("js", "foo"); + ASSERT_NE(jsfoo, cache().end()); + EXPECT_EQ(jsfoo->second.type, "js"); + EXPECT_EQ(jsfoo->second.name, "foo"); + EXPECT_EQ(toLinuxLineEndings(jsfoo->second.content), "let y = () => {\nlet y = 3;\nlet z = 4;\n};\n"); + } + + TEST_F(TestInlineExtractor, DuplicateSectionNameButDifferentTypeDoesNotOverwritePrevious) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo) + let x = () => { + let y = 1; + let z = 2; + }; + // @endinline + // @inline(css, foo) + .foo { + color: red; + } + // @endinline + )"); + + parallelParse(); + + ASSERT_EQ(std::distance(cache().begin(), cache().end()), 2); + + auto jsfoo = cache().find("js", "foo"); + ASSERT_NE(jsfoo, cache().end()); + EXPECT_EQ(jsfoo->second.type, "js"); + EXPECT_EQ(jsfoo->second.name, "foo"); + EXPECT_EQ(toLinuxLineEndings(jsfoo->second.content), "let x = () => {\nlet y = 1;\nlet z = 2;\n};\n"); + + auto cssfoo = cache().find("css", "foo"); + ASSERT_NE(cssfoo, cache().end()); + EXPECT_EQ(cssfoo->second.type, "css"); + EXPECT_EQ(cssfoo->second.name, "foo"); + EXPECT_EQ(toLinuxLineEndings(cssfoo->second.content), ".foo {\ncolor: red;\n}\n"); + } + + TEST_F(TestInlineExtractor, SectionLoadedFromCacheIsOverwritten) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo) + let x = () => {}; + // @endinline + )"); + + parallelParse(); + + ASSERT_NE(cache().begin(), cache().end()); + EXPECT_EQ(cache().begin()->second.type, "js"); + EXPECT_EQ(cache().begin()->second.name, "foo"); + EXPECT_EQ(toLinuxLineEndings(cache().begin()->second.content), "let x = () => {};\n"); + + cache().toFile(tempDir_.path() / "cache.json"); + cache() = {}; + cache().fromFile(tempDir_.path() / "cache.json"); + + createdFiles_.clear(); + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo) + let x = () => { + let y = 3; + let z = 4; + }; + // @endinline + )"); + + parallelParse(); + ASSERT_NE(cache().begin(), cache().end()); + EXPECT_EQ(cache().begin()->second.type, "js"); + EXPECT_EQ(cache().begin()->second.name, "foo"); + EXPECT_EQ(toLinuxLineEndings(cache().begin()->second.content), "let x = () => {\nlet y = 3;\nlet z = 4;\n};\n"); + } + + TEST_F(TestInlineExtractor, LoadedSectionsAreCold) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo) + let x = () => {}; + // @endinline + )"); + + parallelParse(); + + EXPECT_EQ(cache().coldBegin(), cache().coldEnd()); + + cache().toFile(tempDir_.path() / "cache.json"); + cache() = {}; + cache().fromFile(tempDir_.path() / "cache.json"); + + EXPECT_NE(cache().coldBegin(), cache().coldEnd()); + EXPECT_EQ(std::distance(cache().coldBegin(), cache().coldEnd()), 1); + EXPECT_EQ(*cache().coldBegin(), "js.foo"); + } + + TEST_F(TestInlineExtractor, OverwrittenSectionsAreNoLongerCold) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo) + let x = () => {}; + // @endinline + )"); + + parallelParse(); + + EXPECT_EQ(cache().coldBegin(), cache().coldEnd()); + + cache().toFile(tempDir_.path() / "cache.json"); + cache() = {}; + cache().fromFile(tempDir_.path() / "cache.json"); + + EXPECT_NE(cache().coldBegin(), cache().coldEnd()); + EXPECT_EQ(std::distance(cache().coldBegin(), cache().coldEnd()), 1); + EXPECT_EQ(*cache().coldBegin(), "js.foo"); + + createdFiles_.clear(); + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo) + let y = () => {}; + // @endinline + )"); + + parallelParse(); + + EXPECT_EQ(cache().coldBegin(), cache().coldEnd()); + } + + TEST_F(TestInlineExtractor, ColdSectionsAreRemovedByRemoveColdSections) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo) + let x = () => {}; + // @endinline + )"); + + parallelParse(); + + EXPECT_EQ(cache().coldBegin(), cache().coldEnd()); + + cache().toFile(tempDir_.path() / "cache.json"); + cache() = {}; + cache().fromFile(tempDir_.path() / "cache.json"); + + EXPECT_NE(cache().coldBegin(), cache().coldEnd()); + EXPECT_EQ(std::distance(cache().coldBegin(), cache().coldEnd()), 1); + EXPECT_EQ(*cache().coldBegin(), "js.foo"); + + cache().removeColdSections(); + + EXPECT_EQ(cache().coldBegin(), cache().coldEnd()); + EXPECT_EQ(cache().begin(), cache().end()); + } + + TEST_F(TestInlineExtractor, ScriptsAreWrittenToFiles) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo) + let x = () => {}; + // @endinline + // @inline(js, bar) + let y = () => {}; + // @endinline + )"); + + parallelParse(); + + cache().produceToDirectory(tempDir_.path(), "", false); + + std::ifstream reader{tempDir_.path() / "js/foo.js"}; + ASSERT_TRUE(reader.is_open()); + std::string content; + std::getline(reader, content, '\0'); + EXPECT_EQ(toLinuxLineEndings(content), "let x = () => {};\n"); + + reader = std::ifstream{tempDir_.path() / "js/bar.js"}; + ASSERT_TRUE(reader.is_open()); + std::getline(reader, content, '\0'); + EXPECT_EQ(toLinuxLineEndings(content), "let y = () => {};\n"); + + reader = std::ifstream{tempDir_.path() / "inline_imports.js"}; + ASSERT_TRUE(reader.is_open()); + std::getline(reader, content, '\0'); + bool importsEqualInAnyOrder = content == "import './js/foo.js';\nimport './js/bar.js';\n" || + content == "import './js/bar.js';\nimport './js/foo.js';\n"; + EXPECT_TRUE(importsEqualInAnyOrder); + } + + TEST_F(TestInlineExtractor, StylesAreWrittenToFiles) + { + createFile("source.cpp", R"( + int main() {} + // @inline(css, foo) + .foo { + color: red; + } + // @endinline + // @inline(css, bar) + .bar { + color: blue; + } + // @endinline + )"); + + parallelParse(); + + cache().produceToDirectory(tempDir_.path(), "", false); + + std::ifstream reader{tempDir_.path() / "css/foo.css"}; + ASSERT_TRUE(reader.is_open()); + std::string content; + std::getline(reader, content, '\0'); + EXPECT_EQ(toLinuxLineEndings(content), ".foo {\ncolor: red;\n}\n"); + + reader = std::ifstream{tempDir_.path() / "css/bar.css"}; + ASSERT_TRUE(reader.is_open()); + std::getline(reader, content, '\0'); + EXPECT_EQ(toLinuxLineEndings(content), ".bar {\ncolor: blue;\n}\n"); + + reader = std::ifstream{tempDir_.path() / "inline_imports.css"}; + ASSERT_TRUE(reader.is_open()); + std::getline(reader, content, '\0'); + bool importsEqualInAnyOrder = content == "@import \"./css/foo.css\";\n@import \"./css/bar.css\";\n" || + content == "@import \"./css/bar.css\";\n@import \"./css/foo.css\";\n"; + EXPECT_TRUE(importsEqualInAnyOrder); + } + + TEST_F(TestInlineExtractor, AMixOfScriptsAndStylesIsWrittenToFiles) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo) + let x = () => {}; + // @endinline + // @inline(css, bar) + .bar { + color: blue; + } + // @endinline + )"); + + parallelParse(); + + cache().produceToDirectory(tempDir_.path(), "", false); + + std::ifstream reader{tempDir_.path() / "js/foo.js"}; + ASSERT_TRUE(reader.is_open()); + std::string content; + std::getline(reader, content, '\0'); + EXPECT_EQ(toLinuxLineEndings(content), "let x = () => {};\n"); + + reader = std::ifstream{tempDir_.path() / "css/bar.css"}; + ASSERT_TRUE(reader.is_open()); + std::getline(reader, content, '\0'); + EXPECT_EQ(toLinuxLineEndings(content), ".bar {\ncolor: blue;\n}\n"); + + reader = std::ifstream{tempDir_.path() / "inline_imports.js"}; + ASSERT_TRUE(reader.is_open()); + std::getline(reader, content, '\0'); + EXPECT_EQ(toLinuxLineEndings(content), "import './js/foo.js';\n"); + + reader = std::ifstream{tempDir_.path() / "inline_imports.css"}; + ASSERT_TRUE(reader.is_open()); + std::getline(reader, content, '\0'); + EXPECT_EQ(toLinuxLineEndings(content), "@import \"./css/bar.css\";\n"); + } + + TEST_F(TestInlineExtractor, JavascriptAndTypescriptAreWrittenToFiles) + { + createFile("source.cpp", R"( + int main() {} + // @inline(js, foo) + let x = () => {}; + // @endinline + // @inline(ts, bar) + let y = () => {}; + // @endinline + )"); + + parallelParse(); + + cache().produceToDirectory(tempDir_.path(), "", false); + + std::ifstream reader{tempDir_.path() / "js/foo.js"}; + ASSERT_TRUE(reader.is_open()); + std::string content; + std::getline(reader, content, '\0'); + EXPECT_EQ(toLinuxLineEndings(content), "let x = () => {};\n"); + + reader = std::ifstream{tempDir_.path() / "ts/bar.ts"}; + ASSERT_TRUE(reader.is_open()); + std::getline(reader, content, '\0'); + EXPECT_EQ(toLinuxLineEndings(content), "let y = () => {};\n"); + + reader = std::ifstream{tempDir_.path() / "inline_imports.js"}; + ASSERT_TRUE(reader.is_open()); + std::getline(reader, content, '\0'); + bool importsEqualInAnyOrder = content == "import './js/foo.js';\nimport './ts/bar.ts';\n" || + content == "import './ts/bar.ts';\nimport './js/foo.js';\n"; + EXPECT_TRUE(importsEqualInAnyOrder); + } + + TEST_F(TestInlineExtractor, LessSassScssCssAreWrittenToFiles) + { + createFile("source.cpp", R"( + int main() {} + // @inline(less, foo) + .foo { + color: red; + } + // @endinline + // @inline(sass, bar) + .bar { + color: blue; + } + // @endinline + // @inline(scss, baz) + .baz { + color: green; + } + // @endinline + // @inline(css, qux) + .qux { + color: yellow; + } + // @endinline + )"); + + parallelParse(); + + cache().produceToDirectory(tempDir_.path(), "", false); + + std::ifstream reader{tempDir_.path() / "less/foo.less"}; + ASSERT_TRUE(reader.is_open()); + std::string content; + std::getline(reader, content, '\0'); + EXPECT_EQ(toLinuxLineEndings(content), ".foo {\ncolor: red;\n}\n"); + + reader = std::ifstream{tempDir_.path() / "sass/bar.sass"}; + ASSERT_TRUE(reader.is_open()); + std::getline(reader, content, '\0'); + EXPECT_EQ(toLinuxLineEndings(content), ".bar {\ncolor: blue;\n}\n"); + + reader = std::ifstream{tempDir_.path() / "scss/baz.scss"}; + ASSERT_TRUE(reader.is_open()); + std::getline(reader, content, '\0'); + EXPECT_EQ(toLinuxLineEndings(content), ".baz {\ncolor: green;\n}\n"); + + reader = std::ifstream{tempDir_.path() / "css/qux.css"}; + ASSERT_TRUE(reader.is_open()); + std::getline(reader, content, '\0'); + EXPECT_EQ(toLinuxLineEndings(content), ".qux {\ncolor: yellow;\n}\n"); + + reader = std::ifstream{tempDir_.path() / "inline_imports.css"}; + ASSERT_TRUE(reader.is_open()); + std::getline(reader, content, '\0'); + + std::vector splitImports; + std::stringstream ss{content}; + std::string line; + while (std::getline(ss, line)) + { + splitImports.push_back(line); + } + std::sort(splitImports.begin(), splitImports.end()); + ASSERT_EQ(splitImports.size(), 4); + EXPECT_EQ(splitImports[0], "@import \"./css/qux.css\";"); + EXPECT_EQ(splitImports[1], "@import \"./less/foo.less\";"); + EXPECT_EQ(splitImports[2], "@import \"./sass/bar.sass\";"); + EXPECT_EQ(splitImports[3], "@import \"./scss/baz.scss\";"); + } +} \ No newline at end of file diff --git a/tools/inline_parser/test/tests.cpp b/tools/inline_parser/test/tests.cpp new file mode 100644 index 0000000..0e17eb0 --- /dev/null +++ b/tools/inline_parser/test/tests.cpp @@ -0,0 +1,9 @@ +#include "test_inline_extractor.hpp" + +#include + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/tools/parcel_adapter/main.cpp b/tools/parcel_adapter/main.cpp index e6f8afa..2aa1ee5 100644 --- a/tools/parcel_adapter/main.cpp +++ b/tools/parcel_adapter/main.cpp @@ -27,6 +27,7 @@ void disablePolyfillIfNotSet(nlohmann::json& alias, std::string_view aliasName) void addNuiAlias(nlohmann::json& alias) { alias["nui"] = "./nui-js"; + alias["nui-inline"] = "./inline-js"; } void createPackageJsonIfMissing(std::filesystem::path const& where, std::string const& targetName) From ba7ac26d398dad6bc9b2ecdb92ea4f4db0b10aa5 Mon Sep 17 00:00:00 2001 From: Tim Ebbeke Date: Thu, 19 Oct 2023 01:47:15 +0200 Subject: [PATCH 3/7] Added element macro without namespace. --- .../frontend/elements/impl/html_element.tpp | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/nui/include/nui/frontend/elements/impl/html_element.tpp b/nui/include/nui/frontend/elements/impl/html_element.tpp index a9c6b10..d072be9 100644 --- a/nui/include/nui/frontend/elements/impl/html_element.tpp +++ b/nui/include/nui/frontend/elements/impl/html_element.tpp @@ -36,24 +36,29 @@ namespace Nui } } +#define NUI_MAKE_HTML_ELEMENT_RENAME(NAME, HTML_ACTUAL) \ + struct NAME : ::Nui::HtmlElement \ + { \ + NAME(NAME const&) = default; \ + NAME(NAME&&) = default; \ + NAME(std::vector<::Nui::Attribute> const& attributes) \ + : ::Nui::HtmlElement{HTML_ACTUAL, &::Nui::RegularHtmlElementBridge, attributes} \ + {} \ + NAME(std::vector<::Nui::Attribute>&& attributes) \ + : ::Nui::HtmlElement{HTML_ACTUAL, &::Nui::RegularHtmlElementBridge, std::move(attributes)} \ + {} \ + template \ + NAME(T&&... attributes) \ + : ::Nui::HtmlElement{HTML_ACTUAL, &::Nui::RegularHtmlElementBridge, std::forward(attributes)...} \ + {} \ + }; + +#define NUI_MAKE_HTML_ELEMENT(NAME) NUI_MAKE_HTML_ELEMENT_RENAME(NAME, #NAME) + #define NUI_DECLARE_HTML_ELEMENT_RENAME(NAME, HTML_ACTUAL) \ namespace Nui::Elements \ { \ - struct NAME : HtmlElement \ - { \ - NAME(NAME const&) = default; \ - NAME(NAME&&) = default; \ - NAME(std::vector const& attributes) \ - : HtmlElement{HTML_ACTUAL, &RegularHtmlElementBridge, attributes} \ - {} \ - NAME(std::vector&& attributes) \ - : HtmlElement{HTML_ACTUAL, &RegularHtmlElementBridge, std::move(attributes)} \ - {} \ - template \ - NAME(T&&... attributes) \ - : HtmlElement{HTML_ACTUAL, &RegularHtmlElementBridge, std::forward(attributes)...} \ - {} \ - }; \ + NUI_MAKE_HTML_ELEMENT_RENAME(NAME, HTML_ACTUAL) \ } #define NUI_DECLARE_HTML_ELEMENT(NAME) NUI_DECLARE_HTML_ELEMENT_RENAME(NAME, #NAME) \ No newline at end of file From 2f85fc20270fa27c951a417e950892c9c852f862 Mon Sep 17 00:00:00 2001 From: Tim Ebbeke Date: Thu, 19 Oct 2023 01:47:28 +0200 Subject: [PATCH 4/7] Added index file as dependency. --- cmake/backend/emscripten.cmake | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmake/backend/emscripten.cmake b/cmake/backend/emscripten.cmake index fe21a89..5eb5d0c 100644 --- a/cmake/backend/emscripten.cmake +++ b/cmake/backend/emscripten.cmake @@ -131,7 +131,9 @@ function(nui_add_emscripten_target) ) add_custom_target( ${NUI_ADD_EMSCRIPTEN_TARGET_ARGS_TARGET}-parcel-dep - DEPENDS ${NUI_ADD_EMSCRIPTEN_TARGET_ARGS_TARGET}-emscripten + DEPENDS + ${NUI_ADD_EMSCRIPTEN_TARGET_ARGS_TARGET}-emscripten + "${CMAKE_BINARY_DIR}/module_${NUI_ADD_EMSCRIPTEN_TARGET_ARGS_TARGET}/bin/index.html" ) add_dependencies( ${NUI_ADD_EMSCRIPTEN_TARGET_ARGS_TARGET}-emscripten From dd3908b7b4db8be7de3fbbf73048c5112ec4a9e5 Mon Sep 17 00:00:00 2001 From: Tim Ebbeke Date: Thu, 19 Oct 2023 01:49:48 +0200 Subject: [PATCH 5/7] Changed inline switch parameter name. --- cmake/frontend/emscripten.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/frontend/emscripten.cmake b/cmake/frontend/emscripten.cmake index 7bf884f..01481ee 100644 --- a/cmake/frontend/emscripten.cmake +++ b/cmake/frontend/emscripten.cmake @@ -1,7 +1,7 @@ function(nui_prepare_emscripten_target) cmake_parse_arguments( NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS - "NO_INLINE_JS" + "NO_INLINE" "TARGET;PREJS;STATIC;UNPACKED_MODE" "EMSCRIPTEN_LINK_OPTIONS;EMSCRIPTEN_COMPILE_OPTIONS;PARCEL_ARGS" ${ARGN} @@ -14,7 +14,7 @@ function(nui_prepare_emscripten_target) nui_set_target_output_directories(${NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS_TARGET}) - if (NOT NO_INLINE_JS) + if (NOT NO_INLINE) nui_enable_inline(TARGET ${NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS_TARGET}) endif() From 640f35b51a756505bb680b5bfba2f0ee93e5c928 Mon Sep 17 00:00:00 2001 From: Tim Ebbeke Date: Thu, 19 Oct 2023 01:51:42 +0200 Subject: [PATCH 6/7] Changed target name. --- cmake/inline_extractor.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/inline_extractor.cmake b/cmake/inline_extractor.cmake index 55ed051..8de4af1 100644 --- a/cmake/inline_extractor.cmake +++ b/cmake/inline_extractor.cmake @@ -65,10 +65,10 @@ function(nui_enable_inline) ) add_custom_target( - inline-js-${nui_enable_inline_ARGS_TARGET} + nui-inline-${nui_enable_inline_ARGS_TARGET} ALL DEPENDS ${INLINE_IMPORTS_SCRIPTS} DEPENDS ${INLINE_IMPORTS_STYLES} ) - add_dependencies(${nui_enable_inline_ARGS_TARGET} inline-js-${nui_enable_inline_ARGS_TARGET}) + add_dependencies(${nui_enable_inline_ARGS_TARGET} nui-inline-${nui_enable_inline_ARGS_TARGET}) endfunction() \ No newline at end of file From 650cc4b3c636aaac19030aed93c4b2954ade42cd Mon Sep 17 00:00:00 2001 From: Tim Ebbeke Date: Thu, 19 Oct 2023 19:01:32 +0200 Subject: [PATCH 7/7] Implemented inline injector. --- CMakeLists.txt | 1 + cmake/backend/emscripten.cmake | 3 +- cmake/frontend/emscripten.cmake | 15 +++-- cmake/inline_extractor.cmake | 24 +++++++- tools/inline_injector/CMakeLists.txt | 11 ++++ tools/inline_injector/main.cpp | 86 ++++++++++++++++++++++++++++ 6 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 tools/inline_injector/CMakeLists.txt create mode 100644 tools/inline_injector/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 09aa3fc..541c4ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,7 @@ else() add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/tools/parcel_adapter) add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/tools/patch_emscripten_config) add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/tools/inline_parser) + add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/tools/inline_injector) include(${CMAKE_CURRENT_LIST_DIR}/cmake/backend/common.cmake) include(${CMAKE_CURRENT_LIST_DIR}/cmake/backend/emscripten.cmake) endif() diff --git a/cmake/backend/emscripten.cmake b/cmake/backend/emscripten.cmake index 5eb5d0c..c2e2e7b 100644 --- a/cmake/backend/emscripten.cmake +++ b/cmake/backend/emscripten.cmake @@ -110,6 +110,7 @@ function(nui_add_emscripten_target) ${EMCMAKE} cmake ${NUI_ADD_EMSCRIPTEN_TARGET_ARGS_CMAKE_OPTIONS} -DNUI_INLINE_EXTRACTOR_TARGET_FILE=$ + -DNUI_INLINE_INJECTOR_TARGET_FILE=$ "${SOURCE_DIR}" # copy over package.json and fill parcel options that do not exist on it ${BUILD_COMMAND} @@ -121,7 +122,7 @@ function(nui_add_emscripten_target) BUILD_ALWAYS 1 BUILD_BYPRODUCTS "${CMAKE_BINARY_DIR}/module_${NUI_ADD_EMSCRIPTEN_TARGET_ARGS_TARGET}/bin/index.html" INSTALL_COMMAND "" - DEPENDS inline-parser + DEPENDS inline-parser inline-injector parcel-adapter ) add_dependencies(${NUI_ADD_EMSCRIPTEN_TARGET_ARGS_TARGET} ${NUI_ADD_EMSCRIPTEN_TARGET_ARGS_TARGET}-emscripten) add_dependencies(${NUI_ADD_EMSCRIPTEN_TARGET_ARGS_TARGET}-emscripten bin2hpp) diff --git a/cmake/frontend/emscripten.cmake b/cmake/frontend/emscripten.cmake index 01481ee..2a30f78 100644 --- a/cmake/frontend/emscripten.cmake +++ b/cmake/frontend/emscripten.cmake @@ -1,7 +1,7 @@ function(nui_prepare_emscripten_target) cmake_parse_arguments( NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS - "NO_INLINE" + "NO_INLINE;NO_INLINE_INJECT" "TARGET;PREJS;STATIC;UNPACKED_MODE" "EMSCRIPTEN_LINK_OPTIONS;EMSCRIPTEN_COMPILE_OPTIONS;PARCEL_ARGS" ${ARGN} @@ -14,8 +14,16 @@ function(nui_prepare_emscripten_target) nui_set_target_output_directories(${NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS_TARGET}) + if (NOT NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS_UNPACKED_MODE) + set(NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS_UNPACKED_MODE off) + endif() + + set(INLINER_COMMAND "") if (NOT NO_INLINE) nui_enable_inline(TARGET ${NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS_TARGET}) + if (NOT NO_INLINE_INJECT) + set(INLINER_COMMAND COMMAND ${NUI_INLINE_INJECTOR_TARGET_FILE} "${CMAKE_BINARY_DIR}/static/index.html" "${CMAKE_BINARY_DIR}/nui-inline/inline_imports.js" "${CMAKE_BINARY_DIR}/nui-inline/inline_imports.css") + endif() endif() add_custom_target( @@ -28,16 +36,13 @@ function(nui_prepare_emscripten_target) ${NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS_TARGET}-parcel COMMAND ${CMAKE_COMMAND} -E copy_directory "${NUI_SOURCE_DIRECTORY}/nui/js" "${CMAKE_BINARY_DIR}/nui-js" COMMAND ${CMAKE_COMMAND} -E copy_directory ${NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS_STATIC} "${CMAKE_BINARY_DIR}/static" + ${INLINER_COMMAND} COMMAND "${CMAKE_BINARY_DIR}/node_modules/.bin/parcel" build --dist-dir "${CMAKE_BINARY_DIR}/bin" ${NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS_PARCEL_ARGS} WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" BYPRODUCTS "${CMAKE_BINARY_DIR}/bin/index.html" DEPENDS "${CMAKE_BINARY_DIR}/bin/index.js" ) - if (NOT NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS_UNPACKED_MODE) - set(NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS_UNPACKED_MODE off) - endif() - if (${NUI_PREPARE_EMSCRIPTEN_TARGET_ARGS_UNPACKED_MODE}) set(SINGLE_FILE_STRING "") add_custom_target( diff --git a/cmake/inline_extractor.cmake b/cmake/inline_extractor.cmake index 8de4af1..cb7a5f0 100644 --- a/cmake/inline_extractor.cmake +++ b/cmake/inline_extractor.cmake @@ -23,7 +23,7 @@ function(nui_preprocess_inline_js) endfunction() function(nui_enable_inline) - set(one_value_args TARGET) + set(one_value_args TARGET UNPACKED_MODE) set(multi_value_args) cmake_parse_arguments(nui_enable_inline_ARGS "" "${one_value_args}" "${multi_value_args}" ${ARGN}) @@ -71,4 +71,26 @@ function(nui_enable_inline) DEPENDS ${INLINE_IMPORTS_STYLES} ) add_dependencies(${nui_enable_inline_ARGS_TARGET} nui-inline-${nui_enable_inline_ARGS_TARGET}) + + if (NOT nui_enable_inline_ARGS_UNPACKED_MODE) + set(nui_enable_inline_ARGS_UNPACKED_MODE off) + endif() + + if (NOT ${nui_enable_inline_ARGS_UNPACKED_MODE}) + add_custom_command( + OUTPUT + "${CMAKE_BINARY_DIR}/index_inserts.html" + COMMAND ${NUI_INLINE_INJECTOR_TARGET_FILE} "${CMAKE_BINARY_DIR}/module_${nui_enable_inline_ARGS_TARGET}/bin/index.html" ${INLINE_IMPORTS_SCRIPTS} ${INLINE_IMPORTS_STYLES} + DEPENDS + ${INLINE_IMPORTS_SCRIPTS} + ${INLINE_IMPORTS_STYLES} + "${CMAKE_BINARY_DIR}/module_${nui_enable_inline_ARGS_TARGET}/bin/index.html" + ) + + add_custom_target( + nui-inline-inject-${nui_enable_inline_ARGS_TARGET} + ALL + DEPENDS "${CMAKE_BINARY_DIR}/index_inserts.html" + ) + endif() endfunction() \ No newline at end of file diff --git a/tools/inline_injector/CMakeLists.txt b/tools/inline_injector/CMakeLists.txt new file mode 100644 index 0000000..a7d0ff0 --- /dev/null +++ b/tools/inline_injector/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.16) + +project(inline-injector VERSION 0.1.0) + +add_executable(inline-injector main.cpp) +target_compile_features(inline-injector PRIVATE cxx_std_20) + +set_target_properties(inline-injector + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tools_bin" +) \ No newline at end of file diff --git a/tools/inline_injector/main.cpp b/tools/inline_injector/main.cpp new file mode 100644 index 0000000..641d1c4 --- /dev/null +++ b/tools/inline_injector/main.cpp @@ -0,0 +1,86 @@ +#include +#include +#include +#include + +std::string readFile(const std::filesystem::path& path) +{ + std::ifstream file{path, std::ios_base::binary}; + if (!file) + { + throw std::runtime_error{"Could not open " + path.string()}; + } + + file.seekg(0, std::ios::end); + std::string content; + content.resize(file.tellg()); + file.seekg(0, std::ios::beg); + file.read(&content[0], content.size()); + return content; +} + +int main(int argc, char** argv) +{ + if (argc != 4) + { + std::cout << "Expected 3 argument: , but got " << argc - 1 << "\n"; + return 1; + } + + const auto index = std::filesystem::path{argv[1]}; + const auto importScripts = std::filesystem::path{argv[2]}; + const auto importStyles = std::filesystem::path{argv[3]}; + + std::string indexHtml; + try + { + indexHtml = readFile(index); + } + catch (const std::exception& e) + { + std::cout << "Error reading file: " << e.what() << "\n"; + return 1; + } + + // make relative path of import scripts to index file: + const auto relativeImportScriptsFile = std::filesystem::relative(importScripts, index.parent_path()); + const auto relativeImportStylesFile = std::filesystem::relative(importStyles, index.parent_path()); + const auto binIndex = + std::filesystem::relative(index.parent_path() / ".." / "bin" / "index.js", index.parent_path()); + + const std::string importScriptsHtml = "\t\n"; + const std::string importStylesHtml = + "\t\n"; + const std::string importBinIndexHtml = + "\t\n"; + + // find end of header from behind in indexHtml: + const auto headEnd = indexHtml.rfind(""); + if (headEnd == std::string::npos) + { + std::cout << "Could not find in " << index << "\n"; + return 1; + } + + // insert importScriptsHtml before headEnd: + indexHtml.insert(headEnd, importScriptsHtml); + + // insert importStylesHtml before headEnd: + indexHtml.insert(headEnd, importStylesHtml); + + // insert importBinIndexHtml before headEnd: + if (indexHtml.find(binIndex.generic_string()) == std::string::npos) + indexHtml.insert(headEnd, importBinIndexHtml); + + // write indexHtml back to index file: + std::ofstream file{index, std::ios_base::binary}; + if (!file) + { + std::cout << "Could not open " << index << " for writing\n"; + return 1; + } + + file.write(indexHtml.data(), indexHtml.size()); + return 0; +} \ No newline at end of file