strutil

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 10, 2026 License: MIT Imports: 6 Imported by: 0

README

go-strutil

A small, focused collection of string manipulation helpers for Go, modeled loosely on Laravel's Illuminate\Support\Str facade but translated to idiomatic, rune-safe Go. All functions are top-level, pure, and handle empty input gracefully. The only non-stdlib dependency is golang.org/x/text for Unicode normalization inside Slugify.

Status: pre-1.0 (v0.1.x). API surface is stable enough for production use but may evolve in minor versions. Breaking changes will be called out loudly in CHANGELOG.md.

API reference: https://pkg.go.dev/github.com/hollis-labs/go-strutil

Installation

go get github.com/hollis-labs/go-strutil
import "github.com/hollis-labs/go-strutil"

slug := strutil.Slugify("Café du Monde") // "cafe-du-monde"

See examples/ for runnable demonstrations of each function group.

Function reference

Slug and normalization
Function Signature Description Example
Slugify Slugify(s string) string URL-safe kebab-case slug; transliterates accents via NFKD. Slugify("Café du Monde")"cafe-du-monde"
SlugifyN SlugifyN(s string, maxRunes int) string Slugify capped at maxRunes runes; trims trailing hyphens left by the cut. SlugifyN("hello world foo bar", 12)"hello-world"
DefaultMaxSlugLength const DefaultMaxSlugLength = 80 Sensible default cap: fits VARCHAR(100), keeps URLs readable. SlugifyN(title, strutil.DefaultMaxSlugLength)
Case conversion
Function Signature Description Example
SnakeCase SnakeCase(s string) string Any case style → snake_case. SnakeCase("helloWorld")"hello_world"
KebabCase KebabCase(s string) string Any case style → kebab-case (preserves multi-byte characters). KebabCase("HelloWorld")"hello-world"
CamelCase CamelCase(s string) string Any case style → camelCase. CamelCase("hello_world")"helloWorld"
StudlyCase StudlyCase(s string) string Any case style → StudlyCase (PascalCase). StudlyCase("hello-world")"HelloWorld"
Title Title(s string) string Title Case — capitalizes each whitespace-separated word; underscores are not separators. Title("hello world")"Hello World"
UcFirst UcFirst(s string) string Uppercases the first rune only. UcFirst("hello")"Hello"
LcFirst LcFirst(s string) string Lowercases the first rune only. LcFirst("Hello")"hello"
Truncation
Function Signature Description Example
Truncate Truncate(s string, length int, suffix string) string Cap to length runes, append suffix if cut. Truncate("hello world", 5, "...")"hello..."
Words Words(s string, count int, suffix string) string Cap to count whitespace-separated words. Words("the quick brown fox", 2, "...")"the quick..."
Limit Limit(s string, length int) string Hard rune cap with no suffix. Limit("hello world", 5)"hello"
Manipulation
Function Signature Description Example
Squish Squish(s string) string Collapse whitespace runs to single spaces and trim. Squish(" a b ")"a b"
Finish Finish(s, suffix string) string Ensure s ends with suffix. Finish("dir", "/")"dir/"
Start Start(s, prefix string) string Ensure s starts with prefix. Start("path", "/")"/path"
After After(s, search string) string Substring after first occurrence of search. After("one.two.three", ".")"two.three"
Before Before(s, search string) string Substring before first occurrence of search. Before("one.two.three", ".")"one"
Between Between(s, start, end string) string Substring between first start and first subsequent end. Between("[hello]", "[", "]")"hello"
Inspection
Function Signature Description Example
ContainsAll ContainsAll(s string, subs []string) bool True iff every substring is present. ContainsAll("hello world", []string{"hello","world"})true
ContainsAny ContainsAny(s string, subs []string) bool True iff at least one substring is present. ContainsAny("hello world", []string{"mars","world"})true
Random
Function Signature Description Example
Random Random(length int) string Cryptographically secure random alphanumeric string using crypto/rand. Random(8)"xK2mP9qR"

Design notes

  • Rune-safe. Truncate, Limit, and case conversions iterate runes, not bytes, so multi-byte UTF-8 strings are handled correctly.
  • Acronym splitting. splitWords (internal) breaks XMLParser into ["XML", "Parser"] — a run of uppercase letters followed by a lowercase letter splits before the last upper. Trailing acronyms stay intact: parseXML["parse", "XML"].
  • No package state. Every function is pure; there are no init-time side effects and no global configuration.
  • No panics on bad input (except Random if the OS CSPRNG is broken, which is already unrecoverable). Empty inputs yield empty outputs.
  • Slugify vs KebabCase. Slugify transliterates accents and strips non-ASCII; KebabCase preserves multi-byte characters and only changes casing/separators.

Development

go test ./... -v                                       # run tests
go test ./... -cover                                   # coverage (currently >96%)
go test ./... -run '^$' -fuzz='^FuzzSlugify$' -fuzztime=10s   # fuzz Slugify invariants
go test ./... -run '^$' -fuzz='^FuzzSlugifyN$' -fuzztime=10s  # fuzz SlugifyN invariants
go vet ./...                                           # static checks
gofmt -l .                                             # formatting check (should print nothing)
Fuzz invariants

FuzzSlugify and FuzzSlugifyN verify the structural contract every slug must meet for any input: ASCII-only [a-z0-9-], no leading/trailing hyphen, no consecutive hyphens, Slugify is idempotent, and SlugifyN always respects its rune cap.

Out of scope (for now)

The following Laravel Str helpers were intentionally deferred and may land in later releases or separate modules:

  • Plural / Singular — needs an inflection dictionary.
  • Markdown / Html stripping — large scope.
  • Mask / Censor / Redact.
  • Ulid / Uuid — belong in their own module.
  • Is (glob-style match).
  • Fluent Stringable wrapper — not idiomatic Go.

License

MIT — see LICENSE.

Documentation

Overview

Package strutil provides a small, focused collection of string manipulation helpers for Go, modeled loosely on Laravel's Illuminate\Support\Str facade but translated to idiomatic, rune-safe Go.

All functions are top-level, pure, and handle empty inputs gracefully — there is no package-level state, no init-time side effects, and no global configuration. The only non-stdlib dependency is golang.org/x/text, used solely for Unicode normalization inside Slugify.

Function groups

  • Slug and normalization: Slugify, SlugifyN, DefaultMaxSlugLength.
  • Case conversion: SnakeCase, KebabCase, CamelCase, StudlyCase, Title, UcFirst, LcFirst.
  • Truncation: Truncate, Words, Limit.
  • Manipulation: Squish, Finish, Start, After, Before, Between.
  • Inspection: ContainsAll, ContainsAny.
  • Random: Random (crypto/rand-backed alphanumeric).

Rune safety

Truncation and case conversion iterate runes rather than bytes, so multi-byte UTF-8 strings are handled correctly. Slugify performs NFKD decomposition and strips combining marks, so accented characters transliterate to their ASCII bases (for example, "Café" → "cafe").

Slugify vs KebabCase

Slugify is destructive: it lowercases, transliterates accents, and strips any non-ASCII alphanumeric characters. KebabCase is non-destructive: it preserves multi-byte characters and only changes case and separators.

See the examples directory for runnable demonstrations of each group.

Index

Constants

View Source
const DefaultMaxSlugLength = 80

DefaultMaxSlugLength is a sensible default cap for slug lengths. 80 runes fits comfortably inside VARCHAR(100) columns, keeps URLs readable, and stays well under practical URL-component length limits (~255 bytes on most systems, 2048 for whole URLs). Callers free to choose their own limit via SlugifyN — this constant is a starting point, not a ceiling.

Variables

This section is empty.

Functions

func After

func After(s, search string) string

After returns the substring of s after the first occurrence of search. If search is empty or not found, returns s unchanged.

After("hello world", " ")   → "world"
After("one.two.three", ".") → "two.three"
After("hello", "z")         → "hello"

func Before

func Before(s, search string) string

Before returns the substring of s before the first occurrence of search. If search is empty or not found, returns s unchanged.

Before("hello world", " ")   → "hello"
Before("one.two.three", ".") → "one"

func Between

func Between(s, start, end string) string

Between returns the substring of s between start and end markers. It uses the first occurrence of start and the first occurrence of end *after* start. If either marker is missing, returns "".

Between("[hello]", "[", "]") → "hello"
Between("a-b-c", "-", "-")   → "b"
Between("no markers", "[", "]") → ""

func CamelCase

func CamelCase(s string) string

CamelCase converts any case style to camelCase.

CamelCase("hello_world") → "helloWorld"
CamelCase("hello-world") → "helloWorld"
CamelCase("Hello World") → "helloWorld"

func ContainsAll

func ContainsAll(s string, subs []string) bool

ContainsAll reports whether every substring in subs is present in s. An empty subs slice returns true.

ContainsAll("hello world", []string{"hello", "world"}) → true
ContainsAll("hello world", []string{"hello", "mars"})  → false

func ContainsAny

func ContainsAny(s string, subs []string) bool

ContainsAny reports whether at least one substring in subs is present in s. An empty subs slice returns false.

ContainsAny("hello world", []string{"mars", "world"}) → true
ContainsAny("hello world", []string{"mars", "venus"}) → false

func Finish

func Finish(s, suffix string) string

Finish ensures s ends with suffix, appending it if missing. If s already ends with suffix, returns s unchanged. An empty suffix is a no-op.

Finish("path/to/dir", "/")  → "path/to/dir/"
Finish("path/to/dir/", "/") → "path/to/dir/"

func KebabCase

func KebabCase(s string) string

KebabCase converts any case style to kebab-case. Unlike Slugify it does NOT perform Unicode transliteration — multi-byte characters are preserved, only case and separators change.

KebabCase("helloWorld")  → "hello-world"
KebabCase("HelloWorld")  → "hello-world"
KebabCase("hello_world") → "hello-world"

func LcFirst

func LcFirst(s string) string

LcFirst lowercases the first rune of the string and leaves the rest untouched. Rune-safe for multi-byte input.

LcFirst("Hello") → "hello"
LcFirst("")      → ""

func Limit

func Limit(s string, length int) string

Limit is Truncate without a suffix — a hard rune cap. Returns s unchanged if it fits within length.

Limit("hello world", 5) → "hello"

func Random

func Random(length int) string

Random returns a cryptographically secure random alphanumeric string of the given length. It uses crypto/rand as the source of entropy and draws from the charset [A-Za-z0-9]. A length of 0 or less returns "". Panics only if the system's crypto/rand is unavailable — a condition that already makes the process unable to function securely.

Random(8) → e.g. "xK2mP9qR"
Random(0) → ""

func Slugify

func Slugify(s string) string

Slugify converts any string to a URL-safe kebab-case slug. It lowercases, NFKD-decomposes to strip accents ("café" → "cafe"), replaces runs of non-alphanumeric characters with single hyphens, and trims leading/trailing hyphens. Returns "" for inputs that normalize to nothing (e.g. "!!!", " ", "").

Examples:

Slugify("Frontend Bug")  → "frontend-bug"
Slugify("Café du Monde") → "cafe-du-monde"
Slugify("HELLO_world")   → "hello-world"
Slugify("  !!  ")        → ""

func SlugifyN

func SlugifyN(s string, maxRunes int) string

SlugifyN behaves like Slugify but caps the result at maxRunes runes. If truncation would leave trailing hyphens (because the cut landed in the middle of a word boundary), those hyphens are stripped so the slug stays well-formed. maxRunes <= 0 returns "".

Examples:

SlugifyN("hello world foo bar", 12) → "hello-world"
SlugifyN("Café du Monde", 10)        → "cafe-du-mo"
SlugifyN("abc", 100)                 → "abc"
SlugifyN("hello", 0)                 → ""

Use DefaultMaxSlugLength if you don't have a specific column or URL constraint in mind.

func SnakeCase

func SnakeCase(s string) string

SnakeCase converts any case style to snake_case.

SnakeCase("helloWorld")  → "hello_world"
SnakeCase("HelloWorld")  → "hello_world"
SnakeCase("hello-world") → "hello_world"
SnakeCase("hello world") → "hello_world"

func Squish

func Squish(s string) string

Squish collapses runs of whitespace (including tabs and newlines) into single spaces and trims leading/trailing whitespace.

Squish("  hello   world  ") → "hello world"
Squish("line1\n\tline2")    → "line1 line2"

func Start

func Start(s, prefix string) string

Start ensures s starts with prefix, prepending it if missing. An empty prefix is a no-op.

Start("path/to/file", "/")  → "/path/to/file"
Start("/path/to/file", "/") → "/path/to/file"

func StudlyCase

func StudlyCase(s string) string

StudlyCase (aka PascalCase) converts any case style to StudlyCase.

StudlyCase("hello_world") → "HelloWorld"
StudlyCase("hello-world") → "HelloWorld"
StudlyCase("hello world") → "HelloWorld"

func Title

func Title(s string) string

Title converts a string to Title Case (each space-separated word capitalized). Does NOT change separators — only capitalization. Underscores and hyphens are not treated as word boundaries.

Title("hello world") → "Hello World"
Title("HELLO WORLD") → "Hello World"
Title("hello_world") → "Hello_world"

func Truncate

func Truncate(s string, length int, suffix string) string

Truncate returns s truncated to at most length runes (not bytes), appending suffix if truncation actually happened. If s fits within length, it is returned unchanged. length is measured in runes so multi-byte UTF-8 is safe.

Truncate("hello world", 5, "...") → "hello..."
Truncate("hello", 10, "...")      → "hello"
Truncate("café", 3, "…")          → "caf…"

func UcFirst

func UcFirst(s string) string

UcFirst capitalizes the first rune of the string and leaves the rest untouched. Rune-safe for multi-byte input.

UcFirst("hello") → "Hello"
UcFirst("")      → ""

func Words

func Words(s string, count int, suffix string) string

Words returns s truncated to at most count words, appending suffix if truncation happened. Words are split on any whitespace; leading, trailing, and interior runs of whitespace collapse during splitting.

Words("the quick brown fox", 2, "...") → "the quick..."
Words("hello", 5, "...")                → "hello"

Types

This section is empty.

Directories

Path Synopsis
examples
case command
Package main demonstrates the case-conversion helpers: SnakeCase, KebabCase, CamelCase, StudlyCase, Title, UcFirst, LcFirst.
Package main demonstrates the case-conversion helpers: SnakeCase, KebabCase, CamelCase, StudlyCase, Title, UcFirst, LcFirst.
inspect command
Package main demonstrates the inspection helpers: ContainsAll, ContainsAny.
Package main demonstrates the inspection helpers: ContainsAll, ContainsAny.
manipulate command
Package main demonstrates the manipulation helpers: Squish, Finish, Start, After, Before, Between.
Package main demonstrates the manipulation helpers: Squish, Finish, Start, After, Before, Between.
random command
Package main demonstrates Random: a cryptographically secure alphanumeric string generator backed by crypto/rand.
Package main demonstrates Random: a cryptographically secure alphanumeric string generator backed by crypto/rand.
slugify command
Package main demonstrates Slugify and SlugifyN: NFKD-based URL slug generation with an optional rune cap.
Package main demonstrates Slugify and SlugifyN: NFKD-based URL slug generation with an optional rune cap.
truncate command
Package main demonstrates the truncation helpers: Truncate, Words, Limit.
Package main demonstrates the truncation helpers: Truncate, Words, Limit.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL