commit 0641a95770cd20be20a1b2702cd96b33a6a466fb Author: speedie Date: Sun May 5 23:31:42 2024 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7194ea7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.cache +build diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b8ef161 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,83 @@ +cmake_minimum_required(VERSION 3.1...3.29) + +project(docpp LANGUAGES CXX VERSION 0.0.1) + +if (MSVC) + set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) +endif() + +set_property(GLOBAL PROPERTY CXX_STANDARD 17) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(${PROJECT_NAME} SHARED) + +target_sources(${PROJECT_NAME} PRIVATE + "src/docpp.cpp" +) + +target_include_directories(${PROJECT_NAME} PRIVATE "${PROJECT_SOURCE_DIR}") + +set_target_properties(${PROJECT_NAME} PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION} + PUBLIC_HEADER "include/docpp.hpp" +) + +install(TARGETS ${PROJECT_NAME} + EXPORT ${PROJECT_NAME}Targets + FILE_SET HEADERS + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib + RUNTIME DESTINATION bin + PUBLIC_HEADER DESTINATION include/docpp + INCLUDES DESTINATION include) + +set(INCLUDE_INSTALL_DIR include/) + +include(CMakePackageConfigHelpers) +write_basic_package_version_file( + "${PROJECT_NAME}ConfigVersion.cmake" + VERSION ${PROJECT_VERSION} + COMPATIBILITY AnyNewerVersion) + +install(EXPORT ${PROJECT_NAME}Targets + FILE ${PROJECT_NAME}Targets.cmake + NAMESPACE docpp:: + DESTINATION lib/cmake/${PROJECT_NAME}) + +install(FILES "${PROJECT_SOURCE_DIR}/cmake/${PROJECT_NAME}Config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" + DESTINATION lib/cmake/${PROJECT_NAME}) + +if (!${ENABLE_TESTS}) + return() +else() + enable_testing() +endif() + +find_package(Catch2 3 REQUIRED) + +add_executable(${PROJECT_NAME}_test + tests/test.cpp +) + +target_include_directories(${PROJECT_NAME}_test PRIVATE + "${PROJECT_SOURCE_DIR}" +) + +target_link_libraries(${PROJECT_NAME}_test PRIVATE + Catch2::Catch2WithMain +) + +include(CTest) +include(Catch) +catch_discover_tests(${PROJECT_NAME}_test) + +add_custom_command( + TARGET ${PROJECT_NAME}_test + COMMENT "Run tests" + POST_BUILD + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure --rerun-failed -C + $ -R "^${PROJECT_NAME}_test$" +) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0a04128 --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/README.md b/README.md new file mode 100644 index 0000000..14cf185 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# docpp + +Small C++ library for generating XML, HTML and CSS. + +## Installation + +To install the library, you can utilize the provided CMakeLists.txt file: + +```sh +mkdir build +cd build +cmake .. +cmake --build . +cmake --install . --prefix /usr +``` + +## Usage + +Just include docpp.hpp in your project and link against the library. Examples can be found in the examples directory. + +## License + +This project is licensed under the GNU Lesser General Public License v3.0 - see the [LICENSE.md](LICENSE.md) file for details. + +## Code of Conduct + +None. Just don't blow up my house. diff --git a/cmake/Catch.cmake b/cmake/Catch.cmake new file mode 100644 index 0000000..8f30688 --- /dev/null +++ b/cmake/Catch.cmake @@ -0,0 +1,304 @@ +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file Copyright.txt or https://cmake.org/licensing for details. + +#[=======================================================================[.rst: +Catch +----- + +This module defines a function to help use the Catch test framework. + +The :command:`catch_discover_tests` discovers tests by asking the compiled test +executable to enumerate its tests. This does not require CMake to be re-run +when tests change. However, it may not work in a cross-compiling environment, +and setting test properties is less convenient. + +This command is intended to replace use of :command:`add_test` to register +tests, and will create a separate CTest test for each Catch test case. Note +that this is in some cases less efficient, as common set-up and tear-down logic +cannot be shared by multiple test cases executing in the same instance. +However, it provides more fine-grained pass/fail information to CTest, which is +usually considered as more beneficial. By default, the CTest test name is the +same as the Catch name; see also ``TEST_PREFIX`` and ``TEST_SUFFIX``. + +.. command:: catch_discover_tests + + Automatically add tests with CTest by querying the compiled test executable + for available tests:: + + catch_discover_tests(target + [TEST_SPEC arg1...] + [EXTRA_ARGS arg1...] + [WORKING_DIRECTORY dir] + [TEST_PREFIX prefix] + [TEST_SUFFIX suffix] + [PROPERTIES name1 value1...] + [TEST_LIST var] + [REPORTER reporter] + [OUTPUT_DIR dir] + [OUTPUT_PREFIX prefix] + [OUTPUT_SUFFIX suffix] + [DISCOVERY_MODE ] + ) + + ``catch_discover_tests`` sets up a post-build command on the test executable + that generates the list of tests by parsing the output from running the test + with the ``--list-test-names-only`` argument. This ensures that the full + list of tests is obtained. Since test discovery occurs at build time, it is + not necessary to re-run CMake when the list of tests changes. + However, it requires that :prop_tgt:`CROSSCOMPILING_EMULATOR` is properly set + in order to function in a cross-compiling environment. + + Additionally, setting properties on tests is somewhat less convenient, since + the tests are not available at CMake time. Additional test properties may be + assigned to the set of tests as a whole using the ``PROPERTIES`` option. If + more fine-grained test control is needed, custom content may be provided + through an external CTest script using the :prop_dir:`TEST_INCLUDE_FILES` + directory property. The set of discovered tests is made accessible to such a + script via the ``_TESTS`` variable. + + The options are: + + ``target`` + Specifies the Catch executable, which must be a known CMake executable + target. CMake will substitute the location of the built executable when + running the test. + + ``TEST_SPEC arg1...`` + Specifies test cases, wildcarded test cases, tags and tag expressions to + pass to the Catch executable with the ``--list-test-names-only`` argument. + + ``EXTRA_ARGS arg1...`` + Any extra arguments to pass on the command line to each test case. + + ``WORKING_DIRECTORY dir`` + Specifies the directory in which to run the discovered test cases. If this + option is not provided, the current binary directory is used. + + ``TEST_PREFIX prefix`` + Specifies a ``prefix`` to be prepended to the name of each discovered test + case. This can be useful when the same test executable is being used in + multiple calls to ``catch_discover_tests()`` but with different + ``TEST_SPEC`` or ``EXTRA_ARGS``. + + ``TEST_SUFFIX suffix`` + Similar to ``TEST_PREFIX`` except the ``suffix`` is appended to the name of + every discovered test case. Both ``TEST_PREFIX`` and ``TEST_SUFFIX`` may + be specified. + + ``PROPERTIES name1 value1...`` + Specifies additional properties to be set on all tests discovered by this + invocation of ``catch_discover_tests``. + + ``TEST_LIST var`` + Make the list of tests available in the variable ``var``, rather than the + default ``_TESTS``. This can be useful when the same test + executable is being used in multiple calls to ``catch_discover_tests()``. + Note that this variable is only available in CTest. + + ``REPORTER reporter`` + Use the specified reporter when running the test case. The reporter will + be passed to the Catch executable as ``--reporter reporter``. + + ``OUTPUT_DIR dir`` + If specified, the parameter is passed along as + ``--out dir/`` to Catch executable. The actual file name is the + same as the test name. This should be used instead of + ``EXTRA_ARGS --out foo`` to avoid race conditions writing the result output + when using parallel test execution. + + ``OUTPUT_PREFIX prefix`` + May be used in conjunction with ``OUTPUT_DIR``. + If specified, ``prefix`` is added to each output file name, like so + ``--out dir/prefix``. + + ``OUTPUT_SUFFIX suffix`` + May be used in conjunction with ``OUTPUT_DIR``. + If specified, ``suffix`` is added to each output file name, like so + ``--out dir/suffix``. This can be used to add a file extension to + the output e.g. ".xml". + + ``DL_PATHS path...`` + Specifies paths that need to be set for the dynamic linker to find shared + libraries/DLLs when running the test executable (PATH/LD_LIBRARY_PATH respectively). + These paths will both be set when retrieving the list of test cases from the + test executable and when the tests are executed themselves. This requires + cmake/ctest >= 3.22. + + `DISCOVERY_MODE mode`` + Provides control over when ``catch_discover_tests`` performs test discovery. + By default, ``POST_BUILD`` sets up a post-build command to perform test discovery + at build time. In certain scenarios, like cross-compiling, this ``POST_BUILD`` + behavior is not desirable. By contrast, ``PRE_TEST`` delays test discovery until + just prior to test execution. This way test discovery occurs in the target environment + where the test has a better chance at finding appropriate runtime dependencies. + + ``DISCOVERY_MODE`` defaults to the value of the + ``CMAKE_CATCH_DISCOVER_TESTS_DISCOVERY_MODE`` variable if it is not passed when + calling ``catch_discover_tests``. This provides a mechanism for globally selecting + a preferred test discovery behavior without having to modify each call site. + +#]=======================================================================] + +#------------------------------------------------------------------------------ +function(catch_discover_tests TARGET) + + cmake_parse_arguments( + "" + "" + "TEST_PREFIX;TEST_SUFFIX;WORKING_DIRECTORY;TEST_LIST;REPORTER;OUTPUT_DIR;OUTPUT_PREFIX;OUTPUT_SUFFIX;DISCOVERY_MODE" + "TEST_SPEC;EXTRA_ARGS;PROPERTIES;DL_PATHS" + ${ARGN} + ) + + if(NOT _WORKING_DIRECTORY) + set(_WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") + endif() + if(NOT _TEST_LIST) + set(_TEST_LIST ${TARGET}_TESTS) + endif() + if (_DL_PATHS) + if(${CMAKE_VERSION} VERSION_LESS "3.22.0") + message(FATAL_ERROR "The DL_PATHS option requires at least cmake 3.22") + endif() + endif() + if(NOT _DISCOVERY_MODE) + if(NOT CMAKE_CATCH_DISCOVER_TESTS_DISCOVERY_MODE) + set(CMAKE_CATCH_DISCOVER_TESTS_DISCOVERY_MODE "POST_BUILD") + endif() + set(_DISCOVERY_MODE ${CMAKE_CATCH_DISCOVER_TESTS_DISCOVERY_MODE}) + endif() + if (NOT _DISCOVERY_MODE MATCHES "^(POST_BUILD|PRE_TEST)$") + message(FATAL_ERROR "Unknown DISCOVERY_MODE: ${_DISCOVERY_MODE}") + endif() + + ## Generate a unique name based on the extra arguments + string(SHA1 args_hash "${_TEST_SPEC} ${_EXTRA_ARGS} ${_REPORTER} ${_OUTPUT_DIR} ${_OUTPUT_PREFIX} ${_OUTPUT_SUFFIX}") + string(SUBSTRING ${args_hash} 0 7 args_hash) + + # Define rule to generate test list for aforementioned test executable + set(ctest_file_base "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}-${args_hash}") + set(ctest_include_file "${ctest_file_base}_include.cmake") + set(ctest_tests_file "${ctest_file_base}_tests.cmake") + + get_property(crosscompiling_emulator + TARGET ${TARGET} + PROPERTY CROSSCOMPILING_EMULATOR + ) + + if(_DISCOVERY_MODE STREQUAL "POST_BUILD") + add_custom_command( + TARGET ${TARGET} POST_BUILD + BYPRODUCTS "${ctest_tests_file}" + COMMAND "${CMAKE_COMMAND}" + -D "TEST_TARGET=${TARGET}" + -D "TEST_EXECUTABLE=$" + -D "TEST_EXECUTOR=${crosscompiling_emulator}" + -D "TEST_WORKING_DIR=${_WORKING_DIRECTORY}" + -D "TEST_SPEC=${_TEST_SPEC}" + -D "TEST_EXTRA_ARGS=${_EXTRA_ARGS}" + -D "TEST_PROPERTIES=${_PROPERTIES}" + -D "TEST_PREFIX=${_TEST_PREFIX}" + -D "TEST_SUFFIX=${_TEST_SUFFIX}" + -D "TEST_LIST=${_TEST_LIST}" + -D "TEST_REPORTER=${_REPORTER}" + -D "TEST_OUTPUT_DIR=${_OUTPUT_DIR}" + -D "TEST_OUTPUT_PREFIX=${_OUTPUT_PREFIX}" + -D "TEST_OUTPUT_SUFFIX=${_OUTPUT_SUFFIX}" + -D "TEST_DL_PATHS=${_DL_PATHS}" + -D "CTEST_FILE=${ctest_tests_file}" + -P "${_CATCH_DISCOVER_TESTS_SCRIPT}" + VERBATIM + ) + + file(WRITE "${ctest_include_file}" + "if(EXISTS \"${ctest_tests_file}\")\n" + " include(\"${ctest_tests_file}\")\n" + "else()\n" + " add_test(${TARGET}_NOT_BUILT-${args_hash} ${TARGET}_NOT_BUILT-${args_hash})\n" + "endif()\n" + ) + + elseif(_DISCOVERY_MODE STREQUAL "PRE_TEST") + + get_property(GENERATOR_IS_MULTI_CONFIG GLOBAL + PROPERTY GENERATOR_IS_MULTI_CONFIG + ) + + if(GENERATOR_IS_MULTI_CONFIG) + set(ctest_tests_file "${ctest_file_base}_tests-$.cmake") + endif() + + string(CONCAT ctest_include_content + "if(EXISTS \"$\")" "\n" + " if(NOT EXISTS \"${ctest_tests_file}\" OR" "\n" + " NOT \"${ctest_tests_file}\" IS_NEWER_THAN \"$\" OR\n" + " NOT \"${ctest_tests_file}\" IS_NEWER_THAN \"\${CMAKE_CURRENT_LIST_FILE}\")\n" + " include(\"${_CATCH_DISCOVER_TESTS_SCRIPT}\")" "\n" + " catch_discover_tests_impl(" "\n" + " TEST_EXECUTABLE" " [==[" "$" "]==]" "\n" + " TEST_EXECUTOR" " [==[" "${crosscompiling_emulator}" "]==]" "\n" + " TEST_WORKING_DIR" " [==[" "${_WORKING_DIRECTORY}" "]==]" "\n" + " TEST_SPEC" " [==[" "${_TEST_SPEC}" "]==]" "\n" + " TEST_EXTRA_ARGS" " [==[" "${_EXTRA_ARGS}" "]==]" "\n" + " TEST_PROPERTIES" " [==[" "${_PROPERTIES}" "]==]" "\n" + " TEST_PREFIX" " [==[" "${_TEST_PREFIX}" "]==]" "\n" + " TEST_SUFFIX" " [==[" "${_TEST_SUFFIX}" "]==]" "\n" + " TEST_LIST" " [==[" "${_TEST_LIST}" "]==]" "\n" + " TEST_REPORTER" " [==[" "${_REPORTER}" "]==]" "\n" + " TEST_OUTPUT_DIR" " [==[" "${_OUTPUT_DIR}" "]==]" "\n" + " TEST_OUTPUT_PREFIX" " [==[" "${_OUTPUT_PREFIX}" "]==]" "\n" + " TEST_OUTPUT_SUFFIX" " [==[" "${_OUTPUT_SUFFIX}" "]==]" "\n" + " CTEST_FILE" " [==[" "${ctest_tests_file}" "]==]" "\n" + " TEST_DL_PATHS" " [==[" "${_DL_PATHS}" "]==]" "\n" + " CTEST_FILE" " [==[" "${CTEST_FILE}" "]==]" "\n" + " )" "\n" + " endif()" "\n" + " include(\"${ctest_tests_file}\")" "\n" + "else()" "\n" + " add_test(${TARGET}_NOT_BUILT ${TARGET}_NOT_BUILT)" "\n" + "endif()" "\n" + ) + + if(GENERATOR_IS_MULTI_CONFIG) + foreach(_config ${CMAKE_CONFIGURATION_TYPES}) + file(GENERATE OUTPUT "${ctest_file_base}_include-${_config}.cmake" CONTENT "${ctest_include_content}" CONDITION $) + endforeach() + string(CONCAT ctest_include_multi_content + "if(NOT CTEST_CONFIGURATION_TYPE)" "\n" + " message(\"No configuration for testing specified, use '-C '.\")" "\n" + "else()" "\n" + " include(\"${ctest_file_base}_include-\${CTEST_CONFIGURATION_TYPE}.cmake\")" "\n" + "endif()" "\n" + ) + file(GENERATE OUTPUT "${ctest_include_file}" CONTENT "${ctest_include_multi_content}") + else() + file(GENERATE OUTPUT "${ctest_file_base}_include.cmake" CONTENT "${ctest_include_content}") + file(WRITE "${ctest_include_file}" "include(\"${ctest_file_base}_include.cmake\")") + endif() + endif() + + if(NOT ${CMAKE_VERSION} VERSION_LESS "3.10.0") + # Add discovered tests to directory TEST_INCLUDE_FILES + set_property(DIRECTORY + APPEND PROPERTY TEST_INCLUDE_FILES "${ctest_include_file}" + ) + else() + # Add discovered tests as directory TEST_INCLUDE_FILE if possible + get_property(test_include_file_set DIRECTORY PROPERTY TEST_INCLUDE_FILE SET) + if (NOT ${test_include_file_set}) + set_property(DIRECTORY + PROPERTY TEST_INCLUDE_FILE "${ctest_include_file}" + ) + else() + message(FATAL_ERROR "Cannot set more than one TEST_INCLUDE_FILE") + endif() + endif() + +endfunction() + +############################################################################### + +set(_CATCH_DISCOVER_TESTS_SCRIPT + ${CMAKE_CURRENT_LIST_DIR}/CatchAddTests.cmake + CACHE INTERNAL "Catch2 full path to CatchAddTests.cmake helper file" +) diff --git a/cmake/docppConfig.cmake b/cmake/docppConfig.cmake new file mode 100644 index 0000000..bcaafdf --- /dev/null +++ b/cmake/docppConfig.cmake @@ -0,0 +1,2 @@ +include(CMakeFindDependencyMacro) +include(${CMAKE_CURRENT_LIST_DIR}/docppTargets.cmake) diff --git a/compile_commands.json b/compile_commands.json new file mode 120000 index 0000000..25eb4b2 --- /dev/null +++ b/compile_commands.json @@ -0,0 +1 @@ +build/compile_commands.json \ No newline at end of file diff --git a/examples/hello-world.cpp b/examples/hello-world.cpp new file mode 100644 index 0000000..7844e18 --- /dev/null +++ b/examples/hello-world.cpp @@ -0,0 +1,116 @@ +// Compile with: g++ hello-world.cpp -o hello-world -ldocpp +#include +#include +#include + +int main() { + /* This is your root document. It can hold *one* HTML section, and that section can hold any number of elements and/or sections. + * By default, the root document will prepend a doctype declaration. If you don't want that (e.g., if you're writing XML), you can + * use docpp::HTML::HTMLDocument::setDocType() to set the doctype to your preferred value. + * + * To get the document as an std::string object, call doc.get(). + */ + docpp::HTML::HTMLDocument doc{}; + + /* This is an HTML section. It can hold any number of elements and/or sections. + * The first argument is the type of section, and this can either be a predefined value (e.g., docpp::HTML::SECTION_HTML) or a + * custom value in the form of an std::string object. + * + * The second argument is an HTMLElementProperties object, which is a collection of HTMLProperty objects. Each property is a std::pair of an + * attribute name and an attribute value. If you don't want to specify any attributes, you can pass an empty HTMLElementAttributes object. + * If you need to change the tag and/or attributes later, you can use the set() method. + * + * Note that it is very important that you do not append a section or element to a section until you have finished its construction, + * because push_back() makes a copy of the object you pass to it. If you need to make changes later, you can use methods such as find() and swap(), + * or construct a new object. + * + * To get the section as an std::string object, call section.get(). + */ + docpp::HTML::HTMLSection htmlSection(docpp::HTML::SECTION_HTML, {}); // + docpp::HTML::HTMLSection headSection(docpp::HTML::SECTION_HEAD, {}); // + docpp::HTML::HTMLSection bodySection(docpp::HTML::SECTION_BODY, {}); // + docpp::HTML::HTMLSection footerSection(docpp::HTML::SECTION_FOOTER, {}); //
+ + /* This is an HTML element. Unlike a section, an element cannot hold any other elements or sections, rather it holds text and/or attributes. + * The first argument is the type of element, and this should simply be the tag name (e.g., "p", "h1", "a", etc.). + * + * The second argument is an HTMLElementProperties object, which is a collection of HTMLProperty objects. Each property is a std::pair of an + * attribute name and an attribute value. If you don't want to specify any attributes, you can pass an empty HTMLElementAttributes object. + * If you need to change the element's tag, attributes, type or text later, you can use the set() method. + * + * The third argument is the text content of the element. If you don't want to specify any text, you can pass an empty std::string object. + * + * The fourth argument is an integer representing the closing tag type. This can be one of the following: + * + * - docpp::HTML::TYPE_NON_CLOSED: No closing tag will be appended. + * Example: + * - docpp::HTML::TYPE_NON_SELF_CLOSING: A closing tag will be appended. + * Example:

Hello world

+ * - docpp::HTML::TYPE_SELF_CLOSING: A self-closing tag will be appended. + * Example: + * + * To get the element as an std::string object, call element.get(). + */ + docpp::HTML::HTMLElement titleElement("title", {}, "Hello world document"); // Hello world document + + /* Add the title and meta elements to the head section. */ + headSection.push_back(titleElement); + headSection.push_back(docpp::HTML::HTMLElement("meta", {{docpp::HTML::HTMLProperty("name", "description"), docpp::HTML::HTMLProperty("content", "Hello world document description!")}}, "", docpp::HTML::TYPE_NON_CLOSED)); + + /* This is a CSS document. It is essentially the CSS equivalent of an HTML section. + * It is essentially a collection of CSSElement objects, which is a collection of CSSProperty objects. + * + * In other words, a CSS document is a collection of CSS elements, which are collections of CSS properties. + */ + docpp::CSS::CSSStylesheet stylesheet{}; + + /* This is a CSS element. It is essentially a collection of CSS properties. + * The first argument is the type of element, and this should simply be the tag name (e.g., "body", "p", "h1", etc.). + * + * The second argument is a CSSProperties object, which is a collection of CSSProperty objects. Each property is a std::pair of a + * property name and a property value. If you don't want to specify any properties, you can pass an empty CSSProperties object, but... of course, that's not very useful. + * + * To get the element as an std::string object, call element.get(). + */ + docpp::CSS::CSSElement bodyStyling("body", {{docpp::CSS::CSSProperty("background-color", "black"), docpp::CSS::CSSProperty("color", "white")}}); // body { background-color: black; color: white; } + + /* Now, let's add the body style to the stylesheet. */ + stylesheet.push_back(bodyStyling); + + /* To get the stylesheet as an std::string object, call stylesheet.get(). It can then be used in an HTMLElement object. */ + const std::string& css = stylesheet.get(); // body { background-color: black; color: white; } + + headSection.push_back(docpp::HTML::HTMLElement("style", {}, css)); // + + /* Add a paragraph element to the body section. */ + bodySection.push_back(docpp::HTML::HTMLElement("p", {}, "Hello, world!")); //

