0
# Text Operations
1
2
## Overview
3
4
The `Text` type in pycrdt provides collaborative text editing capabilities similar to Python strings, but with automatic conflict resolution across multiple clients. It supports rich text formatting with attributes, embedded objects, and comprehensive change tracking through delta operations.
5
6
## Core Types
7
8
### Text
9
10
Collaborative text editing with string-like interface and rich formatting support.
11
12
```python { .api }
13
class Text:
14
def __init__(
15
self,
16
init: str | None = None,
17
*,
18
_doc: Doc | None = None,
19
_integrated: _Text | None = None,
20
) -> None:
21
"""
22
Create a new collaborative text object.
23
24
Args:
25
init (str, optional): Initial text content
26
_doc (Doc, optional): Parent document
27
_integrated (_Text, optional): Native text instance
28
"""
29
30
# String-like interface
31
def __len__(self) -> int:
32
"""Get the length of the text."""
33
34
def __str__(self) -> str:
35
"""Get the text content as a string."""
36
37
def __iter__(self) -> Iterator[str]:
38
"""Iterate over characters in the text."""
39
40
def __contains__(self, item: str) -> bool:
41
"""Check if substring exists in text."""
42
43
def __getitem__(self, key: int | slice) -> str:
44
"""Get character or substring by index/slice."""
45
46
def __setitem__(self, key: int | slice, value: str) -> None:
47
"""Set character or substring by index/slice."""
48
49
def __delitem__(self, key: int | slice) -> None:
50
"""Delete character or substring by index/slice."""
51
52
def __iadd__(self, value: str) -> Text:
53
"""Append text using += operator."""
54
55
# Text manipulation methods
56
def insert(self, index: int, value: str, attrs: dict[str, Any] | None = None) -> None:
57
"""
58
Insert text at the specified index.
59
60
Args:
61
index (int): Position to insert text
62
value (str): Text to insert
63
attrs (dict, optional): Formatting attributes for the inserted text
64
"""
65
66
def insert_embed(self, index: int, value: Any, attrs: dict[str, Any] | None = None) -> None:
67
"""
68
Insert an embedded object at the specified index.
69
70
Args:
71
index (int): Position to insert object
72
value: Object to embed
73
attrs (dict, optional): Formatting attributes for the embedded object
74
"""
75
76
def format(self, start: int, stop: int, attrs: dict[str, Any]) -> None:
77
"""
78
Apply formatting attributes to a text range.
79
80
Args:
81
start (int): Start index of the range
82
stop (int): End index of the range
83
attrs (dict): Formatting attributes to apply
84
"""
85
86
def diff(self) -> list[tuple[Any, dict[str, Any] | None]]:
87
"""
88
Get the formatted text as a list of (content, attributes) tuples.
89
90
Returns:
91
list: List of (content, attributes) pairs representing formatted text
92
"""
93
94
def clear(self) -> None:
95
"""Remove all text content."""
96
97
def to_py(self) -> str | None:
98
"""
99
Convert text to a Python string.
100
101
Returns:
102
str | None: Text content as string, or None if empty
103
"""
104
105
def observe(self, callback: Callable[[TextEvent], None]) -> Subscription:
106
"""
107
Observe text changes.
108
109
Args:
110
callback: Function called when text changes occur
111
112
Returns:
113
Subscription: Handle for unsubscribing
114
"""
115
116
def observe_deep(self, callback: Callable[[list[TextEvent]], None]) -> Subscription:
117
"""
118
Observe deep changes including nested structures.
119
120
Args:
121
callback: Function called with list of change events
122
123
Returns:
124
Subscription: Handle for unsubscribing
125
"""
126
127
def unobserve(self, subscription: Subscription) -> None:
128
"""
129
Remove an event observer.
130
131
Args:
132
subscription: Subscription handle to remove
133
"""
134
135
async def events(
136
self,
137
deep: bool = False,
138
max_buffer_size: float = float("inf")
139
) -> MemoryObjectReceiveStream:
140
"""
141
Get an async stream of text events.
142
143
Args:
144
deep (bool): Include deep change events
145
max_buffer_size (float): Maximum event buffer size
146
147
Returns:
148
MemoryObjectReceiveStream: Async event stream
149
"""
150
151
def sticky_index(self, index: int, assoc: Assoc = Assoc.AFTER) -> StickyIndex:
152
"""
153
Create a sticky index that maintains its position during edits.
154
155
Args:
156
index (int): Initial index position
157
assoc (Assoc): Association type (BEFORE or AFTER)
158
159
Returns:
160
StickyIndex: Persistent position tracker
161
"""
162
```
163
164
### TextEvent
165
166
Event emitted when text changes occur.
167
168
```python { .api }
169
class TextEvent:
170
@property
171
def target(self) -> Text:
172
"""Get the text object that changed."""
173
174
@property
175
def delta(self) -> list[dict[str, Any]]:
176
"""
177
Get the delta describing the changes.
178
179
Delta format:
180
- {"retain": n} - Keep n characters unchanged
181
- {"insert": "text", "attributes": {...}} - Insert text with attributes
182
- {"delete": n} - Delete n characters
183
"""
184
185
@property
186
def path(self) -> list[int | str]:
187
"""Get the path to the changed text within the document structure."""
188
```
189
190
## Usage Examples
191
192
### Basic Text Operations
193
194
```python
195
from pycrdt import Doc, Text
196
197
doc = Doc()
198
text = doc.get("content", type=Text)
199
200
# Basic string operations
201
text.insert(0, "Hello, world!")
202
print(str(text)) # "Hello, world!"
203
print(len(text)) # 13
204
205
# String-like access
206
print(text[0]) # "H"
207
print(text[0:5]) # "Hello"
208
print("world" in text) # True
209
210
# Modification
211
text[7:12] = "Python"
212
print(str(text)) # "Hello, Python!"
213
214
# Append text
215
text += " How are you?"
216
print(str(text)) # "Hello, Python! How are you?"
217
```
218
219
### Rich Text Formatting
220
221
```python
222
from pycrdt import Doc, Text
223
224
doc = Doc()
225
text = doc.get("document", type=Text)
226
227
# Insert text with formatting
228
text.insert(0, "Bold Text", {"bold": True})
229
text.insert(9, " and ", None)
230
text.insert(14, "Italic Text", {"italic": True})
231
232
# Apply formatting to existing text
233
text.format(0, 4, {"color": "red"}) # Make "Bold" red
234
text.format(19, 25, {"underline": True}) # Underline "Italic"
235
236
# Get formatted content
237
diff = text.diff()
238
for content, attrs in diff:
239
print(f"Content: {content}, Attributes: {attrs}")
240
```
241
242
### Embedded Objects
243
244
```python
245
from pycrdt import Doc, Text
246
247
doc = Doc()
248
text = doc.get("rich_content", type=Text)
249
250
# Insert text and embedded objects
251
text.insert(0, "Check out this image: ")
252
text.insert_embed(22, {"type": "image", "src": "photo.jpg"}, {"width": 300})
253
text.insert(23, " and this link: ")
254
text.insert_embed(39, {"type": "link", "url": "https://example.com"}, {"color": "blue"})
255
256
# Process mixed content
257
diff = text.diff()
258
for content, attrs in diff:
259
if isinstance(content, dict):
260
print(f"Embedded object: {content}")
261
else:
262
print(f"Text: {content}")
263
```
264
265
### Position Tracking
266
267
```python
268
from pycrdt import Doc, Text, Assoc
269
270
doc = Doc()
271
text = doc.get("content", type=Text)
272
273
text.insert(0, "Hello, world!")
274
275
# Create sticky indices
276
start_pos = text.sticky_index(7, Assoc.BEFORE) # Before "world"
277
end_pos = text.sticky_index(12, Assoc.AFTER) # After "world"
278
279
# Insert text before the tracked region
280
text.insert(0, "Well, ")
281
print(f"Start position: {start_pos.get_index()}") # Adjusted position
282
print(f"End position: {end_pos.get_index()}") # Adjusted position
283
284
# Extract text using sticky positions
285
with doc.transaction() as txn:
286
start_idx = start_pos.get_index(txn)
287
end_idx = end_pos.get_index(txn)
288
tracked_text = text[start_idx:end_idx]
289
print(f"Tracked text: {tracked_text}") # "world"
290
```
291
292
### Event Observation
293
294
```python
295
from pycrdt import Doc, Text, TextEvent
296
297
doc = Doc()
298
text = doc.get("content", type=Text)
299
300
def on_text_change(event: TextEvent):
301
print(f"Text changed in: {event.target}")
302
print(f"Delta: {event.delta}")
303
for op in event.delta:
304
if "retain" in op:
305
print(f" Retain {op['retain']} characters")
306
elif "insert" in op:
307
attrs = op.get("attributes", {})
308
print(f" Insert '{op['insert']}' with {attrs}")
309
elif "delete" in op:
310
print(f" Delete {op['delete']} characters")
311
312
# Subscribe to changes
313
subscription = text.observe(on_text_change)
314
315
# Make changes to trigger events
316
text.insert(0, "Hello")
317
text.insert(5, ", world!")
318
text.format(0, 5, {"bold": True})
319
320
# Clean up
321
text.unobserve(subscription)
322
```
323
324
### Async Event Streaming
325
326
```python
327
import anyio
328
from pycrdt import Doc, Text
329
330
async def monitor_text_changes(text: Text):
331
async with text.events() as event_stream:
332
async for event in event_stream:
333
print(f"Text event: {event.delta}")
334
335
doc = Doc()
336
text = doc.get("content", type=Text)
337
338
# Start monitoring in background
339
async def main():
340
async with anyio.create_task_group() as tg:
341
tg.start_soon(monitor_text_changes, text)
342
343
# Make changes
344
await anyio.sleep(0.1)
345
text.insert(0, "Hello")
346
await anyio.sleep(0.1)
347
text += ", World!"
348
349
anyio.run(main)
350
```
351
352
### Collaborative Editing Simulation
353
354
```python
355
from pycrdt import Doc, Text
356
357
# Simulate two clients editing the same document
358
doc1 = Doc(client_id=1)
359
doc2 = Doc(client_id=2)
360
361
text1 = doc1.get("shared_text", type=Text)
362
text2 = doc2.get("shared_text", type=Text)
363
364
# Client 1 makes changes
365
with doc1.transaction(origin="client1") as txn:
366
text1.insert(0, "Hello from client 1")
367
368
# Get update and apply to client 2
369
update = doc1.get_update()
370
doc2.apply_update(update)
371
372
print(str(text2)) # "Hello from client 1"
373
374
# Client 2 makes concurrent changes
375
with doc2.transaction(origin="client2") as txn:
376
text2.insert(0, "Hi! ")
377
text2.insert(len(text2), " - and client 2")
378
379
# Sync back to client 1
380
update = doc2.get_update(doc1.get_state())
381
doc1.apply_update(update)
382
383
print(str(text1)) # "Hi! Hello from client 1 - and client 2"
384
```
385
386
### Complex Text Processing
387
388
```python
389
from pycrdt import Doc, Text
390
391
doc = Doc()
392
text = doc.get("document", type=Text)
393
394
# Build a document with mixed content
395
text.insert(0, "Document Title", {"heading": 1, "bold": True})
396
text.insert(14, "\n\n")
397
text.insert(16, "This is the first paragraph with ", {"paragraph": True})
398
text.insert(50, "bold text", {"bold": True})
399
text.insert(59, " and ")
400
text.insert(64, "italic text", {"italic": True})
401
text.insert(75, ".")
402
403
text.insert(76, "\n\n")
404
text.insert(78, "Second paragraph with a ")
405
text.insert_embed(102, {"type": "link", "url": "example.com", "text": "link"})
406
text.insert(103, " and an ")
407
text.insert_embed(111, {"type": "image", "src": "diagram.png", "alt": "Diagram"})
408
text.insert(112, ".")
409
410
# Process the document
411
def analyze_content(text: Text):
412
"""Analyze text content and structure."""
413
diff = text.diff()
414
415
text_parts = []
416
embeds = []
417
418
for content, attrs in diff:
419
if isinstance(content, dict):
420
embeds.append(content)
421
else:
422
if attrs and "heading" in attrs:
423
text_parts.append(("heading", content))
424
elif attrs and "paragraph" in attrs:
425
text_parts.append(("paragraph", content))
426
else:
427
text_parts.append(("text", content))
428
429
return text_parts, embeds
430
431
parts, objects = analyze_content(text)
432
print(f"Text parts: {len(parts)}")
433
print(f"Embedded objects: {len(objects)}")
434
```
435
436
## Delta Operations
437
438
Text changes are represented as delta operations that describe insertions, deletions, and retains:
439
440
```python
441
# Example delta operations
442
delta_examples = [
443
{"retain": 5}, # Keep 5 characters
444
{"insert": "Hello", "attributes": {"bold": True}}, # Insert formatted text
445
{"delete": 3}, # Delete 3 characters
446
{"insert": {"type": "image", "src": "photo.jpg"}}, # Insert embed
447
]
448
449
# Processing deltas
450
def apply_delta(text: Text, delta: list[dict]):
451
"""Apply a delta to text (conceptual example)."""
452
pos = 0
453
for op in delta:
454
if "retain" in op:
455
pos += op["retain"]
456
elif "insert" in op:
457
content = op["insert"]
458
attrs = op.get("attributes")
459
if isinstance(content, str):
460
text.insert(pos, content, attrs)
461
pos += len(content)
462
else:
463
text.insert_embed(pos, content, attrs)
464
pos += 1
465
elif "delete" in op:
466
del text[pos:pos + op["delete"]]
467
```
468
469
## Error Handling
470
471
```python
472
from pycrdt import Doc, Text
473
474
doc = Doc()
475
text = doc.get("content", type=Text)
476
477
try:
478
# Invalid index operations
479
text.insert(-1, "Invalid") # May raise ValueError
480
481
# Invalid slice operations
482
text[100:200] = "Out of bounds" # May raise ValueError
483
484
# Invalid formatting
485
text.format(10, 5, {"invalid": "range"}) # start > stop
486
487
except (ValueError, IndexError) as e:
488
print(f"Text operation failed: {e}")
489
```