diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 67322585..9d78f5d0 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -18,10 +18,20 @@ jobs: UPA_DOCS_VERSION: ${{ github.ref_name }} steps: - uses: actions/checkout@v4 + + # Check C++ examples in documentation + - name: Extract and try to build C++ examples from docs + run: | + tools/extract-cpp.py examples/extracted-cpp README.md + cmake -S . -B build -DCMAKE_CXX_COMPILER=g++ -DCMAKE_CXX_STANDARD=20 -DURL_BUILD_TESTS=OFF -DURL_BUILD_EXTRACTED=ON + cmake --build build + + # Run doxygen - name: Download theme run: doc/download-theme.sh - uses: mattnotmitt/doxygen-action@edge + # Deploy docs - name: Is there a gh-pages branch? if: env.upa_deploy run: echo "upa_checkout=$(git ls-remote --heads https://github.com/$upa_docs_repository.git refs/heads/gh-pages)" >> "$GITHUB_ENV" diff --git a/CMakeLists.txt b/CMakeLists.txt index 71ff9b52..ac1f8872 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,9 +33,10 @@ if (NOT DEFINED URL_MAIN_PROJECT) endif() # Options -option(URL_BUILD_TESTS "Build the URL tests." ${URL_MAIN_PROJECT}) -option(URL_BUILD_FUZZER "Build the URL fuzzer." OFF) -option(URL_BUILD_EXAMPLES "Build the URL examples." OFF) +option(URL_BUILD_TESTS "Build the Upa URL tests." ${URL_MAIN_PROJECT}) +option(URL_BUILD_FUZZER "Build the Upa URL fuzzer." OFF) +option(URL_BUILD_EXAMPLES "Build the Upa URL examples." OFF) +option(URL_BUILD_EXTRACTED "Build Upa URL examples extracted from the docs." OFF) option(URL_BUILD_TOOLS "Build tools." OFF) option(URL_INSTALL "Generate the install target." ON) # library options @@ -99,7 +100,8 @@ endif() include_directories(deps) # Are Upa URL and ICU libraries needed? -if (URL_BUILD_TESTS OR URL_BUILD_FUZZER OR URL_BUILD_EXAMPLES OR URL_INSTALL OR NOT URL_BUILD_TOOLS) +if (URL_BUILD_TESTS OR URL_BUILD_FUZZER OR URL_BUILD_EXAMPLES OR URL_BUILD_EXTRACTED OR + URL_INSTALL OR NOT URL_BUILD_TOOLS) # This library depends on ICU find_package(ICU REQUIRED COMPONENTS i18n uc) @@ -199,6 +201,10 @@ if (URL_BUILD_EXAMPLES) target_link_libraries(urlparse ${upa_lib_target}) endif() +if (URL_BUILD_EXTRACTED) + add_subdirectory(examples/extracted-cpp) +endif() + # Tool's targets if (URL_BUILD_TOOLS) diff --git a/examples/extracted-cpp/.gitignore b/examples/extracted-cpp/.gitignore new file mode 100644 index 00000000..ce1da4c5 --- /dev/null +++ b/examples/extracted-cpp/.gitignore @@ -0,0 +1 @@ +*.cpp diff --git a/examples/extracted-cpp/CMakeLists.txt b/examples/extracted-cpp/CMakeLists.txt new file mode 100644 index 00000000..945bff58 --- /dev/null +++ b/examples/extracted-cpp/CMakeLists.txt @@ -0,0 +1,8 @@ +# Examples to build +file(GLOB example_sources *.cpp) + +foreach(source ${example_sources}) + get_filename_component(exe_name ${source} NAME_WE) + add_executable(${exe_name} ${source}) + target_link_libraries(${exe_name} PRIVATE upa::url) +endforeach() diff --git a/tools/extract-cpp.py b/tools/extract-cpp.py new file mode 100755 index 00000000..1022885a --- /dev/null +++ b/tools/extract-cpp.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# +# Copyright 2023 Rimas Misevičius +# Distributed under the BSD-style license that can be +# found in the LICENSE file. +from enum import auto, Enum +import os +import sys + +INDENT_TEXT = " " +COMMON_CPP_HEADER = """ +#include \"upa/url.h\" +#include +#include +""" + +def has_main(cpp_text): + return "int main(" in cpp_text + +class CppExamples: + class State(Enum): + Outside = auto() + EnterCpp = auto() + InCpp = auto() + InCppSnipet = auto() + + def __init__(self, out_path): + self._cpp_file_num = 0 + self._cpp_example_num = 0 + self._cpp_examples_text = COMMON_CPP_HEADER + self._out_path = out_path + + def extract_cpp_from_md_file(self, md_path): + state = self.State.Outside.value + cpp_text = "" + + # Open the MD file + print("Processing Markdown file:", md_path) + with open(md_path, "r") as file: + for line_nl in file: + # remove ending newline character + line = line_nl.rstrip() + if state == self.State.Outside.value: + if line == "```cpp": + state = self.State.EnterCpp.value; + cpp_text = "" + elif line == "```": + if state == self.State.InCpp.value: + if has_main(cpp_text): + self._cpp_file_num += 1 + out_file_path = os.path.join(self._out_path, f"example-{self._cpp_file_num}.cpp") + print(" creating:", out_file_path) + with open(out_file_path, "w") as out_file: + out_file.write(f"// Example from: {md_path}\n") + out_file.write(cpp_text) + elif state == self.State.InCppSnipet.value: + self._cpp_example_num += 1 + self._cpp_examples_text += f"\n// Example from: {md_path}\n" + self._cpp_examples_text += f"void example_{self._cpp_example_num}() {{\n" + self._cpp_examples_text += cpp_text + self._cpp_examples_text += "}\n" + state = self.State.Outside.value; + else: + if state == self.State.EnterCpp.value: + state = self.State.InCpp.value if line.startswith("#include") else self.State.InCppSnipet.value + if state == self.State.InCppSnipet.value: + cpp_text += INDENT_TEXT + cpp_text += line_nl + + def output_common_cpp(self): + if self._cpp_example_num > 0: + # create main() + self._cpp_examples_text += "\nint main() {\n" + for num in range(1, self._cpp_example_num + 1): + self._cpp_examples_text += f"{INDENT_TEXT}example_{num}();\n" + self._cpp_examples_text += f"{INDENT_TEXT}return 0;\n" + self._cpp_examples_text += "}\n" + # save code + out_file_path = os.path.join(self._out_path, "examples.cpp") + print(" creating:", out_file_path) + with open(out_file_path, "w") as out_file: + out_file.write(self._cpp_examples_text) + + +if __name__ == "__main__": + # Command line arguments + if len(sys.argv) >= 3: + out_path = sys.argv[1] + md_files = sys.argv[2:] + + print("Output directory:", out_path) + cpp_examples = CppExamples(out_path) + for md_path in md_files: + cpp_examples.extract_cpp_from_md_file(md_path) + cpp_examples.output_common_cpp() + else: + app_name = os.path.basename(os.path.basename(__file__)) + print(f"Usage: {app_name} ...", + file=sys.stderr) + sys.exit(1)