parent
fcf2b8a463
commit
ff5343e1ca
File diff suppressed because it is too large
Load Diff
@ -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
|
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
|
--- a/node_modules/slash-create/lib/structures/interfaces/messageInteraction.d.ts
|
||||||
+++ b/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}. */
|
/** A file within {@link EditMessageOptions}. */
|
||||||
export interface MessageFile {
|
export interface MessageFile {
|
||||||
/** The attachment to send. */
|
/** 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