-
-
Save MaxMorais/4711355b95073e908365652295bdddae to your computer and use it in GitHub Desktop.
Remote DOM in Python
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import json | |
import threading | |
from functools import wraps | |
__all__ = ['Commands', 'Node', 'MessageQueue', 'Pipe', 'Constants', | |
'StyleAttributes', 'SupportedEvents', 'EventDOMNodeAttributes'] | |
def delay(delay=0.): | |
""" | |
Decorator delaying the execution of a function of a while | |
""" | |
def wrap(f): | |
@wraps(f) | |
def delayed(*args, **kwargs): | |
timer = threading.Timer(delay, f, args=args, kwargs=kwargs) | |
timer.start() | |
return timer | |
return delayed | |
return wrap | |
class setterproperty(object): | |
def __init__(self, func, doc=None): | |
self.func = func | |
self.__doc__ = doc if doc is not None else func.__doc__ | |
def __set__(self, obj, value): | |
return self.func(obj, value) | |
Node = type('Node', (object,), { | |
'ELEMENT_NODE': 1, | |
'ATTRIBUTE_NODE': 2, | |
'TEXT_NODE': 3, | |
'CDATA_SECTION_NODE': 4, | |
'ENTITY_REFERENCE_NODE': 5, | |
'ENTITY_NODE': 6, | |
'PROCESSING_INSTRUCTION_NODE': 7, | |
'COMMENT_NODE': 8, | |
'DOCUMENT_NODE': 9, | |
'DOCUMENT_TYPE_NODE': 10, | |
'DOCUMENT_FRAGMENT_NODE': 11, | |
'NOTATION_NODE': 12, | |
'DOCUMENT_POSITION_DISCONNECTED': 1, | |
'DOCUMENT_POSITION_PRECEDING': 2, | |
'DOCUMENT_POSITION_FOLLOWING': 4, | |
'DOCUMENT_POSITION_CONTAINS': 8, | |
'DOCUMENT_POSITION_CONTAINED_BY': 16, | |
'DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC': 32 | |
}) | |
Commands = type('Commands', (object,), { | |
x: x for x in ( | |
'createContainer', | |
'createElement', | |
'createTextNode', | |
'createComment', | |
'createDocumentFragment', | |
'appendChild', | |
'insertBefore', | |
'removeChild', | |
'replaceChild', | |
'setAttribute', | |
'removeAttribute', | |
'setStyles', | |
'setStyle', | |
'innerHTML', | |
'innerText', | |
'textContent', | |
'setValue', | |
'addEventListener', | |
'removeEventListener', | |
'dispatchEvent', | |
'invokeNative', | |
'updateProperties', | |
'initiated', | |
'pause', | |
'play', | |
'src', | |
'focus', | |
'setSelectionRange' | |
) | |
}) | |
Constants = type('Constants', (object,), { | |
x: x for x in ( | |
'REMOTE_DOM', | |
'DOCUMENT', | |
'WINDOW', | |
'DEFAULT_NAME', | |
'QUEUE_INDEX', | |
'NODE_INDEX', | |
'INIT', | |
'EVENT' | |
) | |
}) | |
index = 0 | |
class Pipe(object): | |
def __init__(self, channel, handler): | |
self.channel = channel | |
def message_handler(ev): | |
message = None | |
try: | |
message = json.loads(ev.data) | |
except TypeError: | |
return | |
if Constants.REMOTE_DOM in message: | |
handler(message[Constants.REMOTE_DOM]) | |
self.channel.addEventListener('message', message_handler) | |
def postMessage(self, messages): | |
self.channel.postMessage(json.dumps({Constants.REMOTE_DOM: messages})) | |
class MessagesQueue(object): | |
def __init__(self): | |
self.queue = [] | |
self.index = id(self) | |
self.pipe = None | |
self.timer = None | |
def push(self, message): | |
self.queue.append(message) | |
self.schedule() | |
def setPipe(self, channel, handler, timerFunction = None): | |
self.pipe = Pipe(channel, handler) | |
if timerFunction: | |
self.timerFunction = timerFunction | |
else: | |
def callback(cb): | |
delay()(cb) | |
self.timerFunction = callback | |
def schedule(self): | |
if not self.timer and not self.pipe: | |
return | |
self.timer = self.timerFunction(self.flushQueue) | |
def flushQueue(self): | |
self.timer = None | |
if not self.pipe or not self.queue: | |
return | |
self.pipe.postMessage(self.queue) | |
self.queue = [] | |
EventDOMNodeAttribute = [ | |
'currentTarget', | |
'originalTarget', | |
'srcElement', | |
'target', | |
'toElement', | |
'path', | |
'view' | |
] | |
StyleAttributes = [ | |
'alignContent', | |
'alignItems', | |
'alignSelf', | |
'alignmentBaseline', | |
'all', | |
'animation', | |
'animationDelay', | |
'animationDirection', | |
'animationDuration', | |
'animationFillMode', | |
'animationIterationCount', | |
'animationName', | |
'animationPlayState', | |
'animationTimingFunction', | |
'backfaceVisibility', | |
'background', | |
'backgroundAttachment', | |
'backgroundBlendMode', | |
'backgroundClip', | |
'backgroundColor', | |
'backgroundImage', | |
'backgroundOrigin', | |
'backgroundPosition', | |
'backgroundPositionX', | |
'backgroundPositionY', | |
'backgroundRepeat', | |
'backgroundRepeatX', | |
'backgroundRepeatY', | |
'backgroundSize', | |
'baselineShift', | |
'border', | |
'borderBottom', | |
'borderBottomColor', | |
'borderBottomLeftRadius', | |
'borderBottomRightRadius', | |
'borderBottomStyle', | |
'borderBottomWidth', | |
'borderCollapse', | |
'borderColor', | |
'borderImage', | |
'borderImageOutset', | |
'borderImageRepeat', | |
'borderImageSlice', | |
'borderImageSource', | |
'borderImageWidth', | |
'borderLeft', | |
'borderLeftColor', | |
'borderLeftStyle', | |
'borderLeftWidth', | |
'borderRadius', | |
'borderRight', | |
'borderRightColor', | |
'borderRightStyle', | |
'borderRightWidth', | |
'borderSpacing', | |
'borderStyle', | |
'borderTop', | |
'borderTopColor', | |
'borderTopLeftRadius', | |
'borderTopRightRadius', | |
'borderTopStyle', | |
'borderTopWidth', | |
'borderWidth', | |
'bottom', | |
'boxShadow', | |
'boxSizing', | |
'breakAfter', | |
'breakBefore', | |
'breakInside', | |
'bufferedRendering', | |
'captionSide', | |
'clear', | |
'clip', | |
'clipPath', | |
'clipRule', | |
'color', | |
'colorInterpolation', | |
'colorInterpolationFilters', | |
'colorRendering', | |
'columnCount', | |
'columnFill', | |
'columnGap', | |
'columnRule', | |
'columnRuleColor', | |
'columnRuleStyle', | |
'columnRuleWidth', | |
'columnSpan', | |
'columnWidth', | |
'columns', | |
'contain', | |
'content', | |
'counterIncrement', | |
'counterReset', | |
'cursor', | |
'cx', | |
'cy', | |
'd', | |
'direction', | |
'display', | |
'dominantBaseline', | |
'emptyCells', | |
'fill', | |
'fillOpacity', | |
'fillRule', | |
'filter', | |
'flex', | |
'flexBasis', | |
'flexDirection', | |
'flexFlow', | |
'flexGrow', | |
'flexShrink', | |
'flexWrap', | |
'float', | |
'floodColor', | |
'floodOpacity', | |
'font', | |
'fontFamily', | |
'fontFeatureSettings', | |
'fontKerning', | |
'fontSize', | |
'fontStretch', | |
'fontStyle', | |
'fontVariant', | |
'fontVariantCaps', | |
'fontVariantLigatures', | |
'fontVariantNumeric', | |
'fontWeight', | |
'height', | |
'imageRendering', | |
'isolation', | |
'justifyContent', | |
'left', | |
'letterSpacing', | |
'lightingColor', | |
'lineHeight', | |
'listStyle', | |
'listStyleImage', | |
'listStylePosition', | |
'listStyleType', | |
'margin', | |
'marginBottom', | |
'marginLeft', | |
'marginRight', | |
'marginTop', | |
'marker', | |
'markerEnd', | |
'markerMid', | |
'markerStart', | |
'mask', | |
'maskType', | |
'maxHeight', | |
'maxWidth', | |
'maxZoom', | |
'minHeight', | |
'minWidth', | |
'minZoom', | |
'mixBlendMode', | |
'motion', | |
'motionOffset', | |
'motionPath', | |
'motionRotation', | |
'objectFit', | |
'objectPosition', | |
'opacity', | |
'order', | |
'orientation', | |
'orphans', | |
'outline', | |
'outlineColor', | |
'outlineOffset', | |
'outlineStyle', | |
'outlineWidth', | |
'overflow', | |
'overflowWrap', | |
'overflowX', | |
'overflowY', | |
'padding', | |
'paddingBottom', | |
'paddingLeft', | |
'paddingRight', | |
'paddingTop', | |
'page', | |
'pageBreakAfter', | |
'pageBreakBefore', | |
'pageBreakInside', | |
'paintOrder', | |
'perspective', | |
'perspectiveOrigin', | |
'pointerEvents', | |
'position', | |
'quotes', | |
'r', | |
'resize', | |
'right', | |
'rx', | |
'ry', | |
'shapeImageThreshold', | |
'shapeMargin', | |
'shapeOutside', | |
'shapeRendering', | |
'size', | |
'speak', | |
'src', | |
'stopColor', | |
'stopOpacity', | |
'stroke', | |
'strokeDasharray', | |
'strokeDashoffset', | |
'strokeLinecap', | |
'strokeLinejoin', | |
'strokeMiterlimit', | |
'strokeOpacity', | |
'strokeWidth', | |
'tabSize', | |
'tableLayout', | |
'textAlign', | |
'textAlignLast', | |
'textAnchor', | |
'textCombineUpright', | |
'textDecoration', | |
'textIndent', | |
'textOrientation', | |
'textOverflow', | |
'textRendering', | |
'textShadow', | |
'textSizeAdjust', | |
'textTransform', | |
'top', | |
'touchAction', | |
'transform', | |
'transformOrigin', | |
'transformStyle', | |
'transition', | |
'transitionDelay', | |
'transitionDuration', | |
'transitionProperty', | |
'transitionTimingFunction', | |
'unicodeBidi', | |
'unicodeRange', | |
'userSelect', | |
'userZoom', | |
'vectorEffect', | |
'verticalAlign', | |
'visibility', | |
'webkitAppRegion', | |
'webkitAppearance', | |
'webkitBackgroundClip', | |
'webkitBackgroundOrigin', | |
'webkitBorderAfter', | |
'webkitBorderAfterColor', | |
'webkitBorderAfterStyle', | |
'webkitBorderAfterWidth', | |
'webkitBorderBefore', | |
'webkitBorderBeforeColor', | |
'webkitBorderBeforeStyle', | |
'webkitBorderBeforeWidth', | |
'webkitBorderEnd', | |
'webkitBorderEndColor', | |
'webkitBorderEndStyle', | |
'webkitBorderEndWidth', | |
'webkitBorderHorizontalSpacing', | |
'webkitBorderImage', | |
'webkitBorderStart', | |
'webkitBorderStartColor', | |
'webkitBorderStartStyle', | |
'webkitBorderStartWidth', | |
'webkitBorderVerticalSpacing', | |
'webkitBoxAlign', | |
'webkitBoxDecorationBreak', | |
'webkitBoxDirection', | |
'webkitBoxFlex', | |
'webkitBoxFlexGroup', | |
'webkitBoxLines', | |
'webkitBoxOrdinalGroup', | |
'webkitBoxOrient', | |
'webkitBoxPack', | |
'webkitBoxReflect', | |
'webkitClipPath', | |
'webkitColumnBreakAfter', | |
'webkitColumnBreakBefore', | |
'webkitColumnBreakInside', | |
'webkitFontSizeDelta', | |
'webkitFontSmoothing', | |
'webkitHighlight', | |
'webkitHyphenateCharacter', | |
'webkitLineBreak', | |
'webkitLineClamp', | |
'webkitLocale', | |
'webkitLogicalHeight', | |
'webkitLogicalWidth', | |
'webkitMarginAfter', | |
'webkitMarginAfterCollapse', | |
'webkitMarginBefore', | |
'webkitMarginBeforeCollapse', | |
'webkitMarginBottomCollapse', | |
'webkitMarginCollapse', | |
'webkitMarginEnd', | |
'webkitMarginStart', | |
'webkitMarginTopCollapse', | |
'webkitMask', | |
'webkitMaskBoxImage', | |
'webkitMaskBoxImageOutset', | |
'webkitMaskBoxImageRepeat', | |
'webkitMaskBoxImageSlice', | |
'webkitMaskBoxImageSource', | |
'webkitMaskBoxImageWidth', | |
'webkitMaskClip', | |
'webkitMaskComposite', | |
'webkitMaskImage', | |
'webkitMaskOrigin', | |
'webkitMaskPosition', | |
'webkitMaskPositionX', | |
'webkitMaskPositionY', | |
'webkitMaskRepeat', | |
'webkitMaskRepeatX', | |
'webkitMaskRepeatY', | |
'webkitMaskSize', | |
'webkitMaxLogicalHeight', | |
'webkitMaxLogicalWidth', | |
'webkitMinLogicalHeight', | |
'webkitMinLogicalWidth', | |
'webkitPaddingAfter', | |
'webkitPaddingBefore', | |
'webkitPaddingEnd', | |
'webkitPaddingStart', | |
'webkitPerspectiveOriginX', | |
'webkitPerspectiveOriginY', | |
'webkitPrintColorAdjust', | |
'webkitRtlOrdering', | |
'webkitRubyPosition', | |
'webkitTapHighlightColor', | |
'webkitTextCombine', | |
'webkitTextDecorationsInEffect', | |
'webkitTextEmphasis', | |
'webkitTextEmphasisColor', | |
'webkitTextEmphasisPosition', | |
'webkitTextEmphasisStyle', | |
'webkitTextFillColor', | |
'webkitTextOrientation', | |
'webkitTextSecurity', | |
'webkitTextStroke', | |
'webkitTextStrokeColor', | |
'webkitTextStrokeWidth', | |
'webkitTransformOriginX', | |
'webkitTransformOriginY', | |
'webkitTransformOriginZ', | |
'webkitUserDrag', | |
'webkitUserModify', | |
'webkitWritingMode', | |
'whiteSpace', | |
'widows', | |
'width', | |
'willChange', | |
'wordBreak', | |
'wordSpacing', | |
'wordWrap', | |
'writingMode', | |
'x', | |
'y', | |
'zIndex', | |
'zoom' | |
] | |
SupportedEvents = ['onreadystatechange', | |
'onpointerlockchange', | |
'onpointerlockerror', | |
'onbeforecopy', | |
'onbeforecut', | |
'onbeforepaste', | |
'oncopy', | |
'oncut', | |
'onpaste', | |
'onsearch', | |
'onselectionchange', | |
'onselectstart', | |
'onwheel', | |
'onwebkitfullscreenchange', | |
'onwebkitfullscreenerror', | |
'onabort', | |
'onblur', | |
'oncancel', | |
'oncanplay', | |
'oncanplaythrough', | |
'onchange', | |
'onclick', | |
'onclose', | |
'oncontextmenu', | |
'oncuechange', | |
'ondblclick', | |
'ondrag', | |
'ondragend', | |
'ondragenter', | |
'ondragleave', | |
'ondragover', | |
'ondragstart', | |
'ondrop', | |
'ondurationchange', | |
'onemptied', | |
'onended', | |
'onerror', | |
'onfocus', | |
'oninput', | |
'oninvalid', | |
'onkeydown', | |
'onkeypress', | |
'onkeyup', | |
'onload', | |
'onloadeddata', | |
'onloadedmetadata', | |
'onloadstart', | |
'onmousedown', | |
'onmouseenter', | |
'onmouseleave', | |
'onmousemove', | |
'onmouseout', | |
'onmouseover', | |
'onmouseup', | |
'onmousewheel', | |
'onpause', | |
'onplay', | |
'onplaying', | |
'onprogress', | |
'onratechange', | |
'onreset', | |
'onresize', | |
'onscroll', | |
'onseeked', | |
'onseeking', | |
'onselect', | |
'onshow', | |
'onstalled', | |
'onsubmit', | |
'onsuspend', | |
'ontimeupdate', | |
'ontoggle', | |
'onvolumechange', | |
'onwaiting' | |
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from functools import partial | |
from collections import defaultdict | |
from common import ( | |
Node, Commands, Constants, MessagesQueue, StyleAttributes, | |
EventDOMNodeAttribute, SupportedEvents, setterproperty, index | |
) | |
__all__ = ['window', 'document', 'populateGlobalScope', 'createContainer', | |
'setChannel'] | |
def tree(): return defaultdict(tree) | |
queue = MessagesQueue() | |
eventsByTypeAndTarget = tree() | |
connectedElementsByIndex = {} | |
index = 0 | |
INLINE_EVENT = 'INLINE_EVENT' | |
def addToQueue(command, host, *args): | |
queue.push([command] + list(args) + [host]) | |
def staticpartial(func, *args, **keywords): | |
return staticmethod(partial(func, *args, **keywords)) | |
class MetaRemoteStyle(type): | |
def __new__(cls, name, bases, attrs): | |
for k in StyleAttributes: | |
def setter(self, val): | |
addToQueue(Commands.setStyle, self.host, [self._index, k, val]) | |
self[k] = val | |
setter.__name__ = k | |
def getter(self): | |
return self.get(k) | |
getter.__name__ = k | |
attrs[k] = property(setter, getter) | |
return type.__new__(cls, name, bases, attrs) | |
class RemoteStyle(dict): | |
__metaclass__ = MetaRemoteStyle | |
def __init__(self, index, values=None): | |
self._index = index | |
class RemoteNodeInternal(object): | |
def __init__(self, nodeType, val): | |
self.nodeType = nodeType | |
self._index = id(self) | |
self._host = None | |
self.parentNode = None | |
self.childNodes = [] | |
self._value = val | |
def __repr__(self): | |
return f'<{self.__class__.__name__}(nodeType={self.nodeType!r}, index:{self._index!r})>' | |
toString = __repr__ | |
@property | |
def children(self): | |
return self.childNodes | |
@property | |
def firstChild(self): | |
return self.childNodes[0] if self.childNodes else None | |
@property | |
def nextSibling(self): | |
if not self.parentNode: | |
return None | |
elif not self.parentNode.childNodes: | |
return None | |
elif self not in self.parentNode.childNodes: | |
return None | |
idx = self.parentNode.childNodes.index(self) | |
if idx == len(self.parentNode.childNodes) - 1: | |
return None | |
return self.parentNode.childNodes[idx + 1] | |
@property | |
def prevSibling(self): | |
if not self.parentNode: | |
return None | |
elif not self.parentNode.childNodes: | |
return None | |
elif self not in self.parentNode.childNodes: | |
return None | |
idx = self.parentNode.childNodes.index(self) | |
if idx == len(self.parentNode.childNodes) - 1: | |
return None | |
return self.parentNode.childNodes[idx + 1] | |
@property | |
def owerDocument(self): | |
return NotImplemented | |
@setterproperty | |
def host(self, val): | |
if bool(val) == bool(self._host): | |
return | |
self._host = val | |
if val: | |
connectedElementsByIndex[self._index] = self | |
else: | |
del connectedElementsByIndex[self._index] | |
for child in self.childNodes: | |
child.host = val | |
def appendChild(self, child): | |
addToQueue(Commands.appendChild, self._host, [self._index, child._index]) | |
childrenToAppend = child.childNodes \ | |
if child.nodeType == Node.DOCUMENT_FRAGMENT_NODE else [child] | |
self.childNodes.extend(childrenToAppend) | |
for childNode in childrenToAppend: | |
childNode.parentNode = self | |
childNode.host = self._host | |
return child | |
def insertBefore(self, child, refChild=None): | |
if not child: | |
raise ValueError('No child') | |
try: | |
idx = self.childNodes.index(refChild) | |
except ValueError: | |
raise ValueError("Failed to execute 'insertBefore' on 'Node': " | |
"The node before which the new node is to be inserted is not " | |
"a child of this node.") | |
addToQueue(Commands.insertBefore, self._host, [self._index, | |
child._index, refChild._index if refChild else None ]) | |
childreToInsert = child.childNodes \ | |
if child.nodeType == Node.DOCUMENT_FRAGMENT_NODE else [child] | |
for childNode in childreToInsert: | |
self.childNodes.insert(idx, child) | |
childNode.parentNode = self | |
childNode.host = self._host | |
idx += 1 | |
return child | |
def removeChild(self, child): | |
addToQueue(Commands.removeChild, self._host, [self._index, child._index]) | |
if child in self.childNodes: | |
self.childNodes.remove(child) | |
child.host = None | |
def replaceChild(self, newChild, oldChild): | |
if oldChild not in self.childNodes: | |
raise ValueError("Faile to execute 'replaceChild' on 'Node': " | |
"The node to be replaced is not child of this node.") | |
addToQueue(Commands.replaceChild, self._host, [self._index, | |
newChild._index, oldChild._index]) | |
childrenToInsert = newChild.childNodes \ | |
if newChild.nodeType == Node.DOCUMENT_FRAGMENT_NODE else [newChild] | |
idx = self.childNodes.index(oldChild) | |
self.childNodes.remove(oldChild) | |
for childNode in childrenToInsert: | |
self.childNodes.insert(idx, childNode) | |
childNode.parentNode = self | |
childNode.host = self._host | |
oldChild.parentNode = None | |
oldChild.host = None | |
def addEventListener(self, evtType, callback=None, capture=False): | |
if callback is None: | |
return partial(self.addEventListener, evtType, capture=capture) | |
addEventListener(self._index, self._host, evtType, callback, capture) | |
def removeEventListener(self, eventType, callback): | |
if callback is None: | |
return partial(self.addEventListener, eventType) | |
removeEventListener(self._index, self._host, eventType, callback) | |
def dispatchEvent(self, event): | |
dispatchEvent(self._index, self._host, event) | |
@property | |
def value(self): | |
return self._value | |
@value.setter | |
def value(self, val): | |
self._value = val | |
addToQueue(Commands.setValue, self._host, [self._index, val]) | |
@setterproperty | |
def textContent(self, val): | |
if len(self.childNodes) != 1 \ | |
or self.childNodes[0].nodeType != Node.TEXT_NODE: | |
childTextNode = RemoteTextualNode(val) | |
childTextNode.parentNode = self | |
childTextNode._host = self._host | |
self.childNodes = [childTextNode] | |
addToQueue(Commands.textContent, self._host, [self._index, val, | |
self.childNodes[0]._index]) | |
def invokeNative(self, name, args): | |
addToQueue(Commands.invokeNative, self._host, [self._index, name, args]) | |
class MetaRemoteNode(type): | |
def __new__(cls, name, bases, attrs): | |
for evtType in SupportedEvents: | |
def getter(self): | |
return self._eventHandlers.get(evtType) | |
def setter(self, evtHandler): | |
self._eventHandlers[evtType] = evtHandler | |
setEventListener(self._index, self._host, evtType[2:], | |
evtHandler) | |
attrs[evtType] = property(getter, setter) | |
return type.__new__(cls, name, bases, attrs) | |
class RemoteNode(RemoteNodeInternal): | |
__metaclass__ = MetaRemoteNode | |
def __init__(self, nodeType, val=None): | |
super().__init__(nodeType, val) | |
self._eventHandlers = {} | |
class BaseRemoteTextualNode(RemoteNode): | |
def __init__(self, nodeType, text): | |
super().__init__(nodeType, text) | |
self._value = text | |
@property | |
def nodeValue(self): | |
return self._value | |
@nodeValue.setter | |
def nodeValue(self, val): | |
self._value = val | |
addToQueue(Commands.textContent, self._host, [self._index, val]) | |
class RemoteTextualNode(BaseRemoteTextualNode): | |
def __init__(self, text): | |
super().__init__(Node.TEXT_NODE, text) | |
class RemoteComment(BaseRemoteTextualNode): | |
def __init__(self, text): | |
super().__init__(Node.TEXT_COMMENT, text) | |
class RemoteText(BaseRemoteTextualNode): | |
def __init__(self, text): | |
super().__init__(Node.TEXT_NODE, text) | |
class RemoteElement(RemoteNode): | |
def __init__(self, tagName): | |
super().__init__(Node.ELEMENT_NODE) | |
self.tagName = tagName.upper() | |
self._style = RemoteStyle(self._index) | |
self._attr = {} | |
@property | |
def nodeName(self): | |
return self.tagName | |
def setAttribute(self, k, v): | |
addToQueue(Commands.setAttribute, self._host, [self._index, k, v]) | |
self._attr[k] = v | |
def removeAttribute(self, k): | |
addToQueue(Commands.removeAttribute, self._host, [self._index, k]) | |
self._attr.pop(k, None) | |
def hasAttribute(self, k): | |
return k in self._attr | |
def focus(self): | |
addToQueue(Commands.focus, self._host, [self._index]) | |
def setSelectionRange(self, selectionStart, selectionEnd, selectionDirection): | |
addToQueue(Commands.setSelectionRange, self._host, [self._index, | |
selectionStart, selectionEnd, selectionDirection]) | |
@property | |
def style(self): | |
return self._style | |
@style.setter | |
def style(self, val): | |
addToQueue(Commands.setStyle, self._host, [self._index, val]) | |
def innerHTML(self, val): | |
addToQueue(Commands.innerHTML, self._host, [self._index, val]) | |
self._innerHTML = val | |
def innerText(self, val): | |
addToQueue(Commands.innerText, self._host, [self._index, val]) | |
class RemoteContainer(RemoteElement): | |
def __init__(self): | |
super(RemoteContainer, self).__init__('div') | |
self._host = self._index | |
class RemoteFragment(RemoteNode): | |
def __init__(self): | |
super(RemoteFragment, self).__init__(Node.DOCUMENT_FRAGMENT_NODE) | |
class RemoteVideo(RemoteElement): | |
def __init__(self): | |
super(RemoteVideo, self).__init__('video') | |
self._src = None | |
def pause(self): | |
addToQueue(Commands.pause, self._host, [self._index]) | |
def play(self): | |
addToQueue(Commands.play, self._host, [self._index]) | |
@property | |
def src(self): | |
return self._src | |
@src.setter | |
def src(self, value): | |
self._src = value | |
addToQueue(Commands.src, self._host, [self._index, value]) | |
class RemoteImage(RemoteElement): | |
def __init__(self): | |
super(RemoteImage, self).__init__('img') | |
self._src = None | |
@property | |
def src(self): | |
return self._src | |
@src.setter | |
def src(self, value): | |
self._src = value | |
addToQueue(Commands.src, self._host, [self._index, value]) | |
class RemoteInput(RemoteElement): | |
def __init__(self): | |
super(RemoteInput, self).__init__('input') | |
@property | |
def value(self): | |
return self._value | |
@value.setter | |
def value(self, val): | |
self._value = val | |
addToQueue(Commands.setValue, self._host, [self._index, val]) | |
class RemoteSelect(RemoteElement): | |
def __init__(self): | |
super(RemoteSelect, self).__init__('select') | |
@property | |
def options(self): | |
return filter(lambda c: c.tagName.lower() == 'option', self.childNodes) | |
def createElement(nodeName): | |
if nodeName == 'video': | |
res = RemoteVideo() | |
elif nodeName == 'img': | |
res = RemoteImage() | |
elif nodeName == 'input': | |
res = RemoteInput() | |
elif nodeName == 'select': | |
res = RemoteSelect() | |
else: | |
res = RemoteElement(nodeName) | |
addToQueue(Commands.createElement, res._host, [res._index, res.tagName]) | |
return res | |
def createTextNode(text): | |
res = RemoteText(text) | |
addToQueue(Commands.createTextNode, res._host, [res._index, text]) | |
return res | |
def createComment(text): | |
res = RemoteFragment() | |
addToQueue(Commands.createDocumentFragment, res._host, [res._index]) | |
return res | |
def createDocumentFragment(): | |
res = RemoteFragment() | |
addToQueue(Commands.createDocumentFragment, res._host, [res._index]) | |
return res | |
def createContainer(name): | |
name = name or Constants.DEFAULT_NAME | |
res = RemoteContainer() | |
connectedElementsByIndex[res._index] = res | |
addToQueue(Commands.createContainer, res._host, [res._index, name]) | |
return res | |
def addEventListener(target, host, evtName, callback, capture): | |
index += 1 | |
eventsByTypeAndTarget[evtName][target][index] = callback | |
addToQueue(Commands.addEventListener, host, [target, evtName, index, capture]) | |
def removeEventListener(target, host, evtName, callback): | |
evts = eventsByTypeAndTarget[evtName][target] | |
for k, v in evts.items(): | |
if v == callback: | |
idx = k | |
break | |
del evts[idx] | |
addToQueue(Commands.removeEventListener, host, [target, evtName, index]) | |
def setEventListener(target, host, evtName, evtHandler): | |
if evtHandler and INLINE_EVENT not in eventsByTypeAndTarget[evtName][target]: | |
addToQueue(Commands.addEventListener, host, [target, evtName, | |
INLINE_EVENT, False]) | |
elif not evtHandler and INLINE_EVENT in eventsByTypeAndTarget[evtName][target]: | |
addToQueue(Commands.removeEventListener, host, [target, evtName, INLINE_EVENT]) | |
eventsByTypeAndTarget[evtName][target][INLINE_EVENT] = evtHandler | |
def dispatchEvent(target, host, event): | |
addToQueue(Commands.dispatchEvent, host, [target, event._type, | |
event._eventInit, getattr(event, '_isCustom', False)]) | |
def updateConnectedElement(index, eventData): | |
if index in connectedElementsByIndex and index in eventData: | |
connectedElementsByIndex[index].update(eventData[index]) | |
def handleMessagesFromPipe(messages): | |
for msg in messages: | |
evtIntent = msg[0] | |
if evtIntent == Constants.INIT: | |
#InitMsgPromiseResolver and initMsgPromiseResolver() | |
eventData = msg[1] | |
for index in eventData: | |
updateConnectedElement(index, eventData) | |
else: | |
evtTarget = msg[1] | |
evtName = msg[2] | |
evtJSON = msg[3] | |
if 'extraData' in evtJSON: | |
for index in evtJSON['extraData']: | |
updateConnectedElement(index, evtJSON['extraData']) | |
for field in EventDOMNodeAttribute: | |
if field in evtJSON: | |
if isinstance(evtJSON[field], list): | |
evtJSON[field] = map( | |
lambda val: connectedElementsByIndex[val], | |
evtJSON[field]) | |
else: | |
evtJSON[field] = connectedElementsByIndex[evtJSON[field]] | |
if evtName in eventsByTypeAndTarget \ | |
and evtTarget in eventsByTypeAndTarget[evtName]: | |
for callback in eventsByTypeAndTarget[evtName][evtTarget].values(): | |
callback(evtJSON) | |
def setChannel(channel, timerFunction=None): | |
#initMsgPromise = Promise(lambda resolve: globals().update({'initMsgPromiseResolver': resolve})) | |
queue.setPipe(channel, handleMessagesFromPipe, timerFunction) | |
queue.push([Commands.initiated]) | |
#return initMsgPromise | |
def Image(width, height): | |
imgNode = document.createElement('img') | |
imgNode.setAttribute('width', width) | |
imgNode.setAttribute('height', height) | |
return imgNode | |
def populateGlobalScope(scope): | |
scope.window = window | |
scope.document = document | |
scope.requestAnimationFrame = window.requestAnimationFrame | |
scope.Image = Image | |
document = type('document', (object,), dict(list(dict({ | |
'createElement': staticmethod(createElement), | |
'createTextNode': staticmethod(createTextNode), | |
'createComment': staticmethod(createComment), | |
'createDocumentFragment': staticmethod(createDocumentFragment), | |
'addEventListener': staticpartial( | |
addEventListener, target=Constants.DOCUMENT, host=None), | |
'removeEventListener': staticpartial( | |
removeEventListener, target=Constants.DOCUMENT, host=None), | |
'documentElement': RemoteElement('html'), | |
'dispatchEvent': staticpartial( | |
dispatchEvent, target=Constants.DOCUMENT, host=None) | |
}).items()) + [ | |
(k, staticpartial( | |
addEventListener, target=Constants.DOCUMENT, | |
host=None, evtName=k[2:])) | |
for k in SupportedEvents]) | |
) | |
class Event(object): | |
def __init__(self, typeArg, eventInit): | |
self._type = typeArg | |
self._eventInit = eventInit | |
class CustomEvent(Event): | |
def __init__(self, typeArg, eventInit): | |
super(CustomEvent, self).__init__(typeArg, eventInit) | |
self._isCustom = True | |
window = type('window', (object,), { | |
'Event': Event, | |
'CustomEvent': CustomEvent, | |
'dispatchEvent': staticpartial( | |
dispatchEvent, target=Constants.WINDOW, host=None), | |
'addEventListener': staticpartial( | |
dispatchEvent, target=Constants.WINDOW, host=None), | |
'removeEventListener': staticpartial( | |
removeEventListener, target=Constants.WINDOW, host=None), | |
'document': document, | |
'location': type('location', (object,), { | |
'href': 'https://localhost', | |
'protocol': 'https:' | |
}), | |
'navigator': type('navigator', (object,), { | |
'userAgent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36', | |
}), | |
'history': {}, | |
#'requestAnimationFrame': setTimeout, | |
#'cancelAnimationFrame': clearTimeout, | |
'Image': Image, | |
'top': None | |
}) | |
window.top = window | |
connectedElementsByIndex[Constants.WINDOW] = window | |
connectedElementsByIndex[Constants.DOCUMENT] = document | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment