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 ADDED
@@ -0,0 +1,3 @@
1
+ from .base import Delta
2
+
3
+ __version__ = '1.0.1'
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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