Hello, world!

+ + /* Likewise, add a paragraph element to the footer section. */ + footerSection.push_back(docpp::HTML::HTMLElement("p", {}, "This is the footer.")); //

This is the footer.

+ + /* Now, let's add the header, body and footer section to the html section. + * The order does matter, because an identifier is used internally. You can get this identifier by e.g. using find(). + */ + htmlSection.push_back(headSection); // ... + htmlSection.push_back(bodySection); // ...... + htmlSection.push_back(footerSection); // ......
...
+ + /* Now, let's add the html section to the document. */ + doc.set(htmlSection); + + /* Finally, let's output the document to a file and print it to standard output. */ + std::ofstream file("hello-world.html"); + + /* Optionally, you can use the get() method with the docpp::HTML::FORMATTING_PRETTY argument to get a *slightly* more readable document. + * It still doesn't look hand-made, but it's readable at least. The same goes for the CSS document. + */ + file << doc.get(docpp::HTML::FORMATTING_PRETTY); + + file.close(); + + std::cout << doc.get() << "\n"; + + /* And we're done! */ + return 0; + +} diff --git a/include/docpp.hpp b/include/docpp.hpp new file mode 100644 index 0000000..6150885 --- /dev/null +++ b/include/docpp.hpp @@ -0,0 +1,556 @@ +/** + * docpp - Small C++ library for generating XML, HTML and CSS. + * Licensed under the LGPL-3.0-or-later license. + * + * Author: speedie + * + * @file docpp.hpp + * @brief Header file for docpp + * @author speedie + * @date 2024 + * @copyright GNU Lesser General Public License 3.0 + * @version 0.0.1 + */ +#pragma once + +#include +#include +#include + +/** + * @brief A namespace to represent HTML elements and documents + */ +namespace docpp { + namespace HTML { + enum { + SECTION_HTML, + SECTION_HEAD, + SECTION_BODY, + SECTION_FOOTER, + SECTION_DIV, + TYPE_SELF_CLOSING, + TYPE_NON_SELF_CLOSING, + TYPE_NON_CLOSED, + FORMATTING_NONE, + FORMATTING_PRETTY, + }; + + /** + * @brief A class to represent an HTML property + */ + class HTMLProperty { + private: + std::pair property{}; + protected: + public: + /** + * @brief Construct a new HTMLProperty object + * @param key The key of the property + * @param value The value of the property + */ + HTMLProperty(const std::string& key, const std::string& value); + /** + * @brief Construct a new HTMLProperty object + * @param property The property to set + */ + HTMLProperty(const std::pair& property); + HTMLProperty() = default; + + /** + * @brief Get the key of the property + * @return std::string The key of the property + */ + std::string getKey() const; + /** + * @brief Get the value of the property + * @return std::string The value of the property + */ + std::string getValue() const; + /** + * @brief Get the property. + * @return std::pair The value of the property + */ + std::pair get() const; + /** + * @brief Set the key of the property. + * @param key The key. + */ + void setKey(const std::string& key); + /** + * @brief Set the value of the property. + * @param value The value. + */ + void setValue(const std::string& value); + /** + * @brief Set the property + * @param property The property. + */ + void set(const std::pair& property); + }; + + /** + * @brief A class to represent the properties of an HTML element + */ + class HTMLElementProperties { + private: + std::vector properties{}; + protected: + public: + /** + * @brief Get the properties of the element + * @return std::vector The properties of the element + */ + std::vector get() const; + /** + * @brief Set the properties of the element + * @param properties The properties to set + */ + void set(const std::vector& properties); + /** + * @brief Add a property to the element + * @param property The property to add + */ + void push_back(const HTMLProperty& property); + /** + * @brief Construct a new HTMLElementProperties object + * @param properties The properties to set + */ + HTMLElementProperties(const std::vector& properties); + /** + * @brief Construct a new HTMLElementProperties object + * @param property The property to add + */ + HTMLElementProperties(const HTMLProperty& property); + /** + * @brief Construct a new HTMLElementProperties object + */ + HTMLElementProperties() = default; + }; + + /** + * @brief A class to represent an HTML element + */ + class HTMLElement { + private: + std::string tag{}; + std::string data{}; + int type{TYPE_NON_SELF_CLOSING}; + HTMLElementProperties properties{}; + protected: + public: + /** + * @brief Construct a new HTMLElement object + * @param tag The tag of the element + * @param properties The properties of the element + * @param data The data of the element + */ + HTMLElement(const std::string& tag, const HTMLElementProperties& properties = {}, const std::string& data = {}, const int type = TYPE_NON_SELF_CLOSING); + /** + * @brief Construct a new HTMLElement object + */ + HTMLElement() = default; + /** + * @brief Set the tag, id, and classes of the element + * @param tag The tag of the element + * @param id The id of the element + * @param classes The classes of the element + */ + void set(const std::string& tag, const HTMLElementProperties& properties = {}, const std::string& data = {}, const int type = TYPE_NON_SELF_CLOSING); + + /** + * @brief Get the element in the form of an HTML tag. + * @return std::string The tag of the element + */ + std::string get(const int formatting = FORMATTING_NONE) const; + + /** + * @brief Get the tag of the element + * @return std::string The data of the element + */ + std::string getTag() const; + + /** + * @brief Get the data of the element + * @return std::string The data of the element + */ + std::string getData() const; + }; + + /** + * @brief A class to represent an HTML section (head, body, etc.) + */ + class HTMLSection { + private: + int index{}; + std::string tag{}; + HTMLElementProperties properties{}; + + std::unordered_map elements{}; + std::unordered_map sections{}; + protected: + public: + /** + * @brief Prepend an element to the section + * @param element The element to add + */ + void push_front(const HTMLElement& element); + /** + * @brief Prepend a section to the section + * @param section The section to add + */ + void push_front(const HTMLSection& section); + /** + * @brief Append an element to the section + * @param element The element to add + */ + void push_back(const HTMLElement& element); + /** + * @brief Append a section to the section + * @param section The section to add + */ + void push_back(const HTMLSection& section); + /** + * @brief Erase an element from the section. Note that this will NOT change the size/index. + * @param index The index of the element to erase + */ + void erase(const int index); + /** + * @brief Erase a section from the section, by reading from a section. The section will be erased if it's identical to section. Note that this will NOT change the size/index. + * @param section The section to erase + */ + void erase(const HTMLSection& section); + /** + * @brief Erase an element from the section, by reading from an element. The element will be erased if it's identical to element. Note that this will NOT change the size/index. + * @param element The element to erase + */ + void erase(const HTMLElement& element); + /** + * @brief Find an element in the section + * @param element The element to find + * @return int The index of the element + */ + int find(const HTMLElement& element); + /** + * @brief Find a section in the section + * @param section The section to find + * @return int The index of the section + */ + int find(const HTMLSection& section); + /** + * @brief Insert an element into the section + * @param index The index to insert the element + * @param element The element to insert + */ + void insert(const int index, const HTMLElement& element); + /** + * @brief Insert a section into the section + * @param index The index to insert the section + * @param section The section to insert + */ + void insert(const int index, const HTMLSection& section); + /** + * @brief Get the size of the section + * @return int The size of the section + */ + int size() const; + + /** + * @brief Construct a new HTMLSection object + * @param tag The tag of the section + * @param properties The properties of the section + */ + HTMLSection(const std::string& tag, const HTMLElementProperties& properties = {}); + /** + * @brief Construct a new HTMLSection object + * @param tag The tag of the section + * @param properties The properties of the section + */ + HTMLSection(const int tag, const HTMLElementProperties& properties = {}); + /** + * @brief Construct a new HTMLSection object + */ + HTMLSection() = default; + /** + * @brief Set the tag, id, and classes of the section + * @param tag The tag of the section + * @param id The id of the section + * @param classes The classes of the section + */ + void set(const std::string& tag, const HTMLElementProperties& properties = {}); + /** + * @brief Set the tag, id, and classes of the section + * @param tag The tag of the section + * @param id The id of the section + * @param classes The classes of the section + */ + void set(const int tag, const HTMLElementProperties& properties = {}); + /** + * @brief Swap two elements in the section + * @param index1 The index of the first element + * @param index2 The index of the second element + */ + void swap(const int index1, const int index2); + /** + * @brief Swap two elements in the section + * @param element1 The first element + * @param element2 The second element + */ + void swap(const HTMLElement& element1, const HTMLElement& element2); + /** + * @brief Swap two sections in the section + * @param index1 The index of the first section + * @param index2 The index of the second section + */ + void swap(const HTMLSection& section1, const HTMLSection& section2); + /** + * @brief Get the elements of the section + * @return std::vector The elements of the section + */ + std::vector getHTMLElements(); + /** + * @brief Get the sections of the section + * @return std::vector The sections of the section + */ + std::vector getHTMLSections(); + + /** + * @brief Dump the entire section. + * @return std::string The section + */ + std::string get(const int formatting = FORMATTING_NONE) const; + + }; + + /** + * @brief A class to represent an HTML document + */ + class HTMLDocument { + private: + std::string doctype{""}; + HTMLSection document{}; + protected: + public: + /** + * @brief Get the document + * @param std::string The type to return + * @return std::string The document + */ + std::string get(const int formatting = FORMATTING_NONE) const; + + /** + * @brief Get the doctype of the document + * @return std::string The doctype of the document + */ + std::string getDoctype() const; + + /** + * @brief Set the document + * @param document The document to set + */ + void set(const HTMLSection& document); + /** + * @brief Set the doctype of the document + * @param doctype The doctype to set + */ + void setDoctype(const std::string& doctype); + /** + * @brief Construct a new HTMLDocument object + */ + HTMLDocument() = default; + /** + * @brief Construct a new HTMLDocument object + * @param document The document to set + */ + HTMLDocument(const HTMLSection& document, const std::string& doctype = ""); + }; + } // namespace HTML + + namespace CSS { + enum { + FORMATTING_NONE, + FORMATTING_PRETTY, + }; + + class CSSProperty { + private: + std::pair property{}; + protected: + public: + /** + * @brief Construct a new CSSProperty object + * @param key The key of the property + * @param value The value of the property + */ + CSSProperty(const std::string& key, const std::string& value); + /** + * @brief Construct a new CSSProperty object + * @param property The property to set + */ + CSSProperty(const std::pair& property); + CSSProperty() = default; + + /** + * @brief Get the key of the property + * @return std::string The key of the property + */ + std::string getKey() const; + /** + * @brief Get the value of the property + * @return std::string The value of the property + */ + std::string getValue() const; + /** + * @brief Get the property. + * @return std::pair The value of the property + */ + std::pair get() const; + /** + * @brief Set the key of the property. + * @param key The key. + */ + void setKey(const std::string& key); + /** + * @brief Set the value of the property. + * @param value The value. + */ + void setValue(const std::string& value); + /** + * @brief Set the property + * @param property The property. + */ + void set(const std::pair& property); + /** + * @brief Set the property + * @param key The key of the property + * @param value The value of the property + */ + void set(const std::string& key, const std::string& value); + }; + + class CSSElement { + private: + std::pair> element{}; + protected: + public: + /** + * @brief Construct a new CSSElement object + * @param tag The tag of the element + * @param properties The properties of the element + */ + CSSElement(const std::string& tag, const std::vector& properties); + /** + * @brief Construct a new CSSElement object + * @param element The element to set + */ + CSSElement(const std::pair>& element); + CSSElement() = default; + + /** + * @brief Push a property to the element + * @param property The property to push + */ + void push_back(const CSSProperty& property); + /** + * @brief Set the properties of the element + * @param properties The properties to set + */ + void set(const std::string& tag, const std::vector& properties); + /** + * @brief Set the properties of the element + * @param element The element to set + */ + void set(const std::pair>& element); + /** + * @brief Get the element + * @return std::pair> The element + */ + std::string get(const int formatting = FORMATTING_NONE) const; + /** + * @brief Get the tag of the element + * @return std::string The tag of the element + */ + std::string getTag() const; + /** + * @brief Get the properties of the element + * @return std::vector The properties of the element + */ + std::vector getProperties() const; + }; + + /** + * @brief A class to represent a CSS stylesheet + */ + class CSSStylesheet { + private: + std::vector elements{}; + protected: + public: + /** + * @brief Construct a new CSSStylesheet object + * @param elements The elements to set + */ + CSSStylesheet(const std::vector& elements); + CSSStylesheet() = default; + + /** + * @brief Prepend an element to the stylesheet + * @param element The element to add + */ + void push_front(const CSSElement& element); + /** + * @brief Append an element to the stylesheet + * @param element The element to add + */ + void push_back(const CSSElement& element); + /** + * @brief Insert an element into the stylesheet + * @param index The index to insert the element + * @param element The element to insert + */ + void insert(const int index, const CSSElement& element); + /** + * @brief Erase an element from the stylesheet. Note that this will NOT change the size/index. + * @param index The index of the element to erase + */ + void erase(const int index); + /** + * @brief Find an element in the stylesheet + * @param element The element to find + * @return int The index of the element + */ + int find(const CSSElement& element); + /** + * @brief Get the size of the stylesheet + * @return int The size of the stylesheet + */ + int size() const; + /** + * @brief Swap two elements in the stylesheet + * @param index1 The index of the first element + * @param index2 The index of the second element + */ + void swap(const int index1, const int index2); + /** + * @brief Swap two elements in the stylesheet + * @param element1 The first element + * @param element2 The second element + */ + void swap(const CSSElement& element1, const CSSElement& element2); + /** + * @brief Set the elements of the stylesheet + * @param elements The elements to set + */ + void set(const std::vector& elements); + /** + * @brief Get the elements of the stylesheet + * @return std::vector The elements of the stylesheet + */ + std::vector getElements() const; + /** + * @brief Get the stylesheet + * @return std::string The stylesheet + */ + std::string get(const int formatting = FORMATTING_NONE) const; + }; + } +} diff --git a/src/docpp.cpp b/src/docpp.cpp new file mode 100644 index 0000000..452e1a4 --- /dev/null +++ b/src/docpp.cpp @@ -0,0 +1,538 @@ +/** + * docpp - Small C++ library for generating XML, HTML and CSS. + * Licensed under the LGPL-3.0-or-later license. + * + * Author: speedie + * + * @file docpp.cpp + * @brief Implementation of the docpp library. + * @author speedie + * @date 2024 + * @copyright GNU Lesser General Public License 3.0. + * @version 0.0.1 + */ + +#include +#include +#include +#include +#include + +docpp::HTML::HTMLProperty::HTMLProperty(const std::string& key, const std::string& value) { + this->setKey(key); + this->setValue(value); +} + +docpp::HTML::HTMLProperty::HTMLProperty(const std::pair& property) { + this->set(property); +} + +std::string docpp::HTML::HTMLProperty::getKey() const { + return this->property.first; +} + +std::string docpp::HTML::HTMLProperty::getValue() const { + return this->property.second; +} + +std::pair docpp::HTML::HTMLProperty::get() const { + return this->property; +} + +void docpp::HTML::HTMLProperty::setKey(const std::string& key) { + this->property.first = key; +} + +void docpp::HTML::HTMLProperty::setValue(const std::string& value) { + this->property.second = value; +} + +void docpp::HTML::HTMLProperty::set(const std::pair& property) { + this->property = property; +} + +docpp::HTML::HTMLElementProperties::HTMLElementProperties(const std::vector& properties) { + this->set(properties); +} + +docpp::HTML::HTMLElementProperties::HTMLElementProperties(const docpp::HTML::HTMLProperty& property) { + this->push_back(property); +} + +std::vector docpp::HTML::HTMLElementProperties::get() const { + return this->properties; +} + +void docpp::HTML::HTMLElementProperties::set(const std::vector& properties) { + this->properties = properties; +} + +void docpp::HTML::HTMLElementProperties::push_back(const docpp::HTML::HTMLProperty& property) { + this->properties.push_back(property); +} + +docpp::HTML::HTMLElement::HTMLElement(const std::string& tag, const HTMLElementProperties& properties, const std::string& data, const int type) { + this->set(tag, properties, data, type); +} + +void docpp::HTML::HTMLElement::set(const std::string& tag, const HTMLElementProperties& properties, const std::string& data, const int type) { + this->tag = tag; + this->data = data; + this->properties = properties; + this->type = type; +} + +std::string docpp::HTML::HTMLElement::get(const int formatting) const { + std::string ret{}; + + ret += "<" + this->tag; + + for (const auto& it : this->properties.get()) { + if (!it.getKey().compare("")) continue; + if (!it.getValue().compare("")) continue; + + ret += " " + it.getKey() + "=\"" + it.getValue() + "\""; + } + + if (this->type != docpp::HTML::TYPE_SELF_CLOSING) { + ret += ">"; + } + + if (this->type == docpp::HTML::TYPE_NON_SELF_CLOSING) { + ret += this->data + "tag + ">"; + } else if (this->type == docpp::HTML::TYPE_SELF_CLOSING) { + ret += this->data + "/>"; + } + + if (formatting == docpp::HTML::FORMATTING_PRETTY) { + ret += "\n"; + } + + return std::move(ret); +} + +std::string docpp::HTML::HTMLElement::getTag() const { + return this->tag; +} + +std::string docpp::HTML::HTMLElement::getData() const { + return this->data; +} + +docpp::HTML::HTMLSection::HTMLSection(const std::string& tag, const HTMLElementProperties& properties) { + this->tag = tag; + this->properties = properties; +} + +docpp::HTML::HTMLSection::HTMLSection(const int tag, const HTMLElementProperties& properties) { + if (tag == docpp::HTML::SECTION_DIV) { + this->tag = "div"; + } else if (tag == docpp::HTML::SECTION_BODY) { + this->tag = "body"; + } else if (tag == docpp::HTML::SECTION_FOOTER) { + this->tag = "footer"; + } else if (tag == docpp::HTML::SECTION_HEAD) { + this->tag = "head"; + } else if (tag == docpp::HTML::SECTION_HTML) { + this->tag = "html"; + } + + this->properties = properties; +} + +void docpp::HTML::HTMLSection::set(const std::string& tag, const HTMLElementProperties& properties) { + this->tag = tag; + this->properties = properties; +} + +void docpp::HTML::HTMLSection::set(const int tag, const HTMLElementProperties& properties) { + if (tag == docpp::HTML::SECTION_DIV) { + this->tag = "div"; + } else if (tag == docpp::HTML::SECTION_BODY) { + this->tag = "body"; + } else if (tag == docpp::HTML::SECTION_FOOTER) { + this->tag = "footer"; + } else if (tag == docpp::HTML::SECTION_HEAD) { + this->tag = "head"; + } else if (tag == docpp::HTML::SECTION_HTML) { + this->tag = "html"; + } + + this->properties = properties; +} + +void docpp::HTML::HTMLSection::push_front(const HTMLElement& element) { + for (int i{this->index}; i > 0; i--) { + this->elements[i] = this->elements.at(i - 1); + } + + this->elements[0] = element; + this->index++; +} + +void docpp::HTML::HTMLSection::push_front(const HTMLSection& section) { + for (int i{this->index}; i > 0; i--) { + this->sections.at(i) = this->sections.at(i - 1); + } + + this->sections[0] = section; + this->index++; +} + +void docpp::HTML::HTMLSection::push_back(const HTMLElement& element) { + this->elements[this->index++] = element; +} + +void docpp::HTML::HTMLSection::push_back(const HTMLSection& section) { + this->sections[this->index++] = section; +} + +void docpp::HTML::HTMLSection::erase(const int index) { + bool erased{false}; + if (this->elements.find(index) != this->elements.end()) { + this->elements.erase(index); + erased = true; + } else if (this->sections.find(index) != this->sections.end()) { + this->sections.erase(index); + erased = true; + } + + if (!erased) { + throw std::out_of_range("Index out of range"); + } +} + +void docpp::HTML::HTMLSection::erase(const HTMLSection& section) { + for (int i{0}; i < this->size(); i++) { + const auto it = this->getHTMLSections().at(i); + + if (it.get() == section.get()) { + this->erase(i); + return; + } + } + + throw std::out_of_range("Section not found"); +} + +void docpp::HTML::HTMLSection::erase(const HTMLElement& element) { + for (int i{0}; i < this->size(); i++) { + const auto it = this->getHTMLElements().at(i); + + if (it.get() == element.get()) { + this->erase(i); + return; + } + } + + throw std::out_of_range("Element not found"); +} + +void docpp::HTML::HTMLSection::insert(const int index, const HTMLElement& element) { + if (this->sections.find(index) != this->sections.end()) { + throw std::invalid_argument("Index already occupied by a section"); + } else { + this->elements[index] = element; + } + + this->index = std::max(this->index, index) + 1; +} + +void docpp::HTML::HTMLSection::insert(const int index, const HTMLSection& section) { + this->sections[index] = section; + this->index = std::max(this->index, index) + 1; +} + +int docpp::HTML::HTMLSection::find(const HTMLElement& element) { + for (int i{0}; i < this->size(); i++) { + const auto it = this->getHTMLElements().at(i); + + if (it.get() == element.get()) { + return i; + } + } + + throw std::out_of_range("Element not found"); +} + +int docpp::HTML::HTMLSection::find(const HTMLSection& section) { + for (int i{0}; i < this->size(); i++) { + const auto it = this->getHTMLSections().at(i); + + if (it.get() == section.get()) { + return i; + } + } + + throw std::out_of_range("Section not found"); +} + +int docpp::HTML::HTMLSection::size() const { + return this->index; +} + +std::vector docpp::HTML::HTMLSection::getHTMLElements() { + std::vector ret{}; + ret.reserve(this->index); + for (int i{0}; i < this->index; i++) { + if (this->elements.find(i) != this->elements.end()) { + ret.push_back(this->elements.at(i)); + } + } + return std::move(ret); +} + +std::vector docpp::HTML::HTMLSection::getHTMLSections() { + std::vector ret{}; + ret.reserve(this->index); + + for (int i{0}; i < this->index; i++) { + if (this->sections.find(i) != this->sections.end()) { + ret.push_back(this->sections.at(i)); + } + } + + return std::move(ret); +} + +std::string docpp::HTML::HTMLSection::get(const int formatting) const { + std::string ret{}; + + ret += "<" + this->tag; + + for (const auto& it : this->properties.get()) { + if (!it.getKey().compare("")) continue; + if (!it.getValue().compare("")) continue; + + ret += " " + it.getKey() + "=\"" + it.getValue() + "\""; + } + + ret += ">"; + + if (formatting == docpp::HTML::FORMATTING_PRETTY) { + ret += "\n"; + } + + for (int i{0}; i < this->index; i++) { + if (this->elements.find(i) != this->elements.end()) { + ret += this->elements.at(i).get(formatting); + } else if (this->sections.find(i) != this->sections.end()) { + ret += this->sections.at(i).get(formatting); + + if (formatting == docpp::HTML::FORMATTING_PRETTY) { + ret += "\n"; + } + } + } + + ret += "tag + ">"; + + return std::move(ret); +} + +void docpp::HTML::HTMLSection::swap(const int index1, const int index2) { + if (this->elements.find(index1) != this->elements.end() && this->elements.find(index2) != this->elements.end()) { + std::swap(this->elements[index1], this->elements[index2]); + } else if (this->sections.find(index1) != this->sections.end() && this->sections.find(index2) != this->sections.end()) { + std::swap(this->sections[index1], this->sections[index2]); + } else { + throw std::out_of_range("Index out of range"); + } +} + +void docpp::HTML::HTMLSection::swap(const HTMLElement& element1, const HTMLElement& element2) { + this->swap(this->find(element1), this->find(element2)); +} + +void docpp::HTML::HTMLSection::swap(const HTMLSection& section1, const HTMLSection& section2) { + this->swap(this->find(section1), this->find(section2)); +} + +std::string docpp::HTML::HTMLDocument::get(const int formatting) const { + return this->doctype + (formatting == FORMATTING_PRETTY ? "\n" : "") + this->document.get(formatting); +} + +void docpp::HTML::HTMLDocument::set(const docpp::HTML::HTMLSection& document) { + this->document = document; +} + +void docpp::HTML::HTMLDocument::setDoctype(const std::string& doctype) { + this->doctype = doctype; +} + +docpp::HTML::HTMLDocument::HTMLDocument(const docpp::HTML::HTMLSection& document, const std::string& doctype) { + this->set(document); + this->setDoctype(doctype); +} + +std::string docpp::HTML::HTMLDocument::getDoctype() const { + return this->doctype; +} + +docpp::CSS::CSSProperty::CSSProperty(const std::string& key, const std::string& value) { + this->set(key, value); +} + +docpp::CSS::CSSProperty::CSSProperty(const std::pair& property) { + this->set(property); +} + +std::string docpp::CSS::CSSProperty::getKey() const { + return this->property.first; +} + +std::string docpp::CSS::CSSProperty::getValue() const { + return this->property.second; +} + +std::pair docpp::CSS::CSSProperty::get() const { + return this->property; +} + +void docpp::CSS::CSSProperty::setKey(const std::string& key) { + this->property.first = key; +} + +void docpp::CSS::CSSProperty::setValue(const std::string& value) { + this->property.second = value; +} + +void docpp::CSS::CSSProperty::set(const std::pair& property) { + this->property = property; +} + +void docpp::CSS::CSSProperty::set(const std::string& key, const std::string& value) { + this->property = std::make_pair(key, value); +} + +docpp::CSS::CSSElement::CSSElement(const std::string& tag, const std::vector& properties) { + this->set(tag, properties); +} + +docpp::CSS::CSSElement::CSSElement(const std::pair>& element) { + this->set(element); +} + +void docpp::CSS::CSSElement::set(const std::string& tag, const std::vector& properties) { + this->element.first = tag; + this->element.second = properties; +} + +void docpp::CSS::CSSElement::set(const std::pair>& element) { + this->element = element; +} + +void docpp::CSS::CSSElement::push_back(const CSSProperty& property) { + this->element.second.push_back(property); +} + +std::string docpp::CSS::CSSElement::get(const int formatting) const { + std::string ret{}; + + if (this->element.first.compare("")) { + ret += this->element.first + " {"; + + if (formatting == docpp::CSS::FORMATTING_PRETTY) { + ret += "\n"; + } + + for (const auto& it : this->element.second) { + if (!it.getKey().compare("")) continue; + if (!it.getValue().compare("")) continue; + + ret += it.getKey() + ": " + it.getValue() + ";"; + + if (formatting == docpp::CSS::FORMATTING_PRETTY) { + ret += "\n"; + } + } + + ret += "}"; + + if (formatting == docpp::CSS::FORMATTING_PRETTY) { + ret += "\n"; + } + } + + return std::move(ret); +} + +std::string docpp::CSS::CSSElement::getTag() const { + return this->element.first; +} + +std::vector docpp::CSS::CSSElement::getProperties() const { + return this->element.second; +} + +docpp::CSS::CSSStylesheet::CSSStylesheet(const std::vector& elements) { + this->set(elements); +} + +void docpp::CSS::CSSStylesheet::set(const std::vector& elements) { + this->elements = elements; +} + +void docpp::CSS::CSSStylesheet::push_front(const CSSElement& element) { + this->elements.insert(this->elements.begin(), element); +} + +void docpp::CSS::CSSStylesheet::push_back(const CSSElement& element) { + this->elements.push_back(element); +} + +void docpp::CSS::CSSStylesheet::insert(const int index, const CSSElement& element) { + if (index < 0 || index >= this->elements.size()) { + throw std::out_of_range("Index out of range"); + } + + this->elements.insert(this->elements.begin() + index, element); +} + +void docpp::CSS::CSSStylesheet::erase(const int index) { + if (index < 0 || index >= this->elements.size()) { + throw std::out_of_range("Index out of range"); + } + + this->elements.erase(this->elements.begin() + index); +} + +int docpp::CSS::CSSStylesheet::find(const CSSElement& element) { + for (int i{0}; i < this->elements.size(); i++) { + if (this->elements.at(i).get() == element.get()) { + return i; + } + } + + throw std::out_of_range("Element not found"); +} + +int docpp::CSS::CSSStylesheet::size() const { + return this->elements.size(); +} + +void docpp::CSS::CSSStylesheet::swap(const int index1, const int index2) { + if (index1 < 0 || index1 >= this->elements.size() || index2 < 0 || index2 >= this->elements.size()) { + throw std::out_of_range("Index out of range"); + } + + std::swap(this->elements[index1], this->elements[index2]); +} + +void docpp::CSS::CSSStylesheet::swap(const CSSElement& element1, const CSSElement& element2) { + this->swap(this->find(element1), this->find(element2)); +} + +std::vector docpp::CSS::CSSStylesheet::getElements() const { + return this->elements; +} + +std::string docpp::CSS::CSSStylesheet::get(const int formatting) const { + std::string ret{}; + + for (const auto& it : this->elements) { + ret += it.get(formatting); + } + + return std::move(ret); +} diff --git a/tests/test.cpp b/tests/test.cpp new file mode 100644 index 0000000..826d5ac --- /dev/null +++ b/tests/test.cpp @@ -0,0 +1,200 @@ +#include +#include +#include +#include + +/** + * @brief Test cases for the DuvaHTML namespace. + */ +SCENARIO("Test HTML", "[HTML]") { + auto test1 = []() { + docpp::HTML::HTMLDocument doc{}; + docpp::HTML::HTMLSection html(docpp::HTML::SECTION_HTML, {}); + + docpp::HTML::HTMLSection head(docpp::HTML::SECTION_HEAD, {}); + docpp::HTML::HTMLSection body(docpp::HTML::SECTION_BODY, {}); + docpp::HTML::HTMLSection div(docpp::HTML::SECTION_DIV, {}); + docpp::HTML::HTMLSection footer(docpp::HTML::SECTION_FOOTER, {}); + + head.push_back(docpp::HTML::HTMLElement("title", {}, "Test Title")); + body.push_back(docpp::HTML::HTMLElement("h1", {}, "Test Header")); + body.push_back(docpp::HTML::HTMLElement("p", {}, "Test Paragraph")); + + docpp::HTML::HTMLElementProperties prop{}; + prop.push_back(docpp::HTML::HTMLProperty(std::pair("id", "test_id"))); + + body.push_back(docpp::HTML::HTMLElement("p", prop, "Test Paragraph With ID")); + + div.push_back(docpp::HTML::HTMLElement("p", {}, "Test Paragraph In Div")); + body.push_back(div); + + prop.push_back(docpp::HTML::HTMLProperty(std::pair("class", "class1 class2 class3"))); + + body.push_back(docpp::HTML::HTMLElement("p", prop, "Test Paragraph With ID And Class")); + + html.push_back(head); + html.push_back(body); + html.push_back(footer); + + doc.set(html); + + const std::string expected_html{"Test Title

