min boilerplate 1/2

This commit is contained in:
lovebird 2026-02-18 11:24:00 +01:00
parent 9c73cec7a3
commit e02c295dac
18 changed files with 519 additions and 54 deletions

31
.gitignore vendored
View File

@ -1,4 +1,29 @@
/node_modules
/coverage
*.log
# Build output
/build/
# Compiled objects
*.o
*.obj
*.exe
*.out
*.app
# CMake generated
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
Makefile
# IDE / Editor
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
*.log

View File

@ -1,4 +0,0 @@
./docs
./scripts
./tests
./incoming

66
CMakeLists.txt Normal file
View File

@ -0,0 +1,66 @@
cmake_minimum_required(VERSION 3.20)
project(polymech-cli
VERSION 0.1.0
DESCRIPTION "Polymech C++ CLI"
LANGUAGES CXX C
)
# C++ standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Dependencies
include(FetchContent)
FetchContent_Declare(
cli11
GIT_REPOSITORY https://github.com/CLIUtils/CLI11.git
GIT_TAG v2.4.2
GIT_SHALLOW TRUE
)
FetchContent_Declare(
tomlplusplus
GIT_REPOSITORY https://github.com/marzer/tomlplusplus.git
GIT_TAG v3.4.0
GIT_SHALLOW TRUE
)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.7.1
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(cli11 tomlplusplus Catch2)
# Packages
add_subdirectory(packages/logger)
add_subdirectory(packages/html)
add_subdirectory(packages/postgres)
# Sources
add_executable(${PROJECT_NAME}
src/main.cpp
)
target_link_libraries(${PROJECT_NAME} PRIVATE CLI11::CLI11 tomlplusplus::tomlplusplus logger html postgres)
# Compiler warnings
if(MSVC)
target_compile_options(${PROJECT_NAME} PRIVATE /W4 /permissive-)
else()
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic)
endif()
# Install
install(TARGETS ${PROJECT_NAME}
RUNTIME DESTINATION bin
)
# Tests
enable_testing()
add_subdirectory(tests)

36
CMakePresets.json Normal file
View File

