Files
typst-collection/glossy/src/utils.typ
T

262 lines
7.0 KiB
Typst

// Returns the provided value if it is not none, otherwise returns the default value
// Parameters:
// val: The value to check
// default: The fallback value to use if val is none
// Returns: val if not none, otherwise default
#let __default(val, default) = if val == none { default } else { val }
// Converts an English word to its plural form following standard English pluralization rules
// Parameters:
// word: The singular word to pluralize
// Returns: The pluralized form of the word, or none if input is none
#let __pluralize(word) = {
// Early exit for invalid input
if word == none {
return none
}
// Helper functions for checking endings
let ends_with = (suffix) => lower(word).ends-with(suffix)
let ends_with_any = (suffixes) => suffixes.any((suffix) => ends_with(suffix))
// Map of irregular plurals for common words
let irregulars = (
"alumna": "alumnae",
"alumnus": "alumni",
"analysis": "analyses",
"appendix": "appendices",
"basis": "bases",
"cactus": "cacti",
"child": "children",
"crisis": "crises",
"criterion": "criteria",
"datum": "data",
"foot": "feet",
"fungus": "fungi",
"goose": "geese",
"man": "men",
"medium": "media",
"mouse": "mice",
"nucleus": "nuclei",
"oasis": "oases",
"person": "people",
"phenomenon": "phenomena",
"stimulus": "stimuli",
"thesis": "theses",
"tooth": "teeth",
"woman": "women",
)
// Set of plural-only words
let plural_only = (
"advice",
"alumni",
"bison",
"children",
"criteria",
"data",
"deer",
"feet",
"fish",
"geese",
"information",
"media",
"men",
"mice",
"money",
"moose",
"octopi",
"octopodes",
"people",
"scissors",
"series",
"sheep",
"species",
"teeth",
"trousers",
"women",
)
// Irregular suffixes
let f_to_ves = ("leaf", "loaf", "calf", "half", "wolf", "thief")
let fe_to_ves = ("wife", "knife", "life")
let o_to_os = ("photo", "piano", "halo", "radio", "video", "studio", "solo", "taco", "memo", "zero")
// Convenience
let w = lower(word)
// Check for words already plural
if plural_only.contains(w) {
return word
}
// Check for irregular singulars
if irregulars.keys().contains(w) {
return irregulars.at(w)
}
// Standard pluralization rules
if ends_with("iz") {
word + "zes"
} else if ends_with_any(("s", "x", "z", "sh", "ch")) {
word + "es"
} else if ends_with("y") {
if not ends_with_any(("ay", "ey", "iy", "oy", "uy")) {
word.slice(0, -1) + "ies"
} else {
word + "s"
}
} else if ends_with("f") {
if f_to_ves.contains(w) {
word.slice(0, -1) + "ves"
} else {
word + "s"
}
} else if ends_with("fe") {
if fe_to_ves.contains(w) {
word.slice(0, -2) + "ves"
} else {
word + "s"
}
} else if ends_with("us") {
word.slice(0, -2) + "i"
} else if ends_with("is") {
word.slice(0, -2) + "es"
} else if ends_with("on") {
word + "s"
} else if ends_with("o") {
if o_to_os.contains(w) {
word + "s"
} else {
word + "es"
}
} else {
word + "s"
}
}
// Determines the appropriate indefinite article ("a" or "an") for a given English word.
//
// Heuristic rules:
// 1. If word looks like an acronym (all uppercase or letters separated by dots):
// - Use "an" if the first letter's name starts with a vowel sound, e.g. A (ay), E (ee), F (eff), H (aitch), I (eye), L (el), M (em), N (en), O (oh), R (ar), S (ess), X (ex).
// - Otherwise "a".
//
// 2. Otherwise, lowercase the word and apply standard rules:
// - Starts with a vowel (a, e, i, o): "an".
// - Starts with "u":
// * If it begins with "uni", "use", "user", or "eu" (e.g. "university", "use", "Europe"), we treat it as a "yoo" sound -> "a".
// * Otherwise -> "an".
// - Special silent 'h' words: if it starts with "hour", "honest", "honor", "honour", or other, use "an".
// Otherwise if starts with 'h', use "a".
// - Default: "a".
//
// Notes:
// - This is a best-effort heuristic and won't be perfect.
// - These are for singular forms. If plural form is requested alongside
// articles, the caller should panic before calling this function.
//
// Returns:
// "a" or "an"
//
// Example:
// __determine_article("RF") -> "an" (Ar-Ef)
// __determine_article("radio") -> "a"
// __determine_article("hour") -> "an"
// __determine_article("honest") -> "an"
// __determine_article("university") -> "a"
// __determine_article("umbrella") -> "an"
// __determine_article("Europe") -> "a" (because we assume "yoo-ruhp")
#let __determine_article(word) = {
// Check input
if word == none or word.trim() == "" {
// If invalid, default to none
return none
}
// Normalize the word
let upper_word = word.trim()
let lower_word = lower(upper_word)
// When these letters are spelled out, they get an "an" (i.e. "an RFC")
let acronym_vowels = (
"A", // "ay"
"E", // "ee"
"F", // "eff"
"H", // "aitch"
"I", // "eye"
"L", // "el"
"M", // "em"
"N", // "en"
"O", // "oh"
"R", // "ar"
"S", // "ess"
"X", // "ex"
)
// Helper to detect if word looks like an acronym
let is_acronym = {
// All uppercase or letters separated by non-alphabetical chars
let cleaned = upper_word.replace(".", "").replace("-", "")
cleaned.codepoints().all(c => c == upper(c)) and cleaned.len() > 0
}
if is_acronym {
// Use acronym rules
let first_char = upper_word.codepoints().slice(0,1).first()
if acronym_vowels.contains(first_char) {
"an"
} else {
"a"
}
} else {
// Non-acronym rules
let first_char = lower_word.codepoints().slice(0,1).first()
// Special cases for words matching a set of known 'silent h' prefixes
let silent_h_words = (
"heir", // "air"
"herb", // "erb" (in American English)
"homage", // "oh-mij" or "ah-mij"
"honest", // "on-est"
"honor", // "on-or" (American English)
"honour", // "on-our" (British English)
"hour", // "our"
)
if silent_h_words.any(w => lower_word.starts-with(w)) {
return "an"
}
// Words starting with 'eu', which has the 'yoo' sound
if lower_word.starts-with("eu") {
return "a"
}
// Words starting with 'u', that get the 'yoo' sound
if first_char == "u" {
let yoo_sounds = (
"ubi", // ubiquitous
"uni", // unicycle, uniform, universe, university, unilateral, unique, union, etc.
"usa", // usage, usability
"use", // use, useful, useless
"usu", // usual, usually
"usur", // usurp
"ute", // utensil, uterus
"uti", // utility, utilize, utilization
"uto", // utopia, utopian
)
if yoo_sounds.any(w => lower_word.starts-with(w)) {
return "a"
}
}
// Vowel start
if "aeiou".contains(first_char) {
return "an"
}
// the default case, plus 'h' words not in the silent list
"a"
}
}