Test Header

Test Paragraph

Test Paragraph With ID

Test Paragraph In Div

Test Paragraph With ID And Class

"}; + + REQUIRE(doc.get() == expected_html); + REQUIRE(doc.get(docpp::HTML::FORMATTING_PRETTY) == "\n\n\nTest Title\n\n\n

Test Header

\n

Test Paragraph

\n

Test Paragraph With ID

\n
\n

Test Paragraph In Div

\n
\n

Test Paragraph With ID And Class

\n\n
\n
\n"); + }; + + auto test2 = []() { + docpp::HTML::HTMLSection section(docpp::HTML::SECTION_HTML, {}); + + section.push_back(docpp::HTML::HTMLElement("p", {}, "Test 1")); + section.push_back(docpp::HTML::HTMLElement("p", {}, "Test 2")); + section.push_back(docpp::HTML::HTMLElement("p", {}, "Test 3")); + + section.erase(docpp::HTML::HTMLElement("p", {}, "Test 2")); + + REQUIRE(section.get() == "

Test 1

Test 3

"); + REQUIRE(section.get(docpp::HTML::FORMATTING_PRETTY) == "\n

Test 1

\n

Test 3

\n"); + }; + + auto test3 = []() { + docpp::HTML::HTMLSection section = docpp::HTML::HTMLSection(docpp::HTML::SECTION_HTML, {}); + + section.push_back(docpp::HTML::HTMLElement("p", {}, "Test 1")); + section.push_back(docpp::HTML::HTMLElement("p", {}, "Test 2")); + section.push_back(docpp::HTML::HTMLElement("p", {}, "Test 3")); + + int pos = section.find(docpp::HTML::HTMLElement("p", {}, "Test 2")); + section.insert(pos, docpp::HTML::HTMLElement("p", {}, "Test 2.5")); + + REQUIRE(section.get() == "

