◐ Shell
clean mode source ↗

User:Space Mission - cppreference.com

//! \abstract This program downloads the "Language support library macros"
//! [page](https://github.com/cplusplus/draft/raw/main/source/support.tex) (used to be
//! [page](https://eel.is/c++draft/version.syn)), parses it, and then generates MediaWiki
//! tables for the [page](https://en.cppreference.com/w/cpp/utility/feature_test).
//!
//! \usage: just compile & run, the MediaWiki tables will be sent to the terminal.
//!
//! \dependencies: [curl](https://en.wikipedia.org/wiki/CURL), C++23.
//!
//! \author: (c) 2021-2026. Space Mission. For cppreference.com internal usage.
//! \license: [CC-BY-SA](https://en.cppreference.com/w/Cppreference:Copyright/CC-BY-SA).
//!
//! \news:
//! 2021-06-12 : initial release (used an array that contains the plain text copied from
//!            : the eel page).
//! 2021-12-19 : added options and means to generate the whole wiki-page instead of only
//!            : the wiki-table. Note that this requires updates if the original
//!            : [page](cppreference.com/w/cpp/utility/feature_test) was changed.
//! 2021-12-20 : (optimizations) e.g. the output is generated w/o a temporary vector.
//! 2021-12-21 : added "curl" downloading of the source HTML file from
//!            : [the site](https://eel.is/c++draft/version.syn).
//! 2021-12-23 : added data extractor from the HTML using std::regex library.
//! 2021-12-27 : added more test.
//! 2022-01-29 : fixed SourceDownloader::load() always returned `false`; nonetheless,
//!            : everything worked due to try_local_file_if_download_failed == true.
//! 2023-02-27 : removed page's (volatile) header/footer generating code; left only the
//!            : table generation; placed the macros counter in the table's footer.
//! 2023-04-05 : added support of macros w/o headers, e.g., __cpp_lib_modules.
//! 2024-07-21 : source switching: the source site now is github C++draft repo; the file
//!            : format to parse is "TeX"; added "freestanding" support.
//! 2024-07-22 : table style: added 4th ("freestanding") column; added FTM links.
//! 2024-12-30 : table style: (minor) the header uses multi-line column description style.
//! 2025-04-28 : added generation of the second ("hardening") table.
//! 2025-04-29 : added a quirk for __cpp_lib_freestanding_operator_new.
//! 2026-06-07 : added a quirk for __cpp_lib_replaceable_contract_violation_handler.
//!            : changed wrapper for value (date) from "expensive" {{c}} to {{tt}}.

#include <algorithm>
#include <cassert>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <regex>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>

using namespace std::literals;

/**
 * @brief   global options
 */
struct option {
    //! Do not fail if the source file was not loaded (due to Internet problems) in
    //! this session, but still exists locally (left from a previous session).
    static inline constexpr bool try_local_file_if_download_failed{true};

    //! Remove downloaded source file (only if it was downloaded in this session).
    static inline constexpr bool remove_downloaded_file_at_exit{false};

    //! Print log and additional info, e.g. success/failure.
    static inline constexpr bool verbose{false};

    //! Test mode only.
    static inline constexpr bool enable_self_tests{false};
};

//!
//! \brief The source file downloader that gets the source file from the Internet.
//!
class SourceDownloader final {
    std::string file_name_;

  public:
    SourceDownloader() = default;
    SourceDownloader(SourceDownloader const&) = default;
    SourceDownloader& operator=(SourceDownloader const&) = default;
    ~SourceDownloader();

  public:
    [[nodiscard]] bool load();
    [[nodiscard]] const std::string& file_name() const noexcept { return file_name_; }
};

/**
 * @brief      Loads source TEX file from the C++-draft github repo.
 * @return     true, if the file was downloaded successfully.
 */
inline bool SourceDownloader::load() {
    file_name_ = std::filesystem::temp_directory_path() / "cxx_draft__support.tex";

    const std::string command{
        "curl -LJ https://github.com/cplusplus/draft/raw/main/source/support.tex "s +
        (option::verbose ? "-o "s : "--silent -o "s) + file_name_};

    if constexpr (option::verbose) {
        std::cout << "Downloading with: [" << command << "]\n"
                     "Destination file: " << file_name_ << '\n';
    }

    if (const int ret_code{std::system(command.data())}; ret_code) {
        if constexpr (option::verbose) {
            std::cout << "Can't download the file: error #" << ret_code << '\n';
            // TODO: maybe decipher the error return code.
        }
        return false;
    }

    if constexpr (option::verbose) {
        std::cout << "OK. The file was downloaded successfully.\n";
    }

    return true;
}

