jupyter-ydoc 3.3.2__tar.gz → 3.3.3__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.3}/.pre-commit-config.yaml +1 -1
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/CHANGELOG.md +18 -2
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/PKG-INFO +1 -1
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/javascript/package.json +1 -1
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/jupyter_ydoc/_version.py +1 -1
- jupyter_ydoc-3.3.3/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.3}/.github/dependabot.yml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/.github/workflows/auto_author_assign.yml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/.github/workflows/check-release.yml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/.github/workflows/enforce-label.yml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/.github/workflows/license-header.yml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/.github/workflows/prep-release.yml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/.github/workflows/publish-release.yml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/.github/workflows/test.yml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/.gitignore +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/.licenserc.yaml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/.yarn/releases/yarn-3.4.1.cjs +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/.yarnrc.yml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/LICENSE +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/README.md +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/docs/Makefile +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/docs/make.bat +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/docs/source/_static/jupyter_logo.svg +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/docs/source/_static/logo-icon.png +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/docs/source/conf.py +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/docs/source/custom.md +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/docs/source/index.md +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/docs/source/javascript_api.rst +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/docs/source/overview.md +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/docs/source/python_api.rst +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/docs/source/schema.md +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/jupyter_ydoc/__init__.py +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/jupyter_ydoc/py.typed +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/jupyter_ydoc/utils.py +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/jupyter_ydoc/ybasedoc.py +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/jupyter_ydoc/yblob.py +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/jupyter_ydoc/yfile.py +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/jupyter_ydoc/ynotebook.py +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/lerna.json +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/package.json +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/pyproject.toml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/pytest.ini +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/readthedocs.yml +0 -0
- {jupyter_ydoc-3.3.2 → jupyter_ydoc-3.3.3}/yarn.lock +0 -0
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- <START NEW CHANGELOG ENTRY> -->
|
|
4
4
|
|
|
5
|
+
## 3.3.3
|
|
6
|
+
|
|
7
|
+
([Full Changelog](https://github.com/jupyter-server/jupyter_ydoc/compare/@jupyter/ydoc@3.3.2...3900efdeefef21eb6e117fdd30827ee4c9149ff3))
|
|
8
|
+
|
|
9
|
+
### Bugs fixed
|
|
10
|
+
|
|
11
|
+
- Fix multi-byte Unicode handling in files [#370](https://github.com/jupyter-server/jupyter_ydoc/pull/370) ([@krassowski](https://github.com/krassowski))
|
|
12
|
+
|
|
13
|
+
### Maintenance and upkeep improvements
|
|
14
|
+
|
|
15
|
+
### Contributors to this release
|
|
16
|
+
|
|
17
|
+
([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))
|
|
18
|
+
|
|
19
|
+
[@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)
|
|
20
|
+
|
|
21
|
+
<!-- <END NEW CHANGELOG ENTRY> -->
|
|
22
|
+
|
|
5
23
|
## 3.3.2
|
|
6
24
|
|
|
7
25
|
([Full Changelog](https://github.com/jupyter-server/jupyter_ydoc/compare/@jupyter/ydoc@3.3.1...87e205209dbfed2d367424e6913c88916e77d98e))
|
|
@@ -20,8 +38,6 @@
|
|
|
20
38
|
|
|
21
39
|
[@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
40
|
|
|
23
|
-
<!-- <END NEW CHANGELOG ENTRY> -->
|
|
24
|
-
|
|
25
41
|
## 3.3.1
|
|
26
42
|
|
|
27
43
|
([Full Changelog](https://github.com/jupyter-server/jupyter_ydoc/compare/@jupyter/ydoc@3.3.0...0a0d0bc23b5894db0b00c5b073861025757c72db))
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|