Creating a Wiki with UCW

UCW Style 

UCW styled apps, unlike regular style apps, start with components and actions. Only later do we tie components and actions to urls and pages.


All of our wiki components are going to subclass wiki-manipulator. This class provides the page-name slot and the method on update-url.

(defcomponent wiki-manipulator ()
  ((page-name :accessor page-name
              :initarg :page-name
              :backtrack t)))

We define a method on update-url so that bookmarking this page, or requesting it any time after the session has expired, will view the current page (even if the user was editing it when he created the bookmark). Note that there is nothing automatic about this update-url method. It only works because we know that wiki.ucw?page-name=Foo will show the page named Foo. Embedding the wiki in another application would probably make this assumption untrue and require a different update-url method.

(defmethod update-url ((component wiki-manipulator) url)
  (setf (ucw::uri.path url) "wiki.ucw")
  (push (cons "page-name" (page-name component)) (ucw::uri.query url))


The wiki-viewer component shows a page of the wiki. In particular the component show the html version of the page named by the value of the viewer's page-name slot. Unlike the wiki-editor component a user will use the same wiki-viewer component during the entire course of their browsing.

(defcomponent wiki-viewer (wiki-manipulator)

We split the text of the page into StudlyWords and non StudlyWords. StudlyWords are wrapped in links to view-page actions, everything else is sent as is to the client.

(defmethod render-on ((res response) (page wiki-viewer))
  (let ((scanner (cl-ppcre:create-scanner "((?:[A-Z][a-z]+){2,})")))
    (dolist (part (cl-ppcre:split scanner
                                  (contents (find-wiki-page (page-name page)))
                                  :with-registers-p t))
      (if (cl-ppcre:scan scanner part)
          (let ((part part))
            (<ucw:a :action (view-page page part)
              (<:as-html part)))
          (<:as-is part))))
  (<:p (<ucw:a :action (edit-page page (page-name page)) "Edit")))

The view-page action 

(eval-when (:compile-toplevel :load-toplevel :execute)
  (arnesi:assert-cc 'edit-page))
(defaction view-page ((w wiki-viewer) page-name)
  (unless (find-wiki-page page-name)
    (edit-page w page-name))
  (setf (page-name w) page-name))

view-page simply changes the name of the current page in the wiki-viewer (which is sufficent given how we've defined view-page's render-on method). If the page doesn't already exist we call the edit-page action.

edit-page - Introducing defaction 

(defaction edit-page ((w wiki-viewer) page-name)
  (call 'wiki-editor :page-name page-name)
  (call 'wiki-editor-thankyou :page-name page-name))

This first calls a wiki-editor component, when that returns (by calling answer) the action continues and we call the wiki-editor-thankyou component.

Notice that while this action CALLs multiple components we do not ANSWER. The view-page component basically sits in an infinite loop, it shows various wiki pages and it calls out to various other components, but the wiki-viewer itself will never answer.

If we were to embed a wiki-viewer in other application we would probably subclass wiki-viewer and change its render-on method to produce a link which causes the component to answer.


(defcomponent wiki-editor (wiki-manipulator)

This action, which is called from the edit form, updates the wiki and then returns control to the calling component.

(defaction save-changes ((w wiki-editor) name summary contents)
  (update-wiki-page (page-name w)
                    (make-instance 'wiki-edit
                                   :author name
                                   :summary summary
                                   :contents contents))
  (answer (page-name w)))
(defmethod render-on ((res response) (w wiki-editor))
  (let ((name "")
        (summary "")
        (contents (if (find-wiki-page (page-name w))
                      (contents (find-wiki-page (page-name w)))
    (<ucw:form :action (save-changes w name summary contents)
               :method "POST"
      (<ucw:textarea :rows 15 :cols 80 :accessor contents)
        (<:label :for "author" "Name:")
        (<ucw:text :accessor name :id "author"))
        (<:label :for "summary" "Summary:")
        (<ucw:text :accessor summary :id "summary"))
      (<:p (<:input :type "submit" :value "Save Changes")))))


(defcomponent wiki-editor-thankyou (wiki-manipulator)
(defmethod render-on ((res response) (w wiki-editor-thankyou))
  (<:p "Thank you for editing " (<:as-html (page-name w)))
  (<ucw:a :action (ok w) "Ok."))

wiki-editor-thankyou uses the generic OK action. This action, defined in UCW on all standard-components, simply calls answer.


The wiki components have been designed so that they can be embeded in any other component. What this means is that our trivial application needs some window component we can embed our wiki components in:

(defcomponent wiki-app (window-component)
  ((body :accessor body :component (wiki-viewer :page-name "WelcomePage"))
   (title :accessor title)))
(defmethod initialize-instance :after ((app wiki-app)
                                       &key (page-name "WelcomePage")
  (setf (page-name (body app)) page-name))
(defmethod render-on ((res response) (w wiki-app))
  (when (subtypep (class-of (body w)) (find-class 'wiki-manipulator))
    (setf (title w) (page-name (body w))))
      (<:title (<:as-html (title w))))
      (<:h1 (<:as-html (title w)))
      (render-on res (body w)))))
(defentry-point "wiki.ucw" (:application *example-application*)
    ((page-name "WelcomePage"))
  (call 'wiki-app :page-name page-name))