Test 1

Test 2.5

Test 3

"); + REQUIRE(section.get(docpp::HTML::FORMATTING_PRETTY) == "\n

Test 1

\n

Test 2.5

\n

Test 3

\n"); + }; + + auto test4 = []() { + docpp::HTML::HTMLSection section = docpp::HTML::HTMLSection(docpp::HTML::SECTION_HTML, {}); + + section.push_back(docpp::HTML::HTMLElement("p", {}, "Test 1")); + section.push_back(docpp::HTML::HTMLElement("p", {}, "Test 2")); + section.push_back(docpp::HTML::HTMLElement("p", {}, "Test 3")); + + int pos = section.find(docpp::HTML::HTMLElement("p", {}, "Test 2")); + + section.erase(pos); + + REQUIRE(section.get() == "

Test 1

Test 3

"); + REQUIRE(section.get(docpp::HTML::FORMATTING_PRETTY) == "\n

Test 1

\n

Test 3

\n"); + }; + + auto test5 = []() { + docpp::HTML::HTMLSection section = docpp::HTML::HTMLSection(docpp::HTML::SECTION_HTML, {}); + docpp::HTML::HTMLSection subsection(docpp::HTML::SECTION_DIV, {}); + + subsection.push_back(docpp::HTML::HTMLElement("p", {}, "Test 1")); + + docpp::HTML::HTMLSection subsection2(docpp::HTML::SECTION_DIV, {}); + subsection2.push_back(docpp::HTML::HTMLElement("p", {}, "Test 2")); + + subsection.push_back(subsection2); + + section.push_back(subsection); + + docpp::HTML::HTMLDocument doc{}; + doc.set(section); + + REQUIRE(doc.get() == "

