You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
100 lines
3.5 KiB
100 lines
3.5 KiB
export interface WeightedText {
|
|
readonly wt: number
|
|
readonly v: WeightedTextSelector
|
|
}
|
|
export type WeightedTextListItem = WeightedText | string | WeightedTextList
|
|
export type WeightedTextList = readonly WeightedTextListItem[]
|
|
export type WeightedTextSelector = string | WeightedTextList
|
|
|
|
export interface TextSource {
|
|
[key: string]: WeightedTextSelector
|
|
}
|
|
|
|
function isWeightedText(t: WeightedTextListItem): t is WeightedText {
|
|
return typeof t === 'object' && 'wt' in t
|
|
}
|
|
|
|
interface TotalWeightedText {
|
|
readonly minValue: number
|
|
readonly maxValue: number
|
|
readonly result: WeightedTextSelector
|
|
}
|
|
|
|
function totalWeightedText(minValue: number, t: WeightedTextListItem): TotalWeightedText {
|
|
if (isWeightedText(t)) {
|
|
return {
|
|
minValue,
|
|
maxValue: minValue + t.wt,
|
|
result: t.v,
|
|
}
|
|
}
|
|
return {
|
|
minValue,
|
|
maxValue: minValue + 1,
|
|
result: t,
|
|
}
|
|
}
|
|
|
|
export function selectAndFulfillWeightedText(
|
|
text: WeightedTextSelector,
|
|
data: TextSource,
|
|
random: () => number,
|
|
): string {
|
|
if (typeof text === 'string') {
|
|
if (textRequiresFulfillment(text)) {
|
|
return fulfillText(text, data, random)
|
|
} else {
|
|
return text
|
|
}
|
|
} else {
|
|
if (text.length === 0) {
|
|
return ''
|
|
} else if (text.length === 1) {
|
|
return selectAndFulfillWeightedText(isWeightedText(text[0]) ? text[0].v : text[0], data, random)
|
|
}
|
|
const totalWeightedTexts = text.reduce<readonly TotalWeightedText[]>((accumulated, next) => {
|
|
const weighted =
|
|
accumulated.length === 0
|
|
? totalWeightedText(0, next)
|
|
: totalWeightedText(accumulated[accumulated.length - 1].maxValue, next)
|
|
return [...accumulated, weighted]
|
|
}, [])
|
|
const maxValue = totalWeightedTexts[totalWeightedTexts.length - 1].maxValue
|
|
const selectedValue = random() * maxValue
|
|
const selectedItem =
|
|
totalWeightedTexts.find((value) => {
|
|
return value.minValue <= selectedValue && value.maxValue < selectedValue
|
|
})?.result ?? ''
|
|
return selectAndFulfillWeightedText(selectedItem, data, random)
|
|
}
|
|
}
|
|
|
|
const textReplacementRE = /\\(.)|\{\{\s*([A-Za-z0-9_]+)\s*}}/g
|
|
|
|
export function textRequiresFulfillment(text: string): boolean {
|
|
return textReplacementRE.test(text)
|
|
}
|
|
|
|
export function fulfillText(text: string, data: TextSource, random: () => number): string {
|
|
return text.replaceAll(
|
|
textReplacementRE,
|
|
(substring, escapedCharacter: string | undefined, fieldName: string | undefined): string => {
|
|
if (typeof escapedCharacter === 'string') {
|
|
if (escapedCharacter === '\\' || escapedCharacter === '{') {
|
|
return escapedCharacter
|
|
} else {
|
|
throw Error('unknown escape ' + substring)
|
|
}
|
|
}
|
|
if (typeof fieldName === 'string') {
|
|
const substitution = data[fieldName]
|
|
if (typeof substitution === 'undefined') {
|
|
throw Error('unknown variable or commonText ' + fieldName)
|
|
}
|
|
return selectAndFulfillWeightedText(substitution, data, random)
|
|
}
|
|
// We know this can't happen because one of the capturing groups always captures, but Typescript and Istanbul don't.
|
|
/* istanbul ignore next */ throw Error('unknown replacement ' + substring)
|
|
},
|
|
)
|
|
}
|
|
|