adds compatibility tests for Yjs
17 files changed,  +955, -1
M .gitignore
+1, -0
1@@ -1,3 +1,4 @@
2 examples/load_document
3 docs/
4 ref/
5+node_modules/
A compat_test.go
+615, -0
  1@@ -0,0 +1,615 @@
  2+package ygo_test
  3+
  4+import (
  5+	"encoding/json"
  6+	"os"
  7+	"testing"
  8+
  9+	"github.com/BTBurke/ygo"
 10+)
 11+
 12+func TestYjsCompatibility(t *testing.T) {
 13+	t.Run("shared YText", func(t *testing.T) {
 14+		data, err := os.ReadFile("testdata/shared-YText.yjs")
 15+		if err != nil {
 16+			t.Fatalf("failed to read testdata: %v", err)
 17+		}
 18+
 19+		doc, err := ygo.NewDoc()
 20+		if err != nil {
 21+			t.Fatalf("failed to create doc: %v", err)
 22+		}
 23+		defer doc.Destroy()
 24+
 25+		err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
 26+			update := ygo.UpdateFromBytes(data)
 27+			return txn.ApplyUpdate(update)
 28+		})
 29+		if err != nil {
 30+			t.Fatalf("failed to apply update: %v", err)
 31+		}
 32+
 33+		txt, err := doc.GetText("test")
 34+		if err != nil {
 35+			t.Fatalf("failed to get text: %v", err)
 36+		}
 37+		defer txt.Destroy()
 38+
 39+		var content string
 40+		err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
 41+			var err error
 42+			content, err = txt.String(txn)
 43+			return err
 44+		})
 45+		if err != nil {
 46+			t.Fatalf("failed to read text: %v", err)
 47+		}
 48+
 49+		if content != "test text" {
 50+			t.Errorf("expected 'test text', got %q", content)
 51+		}
 52+	})
 53+
 54+	t.Run("map value types", func(t *testing.T) {
 55+		data, err := os.ReadFile("testdata/map-value-types.yjs")
 56+		if err != nil {
 57+			t.Fatalf("failed to read testdata: %v", err)
 58+		}
 59+
 60+		doc, err := ygo.NewDoc()
 61+		if err != nil {
 62+			t.Fatalf("failed to create doc: %v", err)
 63+		}
 64+		defer doc.Destroy()
 65+
 66+		err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
 67+			update := ygo.UpdateFromBytes(data)
 68+			return txn.ApplyUpdate(update)
 69+		})
 70+		if err != nil {
 71+			t.Fatalf("failed to apply update: %v", err)
 72+		}
 73+
 74+		m, err := doc.GetMap("test")
 75+		if err != nil {
 76+			t.Fatalf("failed to get map: %v", err)
 77+		}
 78+		defer m.Destroy()
 79+
 80+		err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
 81+			out, err := m.Get(txn, "string")
 82+			if err != nil {
 83+				t.Errorf("failed to get 'string': %v", err)
 84+			} else if v, ok := out.String(); !ok || v != "value" {
 85+				t.Errorf("expected string='value', got %v", v)
 86+			}
 87+			out.Destroy()
 88+
 89+			out, err = m.Get(txn, "bool")
 90+			if err != nil {
 91+				t.Errorf("failed to get 'bool': %v", err)
 92+			} else if v, ok := out.Bool(); !ok || v != true {
 93+				t.Errorf("expected bool=true, got %v", v)
 94+			}
 95+			out.Destroy()
 96+
 97+			out, err = m.Get(txn, "number")
 98+			if err != nil {
 99+				t.Errorf("failed to get 'number': %v", err)
100+			} else if v, ok := out.Float(); !ok || v != 1 {
101+				t.Errorf("expected number=1, got %v", v)
102+			}
103+			out.Destroy()
104+
105+			out, err = m.Get(txn, "object")
106+			if err != nil {
107+				t.Errorf("failed to get 'object': %v", err)
108+			} else {
109+				var obj map[string]interface{}
110+				if err := out.JSON(&obj); err != nil {
111+					t.Errorf("failed to parse JSON: %v", err)
112+				} else {
113+					if obj["foo"] != "bar" {
114+						t.Errorf("expected object.foo='bar', got %v", obj["foo"])
115+					}
116+					if obj["num"] != float64(1) {
117+						t.Errorf("expected object.num=1, got %v", obj["num"])
118+					}
119+					if obj["test"] != true {
120+						t.Errorf("expected object.test=true, got %v", obj["test"])
121+					}
122+				}
123+			}
124+			out.Destroy()
125+
126+			return nil
127+		})
128+		if err != nil {
129+			t.Fatalf("map verification failed: %v", err)
130+		}
131+	})
132+
133+	t.Run("shared YArray", func(t *testing.T) {
134+		data, err := os.ReadFile("testdata/shared-YArray.yjs")
135+		if err != nil {
136+			t.Fatalf("failed to read testdata: %v", err)
137+		}
138+
139+		doc, err := ygo.NewDoc()
140+		if err != nil {
141+			t.Fatalf("failed to create doc: %v", err)
142+		}
143+		defer doc.Destroy()
144+
145+		err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
146+			update := ygo.UpdateFromBytes(data)
147+			return txn.ApplyUpdate(update)
148+		})
149+		if err != nil {
150+			t.Fatalf("failed to apply update: %v", err)
151+		}
152+
153+		arr, err := doc.GetArray("test")
154+		if err != nil {
155+			t.Fatalf("failed to get array: %v", err)
156+		}
157+		defer arr.Destroy()
158+
159+		var length uint32
160+		err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
161+			var err error
162+			length, err = arr.Len()
163+			return err
164+		})
165+		if err != nil {
166+			t.Fatalf("failed to get array length: %v", err)
167+		}
168+
169+		if length != 4 {
170+			t.Errorf("expected array length 4, got %d", length)
171+		}
172+
173+		err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
174+			out, err := arr.Get(txn, 0)
175+			if err != nil {
176+				t.Errorf("failed to get element 0: %v", err)
177+			} else if v, ok := out.String(); !ok || v != "first" {
178+				t.Errorf("expected element 0='first', got %v", v)
179+			}
180+			out.Destroy()
181+
182+			out, err = arr.Get(txn, 1)
183+			if err != nil {
184+				t.Errorf("failed to get element 1: %v", err)
185+			} else if v, ok := out.String(); !ok || v != "middle" {
186+				t.Errorf("expected element 1='middle', got %v", v)
187+			}
188+			out.Destroy()
189+
190+			out, err = arr.Get(txn, 2)
191+			if err != nil {
192+				t.Errorf("failed to get element 2: %v", err)
193+			} else if v, ok := out.String(); !ok || v != "second" {
194+				t.Errorf("expected element 2='second', got %v", v)
195+			}
196+			out.Destroy()
197+
198+			out, err = arr.Get(txn, 3)
199+			if err != nil {
200+				t.Errorf("failed to get element 3: %v", err)
201+			} else if v, ok := out.String(); !ok || v != "third" {
202+				t.Errorf("expected element 3='third', got %v", v)
203+			}
204+			out.Destroy()
205+
206+			return nil
207+		})
208+		if err != nil {
209+			t.Fatalf("array element verification failed: %v", err)
210+		}
211+	})
212+
213+	t.Run("nested XmlElement with attrs", func(t *testing.T) {
214+		data, err := os.ReadFile("testdata/nested-XmlElement-with-attrs.yjs")
215+		if err != nil {
216+			t.Fatalf("failed to read testdata: %v", err)
217+		}
218+
219+		doc, err := ygo.NewDoc()
220+		if err != nil {
221+			t.Fatalf("failed to create doc: %v", err)
222+		}
223+		defer doc.Destroy()
224+
225+		err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
226+			update := ygo.UpdateFromBytes(data)
227+			return txn.ApplyUpdate(update)
228+		})
229+		if err != nil {
230+			t.Fatalf("failed to apply update: %v", err)
231+		}
232+
233+		frag, err := doc.GetXmlFragment("test")
234+		if err != nil {
235+			t.Fatalf("failed to get xml fragment: %v", err)
236+		}
237+		defer frag.Destroy()
238+
239+		err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
240+			jsonBytes, err := frag.MarshalJSONWithTxn(txn)
241+			if err != nil {
242+				return err
243+			}
244+
245+			var children []interface{}
246+			if err := json.Unmarshal(jsonBytes, &children); err != nil {
247+				return err
248+			}
249+
250+			if len(children) < 1 {
251+				t.Error("expected at least one child")
252+				return nil
253+			}
254+
255+			div, ok := children[0].(map[string]interface{})
256+			if !ok {
257+				t.Error("expected first child to be a map")
258+				return nil
259+			}
260+
261+			if div["type"] != "div" {
262+				t.Errorf("expected type='div', got %v", div["type"])
263+			}
264+
265+			attrs, ok := div["attrs"].(map[string]interface{})
266+			if !ok {
267+				t.Error("expected attrs to be a map")
268+				return nil
269+			}
270+			if attrs["class"] != "container" {
271+				t.Errorf("expected class='container', got %v", attrs["class"])
272+			}
273+			if attrs["data-id"] != "main" {
274+				t.Errorf("expected data-id='main', got %v", attrs["data-id"])
275+			}
276+
277+			childList, ok := div["children"].([]interface{})
278+			if !ok {
279+				t.Error("expected children to be a list")
280+				return nil
281+			}
282+			if len(childList) != 2 {
283+				t.Errorf("expected 2 children, got %d", len(childList))
284+			}
285+
286+			return nil
287+		})
288+		if err != nil {
289+			t.Fatalf("xml verification failed: %v", err)
290+		}
291+	})
292+
293+	t.Run("mixed content array", func(t *testing.T) {
294+		data, err := os.ReadFile("testdata/mixed-content-array.yjs")
295+		if err != nil {
296+			t.Fatalf("failed to read testdata: %v", err)
297+		}
298+
299+		doc, err := ygo.NewDoc()
300+		if err != nil {
301+			t.Fatalf("failed to create doc: %v", err)
302+		}
303+		defer doc.Destroy()
304+
305+		err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
306+			update := ygo.UpdateFromBytes(data)
307+			return txn.ApplyUpdate(update)
308+		})
309+		if err != nil {
310+			t.Fatalf("failed to apply update: %v", err)
311+		}
312+
313+		arr, err := doc.GetArray("test")
314+		if err != nil {
315+			t.Fatalf("failed to get array: %v", err)
316+		}
317+		defer arr.Destroy()
318+
319+		var length uint32
320+		err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
321+			var err error
322+			length, err = arr.Len()
323+			return err
324+		})
325+		if err != nil {
326+			t.Fatalf("failed to get array length: %v", err)
327+		}
328+
329+		if length != 5 {
330+			t.Errorf("expected array length 5, got %d", length)
331+		}
332+
333+		err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
334+			out, err := arr.Get(txn, 0)
335+			if err != nil {
336+				t.Errorf("failed to get element 0: %v", err)
337+			} else if v, ok := out.String(); !ok || v != "string" {
338+				t.Errorf("expected element 0='string', got %v", v)
339+			}
340+			out.Destroy()
341+
342+			out, err = arr.Get(txn, 1)
343+			if err != nil {
344+				t.Errorf("failed to get element 1: %v", err)
345+			} else if v, ok := out.Float(); !ok || v != 42 {
346+				t.Errorf("expected element 1=42, got %v", v)
347+			}
348+			out.Destroy()
349+
350+			out, err = arr.Get(txn, 2)
351+			if err != nil {
352+				t.Errorf("failed to get element 2: %v", err)
353+			} else if v, ok := out.Bool(); !ok || v != true {
354+				t.Errorf("expected element 2=true, got %v", v)
355+			}
356+			out.Destroy()
357+
358+			out, err = arr.Get(txn, 3)
359+			if err != nil {
360+				t.Errorf("failed to get element 3: %v", err)
361+			} else if !out.IsNull() {
362+				t.Errorf("expected element 3=null")
363+			}
364+			out.Destroy()
365+
366+			out, err = arr.Get(txn, 4)
367+			if err != nil {
368+				t.Errorf("failed to get element 4: %v", err)
369+			} else {
370+				var obj map[string]interface{}
371+				if err := out.JSON(&obj); err != nil {
372+					t.Errorf("failed to parse JSON: %v", err)
373+				} else {
374+					if obj["nested"] != "object" {
375+						t.Errorf("expected element 4.nested='object', got %v", obj["nested"])
376+					}
377+				}
378+			}
379+			out.Destroy()
380+
381+			return nil
382+		})
383+		if err != nil {
384+			t.Fatalf("array element verification failed: %v", err)
385+		}
386+	})
387+
388+	t.Run("nested YMap in YMap", func(t *testing.T) {
389+		data, err := os.ReadFile("testdata/nested-YMap-in-YMap.yjs")
390+		if err != nil {
391+			t.Fatalf("failed to read testdata: %v", err)
392+		}
393+
394+		doc, err := ygo.NewDoc()
395+		if err != nil {
396+			t.Fatalf("failed to create doc: %v", err)
397+		}
398+		defer doc.Destroy()
399+
400+		err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
401+			update := ygo.UpdateFromBytes(data)
402+			return txn.ApplyUpdate(update)
403+		})
404+		if err != nil {
405+			t.Fatalf("failed to apply update: %v", err)
406+		}
407+
408+		m, err := doc.GetMap("test")
409+		if err != nil {
410+			t.Fatalf("failed to get map: %v", err)
411+		}
412+		defer m.Destroy()
413+
414+		err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
415+			out, err := m.Get(txn, "child")
416+			if err != nil {
417+				t.Fatalf("failed to get 'child': %v", err)
418+			}
419+			defer out.Destroy()
420+
421+			if out.Tag() != ygo.TagMap {
422+				t.Fatalf("expected TagMap, got %v", out.Tag())
423+			}
424+
425+			childMap, err := m.GetNestedMap(txn, "child")
426+			if err != nil {
427+				t.Fatalf("failed to get nested map: %v", err)
428+			}
429+			defer childMap.Destroy()
430+
431+			childOut, err := childMap.Get(txn, "key1")
432+			if err != nil {
433+				t.Fatalf("failed to get nested key1: %v", err)
434+			}
435+			defer childOut.Destroy()
436+
437+			val, ok := childOut.String()
438+			if !ok || val != "value1" {
439+				t.Errorf("expected 'value1', got %v", val)
440+			}
441+
442+			childOut, err = childMap.Get(txn, "key2")
443+			if err != nil {
444+				t.Fatalf("failed to get nested key2: %v", err)
445+			}
446+			defer childOut.Destroy()
447+
448+			num, ok := childOut.Float()
449+			if !ok || num != 42 {
450+				t.Errorf("expected 42, got %v", num)
451+			}
452+
453+			return nil
454+		})
455+		if err != nil {
456+			t.Fatalf("nested map test failed: %v", err)
457+		}
458+	})
459+
460+	t.Run("nested YArray in YMap", func(t *testing.T) {
461+		data, err := os.ReadFile("testdata/nested-YArray-in-YMap.yjs")
462+		if err != nil {
463+			t.Fatalf("failed to read testdata: %v", err)
464+		}
465+
466+		doc, err := ygo.NewDoc()
467+		if err != nil {
468+			t.Fatalf("failed to create doc: %v", err)
469+		}
470+		defer doc.Destroy()
471+
472+		err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
473+			update := ygo.UpdateFromBytes(data)
474+			return txn.ApplyUpdate(update)
475+		})
476+		if err != nil {
477+			t.Fatalf("failed to apply update: %v", err)
478+		}
479+
480+		m, err := doc.GetMap("test")
481+		if err != nil {
482+			t.Fatalf("failed to get map: %v", err)
483+		}
484+		defer m.Destroy()
485+
486+		err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
487+			arr, err := m.GetNestedArray(txn, "arr")
488+			if err != nil {
489+				t.Fatalf("failed to get nested array: %v", err)
490+			}
491+			defer arr.Destroy()
492+
493+			length, err := arr.Len()
494+			if err != nil {
495+				t.Fatalf("failed to get array length: %v", err)
496+			}
497+			if length != 3 {
498+				t.Errorf("expected length 3, got %d", length)
499+			}
500+
501+			out, err := arr.Get(txn, 0)
502+			if err != nil {
503+				t.Fatalf("failed to get element 0: %v", err)
504+			}
505+			defer out.Destroy()
506+
507+			val, ok := out.String()
508+			if !ok || val != "a" {
509+				t.Errorf("expected 'a', got %v", val)
510+			}
511+
512+			out, err = arr.Get(txn, 2)
513+			if err != nil {
514+				t.Fatalf("failed to get element 2: %v", err)
515+			}
516+			defer out.Destroy()
517+
518+			val, ok = out.String()
519+			if !ok || val != "c" {
520+				t.Errorf("expected 'c', got %v", val)
521+			}
522+
523+			return nil
524+		})
525+		if err != nil {
526+			t.Fatalf("nested array test failed: %v", err)
527+		}
528+	})
529+
530+	t.Run("nested YMap in YArray", func(t *testing.T) {
531+		data, err := os.ReadFile("testdata/nested-YMap-in-YArray.yjs")
532+		if err != nil {
533+			t.Fatalf("failed to read testdata: %v", err)
534+		}
535+
536+		doc, err := ygo.NewDoc()
537+		if err != nil {
538+			t.Fatalf("failed to create doc: %v", err)
539+		}
540+		defer doc.Destroy()
541+
542+		err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
543+			update := ygo.UpdateFromBytes(data)
544+			return txn.ApplyUpdate(update)
545+		})
546+		if err != nil {
547+			t.Fatalf("failed to apply update: %v", err)
548+		}
549+
550+		arr, err := doc.GetArray("test")
551+		if err != nil {
552+			t.Fatalf("failed to get array: %v", err)
553+		}
554+		defer arr.Destroy()
555+
556+		err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
557+			length, err := arr.Len()
558+			if err != nil {
559+				t.Fatalf("failed to get array length: %v", err)
560+			}
561+			if length != 2 {
562+				t.Errorf("expected length 2, got %d", length)
563+			}
564+
565+			itemOut, err := arr.Get(txn, 0)
566+			if err != nil {
567+				t.Fatalf("failed to get element 0: %v", err)
568+			}
569+			defer itemOut.Destroy()
570+
571+			childMap, err := itemOut.ToMap()
572+			if err != nil {
573+				t.Fatalf("failed to convert to map: %v", err)
574+			}
575+			defer childMap.Destroy()
576+
577+			nameOut, err := childMap.Get(txn, "name")
578+			if err != nil {
579+				t.Fatalf("failed to get nested 'name': %v", err)
580+			}
581+			defer nameOut.Destroy()
582+
583+			val, ok := nameOut.String()
584+			if !ok || val != "first" {
585+				t.Errorf("expected 'first', got %v", val)
586+			}
587+
588+			itemOut, err = arr.Get(txn, 1)
589+			if err != nil {
590+				t.Fatalf("failed to get element 1: %v", err)
591+			}
592+			defer itemOut.Destroy()
593+
594+			childMap, err = itemOut.ToMap()
595+			if err != nil {
596+				t.Fatalf("failed to convert to map: %v", err)
597+			}
598+
599+			nameOut, err = childMap.Get(txn, "name")
600+			if err != nil {
601+				t.Fatalf("failed to get nested 'name' from element 1: %v", err)
602+			}
603+			defer nameOut.Destroy()
604+
605+			val, ok = nameOut.String()
606+			if !ok || val != "second" {
607+				t.Errorf("expected 'second', got %v", val)
608+			}
609+
610+			return nil
611+		})
612+		if err != nil {
613+			t.Fatalf("nested map in array test failed: %v", err)
614+		}
615+	})
616+}
A compat_test.js
+96, -0
 1@@ -0,0 +1,96 @@
 2+import * as Y from "yjs";
 3+import fs from "node:fs";
 4+
 5+const writeTestData = (fname, f) => {
 6+  const name = "testdata/" + fname.replace(/\s/g, "-") + ".yjs";
 7+  console.log(`writing test ${name}`);
 8+
 9+  const doc = f();
10+  const content = Y.encodeStateAsUpdate(doc);
11+  doc.destroy();
12+
13+  fs.writeFileSync(name, content);
14+};
15+
16+writeTestData("shared YText", () => {
17+  const doc = new Y.Doc();
18+  const yt = doc.getText("test");
19+  yt.insert(0, "test text");
20+  return doc;
21+});
22+
23+writeTestData("map value types", () => {
24+  const doc = new Y.Doc();
25+  const ym = doc.getMap("test");
26+  ym.set("string", "value");
27+  ym.set("bool", true);
28+  ym.set("number", 1);
29+  ym.set("object", { foo: "bar", num: 1, test: true });
30+  return doc;
31+});
32+
33+writeTestData("shared YArray", () => {
34+  const doc = new Y.Doc();
35+  const ya = doc.getArray("test");
36+  ya.push(["first", "second", "third"]);
37+  ya.insert(1, ["middle"]);
38+  return doc;
39+});
40+
41+writeTestData("nested XmlElement with attrs", () => {
42+  const doc = new Y.Doc();
43+  const frag = doc.getXmlFragment("test");
44+  
45+  const div = new Y.XmlElement("div");
46+  div.setAttribute("class", "container");
47+  div.setAttribute("data-id", "main");
48+  
49+  const p = new Y.XmlElement("p");
50+  p.setAttribute("style", "color:red");
51+  p.insert(0, [new Y.XmlText("Hello")]);
52+  
53+  div.insert(0, [p]);
54+  div.insert(1, [new Y.XmlText("World")]);
55+  
56+  frag.insert(0, [div]);
57+  return doc;
58+});
59+
60+writeTestData("mixed content array", () => {
61+  const doc = new Y.Doc();
62+  const ya = doc.getArray("test");
63+  ya.push(["string", 42, true, null, { nested: "object" }]);
64+  return doc;
65+});
66+
67+writeTestData("nested YMap in YMap", () => {
68+  const doc = new Y.Doc();
69+  const parent = doc.getMap("test");
70+  const child = new Y.Map();
71+  child.set("key1", "value1");
72+  child.set("key2", 42);
73+  parent.set("child", child);
74+  return doc;
75+});
76+
77+writeTestData("nested YArray in YMap", () => {
78+  const doc = new Y.Doc();
79+  const parent = doc.getMap("test");
80+  const child = new Y.Array();
81+  child.push(["a", "b", "c"]);
82+  parent.set("arr", child);
83+  return doc;
84+});
85+
86+writeTestData("nested YMap in YArray", () => {
87+  const doc = new Y.Doc();
88+  const arr = doc.getArray("test");
89+  const item1 = new Y.Map();
90+  item1.set("name", "first");
91+  const item2 = new Y.Map();
92+  item2.set("name", "second");
93+  arr.push([item1, item2]);
94+  return doc;
95+});
96+
97+console.log("All test files written successfully");
M devbox.json
+3, -1
 1@@ -9,7 +9,9 @@
 2     "ripgrep@latest",
 3     "yarn@latest",
 4     "rustup@latest",
 5-    "rustc@latest"
 6+    "rustc@latest",
 7+    "nodejs@latest",
 8+    "corepack@latest"
 9   ],
