262 lines
7.0 KiB
Typst
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"
|
|
}
|
|
}
|