Test 1

Test 2

"); + REQUIRE(doc.get(docpp::HTML::FORMATTING_PRETTY) == "\n\n
\n

Test 1

\n
\n

Test 2

\n
\n
\n"); + }; + + auto test6 = []() { + docpp::CSS::CSSStylesheet css{}; + docpp::CSS::CSSElement element{"p", {{"color", "red"}, {"font-size", "16px"}, {"font-family", "Arial"}}}; + + css.push_back(element); + + REQUIRE(css.get() == "p {color: red;font-size: 16px;font-family: Arial;}"); + REQUIRE(css.get(docpp::CSS::FORMATTING_PRETTY) == "p {\ncolor: red;\nfont-size: 16px;\nfont-family: Arial;\n}\n"); + }; + + auto test7 = []() { + docpp::CSS::CSSStylesheet css = docpp::CSS::CSSStylesheet{}; + docpp::CSS::CSSElement element = docpp::CSS::CSSElement{"p", {{"color", "red"}, {"font-size", "16px"}, {"font-family", "Arial"}}}; + docpp::CSS::CSSElement element2{"div", {{"color", "blue"}, {"font-size", "12px"}, {"font-family", "Arial"}}}; + + css.push_back(element); + css.push_front(element2); + + REQUIRE(css.get() == "div {color: blue;font-size: 12px;font-family: Arial;}p {color: red;font-size: 16px;font-family: Arial;}"); + }; + + auto test8 = []() { + docpp::CSS::CSSStylesheet css = docpp::CSS::CSSStylesheet{}; + docpp::CSS::CSSElement element = docpp::CSS::CSSElement{"p", {{"color", "red"}, {"font-size", "16px"}, {"font-family", "Arial"}}}; + docpp::CSS::CSSElement element2{"div", {{"color", "blue"}, {"font-size", "12px"}, {"font-family", "Arial"}}}; + + css.push_back(element); + css.push_front(element2); + + css.erase(css.find(element2)); + + REQUIRE(css.get() == "p {color: red;font-size: 16px;font-family: Arial;}"); + }; + + auto test9 = []() { + docpp::CSS::CSSStylesheet css = docpp::CSS::CSSStylesheet{}; + docpp::CSS::CSSElement element = docpp::CSS::CSSElement{"p", {{"color", "red"}, {"font-size", "16px"}, {"font-family", "Arial"}}}; + docpp::CSS::CSSElement element2{"div", {{"color", "blue"}, {"font-size", "12px"}, {"font-family", "Arial"}}}; + + css.push_back(element); + css.push_front(element2); + + css.erase(css.find(element2)); + css.push_front(element2); + + css.swap(css.find(element), css.find(element2)); + + REQUIRE(css.get() == "p {color: red;font-size: 16px;font-family: Arial;}div {color: blue;font-size: 12px;font-family: Arial;}"); + }; + + auto test10 = []() { + docpp::HTML::HTMLSection section = docpp::HTML::HTMLSection(docpp::HTML::SECTION_HTML, {}); + + section.push_back(docpp::HTML::HTMLElement("p", {}, "Test 1")); + section.push_back(docpp::HTML::HTMLElement("p", {}, "Test 2")); + section.push_back(docpp::HTML::HTMLElement("p", {}, "Test 3")); + + section.swap(section.find(docpp::HTML::HTMLElement("p", {}, "Test 2")), section.find(docpp::HTML::HTMLElement("p", {}, "Test 3"))); + + REQUIRE(section.get() == "

Test 1

Test 3

Test 2

"); + }; + + auto test11 = []() { + docpp::HTML::HTMLSection section = docpp::HTML::HTMLSection(docpp::HTML::SECTION_HTML, {}); + + section.push_back(docpp::HTML::HTMLElement("p", {}, "Test 1")); + section.push_back(docpp::HTML::HTMLElement("p", {}, "Test 2")); + section.push_back(docpp::HTML::HTMLElement("p", {}, "Test 3")); + + section.swap(docpp::HTML::HTMLElement("p", {}, "Test 2"), docpp::HTML::HTMLElement("p", {}, "Test 3")); + + REQUIRE(section.get() == "

Test 1

Test 3

Test 2

"); + }; + + auto test12 = []() { + docpp::HTML::HTMLSection section = docpp::HTML::HTMLSection(docpp::HTML::SECTION_HTML, {}); + + section.push_back(docpp::HTML::HTMLElement("p", {}, "Test 1")); + section.push_back(docpp::HTML::HTMLElement("p", {}, "Test 2")); + section.push_back(docpp::HTML::HTMLElement("p", {}, "Test 3")); + + section.push_front(docpp::HTML::HTMLElement("p", {}, "Test 0")); + + REQUIRE(section.get() == "

Test 0

Test 1

Test 2

Test 3

"); + }; + + std::vector tests{test1, test2, test3, test4, test5, test6, test7, test8, test9, test10, test11, test12}; + + for (const auto& test : tests) { + test(); + } +}