From 6eb4e2a985eb1361517a30582b6a9cc00522b0bf Mon Sep 17 00:00:00 2001 From: kamchatka-volcano Date: Tue, 31 Dec 2024 13:57:43 +0500 Subject: [PATCH] -added header and source generation mode; -changed command line interface; -set version to 2.0.0; --- CMakeLists.txt | 14 +- README.md | 91 ++++++--- examples/CMakeLists.txt | 4 +- examples/ex_01/CMakeLists.txt | 3 +- examples/ex_02/CMakeLists.txt | 7 +- examples/ex_02/todolist_printer.cpp | 2 +- examples/ex_03/CMakeLists.txt | 2 +- examples/ex_04/CMakeLists.txt | 5 +- examples/ex_05/CMakeLists.txt | 3 +- examples/ex_06/CMakeLists.txt | 20 ++ examples/ex_06/todolist.htcpp | 24 +++ examples/ex_06/todolist_printer.cpp | 11 ++ examples/ex_07/CMakeLists.txt | 20 ++ examples/ex_07/pageparams.h | 12 ++ examples/ex_07/todolist.htcpp | 16 ++ examples/ex_07/todolist_printer.cpp | 11 ++ hypertextcpp.cmake | 176 ++++++++++++++---- src/header_and_source_transpiler_renderer.cpp | 173 +++++++++++++++++ src/header_and_source_transpiler_renderer.h | 22 +++ src/main.cpp | 166 ++++++++++++----- 20 files changed, 666 insertions(+), 116 deletions(-) create mode 100644 examples/ex_06/CMakeLists.txt create mode 100644 examples/ex_06/todolist.htcpp create mode 100644 examples/ex_06/todolist_printer.cpp create mode 100644 examples/ex_07/CMakeLists.txt create mode 100644 examples/ex_07/pageparams.h create mode 100644 examples/ex_07/todolist.htcpp create mode 100644 examples/ex_07/todolist_printer.cpp create mode 100644 src/header_and_source_transpiler_renderer.cpp create mode 100644 src/header_and_source_transpiler_renderer.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 439a757..5b8510c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,11 @@ cmake_minimum_required(VERSION 3.18) -project(hypertextcpp VERSION 1.2.1 DESCRIPTION "hypertextcpp") +project(hypertextcpp VERSION 2.0.0 DESCRIPTION "hypertextcpp") include(external/seal_lake) -SealLake_Import(cmdlime 2.6.0 +SealLake_Import(cmdlime 2.7.0 GIT_REPOSITORY https://github.com/kamchatka-volcano/cmdlime - GIT_TAG dev + GIT_TAG v2.7.0 ) SealLake_Import(sfun 5.1.0 @@ -24,6 +24,10 @@ SealLake_Bundle( GIT_REPOSITORY https://github.com/ericniebler/range-v3 GIT_TAG 0.12.0 ) +SealLake_Import(fmt 9.1.0 + GIT_REPOSITORY https://github.com/fmtlib/fmt + GIT_TAG 9.1.0 +) set(SRC src/codenode.cpp @@ -41,7 +45,8 @@ set(SRC src/utils.cpp src/node_utils.cpp src/single_header_transpiler_renderer.cpp - src/shared_lib_transpiler_renderer.cpp) + src/shared_lib_transpiler_renderer.cpp + src/header_and_source_transpiler_renderer.cpp) SealLake_Executable( @@ -54,6 +59,7 @@ SealLake_Executable( cmdlime::cmdlime sfun::sfun Microsoft.GSL::GSL + fmt::fmt ) SealLake_OptionalSubProjects(tests examples) diff --git a/README.md b/README.md index 5378892..ca935ec 100644 --- a/README.md +++ b/README.md @@ -252,33 +252,66 @@ Now the tasks list can be output to stdout by itself like this: ### Command line parameters ```console kamchatka-volcano@home:~$ hypertextcpp --help -Usage: hypertextcpp [params] [flags] +Usage: hypertextcpp [commands] [flags] +Flags: + --help show usage info and exit +Commands: + generateHeaderOnly [options] generate header only file + generateSharedLibrarySource [options] generate shared library source + file + generateHeaderAndSource [options] generate header and source files + ``` + +Single header renderer generation: + ```console +kamchatka-volcano@home:~$ hypertextcpp generateHeaderOnly --help +Usage: hypertextcpp generateHeaderOnly [params] [flags] +Arguments: + (path) .htcpp file to transpile + -outputDir= output dir + (if empty, current working directory is used) + (optional, default: "") + -className= generated class name + (if empty, input file name is used) + (optional, default: "") +Flags: + --help show usage info and exit + ``` + +Header and source renderer generation: + ```console +kamchatka-volcano@home:~$ hypertextcpp generateHeaderAndSource --help +Usage: hypertextcpp generateHeaderAndSource -configClassName= [params] [flags] Arguments: (path) .htcpp file to transpile Parameters: - -o, --output output c++ file path + -configClassName= config class name + -outputDir= output dir (if empty, current working directory is used) (optional, default: "") - -c, --class-name generated class name - (optional) + -className= generated class name + (if empty, input file name is used) + (optional, default: "") Flags: - -s, --shared-lib generate result as shared library source - files - --class-pascalcase generate class name by using .htcpp filename - in PascalCase - --class-snakecase generate class name by using .htcpp filename - in snake_case - --class-lowercase generate class name by using .htcpp filename - in lowercase - --help show usage info and exit - - -Process finished with exit code 0 + --help show usage info and exit + ``` +Shared library renderer generation: + ```console +kamchatka-volcano@home:~$ hypertextcpp generateSharedLibrarySource --help +Usage: hypertextcpp generateSharedLibrarySource [params] [flags] +Arguments: + (path) .htcpp file to transpile + -outputDir= output dir + (if empty, current working directory is used) + (optional, default: "") +Flags: + --help show usage info and exit ``` + ### Single header renderer -By default, the **hypertextcpp** transpiler works in a single header mode and 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. You can override the name by using the `--class-name` parameter, or you can specify one of the following flags: `--class-pascalcase`, `--class-snakecase`, or `--class-lowercase` to use the `.htcpp` template's filename converted to the corresponding case as a class name. +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. 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. Note that this function launches the `hypertextcpp` executable, so it should be installed on your system first. @@ -288,23 +321,37 @@ cmake_minimum_required(VERSION 3.18) include(../../hypertextcpp.cmake) -hypertextcpp_GenerateHeader(NAME todolist CLASS_NAME TodoList) +hypertextcpp_GenerateHeader( + TEMPLATE_FILE todolist.htcpp + CLASS_NAME TodoList +) set(SRC todolist_printer.cpp todolist.h) +#Please note that to generate the header todolist.h, it must pe passed to the target sources add_executable(todolist_printer ${SRC}) target_compile_features(todolist_printer PUBLIC cxx_std_17) set_target_properties(todolist_printer PROPERTIES CXX_EXTENSIONS OFF) ``` -Now, every time you change the template, the corresponding header will be regenerated on the next build. +Now, every time you change the template `todolist.htcpp`, the corresponding header will be regenerated on the next build. + +### Header and source renderer +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`). +To do this with CMake, you can use the `hypertextcpp_GenerateHeaderAndSource` function from the `hypertextcpp.cmake` file. + +``` +hypertextcpp_GenerateHeaderAndSource( + TEMPLATE_FILE todolist.htcpp + CONFIG_CLASS_NAME PageParams) +``` ### Shared library renderer -It can feel quite wasteful to rebuild your project each time the template file is changed, so **hypertextcpp** supports the generation of a C++ source file for building templates in the form of shared libraries and linking them dynamically from your application. -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 `--shared-lib` command line flag, 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. + **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. +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. First we need to copy the config structure declaration in the template: [`examples/05/todolist.htcpp`](examples/05/todolist.htcpp) @@ -336,7 +383,7 @@ First we need to copy the config structure declaration in the template: ``` -Be sure to use an exact copy; any mismatch of 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, you'll definitely crash the application and maybe hurt someone as a result. +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. 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`: diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 6a28f83..8d26d5a 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -2,4 +2,6 @@ add_subdirectory(ex_01) add_subdirectory(ex_02) add_subdirectory(ex_03) add_subdirectory(ex_04) -add_subdirectory(ex_05) \ No newline at end of file +add_subdirectory(ex_05) +add_subdirectory(ex_06) +add_subdirectory(ex_07) \ No newline at end of file diff --git a/examples/ex_01/CMakeLists.txt b/examples/ex_01/CMakeLists.txt index 0c8e1df..f40f2c5 100644 --- a/examples/ex_01/CMakeLists.txt +++ b/examples/ex_01/CMakeLists.txt @@ -3,7 +3,8 @@ project(ex_01) include(../../hypertextcpp.cmake) -hypertextcpp_GenerateHeader(NAME todolist) +hypertextcpp_GenerateHeader( + TEMPLATE_FILE todolist.htcpp) set(SRC todolist_printer.cpp diff --git a/examples/ex_02/CMakeLists.txt b/examples/ex_02/CMakeLists.txt index d4e2d55..29eaccd 100644 --- a/examples/ex_02/CMakeLists.txt +++ b/examples/ex_02/CMakeLists.txt @@ -3,11 +3,14 @@ project(ex_02) include(../../hypertextcpp.cmake) -hypertextcpp_GenerateHeader(NAME todolist) +hypertextcpp_GenerateHeader( + TEMPLATE_FILE todolist.htcpp + OUTPUT_DIR template +) set(SRC todolist_printer.cpp - todolist.h) + template/todolist.h) add_executable(${PROJECT_NAME} ${SRC}) diff --git a/examples/ex_02/todolist_printer.cpp b/examples/ex_02/todolist_printer.cpp index 93a8769..9ec7e4f 100644 --- a/examples/ex_02/todolist_printer.cpp +++ b/examples/ex_02/todolist_printer.cpp @@ -1,4 +1,4 @@ -#include "todolist.h" +#include "template/todolist.h" #include #include diff --git a/examples/ex_03/CMakeLists.txt b/examples/ex_03/CMakeLists.txt index 764edc6..cc7451f 100644 --- a/examples/ex_03/CMakeLists.txt +++ b/examples/ex_03/CMakeLists.txt @@ -3,7 +3,7 @@ project(ex_03) include(../../hypertextcpp.cmake) -hypertextcpp_GenerateHeader(NAME todolist) +hypertextcpp_GenerateHeader(TEMPLATE_FILE todolist.htcpp) set(SRC todolist_printer.cpp diff --git a/examples/ex_04/CMakeLists.txt b/examples/ex_04/CMakeLists.txt index 6198576..01614c0 100644 --- a/examples/ex_04/CMakeLists.txt +++ b/examples/ex_04/CMakeLists.txt @@ -3,7 +3,10 @@ project(ex_04) include(../../hypertextcpp.cmake) -hypertextcpp_GenerateHeader(NAME todolist CLASS_NAME TodoList) +hypertextcpp_GenerateHeader( + TEMPLATE_FILE todolist.htcpp + CLASS_NAME TodoList +) set(SRC todolist_printer.cpp diff --git a/examples/ex_05/CMakeLists.txt b/examples/ex_05/CMakeLists.txt index 771cb46..f4a670f 100644 --- a/examples/ex_05/CMakeLists.txt +++ b/examples/ex_05/CMakeLists.txt @@ -14,7 +14,6 @@ target_link_libraries(${PROJECT_NAME} ${CMAKE_DL_LIBS}) include(../../hypertextcpp.cmake) hypertextcpp_BuildSharedLibrary( - NAME todolist - OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR} + TEMPLATE_FILE todolist.htcpp ) add_dependencies(${PROJECT_NAME} todolist) \ No newline at end of file diff --git a/examples/ex_06/CMakeLists.txt b/examples/ex_06/CMakeLists.txt new file mode 100644 index 0000000..61657ba --- /dev/null +++ b/examples/ex_06/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.18) +project(ex_06) + +include(../../hypertextcpp.cmake) + +hypertextcpp_GenerateHeaderAndSource( + TEMPLATE_FILE todolist.htcpp + CLASS_NAME TodoList + CONFIG_CLASS_NAME PageParams) + +set(SRC + todolist_printer.cpp + todolist.h + todolist.cpp) + +add_executable(${PROJECT_NAME} ${SRC}) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_11) +set_target_properties(${PROJECT_NAME} PROPERTIES CXX_EXTENSIONS OFF) + diff --git a/examples/ex_06/todolist.htcpp b/examples/ex_06/todolist.htcpp new file mode 100644 index 0000000..c9173f6 --- /dev/null +++ b/examples/ex_06/todolist.htcpp @@ -0,0 +1,24 @@ +#{ + #include + struct PageParams{ + struct Task{ + std::string name; + bool isCompleted = false; + }; + std::string name = "Bob"; + std::vector tasks = {{"laundry", true}, {"cooking", false}}; + }; +} + +#taskList(){ +
  • $(task.name)
  • @(auto task : cfg.tasks) +} + + +

    $(cfg.name)'s todo list:

    +

    No tasks found

    ?(cfg.tasks.empty()) +
      + $(taskList()) +
    + + diff --git a/examples/ex_06/todolist_printer.cpp b/examples/ex_06/todolist_printer.cpp new file mode 100644 index 0000000..1173e5a --- /dev/null +++ b/examples/ex_06/todolist_printer.cpp @@ -0,0 +1,11 @@ +#include "todolist.h" +#include +#include + +int main() +{ + PageParams pageParams; + auto page = TodoList{}; + page.print(pageParams); + return 0; +} diff --git a/examples/ex_07/CMakeLists.txt b/examples/ex_07/CMakeLists.txt new file mode 100644 index 0000000..33b6b0e --- /dev/null +++ b/examples/ex_07/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.18) +project(ex_07) + +include(../../hypertextcpp.cmake) + +hypertextcpp_GenerateHeaderAndSource( + TEMPLATE_FILE todolist.htcpp + CLASS_NAME TodoList + CONFIG_CLASS_NAME PageParams) + +set(SRC + todolist_printer.cpp + todolist.h + todolist.cpp) + +add_executable(${PROJECT_NAME} ${SRC}) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_11) +set_target_properties(${PROJECT_NAME} PROPERTIES CXX_EXTENSIONS OFF) + diff --git a/examples/ex_07/pageparams.h b/examples/ex_07/pageparams.h new file mode 100644 index 0000000..52e93cf --- /dev/null +++ b/examples/ex_07/pageparams.h @@ -0,0 +1,12 @@ +#pragma once +#include +#include + +struct PageParams{ + struct Task{ + std::string name; + bool isCompleted = false; + }; + std::string name = "Bob"; + std::vector tasks = {{"laundry", true}, {"cooking", false}}; +}; \ No newline at end of file diff --git a/examples/ex_07/todolist.htcpp b/examples/ex_07/todolist.htcpp new file mode 100644 index 0000000..bc5ce24 --- /dev/null +++ b/examples/ex_07/todolist.htcpp @@ -0,0 +1,16 @@ +#{ + #include "pageparams.h" +} + +#taskList(){ +
  • $(task.name)
  • @(auto task : cfg.tasks) +} + + +

    $(cfg.name)'s todo list:

    +

    No tasks found

    ?(cfg.tasks.empty()) +
      + $(taskList()) +
    + + diff --git a/examples/ex_07/todolist_printer.cpp b/examples/ex_07/todolist_printer.cpp new file mode 100644 index 0000000..1173e5a --- /dev/null +++ b/examples/ex_07/todolist_printer.cpp @@ -0,0 +1,11 @@ +#include "todolist.h" +#include +#include + +int main() +{ + PageParams pageParams; + auto page = TodoList{}; + page.print(pageParams); + return 0; +} diff --git a/hypertextcpp.cmake b/hypertextcpp.cmake index 5f4d0c1..a8c2911 100644 --- a/hypertextcpp.cmake +++ b/hypertextcpp.cmake @@ -2,79 +2,183 @@ function(hypertextcpp_GenerateHeader) cmake_parse_arguments( ARG "" - "NAME" - "FILE;CLASS_NAME;OUTPUT_DIR" + "TEMPLATE_FILE;CLASS_NAME;OUTPUT_DIR" + "" ${ARGN} ) - if (NOT ARG_NAME) - message(FATAL_ERROR "[hypertextcpp_GenerateHeader] Argument 'NAME' is missing") + if (NOT ARG_TEMPLATE_FILE) + message(FATAL_ERROR "[hypertextcpp_GenerateHeader] Argument 'TEMPLATE_FILE' is missing") endif() if (ARG_UNPARSED_ARGUMENTS) message(FATAL_ERROR "[hypertextcpp_GenerateHeader] Unsupported argument: ${ARG_UNPARSED_ARGUMENTS}") endif() - if (ARG_FILE) - set(TEMPLATE_FILE ${ARG_FILE}) + if (NOT IS_ABSOLUTE ${ARG_TEMPLATE_FILE}) + set(TEMPLATE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${ARG_TEMPLATE_FILE}) else() - set(TEMPLATE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${ARG_NAME}.htcpp) + set(TEMPLATE_FILE ${ARG_TEMPLATE_FILE}) endif() + _hypertextcppImpl_StringAfterLast(${TEMPLATE_FILE} / TEMPLATE_NAME) + _hypertextcppImpl_StringBeforeLast(${TEMPLATE_NAME} "." TEMPLATE_NAME) + + if (ARG_OUTPUT_DIR) - set(OUTPUT_DIR ${ARG_OUTPUT_DIR}) + if (NOT IS_ABSOLUTE ${ARG_OUTPUT_DIR}) + set(OUTPUT_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${ARG_OUTPUT_DIR}/${TEMPLATE_NAME}.h) + set(OUTPUT_DIR_CMDLINE_PARAM "-outputDir=${CMAKE_CURRENT_SOURCE_DIR}/${ARG_OUTPUT_DIR}") + else() + set(OUTPUT_FILE ${ARG_OUTPUT_DIR}/${TEMPLATE_NAME}.h) + set(OUTPUT_DIR_CMDLINE_PARAM "-outputDir=${ARG_OUTPUT_DIR}") + endif() else() - set(OUTPUT_DIR ${CMAKE_CURRENT_SOURCE_DIR}) + set(OUTPUT_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${TEMPLATE_NAME}.h) + set(OUTPUT_DIR_CMDLINE_PARAM "-outputDir=${CMAKE_CURRENT_SOURCE_DIR}") endif() if (ARG_CLASS_NAME) - add_custom_command( - OUTPUT ${ARG_NAME}.h - COMMAND hypertextcpp ${TEMPLATE_FILE} -o ${OUTPUT_DIR}/${ARG_NAME}.h -c ${ARG_CLASS_NAME} - DEPENDS ${TEMPLATE_FILE} - VERBATIM - ) + set(CLASS_NAME_CMDLINE_PARAM "-className=${ARG_CLASS_NAME}") + endif() + add_custom_command( + OUTPUT ${OUTPUT_FILE} + COMMAND hypertextcpp generateHeaderOnly ${TEMPLATE_FILE} ${OUTPUT_DIR_CMDLINE_PARAM} ${CLASS_NAME_CMDLINE_PARAM} + DEPENDS ${TEMPLATE_FILE} + VERBATIM + ) +endfunction() + +function(hypertextcpp_GenerateHeaderAndSource) + cmake_parse_arguments( + ARG + "" + "TEMPLATE_FILE;CLASS_NAME;OUTPUT_DIR;CONFIG_CLASS_NAME" + "" + ${ARGN} + ) + if (NOT ARG_TEMPLATE_FILE) + message(FATAL_ERROR "[hypertextcpp_GenerateHeader] Argument 'TEMPLATE_FILE' is missing") + endif() + if (NOT ARG_CONFIG_CLASS_NAME) + message(FATAL_ERROR "[hypertextcpp_GenerateHeader] Argument 'CONFIG_CLASS_NAME' is missing") + endif() + if (ARG_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "[hypertextcpp_GenerateHeader] Unsupported argument: ${ARG_UNPARSED_ARGUMENTS}") + endif() + + if (NOT IS_ABSOLUTE ${ARG_TEMPLATE_FILE}) + set(TEMPLATE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${ARG_TEMPLATE_FILE}) + else() + set(TEMPLATE_FILE ${ARG_TEMPLATE_FILE}) + endif() + + _hypertextcppImpl_StringAfterLast(${TEMPLATE_FILE} / TEMPLATE_NAME) + _hypertextcppImpl_StringBeforeLast(${TEMPLATE_NAME} "." TEMPLATE_NAME) + + + if (ARG_OUTPUT_DIR) + if (NOT IS_ABSOLUTE ${ARG_OUTPUT_DIR}) + set(OUTPUT_HEADER_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${ARG_OUTPUT_DIR}/${TEMPLATE_NAME}.h) + set(OUTPUT_SOURCE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${ARG_OUTPUT_DIR}/${TEMPLATE_NAME}.cpp) + set(OUTPUT_DIR_CMDLINE_PARAM "-outputDir=${CMAKE_CURRENT_SOURCE_DIR}/${ARG_OUTPUT_DIR}") + else() + set(OUTPUT_HEADER_FILE ${ARG_OUTPUT_DIR}/${TEMPLATE_NAME}.h) + set(OUTPUT_SOURCE_FILE ${ARG_OUTPUT_DIR}/${TEMPLATE_NAME}.cpp) + set(OUTPUT_DIR_CMDLINE_PARAM "-outputDir=${ARG_OUTPUT_DIR}") + endif() else() - add_custom_command( - OUTPUT ${ARG_NAME}.h - COMMAND hypertextcpp ${TEMPLATE_FILE} -o ${OUTPUT_DIR}/${ARG_NAME}.h - DEPENDS ${TEMPLATE_FILE} - VERBATIM - ) + _hypertextcppImpl_StringBeforeLast(${TEMPLATE_FILE} / TEMPLATE_FILE_DIR) + set(OUTPUT_HEADER_FILE ${TEMPLATE_FILE_DIR}/${TEMPLATE_NAME}.h) + set(OUTPUT_SOURCE_FILE ${TEMPLATE_FILE_DIR}/${TEMPLATE_NAME}.cpp) + set(OUTPUT_DIR_CMDLINE_PARAM "-outputDir=${TEMPLATE_FILE_DIR}") endif() + + if (ARG_CLASS_NAME) + set(CLASS_NAME_CMDLINE_PARAM "-className=${ARG_CLASS_NAME}") + endif() + add_custom_command( + OUTPUT ${OUTPUT_HEADER_FILE} ${OUTPUT_SOURCE_FILE} + COMMAND hypertextcpp generateHeaderAndSource ${TEMPLATE_FILE} -configClassName=${ARG_CONFIG_CLASS_NAME} ${OUTPUT_DIR_CMDLINE_PARAM} ${CLASS_NAME_CMDLINE_PARAM} + DEPENDS ${TEMPLATE_FILE} + VERBATIM + ) endfunction() function(hypertextcpp_BuildSharedLibrary) cmake_parse_arguments( ARG "" - "NAME" - "FILE;OUTPUT_DIR" + "TEMPLATE_FILE;OUTPUT_DIR" + "" ${ARGN} ) - if (NOT ARG_NAME) - message(FATAL_ERROR "[hypertextcpp_BuildSharedLibrary] Argument 'NAME' is missing") + if (NOT ARG_TEMPLATE_FILE) + message(FATAL_ERROR "[hypertextcpp_BuildSharedLibrary] Argument 'TEMPLATE_FILE' is missing") endif() if (ARG_UNPARSED_ARGUMENTS) message(FATAL_ERROR "[hypertextcpp_BuildSharedLibrary] Unsupported argument: ${ARG_UNPARSED_ARGUMENTS}") endif() - if (ARG_FILE) - set(TEMPLATE_FILE ${ARG_FILE}) + if (NOT IS_ABSOLUTE ${ARG_TEMPLATE_FILE}) + set(TEMPLATE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${ARG_TEMPLATE_FILE}) else() - set(TEMPLATE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${ARG_NAME}.htcpp) + set(TEMPLATE_FILE ${ARG_TEMPLATE_FILE}) + endif() + + _hypertextcppImpl_StringAfterLast(${TEMPLATE_FILE} / TEMPLATE_NAME) + _hypertextcppImpl_StringBeforeLast(${TEMPLATE_NAME} "." TEMPLATE_NAME) + + if (ARG_OUTPUT_DIR) + if (NOT IS_ABSOLUTE ${ARG_OUTPUT_DIR}) + set(OUTPUT_FILE ${CMAKE_CURRENT_BINARY_DIR}/${ARG_OUTPUT_DIR}/${TEMPLATE_NAME}.cpp) + set(OUTPUT_DIR_CMDLINE_PARAM "-outputDir=${CMAKE_CURRENT_BINARY_DIR}/${ARG_OUTPUT_DIR}") + else() + set(OUTPUT_FILE ${ARG_OUTPUT_DIR}/${TEMPLATE_NAME}.cpp) + set(OUTPUT_DIR_CMDLINE_PARAM "-outputDir=${ARG_OUTPUT_DIR}") + endif() + else() + set(OUTPUT_FILE ${CMAKE_CURRENT_BINARY_DIR}/${TEMPLATE_NAME}.cpp) + set(OUTPUT_DIR_CMDLINE_PARAM "-outputDir=${CMAKE_CURRENT_BINARY_DIR}") endif() add_custom_command( - OUTPUT ${ARG_NAME}.cpp - COMMAND hypertextcpp ${TEMPLATE_FILE} -o ${CMAKE_CURRENT_BINARY_DIR}/${ARG_NAME}.cpp -s + OUTPUT ${OUTPUT_FILE} + COMMAND hypertextcpp generateSharedLibrarySource ${TEMPLATE_FILE} ${OUTPUT_DIR_CMDLINE_PARAM} DEPENDS ${TEMPLATE_FILE} VERBATIM ) - add_library(${ARG_NAME} SHARED ${CMAKE_CURRENT_BINARY_DIR}/${ARG_NAME}.cpp) - target_compile_features(${ARG_NAME} PUBLIC cxx_std_11) - target_compile_definitions(${ARG_NAME} PUBLIC "HYPERTEXTCPP_EXPORT") - set_target_properties(${ARG_NAME} PROPERTIES CXX_EXTENSIONS OFF) + add_library(${TEMPLATE_NAME} SHARED ${OUTPUT_FILE}) + target_compile_features(${TEMPLATE_NAME} PUBLIC cxx_std_11) + target_compile_definitions(${TEMPLATE_NAME} PUBLIC "HYPERTEXTCPP_EXPORT") + set_target_properties(${TEMPLATE_NAME} PROPERTIES CXX_EXTENSIONS OFF) if (ARG_OUTPUT_DIR) - set_target_properties(${ARG_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${ARG_OUTPUT_DIR}) + set_target_properties(${TEMPLATE_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${ARG_OUTPUT_DIR}) endif() -endfunction() \ No newline at end of file +endfunction() + +function (_hypertextcppImpl_StringBeforeLast STR VALUE RESULT) + _hypertextcppImpl_StringBefore(${STR} ${VALUE} RESULT_VALUE REVERSE) + set(${RESULT} ${RESULT_VALUE} PARENT_SCOPE) +endfunction() + +function (_hypertextcppImpl_StringAfterLast STR VALUE RESULT) + _hypertextcppImpl_StringAfter(${STR} ${VALUE} RESULT_VALUE REVERSE) + set(${RESULT} ${RESULT_VALUE} PARENT_SCOPE) +endfunction() + +function(_hypertextcppImpl_StringBefore STR VALUE RESULT REVERSE) + string(FIND ${STR} ${VALUE} VALUE_POS ${REVERSE}) + string(LENGTH ${STR} STR_LENGTH) + string(SUBSTRING ${STR} 0 ${VALUE_POS} RESULT_VALUE) + set(${RESULT} ${RESULT_VALUE} PARENT_SCOPE) +endfunction() + +function (_hypertextcppImpl_StringAfter STR VALUE RESULT REVERSE) + string(FIND ${STR} ${VALUE} VALUE_POS ${REVERSE}) + string(LENGTH ${STR} STR_LENGTH) + string(LENGTH ${VALUE} VALUE_LENGTH) + MATH(EXPR RESULT_LENGTH "${STR_LENGTH} - ${VALUE_POS} - ${VALUE_LENGTH}") + MATH(EXPR RESULT_POS "${VALUE_POS} + ${VALUE_LENGTH}") + string(SUBSTRING ${STR} ${RESULT_POS} ${RESULT_LENGTH} RESULT_VALUE) + set(${RESULT} ${RESULT_VALUE} PARENT_SCOPE) +endfunction() diff --git a/src/header_and_source_transpiler_renderer.cpp b/src/header_and_source_transpiler_renderer.cpp new file mode 100644 index 0000000..f47daf8 --- /dev/null +++ b/src/header_and_source_transpiler_renderer.cpp @@ -0,0 +1,173 @@ +#include "header_and_source_transpiler_renderer.h" +#include "idocumentnoderenderer.h" +#include "procedurenode.h" +#include + +namespace htcpp { + +HeaderAndSourceTranspilerRenderer::HeaderAndSourceTranspilerRenderer( + std::string className, + std::string headerFileName, + std::string configTypeName) + : className_(std::move(className)) + , headerFileName_(std::move(headerFileName)) + , configTypeName_(std::move(configTypeName)) +{ +} + +namespace { + +std::string generateHeader( + const std::string& className, + const std::string& configTypeName, + const std::vector>& globalStatements, + const std::vector>& procedures) +{ + auto globalStatementsCode = std::string{}; + for (const auto& globalStatement : globalStatements) + globalStatementsCode += globalStatement->renderingCode() + "\n"; + + auto proceduresDeclarations = std::string{}; + for (const auto& procedure : procedures) + proceduresDeclarations += " std::string " + procedure->name() + "() const;\n"; + + return fmt::format( + R"( +#pragma once +#include +#include +#include + +{globalStatementsCode} + +class {className} {{ +private: +class Renderer{{ + const {configTypeName}& cfg; + std::ostream& out; + {proceduresDeclarations} + +public: + Renderer(const {configTypeName}& cfg, std::ostream& out); + void renderHTML() const; + void renderHTMLPart(const std::string& name) const; +}}; +public: + std::string render(const {configTypeName}& cfg) const; + std::string render(const std::string& renderFuncName, const {configTypeName}& cfg) const; + void print(const {configTypeName}& cfg) const; + void print(const std::string& renderFuncName, const {configTypeName}& cfg) const; + void print(const {configTypeName}& cfg, std::ostream& stream) const; + void print(const std::string& renderFuncName, const {configTypeName}& cfg, std::ostream& stream) const; +}};)", + fmt::arg("className", className), + fmt::arg("configTypeName", configTypeName), + fmt::arg("globalStatementsCode", globalStatementsCode), + fmt::arg("proceduresDeclarations", proceduresDeclarations)); +} + +std::string generateSource( + const std::string& headerFileName, + const std::string& className, + const std::string& configTypeName, + const std::vector>& procedures, + const std::vector>& nodes) +{ + auto proceduresImplementations = std::string{}; + for (const auto& procedure : procedures) { + proceduresImplementations += "std::string " + className + "::Renderer::" + procedure->name() + "() const{\n"; + proceduresImplementations += procedure->renderingCode() + "\n return {};\n}\n"; + } + + auto templateHtmlRenderingCode = std::string{}; + for (const auto& node : nodes) + templateHtmlRenderingCode += node->renderingCode(); + + auto procedureInvocations = std::string{}; + for (const auto& procedure : procedures) { + procedureInvocations += "if (name == \"" + procedure->name() + "\")\n"; + procedureInvocations += " " + procedure->name() + "();\n"; + } + + return fmt::format( + R"( +#include "{headerFileName}" + +{procedureImplementations} + +{className}::Renderer::Renderer(const {configTypeName}& cfg, std::ostream& out) + : cfg(cfg), out(out) +{{}} + +void {className}::Renderer::renderHTML() const +{{ +{templateHtmlRenderingCode} +}} + +void {className}::Renderer::renderHTMLPart(const std::string& name) const +{{ + static_cast(name); +{procedureInvocations} +}} + +std::string {className}::render(const {configTypeName}& cfg) const +{{ + auto stream = std::stringstream{{}}; + auto renderer = Renderer{{cfg, stream}}; + renderer.renderHTML(); + return stream.str(); +}} + +std::string {className}::render(const std::string& renderFuncName, const {configTypeName}& cfg) const +{{ + auto stream = std::stringstream{{}}; + auto renderer = Renderer{{cfg, stream}}; + renderer.renderHTMLPart(renderFuncName); + return stream.str(); +}} + +void {className}::print(const {configTypeName}& cfg) const +{{ + auto renderer = Renderer{{cfg, std::cout}}; + renderer.renderHTML(); +}} + +void {className}::print(const std::string& renderFuncName, const {configTypeName}& cfg) const +{{ + auto renderer = Renderer{{cfg, std::cout}}; + renderer.renderHTMLPart(renderFuncName); +}} + +void {className}::print(const {configTypeName}& cfg, std::ostream& stream) const +{{ + auto renderer = Renderer{{cfg, stream}}; + renderer.renderHTML(); +}} + +void {className}::print(const std::string& renderFuncName, const {configTypeName}& cfg, std::ostream& stream) const +{{ + auto renderer = Renderer{{cfg, stream}}; + renderer.renderHTMLPart(renderFuncName); +}} +)", + fmt::arg("headerFileName", headerFileName), + fmt::arg("procedureImplementations", proceduresImplementations), + fmt::arg("className", className), + fmt::arg("configTypeName", configTypeName), + fmt::arg("templateHtmlRenderingCode", templateHtmlRenderingCode), + fmt::arg("procedureInvocations", procedureInvocations)); +} + +} //namespace + +std::unordered_map HeaderAndSourceTranspilerRenderer::generateCode( + const std::vector>& globalStatements, + const std::vector>& procedures, + const std::vector>& nodes) const +{ + return {{GeneratedFileType::Header, generateHeader(className_, configTypeName_, globalStatements, procedures)}, + {GeneratedFileType::Source, + generateSource(headerFileName_, className_, configTypeName_, procedures, nodes)}}; +} + +} //namespace htcpp diff --git a/src/header_and_source_transpiler_renderer.h b/src/header_and_source_transpiler_renderer.h new file mode 100644 index 0000000..5c21b1a --- /dev/null +++ b/src/header_and_source_transpiler_renderer.h @@ -0,0 +1,22 @@ +#pragma once +#include "itranspiler_renderer.h" +#include "procedurenode.h" + +namespace htcpp { + +class HeaderAndSourceTranspilerRenderer : public ITranspilerRenderer { +public: + HeaderAndSourceTranspilerRenderer(std::string className, std::string headerFileName, std::string configTypeName); + + std::unordered_map generateCode( + const std::vector>& globalStatements, + const std::vector>& procedures, + const std::vector>& nodes) const override; + +private: + std::string className_; + std::string headerFileName_; + std::string configTypeName_; +}; + +} //namespace htcpp diff --git a/src/main.cpp b/src/main.cpp index 1528d92..e7e3e8f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,10 +1,14 @@ #include "errors.h" +#include "header_and_source_transpiler_renderer.h" #include "nameutils.h" #include "shared_lib_transpiler_renderer.h" #include "single_header_transpiler_renderer.h" #include "transpiler.h" #include #include +#include +#include +#include #include #include #include @@ -13,31 +17,77 @@ namespace fs = std::filesystem; namespace { -struct Cfg : public cmdlime::Config { +struct HeaderOnlyResultModeCfg : public cmdlime::Config { + CMDLIME_ARG(input, fs::path) << ".htcpp file to transpile"; + CMDLIME_PARAM(outputDir, fs::path)() << "output dir\n(if empty, current working directory is used)"; + CMDLIME_PARAM(className, std::string)() << "generated class name\n(if empty, input file name is used)"; +}; + +struct SharedLibrarySourceResultModeCfg : public cmdlime::Config { + CMDLIME_ARG(input, fs::path) << ".htcpp file to transpile"; + CMDLIME_PARAM(outputDir, fs::path)() << "output dir\n(if empty, current working directory is used)"; +}; + +struct HeaderAndSourceResultModeCfg : public cmdlime::Config { CMDLIME_ARG(input, fs::path) << ".htcpp file to transpile"; - CMDLIME_PARAM(output, fs::path)() << "output c++ file path\n(if empty, current working directory is used)"; - CMDLIME_PARAM(className, std::string)() << "generated class name"; - CMDLIME_FLAG(sharedLib) << "generate result as shared library source files"; - CMDLIME_FLAG(classPascalcase) << cmdlime::WithoutShortName{} - << "generate class name by using .htcpp filename in PascalCase"; - CMDLIME_FLAG(classSnakecase) << cmdlime::WithoutShortName{} - << "generate class name by using .htcpp filename in snake_case"; - CMDLIME_FLAG(classLowercase) << cmdlime::WithoutShortName{} - << "generate class name by using .htcpp filename in lowercase"; + CMDLIME_PARAM(outputDir, fs::path)() << "output dir\n(if empty, current working directory is used)"; + CMDLIME_PARAM(className, std::string)() << "generated class name\n(if empty, input file name is used)"; + CMDLIME_PARAM(configClassName, std::string) << "config class name"; }; -fs::path getOutputFilePath(const Cfg& cfg, htcpp::GeneratedFileType fileType); -std::string getClassName(const Cfg& cfg); -std::unique_ptr makeTranspilerRenderer(const Cfg& cfg); + +struct Cfg : public cmdlime::Config { + CMDLIME_COMMAND(generateHeaderOnly, HeaderOnlyResultModeCfg) << "generate header only file"; + CMDLIME_COMMAND(generateSharedLibrarySource, SharedLibrarySourceResultModeCfg) << "generate shared library source file"; + CMDLIME_COMMAND(generateHeaderAndSource, HeaderAndSourceResultModeCfg) << "generate header and source files"; +}; + +using CommandCfg = + std::variant; + +std::unique_ptr makeTranspilerRenderer(const CommandCfg& cfg); +std::variant getCommand( + const Cfg& cfg); + +std::string getClassName(const auto& commandCfg) +{ + const auto& className = commandCfg.className; + if (className.empty()) + return commandCfg.input.stem().string(); + + return className; +} + +fs::path getOutputFilePath(const CommandCfg& command, htcpp::GeneratedFileType fileType); + } //namespace +namespace cmdlime { +template<> +struct PostProcessor { + void operator()(Cfg& cfg) + { + if (!cfg.generateHeaderOnly.has_value() && !cfg.generateHeaderAndSource.has_value() && + !cfg.generateSharedLibrarySource.has_value()) + throw cmdlime::ValidationError{"At least one command must be specified"}; + } +}; +} //namespace cmdlime + int run(const Cfg& cfg) { - auto transpilerRenderer = makeTranspilerRenderer(cfg); + const auto command = getCommand(cfg); + auto transpilerRenderer = makeTranspilerRenderer(command); auto transpiler = htcpp::Transpiler{*transpilerRenderer}; try { - const auto result = transpiler.process(cfg.input); + const auto input = std::visit( + [](const auto& cfg) + { + return cfg.input; + }, + command); + const auto result = transpiler.process(input); for (const auto& [fileType, code] : result) { - auto stream = std::ofstream{getOutputFilePath(cfg, fileType)}; + auto stream = std::ofstream{getOutputFilePath(command, fileType)}; stream << code; } } @@ -54,49 +104,75 @@ int run(const Cfg& cfg) int main(int argc, char** argv) { - return cmdlime::CommandLineReader{"hypertextcpp"}.exec(argc, argv, run); + return cmdlime::CommandLineReader{"hypertextcpp"}.exec(argc, argv, run); } namespace { -fs::path getOutputFilePath(const Cfg& cfg, htcpp::GeneratedFileType fileType) + +fs::path getOutputFilePath(const CommandCfg& command, htcpp::GeneratedFileType fileType) { + const auto inputFile = std::visit( + [](const auto& cfg) + { + return cfg.input; + }, + command); + const auto outputDir = std::visit( + [](const auto& cfg) + { + return cfg.outputDir; + }, + command); + auto path = fs::current_path(); - if (!cfg.output.empty()) { - if (cfg.output.is_absolute()) - path = cfg.output; + if (!outputDir.empty()) { + if (outputDir.is_absolute()) + path = outputDir; else - path /= cfg.output; - } - else { - if (fileType == htcpp::GeneratedFileType::Source) - path /= (cfg.input.stem().string() + ".cpp"); - else if (fileType == htcpp::GeneratedFileType::Header) - path /= (cfg.input.stem().string() + ".h"); + path /= outputDir; } + + if (fileType == htcpp::GeneratedFileType::Source) + path /= (inputFile.stem().string() + ".cpp"); + else if (fileType == htcpp::GeneratedFileType::Header) + path /= (inputFile.stem().string() + ".h"); + return path; } -std::string getClassName(const Cfg& cfg) +std::unique_ptr makeTranspilerRenderer(const CommandCfg& cfg) { - auto result = cfg.className; - if (result.empty()) { - result = cfg.input.stem().string(); - if (cfg.classPascalcase) - result = htcpp::utils::toPascalCase(result); - if (cfg.classSnakecase) - result = htcpp::utils::toSnakeCase(result); - if (cfg.classLowercase) - result = htcpp::utils::toLowerCase(result); - } - return result; + return std::visit( + sfun::overloaded{ + [](const HeaderOnlyResultModeCfg& commandCfg) -> std::unique_ptr + { + return std::make_unique(getClassName(commandCfg)); + }, + [](const SharedLibrarySourceResultModeCfg&) -> std::unique_ptr + { + return std::make_unique(); + }, + [](const HeaderAndSourceResultModeCfg& commandCfg) -> std::unique_ptr + { + return std::make_unique( + getClassName(commandCfg), + getOutputFilePath(commandCfg, htcpp::GeneratedFileType::Header).filename().string(), + commandCfg.configClassName); + }}, + cfg); } -std::unique_ptr makeTranspilerRenderer(const Cfg& cfg) +std::variant getCommand( + const Cfg& cfg) { - if (cfg.sharedLib) - return std::make_unique(); - else - return std::make_unique(getClassName(cfg)); + if (cfg.generateHeaderOnly.has_value()) + return cfg.generateHeaderOnly.value(); + if (cfg.generateSharedLibrarySource.has_value()) + return cfg.generateSharedLibrarySource.value(); + if (cfg.generateHeaderAndSource.has_value()) + return cfg.generateHeaderAndSource.value(); + + sfun::unreachable(); } } //namespace