├── doc └── logo.png ├── functional_tests ├── ci.cfg ├── lunchtoast.cfg ├── example_3 │ └── test.toast ├── example_1 │ └── test.toast ├── example_2 │ └── test.toast ├── example_4 │ └── test.toast └── example_5 │ └── test.toast ├── src ├── streamreaderposition.h ├── generated_file_type.h ├── nameutils.h ├── irendered_as_string_part.h ├── idocumentnoderenderer.h ├── inodecollection.h ├── textnode.cpp ├── node_utils.h ├── nodereader.h ├── shared_lib_transpiler_renderer.h ├── nodeextension.h ├── streamreader.h ├── utils.h ├── single_header_transpiler_renderer.h ├── textnode.h ├── errors.h ├── sectionnode.h ├── control_flow_statement_node.cpp ├── itranspiler_renderer.h ├── procedurenode.h ├── control_flow_statement_node.h ├── header_and_source_transpiler_renderer.h ├── transpiler.h ├── tagnode.h ├── document_node_interface_access.h ├── idocumentnode.h ├── node_utils.cpp ├── streamreader.cpp ├── procedurenode.cpp ├── codenode.h ├── nodeextension.cpp ├── nameutils.cpp ├── nodereader.cpp ├── sectionnode.cpp ├── transpiler.cpp ├── single_header_transpiler_renderer.cpp ├── codenode.cpp ├── utils.cpp ├── shared_lib_transpiler_renderer.cpp ├── tagnode.cpp ├── header_and_source_transpiler_renderer.cpp └── main.cpp ├── examples ├── CMakeLists.txt ├── ex_06 │ ├── todolist_printer.cpp │ ├── CMakeLists.txt │ └── todolist.htcpp ├── ex_07 │ ├── todolist_printer.cpp │ ├── pageparams.h │ ├── todolist.htcpp │ └── CMakeLists.txt ├── ex_01 │ ├── todolist.htcpp │ ├── todolist_printer.cpp │ └── CMakeLists.txt ├── ex_02 │ ├── todolist.htcpp │ ├── todolist_printer.cpp │ └── CMakeLists.txt ├── ex_03 │ ├── todolist.htcpp │ ├── CMakeLists.txt │ └── todolist_printer.cpp ├── ex_04 │ ├── todolist.htcpp │ ├── todolist_printer.cpp │ └── CMakeLists.txt └── ex_05 │ ├── CMakeLists.txt │ ├── todolist.htcpp │ └── todolist_printer.cpp ├── shared_lib_api └── include │ └── hypertextcpp │ ├── templateloadingerror.h │ ├── itemplate.h │ └── templateloader.h ├── external └── seal_lake ├── tests ├── assert_exception.h ├── CMakeLists.txt ├── test_procedurenode.cpp ├── test_nameutils.cpp ├── test_utils.cpp ├── test_sectionnode.cpp ├── test_tagnode.cpp └── test_codenode.cpp ├── Dockerfile ├── .github └── workflows │ ├── release.yml │ └── build_and_test.yml ├── .clang-format ├── CMakeLists.txt ├── LICENSE.md ├── CMakePresets.json ├── hypertextcpp.cmake └── README.md /doc/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamchatka-volcano/hypertextcpp/HEAD/doc/logo.png -------------------------------------------------------------------------------- /functional_tests/ci.cfg: -------------------------------------------------------------------------------- 1 | #actions: 2 | ### 3 | format = Check example #%1 4 | command = ../../examples-build/ex_0%1 5 | checkOutput = %input 6 | --- -------------------------------------------------------------------------------- /functional_tests/lunchtoast.cfg: -------------------------------------------------------------------------------- 1 | #actions: 2 | ### 3 | format = Check example #%1 4 | command = ../../build/examples/ex_0%1/ex_0%1 5 | checkOutput = %input 6 | --- -------------------------------------------------------------------------------- /src/streamreaderposition.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace htcpp{ 4 | 5 | struct StreamReaderPosition{ 6 | int line = 0; 7 | int column = 0; 8 | }; 9 | 10 | } -------------------------------------------------------------------------------- /functional_tests/example_3/test.toast: -------------------------------------------------------------------------------- 1 | -Suite: examples 2 | 3 | -Check example #3: 4 | 5 |
  • laundry
  • cooking
  • 6 | 7 | --- -------------------------------------------------------------------------------- /src/generated_file_type.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace htcpp { 4 | 5 | enum class GeneratedFileType { 6 | Header, 7 | Source, 8 | }; 9 | 10 | } //namespace htcpp -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(ex_01) 2 | add_subdirectory(ex_02) 3 | add_subdirectory(ex_03) 4 | add_subdirectory(ex_04) 5 | add_subdirectory(ex_05) 6 | add_subdirectory(ex_06) 7 | add_subdirectory(ex_07) -------------------------------------------------------------------------------- /examples/ex_06/todolist_printer.cpp: -------------------------------------------------------------------------------- 1 | #include "todolist.h" 2 | #include 3 | #include 4 | 5 | int main() 6 | { 7 | PageParams pageParams; 8 | auto page = TodoList{}; 9 | page.print(pageParams); 10 | return 0; 11 | } 12 | -------------------------------------------------------------------------------- /examples/ex_07/todolist_printer.cpp: -------------------------------------------------------------------------------- 1 | #include "todolist.h" 2 | #include 3 | #include 4 | 5 | int main() 6 | { 7 | PageParams pageParams; 8 | auto page = TodoList{}; 9 | page.print(pageParams); 10 | return 0; 11 | } 12 | -------------------------------------------------------------------------------- /src/nameutils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace htcpp::utils{ 5 | 6 | std::string toPascalCase(const std::string& name); 7 | std::string toSnakeCase(const std::string& name); 8 | std::string toLowerCase(const std::string& name); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /examples/ex_01/todolist.htcpp: -------------------------------------------------------------------------------- 1 | 2 | 3 |

    $(cfg.name)'s todo list:

    4 |

    No tasks found

    ?(cfg.tasks.empty()) 5 |
      6 |
    • $(task.name)
    • @(auto task : cfg.tasks) 7 |
    8 | 9 | 10 | -------------------------------------------------------------------------------- /functional_tests/example_1/test.toast: -------------------------------------------------------------------------------- 1 | -Suite: examples 2 | 3 | -Check example #1: 4 | 5 | 6 |

    Bob's todo list:

    7 | 8 |
      9 |
    • laundry
    • cooking
    • 10 |
    11 | 12 | 13 | 14 | --- -------------------------------------------------------------------------------- /src/irendered_as_string_part.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | namespace htcpp{ 6 | 7 | class IRenderedAsStringPart : private sfun::interface { 8 | public: 9 | virtual std::string content() const = 0; 10 | }; 11 | 12 | } -------------------------------------------------------------------------------- /src/idocumentnoderenderer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | namespace htcpp{ 6 | 7 | class IDocumentNodeRenderer : private sfun::interface { 8 | public: 9 | virtual std::string renderingCode() const = 0; 10 | }; 11 | 12 | } -------------------------------------------------------------------------------- /src/inodecollection.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | namespace htcpp{ 7 | 8 | class INodeCollection : private sfun::interface { 9 | public: 10 | virtual std::vector> flatten() = 0; 11 | }; 12 | 13 | } -------------------------------------------------------------------------------- /functional_tests/example_2/test.toast: -------------------------------------------------------------------------------- 1 | -Suite: examples 2 | 3 | -Check example #2: 4 | 5 | 6 |

    Bob's todo list:

    7 | 8 |
      9 |
    • laundry
    • cooking
    • 10 |
    11 | 12 | 13 | 14 | --- -------------------------------------------------------------------------------- /examples/ex_07/pageparams.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | struct PageParams{ 6 | struct Task{ 7 | std::string name; 8 | bool isCompleted = false; 9 | }; 10 | std::string name = "Bob"; 11 | std::vector tasks = {{"laundry", true}, {"cooking", false}}; 12 | }; -------------------------------------------------------------------------------- /functional_tests/example_4/test.toast: -------------------------------------------------------------------------------- 1 | -Suite: examples 2 | 3 | -Check example #4: 4 | 5 | 6 | 7 |

    Bob's todo list:

    8 | 9 |
      10 | 11 |
    • laundry
    • cooking
    • 12 |
    13 | 14 | 15 | 16 | --- -------------------------------------------------------------------------------- /examples/ex_02/todolist.htcpp: -------------------------------------------------------------------------------- 1 | 2 | 3 |

    $(cfg.name)'s todo list:

    4 |

    No tasks found

    ?(cfg.tasks.empty()) 5 |
      6 |
    • $(task.name)
    • @(auto task : cfg.tasks) 7 |
    8 | 9 | 10 | -------------------------------------------------------------------------------- /functional_tests/example_5/test.toast: -------------------------------------------------------------------------------- 1 | -Suite: examples 2 | -Contents: libtodolist.so todolist.dll 3 | -Check example #5: 4 | 5 | 6 | 7 | 8 | 9 |

    Bob's todo list:

    10 | 11 |
      12 | 13 |
    • laundry
    • cooking
    • 14 |
    15 | 16 | 17 | 18 | --- -------------------------------------------------------------------------------- /examples/ex_03/todolist.htcpp: -------------------------------------------------------------------------------- 1 | #taskList(){ 2 |
  • $(task.name)
  • @(auto task : cfg.tasks) 3 | } 4 | 5 | 6 |

    $(cfg.name)'s todo list:

    7 |

    No tasks found

    ?(cfg.tasks.empty()) 8 |
      9 | $(taskList()) 10 |
    11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/ex_04/todolist.htcpp: -------------------------------------------------------------------------------- 1 | #taskList(){ 2 |
  • $(task.name)
  • @(auto task : cfg.tasks) 3 | } 4 | 5 | 6 |

    $(cfg.name)'s todo list:

    7 |

    No tasks found

    ?(cfg.tasks.empty()) 8 |
      9 | $(taskList()) 10 |
    11 | 12 | 13 | -------------------------------------------------------------------------------- /shared_lib_api/include/hypertextcpp/templateloadingerror.h: -------------------------------------------------------------------------------- 1 | #ifndef HYPERTEXTCPP_TEMPLATELOADINGERROR_H 2 | #define HYPERTEXTCPP_TEMPLATELOADINGERROR_H 3 | 4 | #include 5 | 6 | namespace htcpp{ 7 | 8 | class TemplateLoadingError: public std::runtime_error 9 | { 10 | using std::runtime_error::runtime_error; 11 | }; 12 | 13 | } 14 | 15 | #endif // HYPERTEXTCPP_TEMPLATELOADINGERROR_H -------------------------------------------------------------------------------- /examples/ex_01/todolist_printer.cpp: -------------------------------------------------------------------------------- 1 | #include "todolist.h" 2 | #include 3 | #include 4 | 5 | int main() 6 | { 7 | struct PageParams{ 8 | struct Task{ 9 | std::string name; 10 | }; 11 | std::string name = "Bob"; 12 | std::vector tasks = {{"laundry"}, {"cooking"}}; 13 | } pageParams; 14 | auto page = todolist{}; 15 | page.print(pageParams); 16 | return 0; 17 | } 18 | -------------------------------------------------------------------------------- /src/textnode.cpp: -------------------------------------------------------------------------------- 1 | #include "textnode.h" 2 | 3 | namespace htcpp { 4 | 5 | TextNode::TextNode(std::string value) 6 | : content_{std::move(value)} 7 | { 8 | } 9 | 10 | std::string TextNode::renderingCode() const 11 | { 12 | return "out << R\"_htcpp_str_(" + content_ + ")_htcpp_str_\";"; 13 | } 14 | 15 | std::string TextNode::content() const 16 | { 17 | return content_; 18 | } 19 | 20 | } //namespace htcpp 21 | -------------------------------------------------------------------------------- /src/node_utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "idocumentnode.h" 3 | #include "inodecollection.h" 4 | #include "textnode.h" 5 | #include 6 | #include 7 | 8 | namespace htcpp{ 9 | 10 | std::vector> flattenNodes(std::vector> nodes); 11 | std::vector> optimizeNodes(std::vector> nodes); 12 | 13 | } -------------------------------------------------------------------------------- /examples/ex_07/todolist.htcpp: -------------------------------------------------------------------------------- 1 | #{ 2 | #include "pageparams.h" 3 | } 4 | 5 | #taskList(){ 6 |
  • $(task.name)
  • @(auto task : cfg.tasks) 7 | } 8 | 9 | 10 |

    $(cfg.name)'s todo list:

    11 |

    No tasks found

    ?(cfg.tasks.empty()) 12 |
      13 | $(taskList()) 14 |
    15 | 16 | 17 | -------------------------------------------------------------------------------- /external/seal_lake: -------------------------------------------------------------------------------- 1 | include(FetchContent) 2 | set(SEAL_LAKE_VERSION v0.2.0) 3 | set(FETCHCONTENT_QUIET FALSE) 4 | FetchContent_Declare(seal_lake_${SEAL_LAKE_VERSION} 5 | SOURCE_DIR seal_lake_${SEAL_LAKE_VERSION} 6 | GIT_REPOSITORY "https://github.com/kamchatka-volcano/seal_lake.git" 7 | GIT_TAG ${SEAL_LAKE_VERSION} 8 | ) 9 | FetchContent_MakeAvailable(seal_lake_${SEAL_LAKE_VERSION}) 10 | include(${seal_lake_${SEAL_LAKE_VERSION}_SOURCE_DIR}/seal_lake.cmake) -------------------------------------------------------------------------------- /examples/ex_03/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.18) 2 | project(ex_03) 3 | 4 | include(../../hypertextcpp.cmake) 5 | 6 | hypertextcpp_GenerateHeader(TEMPLATE_FILE todolist.htcpp) 7 | 8 | set(SRC 9 | todolist_printer.cpp 10 | todolist.h) 11 | 12 | add_executable(${PROJECT_NAME} ${SRC}) 13 | 14 | target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_11) 15 | set_target_properties(${PROJECT_NAME} PROPERTIES CXX_EXTENSIONS OFF) 16 | 17 | -------------------------------------------------------------------------------- /examples/ex_01/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.18) 2 | project(ex_01) 3 | 4 | include(../../hypertextcpp.cmake) 5 | 6 | hypertextcpp_GenerateHeader( 7 | TEMPLATE_FILE todolist.htcpp) 8 | 9 | set(SRC 10 | todolist_printer.cpp 11 | todolist.h) 12 | 13 | add_executable(${PROJECT_NAME} ${SRC}) 14 | 15 | target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_11) 16 | set_target_properties(${PROJECT_NAME} PROPERTIES CXX_EXTENSIONS OFF) 17 | 18 | -------------------------------------------------------------------------------- /examples/ex_04/todolist_printer.cpp: -------------------------------------------------------------------------------- 1 | #include "todolist.h" 2 | #include 3 | #include 4 | 5 | int main() 6 | { 7 | struct PageParams{ 8 | struct Task{ 9 | std::string name; 10 | bool isCompleted = false; 11 | }; 12 | std::string name = "Bob"; 13 | std::vector tasks = {{"laundry", true}, {"cooking", false}}; 14 | } pageParams; 15 | auto page = TodoList{}; 16 | page.print(pageParams); 17 | return 0; 18 | } 19 | -------------------------------------------------------------------------------- /examples/ex_02/todolist_printer.cpp: -------------------------------------------------------------------------------- 1 | #include "template/todolist.h" 2 | #include 3 | #include 4 | 5 | int main() 6 | { 7 | struct PageParams{ 8 | struct Task{ 9 | std::string name; 10 | bool isCompleted = false; 11 | }; 12 | std::string name = "Bob"; 13 | std::vector tasks = {{"laundry", true}, {"cooking", false}}; 14 | } pageParams; 15 | auto page = todolist{}; 16 | page.print(pageParams); 17 | return 0; 18 | } 19 | -------------------------------------------------------------------------------- /examples/ex_03/todolist_printer.cpp: -------------------------------------------------------------------------------- 1 | #include "todolist.h" 2 | #include 3 | #include 4 | 5 | int main() 6 | { 7 | struct PageParams{ 8 | struct Task{ 9 | std::string name; 10 | bool isCompleted = false; 11 | }; 12 | std::string name = "Bob"; 13 | std::vector tasks = {{"laundry", true}, {"cooking", false}}; 14 | } pageParams; 15 | auto page = todolist{}; 16 | page.print("taskList", pageParams); 17 | return 0; 18 | } 19 | -------------------------------------------------------------------------------- /examples/ex_04/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.18) 2 | project(ex_04) 3 | 4 | include(../../hypertextcpp.cmake) 5 | 6 | hypertextcpp_GenerateHeader( 7 | TEMPLATE_FILE todolist.htcpp 8 | CLASS_NAME TodoList 9 | ) 10 | 11 | set(SRC 12 | todolist_printer.cpp 13 | todolist.h) 14 | 15 | add_executable(${PROJECT_NAME} ${SRC}) 16 | 17 | target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_11) 18 | set_target_properties(${PROJECT_NAME} PROPERTIES CXX_EXTENSIONS OFF) 19 | 20 | -------------------------------------------------------------------------------- /examples/ex_02/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.18) 2 | project(ex_02) 3 | 4 | include(../../hypertextcpp.cmake) 5 | 6 | hypertextcpp_GenerateHeader( 7 | TEMPLATE_FILE todolist.htcpp 8 | OUTPUT_DIR template 9 | ) 10 | 11 | set(SRC 12 | todolist_printer.cpp 13 | template/todolist.h) 14 | 15 | add_executable(${PROJECT_NAME} ${SRC}) 16 | 17 | target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_11) 18 | set_target_properties(${PROJECT_NAME} PROPERTIES CXX_EXTENSIONS OFF) 19 | 20 | -------------------------------------------------------------------------------- /tests/assert_exception.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | template 6 | void assert_exception(std::function throwingCode, std::function exceptionContentChecker) 7 | { 8 | try{ 9 | throwingCode(); 10 | FAIL() << "exception wasn't thrown!"; 11 | } 12 | catch(const ExceptionType& e){ 13 | exceptionContentChecker(e); 14 | } 15 | catch(...){ 16 | FAIL() << "Unexpected exception was thrown"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/ex_06/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.18) 2 | project(ex_06) 3 | 4 | include(../../hypertextcpp.cmake) 5 | 6 | hypertextcpp_GenerateHeaderAndSource( 7 | TEMPLATE_FILE todolist.htcpp 8 | CLASS_NAME TodoList 9 | CONFIG_CLASS_NAME PageParams) 10 | 11 | set(SRC 12 | todolist_printer.cpp 13 | todolist.h 14 | todolist.cpp) 15 | 16 | add_executable(${PROJECT_NAME} ${SRC}) 17 | 18 | target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_11) 19 | set_target_properties(${PROJECT_NAME} PROPERTIES CXX_EXTENSIONS OFF) 20 | 21 | -------------------------------------------------------------------------------- /examples/ex_07/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.18) 2 | project(ex_07) 3 | 4 | include(../../hypertextcpp.cmake) 5 | 6 | hypertextcpp_GenerateHeaderAndSource( 7 | TEMPLATE_FILE todolist.htcpp 8 | CLASS_NAME TodoList 9 | CONFIG_CLASS_NAME PageParams) 10 | 11 | set(SRC 12 | todolist_printer.cpp 13 | todolist.h 14 | todolist.cpp) 15 | 16 | add_executable(${PROJECT_NAME} ${SRC}) 17 | 18 | target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_11) 19 | set_target_properties(${PROJECT_NAME} PROPERTIES CXX_EXTENSIONS OFF) 20 | 21 | -------------------------------------------------------------------------------- /src/nodereader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "idocumentnode.h" 3 | #include 4 | #include 5 | 6 | namespace htcpp{ 7 | class GlobalStatementNode; 8 | class ProcedureNode; 9 | class StreamReader; 10 | 11 | std::unique_ptr readTagAttributeNode(StreamReader& stream); 12 | std::unique_ptr readTagContentNode(StreamReader& stream); 13 | std::unique_ptr readNonTagNode(StreamReader& stream); 14 | std::unique_ptr readGlobalStatement(StreamReader& stream); 15 | std::unique_ptr readProcedure(StreamReader& stream); 16 | } 17 | -------------------------------------------------------------------------------- /src/shared_lib_transpiler_renderer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "itranspiler_renderer.h" 3 | #include "procedurenode.h" 4 | 5 | namespace htcpp { 6 | class IDocumentNodeRenderer; 7 | 8 | class SharedLibTranspilerRenderer : public ITranspilerRenderer { 9 | public: 10 | std::unordered_map generateCode( 11 | const std::vector>& globalStatements, 12 | const std::vector>& procedures, 13 | const std::vector>& nodes) const override; 14 | }; 15 | 16 | } //namespace htcpp 17 | -------------------------------------------------------------------------------- /examples/ex_05/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.18) 2 | project(ex_05) 3 | 4 | set(SRC 5 | todolist_printer.cpp) 6 | 7 | add_executable(${PROJECT_NAME} ${SRC}) 8 | 9 | target_include_directories(${PROJECT_NAME} PUBLIC ../../shared_lib_api/include) 10 | target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) 11 | set_target_properties(${PROJECT_NAME} PROPERTIES CXX_EXTENSIONS OFF) 12 | 13 | target_link_libraries(${PROJECT_NAME} ${CMAKE_DL_LIBS}) 14 | 15 | include(../../hypertextcpp.cmake) 16 | hypertextcpp_BuildSharedLibrary( 17 | TEMPLATE_FILE todolist.htcpp 18 | ) 19 | add_dependencies(${PROJECT_NAME} todolist) -------------------------------------------------------------------------------- /src/nodeextension.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | namespace htcpp { 6 | class StreamReader; 7 | 8 | class NodeExtension { 9 | public: 10 | enum class Type { 11 | Conditional, 12 | Loop 13 | }; 14 | 15 | Type type() const; 16 | const std::string& content() const; 17 | 18 | private: 19 | NodeExtension(Type type, std::string content); 20 | 21 | private: 22 | Type type_; 23 | std::string content_; 24 | 25 | friend std::optional readNodeExtension(StreamReader& stream); 26 | }; 27 | 28 | std::optional readNodeExtension(StreamReader& stream); 29 | 30 | } //namespace htcpp 31 | -------------------------------------------------------------------------------- /src/streamreader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "streamreaderposition.h" 3 | #include 4 | #include 5 | #include 6 | 7 | namespace htcpp{ 8 | 9 | class StreamReader{ 10 | public: 11 | explicit StreamReader(std::istream& stream, const StreamReaderPosition& startPosition = StreamReaderPosition{1, 1}); 12 | std::string read(int size = 1); 13 | std::string peek(int size = 1); 14 | void skip(int size); 15 | bool atEnd(); 16 | StreamReaderPosition position() const; 17 | 18 | private: 19 | sfun::member stream_; 20 | StreamReaderPosition position_; 21 | StreamReaderPosition startPosition_; 22 | }; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /examples/ex_06/todolist.htcpp: -------------------------------------------------------------------------------- 1 | #{ 2 | #include 3 | struct PageParams{ 4 | struct Task{ 5 | std::string name; 6 | bool isCompleted = false; 7 | }; 8 | std::string name = "Bob"; 9 | std::vector tasks = {{"laundry", true}, {"cooking", false}}; 10 | }; 11 | } 12 | 13 | #taskList(){ 14 |
  • $(task.name)
  • @(auto task : cfg.tasks) 15 | } 16 | 17 | 18 |

    $(cfg.name)'s todo list:

    19 |

    No tasks found

    ?(cfg.tasks.empty()) 20 |
      21 | $(taskList()) 22 |
    23 | 24 | 25 | -------------------------------------------------------------------------------- /src/utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | namespace htcpp{ 7 | class IDocumentNode; 8 | struct StreamReaderPosition; 9 | } 10 | 11 | namespace htcpp::utils{ 12 | 13 | bool isTagEmptyElement(const std::string& tagName); 14 | std::string transformRawStrings(const std::string& cppCode, const StreamReaderPosition& position); 15 | bool isBlank(const std::string& str); 16 | void trimBlankLines(std::string& str); 17 | 18 | void consumeReadAttributesText(std::string& readText, std::vector>& nodes); 19 | void consumeReadText(std::string& readText, std::vector>& nodes, 20 | IDocumentNode* newNode = nullptr); 21 | } 22 | -------------------------------------------------------------------------------- /src/single_header_transpiler_renderer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "itranspiler_renderer.h" 3 | #include "procedurenode.h" 4 | 5 | namespace htcpp { 6 | 7 | class SingleHeaderTranspilerRenderer : public ITranspilerRenderer { 8 | public: 9 | explicit SingleHeaderTranspilerRenderer(std::string className); 10 | std::unordered_map generateCode( 11 | const std::vector>& globalStatements, 12 | const std::vector>& procedures, 13 | const std::vector>& nodes) const override; 14 | 15 | private: 16 | std::string className_; 17 | }; 18 | 19 | } //namespace htcpp 20 | -------------------------------------------------------------------------------- /src/textnode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "document_node_interface_access.h" 3 | #include "idocumentnode.h" 4 | #include "idocumentnoderenderer.h" 5 | #include "irendered_as_string_part.h" 6 | 7 | namespace htcpp { 8 | 9 | class TextNode : public IDocumentNode, 10 | public IDocumentNodeRenderer, 11 | public IRenderedAsStringPart { 12 | DOCUMENT_NODE_INTERFACE_ACCESS(IDocumentNodeRenderer) 13 | DOCUMENT_NODE_INTERFACE_ACCESS(IRenderedAsStringPart) 14 | 15 | public: 16 | explicit TextNode(std::string value); 17 | std::string renderingCode() const override; 18 | std::string content() const override; 19 | 20 | private: 21 | std::string content_; 22 | }; 23 | 24 | } //namespace htcpp 25 | -------------------------------------------------------------------------------- /src/errors.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "streamreaderposition.h" 3 | #include 4 | #include 5 | 6 | namespace htcpp{ 7 | 8 | class Error : public std::runtime_error{ 9 | public: 10 | explicit Error(const std::string& errorMsg) 11 | : std::runtime_error(errorMsg) 12 | {} 13 | }; 14 | 15 | class ParsingError : public Error{ 16 | using Error::Error; 17 | }; 18 | 19 | class TemplateError : public Error{ 20 | public: 21 | TemplateError(const StreamReaderPosition& errorPosition, const std::string& errorMsg) 22 | : Error("[line:" + std::to_string(errorPosition.line) + 23 | ", column:" + std::to_string(errorPosition.column) + "] " + errorMsg) 24 | { 25 | } 26 | }; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /examples/ex_05/todolist.htcpp: -------------------------------------------------------------------------------- 1 | #{ 2 | #include 3 | struct PageParams{ 4 | struct Task{ 5 | std::string name; 6 | bool isCompleted = false; 7 | }; 8 | std::string name = "Bob"; 9 | std::vector tasks = {{"laundry", true}, {"cooking", false}}; 10 | } pageParams; 11 | HTCPP_CONFIG(PageParams); 12 | } 13 | 14 | #taskList(){ 15 |
  • $(task.name)
  • @(auto task : cfg.tasks) 16 | } 17 | 18 | 19 |

    $(cfg.name)'s todo list:

    20 |

    No tasks found

    ?(cfg.tasks.empty()) 21 |
      22 | $(taskList()) 23 |
    24 | 25 | 26 | -------------------------------------------------------------------------------- /src/sectionnode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "document_node_interface_access.h" 3 | #include "idocumentnode.h" 4 | #include "inodecollection.h" 5 | #include "nodeextension.h" 6 | #include 7 | #include 8 | #include 9 | 10 | namespace htcpp{ 11 | 12 | class SectionNode : public IDocumentNode, 13 | public INodeCollection { 14 | DOCUMENT_NODE_INTERFACE_ACCESS(INodeCollection) 15 | 16 | public: 17 | explicit SectionNode(StreamReader& stream); 18 | std::vector> flatten() override; 19 | 20 | private: 21 | void load(StreamReader& stream); 22 | 23 | private: 24 | std::vector> contentNodes_; 25 | std::optional extension_; 26 | }; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/control_flow_statement_node.cpp: -------------------------------------------------------------------------------- 1 | #include "control_flow_statement_node.h" 2 | 3 | namespace htcpp { 4 | ControlFlowStatementNode::ControlFlowStatementNode(ControlFlowStatementNodeType type, NodeExtension nodeExtension) 5 | : type_{type} 6 | , nodeExtension_{std::move(nodeExtension)} 7 | { 8 | } 9 | 10 | std::string ControlFlowStatementNode::renderingCode() const 11 | { 12 | if (type_ == ControlFlowStatementNodeType::Open) { 13 | if (nodeExtension_.type() == NodeExtension::Type::Conditional) 14 | return "if (" + nodeExtension_.content() + "){ "; 15 | else if (nodeExtension_.type() == NodeExtension::Type::Loop) 16 | return "for (" + nodeExtension_.content() + "){ "; 17 | } 18 | 19 | return " } "; 20 | } 21 | 22 | } //namespace htcpp 23 | -------------------------------------------------------------------------------- /examples/ex_05/todolist_printer.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | struct PageParams{ 7 | struct Task{ 8 | std::string name; 9 | bool isCompleted = false; 10 | }; 11 | std::string name = "Bob"; 12 | std::vector tasks = {{"laundry", true}, {"cooking", false}}; 13 | }; 14 | HTCPP_CONFIG(PageParams); 15 | 16 | int main() 17 | { 18 | auto pageParams = PageParams{}; 19 | #ifdef _WIN32 20 | const auto libFilename = std::string{"./todolist.dll"}; 21 | #else 22 | const auto libFilename = std::string{"./libtodolist.so"}; 23 | #endif 24 | auto page = htcpp::loadTemplate(std::filesystem::canonical(libFilename).string()); 25 | page->print(pageParams); 26 | return 0; 27 | } 28 | -------------------------------------------------------------------------------- /src/itranspiler_renderer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "generated_file_type.h" 3 | #include "idocumentnoderenderer.h" 4 | #include "procedurenode.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace htcpp { 13 | class IDocumentNodeRenderer; 14 | 15 | class ITranspilerRenderer : private sfun::interface { 16 | public: 17 | virtual std::unordered_map generateCode( 18 | const std::vector>& globalStatements, 19 | const std::vector>& procedures, 20 | const std::vector>& nodes) const = 0; 21 | }; 22 | 23 | } //namespace htcpp -------------------------------------------------------------------------------- /src/procedurenode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "document_node_interface_access.h" 3 | #include "idocumentnode.h" 4 | #include "idocumentnoderenderer.h" 5 | #include 6 | #include 7 | #include 8 | 9 | namespace htcpp { 10 | class StreamReader; 11 | 12 | class ProcedureNode : public IDocumentNode, 13 | public IDocumentNodeRenderer { 14 | DOCUMENT_NODE_INTERFACE_ACCESS(IDocumentNodeRenderer) 15 | 16 | public: 17 | ProcedureNode(std::string procedureName, StreamReader& stream); 18 | const std::string& name() const; 19 | 20 | std::string renderingCode() const override; 21 | 22 | private: 23 | void load(StreamReader& stream); 24 | 25 | private: 26 | std::string procedureName_; 27 | std::vector> contentNodes_; 28 | }; 29 | 30 | } //namespace htcpp 31 | -------------------------------------------------------------------------------- /src/control_flow_statement_node.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "document_node_interface_access.h" 3 | #include "idocumentnode.h" 4 | #include "idocumentnoderenderer.h" 5 | #include "nodeextension.h" 6 | #include 7 | #include 8 | 9 | namespace htcpp { 10 | 11 | enum class ControlFlowStatementNodeType { 12 | Open, 13 | Close 14 | }; 15 | 16 | class ControlFlowStatementNode : public IDocumentNode, 17 | public IDocumentNodeRenderer { 18 | DOCUMENT_NODE_INTERFACE_ACCESS(IDocumentNodeRenderer) 19 | 20 | public: 21 | ControlFlowStatementNode(ControlFlowStatementNodeType, NodeExtension nodeExtension); 22 | std::string renderingCode() const override; 23 | 24 | private: 25 | ControlFlowStatementNodeType type_; 26 | NodeExtension nodeExtension_; 27 | }; 28 | 29 | } //namespace htcpp -------------------------------------------------------------------------------- /src/header_and_source_transpiler_renderer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "itranspiler_renderer.h" 3 | #include "procedurenode.h" 4 | 5 | namespace htcpp { 6 | 7 | class HeaderAndSourceTranspilerRenderer : public ITranspilerRenderer { 8 | public: 9 | HeaderAndSourceTranspilerRenderer(std::string className, std::string headerFileName, std::string configTypeName); 10 | 11 | std::unordered_map generateCode( 12 | const std::vector>& globalStatements, 13 | const std::vector>& procedures, 14 | const std::vector>& nodes) const override; 15 | 16 | private: 17 | std::string className_; 18 | std::string headerFileName_; 19 | std::string configTypeName_; 20 | }; 21 | 22 | } //namespace htcpp 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.18.3 AS hypertextcpp-build-container 2 | ARG cmake_preset 3 | RUN apk update && \ 4 | apk add --no-cache \ 5 | git \ 6 | build-base \ 7 | ccache \ 8 | cmake \ 9 | clang \ 10 | clang-dev \ 11 | mold \ 12 | samurai 13 | 14 | WORKDIR /hypertextcpp_src 15 | COPY external ./external/ 16 | COPY src ./src/ 17 | COPY tests ./tests/ 18 | COPY CMakeLists.txt . 19 | COPY CMakePresets.json . 20 | RUN cmake --preset "$cmake_preset" -B build -DCMAKE_BUILD_TYPE=MinSizeRel -DCMAKE_EXE_LINKER_FLAGS="-static" -DENABLE_TESTS=ON 21 | RUN cmake --build build 22 | RUN strip --strip-all build/hypertextcpp 23 | 24 | 25 | FROM scratch AS hypertextcpp-build 26 | COPY --from=hypertextcpp-build-container /hypertextcpp_src/build/hypertextcpp . 27 | COPY --from=hypertextcpp-build-container /hypertextcpp_src/build/tests/test_hypertextcpp ./tests/ 28 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.18) 2 | project(test_hypertextcpp) 3 | 4 | set(SRC 5 | test_codenode.cpp 6 | test_procedurenode.cpp 7 | test_nameutils.cpp 8 | test_sectionnode.cpp 9 | test_tagnode.cpp 10 | test_utils.cpp 11 | ../src/codenode.cpp 12 | ../src/procedurenode.cpp 13 | ../src/nameutils.cpp 14 | ../src/nodeextension.cpp 15 | ../src/nodereader.cpp 16 | ../src/sectionnode.cpp 17 | ../src/streamreader.cpp 18 | ../src/tagnode.cpp 19 | ../src/textnode.cpp 20 | ../src/transpiler.cpp 21 | ../src/utils.cpp 22 | ../src/node_utils.cpp 23 | ../src/control_flow_statement_node.cpp 24 | ) 25 | 26 | SealLake_GoogleTest( 27 | SOURCES ${SRC} 28 | COMPILE_FEATURES cxx_std_20 29 | PROPERTIES 30 | CXX_EXTENSIONS OFF 31 | INCLUDES 32 | ../src 33 | ${SEAL_LAKE_SOURCE_range-v3}/include 34 | LIBRARIES 35 | Microsoft.GSL::GSL 36 | sfun::sfun 37 | ) 38 | 39 | -------------------------------------------------------------------------------- /src/transpiler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "generated_file_type.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace htcpp{ 11 | namespace fs = std::filesystem; 12 | class IDocumentNode; 13 | class ITranspilerRenderer; 14 | class GlobalStatementNode; 15 | class ProcedureNode; 16 | class StreamReader; 17 | 18 | class Transpiler final{ 19 | public: 20 | explicit Transpiler(ITranspilerRenderer&); 21 | std::unordered_map process(const fs::path& filePath); 22 | 23 | private: 24 | bool readNode(StreamReader& stream); 25 | void parseTemplateFile(const fs::path& filePath); 26 | void reset(); 27 | 28 | private: 29 | sfun::member renderer_; 30 | std::string readText_; 31 | std::vector> nodeList_; 32 | std::vector> globalStatementList_; 33 | std::vector> procedureList_; 34 | }; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/tagnode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "document_node_interface_access.h" 3 | #include "idocumentnode.h" 4 | #include "inodecollection.h" 5 | #include "nodeextension.h" 6 | #include "streamreaderposition.h" 7 | #include 8 | #include 9 | #include 10 | 11 | namespace htcpp { 12 | 13 | class TagNode : public IDocumentNode, 14 | public INodeCollection { 15 | DOCUMENT_NODE_INTERFACE_ACCESS(INodeCollection) 16 | 17 | enum class ReadResult { 18 | Ok, 19 | ParsingCompleted 20 | }; 21 | 22 | public: 23 | explicit TagNode(StreamReader& stream); 24 | std::vector> flatten() override; 25 | 26 | private: 27 | void load(StreamReader& stream); 28 | TagNode::ReadResult readName(StreamReader& stream, const htcpp::StreamReaderPosition& nodePos); 29 | ReadResult readAttributes(StreamReader& stream); 30 | 31 | private: 32 | std::string readText_; 33 | std::string name_; 34 | bool attributesRead_ = false; 35 | std::vector> attributeNodes_; 36 | std::vector> contentNodes_; 37 | std::optional extension_; 38 | }; 39 | 40 | } //namespace htcpp 41 | -------------------------------------------------------------------------------- /src/document_node_interface_access.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | #define DOCUMENT_NODE_ADD_INTERFACE_GETTER(InterfaceName) \ 5 | virtual sfun::optional_ref get##InterfaceName() const { return {}; } \ 6 | virtual sfun::optional_ref get##InterfaceName() { return {}; } 7 | 8 | 9 | #define DOCUMENT_NODE_REGISTER_INTERFACE(InterfaceName) \ 10 | class InterfaceName; \ 11 | template<> \ 12 | struct InterfaceGetterMapping{ \ 13 | using GetterPtr = sfun::optional_ref(IDocumentNode::*)(); \ 14 | constexpr static GetterPtr getterPtr(){return &IDocumentNode::get##InterfaceName;} \ 15 | }; 16 | 17 | 18 | #define DOCUMENT_NODE_INTERFACE_ACCESS_READ(InterfaceName) \ 19 | sfun::optional_ref getInterfaceName() const override { return *this; } 20 | 21 | #define DOCUMENT_NODE_INTERFACE_ACCESS_WRITE(InterfaceName) \ 22 | sfun::optional_ref getInterfaceName() override { return *this; } 23 | 24 | #define DOCUMENT_NODE_INTERFACE_ACCESS(InterfaceName) \ 25 | sfun::optional_ref get##InterfaceName() const override { return *this; } \ 26 | sfun::optional_ref get##InterfaceName() override { return *this; } 27 | -------------------------------------------------------------------------------- /src/idocumentnode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "document_node_interface_access.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace htcpp { 12 | class INodeCollection; 13 | class IDocumentNodeRenderer; 14 | class IRenderedAsStringPart; 15 | 16 | template 17 | struct InterfaceGetterMapping; 18 | 19 | class IDocumentNode : private sfun::interface 20 | { 21 | public: 22 | template 23 | auto interface() const 24 | { 25 | return (this->*InterfaceGetterMapping::getterPtr())(); 26 | } 27 | 28 | template 29 | auto interface() 30 | { 31 | return (this->*InterfaceGetterMapping::getterPtr())(); 32 | } 33 | 34 | template 35 | bool hasType() 36 | { 37 | return typeid(*this) == typeid(T); 38 | } 39 | 40 | private: 41 | DOCUMENT_NODE_ADD_INTERFACE_GETTER(INodeCollection); 42 | DOCUMENT_NODE_ADD_INTERFACE_GETTER(IDocumentNodeRenderer); 43 | DOCUMENT_NODE_ADD_INTERFACE_GETTER(IRenderedAsStringPart); 44 | 45 | template 46 | friend struct InterfaceGetterMapping; 47 | }; 48 | 49 | DOCUMENT_NODE_REGISTER_INTERFACE(INodeCollection); 50 | DOCUMENT_NODE_REGISTER_INTERFACE(IDocumentNodeRenderer); 51 | DOCUMENT_NODE_REGISTER_INTERFACE(IRenderedAsStringPart); 52 | 53 | 54 | } // namespace htcpp 55 | -------------------------------------------------------------------------------- /src/node_utils.cpp: -------------------------------------------------------------------------------- 1 | #include "node_utils.h" 2 | #include 3 | #include 4 | 5 | namespace htcpp{ 6 | 7 | std::vector> flattenNodes(std::vector> nodes) 8 | { 9 | auto flattenNodes = std::vector>{}; 10 | for (auto& node : nodes) { 11 | if (const auto nodeCollection = node->template interface()) 12 | std::ranges::move(nodeCollection->flatten(), std::back_inserter(flattenNodes)); 13 | else 14 | flattenNodes.emplace_back(std::move(node)); 15 | } 16 | return flattenNodes; 17 | } 18 | 19 | std::vector> optimizeNodes(std::vector> nodes) 20 | { 21 | auto processedNodes = std::vector>{}; 22 | for (auto& node : nodes) { 23 | if (auto text = node->interface()) { 24 | if (!processedNodes.empty() && processedNodes.back()->interface()) { 25 | auto prevText = processedNodes.back()->interface()->content(); 26 | processedNodes.back() = std::make_unique(prevText + text->content()); 27 | } 28 | else 29 | processedNodes.emplace_back(std::make_unique(text->content())); 30 | } 31 | else { 32 | processedNodes.emplace_back(std::move(node)); 33 | } 34 | } 35 | return processedNodes; 36 | } 37 | } -------------------------------------------------------------------------------- /tests/test_procedurenode.cpp: -------------------------------------------------------------------------------- 1 | #include "assert_exception.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace{ 9 | 10 | void test(const std::string& input, const std::string& expectedName, const std::string& expectedCode) 11 | { 12 | auto stream = std::istringstream{input}; 13 | auto streamReader = htcpp::StreamReader{stream}; 14 | auto procedure = readProcedure(streamReader); 15 | auto result = procedure->renderingCode(); 16 | EXPECT_EQ(result, expectedCode); 17 | EXPECT_EQ(expectedName, procedure->name()); 18 | } 19 | 20 | void testError(const std::string& input, const std::string& expectedErrorMsg) 21 | { 22 | assert_exception( 23 | [input]{ 24 | auto stream = std::istringstream{input}; 25 | auto streamReader = htcpp::StreamReader{stream}; 26 | auto procedure = readProcedure(streamReader); 27 | }, 28 | [expectedErrorMsg](const htcpp::TemplateError& e){ 29 | EXPECT_EQ(e.what(), expectedErrorMsg); 30 | }); 31 | } 32 | 33 | } 34 | 35 | TEST(ProcedureNode, Basic) 36 | { 37 | test("#hello_world(){

    Hello World!

    }", 38 | "hello_world", 39 | "out << R\"_htcpp_str_(

    Hello World!

    )_htcpp_str_\";"); 40 | } 41 | 42 | 43 | TEST(InvalidProcedureNode, Unclosed) 44 | { 45 | testError("#hello_world(){

    Hello World!

    ", 46 | "[line:1, column:1] Procedure isn't closed with '}'"); 47 | } 48 | -------------------------------------------------------------------------------- /src/streamreader.cpp: -------------------------------------------------------------------------------- 1 | #include "streamreader.h" 2 | 3 | namespace htcpp{ 4 | 5 | StreamReader::StreamReader(std::istream& stream, const StreamReaderPosition& startPosition) 6 | : stream_(stream) 7 | , startPosition_(startPosition) 8 | { 9 | } 10 | 11 | std::string StreamReader::read(int size) 12 | { 13 | auto result = std::string{}; 14 | auto ch = char{}; 15 | for (auto i = 0; i < size; ++i){ 16 | if (!stream_.get().get(ch)) 17 | return {}; 18 | if (ch == '\n'){ 19 | position_.line++; 20 | position_.column = 0; 21 | } 22 | else if (ch == '\t') 23 | position_.column += 4; 24 | else 25 | position_.column++; 26 | result.push_back(ch); 27 | } 28 | return result; 29 | } 30 | 31 | std::string StreamReader::peek(int size) 32 | { 33 | auto result = std::string{}; 34 | auto ch = char{}; 35 | auto pos = stream_.get().tellg(); 36 | for (auto i = 0; i < size; ++i){ 37 | if (!stream_.get().get(ch)){ 38 | stream_.get().clear(); 39 | result.clear(); 40 | break; 41 | } 42 | result.push_back(ch); 43 | } 44 | stream_.get().seekg(pos); 45 | return result; 46 | } 47 | 48 | void StreamReader::skip(int size) 49 | { 50 | read(size); 51 | } 52 | 53 | bool StreamReader::atEnd() 54 | { 55 | return peek().empty(); 56 | } 57 | 58 | StreamReaderPosition StreamReader::position() const 59 | { 60 | return {startPosition_.line + position_.line, 61 | startPosition_.column + position_.column}; 62 | } 63 | 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/procedurenode.cpp: -------------------------------------------------------------------------------- 1 | #include "procedurenode.h" 2 | #include "errors.h" 3 | #include "idocumentnoderenderer.h" 4 | #include "nodereader.h" 5 | #include "streamreader.h" 6 | #include "utils.h" 7 | #include "node_utils.h" 8 | 9 | namespace htcpp{ 10 | 11 | ProcedureNode::ProcedureNode(std::string procedureName, 12 | StreamReader& stream) 13 | : procedureName_(std::move(procedureName)) 14 | { 15 | Expects(!procedureName_.empty()); 16 | load(stream); 17 | } 18 | 19 | const std::string& ProcedureNode::name() const 20 | { 21 | return procedureName_; 22 | } 23 | 24 | void ProcedureNode::load(StreamReader& stream) 25 | { 26 | auto nodePos = stream.position(); 27 | auto openSeq = stream.read(static_cast(procedureName_.size()) + 4); 28 | Expects(openSeq == "#" + procedureName_ + "(){"); 29 | 30 | auto readText = std::string{}; 31 | while (!stream.atEnd()){ 32 | if (stream.peek() == "}"){ 33 | stream.skip(1); 34 | utils::consumeReadText(readText, contentNodes_); 35 | contentNodes_ = optimizeNodes(flattenNodes(std::move(contentNodes_))); 36 | return; 37 | } 38 | auto node = readNonTagNode(stream); 39 | if (node){ 40 | utils::consumeReadText(readText, contentNodes_, node.get()); 41 | contentNodes_.emplace_back(std::move(node)); 42 | } 43 | else 44 | readText += stream.read(); 45 | } 46 | throw TemplateError{nodePos, "Procedure isn't closed with '}'"}; 47 | } 48 | 49 | std::string ProcedureNode::renderingCode() const 50 | { 51 | auto result = std::string{}; 52 | for (auto& node : contentNodes_) 53 | result += node->interface()->renderingCode(); 54 | return result; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/codenode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "document_node_interface_access.h" 3 | #include "idocumentnode.h" 4 | #include "idocumentnoderenderer.h" 5 | #include "nodeextension.h" 6 | #include 7 | 8 | namespace htcpp { 9 | 10 | class CodeNode { 11 | public: 12 | CodeNode(std::string nodeTypeName, char idToken, char openToken, char closeToken, StreamReader& stream); 13 | const std::string& content() const; 14 | const std::optional& extension() const; 15 | 16 | private: 17 | void load(StreamReader& stream); 18 | 19 | private: 20 | std::string nodeTypeName_; 21 | char idToken_; 22 | char openToken_; 23 | char closeToken_; 24 | std::string content_; 25 | std::optional extension_; 26 | }; 27 | 28 | class ExpressionNode : public IDocumentNode, 29 | public IDocumentNodeRenderer { 30 | DOCUMENT_NODE_INTERFACE_ACCESS(IDocumentNodeRenderer) 31 | public: 32 | explicit ExpressionNode(StreamReader& stream); 33 | std::string renderingCode() const override; 34 | 35 | private: 36 | CodeNode impl_; 37 | }; 38 | 39 | class StatementNode : public IDocumentNode, 40 | public IDocumentNodeRenderer { 41 | DOCUMENT_NODE_INTERFACE_ACCESS(IDocumentNodeRenderer) 42 | 43 | public: 44 | explicit StatementNode(StreamReader& stream); 45 | std::string renderingCode() const override; 46 | 47 | private: 48 | CodeNode impl_; 49 | }; 50 | 51 | class GlobalStatementNode : public IDocumentNode, 52 | public IDocumentNodeRenderer { 53 | DOCUMENT_NODE_INTERFACE_ACCESS(IDocumentNodeRenderer) 54 | public: 55 | explicit GlobalStatementNode(StreamReader& stream); 56 | std::string renderingCode() const override; 57 | 58 | private: 59 | CodeNode impl_; 60 | }; 61 | 62 | } 63 | -------------------------------------------------------------------------------- /shared_lib_api/include/hypertextcpp/itemplate.h: -------------------------------------------------------------------------------- 1 | #ifndef HYPERTEXTCPP_ITEMPLATE_H 2 | #define HYPERTEXTCPP_ITEMPLATE_H 3 | 4 | #include 5 | #include 6 | #ifdef _WIN32 7 | #include 8 | #define HTCPP_TEMPLATE_LIB_HANDLE HMODULE 9 | #else 10 | #include 11 | #define HTCPP_TEMPLATE_LIB_HANDLE void* 12 | #endif 13 | 14 | namespace htcpp{ 15 | 16 | template 17 | class ITemplate{ 18 | public: 19 | virtual ~ITemplate() = default; 20 | virtual std::string render(const TCfg&) const = 0; 21 | virtual std::string render(const std::string& renderFuncName, const TCfg&) const = 0; 22 | virtual void print(const TCfg&) const = 0; 23 | virtual void print(const std::string& renderFuncName, const TCfg&) const = 0; 24 | virtual void print(const TCfg& cfg, std::ostream& stream) const = 0; 25 | virtual void print(const std::string& renderFuncName, const TCfg& cfg, std::ostream& stream) const = 0; 26 | }; 27 | 28 | template 29 | class TemplatePtrDeleter 30 | { 31 | using DeleteTemplateFunc = void(htcpp::ITemplate*); 32 | 33 | public: 34 | TemplatePtrDeleter(DeleteTemplateFunc deleteFunc, HTCPP_TEMPLATE_LIB_HANDLE templateHandle) 35 | : deleteFunc_{deleteFunc} 36 | , templateHandle_{templateHandle} 37 | {} 38 | 39 | void operator()(ITemplate* templatePtr) 40 | { 41 | deleteFunc_(templatePtr); 42 | #ifdef _WIN32 43 | FreeLibrary(templateHandle_); 44 | #else 45 | dlclose(templateHandle_); 46 | #endif 47 | } 48 | 49 | private: 50 | void(*deleteFunc_)(htcpp::ITemplate*); 51 | HTCPP_TEMPLATE_LIB_HANDLE templateHandle_; 52 | }; 53 | 54 | template 55 | using TemplatePtr = std::unique_ptr, TemplatePtrDeleter>; 56 | 57 | } 58 | 59 | #undef HTCPP_TEMPLATE_LIB_HANDLE 60 | #endif // HYPERTEXTCPP_ITEMPLATE_H -------------------------------------------------------------------------------- /tests/test_nameutils.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | using namespace htcpp::utils; 5 | 6 | TEST(NameUtils, SnakeToPascal) 7 | { 8 | EXPECT_EQ(toPascalCase(""), ""); 9 | EXPECT_EQ(toPascalCase("hello_world"), "HelloWorld"); 10 | EXPECT_EQ(toPascalCase("_hello_world_"), "HelloWorld"); 11 | EXPECT_EQ(toPascalCase("great_2_see_u"), "Great2SeeU"); 12 | EXPECT_EQ(toPascalCase("helloWorld"), "HelloWorld"); 13 | EXPECT_EQ(toPascalCase("helloWorld2"), "HelloWorld2"); 14 | EXPECT_EQ(toPascalCase("HelloWorld2"), "HelloWorld2"); 15 | } 16 | 17 | TEST(NameUtils, KebabToSnake) 18 | { 19 | EXPECT_EQ(toSnakeCase(""), ""); 20 | EXPECT_EQ(toSnakeCase("hello-world"), "hello_world"); 21 | EXPECT_EQ(toSnakeCase("hello1-"), "hello1"); 22 | EXPECT_EQ(toSnakeCase("-hello"), "hello"); 23 | EXPECT_EQ(toSnakeCase("_hello1"), "hello1"); 24 | EXPECT_EQ(toSnakeCase("_hello_1_"), "hello_1"); 25 | } 26 | 27 | TEST(NameUtils, CamelToSnake) 28 | { 29 | EXPECT_EQ(toSnakeCase("helloWorld"), "hello_world"); 30 | EXPECT_EQ(toSnakeCase("hello"), "hello"); 31 | EXPECT_EQ(toSnakeCase("_helloWorld_"), "hello_world"); 32 | } 33 | 34 | TEST(NameUtils, PascalToSnake) 35 | { 36 | EXPECT_EQ(toSnakeCase("HelloWorld"), "hello_world"); 37 | EXPECT_EQ(toSnakeCase("Hello"), "hello"); 38 | EXPECT_EQ(toSnakeCase("_HelloWorld_"), "hello_world"); 39 | } 40 | 41 | TEST(NameUtils, SnakeToLower) 42 | { 43 | EXPECT_EQ(toLowerCase(""), ""); 44 | EXPECT_EQ(toLowerCase("hello_world"), "helloworld"); 45 | EXPECT_EQ(toLowerCase("_hello_world_"), "helloworld"); 46 | EXPECT_EQ(toLowerCase("hello"), "hello"); 47 | } 48 | 49 | TEST(NameUtils, CamelToLower) 50 | { 51 | EXPECT_EQ(toLowerCase("helloWorld"), "helloworld"); 52 | EXPECT_EQ(toLowerCase("_helloWorld_"), "helloworld"); 53 | EXPECT_EQ(toLowerCase("hello"), "hello"); 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: [ "v*" ] 6 | 7 | jobs: 8 | build-windows: 9 | name: Build Windows version 10 | runs-on: windows-latest 11 | steps: 12 | - name: Install ninja 13 | run: choco install ninja 14 | 15 | - uses: actions/checkout@v4 16 | - uses: rui314/setup-mold@v1 17 | - uses: ilammy/msvc-dev-cmd@v1 18 | 19 | - name: Configure CMake 20 | run: cmake --preset=msvc-release -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=Release 21 | 22 | - name: Build 23 | run: cmake --build ${{github.workspace}}/build --config Release 24 | 25 | - name: Upload build artifact 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: hypertextcpp-windows-latest 29 | path: | 30 | ${{github.workspace}}/build/hypertextcpp.exe 31 | 32 | release_hypertextcpp: 33 | name: Release hypertextcpp 34 | runs-on: ubuntu-latest 35 | needs: build-windows 36 | steps: 37 | - name: Git checkout 38 | uses: actions/checkout@v4 39 | 40 | - name: Build linux version in Docker 41 | run: DOCKER_BUILDKIT=1 docker build --build-arg cmake_preset=clang-release --output build . 42 | 43 | - name: Run unit tests 44 | working-directory: ${{github.workspace}}/build/tests 45 | run: ./test_hypertextcpp 46 | 47 | - name: Download hypertextcpp Windows build 48 | uses: actions/download-artifact@v4 49 | with: 50 | name: hypertextcpp-windows-latest 51 | path: build 52 | - name: Archive shared_lib_api 53 | run: zip -r shared_lib_api.zip shared_lib_api 54 | - name: Upload release 55 | uses: softprops/action-gh-release@v2 56 | with: 57 | files: | 58 | build/hypertextcpp 59 | build/hypertextcpp.exe 60 | shared_lib_api.zip -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: WebKit 3 | AlignAfterOpenBracket: AlwaysBreak 4 | AllowAllArgumentsOnNextLine: false 5 | AllowAllConstructorInitializersOnNextLine: false 6 | AllowAllParametersOfDeclarationOnNextLine: false 7 | AllowShortFunctionsOnASingleLine: Empty 8 | AllowShortLambdasOnASingleLine: Empty 9 | AllowShortEnumsOnASingleLine: false 10 | AllowShortIfStatementsOnASingleLine: Never 11 | AllowShortLoopsOnASingleLine: false 12 | AllowShortBlocksOnASingleLine: Empty 13 | AlwaysBreakTemplateDeclarations: Yes 14 | BinPackArguments: false 15 | BinPackParameters: false 16 | BraceWrapping: 17 | AfterFunction: true 18 | BeforeElse: true 19 | BeforeLambdaBody: true 20 | BeforeWhile: true 21 | BeforeCatch: true 22 | BreakBeforeBraces: Custom 23 | BreakBeforeBinaryOperators: None 24 | BreakInheritanceList: AfterComma 25 | ColumnLimit: 120 26 | ContinuationIndentWidth: 8 27 | Cpp11BracedListStyle: true 28 | NamespaceIndentation: None 29 | PenaltyBreakBeforeFirstCallParameter: 0 30 | PenaltyReturnTypeOnItsOwnLine: 1000 31 | PenaltyBreakAssignment: 10 32 | SpaceBeforeCpp11BracedList: false 33 | SpaceInEmptyBlock: false 34 | SpaceInEmptyParentheses: false 35 | SpaceAfterTemplateKeyword: false 36 | SpacesInLineCommentPrefix: 37 | Minimum: 0 38 | Maximum: -1 39 | FixNamespaceComments: true 40 | UseCRLF: false 41 | IncludeCategories: 42 | # Headers in <> without extension. 43 | - Regex: '<[[:alnum:]\-_]+>' 44 | Priority: 6 45 | # Headers in <> from specific external libraries. 46 | - Regex: '<(gtest|gmock|boost|gsl)\/' 47 | Priority: 5 48 | # Headers in <> with subdirectory. 49 | - Regex: '<[[:alnum:]\-_]+\/' 50 | Priority: 4 51 | # Headers in <> with extension. 52 | - Regex: '<[[:alnum:].\-_]+>' 53 | Priority: 3 54 | # Headers in "" with subdirectory. 55 | - Regex: '"[[:alnum:]\-_]+\/' 56 | Priority: 2 57 | # Headers in "" with extension. 58 | - Regex: '"[[:alnum:].\-_]+"' 59 | Priority: 1 60 | ... 61 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.18) 2 | project(hypertextcpp VERSION 2.0.0 DESCRIPTION "hypertextcpp") 3 | 4 | include(external/seal_lake) 5 | 6 | SealLake_Import(cmdlime 2.7.0 7 | GIT_REPOSITORY https://github.com/kamchatka-volcano/cmdlime 8 | GIT_TAG v2.7.0 9 | ) 10 | 11 | SealLake_Import(sfun 5.1.0 12 | GIT_REPOSITORY https://github.com/kamchatka-volcano/sfun 13 | GIT_TAG v5.1.0 14 | ) 15 | 16 | SealLake_Import(gsl 4.0.0 17 | GIT_REPOSITORY https://github.com/microsoft/GSL.git 18 | GIT_TAG v4.0.0 19 | ) 20 | 21 | SealLake_Bundle( 22 | NAME range-v3 23 | SKIP_LOAD 24 | GIT_REPOSITORY https://github.com/ericniebler/range-v3 25 | GIT_TAG 0.12.0 26 | ) 27 | SealLake_Import(fmt 9.1.0 28 | GIT_REPOSITORY https://github.com/fmtlib/fmt 29 | GIT_TAG 9.1.0 30 | ) 31 | 32 | set(SRC 33 | src/codenode.cpp 34 | src/control_flow_statement_node.cpp 35 | src/main.cpp 36 | src/procedurenode.cpp 37 | src/nameutils.cpp 38 | src/nodeextension.cpp 39 | src/nodereader.cpp 40 | src/sectionnode.cpp 41 | src/streamreader.cpp 42 | src/tagnode.cpp 43 | src/textnode.cpp 44 | src/transpiler.cpp 45 | src/utils.cpp 46 | src/node_utils.cpp 47 | src/single_header_transpiler_renderer.cpp 48 | src/shared_lib_transpiler_renderer.cpp 49 | src/header_and_source_transpiler_renderer.cpp) 50 | 51 | 52 | SealLake_Executable( 53 | SOURCES ${SRC} 54 | COMPILE_FEATURES cxx_std_20 55 | PROPERTIES 56 | CXX_EXTENSIONS OFF 57 | INCLUDES ${SEAL_LAKE_SOURCE_range-v3}/include 58 | LIBRARIES 59 | cmdlime::cmdlime 60 | sfun::sfun 61 | Microsoft.GSL::GSL 62 | fmt::fmt 63 | ) 64 | 65 | SealLake_OptionalSubProjects(tests examples) 66 | 67 | install(DIRECTORY shared_lib_api/include/hypertextcpp 68 | COMPONENT shared_lib_api 69 | EXCLUDE_FROM_ALL 70 | DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) 71 | -------------------------------------------------------------------------------- /src/nodeextension.cpp: -------------------------------------------------------------------------------- 1 | #include "nodeextension.h" 2 | #include "streamreader.h" 3 | #include "errors.h" 4 | #include "utils.h" 5 | 6 | namespace htcpp{ 7 | 8 | namespace{ 9 | std::string nodeExtensionName(NodeExtension::Type type) 10 | { 11 | switch(type){ 12 | case NodeExtension::Type::Conditional : return "Conditional extension"; 13 | case NodeExtension::Type::Loop: return "Loop extension"; 14 | default: return {}; 15 | } 16 | } 17 | } 18 | 19 | NodeExtension::NodeExtension(NodeExtension::Type type, 20 | std::string content) 21 | : type_(type) 22 | , content_(std::move(content)) 23 | { 24 | } 25 | 26 | NodeExtension::Type NodeExtension::type() const 27 | { 28 | return type_; 29 | } 30 | 31 | const std::string& NodeExtension::content() const 32 | { 33 | return content_; 34 | } 35 | 36 | std::optional readNodeExtension(StreamReader& stream) 37 | { 38 | const auto nodePos = stream.position(); 39 | const auto nextChars = stream.peek(2); 40 | if (nextChars.empty()) 41 | return std::nullopt; 42 | 43 | auto extensionType = NodeExtension::Type{}; 44 | if (nextChars == "?(") 45 | extensionType = NodeExtension::Type::Conditional; 46 | else if (nextChars == "@(") 47 | extensionType = NodeExtension::Type::Loop; 48 | else 49 | return std::nullopt; 50 | 51 | stream.skip(2); 52 | auto openParenthesisNum = 1; 53 | auto extensionContent = std::string{}; 54 | while(!stream.atEnd()){ 55 | auto res = stream.read(); 56 | if (res == "(") 57 | openParenthesisNum++; 58 | else if (res == ")"){ 59 | openParenthesisNum--; 60 | if (openParenthesisNum == 0){ 61 | if (utils::isBlank(extensionContent)) 62 | throw TemplateError{nodePos, nodeExtensionName(extensionType) + " can't be empty"}; 63 | return NodeExtension{extensionType, extensionContent}; 64 | } 65 | } 66 | extensionContent += res; 67 | } 68 | throw TemplateError{nodePos, nodeExtensionName(extensionType) + " isn't closed with ')'"}; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /tests/test_utils.cpp: -------------------------------------------------------------------------------- 1 | #include "assert_exception.h" 2 | #include 3 | #include 4 | #include 5 | 6 | TEST(Utils, TransformRawString) 7 | { 8 | { 9 | auto code = "auto x = `Hello World`;"; 10 | EXPECT_EQ(htcpp::utils::transformRawStrings(code, {1, 1}), 11 | "auto x = R\"_htcpp_str_(Hello World)_htcpp_str_\";"); 12 | } 13 | { 14 | auto code = "auto x = ``;"; 15 | EXPECT_EQ(htcpp::utils::transformRawStrings(code, {1, 1}), 16 | "auto x = R\"_htcpp_str_()_htcpp_str_\";"); 17 | } 18 | } 19 | 20 | TEST(Utils, TransformInvalidRawString) 21 | { 22 | assert_exception( 23 | []{ 24 | auto result = htcpp::utils::transformRawStrings("auto x = `Hello World;", {1, 1}); 25 | }, 26 | [](const auto& error) 27 | { 28 | EXPECT_EQ(error.what(), std::string{"[line:1, column:11] String is unclosed"}); 29 | }); 30 | } 31 | 32 | TEST(Utils, TrimLastBlankLine) 33 | { 34 | { 35 | auto str = std::string{}; 36 | htcpp::utils::trimBlankLines(str); 37 | EXPECT_EQ(str, ""); 38 | } 39 | { 40 | auto str = std::string{"Hello\n "}; 41 | htcpp::utils::trimBlankLines(str); 42 | EXPECT_EQ(str, "Hello"); 43 | } 44 | { 45 | auto str = std::string{"Hello \n "}; 46 | htcpp::utils::trimBlankLines(str); 47 | EXPECT_EQ(str, "Hello \n "); 48 | } 49 | { 50 | auto str = std::string{"Hello\r\n "}; 51 | htcpp::utils::trimBlankLines(str); 52 | EXPECT_EQ(str, "Hello"); 53 | } 54 | { 55 | auto str = std::string{"Hello \r\n "}; 56 | htcpp::utils::trimBlankLines(str); 57 | EXPECT_EQ(str, "Hello \r\n "); 58 | } 59 | { 60 | auto str = std::string{"\n Hello"}; 61 | htcpp::utils::trimBlankLines(str); 62 | EXPECT_EQ(str, "Hello"); 63 | } 64 | { 65 | auto str = std::string{"\r\n Hello"}; 66 | htcpp::utils::trimBlankLines(str); 67 | EXPECT_EQ(str, "Hello"); 68 | } 69 | { 70 | auto str = std::string{"\n Hello\r\n "}; 71 | htcpp::utils::trimBlankLines(str); 72 | EXPECT_EQ(str, "Hello"); 73 | } 74 | { 75 | auto str = std::string{"\r\n Hello\n "}; 76 | htcpp::utils::trimBlankLines(str); 77 | EXPECT_EQ(str, "Hello"); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/nameutils.cpp: -------------------------------------------------------------------------------- 1 | #include "nameutils.h" 2 | #include 3 | #include 4 | 5 | 6 | namespace htcpp::utils{ 7 | namespace{ 8 | std::string formatName(const std::string& name) 9 | { 10 | auto result = name; 11 | //remove front non-alphabet characters 12 | result.erase(result.begin(), std::find_if(result.begin(), result.end(), 13 | [](int ch){ 14 | return std::isalpha(ch); 15 | }) 16 | ); 17 | //remove back non-alphabet and non-digit characters 18 | result.erase(std::find_if(result.rbegin(), result.rend(), 19 | [](int ch){ return std::isalnum(ch);}).base(), result.end()); 20 | return result; 21 | } 22 | 23 | } 24 | 25 | std::string toPascalCase(const std::string& name) 26 | { 27 | auto result = std::string{}; 28 | auto prevCharNonAlpha = false; 29 | auto formattedName = formatName(name); 30 | if (!formattedName.empty()) 31 | formattedName[0] = static_cast(std::toupper(formattedName[0])); 32 | for (auto ch : formattedName){ 33 | if (!std::isalpha(ch)){ 34 | if (std::isdigit(ch)) 35 | result.push_back(ch); 36 | if (!result.empty()) 37 | prevCharNonAlpha = true; 38 | continue; 39 | } 40 | if (prevCharNonAlpha) 41 | ch = static_cast(std::toupper(ch)); 42 | result.push_back(ch); 43 | prevCharNonAlpha = false; 44 | } 45 | return result; 46 | } 47 | 48 | std::string toSnakeCase(const std::string& name) 49 | { 50 | auto result = std::string{}; 51 | auto formattedName = formatName(sfun::replace(name, "-", "_")); 52 | if (!formattedName.empty()) 53 | formattedName[0] = static_cast(std::tolower(formattedName[0])); 54 | for (auto ch : formattedName){ 55 | if (std::isupper(ch) && !result.empty()){ 56 | result.push_back('_'); 57 | result.push_back(static_cast(std::tolower(ch))); 58 | } 59 | else 60 | result.push_back(ch); 61 | } 62 | return result; 63 | } 64 | 65 | std::string toLowerCase(const std::string& name) 66 | { 67 | auto result = std::string{}; 68 | for (auto ch : formatName(name)){ 69 | if (std::isalnum(ch)) 70 | result.push_back(static_cast(std::tolower(ch))); 71 | } 72 | return result; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/nodereader.cpp: -------------------------------------------------------------------------------- 1 | #include "nodereader.h" 2 | #include "tagnode.h" 3 | #include "sectionnode.h" 4 | #include "streamreader.h" 5 | #include "codenode.h" 6 | #include "procedurenode.h" 7 | 8 | namespace htcpp{ 9 | 10 | namespace{ 11 | std::unique_ptr readCommonNodes(StreamReader& stream) 12 | { 13 | if (stream.atEnd()) 14 | return nullptr; 15 | if (stream.peek(2) == "[[") 16 | return std::make_unique(stream); 17 | if (stream.peek(2) == "$(") 18 | return std::make_unique(stream); 19 | if (stream.peek(2) == "${") 20 | return std::make_unique(stream); 21 | return nullptr; 22 | } 23 | } 24 | 25 | std::unique_ptr readTagAttributeNode(StreamReader& stream) 26 | { 27 | return readCommonNodes(stream); 28 | } 29 | 30 | std::unique_ptr readTagContentNode(StreamReader& stream) 31 | { 32 | if (stream.atEnd()) 33 | return nullptr; 34 | if (stream.peek(2) != "(stream); 36 | 37 | return readCommonNodes(stream); 38 | } 39 | 40 | std::unique_ptr readNonTagNode(StreamReader& stream) 41 | { 42 | if (stream.atEnd()) 43 | return nullptr; 44 | if (stream.peek() == "<") 45 | return std::make_unique(stream); 46 | 47 | return readCommonNodes(stream); 48 | } 49 | 50 | std::unique_ptr readGlobalStatement(StreamReader& stream) 51 | { 52 | if (stream.atEnd()) 53 | return nullptr; 54 | if (stream.peek(2) == "#{") 55 | return std::make_unique(stream); 56 | return nullptr; 57 | } 58 | 59 | std::unique_ptr readProcedure(StreamReader& stream) 60 | { 61 | if (stream.atEnd()) 62 | return nullptr; 63 | if (stream.peek() == "#"){ 64 | for(auto i = 2;; ++i){ 65 | auto text = stream.peek(i); 66 | if (text.empty()) 67 | return nullptr; 68 | if (std::isspace(text.back())) 69 | return nullptr; 70 | if (text.back() == '(' && text.size() > 1){ 71 | auto name = std::string {text.begin() + 1, text.end() - 1}; 72 | if (stream.peek(static_cast(name.size()) + 4) == "#" + name + "(){") 73 | return std::make_unique(name, stream); 74 | } 75 | } 76 | } 77 | return nullptr; 78 | } 79 | 80 | } 81 | 82 | -------------------------------------------------------------------------------- /src/sectionnode.cpp: -------------------------------------------------------------------------------- 1 | #include "sectionnode.h" 2 | #include "control_flow_statement_node.h" 3 | #include "errors.h" 4 | #include "idocumentnoderenderer.h" 5 | #include "nodereader.h" 6 | #include "streamreader.h" 7 | #include "textnode.h" 8 | #include "utils.h" 9 | #include 10 | 11 | namespace htcpp{ 12 | 13 | SectionNode::SectionNode(StreamReader& stream) 14 | { 15 | load(stream); 16 | } 17 | 18 | void SectionNode::load(StreamReader& stream) 19 | { 20 | const auto nodePos = stream.position(); 21 | auto openSeq = stream.read(2); 22 | Expects(openSeq == "[["); 23 | 24 | extension_ = readNodeExtension(stream); 25 | 26 | auto readText = std::string{}; 27 | while (!stream.atEnd()){ 28 | if (stream.peek(2) == "]]"){ 29 | stream.skip(2); 30 | utils::consumeReadText(readText, contentNodes_); 31 | const auto extensionPos = stream.position(); 32 | const auto closingBracesExtension = readNodeExtension(stream); 33 | if (closingBracesExtension.has_value()){ 34 | if (extension_.has_value()) 35 | throw TemplateError{extensionPos, "Section can't have multiple extensions"}; 36 | extension_ = closingBracesExtension; 37 | } 38 | if (contentNodes_.empty()) 39 | throw TemplateError{nodePos, "Section can't be empty"}; 40 | return; 41 | } 42 | auto node = readNonTagNode(stream); 43 | if (node){ 44 | utils::consumeReadText(readText, contentNodes_, node.get()); 45 | contentNodes_.emplace_back(std::move(node)); 46 | } 47 | else 48 | readText += stream.read(); 49 | } 50 | throw TemplateError{nodePos, "Section isn't closed with ']]'"}; 51 | } 52 | 53 | std::vector> SectionNode::flatten() 54 | { 55 | auto result = std::vector>{}; 56 | if (extension_.has_value()) 57 | result.emplace_back(std::make_unique(ControlFlowStatementNodeType::Open, extension_.value())); 58 | for (auto& node : contentNodes_) { 59 | if (auto nodeCollection = node->interface()) 60 | std::ranges::move(nodeCollection->flatten(), std::back_inserter(result)); 61 | else 62 | result.emplace_back(std::move(node)); 63 | } 64 | if (extension_.has_value()) 65 | result.emplace_back(std::make_unique(ControlFlowStatementNodeType::Close, extension_.value())); 66 | 67 | return result; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Microsoft Public License (Ms-PL) 2 | Copyright 2021 Gorelyy PA 3 | 4 | This license governs use of the accompanying software. If you use the software, 5 | you accept this license. If you do not accept the license, do not use the 6 | software. 7 | 8 | 1. Definitions 9 | 10 | The terms "reproduce," "reproduction," "derivative works," and "distribution" 11 | have the same meaning here as under U.S. copyright law. 12 | 13 | A "contribution" is the original software, or any additions or changes to the 14 | software. 15 | 16 | A "contributor" is any person that distributes its contribution under this 17 | license. 18 | 19 | "Licensed patents" are a contributor's patent claims that read directly on its 20 | contribution. 21 | 22 | 2. Grant of Rights 23 | 24 | (A) Copyright Grant- Subject to the terms of this license, including the 25 | license conditions and limitations in section 3, each contributor grants you a 26 | non-exclusive, worldwide, royalty-free copyright license to reproduce its 27 | contribution, prepare derivative works of its contribution, and distribute its 28 | contribution or any derivative works that you create. 29 | 30 | (B) Patent Grant- Subject to the terms of this license, including the license 31 | conditions and limitations in section 3, each contributor grants you a 32 | non-exclusive, worldwide, royalty-free license under its licensed patents to 33 | make, have made, use, sell, offer for sale, import, and/or otherwise dispose of 34 | its contribution in the software or derivative works of the contribution in the 35 | software. 36 | 37 | 3. Conditions and Limitations 38 | 39 | (A) No Trademark License- This license does not grant you rights to use any 40 | contributors' name, logo, or trademarks. 41 | 42 | (B) If you bring a patent claim against any contributor over patents that you 43 | claim are infringed by the software, your patent license from such contributor 44 | to the software ends automatically. 45 | 46 | (C) If you distribute any portion of the software, you must retain all 47 | copyright, patent, trademark, and attribution notices that are present in the 48 | software. 49 | 50 | (D) If you distribute any portion of the software in source code form, you may 51 | do so only under this license by including a complete copy of this license with 52 | your distribution. If you distribute any portion of the software in compiled or 53 | object code form, you may only do so under a license that complies with this 54 | license. 55 | 56 | (E) The software is licensed "as-is." You bear the risk of using it. The 57 | contributors give no express warranties, guarantees or conditions. You may have 58 | additional consumer rights under your local laws which this license cannot 59 | change. To the extent permitted under your local laws, the contributors exclude 60 | the implied warranties of merchantability, fitness for a particular purpose and 61 | non-infringement. 62 | -------------------------------------------------------------------------------- /shared_lib_api/include/hypertextcpp/templateloader.h: -------------------------------------------------------------------------------- 1 | #ifndef HYPERTEXTCPP_TEMPLATELOADER_H 2 | #define HYPERTEXTCPP_TEMPLATELOADER_H 3 | 4 | #include "itemplate.h" 5 | #include "templateloadingerror.h" 6 | #include 7 | #include 8 | #ifdef _WIN32 9 | #include 10 | #else 11 | #include 12 | #endif 13 | 14 | 15 | namespace htcpp{ 16 | 17 | template 18 | inline TemplatePtr loadTemplate(const std::string&) 19 | { 20 | return nullptr; 21 | } 22 | 23 | #ifdef _WIN32 24 | #define HTCPP_CONFIG(TCfg)\ 25 | template <>\ 26 | inline htcpp::TemplatePtr htcpp::loadTemplate(const std::string& libraryName)\ 27 | {\ 28 | using MakeTemplateFunc = htcpp::ITemplate*();\ 29 | using DeleteTemplateFunc = void(htcpp::ITemplate*);\ 30 | \ 31 | HMODULE templateLib = LoadLibrary(libraryName.c_str());\ 32 | if (!templateLib)\ 33 | throw TemplateLoadingError{std::string{} + "Cannot load library " + libraryName};\ 34 | \ 35 | \ 36 | MakeTemplateFunc* makeTemplate = reinterpret_cast(GetProcAddress(templateLib, "makeTemplate"));\ 37 | if (!makeTemplate) {\ 38 | throw TemplateLoadingError{"Cannot load symbol makeTemplate"};\ 39 | }\ 40 | \ 41 | DeleteTemplateFunc* deleteTemplate = reinterpret_cast(GetProcAddress(templateLib, "deleteTemplate"));\ 42 | if (!deleteTemplate)\ 43 | throw TemplateLoadingError{std::string{} + "Cannot load symbol deleteTemplate"};\ 44 | \ 45 | return htcpp::TemplatePtr{makeTemplate(), TemplatePtrDeleter{deleteTemplate, templateLib}};\ 46 | } 47 | #else 48 | #define HTCPP_CONFIG(TCfg)\ 49 | template <>\ 50 | inline htcpp::TemplatePtr htcpp::loadTemplate(const std::string& libraryName)\ 51 | {\ 52 | using MakeTemplateFunc = htcpp::ITemplate*();\ 53 | using DeleteTemplateFunc = void(htcpp::ITemplate*);\ 54 | \ 55 | void* templateLib = dlopen(libraryName.c_str(), RTLD_LAZY);\ 56 | if (!templateLib)\ 57 | throw TemplateLoadingError{std::string{} + "Cannot load library " + libraryName + ": " + dlerror()};\ 58 | \ 59 | dlerror();\ 60 | \ 61 | MakeTemplateFunc* makeTemplate = reinterpret_cast(dlsym(templateLib, "makeTemplate"));\ 62 | const char* dlsym_error = dlerror();\ 63 | if (dlsym_error) {\ 64 | throw TemplateLoadingError{std::string{} + "Cannot load symbol makeTemplate: " + dlsym_error};\ 65 | }\ 66 | \ 67 | DeleteTemplateFunc* deleteTemplate = reinterpret_cast(dlsym(templateLib, "deleteTemplate"));\ 68 | dlsym_error = dlerror();\ 69 | if (dlsym_error)\ 70 | throw TemplateLoadingError{std::string{} + "Cannot load symbol deleteTemplate: " + dlsym_error};\ 71 | \ 72 | return htcpp::TemplatePtr{makeTemplate(), TemplatePtrDeleter{deleteTemplate, templateLib}};\ 73 | } 74 | #endif 75 | 76 | 77 | } 78 | 79 | #endif // HYPERTEXTCPP_TEMPLATELOADER_H -------------------------------------------------------------------------------- /src/transpiler.cpp: -------------------------------------------------------------------------------- 1 | #include "transpiler.h" 2 | #include "errors.h" 3 | #include "itranspiler_renderer.h" 4 | #include "node_utils.h" 5 | #include "nodereader.h" 6 | #include "procedurenode.h" 7 | #include "streamreader.h" 8 | #include "utils.h" 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace htcpp { 17 | 18 | Transpiler::Transpiler(ITranspilerRenderer& renderer) 19 | : renderer_{renderer} 20 | { 21 | } 22 | 23 | void Transpiler::parseTemplateFile(const fs::path& filePath) 24 | { 25 | auto fileStream = std::ifstream{filePath, std::ios_base::binary}; 26 | if (!fileStream.is_open()) 27 | throw ParsingError("Can't open file " + filePath.string()); 28 | 29 | auto stream = StreamReader{fileStream}; 30 | auto prototypeFuncMap = std::map{}; 31 | 32 | while (!stream.atEnd()) 33 | if (!readNode(stream)) 34 | readText_ += stream.read(); 35 | 36 | utils::consumeReadText(readText_, nodeList_); 37 | } 38 | 39 | bool Transpiler::readNode(StreamReader& stream) 40 | { 41 | auto node = readNonTagNode(stream); 42 | if (node) { 43 | utils::consumeReadText(readText_, nodeList_, node.get()); 44 | nodeList_.push_back(std::move(node)); 45 | return true; 46 | } 47 | auto globalStatement = readGlobalStatement(stream); 48 | if (globalStatement) { 49 | utils::consumeReadText(readText_, nodeList_); 50 | globalStatementList_.push_back(std::move(globalStatement)); 51 | return true; 52 | } 53 | auto procedure = readProcedure(stream); 54 | if (procedure) { 55 | utils::consumeReadText(readText_, nodeList_); 56 | procedureList_.push_back(std::move(procedure)); 57 | return true; 58 | } 59 | return false; 60 | } 61 | 62 | namespace { 63 | std::vector> nodesToNodeRenderers( 64 | const std::vector>& nodes) 65 | { 66 | const auto toRenderer = [](const std::unique_ptr& node) 67 | { 68 | Expects(node->interface().has_value()); 69 | return gsl::not_null{ &node->interface().value()}; 70 | }; 71 | return nodes | ranges::views::transform(toRenderer) | ranges::to(); 72 | } 73 | 74 | } //namespace 75 | 76 | std::unordered_map Transpiler::process(const fs::path& filePath) 77 | { 78 | reset(); 79 | parseTemplateFile(filePath); 80 | return renderer_.get().generateCode( 81 | nodesToNodeRenderers(globalStatementList_), 82 | procedureList_, 83 | nodesToNodeRenderers(optimizeNodes(flattenNodes(std::move(nodeList_))))); 84 | } 85 | 86 | void Transpiler::reset() 87 | { 88 | nodeList_.clear(); 89 | globalStatementList_.clear(); 90 | procedureList_.clear(); 91 | readText_.clear(); 92 | } 93 | 94 | } //namespace htcpp 95 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 6, 3 | "configurePresets": [ 4 | { 5 | "name": "base-linux", 6 | "hidden": true, 7 | "displayName": "linux base preset", 8 | "generator": "Ninja", 9 | "binaryDir" : "build-${presetName}", 10 | "cacheVariables": { 11 | "CMAKE_EXE_LINKER_FLAGS" : "-fuse-ld=mold", 12 | "CMAKE_CXX_COMPILER_LAUNCHER": "ccache", 13 | "CPM_SOURCE_CACHE": "cpm_cache" 14 | } 15 | }, 16 | { 17 | "name": "clang-base", 18 | "hidden": true, 19 | "displayName": "clang base preset", 20 | "inherits": "base-linux", 21 | "cacheVariables": { 22 | "CMAKE_CXX_COMPILER": "clang++", 23 | "CMAKE_C_COMPILER": "clang", 24 | "CMAKE_CXX_FLAGS" : "-Wall -Wextra -Wpedantic -Werror -Wcast-align -Wnon-virtual-dtor -Woverloaded-virtual -Wunused" 25 | } 26 | }, 27 | { 28 | "name": "clang-debug", 29 | "displayName": "clang (Debug)", 30 | "inherits": "clang-base", 31 | "cacheVariables": { 32 | "CMAKE_BUILD_TYPE": "Debug" 33 | } 34 | }, 35 | { 36 | "name": "clang-release", 37 | "displayName": "clang (Release)", 38 | "inherits": "clang-base", 39 | "cacheVariables": { 40 | "CMAKE_BUILD_TYPE": "Release" 41 | } 42 | }, 43 | { 44 | "name": "gcc-base", 45 | "hidden": true, 46 | "displayName": "gcc base preset", 47 | "inherits": "base-linux", 48 | "cacheVariables": { 49 | "CMAKE_CXX_COMPILER": "g++", 50 | "CMAKE_C_COMPILER": "gcc", 51 | "CMAKE_CXX_FLAGS" : "-Wall -Wextra -Wpedantic -Werror -Wcast-align -Wnon-virtual-dtor -Woverloaded-virtual -Wunused -Wno-nonnull-compare" 52 | } 53 | }, 54 | { 55 | "name": "gcc-debug", 56 | "displayName": "gcc (Debug)", 57 | "inherits": "gcc-base", 58 | "cacheVariables": { 59 | "CMAKE_BUILD_TYPE": "Debug" 60 | } 61 | }, 62 | { 63 | "name": "gcc-release", 64 | "displayName": "gcc (Release)", 65 | "inherits": "gcc-base", 66 | "cacheVariables": { 67 | "CMAKE_BUILD_TYPE": "Release" 68 | } 69 | }, 70 | { 71 | "name": "base-windows", 72 | "displayName": "windows base preset", 73 | "hidden": true, 74 | "generator": "Ninja", 75 | "binaryDir": "build-${presetName}", 76 | "architecture": { 77 | "value": "x64", 78 | "strategy": "external" 79 | }, 80 | "cacheVariables": { 81 | "CPM_SOURCE_CACHE": "cpm_cache" 82 | }, 83 | "vendor": { 84 | "microsoft.com/VisualStudioSettings/CMake/1.0": { 85 | "hostOS": [ "Windows" ] 86 | }, 87 | "jetbrains.com/clion": { 88 | "toolchain": "Visual Studio" 89 | } 90 | } 91 | }, 92 | { 93 | "name": "msvc-base", 94 | "hidden": true, 95 | "displayName": "msvc base preset", 96 | "inherits": "base-windows", 97 | "cacheVariables": { 98 | "CMAKE_CXX_COMPILER": "cl", 99 | "CMAKE_C_COMPILER": "cl", 100 | "CMAKE_CXX_FLAGS" : "/EHsc /W4 /WX" 101 | } 102 | }, 103 | { 104 | "name": "msvc-debug", 105 | "displayName": "msvc (Debug)", 106 | "inherits": "msvc-base", 107 | "cacheVariables": { 108 | "CMAKE_BUILD_TYPE": "Debug" 109 | } 110 | }, 111 | { 112 | "name": "msvc-release", 113 | "displayName": "msvc (Release)", 114 | "inherits": "msvc-base", 115 | "cacheVariables": { 116 | "CMAKE_BUILD_TYPE": "Release" 117 | } 118 | } 119 | ] 120 | } -------------------------------------------------------------------------------- /src/single_header_transpiler_renderer.cpp: -------------------------------------------------------------------------------- 1 | #include "single_header_transpiler_renderer.h" 2 | #include "idocumentnoderenderer.h" 3 | #include "procedurenode.h" 4 | 5 | namespace htcpp { 6 | 7 | SingleHeaderTranspilerRenderer::SingleHeaderTranspilerRenderer(std::string className) 8 | : className_(std::move(className)) 9 | { 10 | } 11 | 12 | std::unordered_map SingleHeaderTranspilerRenderer::generateCode( 13 | const std::vector>& globalStatements, 14 | const std::vector>& procedures, 15 | const std::vector>& nodes) const 16 | { 17 | auto result = std::string{}; 18 | result += "#pragma once\n" 19 | "#include \n" 20 | "#include \n" 21 | "#include \n"; 22 | for (const auto& globalStatement : globalStatements) 23 | result += globalStatement->renderingCode() + "\n"; 24 | result += "\n"; 25 | 26 | result += "class " + className_ + "{\n"; 27 | result += "private:\n"; 28 | result += "template\n"; 29 | result += "class Renderer{\n"; 30 | result += "const TCfg& cfg;\n"; 31 | result += "std::ostream& out;\n"; 32 | for (const auto& procedure : procedures) { 33 | result += " std::string " + procedure->name() + "() const{\n"; 34 | result += procedure->renderingCode() + "\nreturn {};\n}\n"; 35 | } 36 | 37 | result += R"( 38 | public: 39 | Renderer(const TCfg& cfg, std::ostream& out) 40 | : cfg(cfg), out(out) 41 | {} 42 | void renderHTML() const 43 | { 44 | )"; 45 | for (const auto& node : nodes) 46 | result += node->renderingCode(); 47 | result += " }\n"; 48 | 49 | result += R"( 50 | void renderHTMLPart(const std::string& name) const 51 | { 52 | static_cast(name); 53 | )"; 54 | 55 | for (const auto& procedure : procedures) { 56 | result += "if (name == \"" + procedure->name() + "\")\n"; 57 | result += " " + procedure->name() + "();\n"; 58 | } 59 | result += " }\n"; 60 | 61 | result += "};\n"; 62 | result += R"( 63 | public: 64 | template 65 | std::string render(const TCfg& cfg) const 66 | { 67 | auto stream = std::stringstream{}; 68 | auto renderer = Renderer{cfg, stream}; 69 | renderer.renderHTML(); 70 | return stream.str(); 71 | } 72 | template 73 | std::string render(const std::string& renderFuncName, const TCfg& cfg) const 74 | { 75 | auto stream = std::stringstream{}; 76 | auto renderer = Renderer{cfg, stream}; 77 | renderer.renderHTMLPart(renderFuncName); 78 | return stream.str(); 79 | } 80 | template 81 | void print(const TCfg& cfg) const 82 | { 83 | auto renderer = Renderer{cfg, std::cout}; 84 | renderer.renderHTML(); 85 | } 86 | template 87 | void print(const std::string& renderFuncName, const TCfg& cfg) const 88 | { 89 | auto renderer = Renderer{cfg, std::cout}; 90 | renderer.renderHTMLPart(renderFuncName); 91 | } 92 | template 93 | void print(const TCfg& cfg, std::ostream& stream) const 94 | { 95 | auto renderer = Renderer{cfg, stream}; 96 | renderer.renderHTML(cfg, stream); 97 | } 98 | template 99 | void print(const std::string& renderFuncName, const TCfg& cfg, std::ostream& stream) const 100 | { 101 | auto renderer = Renderer{cfg, stream}; 102 | renderer.renderHTML(renderFuncName, cfg, stream); 103 | } 104 | };)"; 105 | 106 | return {{GeneratedFileType::Header, std::move(result)}}; 107 | } 108 | 109 | } //namespace htcpp 110 | -------------------------------------------------------------------------------- /src/codenode.cpp: -------------------------------------------------------------------------------- 1 | #include "codenode.h" 2 | #include "streamreader.h" 3 | #include "errors.h" 4 | #include "utils.h" 5 | #include 6 | 7 | namespace htcpp{ 8 | 9 | CodeNode::CodeNode(std::string nodeTypeName, 10 | char idToken, 11 | char openToken, 12 | char closeToken, 13 | StreamReader& stream) 14 | : nodeTypeName_(std::move(nodeTypeName)) 15 | , idToken_(idToken) 16 | , openToken_(openToken) 17 | , closeToken_(closeToken) 18 | { 19 | load(stream); 20 | } 21 | 22 | void CodeNode::load(StreamReader& stream) 23 | { 24 | auto nodePos = stream.position(); 25 | auto openSeq = stream.read(2); 26 | Expects(openSeq == std::string{} + idToken_ + openToken_); 27 | extension_ = readNodeExtension(stream); 28 | 29 | auto openParenthesisNum = 1; 30 | auto insideString = char{0}; 31 | auto lastStringPos = StreamReaderPosition{}; 32 | while(!stream.atEnd()){ 33 | auto res = stream.read(); 34 | if (!insideString){ 35 | if (res == "'" || res == "`" || res == "\"") 36 | insideString = res.front(); 37 | lastStringPos = stream.position(); 38 | } 39 | else if (res.front() == insideString) { 40 | insideString = 0; 41 | lastStringPos = stream.position(); 42 | } 43 | 44 | if (!insideString){ 45 | if (res.front() == openToken_) 46 | openParenthesisNum++; 47 | else if (res.front() == closeToken_){ 48 | openParenthesisNum--; 49 | if (openParenthesisNum == 0){ 50 | const auto extensionPos = stream.position(); 51 | const auto closingTokenExtension = readNodeExtension(stream); 52 | if (closingTokenExtension.has_value()){ 53 | if (extension_.has_value()) 54 | throw TemplateError{extensionPos, nodeTypeName_ + " can't have multiple extensions"}; 55 | extension_ = closingTokenExtension; 56 | } 57 | if (utils::isBlank(content_)) 58 | throw TemplateError(nodePos, nodeTypeName_ + " can't be empty"); 59 | 60 | content_ = utils::transformRawStrings(content_, nodePos); 61 | return; 62 | } 63 | } 64 | } 65 | content_ += res; 66 | } 67 | if (insideString) { 68 | if (insideString != '\'') 69 | throw TemplateError{lastStringPos, std::string{"String isn't closed with '"} + insideString + "'"}; 70 | else 71 | throw TemplateError{lastStringPos, "Char literal isn't closed with \"'\""}; 72 | } 73 | 74 | throw TemplateError{nodePos, nodeTypeName_ + " isn't closed with '" + closeToken_ + "'"}; 75 | } 76 | 77 | const std::string& CodeNode::content() const 78 | { 79 | return content_; 80 | } 81 | 82 | const std::optional& CodeNode::extension() const 83 | { 84 | return extension_; 85 | } 86 | 87 | std::string ExpressionNode::renderingCode() const 88 | { 89 | auto result = std::string{}; 90 | const auto& extension = impl_.extension(); 91 | if (extension.has_value()){ 92 | if (extension.value().type() == NodeExtension::Type::Conditional) 93 | result += "if (" + extension.value().content() + "){ "; 94 | else if (extension.value().type() == NodeExtension::Type::Loop) 95 | result += "for (" + extension.value().content() + "){ "; 96 | } 97 | result += "out << (" + impl_.content() + ");"; 98 | if (extension.has_value()) 99 | result += " } "; 100 | return result; 101 | } 102 | 103 | ExpressionNode::ExpressionNode(StreamReader& stream) 104 | : impl_("Expression", '$', '(', ')', stream) 105 | {} 106 | 107 | StatementNode::StatementNode(StreamReader& stream) 108 | :impl_("Statement", '$', '{', '}', stream) 109 | {} 110 | 111 | std::string StatementNode::renderingCode() const 112 | { 113 | return impl_.content(); 114 | } 115 | 116 | GlobalStatementNode::GlobalStatementNode(StreamReader& stream) 117 | : impl_("Global statement", '#', '{', '}', stream) 118 | {} 119 | 120 | std::string GlobalStatementNode::renderingCode() const 121 | { 122 | return impl_.content(); 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /src/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | #include "streamreader.h" 3 | #include "streamreaderposition.h" 4 | #include "errors.h" 5 | #include "textnode.h" 6 | #include "tagnode.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | 13 | namespace htcpp::utils{ 14 | 15 | //https://developer.mozilla.org/en-US/docs/Glossary/Empty_element 16 | bool isTagEmptyElement(const std::string& tagName) 17 | { 18 | if (tagName == "area") 19 | return true; 20 | if (tagName == "base") 21 | return true; 22 | if (tagName == "br") 23 | return true; 24 | if (tagName == "col") 25 | return true; 26 | if (tagName == "embed") 27 | return true; 28 | if (tagName == "hr") 29 | return true; 30 | if (tagName == "img") 31 | return true; 32 | if (tagName == "input") 33 | return true; 34 | if (tagName == "keygen") 35 | return true; 36 | if (tagName == "link") 37 | return true; 38 | if (tagName == "meta") 39 | return true; 40 | if (tagName == "param") 41 | return true; 42 | if (tagName == "source") 43 | return true; 44 | if (tagName == "track") 45 | return true; 46 | if (tagName == "wbr") 47 | return true; 48 | if (tagName.front() == '!') 49 | return true; 50 | 51 | return false; 52 | } 53 | 54 | bool isBlank(const std::string& str) 55 | { 56 | auto nonWhitespaceIt = std::find_if(str.begin(), str.end(), [](auto ch){return !std::isspace(ch);}); 57 | return nonWhitespaceIt == str.end(); 58 | } 59 | 60 | std::string transformRawStrings(const std::string& cppCode, const StreamReaderPosition& position) 61 | { 62 | auto result = std::string{}; 63 | auto codeStream = std::stringstream{cppCode}; 64 | auto stream = StreamReader{codeStream, position}; 65 | auto insideString = false; 66 | auto rawStringPos = StreamReaderPosition{}; 67 | while (!stream.atEnd()){ 68 | auto res = stream.read(); 69 | if (res == "`"){ 70 | if (!insideString) { 71 | rawStringPos = stream.position(); 72 | result += "R\"_htcpp_str_("; 73 | } 74 | else 75 | result += ")_htcpp_str_\""; 76 | insideString = !insideString; 77 | } 78 | else 79 | result += res; 80 | } 81 | if (insideString) 82 | throw TemplateError{rawStringPos, "String is unclosed"}; 83 | 84 | return result; 85 | } 86 | 87 | namespace{ 88 | void trimFrontNewLine(std::string& str) 89 | { 90 | if (str.find('\n') == 0 || str.find("\r\n") == 0) 91 | str.erase(str.begin(), std::find_if(str.begin(), str.end(), [](int ch) { 92 | return !std::isspace(ch); 93 | })); 94 | } 95 | 96 | void trimLastBlankLine(std::string& str) 97 | { 98 | auto trimFrom = 99 | [&str](auto newLinePos) 100 | { 101 | if (newLinePos == std::string::npos) 102 | return; 103 | if (newLinePos > 0 && std::isspace(str[newLinePos - 1])) 104 | return; 105 | const auto isLastLineBlank = std::find_if(str.begin() + static_cast(newLinePos), str.end(), 106 | [](auto ch){return !std::isspace(ch);}) == str.end(); 107 | if (isLastLineBlank) 108 | str.resize(newLinePos); 109 | }; 110 | trimFrom(str.rfind('\n')); 111 | trimFrom(str.rfind("\r\n")); 112 | } 113 | 114 | } 115 | 116 | void trimBlankLines(std::string& str) 117 | { 118 | trimFrontNewLine(str); 119 | trimLastBlankLine(str); 120 | } 121 | 122 | void consumeReadAttributesText(std::string& readText, std::vector>& nodes) 123 | { 124 | if (readText.empty()) 125 | return; 126 | 127 | utils::trimBlankLines(readText); 128 | if (!readText.empty()) 129 | nodes.emplace_back(std::make_unique(readText)); 130 | readText.clear(); 131 | } 132 | 133 | void consumeReadText(std::string& readText, std::vector>& nodes, IDocumentNode* newNode) 134 | { 135 | if (readText.empty()) 136 | return; 137 | 138 | if (nodes.empty() || nodes.back()->hasType() || (newNode && newNode->hasType())){ 139 | nodes.emplace_back(std::make_unique(readText)); 140 | readText.clear(); 141 | return; 142 | } 143 | 144 | utils::trimBlankLines(readText); 145 | if (!readText.empty()) 146 | nodes.emplace_back(std::make_unique(readText)); 147 | readText.clear(); 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /tests/test_sectionnode.cpp: -------------------------------------------------------------------------------- 1 | #include "assert_exception.h" 2 | #include "node_utils.h" 3 | #include "idocumentnoderenderer.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace{ 10 | 11 | void test(const std::string& input, const std::string& expected) 12 | { 13 | auto stream = std::istringstream{input}; 14 | auto streamReader = htcpp::StreamReader{stream}; 15 | auto sectionNode = htcpp::SectionNode{streamReader}; 16 | auto nodes = htcpp::optimizeNodes(sectionNode.flatten()); 17 | auto result = std::string{}; 18 | for (auto& node : nodes) 19 | result += node->interface()->renderingCode(); 20 | EXPECT_EQ(result, expected); 21 | } 22 | 23 | void testError(const std::string& input, const std::string& expectedErrorMsg) 24 | { 25 | assert_exception( 26 | [input]{ 27 | auto stream = std::istringstream{input}; 28 | auto streamReader = htcpp::StreamReader{stream}; 29 | auto node = htcpp::SectionNode{streamReader}; 30 | }, 31 | [expectedErrorMsg](const htcpp::TemplateError& e){ 32 | EXPECT_EQ(e.what(), expectedErrorMsg); 33 | }); 34 | } 35 | 36 | } 37 | 38 | TEST(SectionNode, Basic) 39 | { 40 | test("[[ Hello world! ]]", 41 | "out << R\"_htcpp_str_( Hello world! )_htcpp_str_\";"); 42 | } 43 | 44 | TEST(SectionNode, BasicWithConditionalExtension) 45 | { 46 | test("[[?(isVisible) Hello world! ]]", 47 | "if (isVisible){ out << R\"_htcpp_str_( Hello world! )_htcpp_str_\"; } "); 48 | } 49 | 50 | TEST(SectionNode, BasicWithLoopExtension) 51 | { 52 | test("[[@(auto i = 0; i < 5; ++i) Hello world! ]]", 53 | "for (auto i = 0; i < 5; ++i){ out << R\"_htcpp_str_( Hello world! )_htcpp_str_\"; } "); 54 | } 55 | 56 | TEST(SectionNode, BasicWithConditionalExtensionOnClosingBraces) 57 | { 58 | test("[[ Hello world! ]]?(isVisible)", 59 | "if (isVisible){ out << R\"_htcpp_str_( Hello world! )_htcpp_str_\"; } "); 60 | } 61 | 62 | TEST(SectionNode, BasicWithLoopExtensionOnClosingBraces) 63 | { 64 | test("[[ Hello world! ]]@(auto i = 0; i < 5; ++i)", 65 | "for (auto i = 0; i < 5; ++i){ out << R\"_htcpp_str_( Hello world! )_htcpp_str_\"; } "); 66 | } 67 | 68 | 69 | TEST(SectionNode, Nested) 70 | { 71 | test("[[ Hello

    world

    [[!]] ]]", 72 | "out << R\"_htcpp_str_( Hello

    world

    ! )_htcpp_str_\";"); 73 | } 74 | 75 | TEST(SectionNode, NestedWithConditionalExtension) 76 | { 77 | test("[[ Hello

    ?(isVisible)world

    [[!]]?(isVisible) ]]?(isVisible)", 78 | "if (isVisible){ out << R\"_htcpp_str_( Hello )_htcpp_str_\";if (isVisible){ out << R\"_htcpp_str_(

    world

    )_htcpp_str_\"; } out << R\"_htcpp_str_( )_htcpp_str_\";if " 79 | "(isVisible){ out << R\"_htcpp_str_(!)_htcpp_str_\"; } out << R\"_htcpp_str_( )_htcpp_str_\"; } "); 80 | } 81 | 82 | TEST(SectionNode, NestedWithLoopExtension) 83 | { 84 | test("[[ Hello

    @(auto i = 0; i < 5; ++i)world

    [[!]]@(auto i = 0; i < 3; ++i) ]]@(auto i = 0; i < 5; ++i)", 85 | "for (auto i = 0; i < 5; ++i){ out << R\"_htcpp_str_( Hello )_htcpp_str_\";for (auto i = 0; i < 5; ++i){ out << " 86 | "R\"_htcpp_str_(

    world

    )_htcpp_str_\"; } out << R\"_htcpp_str_( )_htcpp_str_\";for (auto i = 0; i < 3; ++i){ out << R\"_htcpp_str_(!)_htcpp_str_\"; } out << R\"_htcpp_str_( )_htcpp_str_\"; } "); 87 | } 88 | 89 | 90 | TEST(InvalidSectionNode, Unclosed) 91 | { 92 | testError("[[ Hello world! ", "[line:1, column:1] Section isn't closed with ']]'"); 93 | } 94 | 95 | TEST(InvalidSectionNode, Empty) 96 | { 97 | testError("[[]]", "[line:1, column:1] Section can't be empty"); 98 | } 99 | 100 | TEST(InvalidSectionNode, MultipleExtensionsTwoConditionals) 101 | { 102 | testError("[[?(isVisible) Hello world! ]]?(isVisible)", 103 | "[line:1, column:31] Section can't have multiple extensions"); 104 | } 105 | 106 | TEST(InvalidSectionNode, MultipleExtensionsLoopAndConditional) 107 | { 108 | testError("[[@(auto i; i < 5; ++i) Hello world! ]]?(isVisible)", 109 | "[line:1, column:40] Section can't have multiple extensions"); 110 | } 111 | 112 | TEST(InvalidSectionNode, MultipleExtensionsConditionalAndLoop) 113 | { 114 | testError("[[?(isVisible) Hello world! ]]@(auto i; i < 5; ++i)", 115 | "[line:1, column:31] Section can't have multiple extensions"); 116 | } 117 | 118 | TEST(InvalidSectionNode, MultipleExtensionsTwoLoops) 119 | { 120 | testError("[[@(auto i; i < 5; ++i) Hello world! ]]@(auto i; i < 5; ++i)", 121 | "[line:1, column:40] Section can't have multiple extensions"); 122 | } 123 | -------------------------------------------------------------------------------- /src/shared_lib_transpiler_renderer.cpp: -------------------------------------------------------------------------------- 1 | #include "shared_lib_transpiler_renderer.h" 2 | #include "codenode.h" 3 | #include "idocumentnoderenderer.h" 4 | #include "procedurenode.h" 5 | 6 | namespace htcpp { 7 | std::unordered_map SharedLibTranspilerRenderer::generateCode( 8 | const std::vector>& globalStatements, 9 | const std::vector>& procedures, 10 | const std::vector>& nodes) const 11 | { 12 | auto result = std::string{R"( 13 | #include 14 | #include 15 | #include 16 | 17 | #ifdef _WIN32 18 | #ifdef HYPERTEXTCPP_EXPORT 19 | #define HYPERTEXTCPP_API __declspec(dllexport) 20 | #else 21 | #define HYPERTEXTCPP_API __declspec(dllimport) 22 | #endif 23 | #else 24 | #define HYPERTEXTCPP_API 25 | #endif 26 | 27 | namespace htcpp{ 28 | template 29 | class ITemplate{ 30 | public: 31 | virtual ~ITemplate() = default; 32 | virtual std::string render(const TCfg&) const = 0; 33 | virtual std::string render(const std::string& renderFuncName, const TCfg&) const = 0; 34 | virtual void print(const TCfg&) const = 0; 35 | virtual void print(const std::string& renderFuncName, const TCfg&) const = 0; 36 | virtual void print(const TCfg& cfg, std::ostream& stream) const = 0; 37 | virtual void print(const std::string& renderFuncName, const TCfg& cfg, std::ostream& stream) const = 0; 38 | }; 39 | } 40 | #define HTCPP_CONFIG(TCfg) using Cfg = TCfg;\ 41 | namespace {\ 42 | struct Template : public htcpp::ITemplate{\ 43 | class Renderer{\ 44 | const Cfg& cfg;\ 45 | std::ostream& out;\ 46 | public:\ 47 | Renderer(const Cfg& cfg, std::ostream& out)\ 48 | : cfg(cfg), out(out)\ 49 | {}\)"}; 50 | result += "\n"; 51 | for (const auto& procedure : procedures) { 52 | result += " std::string " + procedure->name() + "() const;\\\n"; 53 | } 54 | result += R"(\ 55 | void renderHTML() const;\ 56 | void renderHTMLPart(const std::string& name) const;\ 57 | };\ 58 | \ 59 | std::string render(const Cfg& cfg) const override\ 60 | {\ 61 | auto stream = std::stringstream{};\ 62 | auto renderer = Renderer{cfg, stream};\ 63 | renderer.renderHTML();\ 64 | return stream.str();\ 65 | }\ 66 | std::string render(const std::string& renderFuncName, const Cfg& cfg) const override\ 67 | {\ 68 | auto stream = std::stringstream{};\ 69 | auto renderer = Renderer{cfg, stream};\ 70 | renderer.renderHTMLPart(renderFuncName);\ 71 | return stream.str();\ 72 | }\ 73 | \ 74 | void print(const Cfg& cfg) const override\ 75 | {\ 76 | auto renderer = Renderer{cfg, std::cout};\ 77 | renderer.renderHTML();\ 78 | }\ 79 | void print(const std::string& renderFuncName, const Cfg& cfg) const override\ 80 | {\ 81 | auto renderer = Renderer{cfg, std::cout};\ 82 | renderer.renderHTMLPart(renderFuncName);\ 83 | }\ 84 | \ 85 | void print(const Cfg& cfg, std::ostream& stream) const override\ 86 | {\ 87 | auto renderer = Renderer{cfg, stream};\ 88 | renderer.renderHTML();\ 89 | }\ 90 | void print(const std::string& renderFuncName, const Cfg& cfg, std::ostream& stream) const override\ 91 | {\ 92 | auto renderer = Renderer{cfg, stream};\ 93 | renderer.renderHTMLPart(renderFuncName);\ 94 | }\ 95 | };\ 96 | }\ 97 | \ 98 | extern "C" \ 99 | HYPERTEXTCPP_API htcpp::ITemplate* makeTemplate()\ 100 | {\ 101 | return new Template;\ 102 | }\ 103 | \ 104 | extern "C" \ 105 | HYPERTEXTCPP_API void deleteTemplate(htcpp::ITemplate* ptr)\ 106 | {\ 107 | delete ptr;\ 108 | }\ 109 | 110 | )"; 111 | for (const auto& globalStatement : globalStatements) 112 | result += globalStatement->renderingCode() + "\n"; 113 | 114 | result += "namespace {\n"; 115 | for (const auto& procedure : procedures) { 116 | result += "std::string Template::Renderer::" + procedure->name() + "() const{\n"; 117 | result += procedure->renderingCode() + "\n return {};}\n"; 118 | } 119 | result += "void Template::Renderer::renderHTML() const\n{\n"; 120 | 121 | for (auto& node : nodes) 122 | result += node->renderingCode(); 123 | result += "\n}\n"; 124 | 125 | result += "void Template::Renderer::renderHTMLPart(const std::string& name) const\n" 126 | "{\n"; 127 | result += "static_cast(name);\n"; 128 | for (const auto& procedure : procedures) { 129 | result += "if (name == \"" + procedure->name() + "\")\n"; 130 | result += " " + procedure->name() + "();\n"; 131 | } 132 | result += "\n}\n"; 133 | result += "}"; 134 | 135 | return {{GeneratedFileType::Source, result}}; 136 | } 137 | 138 | } //namespace htcpp -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | name: build & test (clang, gcc, MSVC) 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ "master", "dev"] 7 | paths-ignore: 8 | - '**.md' 9 | pull_request: 10 | branches: [ "master", "dev"] 11 | paths-ignore: 12 | - '**.md' 13 | 14 | jobs: 15 | build: 16 | name: ${{ matrix.config.name }} 17 | runs-on: ${{ matrix.config.os }} 18 | 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | config: 23 | - { 24 | name: "Ubuntu Latest gcc", 25 | os: ubuntu-latest, 26 | cmake-preset: gcc-release, 27 | artifacts-path: "" 28 | } 29 | - { 30 | name: "Ubuntu Latest clang", 31 | os: ubuntu-latest, 32 | cmake-preset: clang-release, 33 | artifacts-path: "" 34 | } 35 | - { 36 | name: "Windows Latest MSVC", 37 | os: windows-latest, 38 | cmake-preset: msvc-release, 39 | artifacts-path: "/Release" 40 | } 41 | 42 | steps: 43 | - name: Install ninja (Windows) 44 | if: matrix.config.os == 'windows-latest' 45 | run: choco install ninja 46 | - name: Install ninja (Linux) 47 | if: matrix.config.os == 'ubuntu-latest' 48 | run: sudo apt install ninja-build 49 | - uses: actions/checkout@v4 50 | 51 | - uses: rui314/setup-mold@v1 52 | - uses: hendrikmuhs/ccache-action@v1.2 53 | - uses: ilammy/msvc-dev-cmd@v1 54 | 55 | - name: Configure CMake 56 | run: cmake --preset=${{ matrix.config.cmake-preset }} -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=Release -DENABLE_TESTS=ON -DENABLE_EXAMPLES=ON 57 | 58 | - name: Build 59 | run: cmake --build ${{github.workspace}}/build --config Release 60 | 61 | - name: Test 62 | working-directory: ${{github.workspace}}/build 63 | run: ctest 64 | 65 | - name: Prepare examples artifacts 66 | shell: bash 67 | run: | 68 | mkdir examples-build 69 | find build/examples -type f -name "ex_*" -exec cp {} examples-build \; 70 | find build/examples -type f -name "*.so" -exec cp {} examples-build \; 71 | find build/examples -type f -name "*.dll" -exec cp {} examples-build \; 72 | 73 | - name: Upload examples build artifact 74 | if: matrix.config.cmake-preset != 'gcc-release' 75 | uses: actions/upload-artifact@v4 76 | with: 77 | name: hypertextcpp-examples-${{ matrix.config.os }} 78 | path: | 79 | ${{github.workspace}}/examples-build 80 | 81 | functional_tests: 82 | name: Functional testing (${{ matrix.config.name }}) 83 | needs: build 84 | runs-on: ${{ matrix.config.os }} 85 | strategy: 86 | fail-fast: false 87 | matrix: 88 | config: 89 | - { 90 | name: "Windows", 91 | os: "windows-latest", 92 | lunchtoast_exec: "lunchtoast.exe", 93 | shell_command: -shell="msys2 -c", 94 | } 95 | - { 96 | name: "Linux", 97 | os: "ubuntu-latest", 98 | lunchtoast_exec: "lunchtoast", 99 | shell_command: "", 100 | } 101 | steps: 102 | - uses: actions/checkout@v4 103 | - if: matrix.config.name == 'Windows' 104 | uses: msys2/setup-msys2@v2 105 | with: 106 | path-type: inherit 107 | - uses: robinraju/release-downloader@v1.11 108 | with: 109 | repository: "kamchatka-volcano/lunchtoast" 110 | latest: true 111 | filename: ${{ matrix.config.lunchtoast_exec }} 112 | out-file-path: "lunchtoast/" 113 | - name: Set lunchtoast execute permissions 114 | if: matrix.config.name == 'Linux' 115 | shell: sh 116 | working-directory: ${{github.workspace}}/lunchtoast 117 | run: chmod +x lunchtoast 118 | - name: Download hypertextcpp examples build 119 | id: pre_launch_tests 120 | uses: actions/download-artifact@v4 121 | with: 122 | name: hypertextcpp-examples-${{ matrix.config.os }} 123 | path: examples-build 124 | - name: Set artifacts execute permissions 125 | if: matrix.config.name == 'Linux' 126 | shell: sh 127 | working-directory: ${{github.workspace}}/examples-build 128 | run: chmod +x ex* 129 | - name: Prepare tests 130 | shell: bash 131 | run: | 132 | cp examples-build/*.so functional_tests/example_5/ || true 133 | cp examples-build/*.dll functional_tests/example_5/ || true 134 | ls functional_tests/example_5/ 135 | - name: Launch tests 136 | id: launch_tests 137 | working-directory: ${{github.workspace}} 138 | run: lunchtoast/lunchtoast functional_tests ${{ matrix.config.shell_command }} -collectFailedTests=failed_tests -config="ci.cfg" 139 | - name: Upload failed tests 140 | if: failure() && steps.launch_tests.outcome != 'success' && steps.pre_launch_tests.outcome == 'success' 141 | uses: actions/upload-artifact@v4 142 | with: 143 | name: hypertextcpp-failed-tests-${{ matrix.config.os }} 144 | path: failed_tests 145 | -------------------------------------------------------------------------------- /src/tagnode.cpp: -------------------------------------------------------------------------------- 1 | #include "tagnode.h" 2 | #include "control_flow_statement_node.h" 3 | #include "errors.h" 4 | #include "nodereader.h" 5 | #include "streamreader.h" 6 | #include "textnode.h" 7 | #include "utils.h" 8 | #include 9 | #include 10 | 11 | namespace htcpp { 12 | 13 | TagNode::TagNode(StreamReader& stream) 14 | { 15 | load(stream); 16 | } 17 | 18 | void TagNode::load(StreamReader& stream) 19 | { 20 | const auto nodePos = stream.position(); 21 | auto openSeq = stream.read(); 22 | Expects(openSeq == "<"); 23 | 24 | while (!stream.atEnd()) { 25 | if (name_.empty()) { 26 | if (readName(stream, nodePos) == ReadResult::ParsingCompleted) 27 | return; 28 | continue; 29 | } 30 | if (!attributesRead_) { 31 | if (readAttributes(stream) == ReadResult::ParsingCompleted) 32 | return; 33 | continue; 34 | } 35 | const auto closingTag = ""; 36 | const auto closingTagSize = static_cast(closingTag.size()); 37 | if (stream.peek(closingTagSize) == closingTag) { 38 | utils::consumeReadText(readText_, contentNodes_); 39 | stream.skip(closingTagSize); 40 | const auto extensionPos = stream.position(); 41 | const auto closingTagExtension = readNodeExtension(stream); 42 | if (closingTagExtension.has_value()) { 43 | if (extension_.has_value()) 44 | throw TemplateError{extensionPos, "Tag can't have multiple extensions"}; 45 | extension_ = closingTagExtension; 46 | } 47 | return; 48 | } 49 | auto node = readTagContentNode(stream); 50 | if (node) { 51 | utils::consumeReadText(readText_, contentNodes_, node.get()); 52 | contentNodes_.emplace_back(std::move(node)); 53 | } 54 | else 55 | readText_ += stream.read(); 56 | } 57 | if (name_.empty()) 58 | throw TemplateError{nodePos, "Tag's name can't be empty"}; 59 | if (attributesRead_) 60 | throw TemplateError{nodePos, "Tag isn't closed with "}; 61 | else 62 | throw TemplateError{nodePos, "Tag isn't closed with '>'"}; 63 | } 64 | 65 | TagNode::ReadResult TagNode::readName(StreamReader& stream, const StreamReaderPosition& nodePos) 66 | { 67 | if (std::isspace(stream.peek().front())) { 68 | name_ = readText_; 69 | if (utils::isBlank(name_)) 70 | throw TemplateError{nodePos, "Tag's name can't be empty"}; 71 | readText_.clear(); 72 | } 73 | else if (stream.peek() == ">") { 74 | name_ = readText_; 75 | if (utils::isBlank(name_)) 76 | throw TemplateError{nodePos, "Tag's name can't be empty"}; 77 | 78 | readText_.clear(); 79 | stream.skip(1); 80 | extension_ = readNodeExtension(stream); 81 | attributesRead_ = true; 82 | if (utils::isTagEmptyElement(name_)) 83 | return ReadResult::ParsingCompleted; 84 | } 85 | else 86 | readText_ += stream.read(); 87 | 88 | return ReadResult::Ok; 89 | } 90 | 91 | TagNode::ReadResult TagNode::readAttributes(StreamReader& stream) 92 | { 93 | if (stream.peek() == ">") { 94 | attributesRead_ = true; 95 | utils::consumeReadAttributesText(readText_, attributeNodes_); 96 | stream.skip(1); 97 | extension_ = readNodeExtension(stream); 98 | if (utils::isTagEmptyElement(name_)) 99 | return ReadResult::ParsingCompleted; 100 | return ReadResult::Ok; 101 | } 102 | 103 | auto node = readTagAttributeNode(stream); 104 | if (node) { 105 | utils::consumeReadAttributesText(readText_, attributeNodes_); 106 | attributeNodes_.emplace_back(std::move(node)); 107 | } 108 | else 109 | readText_ += stream.read(); 110 | 111 | return ReadResult::Ok; 112 | } 113 | 114 | std::vector> TagNode::flatten() 115 | { 116 | auto result = std::vector>{}; 117 | if (extension_.has_value()) 118 | result.emplace_back( 119 | std::make_unique(ControlFlowStatementNodeType::Open, extension_.value())); 120 | result.emplace_back(std::make_unique("<" + name_)); 121 | for (auto& node : attributeNodes_){ 122 | if (auto nodeCollection = node->interface()) 123 | std::ranges::move(nodeCollection->flatten(), std::back_inserter(result)); 124 | else 125 | result.emplace_back(std::move(node)); 126 | } 127 | result.emplace_back(std::make_unique(">")); 128 | for (auto& node : contentNodes_){ 129 | if (auto nodeCollection = node->interface()) 130 | std::ranges::move(nodeCollection->flatten(), std::back_inserter(result)); 131 | else 132 | result.emplace_back(std::move(node)); 133 | } 134 | if (!utils::isTagEmptyElement(name_)) 135 | result.emplace_back(std::make_unique("")); 136 | 137 | if (extension_.has_value()) 138 | result.emplace_back( 139 | std::make_unique(ControlFlowStatementNodeType::Close, extension_.value())); 140 | 141 | return result; 142 | } 143 | 144 | } // namespace htcpp 145 | -------------------------------------------------------------------------------- /tests/test_tagnode.cpp: -------------------------------------------------------------------------------- 1 | #include "assert_exception.h" 2 | #include "idocumentnoderenderer.h" 3 | #include "node_utils.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace{ 11 | 12 | void test(const std::string& input, const std::string& expected) 13 | { 14 | auto stream = std::istringstream{input}; 15 | auto streamReader = htcpp::StreamReader{stream}; 16 | auto funcMap = std::map{}; 17 | auto tagNode = htcpp::TagNode{streamReader}; 18 | auto nodes = htcpp::optimizeNodes(tagNode.flatten()); 19 | auto result = std::string{}; 20 | for (auto& node : nodes) 21 | result += node->interface()->renderingCode(); 22 | EXPECT_EQ(result, expected); 23 | } 24 | 25 | void testError(const std::string& input, const std::string& expectedErrorMsg) 26 | { 27 | assert_exception( 28 | [input]{ 29 | auto stream = std::istringstream{input}; 30 | auto streamReader = htcpp::StreamReader{stream}; 31 | auto funcMap = std::map{}; 32 | auto token = htcpp::TagNode{streamReader}; 33 | }, 34 | [expectedErrorMsg](const htcpp::TemplateError& e){ 35 | EXPECT_EQ(e.what(), expectedErrorMsg); 36 | }); 37 | } 38 | 39 | } 40 | 41 | TEST(TagNode, BasicNoAttributes) 42 | { 43 | test("

    Hello world!

    ", 44 | "out << R\"_htcpp_str_(

    Hello world!

    )_htcpp_str_\";"); 45 | } 46 | 47 | TEST(TagNode, Basic) 48 | { 49 | test("
    Hello world!
    ", 50 | "out << R\"_htcpp_str_(
    Hello world!
    )_htcpp_str_\";"); 51 | } 52 | 53 | TEST(TagNode, BasicWithConditionalExtension) 54 | { 55 | test("
    ?(isVisible) Hello world!
    ", 56 | "if (isVisible){ out << R\"_htcpp_str_(
    Hello world!
    )_htcpp_str_\"; } "); 57 | } 58 | 59 | TEST(TagNode, BasicWithConditionalExtensionOnClosingTag) 60 | { 61 | test("
    Hello world!
    ?(isVisible)", 62 | "if (isVisible){ out << R\"_htcpp_str_(
    Hello world!
    )_htcpp_str_\"; } "); 63 | } 64 | 65 | TEST(TagNode, BasicWithLoopExtension) 66 | { 67 | test("
    @(auto i = 0; i < 5; ++i) Hello world!
    ", 68 | "for (auto i = 0; i < 5; ++i){ out << R\"_htcpp_str_(
    Hello world!
    )_htcpp_str_\"; } "); 69 | } 70 | 71 | TEST(TagNode, BasicWithLoopExtensionOnClosingTag) 72 | { 73 | test("
    Hello world!
    @(auto i = 0; i < 5; ++i)", 74 | "for (auto i = 0; i < 5; ++i){ out << R\"_htcpp_str_(
    Hello world!
    )_htcpp_str_\"; } "); 75 | } 76 | 77 | TEST(TagNode, WithOtherNodes) 78 | { 79 | test("", 80 | "out << R\"_htcpp_str_()_htcpp_str_\";"); 82 | } 83 | 84 | 85 | TEST(TagNode, NestedWithAttributes) 86 | { 87 | test("

    Hello world!

    ", 88 | "out << R\"_htcpp_str_(

    Hello world!

    )_htcpp_str_\";"); 89 | } 90 | 91 | TEST(TagNode, NestedWithAttributesWithConditionalExtension) 92 | { 93 | test("
    ?(isVisible)

    ?(isVisible)Hello world!

    ", 94 | "if (isVisible){ out << R\"_htcpp_str_(
    )_htcpp_str_\";if (isVisible){ out << R\"_htcpp_str_(

    Hello world!

    )_htcpp_str_\"; } out << " 95 | "R\"_htcpp_str_(
    )_htcpp_str_\"; } "); 96 | } 97 | 98 | TEST(TagNode, NestedWithAttributesWithLoopExtension) 99 | { 100 | test("
    @(auto i = 0; i < 5; ++i)

    @(auto i = 0; i < 5; ++i)Hello world!

    ", 101 | "for (auto i = 0; i < 5; ++i){ out << R\"_htcpp_str_(
    )_htcpp_str_\";for (auto i = 0; i < 5; ++i){ out << " 102 | "R\"_htcpp_str_(

    Hello world!

    )_htcpp_str_\"; } out << R\"_htcpp_str_(
    )_htcpp_str_\"; } "); 103 | } 104 | 105 | TEST(TagNode, EmptyElementNoAttributes) 106 | { 107 | test("
    ", 108 | "out << R\"_htcpp_str_(
    )_htcpp_str_\";"); 109 | } 110 | 111 | TEST(TagNode, EmptyElement) 112 | { 113 | test("", 114 | "out << R\"_htcpp_str_()_htcpp_str_\";"); 115 | } 116 | 117 | TEST(TagNode, EmptyElementWithConditionalExtension) 118 | { 119 | test("?(isVisible)", 120 | "if (isVisible){ out << R\"_htcpp_str_()_htcpp_str_\"; } "); 121 | } 122 | 123 | TEST(TagNode, EmptyElementWithLoopExtension) 124 | { 125 | test("@(auto i = 0; i < 5; ++i)", 126 | "for (auto i = 0; i < 5; ++i){ out << R\"_htcpp_str_()_htcpp_str_\"; } "); 127 | } 128 | 129 | TEST(InvalidTagNode, UnclosedTag) 130 | { 131 | testError("

    Hello world!", 132 | "[line:1, column:1] Tag isn't closed with

    "); 133 | } 134 | 135 | TEST(InvalidTagNode, UnclosedOpeningTag) 136 | { 137 | testError("

    '"); 139 | } 140 | 141 | TEST(InvalidTagNode, EmptyName) 142 | { 143 | testError("< >Hello world!", "[line:1, column:1] Tag's name can't be empty"); 144 | } 145 | 146 | TEST(InvalidTagNode, MultipleExtensionsTwoConditionals) 147 | { 148 | testError("

    ?(isVisible) Hello world!

    ?(isVisible)", 149 | "[line:1, column:34] Tag can't have multiple extensions"); 150 | } 151 | 152 | TEST(InvalidTagNode, MultipleExtensionsLoopAndConditional) 153 | { 154 | testError("

    @(auto i; i < 5; ++i) Hello world!

    ?(isVisible)", 155 | "[line:1, column:43] Tag can't have multiple extensions"); 156 | } 157 | 158 | TEST(InvalidTagNode, MultipleExtensionsConditionalAndLoop) 159 | { 160 | testError("

    ?(isVisible) Hello world!

    @(auto i; i < 5; ++i)", 161 | "[line:1, column:34] Tag can't have multiple extensions"); 162 | } 163 | 164 | TEST(InvalidTagNode, MultipleExtensionsTwoLoops) 165 | { 166 | testError("

    @(auto i; i < 5; ++i) Hello world!

    @(auto i; i < 5; ++i)", 167 | "[line:1, column:43] Tag can't have multiple extensions"); 168 | } 169 | -------------------------------------------------------------------------------- /src/header_and_source_transpiler_renderer.cpp: -------------------------------------------------------------------------------- 1 | #include "header_and_source_transpiler_renderer.h" 2 | #include "idocumentnoderenderer.h" 3 | #include "procedurenode.h" 4 | #include 5 | 6 | namespace htcpp { 7 | 8 | HeaderAndSourceTranspilerRenderer::HeaderAndSourceTranspilerRenderer( 9 | std::string className, 10 | std::string headerFileName, 11 | std::string configTypeName) 12 | : className_(std::move(className)) 13 | , headerFileName_(std::move(headerFileName)) 14 | , configTypeName_(std::move(configTypeName)) 15 | { 16 | } 17 | 18 | namespace { 19 | 20 | std::string generateHeader( 21 | const std::string& className, 22 | const std::string& configTypeName, 23 | const std::vector>& globalStatements, 24 | const std::vector>& procedures) 25 | { 26 | auto globalStatementsCode = std::string{}; 27 | for (const auto& globalStatement : globalStatements) 28 | globalStatementsCode += globalStatement->renderingCode() + "\n"; 29 | 30 | auto proceduresDeclarations = std::string{}; 31 | for (const auto& procedure : procedures) 32 | proceduresDeclarations += " std::string " + procedure->name() + "() const;\n"; 33 | 34 | return fmt::format( 35 | R"( 36 | #pragma once 37 | #include 38 | #include 39 | #include 40 | 41 | {globalStatementsCode} 42 | 43 | class {className} {{ 44 | private: 45 | class Renderer{{ 46 | const {configTypeName}& cfg; 47 | std::ostream& out; 48 | {proceduresDeclarations} 49 | 50 | public: 51 | Renderer(const {configTypeName}& cfg, std::ostream& out); 52 | void renderHTML() const; 53 | void renderHTMLPart(const std::string& name) const; 54 | }}; 55 | public: 56 | std::string render(const {configTypeName}& cfg) const; 57 | std::string render(const std::string& renderFuncName, const {configTypeName}& cfg) const; 58 | void print(const {configTypeName}& cfg) const; 59 | void print(const std::string& renderFuncName, const {configTypeName}& cfg) const; 60 | void print(const {configTypeName}& cfg, std::ostream& stream) const; 61 | void print(const std::string& renderFuncName, const {configTypeName}& cfg, std::ostream& stream) const; 62 | }};)", 63 | fmt::arg("className", className), 64 | fmt::arg("configTypeName", configTypeName), 65 | fmt::arg("globalStatementsCode", globalStatementsCode), 66 | fmt::arg("proceduresDeclarations", proceduresDeclarations)); 67 | } 68 | 69 | std::string generateSource( 70 | const std::string& headerFileName, 71 | const std::string& className, 72 | const std::string& configTypeName, 73 | const std::vector>& procedures, 74 | const std::vector>& nodes) 75 | { 76 | auto proceduresImplementations = std::string{}; 77 | for (const auto& procedure : procedures) { 78 | proceduresImplementations += "std::string " + className + "::Renderer::" + procedure->name() + "() const{\n"; 79 | proceduresImplementations += procedure->renderingCode() + "\n return {};\n}\n"; 80 | } 81 | 82 | auto templateHtmlRenderingCode = std::string{}; 83 | for (const auto& node : nodes) 84 | templateHtmlRenderingCode += node->renderingCode(); 85 | 86 | auto procedureInvocations = std::string{}; 87 | for (const auto& procedure : procedures) { 88 | procedureInvocations += "if (name == \"" + procedure->name() + "\")\n"; 89 | procedureInvocations += " " + procedure->name() + "();\n"; 90 | } 91 | 92 | return fmt::format( 93 | R"( 94 | #include "{headerFileName}" 95 | 96 | {procedureImplementations} 97 | 98 | {className}::Renderer::Renderer(const {configTypeName}& cfg, std::ostream& out) 99 | : cfg(cfg), out(out) 100 | {{}} 101 | 102 | void {className}::Renderer::renderHTML() const 103 | {{ 104 | {templateHtmlRenderingCode} 105 | }} 106 | 107 | void {className}::Renderer::renderHTMLPart(const std::string& name) const 108 | {{ 109 | static_cast(name); 110 | {procedureInvocations} 111 | }} 112 | 113 | std::string {className}::render(const {configTypeName}& cfg) const 114 | {{ 115 | auto stream = std::stringstream{{}}; 116 | auto renderer = Renderer{{cfg, stream}}; 117 | renderer.renderHTML(); 118 | return stream.str(); 119 | }} 120 | 121 | std::string {className}::render(const std::string& renderFuncName, const {configTypeName}& cfg) const 122 | {{ 123 | auto stream = std::stringstream{{}}; 124 | auto renderer = Renderer{{cfg, stream}}; 125 | renderer.renderHTMLPart(renderFuncName); 126 | return stream.str(); 127 | }} 128 | 129 | void {className}::print(const {configTypeName}& cfg) const 130 | {{ 131 | auto renderer = Renderer{{cfg, std::cout}}; 132 | renderer.renderHTML(); 133 | }} 134 | 135 | void {className}::print(const std::string& renderFuncName, const {configTypeName}& cfg) const 136 | {{ 137 | auto renderer = Renderer{{cfg, std::cout}}; 138 | renderer.renderHTMLPart(renderFuncName); 139 | }} 140 | 141 | void {className}::print(const {configTypeName}& cfg, std::ostream& stream) const 142 | {{ 143 | auto renderer = Renderer{{cfg, stream}}; 144 | renderer.renderHTML(); 145 | }} 146 | 147 | void {className}::print(const std::string& renderFuncName, const {configTypeName}& cfg, std::ostream& stream) const 148 | {{ 149 | auto renderer = Renderer{{cfg, stream}}; 150 | renderer.renderHTMLPart(renderFuncName); 151 | }} 152 | )", 153 | fmt::arg("headerFileName", headerFileName), 154 | fmt::arg("procedureImplementations", proceduresImplementations), 155 | fmt::arg("className", className), 156 | fmt::arg("configTypeName", configTypeName), 157 | fmt::arg("templateHtmlRenderingCode", templateHtmlRenderingCode), 158 | fmt::arg("procedureInvocations", procedureInvocations)); 159 | } 160 | 161 | } //namespace 162 | 163 | std::unordered_map HeaderAndSourceTranspilerRenderer::generateCode( 164 | const std::vector>& globalStatements, 165 | const std::vector>& procedures, 166 | const std::vector>& nodes) const 167 | { 168 | return {{GeneratedFileType::Header, generateHeader(className_, configTypeName_, globalStatements, procedures)}, 169 | {GeneratedFileType::Source, 170 | generateSource(headerFileName_, className_, configTypeName_, procedures, nodes)}}; 171 | } 172 | 173 | } //namespace htcpp 174 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "errors.h" 2 | #include "header_and_source_transpiler_renderer.h" 3 | #include "nameutils.h" 4 | #include "shared_lib_transpiler_renderer.h" 5 | #include "single_header_transpiler_renderer.h" 6 | #include "transpiler.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace fs = std::filesystem; 18 | 19 | namespace { 20 | struct HeaderOnlyResultModeCfg : public cmdlime::Config { 21 | CMDLIME_ARG(input, fs::path) << ".htcpp file to transpile"; 22 | CMDLIME_PARAM(outputDir, fs::path)() << "output dir\n(if empty, current working directory is used)"; 23 | CMDLIME_PARAM(className, std::string)() << "generated class name\n(if empty, input file name is used)"; 24 | }; 25 | 26 | struct SharedLibrarySourceResultModeCfg : public cmdlime::Config { 27 | CMDLIME_ARG(input, fs::path) << ".htcpp file to transpile"; 28 | CMDLIME_PARAM(outputDir, fs::path)() << "output dir\n(if empty, current working directory is used)"; 29 | }; 30 | 31 | struct HeaderAndSourceResultModeCfg : public cmdlime::Config { 32 | CMDLIME_ARG(input, fs::path) << ".htcpp file to transpile"; 33 | CMDLIME_PARAM(outputDir, fs::path)() << "output dir\n(if empty, current working directory is used)"; 34 | CMDLIME_PARAM(className, std::string)() << "generated class name\n(if empty, input file name is used)"; 35 | CMDLIME_PARAM(configClassName, std::string) << "config class name"; 36 | }; 37 | 38 | struct Cfg : public cmdlime::Config { 39 | CMDLIME_COMMAND(generateHeaderOnly, HeaderOnlyResultModeCfg) << "generate header only file"; 40 | CMDLIME_COMMAND(generateSharedLibrarySource, SharedLibrarySourceResultModeCfg) << "generate shared library source file"; 41 | CMDLIME_COMMAND(generateHeaderAndSource, HeaderAndSourceResultModeCfg) << "generate header and source files"; 42 | }; 43 | 44 | using CommandCfg = 45 | std::variant; 46 | 47 | std::unique_ptr makeTranspilerRenderer(const CommandCfg& cfg); 48 | std::variant getCommand( 49 | const Cfg& cfg); 50 | 51 | std::string getClassName(const auto& commandCfg) 52 | { 53 | const auto& className = commandCfg.className; 54 | if (className.empty()) 55 | return commandCfg.input.stem().string(); 56 | 57 | return className; 58 | } 59 | 60 | fs::path getOutputFilePath(const CommandCfg& command, htcpp::GeneratedFileType fileType); 61 | 62 | } //namespace 63 | 64 | namespace cmdlime { 65 | template<> 66 | struct PostProcessor { 67 | void operator()(Cfg& cfg) 68 | { 69 | if (!cfg.generateHeaderOnly.has_value() && !cfg.generateHeaderAndSource.has_value() && 70 | !cfg.generateSharedLibrarySource.has_value()) 71 | throw cmdlime::ValidationError{"At least one command must be specified"}; 72 | } 73 | }; 74 | } //namespace cmdlime 75 | 76 | int run(const Cfg& cfg) 77 | { 78 | const auto command = getCommand(cfg); 79 | auto transpilerRenderer = makeTranspilerRenderer(command); 80 | auto transpiler = htcpp::Transpiler{*transpilerRenderer}; 81 | try { 82 | const auto input = std::visit( 83 | [](const auto& cfg) 84 | { 85 | return cfg.input; 86 | }, 87 | command); 88 | const auto result = transpiler.process(input); 89 | for (const auto& [fileType, code] : result) { 90 | auto stream = std::ofstream{getOutputFilePath(command, fileType)}; 91 | stream << code; 92 | } 93 | } 94 | catch (const htcpp::Error& e) { 95 | std::cerr << e.what() << std::endl; 96 | return 1; 97 | } 98 | catch (const std::exception& e) { 99 | std::cerr << "Unknown critical error:" << e.what() << std::endl; 100 | return 1; 101 | } 102 | return 0; 103 | } 104 | 105 | int main(int argc, char** argv) 106 | { 107 | return cmdlime::CommandLineReader{"hypertextcpp"}.exec(argc, argv, run); 108 | } 109 | 110 | namespace { 111 | 112 | fs::path getOutputFilePath(const CommandCfg& command, htcpp::GeneratedFileType fileType) 113 | { 114 | const auto inputFile = std::visit( 115 | [](const auto& cfg) 116 | { 117 | return cfg.input; 118 | }, 119 | command); 120 | const auto outputDir = std::visit( 121 | [](const auto& cfg) 122 | { 123 | return cfg.outputDir; 124 | }, 125 | command); 126 | 127 | auto path = fs::current_path(); 128 | if (!outputDir.empty()) { 129 | if (outputDir.is_absolute()) 130 | path = outputDir; 131 | else 132 | path /= outputDir; 133 | } 134 | 135 | if (fileType == htcpp::GeneratedFileType::Source) 136 | path /= (inputFile.stem().string() + ".cpp"); 137 | else if (fileType == htcpp::GeneratedFileType::Header) 138 | path /= (inputFile.stem().string() + ".h"); 139 | 140 | return path; 141 | } 142 | 143 | std::unique_ptr makeTranspilerRenderer(const CommandCfg& cfg) 144 | { 145 | return std::visit( 146 | sfun::overloaded{ 147 | [](const HeaderOnlyResultModeCfg& commandCfg) -> std::unique_ptr 148 | { 149 | return std::make_unique(getClassName(commandCfg)); 150 | }, 151 | [](const SharedLibrarySourceResultModeCfg&) -> std::unique_ptr 152 | { 153 | return std::make_unique(); 154 | }, 155 | [](const HeaderAndSourceResultModeCfg& commandCfg) -> std::unique_ptr 156 | { 157 | return std::make_unique( 158 | getClassName(commandCfg), 159 | getOutputFilePath(commandCfg, htcpp::GeneratedFileType::Header).filename().string(), 160 | commandCfg.configClassName); 161 | }}, 162 | cfg); 163 | } 164 | 165 | std::variant getCommand( 166 | const Cfg& cfg) 167 | { 168 | if (cfg.generateHeaderOnly.has_value()) 169 | return cfg.generateHeaderOnly.value(); 170 | if (cfg.generateSharedLibrarySource.has_value()) 171 | return cfg.generateSharedLibrarySource.value(); 172 | if (cfg.generateHeaderAndSource.has_value()) 173 | return cfg.generateHeaderAndSource.value(); 174 | 175 | sfun::unreachable(); 176 | } 177 | 178 | } //namespace 179 | -------------------------------------------------------------------------------- /tests/test_codenode.cpp: -------------------------------------------------------------------------------- 1 | #include "assert_exception.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace{ 8 | 9 | template 10 | void test(const std::string& input, const std::string& expected) 11 | { 12 | auto stream = std::istringstream{input}; 13 | auto streamReader = htcpp::StreamReader{stream}; 14 | auto node = TNode{streamReader}; 15 | auto result = node.renderingCode(); 16 | EXPECT_EQ(result, expected); 17 | } 18 | 19 | template 20 | void testError(const std::string& input, const std::string& expectedErrorMsg) 21 | { 22 | assert_exception( 23 | [input]{ 24 | auto stream = std::istringstream{input}; 25 | auto streamReader = htcpp::StreamReader{stream}; 26 | auto node = TNode{streamReader}; 27 | }, 28 | [expectedErrorMsg](const htcpp::TemplateError& e){ 29 | EXPECT_EQ(e.what(), expectedErrorMsg); 30 | }); 31 | } 32 | 33 | } 34 | 35 | TEST(ExpressionNode, Basic) 36 | { 37 | test 38 | ("$( isVisible ? 100 : defaultValue())", 39 | "out << ( isVisible ? 100 : defaultValue());"); 40 | } 41 | 42 | TEST(ExpressionNode, WithStringOutput) 43 | { 44 | test 45 | ("$( isVisible ? \"Hello:)_htcpp_str_\" : defaultValue())", 46 | "out << ( isVisible ? \"Hello:)_htcpp_str_\" : defaultValue());"); 47 | } 48 | 49 | TEST(ExpressionNode, WithRawStringOutput) 50 | { 51 | test 52 | ("$( isVisible ? R\"_htcpp_str_(Hello:))_htcpp_str_\" : defaultValue())", 53 | "out << ( isVisible ? R\"_htcpp_str_(Hello:))_htcpp_str_\" : defaultValue());"); 54 | } 55 | 56 | TEST(ExpressionNode, WithCustomRawStringOutput) 57 | { 58 | test 59 | ("$( isVisible ? `Hello:)` : defaultValue())", 60 | "out << ( isVisible ? R\"_htcpp_str_(Hello:))_htcpp_str_\" : defaultValue());"); 61 | } 62 | 63 | TEST(ExpressionNode, WithCharOutput) 64 | { 65 | test 66 | ("$( isVisible ? std::string{\"Hello:\"} + ')' : defaultValue())", 67 | "out << ( isVisible ? std::string{\"Hello:\"} + ')' : defaultValue());"); 68 | } 69 | 70 | TEST(ExpressionNode, WithConditionalExtension) 71 | { 72 | test 73 | ("$(?(isVisible) isVisible ? 100 : defaultValue())", 74 | "if (isVisible){ out << ( isVisible ? 100 : defaultValue()); } "); 75 | } 76 | 77 | TEST(ExpressionNode, WithLoopExtension) 78 | { 79 | test 80 | ("$(@(auto i = 0; i < 5; ++i) isVisible ? 100 : defaultValue())", 81 | "for (auto i = 0; i < 5; ++i){ out << ( isVisible ? 100 : defaultValue()); } "); 82 | } 83 | 84 | TEST(ExpressionNode, WithConditionalExtensionOnClosingBraces) 85 | { 86 | test 87 | ("$( isVisible ? 100 : defaultValue())?(isVisible)", 88 | "if (isVisible){ out << ( isVisible ? 100 : defaultValue()); } "); 89 | } 90 | 91 | TEST(ExpressionNode, WithLoopExtensionOnClosingBraces) 92 | { 93 | test 94 | ("$( isVisible ? 100 : defaultValue())@(auto i = 0; i < 5; ++i)", 95 | "for (auto i = 0; i < 5; ++i){ out << ( isVisible ? 100 : defaultValue()); } "); 96 | } 97 | 98 | 99 | TEST(ExpressionNode, WithMacroExtensionOnClosingBraces) 100 | { 101 | test 102 | ("$( isVisible ? 100 : defaultValue())#(test_macro)", 103 | "out << ( isVisible ? 100 : defaultValue());"); 104 | } 105 | 106 | 107 | TEST(StatementNode, Basic) 108 | { 109 | test 110 | ("${ auto a = 0; {/*local scope*/} }", 111 | " auto a = 0; {/*local scope*/} "); 112 | } 113 | 114 | TEST(StatementNode, BasicWithString) 115 | { 116 | test 117 | ("${ auto a = 0; {auto str = `Hello world's{}`;} }", 118 | " auto a = 0; {auto str = R\"_htcpp_str_(Hello world's{})_htcpp_str_\";} "); 119 | } 120 | 121 | TEST(GlobalStatementNode, Basic) 122 | { 123 | test 124 | ("#{ auto a = 0; {/*local scope*/} }", 125 | " auto a = 0; {/*local scope*/} "); 126 | } 127 | 128 | TEST(GlobalStatementNode, BasicWithString) 129 | { 130 | test 131 | ("#{ auto a = 0; {auto str = `Hello {}`;} }", 132 | " auto a = 0; {auto str = R\"_htcpp_str_(Hello {})_htcpp_str_\";} "); 133 | } 134 | 135 | TEST(InvalidExpressionNode, Unclosed) 136 | { 137 | testError("$(cfg.value", 138 | "[line:1, column:1] Expression isn't closed with ')'"); 139 | } 140 | 141 | TEST(InvalidExpressionNode, UnclosedString) 142 | { 143 | testError("$(\"Hello)", 144 | "[line:1, column:4] String isn't closed with '\"'"); 145 | } 146 | 147 | TEST(InvalidExpressionNode, UnclosedRawString) 148 | { 149 | testError("$(`Hello)", 150 | "[line:1, column:4] String isn't closed with '`'"); 151 | } 152 | 153 | TEST(InvalidExpressionNode, UnclosedChar) 154 | { 155 | testError("$(std::string{\"Hello\"} + '!)", 156 | "[line:1, column:27] Char literal isn't closed with \"'\""); 157 | } 158 | 159 | 160 | TEST(InvalidExpressionNode, Empty) 161 | { 162 | testError("$( )", 163 | "[line:1, column:1] Expression can't be empty"); 164 | } 165 | 166 | TEST(InvalidExpressionNode, MultipleExtensionsTwoConditionals) 167 | { 168 | testError("$(?(isVisible) cfg.value)?(isVisible)", 169 | "[line:1, column:26] Expression can't have multiple extensions"); 170 | } 171 | 172 | TEST(InvalidExpressionNode, MultipleExtensionsLoopAndConditional) 173 | { 174 | testError("$(@(auto i; i < 5; ++i) cfg.value)?(isVisible)", 175 | "[line:1, column:35] Expression can't have multiple extensions"); 176 | } 177 | 178 | TEST(InvalidExpressionNode, MultipleExtensionsConditionalAndLoop) 179 | { 180 | testError("$(?(isVisible) cfg.value)@(auto i; i < 5; ++i)", 181 | "[line:1, column:26] Expression can't have multiple extensions"); 182 | } 183 | 184 | TEST(InvalidExpressionNode, MultipleExtensionsTwoLoops) 185 | { 186 | testError("$(@(auto i; i < 5; ++i) cfg.value)@(auto i; i < 5; ++i)", 187 | "[line:1, column:35] Expression can't have multiple extensions"); 188 | } 189 | 190 | TEST(InvalidStatementNode, Unclosed) 191 | { 192 | testError("${ auto x = 0;", 193 | "[line:1, column:1] Statement isn't closed with '}'"); 194 | } 195 | 196 | TEST(InvalidStatementNode, Empty) 197 | { 198 | testError("${ }", 199 | "[line:1, column:1] Statement can't be empty"); 200 | } 201 | 202 | TEST(InvalidGlobalStatementNode, Unclosed) 203 | { 204 | testError("#{ #include ", 205 | "[line:1, column:1] Global statement isn't closed with '}'"); 206 | } 207 | 208 | TEST(InvalidGlobalStatementNode, Empty) 209 | { 210 | testError("#{ }", 211 | "[line:1, column:1] Global statement can't be empty"); 212 | } 213 | 214 | -------------------------------------------------------------------------------- /hypertextcpp.cmake: -------------------------------------------------------------------------------- 1 | function(hypertextcpp_GenerateHeader) 2 | cmake_parse_arguments( 3 | ARG 4 | "" 5 | "TEMPLATE_FILE;CLASS_NAME;OUTPUT_DIR" 6 | "" 7 | ${ARGN} 8 | ) 9 | if (NOT ARG_TEMPLATE_FILE) 10 | message(FATAL_ERROR "[hypertextcpp_GenerateHeader] Argument 'TEMPLATE_FILE' is missing") 11 | endif() 12 | if (ARG_UNPARSED_ARGUMENTS) 13 | message(FATAL_ERROR "[hypertextcpp_GenerateHeader] Unsupported argument: ${ARG_UNPARSED_ARGUMENTS}") 14 | endif() 15 | 16 | if (NOT IS_ABSOLUTE ${ARG_TEMPLATE_FILE}) 17 | set(TEMPLATE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${ARG_TEMPLATE_FILE}) 18 | else() 19 | set(TEMPLATE_FILE ${ARG_TEMPLATE_FILE}) 20 | endif() 21 | 22 | _hypertextcppImpl_StringAfterLast(${TEMPLATE_FILE} / TEMPLATE_NAME) 23 | _hypertextcppImpl_StringBeforeLast(${TEMPLATE_NAME} "." TEMPLATE_NAME) 24 | 25 | 26 | if (ARG_OUTPUT_DIR) 27 | if (NOT IS_ABSOLUTE ${ARG_OUTPUT_DIR}) 28 | set(OUTPUT_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${ARG_OUTPUT_DIR}/${TEMPLATE_NAME}.h) 29 | set(OUTPUT_DIR_CMDLINE_PARAM "-outputDir=${CMAKE_CURRENT_SOURCE_DIR}/${ARG_OUTPUT_DIR}") 30 | else() 31 | set(OUTPUT_FILE ${ARG_OUTPUT_DIR}/${TEMPLATE_NAME}.h) 32 | set(OUTPUT_DIR_CMDLINE_PARAM "-outputDir=${ARG_OUTPUT_DIR}") 33 | endif() 34 | else() 35 | set(OUTPUT_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${TEMPLATE_NAME}.h) 36 | set(OUTPUT_DIR_CMDLINE_PARAM "-outputDir=${CMAKE_CURRENT_SOURCE_DIR}") 37 | endif() 38 | 39 | if (ARG_CLASS_NAME) 40 | set(CLASS_NAME_CMDLINE_PARAM "-className=${ARG_CLASS_NAME}") 41 | endif() 42 | add_custom_command( 43 | OUTPUT ${OUTPUT_FILE} 44 | COMMAND hypertextcpp generateHeaderOnly ${TEMPLATE_FILE} ${OUTPUT_DIR_CMDLINE_PARAM} ${CLASS_NAME_CMDLINE_PARAM} 45 | DEPENDS ${TEMPLATE_FILE} 46 | VERBATIM 47 | ) 48 | endfunction() 49 | 50 | function(hypertextcpp_GenerateHeaderAndSource) 51 | cmake_parse_arguments( 52 | ARG 53 | "" 54 | "TEMPLATE_FILE;CLASS_NAME;OUTPUT_DIR;CONFIG_CLASS_NAME" 55 | "" 56 | ${ARGN} 57 | ) 58 | if (NOT ARG_TEMPLATE_FILE) 59 | message(FATAL_ERROR "[hypertextcpp_GenerateHeader] Argument 'TEMPLATE_FILE' is missing") 60 | endif() 61 | if (NOT ARG_CONFIG_CLASS_NAME) 62 | message(FATAL_ERROR "[hypertextcpp_GenerateHeader] Argument 'CONFIG_CLASS_NAME' is missing") 63 | endif() 64 | if (ARG_UNPARSED_ARGUMENTS) 65 | message(FATAL_ERROR "[hypertextcpp_GenerateHeader] Unsupported argument: ${ARG_UNPARSED_ARGUMENTS}") 66 | endif() 67 | 68 | if (NOT IS_ABSOLUTE ${ARG_TEMPLATE_FILE}) 69 | set(TEMPLATE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${ARG_TEMPLATE_FILE}) 70 | else() 71 | set(TEMPLATE_FILE ${ARG_TEMPLATE_FILE}) 72 | endif() 73 | 74 | _hypertextcppImpl_StringAfterLast(${TEMPLATE_FILE} / TEMPLATE_NAME) 75 | _hypertextcppImpl_StringBeforeLast(${TEMPLATE_NAME} "." TEMPLATE_NAME) 76 | 77 | 78 | if (ARG_OUTPUT_DIR) 79 | if (NOT IS_ABSOLUTE ${ARG_OUTPUT_DIR}) 80 | set(OUTPUT_HEADER_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${ARG_OUTPUT_DIR}/${TEMPLATE_NAME}.h) 81 | set(OUTPUT_SOURCE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${ARG_OUTPUT_DIR}/${TEMPLATE_NAME}.cpp) 82 | set(OUTPUT_DIR_CMDLINE_PARAM "-outputDir=${CMAKE_CURRENT_SOURCE_DIR}/${ARG_OUTPUT_DIR}") 83 | else() 84 | set(OUTPUT_HEADER_FILE ${ARG_OUTPUT_DIR}/${TEMPLATE_NAME}.h) 85 | set(OUTPUT_SOURCE_FILE ${ARG_OUTPUT_DIR}/${TEMPLATE_NAME}.cpp) 86 | set(OUTPUT_DIR_CMDLINE_PARAM "-outputDir=${ARG_OUTPUT_DIR}") 87 | endif() 88 | else() 89 | _hypertextcppImpl_StringBeforeLast(${TEMPLATE_FILE} / TEMPLATE_FILE_DIR) 90 | set(OUTPUT_HEADER_FILE ${TEMPLATE_FILE_DIR}/${TEMPLATE_NAME}.h) 91 | set(OUTPUT_SOURCE_FILE ${TEMPLATE_FILE_DIR}/${TEMPLATE_NAME}.cpp) 92 | set(OUTPUT_DIR_CMDLINE_PARAM "-outputDir=${TEMPLATE_FILE_DIR}") 93 | endif() 94 | 95 | if (ARG_CLASS_NAME) 96 | set(CLASS_NAME_CMDLINE_PARAM "-className=${ARG_CLASS_NAME}") 97 | endif() 98 | add_custom_command( 99 | OUTPUT ${OUTPUT_HEADER_FILE} ${OUTPUT_SOURCE_FILE} 100 | COMMAND hypertextcpp generateHeaderAndSource ${TEMPLATE_FILE} -configClassName=${ARG_CONFIG_CLASS_NAME} ${OUTPUT_DIR_CMDLINE_PARAM} ${CLASS_NAME_CMDLINE_PARAM} 101 | DEPENDS ${TEMPLATE_FILE} 102 | VERBATIM 103 | ) 104 | endfunction() 105 | 106 | function(hypertextcpp_BuildSharedLibrary) 107 | cmake_parse_arguments( 108 | ARG 109 | "" 110 | "TEMPLATE_FILE;OUTPUT_DIR" 111 | "" 112 | ${ARGN} 113 | ) 114 | if (NOT ARG_TEMPLATE_FILE) 115 | message(FATAL_ERROR "[hypertextcpp_BuildSharedLibrary] Argument 'TEMPLATE_FILE' is missing") 116 | endif() 117 | if (ARG_UNPARSED_ARGUMENTS) 118 | message(FATAL_ERROR "[hypertextcpp_BuildSharedLibrary] Unsupported argument: ${ARG_UNPARSED_ARGUMENTS}") 119 | endif() 120 | 121 | if (NOT IS_ABSOLUTE ${ARG_TEMPLATE_FILE}) 122 | set(TEMPLATE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${ARG_TEMPLATE_FILE}) 123 | else() 124 | set(TEMPLATE_FILE ${ARG_TEMPLATE_FILE}) 125 | endif() 126 | 127 | _hypertextcppImpl_StringAfterLast(${TEMPLATE_FILE} / TEMPLATE_NAME) 128 | _hypertextcppImpl_StringBeforeLast(${TEMPLATE_NAME} "." TEMPLATE_NAME) 129 | 130 | if (ARG_OUTPUT_DIR) 131 | if (NOT IS_ABSOLUTE ${ARG_OUTPUT_DIR}) 132 | set(OUTPUT_FILE ${CMAKE_CURRENT_BINARY_DIR}/${ARG_OUTPUT_DIR}/${TEMPLATE_NAME}.cpp) 133 | set(OUTPUT_DIR_CMDLINE_PARAM "-outputDir=${CMAKE_CURRENT_BINARY_DIR}/${ARG_OUTPUT_DIR}") 134 | else() 135 | set(OUTPUT_FILE ${ARG_OUTPUT_DIR}/${TEMPLATE_NAME}.cpp) 136 | set(OUTPUT_DIR_CMDLINE_PARAM "-outputDir=${ARG_OUTPUT_DIR}") 137 | endif() 138 | else() 139 | set(OUTPUT_FILE ${CMAKE_CURRENT_BINARY_DIR}/${TEMPLATE_NAME}.cpp) 140 | set(OUTPUT_DIR_CMDLINE_PARAM "-outputDir=${CMAKE_CURRENT_BINARY_DIR}") 141 | endif() 142 | 143 | add_custom_command( 144 | OUTPUT ${OUTPUT_FILE} 145 | COMMAND hypertextcpp generateSharedLibrarySource ${TEMPLATE_FILE} ${OUTPUT_DIR_CMDLINE_PARAM} 146 | DEPENDS ${TEMPLATE_FILE} 147 | VERBATIM 148 | ) 149 | 150 | add_library(${TEMPLATE_NAME} SHARED ${OUTPUT_FILE}) 151 | target_compile_features(${TEMPLATE_NAME} PUBLIC cxx_std_11) 152 | target_compile_definitions(${TEMPLATE_NAME} PUBLIC "HYPERTEXTCPP_EXPORT") 153 | set_target_properties(${TEMPLATE_NAME} PROPERTIES CXX_EXTENSIONS OFF) 154 | if (ARG_OUTPUT_DIR) 155 | set_target_properties(${TEMPLATE_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${ARG_OUTPUT_DIR}) 156 | endif() 157 | endfunction() 158 | 159 | function (_hypertextcppImpl_StringBeforeLast STR VALUE RESULT) 160 | _hypertextcppImpl_StringBefore(${STR} ${VALUE} RESULT_VALUE REVERSE) 161 | set(${RESULT} ${RESULT_VALUE} PARENT_SCOPE) 162 | endfunction() 163 | 164 | function (_hypertextcppImpl_StringAfterLast STR VALUE RESULT) 165 | _hypertextcppImpl_StringAfter(${STR} ${VALUE} RESULT_VALUE REVERSE) 166 | set(${RESULT} ${RESULT_VALUE} PARENT_SCOPE) 167 | endfunction() 168 | 169 | function(_hypertextcppImpl_StringBefore STR VALUE RESULT REVERSE) 170 | string(FIND ${STR} ${VALUE} VALUE_POS ${REVERSE}) 171 | string(LENGTH ${STR} STR_LENGTH) 172 | string(SUBSTRING ${STR} 0 ${VALUE_POS} RESULT_VALUE) 173 | set(${RESULT} ${RESULT_VALUE} PARENT_SCOPE) 174 | endfunction() 175 | 176 | function (_hypertextcppImpl_StringAfter STR VALUE RESULT REVERSE) 177 | string(FIND ${STR} ${VALUE} VALUE_POS ${REVERSE}) 178 | string(LENGTH ${STR} STR_LENGTH) 179 | string(LENGTH ${VALUE} VALUE_LENGTH) 180 | MATH(EXPR RESULT_LENGTH "${STR_LENGTH} - ${VALUE_POS} - ${VALUE_LENGTH}") 181 | MATH(EXPR RESULT_POS "${VALUE_POS} + ${VALUE_LENGTH}") 182 | string(SUBSTRING ${STR} ${RESULT_POS} ${RESULT_LENGTH} RESULT_VALUE) 183 | set(${RESULT} ${RESULT_VALUE} PARENT_SCOPE) 184 | endfunction() 185 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

    2 | 3 |

    4 | 5 | **hypertextcpp** is an HTML templating system for C++ applications. 6 | It provides an [.htcpp](https://marketplace.visualstudio.com/items?itemName=kamchatka-volcano.htcpp) template file format and a command-line utility that transpiles it to C++ HTML rendering code. Include the generated C++ header file in your project, set up a build system to update it when necessary, and you're all set. 7 | 8 | A quick example: 9 | 10 | [`examples/01/todolist.htcpp`](examples/01/todolist.htcpp) 11 | ```html 12 | 13 | 14 |

    $(cfg.name)'s todo list:

    15 |

    No tasks found

    ?(cfg.tasks.empty()) 16 |
      17 |
    • $(task.name)
    • @(auto task : cfg.tasks) 18 |
    19 | 20 | 21 | 22 | ``` 23 | 24 | Now let's generate the C++ header: 25 | ```console 26 | kamchatka-volcano@home:~$ hypertextcpp todolist.htcpp 27 | ``` 28 | 29 | This command creates the `todolist.h` file in the current working directory. All that's left is to use it in our program. 30 | 31 | [`examples/01/todolist_printer.cpp`](examples/01/todolist_printer.cpp) 32 | ```cpp 33 | #include "todolist.h" 34 | #include 35 | #include 36 | 37 | int main() 38 | { 39 | struct PageParams{ 40 | struct Task{ 41 | std::string name; 42 | }; 43 | std::string name = "Bob"; 44 | std::vector tasks = {{"laundry"}, {"cooking"}}; 45 | } pageParams; 46 | auto page = todolist{}; 47 | page.print(pageParams); 48 | return 0; 49 | } 50 | 51 | ``` 52 | 53 | Compile it with your preferred method, launch it, and you will get this output: 54 | ```console 55 | kamchatka-volcano@home:~$ ./todolist_printer 56 | 57 | 58 |

    Bob's todo list:

    59 | 60 |
      61 |
    • laundry
    • cooking
    • 62 |
    63 | 64 | 65 | 66 | ``` 67 | 68 | 69 | ## Table of contents 70 | * [Template file syntax](#template-file-syntax) 71 | * [Expressions](#expressions) 72 | * [Statements](#statements) 73 | * [Global statements](#global-statements) 74 | * [Control flow extensions](#control-flow-extensions) 75 | * [Sections](#sections) 76 | * [Procedures and partial rendering](#procedures-and-partial-rendering) 77 | * [Code generation](#code-generation) 78 | * [Command line parameters](#command-line-parameters) 79 | * [Single header renderer](#single-header-renderer) 80 | * [Header and source renderer](#header-and-source-renderer) 81 | * [Shared library renderer](#shared-library-renderer) 82 | * [Installation](#installation) 83 | * [Running tests](#running-tests) 84 | * [License](#license) 85 | 86 | ## Template file syntax 87 | 88 | ### Expressions 89 | **$(**`c++ expression`**)** 90 | Expressions are used to add the application's data to the template. It can be any valid C++ expression, with the only condition that its result must be streamable to the default output stream `std::ostream`. Expressions can be placed anywhere in the HTML template, except in tag names. 91 | In our todolist example, `$(cfg.name)` is an expression that adds the template config variable `name` to the result page. 92 | 93 | ### Statements 94 | **${**`c++ statement(s)`**}** 95 | Statements are used to add any valid C++ code to the template rendering function. For example, you can add variables, class declarations, or lambdas. Let's say you don't like the default name `cfg` used for passing data to the template. You can create a reference to it with any name you like and use it later: 96 | 97 | ```html 98 | ${ auto& param = cfg;} 99 |

    $(param.name)'s todo list:

    100 | ``` 101 | 102 | Note, that `cfg` and `out` are reserved names used for parameters in the generated 103 | rendering function. Also, don't put anything in the `htcpp` namespace. 104 | 105 | ### Global statements 106 | **#{**`c++ statement(s)`**}** 107 | These statements are used to add any valid C++ code outside the template rendering function. Unlike regular statements, with global ones you can add include directives or function definitions. Global statements can only be placed at the top level of the `htcpp` template, outside any HTML element. Please don't put anything in the `htcpp` namespace. 108 | 109 | ### Control flow extensions 110 | If **hypertextcpp** used a common approach for control flow in HTML template engines, our todolist example would look something like this: 111 | ```html 112 | 113 | 114 |

    %%cfg.name%%'s todo list:

    115 | %%if cfg.tasks.empty()%% 116 |

    No tasks found

    117 | %%end%% 118 |
      119 | %%for auto task : cfg.tasks%% 120 |
    • $(task.name)
    • 121 | %%end%% 122 |
    123 | 124 | 125 | 126 | ``` 127 | 128 | In our opinion, it significantly hurts the readability of the document tree and makes it hard to choose indentation and keep it consistent — notice how different approaches are used for if and for blocks in the example. 129 | 130 | **hypertextcpp** solves this problem by applying control flow to the HTML elements themselves without adding logic block scopes to the document. It uses just two extensions for tags to make this work: 131 | 132 | * Conditional extension 133 | **?(**`c++ condition`**)** 134 | HTML elements with this extension are added to the document only when condition is fulfilled. 135 | Example of usage from our todolist template: 136 | ```html 137 |

    No tasks found

    ?(cfg.tasks.empty()) 138 | ``` 139 | 140 | * Loop extension 141 | **@(**`c++ init-statement; condition; iteration_expression`**)** 142 | or 143 | **@(**`c++ range_declaration : range_expression`**)** 144 | HTML elements with this extension are added to the document multiple times, on each step of the loop or for each element of the iterated range. 145 | Example of usage from our todolist template: 146 | ```html 147 |
  • $(task.name)
  • @(auto task : cfg.tasks) 148 | ``` 149 | Let's add another example for another type of `for` loop: 150 | ```html 151 |
  • Task#$(i)
  • @(auto i = 0 ; i < 3; ++i) 152 | ``` 153 | which evaluates to 154 | ```html 155 |
  • Task#0
  • 156 |
  • Task#1
  • 157 |
  • Task#2
  • 158 | ``` 159 | 160 | Both extensions can be added to the opening or closing tag, but each tag and HTML element can only have one extension. 161 | 162 | It's recommended to add the extension to the opening tag for multiline HTML elements: 163 | ```html 164 |
    ?(cfg.greet) 165 |

    Hello world!

    166 |
    167 | ``` 168 | and to the closing tag for the single-line ones: 169 | ```html 170 |

    Hello world!

    ?(cfg.greet) 171 | ``` 172 | 173 | Note that while extensions hide control flow block scopes from the template document, they're still present in the generated C++ code and implemented with regular `if` and `for` control structures. Therefore, a template like this: 174 | 175 | ```html 176 |
    @(auto i = 0; i<3; ++i) 177 | ${ auto num = i*2;} 178 |

    Item #$(num)

    179 |

    180 |

    Last num is $(num)

    181 | ``` 182 | won't compile because the `num` variable isn't visible outside the `for` block scope generated by the loop extension on the `div` tag. 183 | 184 | ### Sections 185 | **\[\[** `text, html elements, statements, expressions or other sections` **]]** 186 | Sections can contain a part of the template document, and it's possible to attach control flow extensions to them. Their main usage is adding attributes to HTML elements conditionally. 187 | 188 | Let's update the todolist example by adding a line-through text style to completed tasks: 189 | [`examples/02/todolist.htcpp`](examples/02/todolist.htcpp) 190 | ```html 191 | 192 | 193 |

    $(cfg.name)'s todo list:

    194 |

    No tasks found

    ?(cfg.tasks.empty()) 195 |
      196 |
    • $(task.name)
    • @(auto task : cfg.tasks) 197 |
    198 | 199 | 200 | ``` 201 | 202 | Don't forget to update the C++ template config structure! 203 | [`examples/02/todolist_printer.cpp`](examples/02/todolist_printer.cpp) 204 | ```cpp 205 | //... 206 | struct PageParams{ 207 | struct Task{ 208 | std::string name; 209 | bool isCompleted = false; 210 | }; 211 | std::string name = "Bob"; 212 | std::vector tasks = {{"laundry", true}, {"cooking", false}}; 213 | } pageParams; 214 | //... 215 | ``` 216 | 217 | Tip of the day: Keep your template tidy and don't introduce sections when it's possible to attach extensions to HTML elements. 218 | 219 | 220 | ### Procedures and partial rendering 221 | **#**`procedureName`**(){**`html elements, statements, expressions or sections`**}** 222 | 223 | Parts of the `htcpp` template can be placed inside procedures—parameterless functions capturing the `cfg` variable. They are available for call from the C++ application, so if any part of the page needs to be rendered separately from the whole template, procedures are a big help. 224 | Procedures can only be placed at the top level of the `htcpp` template, outside any HTML element. 225 | Let's put the list of tasks from the todolist example in the procedure: 226 | 227 | [`examples/03/todolist.htcpp`](examples/03/todolist.htcpp) 228 | ```html 229 | #taskList(){ 230 |
  • $(task.name)
  • @(auto task : cfg.tasks) 231 | } 232 | 233 | 234 |

    $(cfg.name)'s todo list:

    235 |

    No tasks found

    ?(cfg.tasks.empty()) 236 |
      237 | $(taskList()) 238 |
    239 | 240 | 241 | ``` 242 | 243 | Now the tasks list can be output to stdout by itself like this: 244 | [`examples/03/todolist_printer.cpp`](examples/03/todolist_printer.cpp) 245 | ```cpp 246 | //... 247 | auto page = todolist{}; 248 | page.print("taskList", pageParams); 249 | //... 250 | ``` 251 | 252 | ## Code generation 253 | ### Command line parameters 254 | ```console 255 | kamchatka-volcano@home:~$ hypertextcpp --help 256 | Usage: hypertextcpp [commands] [flags] 257 | Flags: 258 | --help show usage info and exit 259 | Commands: 260 | generateHeaderOnly [options] generate header only file 261 | generateSharedLibrarySource [options] generate shared library source 262 | file 263 | generateHeaderAndSource [options] generate header and source files 264 | ``` 265 | 266 | Single header renderer generation: 267 | ```console 268 | kamchatka-volcano@home:~$ hypertextcpp generateHeaderOnly --help 269 | Usage: hypertextcpp generateHeaderOnly [params] [flags] 270 | Arguments: 271 | (path) .htcpp file to transpile 272 | -outputDir= output dir 273 | (if empty, current working directory is used) 274 | (optional, default: "") 275 | -className= generated class name 276 | (if empty, input file name is used) 277 | (optional, default: "") 278 | Flags: 279 | --help show usage info and exit 280 | ``` 281 | 282 | Header and source renderer generation: 283 | ```console 284 | kamchatka-volcano@home:~$ hypertextcpp generateHeaderAndSource --help 285 | Usage: hypertextcpp generateHeaderAndSource -configClassName= [params] [flags] 286 | Arguments: 287 | (path) .htcpp file to transpile 288 | Parameters: 289 | -configClassName= config class name 290 | -outputDir= output dir 291 | (if empty, current working directory is used) 292 | (optional, default: "") 293 | -className= generated class name 294 | (if empty, input file name is used) 295 | (optional, default: "") 296 | Flags: 297 | --help show usage info and exit 298 | ``` 299 | 300 | Shared library renderer generation: 301 | ```console 302 | kamchatka-volcano@home:~$ hypertextcpp generateSharedLibrarySource --help 303 | Usage: hypertextcpp generateSharedLibrarySource [params] [flags] 304 | Arguments: 305 | (path) .htcpp file to transpile 306 | -outputDir= output dir 307 | (if empty, current working directory is used) 308 | (optional, default: "") 309 | Flags: 310 | --help show usage info and exit 311 | ``` 312 | 313 | 314 | ### Single header renderer 315 | In this mode, the **hypertextcpp** generates a C++ header file that you're supposed to simply include in your project. A generated renderer class has the name of the `.htcpp` template file. 316 | Converting the template to C++ code each time you modify it is a laborious task, so it makes sense to add this step to your build process. To do this with CMake you can use `hypertextcpp_GenerateHeader` function from the `hypertextcpp.cmake` file. 317 | Note that this function launches the `hypertextcpp` executable, so it should be installed on your system first. 318 | 319 | [`examples/04/CMakeLists.txt`](examples/04/CMakeLists.txt) 320 | ``` 321 | cmake_minimum_required(VERSION 3.18) 322 | 323 | 324 | include(../../hypertextcpp.cmake) 325 | hypertextcpp_GenerateHeader( 326 | TEMPLATE_FILE todolist.htcpp 327 | CLASS_NAME TodoList 328 | ) 329 | 330 | set(SRC 331 | todolist_printer.cpp 332 | todolist.h) 333 | 334 | #Please note that to generate the header todolist.h, it must pe passed to the target sources 335 | add_executable(todolist_printer ${SRC}) 336 | 337 | target_compile_features(todolist_printer PUBLIC cxx_std_17) 338 | set_target_properties(todolist_printer PROPERTIES CXX_EXTENSIONS OFF) 339 | ``` 340 | 341 | Now, every time you change the template `todolist.htcpp`, the corresponding header will be regenerated on the next build. 342 | 343 | ### Header and source renderer 344 | It can feel quite wasteful to rebuild all object files that include the renderer header each time the template file is changed, so `hypertextcpp` supports the generation of the renderer as a header and implementation file. In this mode, the generated rendering methods aren't function templates, and you need to provide the config name as a command-line parameter and either define the config structure inside the template or include it from there (check `examples/ex_06` and `examples/ex_07`). 345 | To do this with CMake, you can use the `hypertextcpp_GenerateHeaderAndSource` function from the `hypertextcpp.cmake` file. 346 | 347 | ``` 348 | hypertextcpp_GenerateHeaderAndSource( 349 | TEMPLATE_FILE todolist.htcpp 350 | CONFIG_CLASS_NAME PageParams) 351 | ``` 352 | 353 | ### Shared library renderer 354 | **hypertextcpp** also supports the generation of a C++ source file for building templates in the form of shared libraries and linking them dynamically from your application. This way it's possible to rebuild the template and update it without restarting the application. 355 | It requires duplicating the config declaration in the .htcpp template, registering it with the `HTCPP_CONFIG` macro in both the template and the application source, generating the renderer code with the `generateSharedLibrarySource` command, building the library, and loading it using the tiny API installed from the `shared_lib_api/` directory. It sounds scarier than it is, so let's quickly update the todolist example to see how it works. 356 | 357 | First we need to copy the config structure declaration in the template: 358 | [`examples/05/todolist.htcpp`](examples/05/todolist.htcpp) 359 | ```html 360 | #{ 361 | #include 362 | struct PageParams{ 363 | struct Task{ 364 | std::string name; 365 | bool isCompleted = false; 366 | }; 367 | std::string name = "Bob"; 368 | std::vector tasks = {{"laundry", true}, {"cooking", false}}; 369 | } pageParams; 370 | HTCPP_CONFIG(PageParams); 371 | } 372 | 373 | #taskList(){ 374 |
  • $(task.name)
  • @(auto task : cfg.tasks) 375 | } 376 | 377 | 378 |

    $(cfg.name)'s todo list:

    379 |

    No tasks found

    ?(cfg.tasks.empty()) 380 |
      381 | $(taskList()) 382 |
    383 | 384 | 385 | ``` 386 | 387 | Be sure to use an exact copy; any mismatch in the config structure between the template and the application can't be handled gracefully. So, if you try to load a template library with a different structure, your application will abort with a runtime error. This means that by using a htcpp template in the form of shared libraries, you lose one of the main advantages of hypertextcpp—compile-time type safety. Because of this, it's recommended to use this mode only after your template config has stabilized and doesn't change often. 388 | 389 | Next, we need to build our template renderer as a library. It's not possible to bundle multiple template files in one library, so we can build a library from a single `.htcpp` file by using the `hypertextcpp_BuildSharedLibrary` CMake function from `hypertextcpp.cmake`: 390 | 391 | ``` 392 | cmake_minimum_required(VERSION 3.18) 393 | 394 | hypertextcpp_BuildSharedLibrary( 395 | TEMPLATE_FILE todolist.htcpp 396 | ) 397 | add_dependencies(${PROJECT_NAME} todolist) 398 | ``` 399 | 400 | If everything goes right, you'll get `libtodolist.so` in the build directory. That's our `todolist.htcpp` template compiled as a shared library. 401 | Next, let's modify `todolist_printer.cpp` to be able to load it: 402 | [`examples/05/todolist_printer.cpp`](examples/05/todolist_printer.cpp) 403 | ```cpp 404 | #include 405 | #include 406 | #include 407 | 408 | struct PageParams{ 409 | struct Task{ 410 | std::string name; 411 | bool isCompleted = false; 412 | }; 413 | std::string name = "Bob"; 414 | std::vector tasks = {{"laundry", true}, {"cooking", false}}; 415 | }; 416 | HTCPP_CONFIG(PageParams); 417 | 418 | int main() 419 | { 420 | auto pageParams = PageParams{}; 421 | auto page = htcpp::loadTemplate("libtodolist.so"); 422 | page->print(pageParams); 423 | return 0; 424 | } 425 | ``` 426 | 427 | On Linux, it may be necessary to link the system library `dl` to the build config, as we load the template library dynamically. 428 | `todolist_printer` should compile and work the same as the previous example using the single-header approach. 429 | 430 | ## Installation 431 | 432 | ```console 433 | git clone https://github.com/kamchatka-volcano/hypertextcpp.git 434 | cd hypertextcpp 435 | cmake -S . -B build 436 | cmake --build build 437 | cmake --install build 438 | cmake --install build --component shared_lib_api 439 | ``` 440 | You can skip the installation of `shared_lib_api` component if you don't need to load templates in shared libraries form. 441 | 442 | ## Running tests 443 | ```console 444 | cd hypertextcpp 445 | cmake -S . -B build -DENABLE_TESTS=ON 446 | cmake --build build 447 | cd build/tests && ctest 448 | ``` 449 | 450 | ## License 451 | **hypertextcpp** is licensed under the [MS-PL license](/LICENSE.md) 452 | --------------------------------------------------------------------------------