/**
 * @brief    Conditionally remove downloaded source file.
 */
inline SourceDownloader::~SourceDownloader() {
    if constexpr (option::remove_downloaded_file_at_exit)
        if (!file_name_.empty())
            std::filesystem::remove(file_name_);
}

/**
 * @brief class FtmTableEntry.
 *
 *  Extracts data from a given string, that must be a part of
 *  [source page](https://github.com/cplusplus/draft/raw/main/source/support.tex)
 *  tex page; generates the page [or (conditionally) only the table]:
 *  [target page](https://en.cppreference.com/w/cpp/utility/feature_test)
 */
class FtmTableEntry {
  public:
    FtmTableEntry() = default;

  public:
    [[nodiscard]] std::string generate_entry(std::string_view source);
    void clear();

  private:
    [[nodiscard]] bool parse_line(std::string_view source);

  public:
    static bool generate();

  public:
    [[nodiscard]] static bool self_test();

  private:
    [[nodiscard]] static std::string_view table_head() noexcept;
    [[nodiscard]] static std::string table_tail(unsigned);

  private:
    std::vector<std::string_view> headers_; // e.g. {"vector", "type_traits"}
    std::string macro_; // e.g. "__cpp_lib_any"
    std::string_view date_; // e.g. "201606L"
    bool free_{}; // true, if "freestanding"
};

/**
 * @brief      Parses the source line and sets internal values for:
 */
inline void FtmTableEntry::clear() {
    headers_.clear();
    macro_.clear();
    date_ = std::string_view{};
    free_ = false;
}

/**
 * @brief      Parses the source line and sets internal values for:
 *             macro_, date_, headers_, free_.
 * @param      source, the TEX source line.
 * @exception  std::logic_error, if the source line has a wrong format.
 * @return     true, if line was parsed successfully.
 */
inline bool FtmTableEntry::parse_line(std::string_view source) {
    clear();
    std::cmatch m;
    constexpr std::regex::flag_type flags{std::regex_constants::ECMAScript |
                                          std::regex_constants::optimize};

    // A quirk for irregular __cpp_lib_freestanding_operator_new:
    if (source.contains("#define @\\defnlibxname{"
                        "cpp_lib_freestanding_operator_new}@"sv)) {
        macro_ = "__cpp_lib_freestanding_operator_new", date_ = "202306L"sv, free_ = true;
        headers_.emplace_back("new");
        return true;
    }

    // A quirk for irregular __cpp_lib_replaceable_contract_violation_handler:
    if (source.contains("#define @\\defnlibxname{"
                        "cpp_lib_replaceable_contract_violation_handler}@"sv)) {
        macro_ = "__cpp_lib_replaceable_contract_violation_handler", date_ = "202603L"sv;
        free_ = true, headers_.emplace_back("contracts");
        return true;
    }

    // A fake example:
    // #define @\defnlibxname{cpp_lib_atomic}@ 201911L // freestanding, also in
    //    \libheader{atomic}, \libheader{memory}

    if (static const std::regex re_ftm_and_date{
            R"FTM(#define @\\defnlibxname\{(cpp_lib_[_a-z0-9]{3,50})\}@)FTM"
            R"DATE([ ]+(20[1-4][0-9]{3}L)[ ]+// )DATE",
            flags};
        !std::regex_search(source.data(), m, re_ftm_and_date) or m.size() != 3)
        return false;
    macro_ = "__" + std::string(m[1].first, m[1].second - m[1].first);
    date_ = std::string_view(m[2].first, m[2].second - m[2].first);

    // sanity check:
    if (macro_.length() < "__cpp_lib_xxx"sv.length() or
        date_.length() != (/*sample:*/"202002L"sv).length())
        return false;

    source.remove_prefix(m.suffix().first - source.data()); // the tail
    constexpr auto free{"freestanding"sv};
    // Extract "freestanding" flag, if any.
    if (const auto pos{source.substr(0, free.length() + 8).find(free)}; pos != ""sv.npos) {
        source.remove_prefix(pos + free.length());
        free_ = true;
    }

    // Obtain header(s) in cycle.
    // TEX example: ... also in \libheader{atomic}, \libheader{memory} ...
    static const std::regex re_header{
        R"RE(\libheader\{([a-z][_a-z\d\.]+)\})RE", flags};
    while (std::regex_search(source.data(), m, re_header) and m.size() == 2) {
        headers_.emplace_back(m[1].first, m[1].second - m[1].first);
        source.remove_prefix(m.suffix().first - source.data()); // update the tail
    }
    return true;
}

