Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/website-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
-DSOURCEMETA_CORE_LANG_NUMERIC:BOOL=OFF
-DSOURCEMETA_CORE_LANG_ERROR:BOOL=OFF
-DSOURCEMETA_CORE_LANG_OPTIONS:BOOL=OFF
-DSOURCEMETA_CORE_LANG_TEXT:BOOL=OFF
-DSOURCEMETA_CORE_UNICODE:BOOL=OFF
-DSOURCEMETA_CORE_PUNYCODE:BOOL=OFF
-DSOURCEMETA_CORE_TIME:BOOL=OFF
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/website-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
-DSOURCEMETA_CORE_LANG_NUMERIC:BOOL=OFF
-DSOURCEMETA_CORE_LANG_ERROR:BOOL=OFF
-DSOURCEMETA_CORE_LANG_OPTIONS:BOOL=OFF
-DSOURCEMETA_CORE_LANG_TEXT:BOOL=OFF
-DSOURCEMETA_CORE_UNICODE:BOOL=OFF
-DSOURCEMETA_CORE_PUNYCODE:BOOL=OFF
-DSOURCEMETA_CORE_TIME:BOOL=OFF
Expand Down
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ option(SOURCEMETA_CORE_LANG_PARALLEL "Build the Sourcemeta Core language paralle
option(SOURCEMETA_CORE_LANG_NUMERIC "Build the Sourcemeta Core language numeric library" ON)
option(SOURCEMETA_CORE_LANG_ERROR "Build the Sourcemeta Core language error library" ON)
option(SOURCEMETA_CORE_LANG_OPTIONS "Build the Sourcemeta Core Options library" ON)
option(SOURCEMETA_CORE_LANG_TEXT "Build the Sourcemeta Core language text library" ON)
option(SOURCEMETA_CORE_UNICODE "Build the Sourcemeta Core Unicode library" ON)
option(SOURCEMETA_CORE_PUNYCODE "Build the Sourcemeta Core Punycode library" ON)
option(SOURCEMETA_CORE_TIME "Build the Sourcemeta Core time library" ON)
Expand Down Expand Up @@ -93,6 +94,10 @@ if(SOURCEMETA_CORE_LANG_OPTIONS)
add_subdirectory(src/lang/options)
endif()

if(SOURCEMETA_CORE_LANG_TEXT)
add_subdirectory(src/lang/text)
endif()

if(SOURCEMETA_CORE_UNICODE)
add_subdirectory(src/core/unicode)
endif()
Expand Down Expand Up @@ -231,6 +236,10 @@ if(SOURCEMETA_CORE_TESTS)
add_subdirectory(test/options)
endif()

if(SOURCEMETA_CORE_LANG_TEXT)
add_subdirectory(test/text)
endif()

if(SOURCEMETA_CORE_UNICODE)
add_subdirectory(test/unicode)
endif()
Expand Down
3 changes: 3 additions & 0 deletions config.cmake.in
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ if(NOT SOURCEMETA_CORE_COMPONENTS)
list(APPEND SOURCEMETA_CORE_COMPONENTS markdown)
list(APPEND SOURCEMETA_CORE_COMPONENTS error)
list(APPEND SOURCEMETA_CORE_COMPONENTS options)
list(APPEND SOURCEMETA_CORE_COMPONENTS text)
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding text to the default SOURCEMETA_CORE_COMPONENTS list means an installation built with -DSOURCEMETA_CORE_LANG_TEXT=OFF could still try to include(.../sourcemeta_core_text.cmake) by default and fail at find_package time if that export wasn’t installed. Consider guarding default component selection / inclusion based on whether the component was built and installed.

Severity: medium

Other Locations
  • config.cmake.in:141

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

endif()

include(CMakeFindDependencyMacro)
Expand Down Expand Up @@ -136,6 +137,8 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS})
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_error.cmake")
elseif(component STREQUAL "options")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_options.cmake")
elseif(component STREQUAL "text")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_text.cmake")
else()
message(FATAL_ERROR "Unknown Sourcemeta Core component: ${component}")
endif()
Expand Down
6 changes: 6 additions & 0 deletions src/lang/text/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME text
SOURCES text.cc)

if(SOURCEMETA_CORE_INSTALL)
sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME text)
endif()
39 changes: 39 additions & 0 deletions src/lang/text/include/sourcemeta/core/text.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#ifndef SOURCEMETA_CORE_TEXT_H_
#define SOURCEMETA_CORE_TEXT_H_

#ifndef SOURCEMETA_CORE_TEXT_EXPORT
#include <sourcemeta/core/text_export.h>
#endif

#include <string> // std::string

/// @defgroup text Text
/// @brief A collection of general-purpose text manipulation utilities
///
/// This functionality is included as follows:
///
/// ```cpp
/// #include <sourcemeta/core/text.h>
/// ```

namespace sourcemeta::core {

/// @ingroup text
///
/// Convert a string to Title Case in place. For example:
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment calls this “Title Case”, but the implementation only uppercases the first character of each segment and leaves the rest unchanged (it does not lowercase existing uppercase letters). Consider clarifying the documented semantics so callers don’t assume full title-casing behavior.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

///
/// ```cpp
/// #include <sourcemeta/core/text.h>
/// #include <cassert>
/// #include <string>
///
/// std::string value{"hello_world"};
/// sourcemeta::core::to_title_case(value);
/// assert(value == "Hello World");
/// ```
SOURCEMETA_CORE_TEXT_EXPORT
auto to_title_case(std::string &value) -> void;

} // namespace sourcemeta::core

#endif
37 changes: 37 additions & 0 deletions src/lang/text/text.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#include <sourcemeta/core/text.h>

