blob: 7f103d92d583d2f47ab5ec53e79c7ff6bc4754f4 [file] [log] [blame]
#!/usr/bin/python2.4
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import UserDict
import element
import errors
import util
class Annotation(object):
"""Models an annotation on a document.
Annotations are key/value pairs over a range of content. Annotations
can be used to store data or to be interpreted by a client when displaying
the data.
"""
# Use the following constants to control the display of the client
#: Reserved annotation for setting background color of text.
BACKGROUND_COLOR = "style/backgroundColor"
#: Reserved annotation for setting color of text.
COLOR = "style/color"
#: Reserved annotation for setting font family of text.
FONT_FAMILY = "style/fontFamily"
#: Reserved annotation for setting font family of text.
FONT_SIZE = "style/fontSize"
#: Reserved annotation for setting font style of text.
FONT_STYLE = "style/fontStyle"
#: Reserved annotation for setting font weight of text.
FONT_WEIGHT = "style/fontWeight"
#: Reserved annotation for setting text decoration.
TEXT_DECORATION = "style/textDecoration"
#: Reserved annotation for setting vertical alignment.
VERTICAL_ALIGN = "style/verticalAlign"
#: Reserved annotation for setting link.
LINK = "link/manual"
def __init__(self, name, value, start, end):
self._name = name
self._value = value
self._start = start
self._end = end
@property
def name(self):
return self._name
@property
def value(self):
return self._value
@property
def start(self):
return self._start
@property
def end(self):
return self._end
def _shift(self, where, inc):
"""Shift annotation by 'inc' if it (partly) overlaps with 'where'."""
if self._start >= where:
self._start += inc
if self._end >= where:
self._end += inc
def serialize(self):
"""Serializes the annotation.
Returns:
A dict containing the name, value, and range values.
"""
return {'name': self._name,
'value': self._value,
'range': {'start': self._start,
'end': self._end}}
class Annotations(object, UserDict.DictMixin):
"""A dictionary-like object containing the annotations, keyed by name."""
def __init__(self, operation_queue, blip):
self._operation_queue = operation_queue
self._blip = blip
self._store = {}
def __contains__(self, what):
if isinstance(what, Annotation):
what = what.name
return what in self._store
def _add_internal(self, name, value, start, end):
"""Internal add annotation does not send out operations."""
if name in self._store:
# TODO: use bisect to make this more efficient.
new_list = []
for existing in self._store[name]:
if start > existing.end or end < existing.start:
new_list.append(existing)
else:
if existing.value == value:
# merge the annotations:
start = min(existing.start, start)
end = max(existing.end, end)
else:
# chop the bits off the existing annotation
if existing.start < start:
new_list.append(Annotation(
existing.name, existing.value, existing.start, start))
if existing.end > end:
new_list.append(Annotation(
existing.name, existing.value, existing.end, end))
new_list.append(Annotation(name, value, start, end))
self._store[name] = new_list
else:
self._store[name] = [Annotation(name, value, start, end)]
def _delete_internal(self, name, start=0, end=-1):
"""Remove the passed annotaion from the internal representation."""
if not name in self._store:
return
if end < 0:
end = len(self._blip) + end
new_list = []
for a in self._store[name]:
if start > a.end or end < a.start:
new_list.append(a)
elif start < a.start and end > a.end:
continue
else:
if a.start < start:
new_list.append(Annotation(name, a.value, a.start, start))
if a.end > end:
new_list.append(Annotation(name, a.value, end, a.end))
if new_list:
self._store[name] = new_list
else:
del self._store[name]
def _shift(self, where, inc):
"""Shift annotation by 'inc' if it (partly) overlaps with 'where'."""
for annotations in self._store.values():
for annotation in annotations:
annotation._shift(where, inc)
# Merge fragmented annotations that should be contiguous, for example:
# Annotation('foo', 'bar', 1, 2) and Annotation('foo', 'bar', 2, 3).
for name, annotations in self._store.items():
new_list = []
for i, annotation in enumerate(annotations):
name = annotation.name
value = annotation.value
start = annotation.start
end = annotation.end
# Find the last end index.
for j, next_annotation in enumerate(annotations[i + 1:]):
# Not contiguous, skip.
if (end < next_annotation.start):
break
# Contiguous, merge.
if (end == next_annotation.start and value == next_annotation.value):
end = next_annotation.end
del annotations[j]
new_list.append(Annotation(name, value, start, end))
self._store[name] = new_list
def __len__(self):
return len(self._store)
def __getitem__(self, key):
return self._store[key]
def __iter__(self):
for l in self._store.values():
for ann in l:
yield ann
def names(self):
"""Return the names of the annotations in the store."""
return self._store.keys()
def serialize(self):
"""Return a list of the serialized annotations."""
res = []
for v in self._store.values():
res += [a.serialize() for a in v]
return res
class Blips(object, UserDict.DictMixin):
"""A dictionary-like object containing the blips, keyed on blip ID."""
def __init__(self, blips):
self._blips = blips
def __contains__(self, blip_id):
return blip_id in self._blips
def __getitem__(self, blip_id):
return self._blips[blip_id]
def __iter__(self):
return self._blips.__iter__()
def __len__(self):
return len(self._blips)
def _add(self, ablip):
self._blips[ablip.blip_id] = ablip
def _remove_with_id(self, blip_id):
del_blip = self._blips[blip_id]
if del_blip:
# Remove the reference to this blip from its parent.
parent_blip = self._blips[blip_id].parent_blip
if parent_blip:
parent_blip._child_blip_ids.remove(blip_id)
del self._blips[blip_id]
def get(self, blip_id, default_value=None):
"""Retrieves a blip.
Returns:
A Blip object. If none found for the ID, it returns None,
or if default_value is specified, it returns that.
"""
return self._blips.get(blip_id, default_value)
def serialize(self):
"""Serializes the blips.
Returns:
A dict of serialized blips.
"""
res = {}
for blip_id, item in self._blips.items():
res[blip_id] = item.serialize()
return res
def values(self):
"""Return the blips themselves."""
return self._blips.values()
class BlipRefs(object):
"""Represents a set of references to contents in a blip.
For example, a BlipRefs instance can represent the results
of a search, an explicitly set range, a regular expression,
or refer to the entire blip. BlipRefs are used to express
operations on a blip in a consistent way that can easily
be transfered to the server.
The typical way of creating a BlipRefs object is to use
selector methods on the Blip object. Developers will not
usually instantiate a BlipRefs object directly.
"""
DELETE = 'DELETE'
REPLACE = 'REPLACE'
INSERT = 'INSERT'
INSERT_AFTER = 'INSERT_AFTER'
ANNOTATE = 'ANNOTATE'
CLEAR_ANNOTATION = 'CLEAR_ANNOTATION'
UPDATE_ELEMENT = 'UPDATE_ELEMENT'
def __init__(self, blip, maxres=1):
self._blip = blip
self._maxres = maxres
@classmethod
def all(cls, blip, findwhat, maxres=-1, **restrictions):
"""Construct an instance representing the search for text or elements."""
obj = cls(blip, maxres)
obj._findwhat = findwhat
obj._restrictions = restrictions
obj._hits = lambda: obj._find(findwhat, maxres, **restrictions)
if findwhat is None:
# No findWhat, take the entire blip
obj._params = {}
else:
query = {'maxRes': maxres}
if isinstance(findwhat, basestring):
query['textMatch'] = findwhat
else:
query['elementMatch'] = findwhat.class_type
query['restrictions'] = restrictions
obj._params = {'modifyQuery': query}
return obj
@classmethod
def range(cls, blip, begin, end):
"""Constructs an instance representing an explicitly set range."""
obj = cls(blip)
obj._begin = begin
obj._end = end
obj._hits = lambda: [(begin, end)]
obj._params = {'range': {'start': begin, 'end': end}}
return obj
def _elem_matches(self, elem, clz, **restrictions):
if not isinstance(elem, clz):
return False
for key, val in restrictions.items():
if getattr(elem, key) != val:
return False
return True
def _find(self, what, maxres=-1, **restrictions):
"""Iterates where 'what' occurs in the associated blip.
What can be either a string or a class reference.
Examples:
self._find('hello') will return the first occurence of the word hello
self._find(element.Gadget, url='http://example.com/gadget.xml')
will return the first gadget that has as url example.com.
Args:
what: what to search for. Can be a class or a string. The class
should be an element from element.py
maxres: number of results to return at most, or <= 0 for all.
restrictions: if what specifies a class, further restrictions
of the found instances.
Yields:
Tuples indicating the range of the matches. For a one
character/element match at position x, (x, x+1) is yielded.
"""
blip = self._blip
if what is None:
yield 0, len(blip)
raise StopIteration
if isinstance(what, basestring):
idx = blip._content.find(what)
count = 0
while idx != -1:
yield idx, idx + len(what)
count += 1
if count == maxres:
raise StopIteration
idx = blip._content.find(what, idx + len(what))
else:
count = 0
for idx, el in blip._elements.items():
if self._elem_matches(el, what, **restrictions):
yield idx, idx + 1
count += 1
if count == maxres:
raise StopIteration
def _execute(self, modify_how, what, bundled_annotations=None):
"""Executes this BlipRefs object.
Args:
modify_how: What to do. Any of the operation declared at the top.
what: Depending on the operation. For delete, has to be None.
For the others it is a singleton, a list or a function returning
what to do; for ANNOTATE tuples of (key, value), for the others
either string or elements.
If what is a function, it takes three parameters, the content of
the blip, the beginning of the matching range and the end.
bundled_annotations: Annotations to apply immediately.
Raises:
IndexError when trying to access content outside of the blip.
ValueError when called with the wrong values.
Returns:
self for chainability.
"""
blip = self._blip
if modify_how != BlipRefs.DELETE:
if not isinstance(what, list):
what = [what]
next_index = 0
matched = []
# updated_elements is used to store the element type of the
# element to update
updated_elements = []
# For now, if we find one markup, we'll use it everywhere.
next = None
hit_found = False
for start, end in self._hits():
hit_found = True
if start < 0:
start += len(blip)
if end == 0:
end += len(blip)
if end < 0:
end += len(blip)
if len(blip) == 0:
if start != 0 or end != 0:
raise IndexError('Start and end have to be 0 for empty document')
elif start < 0 or end < 1 or start >= len(blip) or end > len(blip):
raise IndexError('Position outside the document')
if modify_how == BlipRefs.DELETE:
for i in range(start, end):
if i in blip._elements:
del blip._elements[i]
blip._delete_annotations(start, end)
blip._shift(end, start - end)
blip._content = blip._content[:start] + blip._content[end:]
else:
if callable(what):
next = what(blip._content, start, end)
matched.append(next)
else:
next = what[next_index]
next_index = (next_index + 1) % len(what)
if isinstance(next, str):
next = util.force_unicode(next)
if modify_how == BlipRefs.ANNOTATE:
key, value = next
blip.annotations._add_internal(key, value, start, end)
elif modify_how == BlipRefs.CLEAR_ANNOTATION:
blip.annotations._delete_internal(next, start, end)
elif modify_how == BlipRefs.UPDATE_ELEMENT:
el = blip._elements.get(start)
if not el:
raise ValueError('No element found at index %s' % start)
# the passing around of types this way feels a bit dirty:
updated_elements.append(element.Element.from_json({'type': el.type,
'properties': next}))
for k, b in next.items():
setattr(el, k, b)
else:
if modify_how == BlipRefs.INSERT:
end = start
elif modify_how == BlipRefs.INSERT_AFTER:
start = end
elif modify_how == BlipRefs.REPLACE:
pass
else:
raise ValueError('Unexpected modify_how: ' + modify_how)
if isinstance(next, element.Element):
text = ' '
else:
text = next
# in the case of a replace, and the replacement text is shorter,
# delete the delta.
if start != end and len(text) < end - start:
blip._delete_annotations(start + len(text), end)
blip._shift(end, len(text) + start - end)
blip._content = blip._content[:start] + text + blip._content[end:]
if bundled_annotations:
end_annotation = start + len(text)
blip._delete_annotations(start, end_annotation)
for key, value in bundled_annotations:
blip.annotations._add_internal(key, value, start, end_annotation)
if isinstance(next, element.Element):
blip._elements[start] = next
# No match found, return immediately without generating op.
if not hit_found:
return
operation = blip._operation_queue.document_modify(blip.wave_id,
blip.wavelet_id,
blip.blip_id)
for param, value in self._params.items():
operation.set_param(param, value)
modify_action = {'modifyHow': modify_how}
if modify_how == BlipRefs.DELETE:
pass
elif modify_how == BlipRefs.UPDATE_ELEMENT:
modify_action['elements'] = updated_elements
elif (modify_how == BlipRefs.REPLACE or
modify_how == BlipRefs.INSERT or
modify_how == BlipRefs.INSERT_AFTER):
if callable(what):
what = matched
if what:
if not isinstance(next, element.Element):
modify_action['values'] = [util.force_unicode(value) for value in what]
else:
modify_action['elements'] = what
elif modify_how == BlipRefs.ANNOTATE:
modify_action['values'] = [x[1] for x in what]
modify_action['annotationKey'] = what[0][0]
elif modify_how == BlipRefs.CLEAR_ANNOTATION:
modify_action['annotationKey'] = what[0]
if bundled_annotations:
modify_action['bundledAnnotations'] = [
{'key': key, 'value': value} for key, value in bundled_annotations]
operation.set_param('modifyAction', modify_action)
return self
def insert(self, what, bundled_annotations=None):
"""Inserts what at the matched positions."""
return self._execute(
BlipRefs.INSERT, what, bundled_annotations=bundled_annotations)
def insert_after(self, what, bundled_annotations=None):
"""Inserts what just after the matched positions."""
return self._execute(
BlipRefs.INSERT_AFTER, what, bundled_annotations=bundled_annotations)
def replace(self, what, bundled_annotations=None):
"""Replaces the matched positions with what."""
return self._execute(
BlipRefs.REPLACE, what, bundled_annotations=bundled_annotations)
def delete(self):
"""Deletes the content at the matched positions."""
return self._execute(BlipRefs.DELETE, None)
def annotate(self, name, value=None):
"""Annotates the content at the matched positions.
You can either specify both name and value to set the
same annotation, or supply as the first parameter something
that yields name/value pairs. The name and value should both be strings.
"""
if value is None:
what = name
else:
what = (name, value)
return self._execute(BlipRefs.ANNOTATE, what)
def clear_annotation(self, name):
"""Clears the annotation at the matched positions."""
return self._execute(BlipRefs.CLEAR_ANNOTATION, name)
def update_element(self, new_values):
"""Update an existing element with a set of new values.
For example, this code would update a button value:
button.update_element({'value': 'Yes'})
This code would update the 'seen' key in a gadget's state:
gadget.update_element({'seen': 'yes'})
Args:
new_values: A dictionary of property names and values.
"""
return self._execute(BlipRefs.UPDATE_ELEMENT, new_values)
def __nonzero__(self):
"""Return whether we have a value."""
for start, end in self._hits():
return True
return False
def value(self):
"""Convenience method to convert a BlipRefs to value of its first match."""
for start, end in self._hits():
if end - start == 1 and start in self._blip._elements:
return self._blip._elements[start]
else:
return self._blip.text[start:end]
raise ValueError('BlipRefs has no values')
def __getattr__(self, attribute):
"""Mirror the getattr of value().
This allows for clever things like
first(IMAGE).url
or
blip.annotate_with(key, value).upper()
"""
return getattr(self.value(), attribute)
def __radd__(self, other):
"""Make it possible to add this to a string."""
return other + self.value()
def __cmp__(self, other):
"""Support comparision with target."""
return cmp(self.value(), other)
def __iter__(self):
for start_end in self._hits():
yield start_end
class Blip(object):
"""Models a single blip instance.
Blips are essentially the documents that make up a conversation. Blips can
live in a hierarchy of blips. A root blip has no parent blip id, but all
blips have the ids of the wave and wavelet that they are associated with.
Blips also contain annotations, content and elements, which are accessed via
the Document object.
"""
def __init__(self, json, other_blips, operation_queue, thread=None,
reply_threads=None):
"""Inits this blip with JSON data.
Args:
json: JSON data dictionary from Wave server.
other_blips: A dictionary like object that can be used to resolve
ids of blips to blips.
thread: The BlipThread object that this blip belongs to.
reply_threads: A list BlipThread objects that are replies to this blip.
operation_queue: An OperationQueue object to store generated operations
in.
"""
self._blip_id = json.get('blipId')
self._reply_threads = reply_threads or []
self._thread = thread
self._operation_queue = operation_queue
self._child_blip_ids = list(json.get('childBlipIds', []))
self._content = json.get('content', '')
self._contributors = set(json.get('contributors', []))
self._creator = json.get('creator')
self._last_modified_time = json.get('lastModifiedTime', 0)
self._version = json.get('version', 0)
self._parent_blip_id = json.get('parentBlipId')
self._wave_id = json.get('waveId')
self._wavelet_id = json.get('waveletId')
if isinstance(other_blips, Blips):
self._other_blips = other_blips
else:
self._other_blips = Blips(other_blips)
self._annotations = Annotations(operation_queue, self)
for annjson in json.get('annotations', []):
r = annjson['range']
self._annotations._add_internal(annjson['name'],
annjson['value'],
r['start'],
r['end'])
self._elements = {}
json_elements = json.get('elements', {})
for elem in json_elements:
self._elements[int(elem)] = element.Element.from_json(json_elements[elem])
self.raw_data = json
@property
def blip_id(self):
"""The id of this blip."""
return self._blip_id
@property
def wave_id(self):
"""The id of the wave that this blip belongs to."""
return self._wave_id
@property
def wavelet_id(self):
"""The id of the wavelet that this blip belongs to."""
return self._wavelet_id
@property
def child_blip_ids(self):
"""The list of the ids of this blip's children."""
return self._child_blip_ids
@property
def child_blips(self):
"""The list of blips that are children of this blip."""
return [self._other_blips[blid_id] for blid_id in self._child_blip_ids
if blid_id in self._other_blips]
@property
def thread(self):
"""The thread that this blip belongs to."""
return self._thread
@property
def reply_threads(self):
"""The list of threads that are replies to this blip."""
return self._reply_threads
@property
def inline_reply_threads(self):
# TODO: Consider moving to constructor
inline_reply_threads = []
for reply_thread in self._reply_threads:
if reply_thread.location > -1:
inline_reply_threads.append(reply_thread)
return inline_reply_threads
@property
def contributors(self):
"""The set of participant ids that contributed to this blip."""
return self._contributors
@property
def creator(self):
"""The id of the participant that created this blip."""
return self._creator
@property
def last_modified_time(self):
"""The time in seconds since epoch when this blip was last modified."""
return self._last_modified_time
@property
def version(self):
"""The version of this blip."""
return self._version
@property
def parent_blip_id(self):
"""The parent blip_id or None if this is the root blip."""
return self._parent_blip_id
@property
def parent_blip(self):
"""The parent blip or None if it is the root."""
# if parent_blip_id is None, get will also return None
return self._other_blips.get(self._parent_blip_id)
@property
def inline_blip_offset(self):
"""The offset in the parent if this blip is inline or -1 if not.
If the parent is not in the context, this function will always
return -1 since it can't determine the inline blip status.
"""
parent = self.parent_blip
if not parent:
return -1
for offset, el in parent._elements.items():
if el.type == element.Element.INLINE_BLIP_TYPE and el.id == self.blip_id:
return offset
return -1
def is_root(self):
"""Returns whether this is the root blip of a wavelet."""
return self._parent_blip_id is None
@property
def annotations(self):
"""The annotations for this document."""
return self._annotations
@property
def elements(self):
"""Returns a list of elements for this document.
The elements of a blip are things like forms elements and gadgets
that cannot be expressed as plain text. In the text of the blip, you'll
typically find a space as a place holder for the element.
If you want to retrieve the element at a particular index in the blip, use
blip[index].value().
"""
return self._elements.values()
def __len__(self):
return len(self._content)
def __getitem__(self, item):
"""returns a BlipRefs for the given slice."""
if isinstance(item, slice):
if item.step:
raise errors.Error('Step not supported for blip slices')
return self.range(item.start, item.stop)
else:
return self.at(item)
def __setitem__(self, item, value):
"""short cut for self.range/at().replace(value)."""
self.__getitem__(item).replace(value)
def __delitem__(self, item):
"""short cut for self.range/at().delete()."""
self.__getitem__(item).delete()
def _shift(self, where, inc):
"""Move element and annotations after 'where' up by 'inc'."""
new_elements = {}
for idx, el in self._elements.items():
if idx >= where:
idx += inc
new_elements[idx] = el
self._elements = new_elements
self._annotations._shift(where, inc)
def _delete_annotations(self, start, end):
"""Delete all annotations between 'start' and 'end'."""
for annotation_name in self._annotations.names():
self._annotations._delete_internal(annotation_name, start, end)
def all(self, findwhat=None, maxres=-1, **restrictions):
"""Returns a BlipRefs object representing all results for the search.
If searching for an element, the restrictions can be used to specify
additional element properties to filter on, like the url of a Gadget.
"""
return BlipRefs.all(self, findwhat, maxres, **restrictions)
def first(self, findwhat=None, **restrictions):
"""Returns a BlipRefs object representing the first result for the search.
If searching for an element, the restrictions can be used to specify
additional element properties to filter on, like the url of a Gadget.
"""
return BlipRefs.all(self, findwhat, 1, **restrictions)
def at(self, index):
"""Returns a BlipRefs object representing a 1-character range."""
return BlipRefs.range(self, index, index + 1)
def range(self, start, end):
"""Returns a BlipRefs object representing the range."""
return BlipRefs.range(self, start, end)
def serialize(self):
"""Return a dictionary representation of this blip ready for json."""
return {'blipId': self._blip_id,
'childBlipIds': list(self._child_blip_ids),
'content': self._content,
'creator': self._creator,
'contributors': list(self._contributors),
'lastModifiedTime': self._last_modified_time,
'version': self._version,
'parentBlipId': self._parent_blip_id,
'waveId': self._wave_id,
'waveletId': self._wavelet_id,
'annotations': self._annotations.serialize(),
'elements': dict([(index, e.serialize())
for index, e in self._elements.items()])
}
def proxy_for(self, proxy_for_id):
"""Return a view on this blip that will proxy for the specified id.
A shallow copy of the current blip is returned with the proxy_for_id
set. Any modifications made to this copy will be done using the
proxy_for_id, i.e. the robot+<proxy_for_id>@appspot.com address will
be used.
"""
util.check_is_valid_proxy_for_id(proxy_for_id)
operation_queue = self._operation_queue.proxy_for(proxy_for_id)
res = Blip(json={},
other_blips={},
operation_queue=operation_queue)
res._blip_id = self._blip_id
res._child_blip_ids = self._child_blip_ids
res._content = self._content
res._contributors = self._contributors
res._creator = self._creator
res._last_modified_time = self._last_modified_time
res._version = self._version
res._parent_blip_id = self._parent_blip_id
res._wave_id = self._wave_id
res._wavelet_id = self._wavelet_id
res._other_blips = self._other_blips
res._annotations = self._annotations
res._elements = self._elements
res.raw_data = self.raw_data
return res
@property
def text(self):
"""Returns the raw text content of this document."""
return self._content
def find(self, what, **restrictions):
"""Iterate to matching bits of contents.
Yield either elements or pieces of text.
"""
br = BlipRefs.all(self, what, **restrictions)
for start, end in br._hits():
if end - start == 1 and start in self._elements:
yield self._elements[start]
else:
yield self._content[start:end]
raise StopIteration
def append(self, what, bundled_annotations=None):
"""Convenience method covering a common pattern."""
return BlipRefs.all(self, findwhat=None).insert_after(
what, bundled_annotations=bundled_annotations)
def continue_thread(self):
"""Create and return a blip in the same thread as this blip."""
blip_data = self._operation_queue.blip_continue_thread(self.wave_id,
self.wavelet_id,
self.blip_id)
new_blip = Blip(blip_data, self._other_blips, self._operation_queue,
thread=self._thread)
if self._thread:
self._thread._add_internal(new_blip)
self._other_blips._add(new_blip)
return new_blip
def reply(self):
"""Create and return a reply to this blip."""
blip_data = self._operation_queue.blip_create_child(self.wave_id,
self.wavelet_id,
self.blip_id)
new_blip = Blip(blip_data, self._other_blips, self._operation_queue)
self._other_blips._add(new_blip)
return new_blip
def append_markup(self, markup):
"""Interpret the markup text as xhtml and append the result to the doc.
Args:
markup: The markup'ed text to append.
"""
markup = util.force_unicode(markup)
self._operation_queue.document_append_markup(self.wave_id,
self.wavelet_id,
self.blip_id,
markup)
self._content += util.parse_markup(markup)
def insert_inline_blip(self, position):
"""Inserts an inline blip into this blip at a specific position.
Args:
position: Position to insert the blip at. This has to be greater than 0.
Returns:
The JSON data of the blip that was created.
"""
if position <= 0:
raise IndexError(('Illegal inline blip position: %d. Position has to ' +
'be greater than 0.') % position)
blip_data = self._operation_queue.document_inline_blip_insert(
self.wave_id,
self.wavelet_id,
self.blip_id,
position)
new_blip = Blip(blip_data, self._other_blips, self._operation_queue)
self._other_blips._add(new_blip)
return new_blip