parent
fcf2b8a463
commit
ff5343e1ca
@ -1,8 +1,8 @@ |
||||
diff --git a/node_modules/slash-create/lib/structures/interfaces/messageInteraction.d.ts b/node_modules/slash-create/lib/structures/interfaces/messageInteraction.d.ts
|
||||
index 9ecfd7f..70ec168 100644
|
||||
index df895c3..f2a5878 100644
|
||||
--- a/node_modules/slash-create/lib/structures/interfaces/messageInteraction.d.ts
|
||||
+++ b/node_modules/slash-create/lib/structures/interfaces/messageInteraction.d.ts
|
||||
@@ -153,7 +153,7 @@ export interface EditMessageOptions {
|
||||
@@ -132,7 +132,7 @@ export interface EditMessageOptions {
|
||||
/** A file within {@link EditMessageOptions}. */
|
||||
export interface MessageFile {
|
||||
/** The attachment to send. */
|
@ -0,0 +1,51 @@ |
||||
import { describe, expect, test } from '@jest/globals' |
||||
import { fulfillText, TextSource } from './weightedText' |
||||
import { forbidRandomness } from '../types/random' |
||||
|
||||
const source: TextSource = { |
||||
empty: '', |
||||
emptyList: [], |
||||
example: 'basic string value', |
||||
singletonList: ['singleton value'], |
||||
singleWeightedItem: [{ wt: 5, v: 'weighted item' }], |
||||
nestedItem: [[[['layered value']]]], |
||||
nestedWeightedItem: [[{ wt: 5, v: [[['innermost text']]] }]], |
||||
} |
||||
|
||||
describe('fulfillText', () => { |
||||
test.each<[string, string, string]>([ |
||||
[' returns blank string for blank string', '', ''], |
||||
[' returns constant string for no-substitution string', 'candy', 'candy'], |
||||
[' does not replace empty brackets', '{{}}', '{{}}'], |
||||
[' does not replace escaped brackets', '\\{{empty}}', '{{empty}}'], |
||||
[' does not replace half started brackets', '{{ empty', '{{ empty'], |
||||
[' does not replace brackets without only word characters inside', '{{ ??? }}', '{{ ??? }}'], |
||||
[' escapes backslash and replaces brackets after', '\\\\{{empty}}', '\\'], |
||||
[' replaces empty reference with empty value', '{{empty}}', ''], |
||||
[' replaces empty list reference with empty value', '{{emptyList}}', ''], |
||||
[' replaces string reference with value', '{{example}}', 'basic string value'], |
||||
[' replaces singleton list reference with value', '{{singletonList}}', 'singleton value'], |
||||
[' replaces singleton list reference with weighted value', '{{singleWeightedItem}}', 'weighted item'], |
||||
[' replaces multilevel singleton list reference with inner value', '{{nestedItem}}', 'layered value'], |
||||
[ |
||||
' replaces multilevel singleton list reference through weighted item with inner value', |
||||
'{{nestedWeightedItem}}', |
||||
'innermost text', |
||||
], |
||||
])('%s (%p -> %p)', (caseName, input, result) => { |
||||
expect(fulfillText(input, source, forbidRandomness)).toEqual(result) |
||||
}) |
||||
|
||||
test.each<[string, string]>([ |
||||
[' forbids \\ not followed by { or \\', '\\c'], |
||||
[' forbids reference to nonexistent value', '{{ nonexistentKey }}'], |
||||
])('%s (%p -> throws)', (caseName, input) => { |
||||
expect(() => fulfillText(input, source, forbidRandomness)).toThrow() |
||||
}) |
||||
}) |
||||
|
||||
describe('selectAndFulfillWeightedText', () => { |
||||
test(' needs tests', () => { |
||||
expect('tests').toBe('written') |
||||
}) |
||||
}) |
@ -0,0 +1,100 @@ |
||||
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) |
||||
}, |
||||
) |
||||
} |
@ -0,0 +1,8 @@ |
||||
import { describe, test, expect } from '@jest/globals' |
||||
import { forbidRandomness } from './random' |
||||
|
||||
describe('forbidRandomness', () => { |
||||
test(' throws', () => { |
||||
expect(forbidRandomness).toThrow() |
||||
}) |
||||
}) |
@ -0,0 +1,6 @@ |
||||
// Function that returns a floating point number x such that 0 <= x < 1
|
||||
export type RandomFunction = () => number |
||||
|
||||
export function forbidRandomness(): number { |
||||
throw Error('bad random') |
||||
} |
Loading…
Reference in new issue