#include <cctype> // std::isalpha, std::toupper
#include <cstddef> // std::size_t

namespace sourcemeta::core {

auto to_title_case(std::string &value) -> void {
std::size_t write{0};
bool capitalize_next{true};
bool pending_separator{false};
for (const char character : value) {
if (character == '_' || character == '-') {
if (write > 0) {
pending_separator = true;
}
} else {
if (pending_separator) {
value[write++] = ' ';
pending_separator = false;
capitalize_next = true;
}
if (capitalize_next) {
value[write++] = static_cast<char>(
std::toupper(static_cast<unsigned char>(character)));
if (std::isalpha(static_cast<unsigned char>(character))) {
capitalize_next = false;
}
} else {
value[write++] = character;
}
}
}
value.resize(write);
}

} // namespace sourcemeta::core
1 change: 1 addition & 0 deletions test/packaging/find_package/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ target_link_libraries(core_hello PRIVATE sourcemeta::core::markdown)
target_link_libraries(core_hello PRIVATE sourcemeta::core::options)
target_link_libraries(core_hello PRIVATE sourcemeta::core::preprocessor)
target_link_libraries(core_hello PRIVATE sourcemeta::core::jsonrpc)
target_link_libraries(core_hello PRIVATE sourcemeta::core::text)
1 change: 1 addition & 0 deletions test/packaging/find_package/hello.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <sourcemeta/core/parallel.h>
#include <sourcemeta/core/preprocessor.h>
#include <sourcemeta/core/punycode.h>
#include <sourcemeta/core/text.h>
#include <sourcemeta/core/time.h>
#include <sourcemeta/core/unicode.h>
#include <sourcemeta/core/uri.h>
Expand Down
5 changes: 5 additions & 0 deletions test/text/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
sourcemeta_googletest(NAMESPACE sourcemeta PROJECT core NAME text
SOURCES text_to_title_case_test.cc)

target_link_libraries(sourcemeta_core_text_unit
PRIVATE sourcemeta::core::text)
179 changes: 179 additions & 0 deletions test/text/text_to_title_case_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#include <gtest/gtest.h>

#include <sourcemeta/core/text.h>

#include <string> // std::string

TEST(Text_to_title_case, empty_string) {
std::string value{""};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "");
}

TEST(Text_to_title_case, single_lowercase_character) {
std::string value{"a"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "A");
}

TEST(Text_to_title_case, single_uppercase_character) {
std::string value{"A"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "A");
}

TEST(Text_to_title_case, single_underscore_is_empty) {
std::string value{"_"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "");
}

TEST(Text_to_title_case, single_dash_is_empty) {
std::string value{"-"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "");
}

TEST(Text_to_title_case, only_separators_is_empty) {
std::string value{"___"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "");
}

TEST(Text_to_title_case, single_lowercase_word) {
std::string value{"hello"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "Hello");
}

TEST(Text_to_title_case, already_title_cased_word) {
std::string value{"Hello"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "Hello");
}

TEST(Text_to_title_case, all_uppercase_word) {
std::string value{"HELLO"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "HELLO");
}

TEST(Text_to_title_case, snake_case_two_words) {
std::string value{"hello_world"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "Hello World");
}

TEST(Text_to_title_case, kebab_case_two_words) {
std::string value{"hello-world"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "Hello World");
}

TEST(Text_to_title_case, mixed_snake_and_kebab_separators) {
std::string value{"hello_world-test"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "Hello World Test");
}

TEST(Text_to_title_case, snake_case_three_words) {
std::string value{"abc_def_ghi"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "Abc Def Ghi");
}

TEST(Text_to_title_case, preserves_existing_uppercase_after_separator) {
std::string value{"Hello_World"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "Hello World");
}

TEST(Text_to_title_case, leading_underscore_is_stripped) {
std::string value{"_hello"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "Hello");
}

TEST(Text_to_title_case, leading_dash_is_stripped) {
std::string value{"-hello"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "Hello");
}

TEST(Text_to_title_case, trailing_underscore_is_stripped) {
std::string value{"hello_"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "Hello");
}

TEST(Text_to_title_case, trailing_dash_is_stripped) {
std::string value{"hello-"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "Hello");
}

TEST(Text_to_title_case, separators_around_word_are_stripped) {
std::string value{"_hello_"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "Hello");
}

TEST(Text_to_title_case, multiple_leading_separators_are_stripped) {
std::string value{"__hello"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "Hello");
}

TEST(Text_to_title_case, multiple_trailing_separators_are_stripped) {
std::string value{"hello__"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "Hello");
}

TEST(Text_to_title_case, consecutive_separators_collapse_to_single_space) {
std::string value{"hello__world"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "Hello World");
}

TEST(Text_to_title_case, mixed_consecutive_separators_collapse) {
std::string value{"hello_-world"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "Hello World");
}

TEST(Text_to_title_case, single_letter_words) {
std::string value{"a_b_c"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "A B C");
}

TEST(Text_to_title_case, digits_pass_through) {
std::string value{"abc123def"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "Abc123def");
}

TEST(Text_to_title_case, digit_after_separator) {
std::string value{"abc_123"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "Abc 123");
}

TEST(Text_to_title_case, letter_after_leading_digits_in_segment) {
std::string value{"abc_123def"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "Abc 123Def");
}

TEST(Text_to_title_case, leading_digits_then_letter_at_start) {
std::string value{"123abc"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "123Abc");
}

TEST(Text_to_title_case, space_in_input_is_not_a_separator) {
std::string value{"hello world"};
sourcemeta::core::to_title_case(value);
EXPECT_EQ(value, "Hello world");
}
Loading