/**
 * @brief   Performs self test and returns.
 *
 * @return  true, if self-test has passed.
 */
inline bool FtmTableEntry::self_test() {
    FtmTableEntry tab;
    if constexpr (1) {
        tab.clear();
        constexpr auto fake_tex =
            "#define @\\defnlibxname{cpp_lib_atomic_lock}@  "
            "201907L // also in \\libheader{atomic}"sv;
        if (!(tab.parse_line(fake_tex) and tab.macro_ == "__cpp_lib_atomic_lock" and
              tab.date_ == "201907L" and tab.free_ == false and
              tab.headers_.size() == 1 and tab.headers_[0] == "atomic")) {
            std::cerr << "TESTS #1 FAILED.\n";
            return false;
        }
    }
    if constexpr (1) {
        tab.clear();
        constexpr auto fake_tex =
            "#define @\\defnlibxname{cpp_lib_char8_t}@  201907L // freestanding, "
            "also in \\libheader{atomic}, \\libheader{filesystem},  "
            "// \\libheader{istream}, \\libheader{limits}"sv;
        if (!(tab.parse_line(fake_tex) and tab.macro_ == "__cpp_lib_char8_t" and
              tab.date_ == "201907L" and tab.free_ and
              tab.headers_.size() == 4 and
              tab.headers_[0] == "atomic" and tab.headers_[1] == "filesystem" and
              tab.headers_[2] == "istream" and tab.headers_[3] == "limits")) {
            std::cerr << "TESTS #2 FAILED.\n";
            return false;
        }
    }
    if constexpr (option::verbose)
        std::cerr << "OK. Tests passed.\n";
    return true;
}

/**
 * @brief   Generates the header of the table.
 */
inline std::string_view FtmTableEntry::table_head() noexcept {
    return
R"--(
{|class="wikitable sortable" style="font-size: 100%;"
|-
!Macro name
!Value
!Header
!Free-<br>standing
)--"sv;
}

/**
 * @brief   Generates the footer of the table.
 */
inline std::string FtmTableEntry::table_tail(unsigned entry_count) {
    return
    "|-\n"
    "!colspan=\"4\"|Total number of macros: " + std::to_string(entry_count) + "\n"
    "|}\n";
}

/**
 * @brief   Generates cppreference table entry.
 * @param   source - source string to parse.
 * @return  non-empty table entry string, if success. An empty string otherwise.
 */
inline std::string FtmTableEntry::generate_entry(std::string_view source) {
    if (!parse_line(source))
        return {};

    /* cppreference table entry sample:
    |-
    |{{ftm link|__cpp_lib_byte}}
    |201603L
    |{{header|atomic}} {{header|filesystem}} ...
    |{{yes}}
    */
    std::ostringstream str("", std::ios_base::ate);
    str << "|-\n"
           "|{{ftm link|" << macro_ << "}}\n"
           "|{{tt|" << date_ << "}}\n"
           "|";
    for (int n{}; auto const& header : headers_)
        str << (n++ ? " " : "") << "{{header|" << header << "}}";
    str << (free_ ? "\n|{{yes}}" : "\n|");
    return str.str();
}

class SourceFileReader {
public:
    explicit SourceFileReader(std::string_view const file_name);
    SourceFileReader(SourceFileReader const&) = delete;
    SourceFileReader& operator=(SourceFileReader const&) = delete;

    [[nodiscard]] operator std::ifstream& () noexcept { return file_; }
    [[nodiscard]] std::string_view cur_line() const { return cur_; }
    [[nodiscard]] bool is_open() const { return ok_; }
    [[nodiscard]] bool next_line();
    void next_table();

  private:
    std::ifstream file_;
    std::string cur_;
    std::string next_;
    int table_n_{};
    bool ok_{false};
};

/**
 * @brief   Opens the source TEX file.
 *          Sets the success-flag that should be checked with is_open().
 * @param   file_name - the source TEX file name.
 */
