jupyter-ydoc 3.3.2__tar.gz → 3.3.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/.github/workflows/auto_author_assign.yml +1 -1
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/.github/workflows/check-release.yml +1 -1
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/.pre-commit-config.yaml +1 -1
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/CHANGELOG.md +41 -2
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/PKG-INFO +1 -1
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/javascript/package.json +1 -1
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/jupyter_ydoc/_version.py +1 -1
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/jupyter_ydoc/ynotebook.py +37 -13
- jupyter_ydoc-3.3.4/jupyter_ydoc/yunicode.py +181 -0
- jupyter_ydoc-3.3.2/jupyter_ydoc/yunicode.py +0 -120
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/.github/dependabot.yml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/.github/workflows/enforce-label.yml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/.github/workflows/license-header.yml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/.github/workflows/prep-release.yml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/.github/workflows/publish-release.yml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/.github/workflows/test.yml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/.gitignore +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/.licenserc.yaml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/.yarn/releases/yarn-3.4.1.cjs +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/.yarnrc.yml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/LICENSE +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/README.md +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/docs/Makefile +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/docs/make.bat +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/docs/source/_static/jupyter_logo.svg +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/docs/source/_static/logo-icon.png +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/docs/source/conf.py +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/docs/source/custom.md +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/docs/source/index.md +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/docs/source/javascript_api.rst +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/docs/source/overview.md +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/docs/source/python_api.rst +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/docs/source/schema.md +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/jupyter_ydoc/__init__.py +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/jupyter_ydoc/py.typed +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/jupyter_ydoc/utils.py +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/jupyter_ydoc/ybasedoc.py +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/jupyter_ydoc/yblob.py +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/jupyter_ydoc/yfile.py +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/lerna.json +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/package.json +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/pyproject.toml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/pytest.ini +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/readthedocs.yml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.4}/yarn.lock +0 -0
|
@@ -37,7 +37,7 @@ jobs:
|
|
|
37
37
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
38
38
|
- name: Upload Distributions
|
|
39
39
|
if: ${{ matrix.group == 'check_release' }}
|
|
40
|
-
uses: actions/upload-artifact@
|
|
40
|
+
uses: actions/upload-artifact@v6
|
|
41
41
|
with:
|
|
42
42
|
name: jupyter-releaser-dist-${{ github.run_number }}
|
|
43
43
|
path: .jupyter_releaser_checkout/dist
|
|
@@ -2,6 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- <START NEW CHANGELOG ENTRY> -->
|
|
4
4
|
|
|
5
|
+
## 3.3.4
|
|
6
|
+
|
|
7
|
+
([Full Changelog](https://github.com/jupyter-server/jupyter_ydoc/compare/@jupyter/ydoc@3.3.3...654e8f80f619c7820dd82dfbe3c6d985b6a69e28))
|
|
8
|
+
|
|
9
|
+
### Bugs fixed
|
|
10
|
+
|
|
11
|
+
- Fix cell duplication when YStore diverges from disk [#376](https://github.com/jupyter-server/jupyter_ydoc/pull/376) ([@jordanhboxer](https://github.com/jordanhboxer), [@davidbrochart](https://github.com/davidbrochart), [@dmonad](https://github.com/dmonad), [@krassowski](https://github.com/krassowski))
|
|
12
|
+
|
|
13
|
+
### Maintenance and upkeep improvements
|
|
14
|
+
|
|
15
|
+
- Bump toshimaru/auto-author-assign from 3.0.0 to 3.0.1 [#375](https://github.com/jupyter-server/jupyter_ydoc/pull/375) ([@davidbrochart](https://github.com/davidbrochart))
|
|
16
|
+
- Bump toshimaru/auto-author-assign from 2.1.1 to 3.0.0 [#373](https://github.com/jupyter-server/jupyter_ydoc/pull/373) ([@davidbrochart](https://github.com/davidbrochart))
|
|
17
|
+
- Bump actions/upload-artifact from 5 to 6 [#371](https://github.com/jupyter-server/jupyter_ydoc/pull/371) ([@davidbrochart](https://github.com/davidbrochart))
|
|
18
|
+
|
|
19
|
+
### Contributors to this release
|
|
20
|
+
|
|
21
|
+
The following people contributed discussions, new ideas, code and documentation contributions, and review.
|
|
22
|
+
See [our definition of contributors](https://github-activity.readthedocs.io/en/latest/#how-does-this-tool-define-contributions-in-the-reports).
|
|
23
|
+
|
|
24
|
+
([GitHub contributors page for this release](https://github.com/jupyter-server/jupyter_ydoc/graphs/contributors?from=2025-12-10&to=2026-01-09&type=c))
|
|
25
|
+
|
|
26
|
+
@davidbrochart ([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_ydoc+involves%3Adavidbrochart+updated%3A2025-12-10..2026-01-09&type=Issues)) | @dmonad ([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_ydoc+involves%3Admonad+updated%3A2025-12-10..2026-01-09&type=Issues)) | @jordanhboxer ([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_ydoc+involves%3Ajordanhboxer+updated%3A2025-12-10..2026-01-09&type=Issues)) | @krassowski ([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_ydoc+involves%3Akrassowski+updated%3A2025-12-10..2026-01-09&type=Issues))
|
|
27
|
+
|
|
28
|
+
<!-- <END NEW CHANGELOG ENTRY> -->
|
|
29
|
+
|
|
30
|
+
## 3.3.3
|
|
31
|
+
|
|
32
|
+
([Full Changelog](https://github.com/jupyter-server/jupyter_ydoc/compare/@jupyter/ydoc@3.3.2...3900efdeefef21eb6e117fdd30827ee4c9149ff3))
|
|
33
|
+
|
|
34
|
+
### Bugs fixed
|
|
35
|
+
|
|
36
|
+
- Fix multi-byte Unicode handling in files [#370](https://github.com/jupyter-server/jupyter_ydoc/pull/370) ([@krassowski](https://github.com/krassowski))
|
|
37
|
+
|
|
38
|
+
### Maintenance and upkeep improvements
|
|
39
|
+
|
|
40
|
+
### Contributors to this release
|
|
41
|
+
|
|
42
|
+
([GitHub contributors page for this release](https://github.com/jupyter-server/jupyter_ydoc/graphs/contributors?from=2025-12-01&to=2025-12-10&type=c))
|
|
43
|
+
|
|
44
|
+
[@krassowski](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_ydoc+involves%3Akrassowski+updated%3A2025-12-01..2025-12-10&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_ydoc+involves%3Apre-commit-ci+updated%3A2025-12-01..2025-12-10&type=Issues)
|
|
45
|
+
|
|
5
46
|
## 3.3.2
|
|
6
47
|
|
|
7
48
|
([Full Changelog](https://github.com/jupyter-server/jupyter_ydoc/compare/@jupyter/ydoc@3.3.1...87e205209dbfed2d367424e6913c88916e77d98e))
|
|
@@ -20,8 +61,6 @@
|
|
|
20
61
|
|
|
21
62
|
[@dependabot](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_ydoc+involves%3Adependabot+updated%3A2025-11-21..2025-12-01&type=Issues) | [@krassowski](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_ydoc+involves%3Akrassowski+updated%3A2025-11-21..2025-12-01&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_ydoc+involves%3Apre-commit-ci+updated%3A2025-11-21..2025-12-01&type=Issues)
|
|
22
63
|
|
|
23
|
-
<!-- <END NEW CHANGELOG ENTRY> -->
|
|
24
|
-
|
|
25
64
|
## 3.3.1
|
|
26
65
|
|
|
27
66
|
([Full Changelog](https://github.com/jupyter-server/jupyter_ydoc/compare/@jupyter/ydoc@3.3.0...0a0d0bc23b5894db0b00c5b073861025757c72db))
|
|
@@ -251,7 +251,13 @@ class YNotebook(YBaseDoc):
|
|
|
251
251
|
"id": str(uuid4()),
|
|
252
252
|
}
|
|
253
253
|
]
|
|
254
|
-
|
|
254
|
+
# Build dict of old cells by ID, keeping only the first occurrence of each ID
|
|
255
|
+
# to handle the case where the stored doc already has duplicate IDs.
|
|
256
|
+
old_ycells_by_id: dict[str, Map] = {}
|
|
257
|
+
for ycell in self._ycells:
|
|
258
|
+
cell_id = ycell.get("id")
|
|
259
|
+
if cell_id is not None and cell_id not in old_ycells_by_id:
|
|
260
|
+
old_ycells_by_id[cell_id] = ycell
|
|
255
261
|
|
|
256
262
|
with self._ydoc.transaction():
|
|
257
263
|
new_cell_list: list[dict] = []
|
|
@@ -267,34 +273,52 @@ class YNotebook(YBaseDoc):
|
|
|
267
273
|
)
|
|
268
274
|
|
|
269
275
|
if updated_granularly:
|
|
270
|
-
new_cell_list.append(
|
|
276
|
+
new_cell_list.append(new_cell)
|
|
271
277
|
retained_cells.add(cell_id)
|
|
272
278
|
continue
|
|
273
279
|
# New or changed cell
|
|
274
280
|
new_cell_list.append(new_cell)
|
|
275
281
|
|
|
276
|
-
# First delete all non-retained cells
|
|
282
|
+
# First delete all non-retained cells and duplicates
|
|
277
283
|
if not retained_cells:
|
|
278
284
|
# fast path if no cells were retained
|
|
279
285
|
self._ycells.clear()
|
|
280
286
|
else:
|
|
281
287
|
index = 0
|
|
288
|
+
seen: set[str] = set()
|
|
282
289
|
for old_ycell in list(self._ycells):
|
|
283
|
-
|
|
290
|
+
cell_id = old_ycell.get("id")
|
|
291
|
+
if cell_id is None or cell_id not in retained_cells or cell_id in seen:
|
|
284
292
|
self._ycells.pop(index)
|
|
285
293
|
else:
|
|
294
|
+
seen.add(cell_id)
|
|
286
295
|
index += 1
|
|
287
296
|
|
|
288
|
-
# Now
|
|
289
|
-
index
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
297
|
+
# Now reorder/insert cells to match new_cell_list
|
|
298
|
+
for index, new_cell in enumerate(new_cell_list):
|
|
299
|
+
new_id = new_cell.get("id")
|
|
300
|
+
|
|
301
|
+
# Fast path: correct cell already at this position
|
|
302
|
+
if len(self._ycells) > index and self._ycells[index].get("id") == new_id:
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
# Retained cell: find and move it into position
|
|
306
|
+
if new_id is not None and new_id in retained_cells:
|
|
307
|
+
# Linear scan to find the cell (O(n) per retained cell)
|
|
308
|
+
for cur in range(index + 1, len(self._ycells)):
|
|
309
|
+
if self._ycells[cur].get("id") == new_id:
|
|
310
|
+
# Use delete+recreate instead of move() for yjs 13.x compatibility
|
|
311
|
+
# (yjs 13.x doesn't support the move operation that pycrdt generates)
|
|
312
|
+
del self._ycells[cur]
|
|
313
|
+
self._ycells.insert(index, self.create_ycell(new_cell))
|
|
314
|
+
break
|
|
315
|
+
continue
|
|
316
|
+
|
|
317
|
+
# New cell: insert at position
|
|
296
318
|
self._ycells.insert(index, self.create_ycell(new_cell))
|
|
297
|
-
|
|
319
|
+
|
|
320
|
+
# Remove any extra cells at the end
|
|
321
|
+
del self._ycells[len(new_cell_list) :]
|
|
298
322
|
|
|
299
323
|
for key in [
|
|
300
324
|
k for k in self._ystate.keys() if k not in ("dirty", "path", "document_id")
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Copyright (c) Jupyter Development Team.
|
|
2
|
+
# Distributed under the terms of the Modified BSD License.
|
|
3
|
+
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from difflib import SequenceMatcher
|
|
6
|
+
from functools import partial
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pycrdt import Awareness, Doc, Text
|
|
10
|
+
|
|
11
|
+
from .ybasedoc import YBaseDoc
|
|
12
|
+
|
|
13
|
+
# Heuristic threshold as recommended in difflib documentation
|
|
14
|
+
SIMILARITY_THREESHOLD = 0.6
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class YUnicode(YBaseDoc):
|
|
18
|
+
"""
|
|
19
|
+
Extends :class:`YBaseDoc`, and represents a plain text document, encoded as UTF-8.
|
|
20
|
+
|
|
21
|
+
Schema:
|
|
22
|
+
|
|
23
|
+
.. code-block:: json
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
"state": YMap,
|
|
27
|
+
"source": YText
|
|
28
|
+
}
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, ydoc: Doc | None = None, awareness: Awareness | None = None):
|
|
32
|
+
"""
|
|
33
|
+
Constructs a YUnicode.
|
|
34
|
+
|
|
35
|
+
:param ydoc: The :class:`pycrdt.Doc` that will hold the data of the document, if provided.
|
|
36
|
+
:type ydoc: :class:`pycrdt.Doc`, optional.
|
|
37
|
+
:param awareness: The :class:`pycrdt.Awareness` that shares non persistent data
|
|
38
|
+
between clients.
|
|
39
|
+
:type awareness: :class:`pycrdt.Awareness`, optional.
|
|
40
|
+
"""
|
|
41
|
+
super().__init__(ydoc, awareness)
|
|
42
|
+
self._ysource: Text = self._ydoc.get("source", type=Text)
|
|
43
|
+
self.undo_manager.expand_scope(self._ysource)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def version(self) -> str:
|
|
47
|
+
"""
|
|
48
|
+
Returns the version of the document.
|
|
49
|
+
|
|
50
|
+
:return: Document's version.
|
|
51
|
+
:rtype: str
|
|
52
|
+
"""
|
|
53
|
+
return "1.0.0"
|
|
54
|
+
|
|
55
|
+
def get(self) -> str:
|
|
56
|
+
"""
|
|
57
|
+
Returns the content of the document.
|
|
58
|
+
|
|
59
|
+
:return: Document's content.
|
|
60
|
+
:rtype: str
|
|
61
|
+
"""
|
|
62
|
+
return str(self._ysource)
|
|
63
|
+
|
|
64
|
+
def set(self, value: str) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Sets the content of the document.
|
|
67
|
+
|
|
68
|
+
:param value: The content of the document.
|
|
69
|
+
:type value: str
|
|
70
|
+
"""
|
|
71
|
+
old_value = self.get()
|
|
72
|
+
if old_value == value:
|
|
73
|
+
# no-op if the values are already the same,
|
|
74
|
+
# to avoid side-effects such as cursor jumping to the top
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
before_bytes = old_value.encode("utf-8")
|
|
78
|
+
after_bytes = value.encode("utf-8")
|
|
79
|
+
|
|
80
|
+
with self._ydoc.transaction():
|
|
81
|
+
matcher = SequenceMatcher(a=before_bytes, b=after_bytes)
|
|
82
|
+
|
|
83
|
+
if (
|
|
84
|
+
matcher.real_quick_ratio() >= SIMILARITY_THREESHOLD
|
|
85
|
+
and matcher.ratio() >= SIMILARITY_THREESHOLD
|
|
86
|
+
):
|
|
87
|
+
operations = matcher.get_opcodes()
|
|
88
|
+
|
|
89
|
+
# Fix byte ranges and check for problematic overlaps
|
|
90
|
+
fixed_operations = []
|
|
91
|
+
prev_end = 0
|
|
92
|
+
prev_tag = None
|
|
93
|
+
has_overlap = False
|
|
94
|
+
|
|
95
|
+
for tag, i1, i2, j1, j2 in operations:
|
|
96
|
+
# Fix byte ranges to proper UTF-8 character boundaries
|
|
97
|
+
i1_fixed, i2_fixed = _fix_byte_range_to_char_boundary(before_bytes, i1, i2)
|
|
98
|
+
j1_fixed, j2_fixed = _fix_byte_range_to_char_boundary(after_bytes, j1, j2)
|
|
99
|
+
|
|
100
|
+
# Check if this operation overlaps with the previous one
|
|
101
|
+
# which can happen with grapheme clusters (emoji + modifiers, etc.)
|
|
102
|
+
if i1_fixed < prev_end and prev_tag != "equal":
|
|
103
|
+
has_overlap = True
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
prev_end = i2_fixed
|
|
107
|
+
prev_tag = tag
|
|
108
|
+
fixed_operations.append((tag, i1_fixed, i2_fixed, j1_fixed, j2_fixed))
|
|
109
|
+
|
|
110
|
+
# If we detected overlapping operations, fall back to hard reload
|
|
111
|
+
if has_overlap:
|
|
112
|
+
self._ysource.clear()
|
|
113
|
+
if value:
|
|
114
|
+
self._ysource += value
|
|
115
|
+
else:
|
|
116
|
+
# Apply granular operations
|
|
117
|
+
offset = 0
|
|
118
|
+
for tag, i1, i2, j1, j2 in fixed_operations:
|
|
119
|
+
match tag:
|
|
120
|
+
case "replace":
|
|
121
|
+
self._ysource[i1 + offset : i2 + offset] = after_bytes[
|
|
122
|
+
j1:j2
|
|
123
|
+
].decode("utf-8")
|
|
124
|
+
offset += (j2 - j1) - (i2 - i1)
|
|
125
|
+
case "delete":
|
|
126
|
+
del self._ysource[i1 + offset : i2 + offset]
|
|
127
|
+
offset -= i2 - i1
|
|
128
|
+
case "insert":
|
|
129
|
+
self._ysource.insert(
|
|
130
|
+
i1 + offset, after_bytes[j1:j2].decode("utf-8")
|
|
131
|
+
)
|
|
132
|
+
offset += j2 - j1
|
|
133
|
+
case "equal":
|
|
134
|
+
pass
|
|
135
|
+
case _:
|
|
136
|
+
raise ValueError(f"Unknown tag '{tag}' in sequence matcher")
|
|
137
|
+
else:
|
|
138
|
+
# for very different strings, just replace the whole content;
|
|
139
|
+
# this avoids generating a huge number of operations
|
|
140
|
+
|
|
141
|
+
# clear document
|
|
142
|
+
self._ysource.clear()
|
|
143
|
+
# initialize document
|
|
144
|
+
if value:
|
|
145
|
+
self._ysource += value
|
|
146
|
+
|
|
147
|
+
def observe(self, callback: Callable[[str, Any], None]) -> None:
|
|
148
|
+
"""
|
|
149
|
+
Subscribes to document changes.
|
|
150
|
+
|
|
151
|
+
:param callback: Callback that will be called when the document changes.
|
|
152
|
+
:type callback: Callable[[str, Any], None]
|
|
153
|
+
"""
|
|
154
|
+
self.unobserve()
|
|
155
|
+
self._subscriptions[self._ystate] = self._ystate.observe(partial(callback, "state"))
|
|
156
|
+
self._subscriptions[self._ysource] = self._ysource.observe(partial(callback, "source"))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _is_utf8_continuation_byte(byte: int) -> bool:
|
|
160
|
+
"""Check if a byte is a UTF-8 continuation byte (10xxxxxx)."""
|
|
161
|
+
return (byte & 0xC0) == 0x80
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _fix_byte_range_to_char_boundary(data: bytes, start: int, end: int) -> tuple[int, int]:
|
|
165
|
+
"""
|
|
166
|
+
Adjust byte indices to proper UTF-8 character boundaries.
|
|
167
|
+
|
|
168
|
+
:param data: The byte data.
|
|
169
|
+
:param start: The start byte index.
|
|
170
|
+
:param end: The end byte index.
|
|
171
|
+
:return: A tuple of (adjusted_start, adjusted_end).
|
|
172
|
+
"""
|
|
173
|
+
# Move start backward to the beginning of a UTF-8 character
|
|
174
|
+
while start > 0 and start < len(data) and _is_utf8_continuation_byte(data[start]):
|
|
175
|
+
start -= 1
|
|
176
|
+
|
|
177
|
+
# Move end forward to the end of a UTF-8 character
|
|
178
|
+
while end < len(data) and _is_utf8_continuation_byte(data[end]):
|
|
179
|
+
end += 1
|
|
180
|
+
|
|
181
|
+
return start, end
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
# Copyright (c) Jupyter Development Team.
|
|
2
|
-
# Distributed under the terms of the Modified BSD License.
|
|
3
|
-
|
|
4
|
-
from collections.abc import Callable
|
|
5
|
-
from difflib import SequenceMatcher
|
|
6
|
-
from functools import partial
|
|
7
|
-
from typing import Any
|
|
8
|
-
|
|
9
|
-
from pycrdt import Awareness, Doc, Text
|
|
10
|
-
|
|
11
|
-
from .ybasedoc import YBaseDoc
|
|
12
|
-
|
|
13
|
-
# Heuristic threshold as recommended in difflib documentation
|
|
14
|
-
SIMILARITY_THREESHOLD = 0.6
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class YUnicode(YBaseDoc):
|
|
18
|
-
"""
|
|
19
|
-
Extends :class:`YBaseDoc`, and represents a plain text document, encoded as UTF-8.
|
|
20
|
-
|
|
21
|
-
Schema:
|
|
22
|
-
|
|
23
|
-
.. code-block:: json
|
|
24
|
-
|
|
25
|
-
{
|
|
26
|
-
"state": YMap,
|
|
27
|
-
"source": YText
|
|
28
|
-
}
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
def __init__(self, ydoc: Doc | None = None, awareness: Awareness | None = None):
|
|
32
|
-
"""
|
|
33
|
-
Constructs a YUnicode.
|
|
34
|
-
|
|
35
|
-
:param ydoc: The :class:`pycrdt.Doc` that will hold the data of the document, if provided.
|
|
36
|
-
:type ydoc: :class:`pycrdt.Doc`, optional.
|
|
37
|
-
:param awareness: The :class:`pycrdt.Awareness` that shares non persistent data
|
|
38
|
-
between clients.
|
|
39
|
-
:type awareness: :class:`pycrdt.Awareness`, optional.
|
|
40
|
-
"""
|
|
41
|
-
super().__init__(ydoc, awareness)
|
|
42
|
-
self._ysource: Text = self._ydoc.get("source", type=Text)
|
|
43
|
-
self.undo_manager.expand_scope(self._ysource)
|
|
44
|
-
|
|
45
|
-
@property
|
|
46
|
-
def version(self) -> str:
|
|
47
|
-
"""
|
|
48
|
-
Returns the version of the document.
|
|
49
|
-
|
|
50
|
-
:return: Document's version.
|
|
51
|
-
:rtype: str
|
|
52
|
-
"""
|
|
53
|
-
return "1.0.0"
|
|
54
|
-
|
|
55
|
-
def get(self) -> str:
|
|
56
|
-
"""
|
|
57
|
-
Returns the content of the document.
|
|
58
|
-
|
|
59
|
-
:return: Document's content.
|
|
60
|
-
:rtype: str
|
|
61
|
-
"""
|
|
62
|
-
return str(self._ysource)
|
|
63
|
-
|
|
64
|
-
def set(self, value: str) -> None:
|
|
65
|
-
"""
|
|
66
|
-
Sets the content of the document.
|
|
67
|
-
|
|
68
|
-
:param value: The content of the document.
|
|
69
|
-
:type value: str
|
|
70
|
-
"""
|
|
71
|
-
old_value = self.get()
|
|
72
|
-
if old_value == value:
|
|
73
|
-
# no-op if the values are already the same,
|
|
74
|
-
# to avoid side-effects such as cursor jumping to the top
|
|
75
|
-
return
|
|
76
|
-
|
|
77
|
-
with self._ydoc.transaction():
|
|
78
|
-
matcher = SequenceMatcher(a=old_value, b=value)
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
matcher.real_quick_ratio() >= SIMILARITY_THREESHOLD
|
|
82
|
-
and matcher.ratio() >= SIMILARITY_THREESHOLD
|
|
83
|
-
):
|
|
84
|
-
operations = matcher.get_opcodes()
|
|
85
|
-
offset = 0
|
|
86
|
-
for tag, i1, i2, j1, j2 in operations:
|
|
87
|
-
match tag:
|
|
88
|
-
case "replace":
|
|
89
|
-
self._ysource[i1 + offset : i2 + offset] = value[j1:j2]
|
|
90
|
-
offset += (j2 - j1) - (i2 - i1)
|
|
91
|
-
case "delete":
|
|
92
|
-
del self._ysource[i1 + offset : i2 + offset]
|
|
93
|
-
offset -= i2 - i1
|
|
94
|
-
case "insert":
|
|
95
|
-
self._ysource.insert(i1 + offset, value[j1:j2])
|
|
96
|
-
offset += j2 - j1
|
|
97
|
-
case "equal":
|
|
98
|
-
pass
|
|
99
|
-
case _:
|
|
100
|
-
raise ValueError(f"Unknown tag '{tag}' in sequence matcher")
|
|
101
|
-
else:
|
|
102
|
-
# for very different strings, just replace the whole content;
|
|
103
|
-
# this avoids generating a huge number of operations
|
|
104
|
-
|
|
105
|
-
# clear document
|
|
106
|
-
self._ysource.clear()
|
|
107
|
-
# initialize document
|
|
108
|
-
if value:
|
|
109
|
-
self._ysource += value
|
|
110
|
-
|
|
111
|
-
def observe(self, callback: Callable[[str, Any], None]) -> None:
|
|
112
|
-
"""
|
|
113
|
-
Subscribes to document changes.
|
|
114
|
-
|
|
115
|
-
:param callback: Callback that will be called when the document changes.
|
|
116
|
-
:type callback: Callable[[str, Any], None]
|
|
117
|
-
"""
|
|
118
|
-
self.unobserve()
|
|
119
|
-
self._subscriptions[self._ystate] = self._ystate.observe(partial(callback, "state"))
|
|
120
|
-
self._subscriptions[self._ysource] = self._ysource.observe(partial(callback, "source"))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|