Last week I started working on some killer-feature for Splash. It will allow you to write Lua scripts using almost the same Element (Node, HTMLElement) API as in JavaScript plus some additional helpful methods.

For example, you want to save the screenshot of the image when it will be loaded. Here is the script for it:

function main(splash) {
    assert(splash:go(splash.args.url))
    assert(splash:wait(1))
    
    local shots = {}
    
    local element = splash:select('#myImage') -- selecting the element by its CSS selector
    element.onload = function(event)          -- ataching the event listener
       event:preventDefault()
       table.insert(shots, element:png())     -- making a screenshot of the element
    end 
    
    return shots
end

The Element API is still in development and can be changed

JS <-> PyQt <-> Python <-> Lua

Let’s see how the communication between JS and Lua is implemented. Imagine that we are going to execute the following Lua code:

element:click()

Lua

splash is a table which has metatable and prototype Splash. In Lua it means that splash is an instance of Splash class. click method is wrapped into several Lua functions. After executing those function, we eventually will call the click Python method. This is possible because of [Lupa] runtime for Lua which allows to inject Python methods into Lua code.

Python

click is a method of _ExposedElement Python class which contains all the methods and properties which can be accessed in Lua. It binds Python functions with Lua functions.

Let’s return to our click method. It do the following procedure when it’s called:

  • calls private_node_method passing the "click" string which means that we want to call the click method of our JavaScript DOM element
  • private_node_methodis another method _ExposedElement and it calls the node_method method of self.element object which is an instance of HTMLElement class;
  • HTMLElement is a class which have API for communicating with the JavaScript HTMLElement
  • HTMLElement#node_method calls PyQt method evaluateJavaScript() with the following JS code:
window[elements_storage][element_id]["click"]()
  • Description
    • elements_storage is our elements storage which is a PyQT object; it allows us to save DOM elements for the further access
    • element_id is a unique ID which allows us to identify our element object
    • "click" is a method name which want to call (in this case it is “click”)

The elements storage is added to the JS window object using the addToJavaScriptWindowObject method of PyQt.

So, our Python self.element is connected to the JS node using the element_id.

PyQt

PyQt allows us to have WebKit runtime environment in our Python application. Using addToJavaScriptWindowObject we can add instances of QObject to the JS window object. Thereby it will allow us to call Python methods in JS.

JS

In JS our node can be accessed through window[storage_name][element_id] object.

This flow was OK for the one direction: from Lua to JS. But what if want to call Lua function from JS? That can happen when we assign an event handler for some event. In our first example we’ve assigned an event handler for the load event.

JS -> Lua

Let’s examine this code:

element.onload = function(event)
   event:preventDefault()
   table.insert(shots, element:png()) 
end 

We assign an event handler for load event of our element. How it’s working?

  1. When onload property of element is accessed it calls the __newindex metamethod of element.
  2. This metamethod checks whether the requested property has the'on' prefix. If it does, we calls the private method set_event_handler of element.
  3. In its turn set_event_handler calls Python method private_set_event_handler of _ExposedElement passing the event name for which we want to assign a handler, and the reference to handler function itself.
  4. The crazy parts start here. We wrap our Lua function in Lua coroutine which will allow us to execute it when the event will be fired.
  5. We pass that coroutine to set_event_handler method of HTMLElement Python class.
  6. It saves that coroutine and in another storage which is called event handlers storage returns its ID .
  7. Using PyQt evaluateJavaScript() method we execute the following JS code:
window[elements_storage][element_id].onload = function(event) {
    window[event_handlers_storage].run(
        event_handler_id, 
        window[events_storage].add(event),
    )
}

You may think: what does window[event_handlers_storage].run do and what is window[events_storage]?

  • window[event_handlers_storage].run
    • it calls our event handlers storage (which was injected in the same way as elements storage) run method;
    • that method, using the specified event_handler_id, calls the saved coroutine;
    • that coroutine will call our Lua function that was assigned to onload property of our element Lua table;
  • window[events_storage]
    • it’s another storage, but now for events;
    • the main reason for it is calling few methods of our event (preventDefault, stopPropagation, etc).

As you can see, in order to access our DOM element we should go through all the layers until we reach to JS.

The following days I will finish up writing tests and documentation for the newly created Element object. Also I will try to refactor those classes and methods to make the Lua <-> JS path more simple.