@ -0,0 +1,36 @@
{
"version": 6,
"cmakeMinimumRequired": {
"major": 3,
"minor": 20,
"patch": 0
},
"configurePresets": [
{
"name": "dev",
"displayName": "Dev (Debug)",
"binaryDir": "${sourceDir}/build/dev",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "release",
"displayName": "Release",
"binaryDir": "${sourceDir}/build/release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
],
"buildPresets": [
{
"name": "dev",
"configurePreset": "dev"
},
{
"name": "release",
"configurePreset": "release"
}
]
}

View File

@ -1,3 +1,33 @@
# osr-package-template
# polymech-cli
Package basics
Cross-platform C++ CLI built with CMake.
## Prerequisites
| Tool | Version |
|------|---------|
| CMake | ≥ 3.20 |
| C++ compiler | C++17 (MSVC, GCC, or Clang) |
## Build
```bash
# Debug
cmake --preset dev
cmake --build --preset dev
# Release
cmake --preset release
cmake --build --preset release
```
## Usage
```bash
polymech-cli --help
polymech-cli --version
```
## License
BSD-3-Clause

12
config.toml Normal file
View File

@ -0,0 +1,12 @@
[project]
name = "polymech"
version = "0.1.0"
description = "Polymech C++ CLI"
[database]
host = "localhost"
port = 5432
name = "polymech"
[logging]
level = "debug"

View File

@ -1,45 +0,0 @@
{
"name": "@plastichub/template",
"description": "",
"version": "0.3.1",
"main": "main.js",
"typings": "index.d.ts",
"publishConfig": {
"access": "public"
},
"bin": {
"osr-bin": "main.js"
},
"dependencies": {
"@types/node": "^14.17.5",
"@types/yargs": "^17.0.2",
"chalk": "^2.4.1",
"convert-units": "^2.3.4",
"env-var": "^7.0.1",
"typescript": "^4.3.5",
"yargs": "^14.2.3",
"yargs-parser": "^15.0.3"
},
"scripts": {
"test": "tsc; mocha --full-trace mocha \"spec/**/*.spec.js\"",
"test-with-coverage": "istanbul cover node_modules/.bin/_mocha -- 'spec/**/*.spec.js'",
"lint": "tslint --project=./tsconfig.json",
"build": "tsc -p .",
"dev": "tsc -p . --declaration -w",
"typings": "tsc --declaration",
"docs": "npx typedoc src/index.ts",
"dev-test-watch": "mocha-typescript-watch"
},
"homepage": "https://git.osr-plastic.org/plastichub/lib-content",
"repository": {
"type": "git",
"url": "https://git.osr-plastic.org/plastichub/lib-content.git"
},
"engines": {
"node": ">= 14.0.0"
},
"license": "BSD-3-Clause",
"keywords": [
"typescript"
]
}

View File

@ -0,0 +1,26 @@
include(FetchContent)
FetchContent_Declare(
lexbor
GIT_REPOSITORY https://github.com/lexbor/lexbor.git
GIT_TAG v2.4.0
GIT_SHALLOW TRUE
)
# Build lexbor as static
set(LEXBOR_BUILD_SHARED OFF CACHE BOOL "" FORCE)
set(LEXBOR_BUILD_STATIC ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(lexbor)
add_library(html STATIC
src/html.cpp
)
target_include_directories(html
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_link_libraries(html
PUBLIC lexbor_static
)

View File

@ -0,0 +1,21 @@
#pragma once
#include <string>
#include <vector>
namespace html {
/// Parsed element — tag name + text content.
struct Element {
std::string tag;
std::string text;
};
/// Parse an HTML string and return all elements with their text content.
std::vector<Element> parse(const std::string &html_str);
/// Extract the text content of all elements matching a CSS selector.
std::vector<std::string> select(const std::string &html_str,
const std::string &selector);
} // namespace html

129
packages/html/src/html.cpp Normal file
View File

@ -0,0 +1,129 @@
#include "html/html.h"
#include <lexbor/css/css.h>
#include <lexbor/html/html.h>
#include <lexbor/selectors/selectors.h>
namespace html {
// ── helpers ─────────────────────────────────────────────────────────────────
static std::string node_text(lxb_dom_node_t *node) {
size_t len = 0;
lxb_char_t *text = lxb_dom_node_text_content(node, &len);
if (!text)
return {};
std::string result(reinterpret_cast<const char *>(text), len);
lxb_dom_document_destroy_text(node->owner_document, text);
return result;
}
static std::string tag_name(lxb_dom_element_t *el) {
size_t len = 0;
const lxb_char_t *name = lxb_dom_element_qualified_name(el, &len);
if (!name)
return {};
return std::string(reinterpret_cast<const char *>(name), len);
}
// ── walk tree recursively ───────────────────────────────────────────────────
static void walk(lxb_dom_node_t *node, std::vector<Element> &out) {
if (!node)
return;
if (node->type == LXB_DOM_NODE_TYPE_ELEMENT) {
auto *el = lxb_dom_interface_element(node);
auto txt = node_text(node);
if (!txt.empty()) {
out.push_back({tag_name(el), txt});
}
}
auto *child = node->first_child;
while (child) {
walk(child, out);
child = child->next;
}
}
// ── public API ──────────────────────────────────────────────────────────────
std::vector<Element> parse(const std::string &html_str) {
auto *doc = lxb_html_document_create();
if (!doc)
return {};
auto status = lxb_html_document_parse(
doc, reinterpret_cast<const lxb_char_t *>(html_str.c_str()),
html_str.size());
std::vector<Element> result;
if (status == LXB_STATUS_OK) {
auto *body = lxb_dom_interface_node(lxb_html_document_body_element(doc));
walk(body, result);
}
lxb_html_document_destroy(doc);
return result;
}
// ── CSS selector callback ───────────────────────────────────────────────────
struct SelectCtx {
std::vector<std::string> *out;
};
static lxb_status_t select_cb(lxb_dom_node_t *node,
lxb_css_selector_specificity_t spec, void *ctx) {
(void)spec;
auto *sctx = static_cast<SelectCtx *>(ctx);
auto txt = node_text(node);
if (!txt.empty()) {
sctx->out->push_back(txt);
}
return LXB_STATUS_OK;
}
std::vector<std::string> select(const std::string &html_str,
const std::string &selector) {
std::vector<std::string> result;
// Parse document
auto *doc = lxb_html_document_create();
if (!doc)
return result;
auto status = lxb_html_document_parse(
doc, reinterpret_cast<const lxb_char_t *>(html_str.c_str()),
html_str.size());
if (status != LXB_STATUS_OK) {
lxb_html_document_destroy(doc);
return result;
}
// Set up CSS parser + selectors engine
auto *css_parser = lxb_css_parser_create();
lxb_css_parser_init(css_parser, nullptr);
auto *selectors = lxb_selectors_create();
lxb_selectors_init(selectors);
auto *list = lxb_css_selectors_parse(
css_parser, reinterpret_cast<const lxb_char_t *>(selector.c_str()),
selector.size());
if (list) {
SelectCtx ctx{&result};
lxb_selectors_find(
selectors, lxb_dom_interface_node(lxb_html_document_body_element(doc)),
list, select_cb, &ctx);
lxb_css_selector_list_destroy_memory(list);
}
lxb_selectors_destroy(selectors, true);
lxb_css_parser_destroy(css_parser, true);
lxb_html_document_destroy(doc);
return result;
}
} // namespace html

View File

@ -0,0 +1,21 @@
include(FetchContent)
FetchContent_Declare(
spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.15.1
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(spdlog)
add_library(logger STATIC
src/logger.cpp
)
target_include_directories(logger
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_link_libraries(logger
PUBLIC spdlog::spdlog
)

View File

@ -0,0 +1,16 @@
#pragma once
#include <string>
namespace logger {
/// Initialize the default logger (call once at startup).
void init(const std::string &app_name = "polymech");
/// Log at various levels.
void info(const std::string &msg);
void warn(const std::string &msg);
void error(const std::string &msg);
void debug(const std::string &msg);
} // namespace logger

View File

@ -0,0 +1,21 @@
#include "logger/logger.h"
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/spdlog.h>
namespace logger {
void init(const std::string &app_name) {
auto console = spdlog::stdout_color_mt(app_name);
spdlog::set_default_logger(console);
spdlog::set_level(spdlog::level::debug);
spdlog::set_pattern("[%H:%M:%S] [%^%l%$] %v");
}
void info(const std::string &msg) { spdlog::info(msg); }
void warn(const std::string &msg) { spdlog::warn(msg); }
void error(const std::string &msg) { spdlog::error(msg); }
void debug(const std::string &msg) { spdlog::debug(msg); }
} // namespace logger

View File

@ -0,0 +1,11 @@
add_library(postgres STATIC
src/postgres.cpp
)
target_include_directories(postgres
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_link_libraries(postgres
PUBLIC logger
)

View File

@ -0,0 +1,11 @@
#pragma once
#include <string>
namespace postgres {
/// Connect to a PostgreSQL database (stub).
/// Returns a human-readable status string.
std::string connect(const std::string &connection_string);
} // namespace postgres

View File

@ -0,0 +1,12 @@
#include "postgres/postgres.h"
#include "logger/logger.h"
namespace postgres {
std::string connect(const std::string &connection_string) {
logger::debug("postgres::connect → " + connection_string);
// stub — no real connection
return "ok";
}
} // namespace postgres

0
src/.gitignore vendored
View File

77
src/main.cpp Normal file
View File

@ -0,0 +1,77 @@
#include <iostream>
#include <string>
#include <CLI/CLI.hpp>
#include <toml++/toml.hpp>
#include "html/html.h"
#include "logger/logger.h"
#include "postgres/postgres.h"
#ifndef PROJECT_VERSION
#define PROJECT_VERSION "0.1.0"
#endif
int main(int argc, char *argv[]) {
CLI::App app{"polymech-cli — Polymech C++ CLI", "polymech-cli"};
app.set_version_flag("-v,--version", PROJECT_VERSION);
// Subcommand: parse HTML
std::string html_input;
auto *parse_cmd = app.add_subcommand("parse", "Parse HTML and list elements");
parse_cmd->add_option("html", html_input, "HTML string to parse")->required();
// Subcommand: select from HTML
std::string select_input;
std::string selector;
auto *select_cmd =
app.add_subcommand("select", "CSS-select elements from HTML");
select_cmd->add_option("html", select_input, "HTML string")->required();
select_cmd->add_option("selector", selector, "CSS selector")->required();
// Subcommand: config — read a TOML file
std::string config_path;
auto *config_cmd =
app.add_subcommand("config", "Read and display a TOML config file");
config_cmd->add_option("file", config_path, "Path to TOML file")->required();
CLI11_PARSE(app, argc, argv);
logger::init("polymech-cli");
if (parse_cmd->parsed()) {
auto elements = html::parse(html_input);
logger::info("Parsed " + std::to_string(elements.size()) + " elements");
for (const auto &el : elements) {
std::cout << "<" << el.tag << "> " << el.text << "\n";
}
return 0;
}
if (select_cmd->parsed()) {
auto matches = html::select(select_input, selector);
logger::info("Matched " + std::to_string(matches.size()) + " elements");
for (const auto &m : matches) {
std::cout << m << "\n";
}
return 0;
}
if (config_cmd->parsed()) {
try {
auto tbl = toml::parse_file(config_path);
logger::info("Loaded config: " + config_path);
std::cout << tbl << "\n";
} catch (const toml::parse_error &err) {
logger::error("TOML parse error: " + std::string(err.what()));
return 1;
}
return 0;
}
// Default: demo
auto status = postgres::connect("postgresql://localhost:5432/polymech");
logger::info("polymech-cli " + std::string(PROJECT_VERSION) +
" ready (pg: " + status + ")");
return 0;
}