@dicelette/core 1.28.3 → 1.28.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -166,6 +166,7 @@ var templateSchema = z.object({
166
166
  });
167
167
 
168
168
  // src/regex.ts
169
+ var REGEX_CACHE_MAX = 500;
169
170
  var regexCache = /* @__PURE__ */ new Map();
170
171
  var NORMALIZE_SINGLE_DICE = (str) => str.replace(/\b1d(\d+)/gi, "d$1");
171
172
  var REMOVER_PATTERN = {
@@ -180,6 +181,10 @@ function getCachedRegex(pattern, flags = "") {
180
181
  const key = `${pattern}|${flags}`;
181
182
  let regex = regexCache.get(key);
182
183
  if (!regex) {
184
+ if (regexCache.size >= REGEX_CACHE_MAX) {
185
+ const oldest = regexCache.keys().next().value;
186
+ if (oldest !== void 0) regexCache.delete(oldest);
187
+ }
183
188
  regex = new RegExp(pattern, flags);
184
189
  regexCache.set(key, regex);
185
190
  }
@@ -225,12 +230,12 @@ import { NumberGenerator as NumberGenerator2 } from "@dice-roller/rpg-dice-rolle
225
230
  function evalStatsDice(testDice, allStats, engine = NumberGenerator2.engines.nodeCrypto, pity) {
226
231
  let dice = testDice.trimEnd();
227
232
  if (allStats && Object.keys(allStats).length > 0) {
233
+ dice = dice.standardize();
228
234
  const names = Object.keys(allStats);
229
235
  for (const name of names) {
230
- const regex = new RegExp(escapeRegex(name.standardize()), "gi");
231
- if (dice.standardize().match(regex)) {
232
- const statValue = allStats[name];
233
- dice = dice.standardize().replace(regex, statValue.toString()).trimEnd();
236
+ const regex = getCachedRegex(name.standardize().escapeRegex(), "gi");
237
+ if (dice.match(regex)) {
238
+ dice = dice.replace(regex, allStats[name].toString()).trimEnd();
234
239
  }
235
240
  }
236
241
  }
@@ -248,8 +253,8 @@ function diceRandomParse(value, template, engine = NumberGenerator2.engines.node
248
253
  const statNames = Object.keys(template.statistics);
249
254
  let newDice = value;
250
255
  for (const name of statNames) {
251
- const regex = new RegExp(escapeRegex(name.standardize()), "gi");
252
- if (value.match(regex)) {
256
+ const regex = getCachedRegex(name.standardize().escapeRegex(), "gi");
257
+ if (newDice.match(regex)) {
253
258
  let max;
254
259
  let min;
255
260
  const foundStat = template.statistics?.[name];
@@ -259,7 +264,7 @@ function diceRandomParse(value, template, engine = NumberGenerator2.engines.node
259
264
  }
260
265
  const total = template.total || 100;
261
266
  const randomStatValue = generateRandomStat(total, max, min, engine);
262
- newDice = value.replace(regex, randomStatValue.toString());
267
+ newDice = newDice.replace(regex, randomStatValue.toString());
263
268
  }
264
269
  }
265
270
  return replaceFormulaInDice(newDice);
@@ -282,7 +287,7 @@ function evalCombinaison(combinaison, stats) {
282
287
  for (const [stat, combin] of Object.entries(combinaison)) {
283
288
  let formula = combin.standardize();
284
289
  for (const [statName, value] of Object.entries(stats)) {
285
- const regex = new RegExp(statName.standardize(), "gi");
290
+ const regex = getCachedRegex(statName.standardize().escapeRegex(), "gi");
286
291
  formula = formula.replace(regex, value.toString());
287
292
  }
288
293
  try {
@@ -296,7 +301,7 @@ function evalCombinaison(combinaison, stats) {
296
301
  function evalOneCombinaison(combinaison, stats) {
297
302
  let formula = combinaison.standardize();
298
303
  for (const [statName, value] of Object.entries(stats)) {
299
- const regex = new RegExp(statName.standardize(), "gi");
304
+ const regex = getCachedRegex(statName.standardize().escapeRegex(), "gi");
300
305
  formula = formula.replace(regex, value.toString());
301
306
  }
302
307
  try {
@@ -344,7 +349,7 @@ function verifyTemplateValue(template, verify = true, engine = NumberGenerator2.
344
349
  engine
345
350
  );
346
351
  const rolled = roll(cleanedDice2, engine);
347
- if (!rolled) throw new DiceTypeError(cleanedDice2, "no_roll_result", "no roll result");
352
+ if (!rolled) throw new DiceTypeError(cleanedDice2, "roll");
348
353
  }
349
354
  if (statistiqueTemplate.customCritical) {
350
355
  if (!statistiqueTemplate.diceType) {
@@ -369,15 +374,16 @@ function verifyTemplateValue(template, verify = true, engine = NumberGenerator2.
369
374
  }
370
375
  function testDiceRegistered(template, engine = NumberGenerator2.engines.nodeCrypto) {
371
376
  if (!template.damage) return;
372
- if (Object.keys(template.damage).length === 0) throw new EmptyObjectError();
373
- if (Object.keys(template.damage).length > 25) throw new TooManyDice();
374
- for (const [name, dice] of Object.entries(template.damage)) {
377
+ const damageEntries = Object.entries(template.damage);
378
+ if (damageEntries.length === 0) throw new EmptyObjectError();
379
+ if (damageEntries.length > 25) throw new TooManyDice();
380
+ for (const [name, dice] of damageEntries) {
375
381
  if (!dice) continue;
376
382
  const diceReplaced = replaceExpByRandom(dice);
377
383
  const randomDiceParsed = diceRandomParse(diceReplaced, template, engine);
378
384
  try {
379
385
  const rolled = roll(randomDiceParsed, engine);
380
- if (!rolled) throw new DiceTypeError(name, "no_roll_result", dice);
386
+ if (!rolled) throw new DiceTypeError(name, "testDiceRegistered", dice);
381
387
  } catch (error) {
382
388
  throw new DiceTypeError(name, "testDiceRegistered", error);
383
389
  }
@@ -385,19 +391,14 @@ function testDiceRegistered(template, engine = NumberGenerator2.engines.nodeCryp
385
391
  }
386
392
  function testStatCombinaison(template, engine = NumberGenerator2.engines.nodeCrypto) {
387
393
  if (!template.statistics) return;
388
- const onlycombinaisonStats = Object.fromEntries(
389
- Object.entries(template.statistics).filter(
390
- ([_, value]) => value.combinaison !== void 0
391
- )
392
- );
393
- const allOtherStats = Object.fromEntries(
394
- Object.entries(template.statistics).filter(([_, value]) => !value.combinaison)
395
- );
394
+ const onlycombinaisonStats = {};
395
+ const allOtherStats = {};
396
+ for (const [k, v] of Object.entries(template.statistics)) {
397
+ if (v.combinaison !== void 0) onlycombinaisonStats[k] = v;
398
+ else allOtherStats[k] = v;
399
+ }
396
400
  if (Object.keys(onlycombinaisonStats).length === 0) return;
397
- const allStats = Object.keys(template.statistics).filter(
398
- (stat) => !template.statistics[stat].combinaison
399
- );
400
- if (allStats.length === 0) throw new NoStatisticsError();
401
+ if (Object.keys(allOtherStats).length === 0) throw new NoStatisticsError();
401
402
  const error = [];
402
403
  for (const [stat, value] of Object.entries(onlycombinaisonStats)) {
403
404
  let formula = value.combinaison;
@@ -405,7 +406,7 @@ function testStatCombinaison(template, engine = NumberGenerator2.engines.nodeCry
405
406
  const { max, min } = data;
406
407
  const total = template.total || 100;
407
408
  const randomStatValue = generateRandomStat(total, max, min, engine);
408
- const regex = new RegExp(other, "gi");
409
+ const regex = getCachedRegex(other.escapeRegex(), "gi");
409
410
  formula = formula.replace(regex, randomStatValue.toString());
410
411
  }
411
412
  try {
@@ -418,27 +419,21 @@ function testStatCombinaison(template, engine = NumberGenerator2.engines.nodeCry
418
419
  return;
419
420
  }
420
421
  function generateRandomStat(total = 100, max, min, engine = NumberGenerator2.engines.nodeCrypto) {
421
- let randomStatValue = total + 1;
422
+ if (total <= 1) throw new RangeError(`total must be greater than 1, got ${total}`);
422
423
  const random = new Random(engine || NumberGenerator2.engines.nodeCrypto);
423
- while (randomStatValue >= total || randomStatValue === 0) {
424
- if (max && min) randomStatValue = randomInt(min, max, engine, random);
425
- else if (max) randomStatValue = randomInt(1, max, engine, random);
426
- else if (min) randomStatValue = randomInt(min, total, engine, random);
427
- else randomStatValue = randomInt(1, total, engine, random);
428
- }
429
- return randomStatValue;
424
+ const effectiveMin = Math.max(min ?? 1, 1);
425
+ const effectiveMax = Math.min(max ?? total - 1, total - 1);
426
+ if (effectiveMin > effectiveMax) throw new MaxGreater(effectiveMin, effectiveMax);
427
+ return randomInt(effectiveMin, effectiveMax, engine, random);
430
428
  }
431
429
 
432
430
  // src/utils.ts
433
431
  function splitDiceComment(dice) {
434
- const match = /\s+(#|\/{2}|\[|\/\*)(?<comment>.*)/i.exec(dice);
432
+ const match = /\s(#|\/{2}|\[|\/\*)(?<comment>.*)/i.exec(dice);
435
433
  if (!match?.groups) return { dice: dice.trimEnd(), comment: void 0 };
436
434
  const comment = match.groups.comment.trim() || void 0;
437
435
  return { dice: dice.slice(0, match.index).trimEnd(), comment };
438
436
  }
439
- function escapeRegex(string) {
440
- return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
441
- }
442
437
  function standardizeDice(dice) {
443
438
  return dice.replace(
444
439
  /(\[[^\]]+])|([^[]+)/g,
@@ -583,32 +578,31 @@ import { NumberGenerator as NumberGenerator5 } from "@dice-roller/rpg-dice-rolle
583
578
  import { evaluate as evaluate3 } from "mathjs";
584
579
 
585
580
  // src/dice/replace.ts
581
+ var PARENTHESIS_REGEX = /d\((\d+)\)/g;
582
+ var BRACKET_COMMENT_REGEX = /\[(?<comments>.*?)\]/;
583
+ var OPTIONAL_COMMENT_REGEX = /\s+(#|\/\/)(?<comment>.*)/;
586
584
  function replaceUnwantedText(dice, sortOrder) {
587
- const d = dice.replaceAll(/[{}]/g, "").replaceAll(/s[ad]/gi, "");
588
- if (sortOrder) return sortDice(d, sortOrder);
585
+ let d = dice.replaceAll(/[{}]/g, "").replaceAll(/s[ad]/gi, "");
586
+ if (sortOrder) d = sortDice(d, sortOrder);
587
+ if (!d.length) throw new DiceTypeError(dice, "empty_dice");
589
588
  return d;
590
589
  }
591
590
  function sortDice(dice, sortOrder) {
592
591
  if (sortOrder === "none" /* None */) return dice;
593
592
  const dices = dice.split(/; ?/);
593
+ const decorated = dices.map((d) => ({
594
+ d,
595
+ v: Number.parseInt(d.split("= ")[1], 10) || 0
596
+ }));
594
597
  if (sortOrder === "sa" /* Ascending */) {
595
- dices.sort((a, b) => {
596
- const totalA = Number.parseInt(a.split("= ")[1], 10) || 0;
597
- const totalB = Number.parseInt(b.split("= ")[1], 10) || 0;
598
- return totalB - totalA;
599
- });
598
+ decorated.sort((a, b) => b.v - a.v);
600
599
  } else if (sortOrder === "sd" /* Descending */) {
601
- dices.sort((a, b) => {
602
- const totalA = Number.parseInt(a.split("= ")[1], 10) || 0;
603
- const totalB = Number.parseInt(b.split("= ")[1], 10) || 0;
604
- return totalA - totalB;
605
- });
600
+ decorated.sort((a, b) => a.v - b.v);
606
601
  }
607
- return dices.join("; ");
602
+ return decorated.map((x) => x.d).join("; ");
608
603
  }
609
604
  function fixParenthesis(dice) {
610
- const parenthesisRegex = /d\((\d+)\)/g;
611
- return dice.replaceAll(parenthesisRegex, (_match, p1) => `d${p1}`);
605
+ return dice.replaceAll(PARENTHESIS_REGEX, (_match, p1) => `d${p1}`);
612
606
  }
613
607
  function replaceText(element, total, dice) {
614
608
  return {
@@ -617,12 +611,10 @@ function replaceText(element, total, dice) {
617
611
  };
618
612
  }
619
613
  function formatComment(dice) {
620
- const commentsRegex = /\[(?<comments>.*?)\]/;
621
- const commentsMatch = commentsRegex.exec(dice);
614
+ const commentsMatch = BRACKET_COMMENT_REGEX.exec(dice);
622
615
  const comments = commentsMatch?.groups?.comments ? `${commentsMatch.groups.comments}` : "";
623
- const diceWithoutBrackets = dice.replace(commentsRegex, "");
624
- const optionalCommentsRegex = /\s+(#|\/\/)(?<comment>.*)/;
625
- const optionalComments = optionalCommentsRegex.exec(diceWithoutBrackets);
616
+ const diceWithoutBrackets = dice.replace(BRACKET_COMMENT_REGEX, "");
617
+ const optionalComments = OPTIONAL_COMMENT_REGEX.exec(diceWithoutBrackets);
626
618
  const optional = optionalComments?.groups?.comment ? `${optionalComments.groups.comment.trim()}` : "";
627
619
  let finalComment = "";
628
620
  if (comments && optional) finalComment = `__${comments} ${optional}__ \u2014 `;
@@ -774,23 +766,27 @@ function calculateSimilarity(str1, str2) {
774
766
  return (longer.length - distance) / longer.length;
775
767
  }
776
768
  function levenshteinDistance(str1, str2) {
777
- const matrix = Array(str2.length + 1).fill(null).map(() => Array(str1.length + 1).fill(null));
778
- for (let i = 0; i <= str1.length; i++) matrix[0][i] = i;
779
- for (let j = 0; j <= str2.length; j++) matrix[j][0] = j;
769
+ if (str1.length > str2.length) {
770
+ [str1, str2] = [str2, str1];
771
+ }
772
+ let prev = Array.from({ length: str1.length + 1 }, (_, i) => i);
773
+ let curr = new Array(str1.length + 1);
780
774
  for (let j = 1; j <= str2.length; j++) {
775
+ curr[0] = j;
781
776
  for (let i = 1; i <= str1.length; i++) {
782
777
  const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
783
- matrix[j][i] = Math.min(
784
- matrix[j][i - 1] + 1,
778
+ curr[i] = Math.min(
779
+ curr[i - 1] + 1,
785
780
  // insertion
786
- matrix[j - 1][i] + 1,
781
+ prev[i] + 1,
787
782
  // deletion
788
- matrix[j - 1][i - 1] + cost
783
+ prev[i - 1] + cost
789
784
  // substitution
790
785
  );
791
786
  }
787
+ [prev, curr] = [curr, prev];
792
788
  }
793
- return matrix[str2.length][str1.length];
789
+ return prev[str1.length];
794
790
  }
795
791
  function findBestStatMatch(searchTerm, normalizedStats, similarityThreshold = MIN_THRESHOLD_MATCH) {
796
792
  const exact = normalizedStats.get(searchTerm);
@@ -1067,25 +1063,20 @@ function prepareDice(diceInput) {
1067
1063
  dice = dice.replaceAll(REMOVER_PATTERN.CRITICAL_BLOCK, "").trimEnd();
1068
1064
  const explodingSuccess = normalizeExplodingSuccess(dice);
1069
1065
  if (explodingSuccess) dice = explodingSuccess.dice;
1070
- let diceDisplay;
1071
- if (dice.includes(";")) {
1072
- const mainDice = dice.split(";")[0];
1073
- diceDisplay = explodingSuccess?.originalDice ?? mainDice;
1074
- } else {
1075
- diceDisplay = explodingSuccess?.originalDice ?? dice;
1076
- }
1066
+ const sharedSeparatorIndex = dice.indexOf(";");
1067
+ const hasSharedSeparator = sharedSeparatorIndex !== -1;
1068
+ let diceDisplay = explodingSuccess?.originalDice ?? (hasSharedSeparator ? dice.slice(0, sharedSeparatorIndex) : dice);
1077
1069
  const curlyBulkMatch = dice.match(/^\{(\d+#.*)\}$/);
1078
1070
  const isCurlyBulk = !!curlyBulkMatch;
1079
1071
  const bulkContent = isCurlyBulk ? curlyBulkMatch[1] : "";
1080
- const isSharedRoll = dice.includes(";");
1081
1072
  let isSharedCurly = false;
1082
- if (isSharedRoll && dice.match(/^\{.*;\s*.*\}$/)) {
1073
+ if (hasSharedSeparator && dice.match(/^\{.*;\s*.*\}$/)) {
1083
1074
  dice = dice.slice(1, -1);
1084
1075
  isSharedCurly = true;
1085
1076
  diceDisplay = diceDisplay.slice(1);
1086
1077
  }
1087
1078
  let isSimpleCurly = false;
1088
- if (!isCurlyBulk && !isSharedRoll && dice.match(/^\{.*\}$/)) {
1079
+ if (!isCurlyBulk && !hasSharedSeparator && dice.match(/^\{.*\}$/)) {
1089
1080
  const innerContent = dice.slice(1, -1);
1090
1081
  const hasModifiers = innerContent.match(/[+\-*/%^]/);
1091
1082
  const hasComparison = innerContent.match(/(([><=!]+\d+f)|([><=]|!=)+\d+)/);
@@ -1098,7 +1089,7 @@ function prepareDice(diceInput) {
1098
1089
  dice,
1099
1090
  diceDisplay,
1100
1091
  explodingSuccess,
1101
- isSharedRoll,
1092
+ isSharedRoll: hasSharedSeparator,
1102
1093
  isSharedCurly,
1103
1094
  isCurlyBulk,
1104
1095
  bulkContent,
@@ -1115,7 +1106,13 @@ function getSortOrder(dice) {
1115
1106
  function handleBulkRolls(dice, isCurlyBulk, bulkContent, compare, explodingSuccess, diceDisplay, engine, sort) {
1116
1107
  const bulkProcessContent = isCurlyBulk ? bulkContent : dice;
1117
1108
  const diceArray = bulkProcessContent.split("#");
1109
+ if (!isNumber(diceArray[0])) {
1110
+ throw new DiceTypeError(dice, "bulk_number");
1111
+ }
1118
1112
  const numberOfDice = Number.parseInt(diceArray[0], 10);
1113
+ if (numberOfDice <= 0) {
1114
+ throw new DiceTypeError(dice, "bulk_zero");
1115
+ }
1119
1116
  const { dice: diceToRollBase, comment: comments } = splitDiceComment(diceArray[1]);
1120
1117
  let diceToRoll = diceToRollBase;
1121
1118
  let curlyCompare;
@@ -1298,6 +1295,7 @@ function handlePitySystem(dice, compare, diceRoll, roller, engine) {
1298
1295
  isFail = evaluate9(`${res.total}${compare.sign}${compare.value}`);
1299
1296
  }
1300
1297
  }
1298
+ if (!res?.result.length) throw new DiceTypeError(dice, "empty_dice");
1301
1299
  return { rerollCount, result: res };
1302
1300
  }
1303
1301
 
@@ -1399,6 +1397,7 @@ function roll(dice, engine = NumberGenerator8.engines.nodeCrypto, pity, sort, co
1399
1397
  prepared.explodingSuccess.normalizedSegment,
1400
1398
  prepared.explodingSuccess.originalSegment
1401
1399
  );
1400
+ if (!resultOutput.length) throw new DiceTypeError(dice, "empty_dice");
1402
1401
  return {
1403
1402
  dice: prepared.isSimpleCurly ? finalDiceDisplay : prepared.diceDisplay,
1404
1403
  result: resultOutput,
@@ -1608,7 +1607,6 @@ export {
1608
1607
  createCriticalCustom,
1609
1608
  diceRandomParse,
1610
1609
  diceTypeRandomParse,
1611
- escapeRegex,
1612
1610
  evalCombinaison,
1613
1611
  evalOneCombinaison,
1614
1612
  evalStatsDice,