Writing Konqueror-Plugins with Ruby

Hi!

You may not know about it: But you can write Konqueror-plugins, and you can use a scripting-language like Ruby. KDE/KParts/Konqueror have a very nice plugin-system, you can write plugins using C++, but you can also use a scripting-language like Ruby or Python. It should even work with Perl (untested), unfortunately for KJS and QtScript there is no plugin-factory implementing the support. Well, let us have a look at a simple plugin written in Ruby. There are four important files:

A desktop-file containing some meta-data

It works like a normal plugin-desktop-file (see e.g. /usr/share/kde4/apps/khtml/kpartplugins/plugin_adblock.desktop), but you need two special lines:

X-KDE-Library=krubypluginfactory
X-KDE-PluginKeyword=MyPlugin/MyPlugin.rb

Well, X-KDE-PluginKeyword is a not very expressive name for the name of the script to use (because it may be used for other stuff as well), but what is krubypluginfactory? It is a very cool invention, like the usual KDE-plugin-factories it instantiates plugin-objects, but this one instantiates special objects, were virtual method- and slot-invocations will be delegated to Ruby.

As usual you can use X-KDE-ParentApp (in our case konqueror) to tell KParts not to load the plugin in other applications using the KPart.

A .rc-GUI-XMl-file

You may have heard about KXmlGui – you need it to define actions in the menubar etc., but you will even need it if you do not access any GUI-elements, otherwise Konqueror (or any application using KParts-plugins) will not find it:

<!DOCTYPE kpartgui>
<kpartplugin name="MyPlugin" library="krubypluginfactory" version="0.1">
<MenuBar>
  <Menu name="tools"><text>&amp;Tools</text>
    <separator group="tools_operations" />
    <Action name="tools_MyPlugin" group="tools_operations" />
  </Menu>
</MenuBar>
</kpartplugin>

A CMake-file (CMakeLists.txt)

project(myplugin)
find_package(KDE4 REQUIRED)
include (KDE4Defaults)
install(PROGRAMS MyPlugin.rb DESTINATION ${DATA_INSTALL_DIR}/MyPlugin)
install(FILES myplugin.rc myplugin.desktop DESTINATION ${DATA_INSTALL_DIR}/khtml/kpartplugins)

That one is really trivial, some elementary checks, then it will be installed to the correct directories, Ruby is an interpreted language. ;)

The Ruby-Code

The name of the Ruby-class has to match the filename, otherwise KRubyPluginFactory will not be able to find it (KPythonPluginFactory works differently, there you have to create a separate function to instantiate the plugin-object). Like normal plugins they have to inherit KParts::Plugin, the constructor can take a parent-object (that is the KParts::Part-instance), a parent-widget and a list of arguments (which will usually be empty). There it can initialize the actions declared in the XML-file and add signal-slot-connections such that something will happen. What should happen in my example-plugin? It will find bad-links to sites of evil companies (Microsoft, Apple, SAP, Oracle, Facebook, Google, but the list can be easily extended to your own moral standards :D), and when it finds such a link, it will get angry and wil replace all the links on the website with good links, e.g. to Planet KDE, GNU, Wikipedia or the-user.org. :D That for it uses the DOM-API exposed by KHTML.

#!/usr/bin/env ruby
# Qt
require 'Qt'
# KDE-libraries
require 'korundum4'
# KHTML
require 'khtml'
# Ruby-CGI-library (for url-handling)
require 'cgi'
 
include KDE
include Qt
 
module MyPlugin # has to match the directory-name
 
