Skip to content

CkText

Job: clean, shape, compare, check and write out everyday text — names, addresses, free-form fields, delimited lists — so the rest of your code never meets raw input.

Text from a database, a form or a payload arrives messy: stray HTML, shouted capitals, invisible characters, doubled spaces. CkText turns that into something you can safely store, compare and show. Like every CommonKit class it never throws by default, and its rejection value is the empty string: non-empty in, empty out means the value was rejected — check for "" at the call site. Methods that take a fallback run the fallback through the same cleaning and checks, so a bad fallback comes back empty too.

Quick look

using SafeCoder.CommonKit;

CkText.String("  <b>Hello</b>   WORLD  ");   // -> "Hello WORLD"
CkText.Name("JOHN SMITH");                   // -> "John Smith"
CkText.Digits("ID: 12-345 678");             // -> "12345678"
CkText.Slug("Hello, World!");                // -> "hello-world"
CkText.Mask("123-45-6789");                  // -> "***-**-6789"
CkText.String("<p></p>");                    // -> ""  (nothing left — rejected)
CkText.Contains("Hello, World", "world");    // -> true
CkText.IsDigits(" 12345 ");                  // -> true
CkText.WordCount("<p>Hello</p><p>World</p>"); // -> 2
CkText.Initials("mary-jane o'brien");   // -> "MO"
CkText.Plural(3, "box");                // -> "3 boxes"
CkText.Explain("<p></p>");              // -> "nothing left once cleaned"

Methods

String

The general cleaner.