10   "shell": {
11     "init_hook": [
M devbox.lock
+97, -0
  1@@ -173,6 +173,54 @@
  2         }
  3       }
  4     },
  5+    "corepack@latest": {
  6+      "last_modified": "2026-03-21T07:29:51Z",
  7+      "resolved": "github:NixOS/nixpkgs/09061f748ee21f68a089cd5d91ec1859cd93d0be#corepack",
  8+      "source": "devbox-search",
  9+      "version": "0.34.6",
 10+      "systems": {
 11+        "aarch64-darwin": {
 12+          "outputs": [
 13+            {
 14+              "name": "out",
 15+              "path": "/nix/store/b81ff3525q050yr2n31raaij9l86a70z-corepack-0.34.6",
 16+              "default": true
 17+            }
 18+          ],
 19+          "store_path": "/nix/store/b81ff3525q050yr2n31raaij9l86a70z-corepack-0.34.6"
 20+        },
 21+        "aarch64-linux": {
 22+          "outputs": [
 23+            {
 24+              "name": "out",
 25+              "path": "/nix/store/krd99cx9wh0pflf43ghi8yy5bzaqrks6-corepack-0.34.6",
 26+              "default": true
 27+            }
 28+          ],
 29+          "store_path": "/nix/store/krd99cx9wh0pflf43ghi8yy5bzaqrks6-corepack-0.34.6"
 30+        },
 31+        "x86_64-darwin": {
 32+          "outputs": [
 33+            {
 34+              "name": "out",
 35+              "path": "/nix/store/p056js9jzlnq07gpl728fkvw036qdmlz-corepack-0.34.6",
 36+              "default": true
 37+            }
 38+          ],
 39+          "store_path": "/nix/store/p056js9jzlnq07gpl728fkvw036qdmlz-corepack-0.34.6"
 40+        },
 41+        "x86_64-linux": {
 42+          "outputs": [
 43+            {
 44+              "name": "out",
 45+              "path": "/nix/store/pvgjai36liywyb5gyxnxnx5s2fsf7cwz-corepack-0.34.6",
 46+              "default": true
 47+            }
 48+          ],
 49+          "store_path": "/nix/store/pvgjai36liywyb5gyxnxnx5s2fsf7cwz-corepack-0.34.6"
 50+        }
 51+      }
 52+    },
 53     "coreutils@latest": {
 54       "last_modified": "2026-03-21T07:29:51Z",
 55       "resolved": "github:NixOS/nixpkgs/09061f748ee21f68a089cd5d91ec1859cd93d0be#coreutils",
 56@@ -405,6 +453,55 @@
 57         }
 58       }
 59     },
 60+    "nodejs@latest": {
 61+      "last_modified": "2026-03-27T11:17:38Z",
 62+      "plugin_version": "0.0.2",
 63+      "resolved": "github:NixOS/nixpkgs/832efc09b4caf6b4569fbf9dc01bec3082a00611#nodejs_25",
 64+      "source": "devbox-search",
 65+      "version": "25.8.2",
 66+      "systems": {
 67+        "aarch64-darwin": {
 68+          "outputs": [
 69+            {
 70+              "name": "out",
 71+              "path": "/nix/store/qflz0c97921bn5i3jc49dy24hhwpk67d-nodejs-25.8.2",
 72+              "default": true
 73+            }
 74+          ],
 75+          "store_path": "/nix/store/qflz0c97921bn5i3jc49dy24hhwpk67d-nodejs-25.8.2"
 76+        },
 77+        "aarch64-linux": {
 78+          "outputs": [
 79+            {
 80+              "name": "out",
 81+              "path": "/nix/store/vdakqgvs2qjrdsrjz3wpwn39yvj0fmvv-nodejs-25.8.2",
 82+              "default": true
 83+            }
 84+          ],
 85+          "store_path": "/nix/store/vdakqgvs2qjrdsrjz3wpwn39yvj0fmvv-nodejs-25.8.2"
 86+        },
 87+        "x86_64-darwin": {
 88+          "outputs": [
 89+            {
 90+              "name": "out",
 91+              "path": "/nix/store/d94jkiwkdf3qm25pchchdx37sh4fy4g7-nodejs-25.8.2",
 92+              "default": true
 93+            }
 94+          ],
 95+          "store_path": "/nix/store/d94jkiwkdf3qm25pchchdx37sh4fy4g7-nodejs-25.8.2"
 96+        },
 97+        "x86_64-linux": {
 98+          "outputs": [
 99+            {
100+              "name": "out",
101+              "path": "/nix/store/qads515hyg9b6vs0l1cyxhggvc98ycdx-nodejs-25.8.2",
102+              "default": true
103+            }
104+          ],
105+          "store_path": "/nix/store/qads515hyg9b6vs0l1cyxhggvc98ycdx-nodejs-25.8.2"
106+        }
107+      }
108+    },
109     "ripgrep@latest": {
110       "last_modified": "2026-03-21T07:29:51Z",
111       "resolved": "github:NixOS/nixpkgs/09061f748ee21f68a089cd5d91ec1859cd93d0be#ripgrep",
M map.go
+42, -0
 1@@ -124,6 +124,48 @@ func (m *Map) Branch() unsafe.Pointer {
 2 	return unsafe.Pointer(m.branch)
 3 }
 4 
 5+// GetNestedMap retrieves a nested Y.Map value by key.
 6+// Returns error if the key doesn't exist or the value is not a Y.Map.
 7+func (m *Map) GetNestedMap(txn *Transaction, key string) (*Map, error) {
 8+	out, err := m.Get(txn, key)
 9+	if err != nil {
10+		return nil, err
11+	}
12+	defer out.Destroy()
13+
14+	if out.Tag() != TagMap {
15+		return nil, fmt.Errorf("value at key %q is not a Y.Map (got %v)", key, out.Tag())
16+	}
17+
18+	branch := C.youtput_read_ymap(out.ptr)
19+	if branch == nil {
20+		return nil, fmt.Errorf("failed to read nested ymap")
21+	}
22+
23+	return &Map{branch: branch}, nil
24+}
25+
26+// GetNestedArray retrieves a nested Y.Array value by key.
27+// Returns error if the key doesn't exist or the value is not a Y.Array.
28+func (m *Map) GetNestedArray(txn *Transaction, key string) (*Array, error) {
29+	out, err := m.Get(txn, key)
30+	if err != nil {
31+		return nil, err
32+	}
33+	defer out.Destroy()
34+
35+	if out.Tag() != TagArray {
36+		return nil, fmt.Errorf("value at key %q is not a Y.Array (got %v)", key, out.Tag())
37+	}
38+
39+	branch := C.youtput_read_yarray(out.ptr)
40+	if branch == nil {
41+		return nil, fmt.Errorf("failed to read nested yarray")
42+	}
43+
44+	return &Array{branch: branch}, nil
45+}
46+
47 // MapEntry represents a key-value pair from a map.
48 type MapEntry struct {
49 	Key   string
M output.go
+45, -0
 1@@ -60,6 +60,51 @@ func (o *Output) Destroy() {
 2 	}
 3 }
 4 
 5+// Branch returns the underlying branch pointer for shared types.
 6+// Only valid for outputs with TagArray, TagMap, TagText, TagXmlElem, TagXmlText, or TagXmlFrag.
 7+// Returns nil for non-shared types.
 8+func (o *Output) Branch() unsafe.Pointer {
 9+	if o.ptr == nil {
10+		return nil
11+	}
12+	switch o.Tag() {
13+	case TagArray:
14+		branch := C.youtput_read_yarray(o.ptr)
15+		return unsafe.Pointer(branch)
16+	case TagMap:
17+		branch := C.youtput_read_ymap(o.ptr)
18+		return unsafe.Pointer(branch)
19+	default:
20+		return nil
21+	}
22+}
23+
24+// ToMap converts the output to a Map.
25+// Returns error if the output is not a Y.Map.
26+func (o *Output) ToMap() (*Map, error) {
27+	if o.Tag() != TagMap {
28+		return nil, fmt.Errorf("output is not a Y.Map (got %v)", o.Tag())
29+	}
30+	branch := C.youtput_read_ymap(o.ptr)
31+	if branch == nil {
32+		return nil, fmt.Errorf("failed to read ymap from output")
33+	}
34+	return &Map{branch: branch}, nil
35+}
36+
37+// ToArray converts the output to an Array.
38+// Returns error if the output is not a Y.Array.
39+func (o *Output) ToArray() (*Array, error) {
40+	if o.Tag() != TagArray {
41+		return nil, fmt.Errorf("output is not a Y.Array (got %v)", o.Tag())
42+	}
43+	branch := C.youtput_read_yarray(o.ptr)
44+	if branch == nil {
45+		return nil, fmt.Errorf("failed to read yarray from output")
46+	}
47+	return &Array{branch: branch}, nil
48+}
49+
50 // IsNull returns true if the value is null.
51 func (o *Output) IsNull() bool {
52 	return o.Tag() == TagJSONNull
A package.json
+17, -0
 1@@ -0,0 +1,17 @@
 2+{
 3+    "name": "ygo",
 4+    "version": "1.0.0",
 5+    "description": "",
 6+    "main": "index.js",
 7+    "type": "module",
 8+    "scripts": {
 9+        "test": "echo \"Error: no test specified\" && exit 1"
10+    },
11+    "keywords": [],
12+    "author": "",
13+    "license": "ISC",
14+    "packageManager": "[email protected]+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
15+    "devDependencies": {
16+        "yjs": "^13.6.30"
17+    }
18+}
A pnpm-lock.yaml
+39, -0
 1@@ -0,0 +1,39 @@
 2+lockfileVersion: '9.0'
 3+
 4+settings:
 5+  autoInstallPeers: true
 6+  excludeLinksFromLockfile: false
 7+
 8+importers:
 9+
10+  .:
11+    devDependencies:
12+      yjs:
13+        specifier: ^13.6.30
14+        version: 13.6.30
15+
16+packages:
17+
18+  [email protected]:
19+    resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==}
20+
21+  [email protected]:
22+    resolution: {integrity: sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==}
23+    engines: {node: '>=16'}
24+    hasBin: true
25+
26+  [email protected]:
27+    resolution: {integrity: sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ==}
28+    engines: {node: '>=16.0.0', npm: '>=8.0.0'}
29+
30+snapshots:
31+
32+  [email protected]: {}
33+
34+  [email protected]:
35+    dependencies:
36+      isomorphic.js: 0.2.5
37+
38+  [email protected]:
39+    dependencies:
40+      lib0: 0.2.117
A testdata/map-value-types.yjs
+0, -0
A testdata/mixed-content-array.yjs
+0, -0
A testdata/nested-XmlElement-with-attrs.yjs
+0, -0
A testdata/nested-YArray-in-YMap.yjs
+0, -0
A testdata/nested-YMap-in-YArray.yjs
+0, -0
A testdata/nested-YMap-in-YMap.yjs
+0, -0
A testdata/shared-YArray.yjs
+0, -0
A testdata/shared-YText.yjs
+0, -0