From 10cf4b44f8bd1a3c452bfc41273405c3fc57f8fa Mon Sep 17 00:00:00 2001
From: Bartosz Sypytkowski <b.sypytkowski@gmail.com>
Date: Thu, 17 Aug 2023 20:00:48 +0200
Subject: [PATCH] fixed issues with unbounded XmlText.toString

---
 src/structs/Item.js         |  2 +-
 src/types/YText.js          | 23 +++++++++++++----------
 tests/y-weak-links.tests.js | 25 ++++++++++++++++++++++++-
 3 files changed, 38 insertions(+), 12 deletions(-)

diff --git a/src/structs/Item.js b/src/structs/Item.js
index ed657d3d..8cad00cf 100644
--- a/src/structs/Item.js
+++ b/src/structs/Item.js
@@ -314,7 +314,7 @@ export class Item extends AbstractStruct {
      * bit2: countable
      * bit3: deleted
      * bit4: mark - mark node as fast-search-marker
-     * bit5: linked - this item is linked by Weak Link references
+     * bit9: linked - this item is linked by Weak Link references
      * @type {number} byte
      */
     this.info = this.content.isCountable() ? binary.BIT2 : 0
diff --git a/src/types/YText.js b/src/types/YText.js
index 40c861cc..00a43ffb 100644
--- a/src/types/YText.js
+++ b/src/types/YText.js
@@ -1260,13 +1260,13 @@ export const rangeDelta = (parent, start, end, snapshot, prevSnapshot, computeYC
       }
     }
     const computeDelta = () => {
-      // scope represents offset at current block from which we're intersted in picking string
+      // startOffset represents offset at current block from which we're intersted in picking string
       // if it's -1 it means, we're out of scope and we should break at this point
-      let scope = start === null ? 0 : -1 
+      let startOffset = start === null ? 0 : -1 
       loop: while (n !== null) {
-        if (scope < 0 && start !== null) {
+        if (startOffset < 0 && start !== null) {
           if (start.client === n.id.client && start.clock >= n.id.clock && start.clock < n.id.clock + n.length) {
-            scope = n.id.clock + n.length - start.clock - 1
+            startOffset = start.clock - n.id.clock
           }
         }
         if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
@@ -1288,16 +1288,16 @@ export const rangeDelta = (parent, start, end, snapshot, prevSnapshot, computeYC
                 currentAttributes.delete('ychange')
               }
               let s = /** @type {ContentString} */ (n.content).str
-              if (scope > 0) {
-                 str += s.slice(scope)
-                 scope = 0
+              if (startOffset > 0) {
+                 str += s.slice(startOffset)
+                 startOffset = 0
               } else if (end !== null && end.client === n.id.client && end.clock >= n.id.clock && end.clock < n.id.clock + n.length) {
                 // we reached the end or range
-                const offset = n.id.clock + n.length - end.clock - 1
-                str += s.slice(0, s.length + offset) // scope is negative
+                const endOffset = n.id.clock + n.length - end.clock - 1
+                str += s.slice(0, s.length + endOffset) // scope is negative
                 packStr()
                 break loop
-              } else if (scope == 0) {
+              } else if (startOffset == 0) {
                 str += s
               }
               break
@@ -1328,6 +1328,9 @@ export const rangeDelta = (parent, start, end, snapshot, prevSnapshot, computeYC
               }
               break
           }
+        } else if (end !== null && end.client === n.id.client && end.clock >= n.id.clock && end.clock < n.id.clock + n.length) {
+          // block may not passed visibility check, but we still need to verify boundaries
+          break;
         }
         n = n.right
       }
diff --git a/tests/y-weak-links.tests.js b/tests/y-weak-links.tests.js
index b86369d4..2f0750c1 100644
--- a/tests/y-weak-links.tests.js
+++ b/tests/y-weak-links.tests.js
@@ -682,7 +682,7 @@ export const testRemoteMapUpdate = tc => {
 /**
  * @param {t.TestCase} tc
  */
-const testTextBasic = tc => {
+export const testTextBasic = tc => {
   const { testConnector, text0, text1 } = init(tc, { users: 2 })
 
   text0.insert(0, 'abcd')             // 'abcd'
@@ -701,6 +701,29 @@ const testTextBasic = tc => {
   t.compare(insert.toString(), 'be')
 }
 
+/**
+ * @param {t.TestCase} tc
+ */
+export const testXmlTextBasic = tc => {
+  const { testConnector, xml0, xml1 } = init(tc, { users: 2 })
+  const text0 = new Y.XmlText()
+  xml0.insert(0, [text0])
+
+  text0.insert(0, 'abcd')             // 'abcd'
+  const link0 = text0.quote(1, 2)     // quote: [bc]
+  t.compare(link0.toString(), 'bc')
+  text0.insert(2, 'ef')               // 'abefcd', quote: [befc]
+  t.compare(link0.toString(), 'befc') 
+  text0.delete(3, 3)                  // 'abe', quote: [be]
+  t.compare(link0.toString(), 'be')
+  text0.insertEmbed(3, link0)         // 'abe[be]'
+  
+  testConnector.flushAllMessages()
+  const text1 = /** @type {Y.XmlText} */ (xml1.get(0))
+  const delta = text1.toDelta()
+  const { insert } = delta[1] // YWeakLink
+  t.compare(insert.toString(), 'be')
+}
 /**
  * @param {t.TestCase} tc
  */