completeJson.ts 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import type {JsonToken} from './constants';
  2. import {
  3. ARR,
  4. ARR_VAL,
  5. ARR_VAL_COMPLETED,
  6. ARR_VAL_STR,
  7. OBJ,
  8. OBJ_KEY,
  9. OBJ_KEY_STR,
  10. OBJ_VAL,
  11. OBJ_VAL_COMPLETED,
  12. OBJ_VAL_STR,
  13. } from './constants';
  14. const ALLOWED_PRIMITIVES = ['true', 'false', 'null'];
  15. /**
  16. * Complete an incomplete JSON string.
  17. * This will ensure that the last element always has a `"~~"` to indicate it was truncated.
  18. * For example, `[1,2,` will be completed to `[1,2,"~~"]`
  19. * and `{"aa":"b` will be completed to `{"aa":"b~~"}`
  20. */
  21. export function completeJson(incompleteJson: string, stack: JsonToken[]): string {
  22. if (!stack.length) {
  23. return incompleteJson;
  24. }
  25. let json = incompleteJson;
  26. // Most checks are only needed for the last step in the stack
  27. const lastPos = stack.length - 1;
  28. const lastStep = stack[lastPos];
  29. json = _fixLastStep(json, lastStep);
  30. // Complete remaining steps - just add closing brackets
  31. for (let i = lastPos; i >= 0; i--) {
  32. const step = stack[i];
  33. // eslint-disable-next-line default-case
  34. switch (step) {
  35. case OBJ:
  36. json = `${json}}`;
  37. break;
  38. case ARR:
  39. json = `${json}]`;
  40. break;
  41. }
  42. }
  43. return json;
  44. }
  45. function _fixLastStep(json: string, lastStep: JsonToken): string {
  46. switch (lastStep) {
  47. // Object cases
  48. case OBJ:
  49. return `${json}"~~":"~~"`;
  50. case OBJ_KEY:
  51. return `${json}:"~~"`;
  52. case OBJ_KEY_STR:
  53. return `${json}~~":"~~"`;
  54. case OBJ_VAL:
  55. return _maybeFixIncompleteObjValue(json);
  56. case OBJ_VAL_STR:
  57. return `${json}~~"`;
  58. case OBJ_VAL_COMPLETED:
  59. return `${json},"~~":"~~"`;
  60. // Array cases
  61. case ARR:
  62. return `${json}"~~"`;
  63. case ARR_VAL:
  64. return _maybeFixIncompleteArrValue(json);
  65. case ARR_VAL_STR:
  66. return `${json}~~"`;
  67. case ARR_VAL_COMPLETED:
  68. return `${json},"~~"`;
  69. default:
  70. return json;
  71. }
  72. }
  73. function _maybeFixIncompleteArrValue(json: string): string {
  74. const pos = _findLastArrayDelimiter(json);
  75. if (pos > -1) {
  76. const part = json.slice(pos + 1);
  77. if (ALLOWED_PRIMITIVES.includes(part.trim())) {
  78. return `${json},"~~"`;
  79. }
  80. // Everything else is replaced with `"~~"`
  81. return `${json.slice(0, pos + 1)}"~~"`;
  82. }
  83. // fallback, this shouldn't happen, to be save
  84. return json;
  85. }
  86. function _findLastArrayDelimiter(json: string): number {
  87. for (let i = json.length - 1; i >= 0; i--) {
  88. const char = json[i];
  89. if (char === ',' || char === '[') {
  90. return i;
  91. }
  92. }
  93. return -1;
  94. }
  95. function _maybeFixIncompleteObjValue(json: string): string {
  96. const startPos = json.lastIndexOf(':');
  97. const part = json.slice(startPos + 1);
  98. if (ALLOWED_PRIMITIVES.includes(part.trim())) {
  99. return `${json},"~~":"~~"`;
  100. }
  101. // Everything else is replaced with `"~~"`
  102. // This also means we do not have incomplete numbers, e.g `[1` is replaced with `["~~"]`
  103. return `${json.slice(0, startPos + 1)}"~~"`;
  104. }