parser.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import { parse } from "yaml";
  2. import type { JsonPatch } from "@json-render/core";
  3. import { diffToPatches } from "./diff";
  4. /**
  5. * Streaming YAML compiler that incrementally parses YAML text and emits
  6. * JSON Patch operations for each change detected.
  7. *
  8. * Same interface shape as `SpecStreamCompiler` from `@json-render/core`.
  9. */
  10. export interface YamlStreamCompiler<T> {
  11. /** Push a chunk of text. Returns the current result and any new patches. */
  12. push(chunk: string): { result: T; newPatches: JsonPatch[] };
  13. /** Flush remaining buffer and return the final result. */
  14. flush(): { result: T; newPatches: JsonPatch[] };
  15. /** Get the current compiled result. */
  16. getResult(): T;
  17. /** Get all patches that have been applied. */
  18. getPatches(): JsonPatch[];
  19. /** Reset the compiler to initial state. */
  20. reset(initial?: Partial<T>): void;
  21. }
  22. /**
  23. * Create a streaming YAML compiler.
  24. *
  25. * Incrementally parses YAML text as it arrives and emits JSON Patch
  26. * operations by diffing each successful parse against the previous snapshot.
  27. *
  28. * Uses `yaml.parse()` with YAML 1.2 defaults (the `yaml` v2 default).
  29. * YAML 1.2 does not coerce `yes`/`no`/`on`/`off` to booleans.
  30. *
  31. * @example
  32. * ```ts
  33. * const compiler = createYamlStreamCompiler<Spec>();
  34. * compiler.push("root: main\n");
  35. * compiler.push("elements:\n main:\n type: Card\n");
  36. * const { result } = compiler.flush();
  37. * ```
  38. */
  39. export function createYamlStreamCompiler<
  40. T extends Record<string, unknown> = Record<string, unknown>,
  41. >(initial?: Partial<T>): YamlStreamCompiler<T> {
  42. let accumulated = "";
  43. let snapshot: Record<string, unknown> = initial
  44. ? { ...initial }
  45. : ({} as Record<string, unknown>);
  46. let result: T = { ...snapshot } as T;
  47. const allPatches: JsonPatch[] = [];
  48. function tryParse(): { result: T; newPatches: JsonPatch[] } {
  49. const newPatches: JsonPatch[] = [];
  50. try {
  51. const parsed = parse(accumulated);
  52. if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
  53. const patches = diffToPatches(
  54. snapshot,
  55. parsed as Record<string, unknown>,
  56. );
  57. if (patches.length > 0) {
  58. snapshot = structuredClone(parsed as Record<string, unknown>);
  59. result = { ...snapshot } as T;
  60. allPatches.push(...patches);
  61. newPatches.push(...patches);
  62. }
  63. }
  64. } catch {
  65. // Incomplete YAML — wait for more data
  66. }
  67. return { result, newPatches };
  68. }
  69. return {
  70. push(chunk: string): { result: T; newPatches: JsonPatch[] } {
  71. accumulated += chunk;
  72. // Only attempt parse when we have a complete line
  73. if (chunk.includes("\n")) {
  74. return tryParse();
  75. }
  76. return { result, newPatches: [] };
  77. },
  78. flush(): { result: T; newPatches: JsonPatch[] } {
  79. return tryParse();
  80. },
  81. getResult(): T {
  82. return result;
  83. },
  84. getPatches(): JsonPatch[] {
  85. return [...allPatches];
  86. },
  87. reset(newInitial?: Partial<T>): void {
  88. accumulated = "";
  89. snapshot = newInitial
  90. ? { ...newInitial }
  91. : ({} as Record<string, unknown>);
  92. result = { ...snapshot } as T;
  93. allPatches.length = 0;
  94. },
  95. };
  96. }