inline SourceFileReader::SourceFileReader(std::string_view const file_name) {
    const auto file = std::filesystem::path{file_name};
    auto ec = std::error_code{};

    if (!std::filesystem::exists(file, ec)) {
        std::cerr << "ERROR: source file not found: " << file << '\n';
        return;
    }

    constexpr std::uintmax_t min_file_size{200'000}; // bytes
    if (std::uintmax_t size; (size = std::filesystem::file_size(file, ec)) < min_file_size) {
        std::cerr << "ERROR: source file: " << file << "\n"
                     "  is too small, size = " << size << " bytes;\n"
                     "  expected size >= " << min_file_size << " bytes\n";
        return;
    }

    if (file_.open(file_name.data()); not(ok_ = file_.is_open())) {
        std::cerr << "ERROR: can't open the source file: " << file << '\n';
        return;
    }
}

/**
 * @brief   Prepare internal state to read a next table.
 */
inline void SourceFileReader::next_table() {
    ok_ = true;
    cur_.clear();
    ++table_n_;
}

/**
 * @brief      Obtains the next line, if it is available (may concatenate few lines)
 * @exception  std::logic_error, in case of wrong TEX file format
 * @return     true, if next line is available
 */
inline bool SourceFileReader::next_line() {
    if (not ok_)
        return false;

    constexpr auto intro{"#define "sv};
    constexpr auto outro{"\\end"sv}; // \end{codeblock}

    if (cur_.empty()) { // This is a first run. Find the first valid line.
        // Expected preliminary line are:
        //
        // "Future revisions of this document might replace"
        // "    the values of these macros with greater values."
        // "\end{note}"
        // ""
        // "\begin{codeblock}"
        // "#define ..."            <== intro
        //
        if (table_n_ == 0) { // main table
            constexpr auto nearest{"Future revisions of this document"sv};
            while ((ok_ = !!std::getline(file_, cur_)) && !cur_.starts_with(nearest)) {}
            while ((ok_ = !!std::getline(file_, cur_)) && !cur_.starts_with(intro)) {}
            if (not ok_)
                throw std::logic_error("Unexpected file format.");
        } else { // hardening table
            constexpr auto nearest{"Additionally, each of the following macros"sv};
            while ((ok_ = !!std::getline(file_, cur_)) && !cur_.starts_with(nearest)) {}
            while ((ok_ = !!std::getline(file_, cur_)) && !cur_.starts_with(intro)) {}
            if (not ok_)
                throw std::logic_error("Unexpected file format.");
        }
    } else {  // This is not the first run. Thus next_ contains the beginning.
        cur_ = std::move(next_);
    }

    // Find the next begin (or EOB), appending the dependent lines.
    while ((ok_ = !!std::getline(file_, next_)) && !next_.starts_with(intro)) {
        if (next_.starts_with(outro)) {
            ok_ = false; // End Of Block
            break;
        }
        cur_ += next_;
    }

    return true;
}

/**
 * @brief   Generates/prints the table with standard library feature-testing macros.
 * @note    The result is a table described in MediaWiki language. The output should be
 *          copied onto "https://en.cppreference.com/w/cpp/utility/feature_test" page.
 */
inline bool FtmTableEntry::generate() {
    SourceDownloader downloader;
    if (not downloader.load()) {
        if constexpr (option::try_local_file_if_download_failed) {
            if constexpr (option::verbose)
                std::cerr << "Trying to use local source file: "
                          << downloader.file_name() << "\n\n";
        } else {
            return false;
        }
    }

    auto lines{SourceFileReader{downloader.file_name()}};
    if (not lines.is_open()) {
        return false;
    }

    for (int table_n{}; table_n != 2; ++table_n, lines.next_table()) {
        auto entry_count{0U};
        std::cout << FtmTableEntry::table_head();
        try {
            for (FtmTableEntry table; lines.next_line(); ) {
                const std::string line{ table.generate_entry(lines.cur_line()) };
                if (not line.empty()) {
                    std::cout << line << '\n';
                    ++entry_count;
                }
            }
        } catch (std::logic_error const& ex) {
            if constexpr (option::verbose)
                std::cerr << "ERROR: " << ex.what() << '\n';
            return false;
        }
        std::cout << FtmTableEntry::table_tail(entry_count) << '\n';

        // The tables' sanity check :)
        constexpr unsigned min_entries[]{167u, 13u}; // as per 2025-04
        if (entry_count < min_entries[table_n]) {
            std::cerr << "\nWARNING: Too few entries! Expected >= " << min_entries[table_n] << '\n';
        }

        if (table_n == 0) {
            std::cout << "\nA hardened implementation also defines the following macros.\n";
        }
    }

    return true;
}

/**
 * @brief   Performs self test and returns.
 *
 * @return  true, if self-tests passed.
 */
inline bool self_tests() {
    if constexpr (not option::enable_self_tests)
        return true;
    return FtmTableEntry::self_test();
}

int main() {
    return self_tests() and FtmTableEntry::generate() ? EXIT_SUCCESS : EXIT_FAILURE;
}