class MyPlugin < KParts::Plugin # has to match the file-name
  # Slot-Declarations look that way, “a bit” C++-ish
  slots 'toggle(bool)', 'hoverUrl(const QString&)'
  def initialize(parent, parentWidget, args)
    super(parent)
 
    if !parent.is_a? KDE::HTMLPart
      qWarning("MyPlugin: Not a KHTML-Part")
      return
    end
    @part = parent
 
    #
    @action = Qt::Action.new("Enable bad link detection", self)
    @action.checkable = true
    # the action declared in the XML-file
    actionCollection().addAction("tools_MyPlugin", @action)
 
    # unfortunately you have to provide parameter lists
    connect(@action, SIGNAL('toggled(bool)'),
            self,    SLOT('toggle(bool)'))
 
    # good links
    @favLinks = ["http://planetkde.org",
                 "http://the-user.org",
                 "http://gnu.org",
                 "http://en.wikipedia.org"]
    # bad links
    @badLinks = ["microsoft", "apple", "sap",
                 "oracle", "facebook", "google"]
  end
  def toggle(active)
    if active
      connect(@part, SIGNAL('onURL(const QString&)'),
              self,  SLOT('hoverUrl(const QString&)'))
 
      # normal Qt API
      Qt::MessageBox::information(nil, "Success!",
          "Now you are safe, you will only visit good sites!")
    else
      disconnect(@part, SIGNAL('onURL(const QString&)'),
                 self,  SLOT('hoverUrl(const QString&)'))
      Qt::MessageBox::information(nil, "Success!",
          "Bad links will no longer be removed")
    end
  end
  # replace all the links recursively using the DOM-API
  def replaceLinks(dom)
    href = DOM::DOMString.new("href")
    if     dom.nodeName.string == "A"
        && dom.attributes.getNamedItem(href) != nil
      oldUrl = dom.attributes.getNamedItem(href).nodeValue.string
      newUrl =   @favLinks[rand(@favLinks.length)]
               + "/#" + CGI::escape(oldUrl)
      dom.attributes.getNamedItem(href).nodeValue
             = DOM::DOMString.new(newUrl)
    end
 
    # recursion
    children = dom.childNodes
    for i in 0..(children.length-1)
      child = children.item(i)
      replaceLinks(child)
    end
  end
  # slot invoked when hovering a link
  def hoverUrl(url)
    # this will happen when you move the cursor away
    return if url == nil
 
    found = false
    for badLink in @badLinks
      pos = url.index(badLink)
      sharpIndex = url.index("#")
      if pos != nil && (sharpIndex == nil || pos < sharpIndex)
        found = true
        break
      end
    end
 
    # maybe the link is not evil
    return unless found
 
    # replace all the links
    replaceLinks(@part.document())
  end
end
 
end

You may have noticed it: It is not that hard to write a Konqueror-plugin in a scripting-language like Ruby, it is not perfect, sometimes debugging-output is bad, sometimes you need C++-style-stuff, but that is handable without knowing anything about C++. And there is unfortunately no KHotNewStuff-integration. In theory it should work with KWebKit the same way (those techniques work for all applications using the KPluginFactory for creating their plugins in theory, and of course for all KParts), but unfortunately there is no Ruby-binding for KWebKit, so the Ruby-script will be invoked, but Ruby will not recognize that it is a KWebKitPart, it will be just a KParts::ReadOnlyPart, you can access KWebKitPart’s overridden virtual functions, its signals and slots and its meta-object, but the most important method (KWebKitPart::view()) returning the QWebView, which can be used to manipulate all the stuff, will not be accessible. It would require some confguration-files to build Smoke and Korundum with KWebKit support.

Have fun! Maybe somebody will write a useful plugin. ;)

4 Responses to “Writing Konqueror-Plugins with Ruby”

  1. Stefan Says:

    Re the cmake stuff: include, include_directories, add_definitions are not needed because you don’t compile anything.

  2. The User Says:

    Well, the KDE4Defaults may be useful, e.g. creating a handbook. But, yes, include_directories and add_definitions are unimportant for Ruby.

  3. whilo Says:

    I really miss two plugins for Konqueror: noscript and flashgot. Others might miss different plugins compared to Firefox, but these two keep Firefox as my default browser, although Konqui still feels slicker to me. The problem, which you again pointed out, is imo that webkit-kpart is hardly polished. I can only speculate why rekonq progresses quicker than konqueror, although konqueror is already a very strong browser, but the bad webkit support (in this case no bindings for plugins), keeps konqueror out of the game imo. Very sad.

  4. The User Says:

    Well, Rekonq address-bar would be nice in Konqueror. And unfortunately KWebKitPart and Rekonq seem not to work together.

Leave a Reply

XHTML: Use <blockquote cite="name"> for quotations, <pre lang="text    ∨ cpp-qt ∨ cpp ∨ bash ∨ other language"> for code, [latex] for formulas and <em> for em. Contact me if the comment does not get published, it may have accidentally been marked as spam.

Anti-Spam Quiz: