/**
* std/slug — memorable, non-secret names for runs, agents, incidents, and fixtures.
*
* Import with: import "std/slug"
*
* @effects: ["random"]
* @errors: []
*/
const __SLUG_ADJECTIVES = [
"able",
"bright",
"calm",
"careful",
"clever",
"crisp",
"curious",
"dapper",
"direct",
"eager",
"fair",
"fresh",
"gentle",
"honest",
"kind",
"lively",
"lucid",
"merry",
"nimble",
"patient",
"plain",
"precise",
"quiet",
"ready",
"robust",
"steady",
"tidy",
"useful",
"vivid",
"warm",
"witty",
"zesty",
]
const __SLUG_NOUNS = [
"anchor",
"apricot",
"atlas",
"battery",
"beacon",
"bridge",
"button",
"canvas",
"celery",
"circuit",
"compass",
"corpus",
"delta",
"ember",
"fiber",
"garden",
"harbor",
"index",
"kernel",
"lantern",
"ledger",
"matrix",
"meadow",
"notebook",
"orbit",
"parcel",
"pixel",
"prism",
"quartz",
"ribbon",
"socket",
"soup",
"staple",
"summit",
"thread",
"vector",
]
const __SLUG_VERBS = [
"align",
"build",
"chart",
"check",
"craft",
"debug",
"draft",
"gather",
"index",
"inspect",
"merge",
"patch",
"polish",
"reason",
"render",
"route",
"shape",
"sketch",
"sort",
"tune",
]
/**
* Return the default adjective dictionary used by `random_slug` and `slug_from`.
*
* @effects: []
* @errors: []
*/
pub fn slug_adjectives() {
return __SLUG_ADJECTIVES
}
/**
* Return the default noun dictionary used by `random_slug` and `slug_from`.
*
* @effects: []
* @errors: []
*/
pub fn slug_nouns() {
return __SLUG_NOUNS
}
/**
* Return the default verb dictionary used by explicit slug patterns.
*
* @effects: []
* @errors: []
*/
pub fn slug_verbs() {
return __SLUG_VERBS
}
/**
* Return the default `{adjectives, nouns, verbs}` slug dictionary.
*
* @effects: []
* @errors: []
*/
pub fn slug_dictionary() {
return {adjectives: __SLUG_ADJECTIVES, nouns: __SLUG_NOUNS, verbs: __SLUG_VERBS}
}
/**
* Generate a non-secret human-readable slug from the default or supplied dictionary.
*
* Options: `segments`, `separator`, `pattern`, `prefix`, `suffix`, `rng`, and
* `dictionary`.
*
* @effects: ["random"]
* @errors: []
*/
pub fn random_slug(options = nil) {
let opts = __slug_options(options)
var words = []
var index = 0
while index < opts.segments {
let kind = __slug_segment_kind(opts.pattern, index, opts.segments)
let bucket = __slug_bucket(kind, opts.dictionary)
words = words + [__slug_random_word(bucket, opts.rng)]
index = index + 1
}
return __slug_join_with_affixes(words, opts)
}
/**
* Generate a deterministic human-readable slug for `value`.
*
* Options match `random_slug`; `salt` can partition deterministic namespaces.
*
* @effects: []
* @errors: []
*/
pub fn slug_from(value, options = nil) {
let opts = __slug_options(options)
let seed = to_string(opts.salt ?? "") + ":" + to_string(value)
var words = []
var index = 0
while index < opts.segments {
let kind = __slug_segment_kind(opts.pattern, index, opts.segments)
let bucket = __slug_bucket(kind, opts.dictionary)
words = words + [bucket[__slug_seed_index(seed, index, len(bucket))]]
index = index + 1
}
return __slug_join_with_affixes(words, opts)
}
/**
* Alias for `slug_from`.
*
* @effects: []
* @errors: []
*/
pub fn deterministic_slug(value, options = nil) {
return slug_from(value, options)
}
/**
* Generate a random slug when `value` is nil, otherwise a deterministic slug.
*
* @effects: ["random"]
* @errors: []
*/
pub fn slug(value = nil, options = nil) {
if value == nil {
return random_slug(options)
}
return slug_from(value, options)
}
/**
* Convert arbitrary text into a lowercase separator-delimited slug.
*
* @effects: []
* @errors: []
*/
pub fn slugify(value, options = nil) {
let separator = __slug_separator(options?.separator ?? "-")
return __slugify_text(value, separator)
}
fn __slug_options(options) {
let raw = options ?? {}
var segments = to_int(raw?.segments ?? 2)
if segments < 1 {
segments = 1
}
if segments > 8 {
segments = 8
}
let separator = __slug_separator(raw?.separator ?? "-")
let custom_dictionary = raw?.dictionary ?? {}
let prefix = __slug_affix(raw?.prefix, separator)
let suffix = __slug_affix(raw?.suffix, separator)
var pattern = raw?.pattern ?? []
if type_of(pattern) != "list" {
pattern = []
}
return {
segments: segments,
separator: separator,
pattern: pattern,
prefix: prefix,
suffix: suffix,
salt: raw?.salt ?? "",
rng: raw?.rng,
dictionary: {
adjectives: __slug_nonempty_list(custom_dictionary?.adjectives, __SLUG_ADJECTIVES),
nouns: __slug_nonempty_list(custom_dictionary?.nouns, __SLUG_NOUNS),
verbs: __slug_nonempty_list(custom_dictionary?.verbs, __SLUG_VERBS),
},
}
}
fn __slug_separator(value) {
let separator = to_string(value ?? "-")
if separator == "" {
return "-"
}
return separator
}
fn __slug_nonempty_list(value, fallback) {
if type_of(value) == "list" && len(value) > 0 {
return value
}
return fallback
}
fn __slug_segment_kind(pattern, index, segments) {
if len(pattern) > 0 {
return pattern[index % len(pattern)]
}
if segments == 1 {
return "noun"
}
if index == 0 {
return "adjective"
}
return "noun"
}
fn __slug_bucket(kind, dictionary) {
let normalized = lowercase(to_string(kind ?? "noun"))
if normalized == "adjective" || normalized == "adjectives" || normalized == "adj" {
return dictionary.adjectives
}
if normalized == "verb" || normalized == "verbs" {
return dictionary.verbs
}
return dictionary.nouns
}
fn __slug_random_word(bucket, rng) {
var word = nil
if rng != nil {
word = random_choice(rng, bucket)
} else {
word = harness.random.choice(bucket)
}
if word == nil && len(bucket) > 0 {
return bucket[0]
}
return word
}
fn __slug_seed_index(seed, index, size) {
if size <= 0 {
return 0
}
let digest = sha256(seed + ":" + to_string(index))
var value = 0
var offset = 0
while offset < 8 {
value = value * 16 + __slug_hex_digit(substring(digest, offset, offset + 1))
offset = offset + 1
}
return value % size
}
fn __slug_hex_digit(ch) {
let idx = "0123456789abcdef".index_of(lowercase(ch ?? ""))
if idx < 0 {
return 0
}
return idx
}
fn __slug_join_with_affixes(words, opts) {
var parts = []
if opts.prefix != nil && opts.prefix != "" {
parts = parts + [opts.prefix]
}
parts = parts + words
if opts.suffix != nil && opts.suffix != "" {
parts = parts + [opts.suffix]
}
return join(parts, opts.separator)
}
fn __slug_affix(value, separator) {
if value == nil {
return nil
}
return __slugify_text(value, separator)
}
fn __slugify_text(value, separator) {
let text = lowercase(trim(to_string(value ?? "")))
var out = regex_replace("[^a-z0-9]+", "-", text)
out = regex_replace("^-+|-+$", "", out)
if separator != "-" {
out = replace(out, "-", separator)
}
return out
}