quill-delta-python312 0.1__py3-none-any.whl
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.
- delta/__init__.py +3 -0
- delta/base.py +330 -0
- delta/deep_eq.py +143 -0
- delta/html.py +332 -0
- delta/op.py +170 -0
- quill_delta_python312-0.1.dist-info/METADATA +122 -0
- quill_delta_python312-0.1.dist-info/RECORD +10 -0
- quill_delta_python312-0.1.dist-info/WHEEL +5 -0
- quill_delta_python312-0.1.dist-info/licenses/LICENSE.txt +21 -0
- quill_delta_python312-0.1.dist-info/top_level.txt +1 -0
delta/__init__.py
ADDED
delta/base.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import diff_match_patch
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from functools import reduce
|
|
6
|
+
except:
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
from . import op
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
NULL_CHARACTER = chr(0)
|
|
13
|
+
DIFF_EQUAL = 0
|
|
14
|
+
DIFF_INSERT = 1
|
|
15
|
+
DIFF_DELETE = -1
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def merge(a, b):
|
|
19
|
+
return copy.deepcopy(a or {}).update(b or {})
|
|
20
|
+
|
|
21
|
+
def differ(a, b, timeout=1):
|
|
22
|
+
differ = diff_match_patch.diff_match_patch()
|
|
23
|
+
differ.Diff_Timeout = timeout
|
|
24
|
+
return differ.diff_main(a, b)
|
|
25
|
+
|
|
26
|
+
def smallest(*parts):
|
|
27
|
+
return min(filter(lambda x: x is not None, parts))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Delta(object):
|
|
31
|
+
def __init__(self, ops=None, **attrs):
|
|
32
|
+
if hasattr(ops, 'ops'):
|
|
33
|
+
ops = ops.ops
|
|
34
|
+
self.ops = ops or []
|
|
35
|
+
self.__dict__.update(attrs)
|
|
36
|
+
|
|
37
|
+
def __eq__(self, other):
|
|
38
|
+
return self.ops == other.ops
|
|
39
|
+
|
|
40
|
+
def __repr__(self):
|
|
41
|
+
return "{}({})".format(self.__class__.__name__, self.ops)
|
|
42
|
+
|
|
43
|
+
def insert(self, text, **attrs):
|
|
44
|
+
if text == "":
|
|
45
|
+
return self
|
|
46
|
+
new_op = {'insert': text}
|
|
47
|
+
if attrs:
|
|
48
|
+
new_op['attributes'] = attrs
|
|
49
|
+
return self.push(new_op)
|
|
50
|
+
|
|
51
|
+
def delete(self, length):
|
|
52
|
+
if length <= 0:
|
|
53
|
+
return self
|
|
54
|
+
return self.push({'delete': length});
|
|
55
|
+
|
|
56
|
+
def retain(self, length, **attrs):
|
|
57
|
+
if length <= 0:
|
|
58
|
+
return self
|
|
59
|
+
new_op = {'retain': length}
|
|
60
|
+
if attrs:
|
|
61
|
+
new_op['attributes'] = attrs
|
|
62
|
+
return self.push(new_op)
|
|
63
|
+
|
|
64
|
+
def push(self, operation):
|
|
65
|
+
index = len(self.ops)
|
|
66
|
+
new_op = copy.deepcopy(operation)
|
|
67
|
+
try:
|
|
68
|
+
last_op = self.ops[index - 1]
|
|
69
|
+
except IndexError:
|
|
70
|
+
self.ops.append(new_op)
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
if op.type(new_op) == op.type(last_op) == 'delete':
|
|
74
|
+
last_op['delete'] += new_op['delete']
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
if op.type(last_op) == 'delete' and op.type(new_op) == 'insert':
|
|
78
|
+
index -= 1
|
|
79
|
+
try:
|
|
80
|
+
last_op = self.ops[index - 1]
|
|
81
|
+
except IndexError:
|
|
82
|
+
self.ops.insert(0, new_op)
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
if new_op.get('attributes') == last_op.get('attributes'):
|
|
86
|
+
if isinstance(new_op.get('insert'), str) and isinstance(last_op.get('insert'), str):
|
|
87
|
+
last_op['insert'] += new_op['insert']
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
if isinstance(new_op.get('retain'), int) and isinstance(last_op.get('retain'), int):
|
|
91
|
+
last_op['retain'] += new_op['retain']
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
self.ops.insert(index, new_op)
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
def extend(self, ops):
|
|
98
|
+
if hasattr(ops, 'ops'):
|
|
99
|
+
ops = ops.ops
|
|
100
|
+
if not ops:
|
|
101
|
+
return self
|
|
102
|
+
self.push(ops[0])
|
|
103
|
+
self.ops.extend(ops[1:])
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
def concat(self, other):
|
|
107
|
+
delta = self.__class__(copy.deepcopy(self.ops))
|
|
108
|
+
delta.extend(other)
|
|
109
|
+
return delta
|
|
110
|
+
|
|
111
|
+
def chop(self):
|
|
112
|
+
try:
|
|
113
|
+
last_op = self.ops[-1]
|
|
114
|
+
if op.type(last_op) == 'retain' and not last_op.get('attributes'):
|
|
115
|
+
self.ops.pop()
|
|
116
|
+
except IndexError:
|
|
117
|
+
pass
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
def document(self):
|
|
121
|
+
parts = []
|
|
122
|
+
for op in self:
|
|
123
|
+
insert = op.get('insert')
|
|
124
|
+
if insert:
|
|
125
|
+
if isinstance(insert, str):
|
|
126
|
+
parts.append(insert)
|
|
127
|
+
else:
|
|
128
|
+
parts.append(NULL_CHARACTER)
|
|
129
|
+
else:
|
|
130
|
+
raise ValueError("document() can only be called on Deltas that have only insert ops")
|
|
131
|
+
return "".join(parts)
|
|
132
|
+
|
|
133
|
+
def __iter__(self):
|
|
134
|
+
return iter(self.ops)
|
|
135
|
+
|
|
136
|
+
def __getitem__(self, index):
|
|
137
|
+
if isinstance(index, int):
|
|
138
|
+
start = index
|
|
139
|
+
stop = index + 1
|
|
140
|
+
|
|
141
|
+
elif isinstance(index, slice):
|
|
142
|
+
start = index.start or 0
|
|
143
|
+
stop = index.stop or None
|
|
144
|
+
|
|
145
|
+
if index.step is not None:
|
|
146
|
+
print(index)
|
|
147
|
+
raise ValueError("no support for step slices")
|
|
148
|
+
|
|
149
|
+
if (start is not None and start < 0) or (stop is not None and stop < 0):
|
|
150
|
+
raise ValueError("no support for negative indexing.")
|
|
151
|
+
|
|
152
|
+
ops = []
|
|
153
|
+
iter = self.iterator()
|
|
154
|
+
index = 0
|
|
155
|
+
while iter.has_next():
|
|
156
|
+
if stop is not None and index >= stop:
|
|
157
|
+
break
|
|
158
|
+
if index < start:
|
|
159
|
+
next_op = iter.next(start - index)
|
|
160
|
+
else:
|
|
161
|
+
if stop is not None:
|
|
162
|
+
next_op = iter.next(stop-index)
|
|
163
|
+
else:
|
|
164
|
+
next_op = iter.next()
|
|
165
|
+
ops.append(next_op)
|
|
166
|
+
index += op.length(next_op)
|
|
167
|
+
|
|
168
|
+
return Delta(ops)
|
|
169
|
+
|
|
170
|
+
def __len__(self):
|
|
171
|
+
return sum(op.length(o) for o in self.ops)
|
|
172
|
+
|
|
173
|
+
def iterator(self):
|
|
174
|
+
return op.iterator(self.ops)
|
|
175
|
+
|
|
176
|
+
def change_length(self):
|
|
177
|
+
length = 0
|
|
178
|
+
for operator in self:
|
|
179
|
+
if op.type(operator) == 'delete':
|
|
180
|
+
length -= operator['delete']
|
|
181
|
+
else:
|
|
182
|
+
length += op.length(operator)
|
|
183
|
+
return length
|
|
184
|
+
|
|
185
|
+
def length(self):
|
|
186
|
+
return sum(op.length(o) for o in self)
|
|
187
|
+
|
|
188
|
+
def compose(self, other):
|
|
189
|
+
self_it = self.iterator()
|
|
190
|
+
other_it = other.iterator()
|
|
191
|
+
delta = self.__class__()
|
|
192
|
+
while self_it.has_next() or other_it.has_next():
|
|
193
|
+
if other_it.peek_type() == 'insert':
|
|
194
|
+
delta.push(other_it.next())
|
|
195
|
+
elif self_it.peek_type() == 'delete':
|
|
196
|
+
delta.push(self_it.next())
|
|
197
|
+
else:
|
|
198
|
+
length = smallest(self_it.peek_length(), other_it.peek_length())
|
|
199
|
+
self_op = self_it.next(length)
|
|
200
|
+
other_op = other_it.next(length)
|
|
201
|
+
if 'retain' in other_op:
|
|
202
|
+
new_op = {}
|
|
203
|
+
if 'retain' in self_op:
|
|
204
|
+
new_op['retain'] = length
|
|
205
|
+
elif 'insert' in self_op:
|
|
206
|
+
new_op['insert'] = self_op['insert']
|
|
207
|
+
# Preserve null when composing with a retain, otherwise remove it for inserts
|
|
208
|
+
attributes = op.compose(self_op.get('attributes'), other_op.get('attributes'), isinstance(self_op.get('retain'), int))
|
|
209
|
+
if (attributes):
|
|
210
|
+
new_op['attributes'] = attributes
|
|
211
|
+
delta.push(new_op)
|
|
212
|
+
# Other op should be delete, we could be an insert or retain
|
|
213
|
+
# Insert + delete cancels out
|
|
214
|
+
elif op.type(other_op) == 'delete' and 'retain' in self_op:
|
|
215
|
+
delta.push(other_op)
|
|
216
|
+
return delta.chop()
|
|
217
|
+
|
|
218
|
+
def diff(self, other):
|
|
219
|
+
"""
|
|
220
|
+
Returns a diff of two *documents*, which is defined as a delta
|
|
221
|
+
with only inserts.
|
|
222
|
+
"""
|
|
223
|
+
if self.ops == other.ops:
|
|
224
|
+
return self.__class__()
|
|
225
|
+
|
|
226
|
+
self_doc = self.document()
|
|
227
|
+
other_doc = other.document()
|
|
228
|
+
self_it = self.iterator()
|
|
229
|
+
other_it = other.iterator()
|
|
230
|
+
|
|
231
|
+
delta = self.__class__()
|
|
232
|
+
for code, text in differ(self_doc, other_doc):
|
|
233
|
+
length = len(text)
|
|
234
|
+
while length > 0:
|
|
235
|
+
op_length = 0
|
|
236
|
+
if code == DIFF_INSERT:
|
|
237
|
+
op_length = min(other_it.peek_length(), length)
|
|
238
|
+
delta.push(other_it.next(op_length))
|
|
239
|
+
elif code == DIFF_DELETE:
|
|
240
|
+
op_length = min(length, self_it.peek_length())
|
|
241
|
+
self_it.next(op_length)
|
|
242
|
+
delta.delete(op_length)
|
|
243
|
+
elif code == DIFF_EQUAL:
|
|
244
|
+
op_length = min(self_it.peek_length(), other_it.peek_length(), length)
|
|
245
|
+
self_op = self_it.next(op_length)
|
|
246
|
+
other_op = other_it.next(op_length)
|
|
247
|
+
if self_op.get('insert') == other_op.get('insert'):
|
|
248
|
+
attributes = op.diff(self_op.get('attributes'), other_op.get('attributes'))
|
|
249
|
+
delta.retain(op_length, **(attributes or {}))
|
|
250
|
+
else:
|
|
251
|
+
delta.push(other_op).delete(op_length)
|
|
252
|
+
else:
|
|
253
|
+
raise RuntimeError("Diff library returned unknown op code: %r", code)
|
|
254
|
+
if op_length == 0:
|
|
255
|
+
return
|
|
256
|
+
length -= op_length
|
|
257
|
+
return delta.chop()
|
|
258
|
+
|
|
259
|
+
def each_line(self, fn, newline='\n'):
|
|
260
|
+
for line, attributes, index in self.iter_lines():
|
|
261
|
+
if fn(line, attributes, index) is False:
|
|
262
|
+
break
|
|
263
|
+
|
|
264
|
+
def iter_lines(self, newline='\n'):
|
|
265
|
+
iter = self.iterator()
|
|
266
|
+
line = self.__class__()
|
|
267
|
+
i = 0
|
|
268
|
+
while iter.has_next():
|
|
269
|
+
if iter.peek_type() != 'insert':
|
|
270
|
+
return
|
|
271
|
+
self_op = iter.peek()
|
|
272
|
+
start = op.length(self_op) - iter.peek_length()
|
|
273
|
+
if isinstance(self_op.get('insert'), str):
|
|
274
|
+
index = self_op['insert'][start:].find(newline)
|
|
275
|
+
else:
|
|
276
|
+
index = -1
|
|
277
|
+
|
|
278
|
+
if index < 0:
|
|
279
|
+
line.push(iter.next())
|
|
280
|
+
elif index > 0:
|
|
281
|
+
line.push(iter.next(index))
|
|
282
|
+
else:
|
|
283
|
+
yield line, iter.next(1).get('attributes', {}), i
|
|
284
|
+
i += 1
|
|
285
|
+
line = Delta()
|
|
286
|
+
if len(line) > 0:
|
|
287
|
+
yield line, {}, i
|
|
288
|
+
|
|
289
|
+
def transform(self, other, priority=False):
|
|
290
|
+
if isinstance(other, int):
|
|
291
|
+
return self.transform_position(other, priority)
|
|
292
|
+
|
|
293
|
+
self_it = self.iterator()
|
|
294
|
+
other_it = other.iterator()
|
|
295
|
+
delta = Delta()
|
|
296
|
+
|
|
297
|
+
while self_it.has_next() or other_it.has_next():
|
|
298
|
+
if self_it.peek_type() == 'insert' and (priority or other_it.peek_type() != 'insert'):
|
|
299
|
+
delta.retain(op.length(self_it.next()))
|
|
300
|
+
elif other_it.peek_type() == 'insert':
|
|
301
|
+
delta.push(other_it.next())
|
|
302
|
+
else:
|
|
303
|
+
length = smallest(self_it.peek_length(), other_it.peek_length())
|
|
304
|
+
self_op = self_it.next(length)
|
|
305
|
+
other_op = other_it.next(length)
|
|
306
|
+
if self_op.get('delete'):
|
|
307
|
+
# Our delete either makes their delete redundant or removes their retain
|
|
308
|
+
continue
|
|
309
|
+
elif other_op.get('delete'):
|
|
310
|
+
delta.push(other_op)
|
|
311
|
+
else:
|
|
312
|
+
# We retain either their retain or insert
|
|
313
|
+
delta.retain(length, **(op.transform(self_op.get('attributes'), other_op.get('attributes'), priority) or {}))
|
|
314
|
+
|
|
315
|
+
return delta.chop()
|
|
316
|
+
|
|
317
|
+
def transform_position(self, index, priority=False):
|
|
318
|
+
iter = self.iterator()
|
|
319
|
+
offset = 0
|
|
320
|
+
while iter.has_next() and offset <= index:
|
|
321
|
+
length = iter.peek_length()
|
|
322
|
+
next_type = iter.peek_type()
|
|
323
|
+
iter.next()
|
|
324
|
+
if next_type == 'delete':
|
|
325
|
+
index -= min(length, index - offset)
|
|
326
|
+
continue
|
|
327
|
+
elif next_type == 'insert' and (offset < index or not priority):
|
|
328
|
+
index += length
|
|
329
|
+
offset += length
|
|
330
|
+
return index
|
delta/deep_eq.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#Copyright (c) 2010-2013 Samuel Sutch [samuel.sutch@gmail.com]
|
|
2
|
+
#
|
|
3
|
+
#Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
#of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
#in the Software without restriction, including without limitation the rights
|
|
6
|
+
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
#copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
#furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
#The above copyright notice and this permission notice shall be included in
|
|
11
|
+
#all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
#THE SOFTWARE.
|
|
20
|
+
|
|
21
|
+
import datetime, time, functools, operator, types
|
|
22
|
+
|
|
23
|
+
default_fudge = datetime.timedelta(seconds=0, microseconds=0, days=0)
|
|
24
|
+
|
|
25
|
+
def deep_eq(_v1, _v2, datetime_fudge=default_fudge, _assert=False):
|
|
26
|
+
"""
|
|
27
|
+
Tests for deep equality between two python data structures recursing
|
|
28
|
+
into sub-structures if necessary. Works with all python types including
|
|
29
|
+
iterators and generators. This function was dreampt up to test API responses
|
|
30
|
+
but could be used for anything. Be careful. With deeply nested structures
|
|
31
|
+
you may blow the stack.
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
datetime_fudge => this is a datetime.timedelta object which, when
|
|
35
|
+
comparing dates, will accept values that differ
|
|
36
|
+
by the number of seconds specified
|
|
37
|
+
_assert => passing yes for this will raise an assertion error
|
|
38
|
+
when values do not match, instead of returning
|
|
39
|
+
false (very useful in combination with pdb)
|
|
40
|
+
|
|
41
|
+
Doctests included:
|
|
42
|
+
|
|
43
|
+
>>> x1, y1 = ({'a': 'b'}, {'a': 'b'})
|
|
44
|
+
>>> deep_eq(x1, y1)
|
|
45
|
+
True
|
|
46
|
+
>>> x2, y2 = ({'a': 'b'}, {'b': 'a'})
|
|
47
|
+
>>> deep_eq(x2, y2)
|
|
48
|
+
False
|
|
49
|
+
>>> x3, y3 = ({'a': {'b': 'c'}}, {'a': {'b': 'c'}})
|
|
50
|
+
>>> deep_eq(x3, y3)
|
|
51
|
+
True
|
|
52
|
+
>>> x4, y4 = ({'c': 't', 'a': {'b': 'c'}}, {'a': {'b': 'n'}, 'c': 't'})
|
|
53
|
+
>>> deep_eq(x4, y4)
|
|
54
|
+
False
|
|
55
|
+
>>> x5, y5 = ({'a': [1,2,3]}, {'a': [1,2,3]})
|
|
56
|
+
>>> deep_eq(x5, y5)
|
|
57
|
+
True
|
|
58
|
+
>>> x6, y6 = ({'a': [1,'b',8]}, {'a': [2,'b',8]})
|
|
59
|
+
>>> deep_eq(x6, y6)
|
|
60
|
+
False
|
|
61
|
+
>>> x7, y7 = ('a', 'a')
|
|
62
|
+
>>> deep_eq(x7, y7)
|
|
63
|
+
True
|
|
64
|
+
>>> x8, y8 = (['p','n',['asdf']], ['p','n',['asdf']])
|
|
65
|
+
>>> deep_eq(x8, y8)
|
|
66
|
+
True
|
|
67
|
+
>>> x9, y9 = (['p','n',['asdf',['omg']]], ['p', 'n', ['asdf',['nowai']]])
|
|
68
|
+
>>> deep_eq(x9, y9)
|
|
69
|
+
False
|
|
70
|
+
>>> x10, y10 = (1, 2)
|
|
71
|
+
>>> deep_eq(x10, y10)
|
|
72
|
+
False
|
|
73
|
+
>>> deep_eq((str(p) for p in xrange(10)), (str(p) for p in xrange(10)))
|
|
74
|
+
True
|
|
75
|
+
>>> str(deep_eq(range(4), range(4)))
|
|
76
|
+
'True'
|
|
77
|
+
>>> deep_eq(xrange(100), xrange(100))
|
|
78
|
+
True
|
|
79
|
+
>>> deep_eq(xrange(2), xrange(5))
|
|
80
|
+
False
|
|
81
|
+
>>> import datetime
|
|
82
|
+
>>> from datetime import datetime as dt
|
|
83
|
+
>>> d1, d2 = (dt.now(), dt.now() + datetime.timedelta(seconds=4))
|
|
84
|
+
>>> deep_eq(d1, d2)
|
|
85
|
+
False
|
|
86
|
+
>>> deep_eq(d1, d2, datetime_fudge=datetime.timedelta(seconds=5))
|
|
87
|
+
True
|
|
88
|
+
"""
|
|
89
|
+
_deep_eq = functools.partial(deep_eq, datetime_fudge=datetime_fudge,
|
|
90
|
+
_assert=_assert)
|
|
91
|
+
|
|
92
|
+
def _check_assert(R, a, b, reason=''):
|
|
93
|
+
if _assert and not R:
|
|
94
|
+
assert 0, "an assertion has failed in deep_eq (%s) %s != %s" % (
|
|
95
|
+
reason, str(a), str(b))
|
|
96
|
+
return R
|
|
97
|
+
|
|
98
|
+
def _deep_dict_eq(d1, d2):
|
|
99
|
+
k1, k2 = (sorted(d1.keys()), sorted(d2.keys()))
|
|
100
|
+
if k1 != k2: # keys should be exactly equal
|
|
101
|
+
return _check_assert(False, k1, k2, "keys")
|
|
102
|
+
|
|
103
|
+
return _check_assert(operator.eq(sum(_deep_eq(d1[k], d2[k])
|
|
104
|
+
for k in k1),
|
|
105
|
+
len(k1)), d1, d2, "dictionaries")
|
|
106
|
+
|
|
107
|
+
def _deep_iter_eq(l1, l2):
|
|
108
|
+
if len(l1) != len(l2):
|
|
109
|
+
return _check_assert(False, l1, l2, "lengths")
|
|
110
|
+
return _check_assert(operator.eq(sum(_deep_eq(v1, v2)
|
|
111
|
+
for v1, v2 in zip(l1, l2)),
|
|
112
|
+
len(l1)), l1, l2, "iterables")
|
|
113
|
+
|
|
114
|
+
def op(a, b):
|
|
115
|
+
_op = operator.eq
|
|
116
|
+
if type(a) == datetime.datetime and type(b) == datetime.datetime:
|
|
117
|
+
s = datetime_fudge.seconds
|
|
118
|
+
t1, t2 = (time.mktime(a.timetuple()), time.mktime(b.timetuple()))
|
|
119
|
+
l = t1 - t2
|
|
120
|
+
l = -l if l > 0 else l
|
|
121
|
+
return _check_assert((-s if s > 0 else s) <= l, a, b, "dates")
|
|
122
|
+
return _check_assert(_op(a, b), a, b, "values")
|
|
123
|
+
|
|
124
|
+
c1, c2 = (_v1, _v2)
|
|
125
|
+
|
|
126
|
+
# guard against strings because they are iterable and their
|
|
127
|
+
# elements yield iterables infinitely.
|
|
128
|
+
# I N C E P T I O N
|
|
129
|
+
for t in types.StringTypes:
|
|
130
|
+
if isinstance(_v1, t):
|
|
131
|
+
break
|
|
132
|
+
else:
|
|
133
|
+
if isinstance(_v1, types.DictType):
|
|
134
|
+
op = _deep_dict_eq
|
|
135
|
+
else:
|
|
136
|
+
try:
|
|
137
|
+
c1, c2 = (list(iter(_v1)), list(iter(_v2)))
|
|
138
|
+
except TypeError:
|
|
139
|
+
c1, c2 = _v1, _v2
|
|
140
|
+
else:
|
|
141
|
+
op = _deep_iter_eq
|
|
142
|
+
|
|
143
|
+
return op(c1, c2)
|
delta/html.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from .base import Delta
|
|
4
|
+
from lxml.html import HtmlElement, Element
|
|
5
|
+
from lxml import html
|
|
6
|
+
from cssutils import parseStyle
|
|
7
|
+
|
|
8
|
+
CLASSES = {
|
|
9
|
+
'font': {
|
|
10
|
+
'serif': 'ql-font-serif',
|
|
11
|
+
'monospace': 'ql-font-monospace'
|
|
12
|
+
},
|
|
13
|
+
'size': {
|
|
14
|
+
'small': 'ql-size-small',
|
|
15
|
+
'large': 'ql-size-large',
|
|
16
|
+
'huge': 'ql-size-huge',
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
CODE_BLOCK_CLASS = 'ql-syntax'
|
|
21
|
+
VIDEO_IFRAME_CLASS = 'ql-video'
|
|
22
|
+
INDENT_CLASS = 'ql-indent-%d'
|
|
23
|
+
DIRECTION_CLASS = 'ql-direction-%s'
|
|
24
|
+
ALIGN_CLASS = 'ql-align-%s'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger('quill')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Helpers ###
|
|
31
|
+
def sub_element(root, *a, **kwargs):
|
|
32
|
+
e = root.makeelement(*a, **kwargs)
|
|
33
|
+
root.append(e)
|
|
34
|
+
return e
|
|
35
|
+
|
|
36
|
+
def styled(element, styles):
|
|
37
|
+
if element.tag != 'span':
|
|
38
|
+
element = sub_element(element, 'span')
|
|
39
|
+
declare = parseStyle(element.attrib.get('style', ''))
|
|
40
|
+
for k, v in styles.items():
|
|
41
|
+
declare.setProperty(k, v)
|
|
42
|
+
element.attrib['style'] = declare.getCssText(' ')
|
|
43
|
+
return element
|
|
44
|
+
|
|
45
|
+
def classed(element, *classes):
|
|
46
|
+
if element.tag != 'span':
|
|
47
|
+
element = sub_element(element, 'span')
|
|
48
|
+
return add_class(element, *classes)
|
|
49
|
+
|
|
50
|
+
def add_class(element, *classes):
|
|
51
|
+
current = element.attrib.get('class')
|
|
52
|
+
if current:
|
|
53
|
+
current = set(current.split())
|
|
54
|
+
else:
|
|
55
|
+
current = set()
|
|
56
|
+
classes = current.union(set(classes))
|
|
57
|
+
element.attrib['class'] = " ".join(sorted(list(classes)))
|
|
58
|
+
return element
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
### Registry ###
|
|
62
|
+
class Format:
|
|
63
|
+
all = []
|
|
64
|
+
|
|
65
|
+
def __init__(self, fn, name):
|
|
66
|
+
self.all.append(self)
|
|
67
|
+
self.name = name
|
|
68
|
+
self.fn = fn
|
|
69
|
+
self.check_fn = None
|
|
70
|
+
|
|
71
|
+
def __repr__(self):
|
|
72
|
+
return "<%s %r>" % (self.__class__.__name__, self.name)
|
|
73
|
+
|
|
74
|
+
def __call__(self, root, op):
|
|
75
|
+
if self._check(op):
|
|
76
|
+
try:
|
|
77
|
+
el = self.fn(root, op)
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error("Rendering format failed: %r", e)
|
|
80
|
+
el = ""
|
|
81
|
+
return el
|
|
82
|
+
return root
|
|
83
|
+
|
|
84
|
+
def check(self, fn):
|
|
85
|
+
self.check_fn = fn
|
|
86
|
+
return fn
|
|
87
|
+
|
|
88
|
+
def _check(self, op):
|
|
89
|
+
if self.check_fn:
|
|
90
|
+
return self.check_fn(op)
|
|
91
|
+
|
|
92
|
+
attrs = op.get('attributes', None)
|
|
93
|
+
if attrs and self.name in attrs:
|
|
94
|
+
return True
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def format(fn, name=None, cls=Format):
|
|
98
|
+
if isinstance(fn, str):
|
|
99
|
+
name = fn
|
|
100
|
+
def wrapper(fn):
|
|
101
|
+
return format(fn, name, cls)
|
|
102
|
+
return wrapper
|
|
103
|
+
return cls(fn, name or fn.__name__)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class BlockFormat(Format):
|
|
107
|
+
"""
|
|
108
|
+
Block formats change the entire line through the attrs of the endline, not through
|
|
109
|
+
something like the insert.
|
|
110
|
+
"""
|
|
111
|
+
all = []
|
|
112
|
+
|
|
113
|
+
def __init__(self, fn, name):
|
|
114
|
+
self.all.append(self)
|
|
115
|
+
self.name = name
|
|
116
|
+
self.fn = fn
|
|
117
|
+
self.check_fn = None
|
|
118
|
+
|
|
119
|
+
def __call__(self, root, attrs):
|
|
120
|
+
if self.name in attrs:
|
|
121
|
+
root = self.fn(root, attrs)
|
|
122
|
+
return root
|
|
123
|
+
|
|
124
|
+
def __repr__(self):
|
|
125
|
+
return "<BlockFormat %s>" % self.name
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
### Formats ###
|
|
129
|
+
@format
|
|
130
|
+
def header(root, op):
|
|
131
|
+
root.tag = 'h%s' % op['attributes']['header']
|
|
132
|
+
return root
|
|
133
|
+
|
|
134
|
+
@format
|
|
135
|
+
def strong(root, op):
|
|
136
|
+
return sub_element(root, 'strong')
|
|
137
|
+
|
|
138
|
+
@format
|
|
139
|
+
def bold(root, op):
|
|
140
|
+
return strong.fn(root, op)
|
|
141
|
+
|
|
142
|
+
@format
|
|
143
|
+
def em(root, op):
|
|
144
|
+
return sub_element(root, 'em')
|
|
145
|
+
|
|
146
|
+
@format
|
|
147
|
+
def italic(root, op):
|
|
148
|
+
return em.fn(root, 'em')
|
|
149
|
+
|
|
150
|
+
@format
|
|
151
|
+
def underline(root, op):
|
|
152
|
+
return sub_element(root, 'u')
|
|
153
|
+
|
|
154
|
+
@format
|
|
155
|
+
def strike(root, op):
|
|
156
|
+
return sub_element(root, 's')
|
|
157
|
+
|
|
158
|
+
@format
|
|
159
|
+
def script(root, op):
|
|
160
|
+
if op['attributes']['script'] == 'super':
|
|
161
|
+
return sub_element(root, 'sup')
|
|
162
|
+
if op['attributes']['script'] == 'sub':
|
|
163
|
+
return sub_element(root, 'sub')
|
|
164
|
+
return root
|
|
165
|
+
|
|
166
|
+
@format
|
|
167
|
+
def background(root, op):
|
|
168
|
+
return styled(root, {'background-color': op['attributes']['background']})
|
|
169
|
+
|
|
170
|
+
@format
|
|
171
|
+
def color(root, op):
|
|
172
|
+
return styled(root, {'color': op['attributes']['color']})
|
|
173
|
+
|
|
174
|
+
@format
|
|
175
|
+
def link(root, op):
|
|
176
|
+
el = sub_element(root, 'a')
|
|
177
|
+
link = op['attributes']['link']
|
|
178
|
+
|
|
179
|
+
if isinstance(link, str):
|
|
180
|
+
el.attrib['href'] = op['attributes']['link']
|
|
181
|
+
elif isinstance(link, dict):
|
|
182
|
+
for attrname, attrvalue in link.items():
|
|
183
|
+
el.attrib[attrname] = attrvalue
|
|
184
|
+
|
|
185
|
+
return el
|
|
186
|
+
|
|
187
|
+
@format
|
|
188
|
+
def classes(root, op):
|
|
189
|
+
attrs = op.get('attributes', None)
|
|
190
|
+
if attrs:
|
|
191
|
+
for name, options in CLASSES.items():
|
|
192
|
+
value = op['attributes'].get(name)
|
|
193
|
+
if value in options:
|
|
194
|
+
root = classed(root, options[value])
|
|
195
|
+
return root
|
|
196
|
+
|
|
197
|
+
@classes.check
|
|
198
|
+
def classes_check(op):
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
@format
|
|
202
|
+
def image(root, op):
|
|
203
|
+
el = sub_element(root, 'img')
|
|
204
|
+
el.attrib['src'] = op['insert']['image']
|
|
205
|
+
attrs = op.get('attributes', None)
|
|
206
|
+
if attrs and attrs.get('width', None):
|
|
207
|
+
el.attrib['width'] = op['attributes']['width']
|
|
208
|
+
if attrs and attrs.get('height', None):
|
|
209
|
+
el.attrib['height'] = op['attributes']['height']
|
|
210
|
+
return el
|
|
211
|
+
|
|
212
|
+
@image.check
|
|
213
|
+
def image_check(op):
|
|
214
|
+
insert = op.get('insert')
|
|
215
|
+
return isinstance(insert, dict) and insert.get('image')
|
|
216
|
+
|
|
217
|
+
@format
|
|
218
|
+
def video(root, op):
|
|
219
|
+
attributes = op.get('attributes', {})
|
|
220
|
+
iframe = root.makeelement('iframe')
|
|
221
|
+
iframe.attrib.update({
|
|
222
|
+
'class': VIDEO_IFRAME_CLASS,
|
|
223
|
+
'frameborder': '0',
|
|
224
|
+
'allowfullscreen': 'true',
|
|
225
|
+
'src': op['insert']['video']
|
|
226
|
+
})
|
|
227
|
+
if isinstance(attributes, dict) and attributes.get('align', None):
|
|
228
|
+
align_block(iframe, attributes)
|
|
229
|
+
root.addprevious(iframe)
|
|
230
|
+
return iframe
|
|
231
|
+
|
|
232
|
+
@video.check
|
|
233
|
+
def video_check(op):
|
|
234
|
+
insert = op.get('insert')
|
|
235
|
+
return isinstance(insert, dict) and insert.get('video')
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
### Block Formats ###
|
|
239
|
+
LIST_TYPES = {'ordered': 'ol', 'bullet': 'ul'}
|
|
240
|
+
|
|
241
|
+
@format('indent', cls=BlockFormat)
|
|
242
|
+
def indent(block, attrs):
|
|
243
|
+
level = attrs['indent']
|
|
244
|
+
if level >= 1 and level <= 8:
|
|
245
|
+
return add_class(block, INDENT_CLASS % level)
|
|
246
|
+
return block
|
|
247
|
+
|
|
248
|
+
@format('list', cls=BlockFormat)
|
|
249
|
+
def list_block(block, attrs):
|
|
250
|
+
block.tag = 'li'
|
|
251
|
+
previous = block.getprevious()
|
|
252
|
+
list_tag = LIST_TYPES.get(attrs['list'], 'ol')
|
|
253
|
+
if previous is not None and previous.tag == list_tag:
|
|
254
|
+
list_el = previous
|
|
255
|
+
else:
|
|
256
|
+
list_el = sub_element(block.getparent(), list_tag)
|
|
257
|
+
list_el.append(block)
|
|
258
|
+
return block
|
|
259
|
+
|
|
260
|
+
@format('direction', cls=BlockFormat)
|
|
261
|
+
def list_block(block, attrs):
|
|
262
|
+
return add_class(block, DIRECTION_CLASS % attrs['direction'])
|
|
263
|
+
|
|
264
|
+
@format('align', cls=BlockFormat)
|
|
265
|
+
def align_block(block, attrs):
|
|
266
|
+
return add_class(block, ALIGN_CLASS % attrs['align'])
|
|
267
|
+
|
|
268
|
+
@format('header', cls=BlockFormat)
|
|
269
|
+
def header_block(block, attrs):
|
|
270
|
+
block.tag = 'h%s' % attrs['header']
|
|
271
|
+
return block
|
|
272
|
+
|
|
273
|
+
@format('blockquote', cls=BlockFormat)
|
|
274
|
+
def blockquote(block, attrs):
|
|
275
|
+
block.tag = 'blockquote'
|
|
276
|
+
return block
|
|
277
|
+
|
|
278
|
+
@format("code-block")
|
|
279
|
+
def code_block(root, op):
|
|
280
|
+
root.tag = 'pre'
|
|
281
|
+
root.attrib.update({
|
|
282
|
+
'class': CODE_BLOCK_CLASS,
|
|
283
|
+
'spellcheck': 'false'
|
|
284
|
+
})
|
|
285
|
+
return root
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
### Processors ###
|
|
289
|
+
def append_op(root, op):
|
|
290
|
+
for fmt in Format.all:
|
|
291
|
+
root = fmt(root, op)
|
|
292
|
+
|
|
293
|
+
text = op.get('insert')
|
|
294
|
+
if isinstance(text, str) and text:
|
|
295
|
+
if list(root):
|
|
296
|
+
last = root[-1]
|
|
297
|
+
if last.tail:
|
|
298
|
+
last.tail += text
|
|
299
|
+
else:
|
|
300
|
+
last.tail = text
|
|
301
|
+
else:
|
|
302
|
+
if root.text:
|
|
303
|
+
root.text += text
|
|
304
|
+
else:
|
|
305
|
+
root.text = text
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def append_line(root, delta, attrs, index):
|
|
309
|
+
block = sub_element(root, 'p')
|
|
310
|
+
|
|
311
|
+
for op in delta.ops:
|
|
312
|
+
append_op(block, op)
|
|
313
|
+
|
|
314
|
+
if len(block) <= 0 and not block.text:
|
|
315
|
+
br = sub_element(block, 'br')
|
|
316
|
+
|
|
317
|
+
for fmt in BlockFormat.all:
|
|
318
|
+
root = fmt(block, attrs)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def render(delta, method='html', pretty=False):
|
|
322
|
+
if not isinstance(delta, Delta):
|
|
323
|
+
delta = Delta(delta)
|
|
324
|
+
|
|
325
|
+
root = html.fragment_fromstring("<template></template>")
|
|
326
|
+
for line, attrs, index in delta.iter_lines():
|
|
327
|
+
append_line(root, line, attrs, index)
|
|
328
|
+
|
|
329
|
+
result = "".join(
|
|
330
|
+
html.tostring(child, method=method, with_tail=True, encoding='unicode', pretty_print=pretty)
|
|
331
|
+
for child in root)
|
|
332
|
+
return result
|
delta/op.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def compose(a, b, keep_null=False):
|
|
5
|
+
"""
|
|
6
|
+
Compose two operations into one.
|
|
7
|
+
|
|
8
|
+
``keep_null`` [default=false] is a boolean that controls whether None/Null
|
|
9
|
+
attributes are retrained.
|
|
10
|
+
"""
|
|
11
|
+
if a is None:
|
|
12
|
+
a = {}
|
|
13
|
+
if b is None:
|
|
14
|
+
b = {}
|
|
15
|
+
|
|
16
|
+
# deep copy b, but get rid of None values if keep_null is falsey
|
|
17
|
+
attributes = dict((k, copy.deepcopy(v)) for k, v in b.items() if keep_null or v is not None)
|
|
18
|
+
|
|
19
|
+
for k, v in a.items():
|
|
20
|
+
if k not in b:
|
|
21
|
+
attributes[k] = copy.deepcopy(v)
|
|
22
|
+
|
|
23
|
+
return attributes or None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def diff(a, b):
|
|
27
|
+
"""
|
|
28
|
+
Return the difference between operations a and b.
|
|
29
|
+
"""
|
|
30
|
+
if a is None:
|
|
31
|
+
a = {}
|
|
32
|
+
if b is None:
|
|
33
|
+
b = {}
|
|
34
|
+
|
|
35
|
+
keys = set(a.keys()).union(set(b.keys()))
|
|
36
|
+
|
|
37
|
+
attributes = {}
|
|
38
|
+
for k in keys:
|
|
39
|
+
av, bv = a.get(k, None), b.get(k, None)
|
|
40
|
+
if av != bv:
|
|
41
|
+
attributes[k] = bv
|
|
42
|
+
|
|
43
|
+
return attributes or None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def transform(a, b, priority=True):
|
|
47
|
+
"""
|
|
48
|
+
Return the transformation from operation a to b.
|
|
49
|
+
|
|
50
|
+
If ``priority`` is falsey [default=True] then just return b.
|
|
51
|
+
"""
|
|
52
|
+
if a is None:
|
|
53
|
+
a = {}
|
|
54
|
+
if b is None:
|
|
55
|
+
b = {}
|
|
56
|
+
|
|
57
|
+
if not priority:
|
|
58
|
+
return b or None
|
|
59
|
+
|
|
60
|
+
attributes = {}
|
|
61
|
+
for k, v in b.items():
|
|
62
|
+
if k not in a:
|
|
63
|
+
attributes[k] = v
|
|
64
|
+
|
|
65
|
+
return attributes or None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def length_of(op):
|
|
69
|
+
typ = type_of(op)
|
|
70
|
+
if typ == 'delete':
|
|
71
|
+
return op['delete']
|
|
72
|
+
elif typ == 'retain':
|
|
73
|
+
return op['retain']
|
|
74
|
+
elif isinstance(op.get('insert'), str):
|
|
75
|
+
return len(op['insert'])
|
|
76
|
+
else:
|
|
77
|
+
return 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def type_of(op):
|
|
81
|
+
if not op:
|
|
82
|
+
return None
|
|
83
|
+
if isinstance(op.get('delete'), int):
|
|
84
|
+
return 'delete'
|
|
85
|
+
if isinstance(op.get('retain'), int):
|
|
86
|
+
return 'retain'
|
|
87
|
+
return 'insert'
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Iterator(object):
|
|
92
|
+
"""
|
|
93
|
+
An iterator that enables itself to break off operations
|
|
94
|
+
to exactly the length needed via the ``next()`` method.
|
|
95
|
+
"""
|
|
96
|
+
def __init__(self, ops=[]):
|
|
97
|
+
self.ops = ops
|
|
98
|
+
self.reset()
|
|
99
|
+
|
|
100
|
+
def reset(self):
|
|
101
|
+
self.index = 0
|
|
102
|
+
self.offset = 0
|
|
103
|
+
|
|
104
|
+
def has_next(self):
|
|
105
|
+
return self.peek_length() is not None
|
|
106
|
+
|
|
107
|
+
def next(self, length=None):
|
|
108
|
+
offset = self.offset
|
|
109
|
+
|
|
110
|
+
op = self.peek()
|
|
111
|
+
op_type = type_of(op)
|
|
112
|
+
if op is None:
|
|
113
|
+
return { 'retain': None }
|
|
114
|
+
|
|
115
|
+
op_length = length_of(op)
|
|
116
|
+
if (length is None or length >= op_length - offset):
|
|
117
|
+
length = op_length - offset
|
|
118
|
+
self.index += 1
|
|
119
|
+
self.offset = 0
|
|
120
|
+
else:
|
|
121
|
+
self.offset += length
|
|
122
|
+
|
|
123
|
+
if op_type == 'delete':
|
|
124
|
+
return { 'delete': length }
|
|
125
|
+
|
|
126
|
+
result_op = {}
|
|
127
|
+
if op.get('attributes'):
|
|
128
|
+
result_op['attributes'] = op['attributes']
|
|
129
|
+
|
|
130
|
+
if op_type == 'retain':
|
|
131
|
+
result_op['retain'] = length
|
|
132
|
+
elif isinstance(op.get('insert'), str):
|
|
133
|
+
result_op['insert'] = op['insert'][offset:offset+length]
|
|
134
|
+
else:
|
|
135
|
+
assert offset == 0
|
|
136
|
+
assert length == 1
|
|
137
|
+
if 'insert' in op:
|
|
138
|
+
result_op['insert'] = op['insert']
|
|
139
|
+
|
|
140
|
+
return result_op
|
|
141
|
+
|
|
142
|
+
__next__ = next
|
|
143
|
+
|
|
144
|
+
def __length__(self):
|
|
145
|
+
return len(self.ops)
|
|
146
|
+
|
|
147
|
+
def __iter__(self):
|
|
148
|
+
return self
|
|
149
|
+
|
|
150
|
+
def peek(self):
|
|
151
|
+
try:
|
|
152
|
+
return self.ops[self.index]
|
|
153
|
+
except IndexError:
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
def peek_length(self):
|
|
157
|
+
next_op = self.peek()
|
|
158
|
+
if next_op is None:
|
|
159
|
+
return None
|
|
160
|
+
return length_of(next_op) - self.offset
|
|
161
|
+
|
|
162
|
+
def peek_type(self):
|
|
163
|
+
op = self.peek()
|
|
164
|
+
if op is None:
|
|
165
|
+
return 'retain'
|
|
166
|
+
return type_of(op)
|
|
167
|
+
|
|
168
|
+
length = length_of
|
|
169
|
+
type = type_of
|
|
170
|
+
iterator = lambda x: Iterator(x)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: quill-delta-python312
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Fork of delta-python with dependencies updated for python 3.12 and up
|
|
5
|
+
Home-page: https://github.com/yesyves/quill-delta-python312
|
|
6
|
+
Author: Brantley Harris
|
|
7
|
+
Author-email: brantley@forge.works
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE.txt
|
|
11
|
+
Requires-Dist: diff-match-patch>=20181111.0
|
|
12
|
+
Provides-Extra: html
|
|
13
|
+
Requires-Dist: lxml>=6.0; extra == "html"
|
|
14
|
+
Requires-Dist: cssutils>=2.0; extra == "html"
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: author-email
|
|
17
|
+
Dynamic: description
|
|
18
|
+
Dynamic: description-content-type
|
|
19
|
+
Dynamic: home-page
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
Dynamic: provides-extra
|
|
22
|
+
Dynamic: requires-dist
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
Dynamic: summary
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Delta (Python Port)
|
|
28
|
+
|
|
29
|
+
Python port of the javascript Delta library for QuillJS: https://github.com/quilljs/delta
|
|
30
|
+
|
|
31
|
+
Some basic pythonizing has been done, but mostly it works exactly like the above library.
|
|
32
|
+
|
|
33
|
+
There is no other python specific documentation at this time, sorry. Please see the tests
|
|
34
|
+
for reference examples.
|
|
35
|
+
|
|
36
|
+
## Install with [Poetry](https://poetry.eustace.io/docs/#installation)
|
|
37
|
+
|
|
38
|
+
With HTML rendering:
|
|
39
|
+
|
|
40
|
+
> poetry add -E html quill-delta
|
|
41
|
+
|
|
42
|
+
Without HTML rendering:
|
|
43
|
+
|
|
44
|
+
> poetry add quill-delta
|
|
45
|
+
|
|
46
|
+
## Install with pip
|
|
47
|
+
|
|
48
|
+
Note: If you're using `zsh`, see below.
|
|
49
|
+
|
|
50
|
+
With HTML rendering:
|
|
51
|
+
|
|
52
|
+
> pip install quill-delta[html]
|
|
53
|
+
|
|
54
|
+
With HTML rendering (zsh):
|
|
55
|
+
|
|
56
|
+
> pip install quill-delta"[html]"
|
|
57
|
+
|
|
58
|
+
Without HTML rendering:
|
|
59
|
+
|
|
60
|
+
> pip install quill-delta
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Rendering HTML in Python
|
|
64
|
+
|
|
65
|
+
This library includes a module `delta.html` that renders html from an operation list,
|
|
66
|
+
allowing you to render Quill Delta operations in full from a Python server.
|
|
67
|
+
|
|
68
|
+
For example:
|
|
69
|
+
|
|
70
|
+
from delta import html
|
|
71
|
+
|
|
72
|
+
ops = [
|
|
73
|
+
{ "insert":"Quill\nEditor\n\n" },
|
|
74
|
+
{ "insert": "bold",
|
|
75
|
+
"attributes": {"bold": True}},
|
|
76
|
+
{ "insert":" and the " },
|
|
77
|
+
{ "insert":"italic",
|
|
78
|
+
"attributes": { "italic": True }},
|
|
79
|
+
{ "insert":"\n\nNormal\n" },
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
html.render(ops)
|
|
83
|
+
|
|
84
|
+
Result (line formatting added for readability):
|
|
85
|
+
|
|
86
|
+
<p>Quill</p>
|
|
87
|
+
<p>Editor</p>
|
|
88
|
+
<p><br></p>
|
|
89
|
+
<p><strong>bold</strong> and the <em>italic</em></p>
|
|
90
|
+
<p><br></p>
|
|
91
|
+
<p>Normal</p>
|
|
92
|
+
|
|
93
|
+
[See test_html.py](tests/test_html.py) for more examples.
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Developing
|
|
97
|
+
|
|
98
|
+
## Setup
|
|
99
|
+
If you'd to contribute to quill-delta-python, get started setting your development environment by running:
|
|
100
|
+
|
|
101
|
+
Checkout the repository
|
|
102
|
+
|
|
103
|
+
> git clone https://github.com/forgeworks/quill-delta-python.git
|
|
104
|
+
|
|
105
|
+
Make sure you have python 3 installed, e.g.,
|
|
106
|
+
|
|
107
|
+
> python --version
|
|
108
|
+
|
|
109
|
+
From inside your new quill-delta-python directory:
|
|
110
|
+
|
|
111
|
+
> python3 -m venv env
|
|
112
|
+
> source env/bin/activate
|
|
113
|
+
> pip install poetry
|
|
114
|
+
> poetry install -E html
|
|
115
|
+
|
|
116
|
+
## Tests
|
|
117
|
+
To run tests do:
|
|
118
|
+
|
|
119
|
+
> py.test
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
delta/__init__.py,sha256=3YaFu2C462Cj5MdWySBTFBXwHWbPGyGHDpuWkmj0QQI,46
|
|
2
|
+
delta/base.py,sha256=TTUMwdghfhbN7lLr-K7YWwlEpMcNClsBKD1mdBjl1qQ,11092
|
|
3
|
+
delta/deep_eq.py,sha256=yYY2FngVK7syhrnOYCSgNjvQ9VZRexl0IeflvArgiMM,5227
|
|
4
|
+
delta/html.py,sha256=sLao3uc6DC8Mw5VkZos8TuOe2R07-f7kYu6QCvK4qzY,8188
|
|
5
|
+
delta/op.py,sha256=sYHa2uhI6xlctdIUeK_7lk1S-lGOldUdpx6goqvMV-I,3795
|
|
6
|
+
quill_delta_python312-0.1.dist-info/licenses/LICENSE.txt,sha256=dBEV4IpoX-tkLLW75k5Tf2cGf9mVyyNPtkKn0X3eU94,1081
|
|
7
|
+
quill_delta_python312-0.1.dist-info/METADATA,sha256=Z5k4KF2ShbnpDlTSZQi3JuEKNwGglOpCsodYmbio5s0,2772
|
|
8
|
+
quill_delta_python312-0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
quill_delta_python312-0.1.dist-info/top_level.txt,sha256=ZzlT4K1_xTJH9P6twsLUUGOWhA0fh5ZSb0jUczOsdlI,6
|
|
10
|
+
quill_delta_python312-0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Brantley Harris
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
delta
|