PlaceholdersBuffer.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. /**
  2. * Provides a stateful means of managing placeholders in text.
  3. *
  4. * Placeholders are numbers prefixed with the `$` character (e.g. `$1`).
  5. * Each number value represents the order in which a placeholder should
  6. * receive focus if multiple placeholders exist.
  7. *
  8. * Example scenario given `sum($3 offset $1) by($2)`:
  9. * 1. `sum( offset |) by()`
  10. * 2. `sum( offset 1h) by(|)`
  11. * 3. `sum(| offset 1h) by (label)`
  12. */
  13. export default class PlaceholdersBuffer {
  14. private nextMoveOffset: number;
  15. private orders: number[];
  16. private parts: string[];
  17. constructor(text: string) {
  18. const result = this.parse(text);
  19. const nextPlaceholderIndex = result.orders.length ? result.orders[0] : 0;
  20. this.nextMoveOffset = this.getOffsetBetween(result.parts, 0, nextPlaceholderIndex);
  21. this.orders = result.orders;
  22. this.parts = result.parts;
  23. }
  24. clearPlaceholders() {
  25. this.nextMoveOffset = 0;
  26. this.orders = [];
  27. }
  28. getNextMoveOffset(): number {
  29. return this.nextMoveOffset;
  30. }
  31. hasPlaceholders(): boolean {
  32. return this.orders.length > 0;
  33. }
  34. setNextPlaceholderValue(value: string) {
  35. if (this.orders.length === 0) {
  36. return;
  37. }
  38. const currentPlaceholderIndex = this.orders[0];
  39. this.parts[currentPlaceholderIndex] = value;
  40. this.orders = this.orders.slice(1);
  41. if (this.orders.length === 0) {
  42. this.nextMoveOffset = 0;
  43. return;
  44. }
  45. const nextPlaceholderIndex = this.orders[0];
  46. // Case should never happen but handle it gracefully in case
  47. if (currentPlaceholderIndex === nextPlaceholderIndex) {
  48. this.nextMoveOffset = 0;
  49. return;
  50. }
  51. const backwardMove = currentPlaceholderIndex > nextPlaceholderIndex;
  52. const indices = backwardMove
  53. ? { start: nextPlaceholderIndex + 1, end: currentPlaceholderIndex + 1 }
  54. : { start: currentPlaceholderIndex + 1, end: nextPlaceholderIndex };
  55. this.nextMoveOffset = (backwardMove ? -1 : 1) * this.getOffsetBetween(this.parts, indices.start, indices.end);
  56. }
  57. toString(): string {
  58. return this.parts.join('');
  59. }
  60. private getOffsetBetween(parts: string[], startIndex: number, endIndex: number) {
  61. return parts.slice(startIndex, endIndex).reduce((offset, part) => offset + part.length, 0);
  62. }
  63. private parse(text: string): ParseResult {
  64. const placeholderRegExp = /\$(\d+)/g;
  65. const parts = [];
  66. const orders = [];
  67. let textOffset = 0;
  68. while (true) {
  69. const match = placeholderRegExp.exec(text);
  70. if (!match) {
  71. break;
  72. }
  73. const part = text.slice(textOffset, match.index);
  74. parts.push(part);
  75. // Accounts for placeholders at text boundaries
  76. if (part !== '') {
  77. parts.push('');
  78. }
  79. const order = parseInt(match[1], 10);
  80. orders.push({ index: parts.length - 1, order });
  81. textOffset += part.length + match.length;
  82. }
  83. // Ensures string serialization still works if no placeholders were parsed
  84. // and also accounts for the remainder of text with placeholders
  85. parts.push(text.slice(textOffset));
  86. return {
  87. // Placeholder values do not necessarily appear sequentially so sort the
  88. // indices to traverse in priority order
  89. orders: orders.sort((o1, o2) => o1.order - o2.order).map(o => o.index),
  90. parts,
  91. };
  92. }
  93. }
  94. type ParseResult = {
  95. /**
  96. * Indices to placeholder items in `parts` in traversal order.
  97. */
  98. orders: number[];
  99. /**
  100. * Parts comprising the original text with placeholders occupying distinct items.
  101. */
  102. parts: string[];
  103. };