string String(
    string? input = null,
    TextCase textCase = TextCase.None,
    int maxLength = CkSettings.MaxTextInputLength,
    bool clearEscape = false,
    bool normalize = false,
    bool decodeEntities = true,
    bool truncate = true,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw value to clean.
textCase TextCase TextCase.None The casing to apply; None leaves casing alone.
maxLength int CkSettings.MaxTextInputLength (50,000) Maximum length of the cleaned value, measured after markup removal and with outer whitespace set aside — the padding of a fixed-width column is not content. Values below 1 fall back to the default.
clearEscape bool false Flatten line breaks and tabs to spaces.
normalize bool false Replace every character that is not an ASCII letter, digit or whitespace with _ (runs collapse to one), and collapse every whitespace run to a single space.
decodeEntities bool true Decode HTML entities as part of markup removal, so &amp; becomes &. Pass false to strip tags but leave entity text alone — for a URL, where &reg; in a query string is a value, not markup.
truncate bool true Cut an over-long value to fit (the default) instead of rejecting it. The cut runs last and never splits a surrogate pair.
fallback string? null Backup value used when the result would be empty. It goes through the same cleaning and checks as input; a fallback that is itself rejected answers "".
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkTextException that says why, instead of answering "", when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
The value (or the fallback) cleans to usable text The cleaned text: "Hello WORLD" The cleaned text: "Hello WORLD"
Rejected: missing, nothing left once cleaned, or over maxLength with truncate off Empty string Throws CkTextException — carries Input, Expected and Reason

Markup is stripped with patterns, not truly parsed — see Html for the caveats.

String is the general cleaner. It strips markup (HTML tags), decodes entities (&amp; becomes &), removes control and invisible characters, applies the casing you ask for, then tidies whitespace. Switches: clearEscape flattens line breaks and tabs to spaces, normalize replaces everything that is not an ASCII letter, digit or whitespace with _, decodeEntities: false leaves entity text alone (useful for URLs, where &reg; is a value, not markup), and truncate: false rejects an over-long value instead of cutting it to maxLength.

CkText.String("  <b>Hello</b>   WORLD  ");   // -> "Hello WORLD"
CkText.String(
    input: "JOHN DOE",
    textCase: TextCase.Lowercase
);   // -> "john doe"

The textCase parameter is a TextCase enum (from SafeCoder.CommonKit.Enums): None (the default — leave casing alone), Lowercase, Uppercase, Titlecase (capitalize every word), Sentencecase (capitalize every sentence), Capitalize (first letter only), and TitlecaseUnlessMixed (title-case only when the value arrives all-caps or all-lowercase).

Name

A person or company name.

string Name(
    string? input = null,
    int maxLength = CkSettings.MaxNameInputLength,
    bool normalize = false,
    TextCase textCase = TextCase.TitlecaseUnlessMixed,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw name value.
maxLength int CkSettings.MaxNameInputLength (255) Maximum length of the cleaned name, measured after markup removal. An over-long name is truncated, not rejected.
normalize bool false Replace every character that is not an ASCII letter, digit or whitespace with _. Off by default — it would turn "O'Brien" into "O_Brien"; enable only when an ASCII-safe value is required.
textCase TextCase TextCase.TitlecaseUnlessMixed The casing to apply: title-case only a value that arrives all-caps or all-lowercase. Pass TextCase.Titlecase to re-case mixed input mechanically, or TextCase.None to leave casing alone.
fallback string? null Backup value used when the result would be empty. It goes through the same cleaning and checks as input; a fallback that is itself rejected answers "".
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkTextException that says why, instead of answering "", when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
The value (or the fallback) cleans to a name The cleaned name: "John Smith" The cleaned name: "John Smith"
Rejected: missing, or nothing left once cleaned Empty string Throws CkTextException — carries Input, Expected and Reason

Title-casing is mechanical: "mcdonald" becomes "Mcdonald", not "McDonald".

Name cleans a person or company name. Its default casing is TextCase.TitlecaseUnlessMixed: shouting is tidied, but a deliberate mix like "McDonald" is kept exactly as typed. Over-long names are truncated rather than rejected — a clipped name is more useful than none.

CkText.Name("JOHN SMITH");        // -> "John Smith"
CkText.Name("o'brien-smith");     // -> "O'Brien-Smith"
CkText.Name("John McDonald");     // -> "John McDonald"  (mixed case — kept as typed)

Address

A multi-line postal address.

string Address(
    string? input = null,
    int maxLength = CkSettings.MaxAddressInputLength,
    TextCase textCase = TextCase.None,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw address value, one line per address line.
maxLength int CkSettings.MaxAddressInputLength (1,024) Maximum length of the cleaned address, measured after markup removal. Values below 1 fall back to the default.
textCase TextCase TextCase.None The casing to apply; None preserves what the user typed. Titlecase tidies shouted input but also lower-cases meaningful capitals, turning "PO BOX 12 NW" into "Po Box 12 Nw".
fallback string? null Backup value used when the result would be empty. It goes through the same cleaning and checks as input; a fallback that is itself rejected answers "".
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkTextException that says why, instead of answering "", when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
The value (or the fallback) cleans to an address The cleaned lines joined with \n: "1 Main St\nSpringfield" The cleaned lines joined with \n: "1 Main St\nSpringfield"
Rejected: missing, nothing left once cleaned, or over the limit Empty string Throws CkTextException — carries Input, Expected and Reason

Presentation cleanup, not verification — nothing confirms the street, city or country exists, and surplus lines beyond CkSettings.MaxAddressLines (8) are discarded.

Address cleans a multi-line postal address. Line breaks are kept but reduced to one per line, each line is trimmed, blank lines are dropped, and lines are joined with \n whatever the input used. Casing is left alone by default, because address abbreviations like "NW" mean something.

CkText.Address("  1 Main St  \n\n  Springfield  ");   // -> "1 Main St\nSpringfield"

Html

The plain text out of markup.

string Html(
    string? input = null,
    int maxLength = CkSettings.MaxTextInputLength,
    bool truncate = true,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw HTML, or plain text.
maxLength int CkSettings.MaxTextInputLength (50,000) Maximum length of the extracted text. Values below 1 fall back to the default.
truncate bool true Cut over-long text to fit (the default) instead of rejecting it. The cut never splits a surrogate pair.
fallback string? null Backup value used when the result would be empty. It goes through the same cleaning and checks as input; a fallback that is itself rejected answers "".
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkTextException that says why, instead of answering "", when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
The value (or the fallback) holds readable text The extracted text: "Save & close" The extracted text: "Save & close"
Rejected: missing, nothing left once the markup is gone, or over the limit with truncate off Empty string Throws CkTextException — carries Input, Expected and Reason

Text extraction, not an XSS boundary — markup is matched with patterns, and an unclosed <script or <!-- swallows the rest of the value.

Html extracts the plain text from HTML: script and style bodies are discarded, block tags become line breaks, other tags become spaces, entities are decoded, and every line ending (\r\n, a bare \r) comes out as \n. Every other cleaner runs this step first — call it directly only when you want the text of an HTML value and nothing else done to it.

CkText.Html("<p>Save &amp; close</p>");        // -> "Save & close"
CkText.Html("<script>alert(1)</script>Done");  // -> "Done"

Digits

Only the digits.

string Digits(
    string? input = null,
    int minLength = 1,
    int maxLength = CkSettings.MaxDigitsLength,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw value.
minLength int 1 Fewest digits accepted; values with fewer are rejected. Values below 1 count as 1.
maxLength int CkSettings.MaxDigitsLength (64) Most digits accepted; values with more are rejected. Values below 1 fall back to the default.
fallback string? null Backup value used when the result would be empty. It goes through the same cleaning and checks as input; a fallback that is itself rejected answers "".
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkTextException that says why, instead of answering "", when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
The value (or the fallback) yields an in-range digit count The digits: "12345678" The digits: "12345678"
Rejected: missing, oversized, a contradictory range, or a digit count outside it Empty string Throws CkTextException — carries Input, Expected and Reason

Only ASCII 0–9 count — Arabic-Indic, fullwidth and other Unicode digits are dropped like any other character.

Digits keeps only the ASCII digits 0–9 — for account, policy and member numbers that arrive with prefixes or separators. minLength and maxLength bound the digit count; an out-of-range count is rejected, never truncated, so a mangled number can never pass as a valid one.

CkText.Digits("ID: 12-345 678");   // -> "12345678"
CkText.Digits("$1,234.56");        // -> "123456"  (not an amount any more!)

Truncate

Cut to a length.

string Truncate(
    string? input = null,
    int maxLength = CkSettings.MaxTextInputLength,
    bool atWordBoundary = false,
    string? ellipsis = null,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The value to shorten.
maxLength int CkSettings.MaxTextInputLength (50,000) Maximum length of the result, ellipsis included.
atWordBoundary bool false Move the cut back to the last whitespace inside the kept part, so no word is chopped in half. A value with no whitespace before the limit is cut mid-word.
ellipsis string? null Text appended when the value was shortened, e.g. "…" or "...". Null or empty appends nothing.
fallback string? null Backup value used when the result would be empty. It gets the same trim-and-shorten treatment; a fallback that is itself rejected answers "".
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkTextException that says why, instead of answering "", when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
The value (or the fallback) fits, or can be cut to fit The trimmed value unchanged, or the shortened value: "hello" The same
Missing, maxLength below 1, or the ellipsis alone would not fit Empty string Throws CkTextException — carries Input, Expected and Reason

The cut never splits a surrogate pair — one emoji is two characters, kept together.

Truncate cuts a value down to at most maxLength characters. It does not clean — it only trims surrounding whitespace and shortens. An optional ellipsis is appended within the limit, and atWordBoundary: true moves the cut back so no word is chopped in half. A cut is never an error: throwIfInvalid: true fires only when nothing usable came in — no value, a limit below 1, an ellipsis that would not fit.

CkText.Truncate(
    input: "hello world",
    maxLength: 5
);   // -> "hello"
CkText.Truncate(
    input: "hello world",
    maxLength: 5,
    ellipsis: "…"
);   // -> "hell…"  (the ellipsis counts toward the limit)
CkText.Truncate(
    input: "the quick brown fox",
    maxLength: 12,
    atWordBoundary: true
);   // -> "the quick"

Mask

Hide all but the last few.

string Mask(
    string? input = null,
    int keepLast = CkSettings.DefaultMaskKeepLast,
    char maskChar = '*'
)
Parameter Type Default What it does
input string? null The sensitive value. It is not cleaned or validated first — mask what you have.
keepLast int CkSettings.DefaultMaskKeepLast (4) How many trailing letters and digits stay visible; negatives count as 0.
maskChar char '*' The replacement character. Whitespace and control characters fall back to '*'.

Returns

When You get
The value has content The masked value: "***-**-6789" — a value with no more letters and digits than keepLast is masked completely
Missing or whitespace-only Empty string — and it never throws

The result still reveals the length and separator layout — the usual trade-off for a recognisable "ends in 6789".

Mask hides a value for display or logging: every letter and digit except the last keepLast (4 by default) becomes maskChar ('*' by default), while spaces and punctuation stay, so the shape is still recognisable. A value with no more letters and digits than keepLast is masked completely — a short value never leaks whole. The value is not cleaned first: mask what you have.

CkText.Mask("123-45-6789");           // -> "***-**-6789"
CkText.Mask("4111 1111 1111 1234");   // -> "**** **** **** 1234"
CkText.Mask("42");                    // -> "**"  (too short to show anything)

Slug

A key for urls and file names.

string Slug(
    string? input = null,
    char separator = '-',
    int maxLength = CkSettings.MaxSlugLength,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw value.
separator char '-' Placed between words. Letters, digits, whitespace and control characters fall back to '-'.
maxLength int CkSettings.MaxSlugLength (100) Maximum length of the slug; a cut lands on the last separator inside the limit where one exists. Values below 1 fall back to the default.
fallback string? null Backup value used when the result would be empty. It goes through the same cleaning and checks as input; a fallback that is itself rejected answers "".
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkTextException that says why, instead of answering "", when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
The value (or the fallback) keeps a letter or digit The slug: "hello-world" The slug: "hello-world"
Rejected: missing, or no letter or digit survives Empty string Throws CkTextException — carries Input, Expected and Reason

Common special letters transliterate, not just fold — "Straße" becomes "strasse".

Slug turns a value into a slug — lower-case ASCII letters and digits with a single separator ('-' by default) between words — for URLs, file names and lookup keys. Accents fold to their base letter, apostrophes are dropped, and everything else acts as a word boundary. A cut for maxLength lands on the last separator inside the limit where one exists, so no word is left half-finished.

CkText.Slug("Hello, World!");   // -> "hello-world"
CkText.Slug("Café Déjà Vu");    // -> "cafe-deja-vu"
CkText.Slug("don't");           // -> "dont"

Join and Split are round-trip twins for the classic "list in one column" pattern.

Join

A list into one string.

string Join(
    string? separator,
    params IEnumerable<string?> parts
)
Parameter Type Default What it does
separator string? — Placed between kept parts; null means no separator.
parts params IEnumerable<string?> — The candidate parts; null, empty and whitespace-only ones are skipped.

Returns

When You get
At least one part has content The joined value: "1 Main St, Springfield"
No part has content, or parts is null Empty string — an answer, not a failure; it never throws

Join trims but does not otherwise clean — pass untrusted parts through String or the right validator first.

Join joins the non-empty parts with a separator, trimming each part and skipping nulls, empties and whitespace-only parts — no conditionals needed at the call site.

CkText.Join(
    separator: ", ",
    parts: ["1 Main St", "", null, " Springfield "]
);   // -> "1 Main St, Springfield"

Split

One string into a list.

string[] Split(
    string? input = null,
    string? separator = ","
)
Parameter Type Default What it does
input string? null The delimited text.
separator string? "," What separates the pieces; a comma by default.

Returns

When You get
The text splits into usable pieces The cleaned pieces, empties dropped: ["urgent", "manual-review"]
No text, no separator, or text over CkSettings.MaxTextInputLength (50,000) characters Empty list — never null, and it never throws

Exception-free, like Join — there is nothing here that can fail.

Split goes the other way: it splits on the separator (a comma by default), cleans each piece the way String cleans one value, and drops the pieces that come out empty. Split never returns null — no usable input yields an empty array.

CkText.Split("urgent, , manual-review");   // -> ["urgent", "manual-review"]
CkText.Split(
    input: "a|b||c",
    separator: "|"
);   // -> ["a", "b", "c"]

SameText

The same text, ignoring the noise.

bool SameText(
    string? a = null,
    string? b = null,
    double minSimilarity = 1.0
)
Parameter Type Default What it does
a string? null The first value.
b string? null The second value.
minSimilarity double 1.0 The Similarity score, 0 to 1, at or above which the values count as the same; 1 means an exact match after cleanup. Values above 1 or NaN act as 1; values at or below 0 match everything.

Returns

When You get
The values are the same text once cleaned — or score at least minSimilarity true
They differ, or only one of them cleans to nothing false — an answer, not a failure; it never throws

Two values that both clean to nothing — null and empty included — count as the same.

SameText tells whether two values are the same text once markup, whitespace, punctuation, casing and accents are ignored. Digits keep their separators, so "1.5" and "15" stay different. Pass minSimilarity below 1 to make the comparison fuzzy.

CkText.SameText(
    a: "John  Smith",
    b: "john smith"
);   // -> true
CkText.SameText(
    a: "Café",
    b: "cafe"
);   // -> true
CkText.SameText(
    a: "Jon Smith",
    b: "John Smith",
    minSimilarity: 0.85
);   // -> true  (the two are 0.888… similar — one edit in nine letters)

Similarity

How alike two values are.

double Similarity(
    string? a = null,
    string? b = null
)
Parameter Type Default What it does
a string? null The first value.
b string? null The second value.

Returns

When You get
Both values clean to something The score, 0 to 1: 0.75
Both clean to nothing 1.0 — nothing differs from nothing
One cleans to nothing, the other does not 0.0 — and it never throws

Values longer than CkSettings.MaxSimilarityInputLength (4,000) characters after cleanup are only tested for equality, scoring 1 or 0.

Similarity scores how alike two values are, from 0 (nothing in common) to 1 (the same text), after the same cleanup as SameText. The score comes from the edit distance — the fewest single-character changes that turn one value into the other — divided by the longer length, measured on the letters and digits alone. Two values that both clean to nothing score 1; one empty against one non-empty scores 0.

CkText.Similarity(
    a: "abcd",
    b: "abcx"
);   // -> 0.75  (one change in four letters)

Useful for "is this the same person?" checks where source systems disagree on spelling. As a rule of thumb, 0.9 tolerates one typo in a ten-character name.

CommonKit reports "nothing" as "", but a database column usually wants NULL — the next two methods are the bridge.

IsEmpty

Is this field filled in?

bool IsEmpty(
    string? input = null
)
Parameter Type Default What it does
input string? null The raw value.

Returns

When You get
Null, empty, whitespace only, markup with nothing inside, invisible characters alone — anything String would clean to nothing true
Any character survives the cleaning false

IsEmpty is the bool twin of NullIfEmpty and ToStringOrNull: the same verdict, as a yes or no. "0" and "false" are text, so they are not empty.

CkText.IsEmpty("<p></p>");   // -> true
CkText.IsEmpty("&nbsp;");    // -> true  (decodes to whitespace)
CkText.IsEmpty("0");         // -> false

HasLength

Between so many and so many characters.

bool HasLength(
    string? input = null,
    int min = 1,
    int max = CkSettings.MaxTextInputLength
)
Parameter Type Default What it does
input string? null The raw value.
min int 1 The smallest length allowed. Below 0 acts as 0.
max int CkSettings.MaxTextInputLength (50,000) The largest length allowed — the same cap String applies by default. Below 0 acts as 0.

Returns

When You get
The cleaned length is at least min and at most max, both ends included true
Shorter, longer, or min and max contradict each other false

The value is cleaned first as String would — markup stripped, invisible characters removed, whitespace tidied — so the length measured is that of the text you would store. It is counted the way CharacterCount counts: one Unicode code point per character, so an emoji counts once. With the default min of 1 an empty value is false; pass min: 0 to accept it.

CkText.HasLength(
    input: "hello",
    min: 3,
    max: 10
);                                   // -> true
CkText.HasLength(
    input: "<b>hi</b>",
    min: 2,
    max: 2
);                                   // -> true  (measured after cleanup)
CkText.HasLength("🙂🙂", 2, 2);      // -> true  (two characters, not four)

StartsWith

Does it begin with this?

bool StartsWith(
    string? input = null,
    string? value = null,
    bool ignoreCase = true
)
Parameter Type Default What it does
input string? null The raw value.
value string? null The beginning to look for.
ignoreCase bool true Match regardless of case. Pass false for an exact match.

Returns

When You get
The cleaned value begins with the cleaned text true
It does not, or either side cleans to nothing false

Both sides are cleaned as Contains cleans them, so leading whitespace or markup never hides the beginning. Accents are kept: "Café" does not start with "cafe".

CkText.StartsWith("INV-2024-001", "inv-");   // -> true
CkText.StartsWith("  <b>INV</b>-1", "inv");  // -> true
CkText.StartsWith("hello", "");              // -> false  (nothing to look for)

EndsWith

Does it end with this?

bool EndsWith(
    string? input = null,
    string? value = null,
    bool ignoreCase = true
)
Parameter Type Default What it does
input string? null The raw value.
value string? null The ending to look for.
ignoreCase bool true Match regardless of case. Pass false for an exact match.

Returns

When You get
The cleaned value ends with the cleaned text true
It does not, or either side cleans to nothing false

The mirror of StartsWith, with the same cleaning and the same rules.

CkText.EndsWith("report.PDF", ".pdf");      // -> true
CkText.EndsWith("<p>hello world</p>", "world");   // -> true
CkText.EndsWith(
    input: "Hello",
    value: "LO",
    ignoreCase: false
);                                          // -> false

Contains

Does it hold this anywhere?

bool Contains(
    string? input = null,
    string? value = null,
    bool ignoreCase = true
)
Parameter Type Default What it does
input string? null The raw value to search.
value string? null The text to look for.
ignoreCase bool true Match regardless of case. Pass false for an exact match.

Returns

When You get
The cleaned value holds the cleaned text true
It does not, or either side cleans to nothing false

Both sides are cleaned first as String would — markup stripped, invisible characters removed — and line breaks and whitespace runs are flattened to one space, so "<b>Hello</b>\nWorld" holds "hello world". Accents are kept: "Café" does not hold "cafe" — use SameText or InList for an accent-blind comparison. An empty needle finds nothing, unlike the framework's own string.Contains, which says every string contains "".

CkText.Contains("Hello, World", "world");          // -> true
CkText.Contains("<b>Hello</b>\nWorld", "hello world");   // -> true
CkText.Contains("Café", "cafe");                   // -> false  (accents are kept)
CkText.Contains("abc", "");                        // -> false

ContainsAny

Does it hold any of these?

bool ContainsAny(
    string? input = null,
    params IEnumerable<string?> values
)
Parameter Type Default What it does
input string? null The raw value to search.
values params IEnumerable<string?> — The pieces of text to look for. Null, empty and blank entries are skipped.

Returns

When You get
The cleaned value holds at least one entry true
It holds none, the list is empty, or the value cleans to nothing false

The block-list check for comments and free text: ContainsAny(comment, "buy now", "free money"). Each side is cleaned as Contains cleans it and the same rules apply. A params list must be the last parameter, so there is no ignoreCase — the method always ignores case; call Contains once per entry when case matters.

CkText.ContainsAny("Buy now!!!", "buy now", "free money");   // -> true
CkText.ContainsAny("hello", "x", "y");                       // -> false
CkText.ContainsAny("hello");                                 // -> false  (nothing to look for)

InList

Is it one of the allowed values?

bool InList(
    string? input = null,
    params IEnumerable<string?> allowed
)
Parameter Type Default What it does
input string? null The raw value.
allowed params IEnumerable<string?> — The values to accept. Null, empty and blank entries are skipped.

Returns

When You get
The value matches an entry under SameText rules true
It matches none, the list is empty, or the value cleans to nothing false

The allow-list check for status fields, codes and any column that may hold only certain words. Values are compared the way SameText compares — markup, whitespace, punctuation, casing and accents ignored — so "Active ", "ACTIVE" and "<b>active</b>" all match "active". The comparison is exact after cleanup; use SameText with minSimilarity to tolerate typos. Unlike SameText, an empty value never matches an empty entry here. Because the comparison keeps letters and digits alone, a value made of symbols only, "🙂" or "--", matches nothing, not even itself.

CkText.InList("Active ", "active", "inactive");     // -> true
CkText.InList("pending", "active", "inactive");     // -> false
CkText.InList("1.5", "15");                         // -> false  (separators between digits matter)

IsDigits

Digits only.

bool IsDigits(
    string? input = null
)
Parameter Type Default What it does
input string? null The raw value.

Returns

When You get
At least one character survives the cleaning and every one is an ASCII digit, 0 to 9 true
Anything else — a space, a sign, a decimal point, another script's digits, an empty value false

A shape check for identifiers, PINs and postal codes, not a number check: a quantity or an amount is read with CkNumber or CkMoney, and the digits of a formatted value are pulled out with Digits.

CkText.IsDigits("012345");    // -> true
CkText.IsDigits(" 42 ");      // -> true   (cleaned first)
CkText.IsDigits("12.5");      // -> false
CkText.IsDigits("-1");        // -> false

IsLetters

Letters only.

bool IsLetters(
    string? input = null,
    bool allowSpaces = false
)
Parameter Type Default What it does
input string? null The raw value.
allowSpaces bool false Let spaces sit between the letters. Line breaks and tabs count as spaces.

Returns

When You get
At least one character survives the cleaning and every one is a letter (or an allowed space) true
Anything else — a digit, a hyphen, an apostrophe, an empty value, spaces alone false

Letters of any script: "José" and "東京" pass, "R2D2" and "Anne-Marie" do not. For a person's name, which may carry hyphens and apostrophes, use Name instead.

CkText.IsLetters("José");                  // -> true
CkText.IsLetters("hello world");           // -> false
CkText.IsLetters(
    input: "hello world",
    allowSpaces: true
);                                         // -> true

IsAlphanumeric

Letters and digits only.

bool IsAlphanumeric(
    string? input = null,
    bool allowSpaces = false
)
Parameter Type Default What it does
input string? null The raw value.
allowSpaces bool false Let spaces sit between the characters. Line breaks and tabs count as spaces.

Returns

When You get
At least one character survives the cleaning and every one is a letter or a digit (or an allowed space) true
Anything else — punctuation, an underscore, an empty value, spaces alone false

For user names, codes and keys that must survive as identifiers. Letters and digits of any script count, so "Ünal2024" passes; "user_name" does not.

CkText.IsAlphanumeric("abc123");      // -> true
CkText.IsAlphanumeric("user_name");   // -> false
CkText.IsAlphanumeric("abc 123");     // -> false  (unless allowSpaces: true)

IsAscii

Will this survive a legacy system?

bool IsAscii(
    string? input = null
)
Parameter Type Default What it does
input string? null The raw value.

Returns

When You get
At least one character survives the cleaning and every one is below 128 true
An accented letter, an emoji, a curly quote — or an empty value false

The value is cleaned first as String would, so an entity such as &eacute; is decoded before it is judged, and invisible characters never tip the verdict.

CkText.IsAscii("Hello");      // -> true
CkText.IsAscii("Café");       // -> false
CkText.IsAscii("&eacute;");   // -> false  (decoded to é first)

IsUpper

All capitals?

bool IsUpper(
    string? input = null
)
Parameter Type Default What it does
input string? null The raw value.

Returns

When You get
The cleaned value holds at least one cased letter and none of them is lower case true
A lower-case letter is present, or there is no cased letter at all false

Digits, punctuation, whitespace and letters without a case — Chinese characters, say — do not count either way, so "HELLO WORLD 42" passes while "42" does not. Handy for spotting shouted input before Name or String recases it.

CkText.IsUpper("HELLO WORLD 42");   // -> true
CkText.IsUpper("Hello");            // -> false
CkText.IsUpper("42");               // -> false  (nothing has a case)

IsLower

All lower case?

bool IsLower(
    string? input = null
)
Parameter Type Default What it does
input string? null The raw value.

Returns

When You get
The cleaned value holds at least one cased letter and none of them is upper case true
An upper-case letter is present, or there is no cased letter at all false

The mirror of IsUpper, with the same rules.

CkText.IsLower("hello world 42");   // -> true
CkText.IsLower("Hello");            // -> false

Matches

Does it fit this pattern?

bool Matches(
    string? input = null,
    string? pattern = null,
    bool ignoreCase = false,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw value.
pattern string? null The .NET regular expression to test against.
ignoreCase bool false Let letters match regardless of case.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkTextException that says why, instead of answering false, when the pattern is missing, invalid, or runs out of time.

Returns

When Default With throwIfInvalid: true
The cleaned value matches the pattern true true
It does not match, or cleans to nothing false false — no match is an answer, not an error
Rejected: no pattern, not a valid pattern, or the pattern ran out of time false Throws CkTextException — carries Input, Expected and Reason

The escape hatch for a shape none of the other checks describes — a product code, a licence plate, a house-style reference number. The value is cleaned first as String would, so the pattern meets the text you would store rather than the raw markup around it. The pattern runs culture-invariant under the library's regular expression timeout (CkSettings.RegexTimeoutMs). Anchor it with ^ and $ when the whole value must match; without them a match anywhere counts. The flag concerns the pattern, never the value: a value that does not match answers false either way, which is what makes the method safe to call per row.

Regular expression dialects differ at the edges. The common subset — character classes, quantifiers, anchors, groups and alternation — reads the same in .NET, PCRE and JavaScript; named groups, Unicode categories and look-behind do not always travel. Keep a pattern that must be shared between systems inside that subset.

CkText.Matches("AB-1234", @"^[A-Z]{2}-[0-9]{4}$");         // -> true
CkText.Matches("<b>AB-1234</b>", @"^[A-Z]{2}-[0-9]{4}$");  // -> true  (cleaned first)
CkText.Matches(
    input: "ab-1234",
    pattern: @"^[A-Z]{2}-[0-9]{4}$",
    ignoreCase: true
);                                                         // -> true
CkText.Matches("abc", "[bad");                             // -> false (a bad pattern is not a match)

WordCount

How many words?

int WordCount(
    string? input = null,
    bool stripHtml = true
)
Parameter Type Default What it does
input string? null The raw value.
stripHtml bool true Count the visible text once markup is gone. Pass false to count the value untouched, tags and all.

Returns

When You get
The value holds words The count: 4
Null, empty, or nothing with a letter or digit in it 0 — an answer, not a failure; it never throws

A word is a run of non-whitespace characters holding at least one letter or digit, so "well-known" is one word, "a@b.com" is one, and a lone dash or "..." is none. With markup stripped, the default, block tags become the boundary: "<p>Hello</p><p>World</p>" is 2. With stripHtml: false the same input is 1, because nothing separates it. Words are split on whitespace alone, which suits languages that space their words; a run of Chinese or Japanese text without spaces counts as one word.

CkText.WordCount("The quick brown fox");           // -> 4
CkText.WordCount("<p>Hello</p><p>World</p>");      // -> 2
CkText.WordCount(
    input: "<p>Hello</p><p>World</p>",
    stripHtml: false
);                                                 // -> 1

CharacterCount

How many characters?

int CharacterCount(
    string? input = null,
    bool stripHtml = true
)
Parameter Type Default What it does
input string? null The raw value.
stripHtml bool true Count the visible text once markup is gone and the value is tidied. Pass false to count the value untouched, spaces and tags included.

Returns

When You get
The value holds characters The count: 13
Null, empty, or — with markup stripped — a value that cleans to nothing 0 — an answer, not a failure; it never throws

By default the value is cleaned as String would — entities decoded, control and invisible characters removed, whitespace collapsed — so the count is that of the text a reader sees and you would store. Either way one character is one Unicode code point, so an emoji or an accented letter counts once. That is what most other languages and databases call a character count; the maxLength limits elsewhere in this class count UTF-16 units, where an emoji counts twice. HasLength counts the same way as this method.

CkText.CharacterCount("Hello, world!");     // -> 13
CkText.CharacterCount("<p>Hello</p>");      // -> 5
CkText.CharacterCount(
    input: "<p>Hello</p>",
    stripHtml: false
);                                          // -> 12
CkText.CharacterCount("Café 🙂");           // -> 6

NullIfEmpty

Turn empty into null.

string? NullIfEmpty(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The value, typically the result of another CommonKit call.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkTextException instead of answering null when the value is missing, empty or whitespace-only.

Returns

When Default With throwIfInvalid: true
The value has content The value unchanged: "hello" The value unchanged: "hello"
Missing, empty or whitespace-only null Throws CkTextException — carries Input, Expected and Reason

No trimming or cleaning is applied — the value comes back exactly as it went in.

NullIfEmpty is the bridge: null for a missing, empty or whitespace-only value, the value untouched otherwise. Wrap any CommonKit string call in it on the way to storage — CkText.NullIfEmpty(CkEmail.Address(raw)) stores a rejected email as NULL, not as "". That is the pattern for every string class; there are deliberately no EmailOrNull-style methods.

CkText.NullIfEmpty("");        // -> null
CkText.NullIfEmpty("hello");   // -> "hello"

ToStringOrNull

Clean, then null when nothing is left.

string? ToStringOrNull(
    string? input = null,
    TextCase textCase = TextCase.None,
    int maxLength = CkSettings.MaxTextInputLength,
    bool clearEscape = false,
    bool normalize = false,
    bool decodeEntities = true,
    bool truncate = true,
    string? fallback = null
)
Parameter Type Default What it does
input string? null The raw value to clean.
textCase TextCase TextCase.None The casing to apply.
maxLength int CkSettings.MaxTextInputLength (50,000) Maximum length of the cleaned value; see String.
clearEscape bool false Flatten line breaks and tabs to spaces.
normalize bool false Replace every character that is not an ASCII letter, digit or whitespace with _; see String.
decodeEntities bool true Decode HTML entities as part of markup removal; see String.
truncate bool true Cut an over-long value to fit (the default) instead of rejecting it.
fallback string? null Backup value used when the result would be null. It goes through the same cleaning and checks as input; a fallback that is itself rejected answers null.

Returns

When You get
The value (or the fallback) cleans to usable text The cleaned text: "hi"
Missing, nothing left once cleaned, or over the limit with truncate off null — this method never throws

Exactly String's cleaning — only the "nothing" answer changes, from "" to null.

ToStringOrNull combines String and NullIfEmpty in one call: it cleans exactly as String does (same parameters), then answers null instead of "" when nothing remains — for a value headed to a nullable column or a JSON property.

CkText.ToStringOrNull("  <b>hi</b>  ");   // -> "hi"
CkText.ToStringOrNull("<p></p>");         // -> null

When data leaves your process, write it with the next two methods instead of raw ToString(). They use the invariant culture — a fixed, machine-neutral format — so a server set to a European locale never writes "1234,5" with a decimal comma.

NumberToString

A number written out safely.

string NumberToString(
    object? input = null,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The value to convert, typically boxed by a data reader or deserializer.
fallback string? null Backup value used when the result would be empty. Being text, it must parse as a plain number in the invariant culture and is then formatted like input, never echoed as typed: "007" answers "7", and "N/A" answers "".
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkTextException that says why, instead of answering "", when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
The value (or the fallback) is a finite number The plain invariant form: "1234.5" — never "1234,5" The plain invariant form: "1234.5" — never "1234,5"
Unreadable: null, not a numeric type (bools and chars included), or not finite Empty string Throws CkTextException — carries Input, Expected and Reason

Booleans and chars are rejected even though they convert numerically — treating them as numbers is almost always a bug.

NumberToString converts a boxed numeric value to its plain string form: no padding, no grouping, no currency symbol. Floating-point values round-trip cleanly. Native-sized nint/nuint are written as the value they hold, whatever the process bitness. Booleans and chars are rejected — treating them as numbers is almost always a bug. To read a value as a number, that is CkNumber's job.

CkText.NumberToString(1234.5);   // -> "1234.5"  (never "1234,5")
CkText.NumberToString(0.1f);     // -> "0.1"     (not "0.100000001490116")
CkText.NumberToString(19.99m);   // -> "19.99"

BoolToString

A bool written out safely.

string BoolToString(
    object? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The value to write: a real bool, a recognised word like "yes" or "off", or a number that is exactly 1 or 0.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkTextException that says why, instead of answering "False", when the value is missing or cannot be read as a boolean.

Returns

When Default With throwIfInvalid: true
The value reads as a boolean "True" or "False" — "yes" answers "True" "True" or "False" — "yes" answers "True"
Unreadable: missing, an unrecognised word, a number other than 1 or 0, or the wrong type "False" — never empty Throws CkTextException — carries Input, Expected and Reason

Unreadable reads as "False", so a caller cannot tell "absent" from "explicitly no" without the exception.

BoolToString writes a value as "True" or "False" — with capital first letters, the framework's own spelling. The value is read by CkBool.FromValue, so the library's one boolean rule applies: a real bool, the words "true", "t", "yes", "y", "on" and their negatives, or a number that is exactly 1 or 0. It never returns empty.

CkText.BoolToString(true);       // -> "True"
CkText.BoolToString("yes");      // -> "True"
CkText.BoolToString(1);          // -> "True"
CkText.BoolToString(2);          // -> "False"  (unreadable — only exactly 1 is true)
CkText.BoolToString("banana");   // -> "False"  (unreadable reads as "False")

Writing a boolean for JSON, XML, a URL or a database flag? That lives in CkBool

CkBool.Format writes lowercase "true"/"false" by default — "True" is not valid JSON, and an XML boolean rejects it — or "1"/"0" and "Y"/"N" on request, and it answers "" for a value it cannot read instead of a "False" nobody said.

ToAscii

Plain ASCII, case and spacing kept.

string ToAscii(
    string? input = null,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw value.
fallback string? null A spare value, processed the same way.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkTextException that says why, instead of answering "", when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
Every character has an ASCII form The folded text: "Straße" is "Strasse", "Café" is "Cafe", ligatures and full-width letters their plain letters, "™" is "TM", curly quotes straight, dashes hyphens The text
A character has none — a Chinese character, an emoji, a currency sign — or nothing remains "" Throws CkTextException — carries Input, Expected and Reason — the reason names the character

For legacy systems, file names and feeds that cannot carry Unicode. A character with no ASCII form makes the whole value unusable rather than silently dropped, so a name never loses its letters; IsAscii is the check, this the conversion. Markup is stripped and whitespace tidied first, as String does.

CkText.ToAscii("Zoë O'Brien");   // -> "Zoe O'Brien"
CkText.ToAscii("東京");          // -> ""  (no ASCII form — rejected, not mangled)

Initials

The letters an avatar shows.

string Initials(
    string? input = null,
    int max = 2,
    string? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw name.
max int 2 The most initials to keep; below 1 acts as 1.
fallback string? null A spare value, processed the same way.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkTextException that says why, instead of answering "", when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
Some word holds a letter The initials, upper-cased: "John Smith" is "JS", "3M Company" is "MC" The initials
No word does, or nothing remains "" Throws CkTextException — carries Input, Expected and Reason

The first letter of each word, at most max of them. A name with more words keeps the first and the last, so "John Ronald Reuel Tolkien" is "JT" — the shape monograms use.

CkText.Initials("mary-jane o'brien");                   // -> "MO"
CkText.Initials("John Ronald Reuel Tolkien", max: 3);   // -> "JRT"

Between

The text between two markers.

string Between(
    string? input = null,
    string? start = null,
    string? end = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw text, taken as written.
start string? null The marker the slice begins after; empty for the beginning.
end string? null The marker the slice ends before, searched after start; empty for the end.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkTextException that says why, instead of answering "", when a marker is not found or nothing lies between them.

Returns

When Default With throwIfInvalid: true
Both markers are found with something between them The slice, trimmed The slice
A marker is missing, or nothing lies between them "" Throws CkTextException — carries Input, Expected and Reason — the reason says which

Markers match exactly, case included, on the raw text — so markers made of markup work, Between(html, "<title>", "</title>"). The slice is otherwise untouched; clean it with String when it goes further than a comparison.

CkText.Between("Order #1234 - paid", "Order #", " -");   // -> "1234"
CkText.Between("prefix: the rest", "prefix:", "");        // -> "the rest"

Plural

A count with its noun.

string Plural(
    object? count = null,
    string? singular = null,
    string? plural = null,
    bool includeCount = true,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
count object? null The count: a number of any type, or text.
singular string? null The noun for exactly one.
plural string? null The noun for any other count; null to form it by the usual English rules.
includeCount bool true Write the count in front of the noun.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkTextException that says why, instead of answering "", when the count is not a number or the noun is missing.

Returns

When Default With throwIfInvalid: true
The count reads and the noun is given "1 item", "2 items", "0 items" The phrase
Rejected: not a number, or no noun "" Throws CkTextException — carries Input, Expected and Reason

English. The usual rules — "box" to "boxes", "city" to "cities", "day" to "days" — cover most nouns; pass the plural for the ones that break them, "person" and "people".

CkText.Plural(3, "box");                       // -> "3 boxes"
CkText.Plural(3, "person", "people");          // -> "3 people"
CkText.Plural(1, "item", includeCount: false); // -> "item"

Ordinal

1st, 2nd, 3rd.

string Ordinal(
    object? number = null,
    string fallback = "",
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
number object? null The number: any numeric type, or text.
fallback string "" What comes back when the value cannot be used.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkTextException that says why, instead of answering the fallback, when the value is not a whole, non-negative number.

Returns

When Default With throwIfInvalid: true
A whole, non-negative number "1st", "22nd", "111th" The ordinal
Rejected: a fraction, a negative number, junk The fallback Throws CkTextException — carries Input, Expected and Reason

English, with the teens right: 11th, 12th, 13th.

CkText.Ordinal(22);      // -> "22nd"
CkText.Ordinal("3.5");   // -> ""

Explain

Why not — in words, without an exception.

string Explain(
    string? input = null
)
Parameter Type Default What it does
input string? null The raw value.

Returns

When You get
The value passes the reader ""
It does not The reason, the same words the exception would carry — an answer, not a failure; it never throws

The form-friendly twin of throwIfInvalid: the message to show beside the field, without a try and catch. Every class with a reader has one, on the reader's own rules, so the reason is exactly what the reader would say.

CkText.Explain("hello");     // -> ""
CkText.Explain("<p></p>");   // -> "nothing left once cleaned"

Good to know

  • Accents are composed. A base letter plus a combining mark, the way macOS writes "é", becomes the one composed letter before anything else happens, in every reader of every class: it counts as one character, matches the composed spelling, and is stored that way.
  • What leaves is valid text. A .NET string can hold half of a surrogate pair — a value no encoder, filesystem or normalizer takes. Every method here, Truncate, Between and NullIfEmpty included, replaces such a half with U+FFFD, the replacement character a browser would show, so nothing malformed ever comes back.
  • Markup is stripped the way a browser reads it. A page may write &lt and &#60; without the semicolon, and a browser decodes them; so does the strip, before it looks for tags, which is why "&ltscript&gt…" comes out empty and not as text that turns into a tag on the page it lands on. "AT&T" and "fish &chips" begin no such name and stay as they are. A tag holding one lone quote in an attribute (<img title=don't …>) is stripped too.
  • A mask shows only its shape. Mask keeps spaces, punctuation and symbols visible so ***-**-6789 still reads as a number; a control character, a line break, a direction override or an invisible joiner is masked like a letter, so the mask is always one plain line in a log. ToAscii cleans its folded text once more, since folding a fullwidth < into < can make markup where there was none.
  • Html is text extraction, not a security tool. It pulls readable text out of markup; it is not a defence against XSS (cross-site scripting — injected script in a page). Markup is matched with patterns, not truly parsed, so odd input like "a<b and c>d" loses the middle. Encode at render time, and use a real parser for untrusted markup.
  • Mask and Truncate never cut an emoji in half. Some characters, emoji included, are stored as two chars (a "surrogate pair"); both methods keep the pair together.
  • Join, Split, Mask, Truncate, SameText, Similarity, the validators and the counters take no throwIfInvalid at all. They transform, compare, judge and count, so an empty, false or zero result is an answer, not a failure. Matches is the one validator with the flag, and it concerns the pattern — a value that does not match answers false either way.
  • The validators judge the cleaned value. Contains, StartsWith, IsDigits, HasLength and the rest look at the value as String would clean it — markup gone, invisible characters removed, whitespace tidied — so their verdict is about what you would store. Only WordCount and CharacterCount can look at the raw value, with stripHtml: false.
  • Empty is never found. Contains("abc", "") is false here, unlike the framework's string.Contains; an entry that cleans to nothing is skipped by ContainsAny and InList; and an empty value fails every validator except IsEmpty and HasLength with min: 0.
  • Lengths count code points. HasLength and CharacterCount count one per character, so an emoji counts once; maxLength elsewhere in the class counts UTF-16 units, where an emoji counts twice.
  • ToStringOrNull never throws. For an OrNull method, null is the desired answer for anything unusable — there is no throwIfInvalid. Want errors? Clean with String and throwIfInvalid: true, then bridge with NullIfEmpty.
  • Digits drops meaning along with the separators. "1.5" becomes "15" and "$1,234.56" becomes "123456" — parse real numbers and amounts with CkNumber and CkMoney instead.
  • BoolToString cannot tell "absent" from "no". Null, an unrecognised word and a non-boolean type all read as "False". Use a nullable bool at the call site if the difference matters — or throwIfInvalid: true, which lets only real booleans, numbers and recognised words through.
  • The fallback is cleaned too. A fallback goes through the same cleaning and validation as the input and is never returned as typed; a fallback that fails answers "" as well.
  • Non-empty in, empty out means rejected. That is the standard CommonKit signal — check for "" where it matters.

If you want errors instead

Pass throwIfInvalid: true and a value that cannot be used raises a CkTextException (from SafeCoder.CommonKit.Exceptions) instead of returning empty. The exception carries the Input, what was Expected and a short Reason, flattened into a one-line message. Join, Split, Mask, Truncate, SameText, Similarity, the validators and the counters never take the parameter — for them, empty (or false, or 0) is the answer; Matches takes it for its pattern alone. Neither does ToStringOrNull — it never throws, because null is its answer for anything unusable; want errors, clean with String and throwIfInvalid: true first.

CkText.String(
    input: "<p></p>",
    throwIfInvalid: true
);   // throws CkTextException — the message says why