opening it up with Common Lisp

Favorite weblogs

Lisp Related

Bill Clementson

Finding Lisp

Lemonodor

Lispmeister.com

Planet Lisp

Politics

Orcinus

Talking Points Memo

This Modern World

Working for Change

Other home

Polliblog

Recent Readings

Book review: Darwinia
Reviewed: Friday, August 11, 2006

Summer reading: Spin
Reviewed: Saturday, August 5, 2006

Runner
Reviewed: Tuesday, July 18, 2006

the Omnivoire's Delimma
Reviewed: Wednesday, July 12, 2006

the Golem's Eye
Reviewed: Wednesday, May 31, 2006





tinderbox
 width=

CL-Markdown extensions - first steps
Tuesday, August 1, 2006

I've made a little time to push on the CL-Markdown extension mechanism I wrote about a week or two ago. I've added a few new glitches but the change was less painful that I thought it might be (CL-Markdown swings far towards the organic and messy side of the code I write!)

In any case, the text below (written in Markdown format and parsed by CL-Markdown) describes how to write and use CL-Markdown extensions. The output isn't perfect (there are some glitches with quotations marks in code blocks for one thing), but it's pretty darn good if you ask me... (I know, biased, biased, biased... just like that SCLM). Let me know what you think!

CL-Markdown extensions

CL-Markdown aims to mirror the syntax of John Gruber's Markdown language (and it's getting there slowly!).

the Syntax

CL-Markdown uses { and } as new syntax markers. A single pair of curly braces wraps a function call whereas a double pair denotes a sort of wiki-like link. A function calls looks like:

\{function-name [function-argument]*\}

a wiki-like-link looks like

\{\{ syntax as yet to be determined \}\}

Function calls: { and }

Calling extension functions requires three things:

  1. writing (or finding) the extension that you want
  2. telling CL-Markdown that you want to use the extension
  3. writing your Markdown text with calls to the extension

The last part is the easiest; all you need to do is open a curly brace, type the name of extension function, type in the arguments (separated by spaces) and type a closing curly brace. For example:

"{now}" will generate the text "12:23".

The second step is necessary because CL-Markdown won't recognize functions as functions unless you tell it to up front. After all, you wouldn't want to allow people to execute arbitrary code; it might be a security risk (smile). Because CL-Markdown operates in two stages, there are two times when functions can be called: during parsing and during rendering. Functions active during these stages are keep in the special variables *render-active-functions* and *parse-active-functions*.

An example maight make this clearer. First, we'll call Markdown without specifying any functions:

? (markdown "Today is {today}. It is {now}." 
  :format :html :stream t)
<P>
Today is 
; Warning: Inactive or undefined CL-Markdown function TODAY
; While executing: #
<STANDARD-METHOD RENDER-SPAN-TO-HTML ((EQL EVAL) T)>
. It is 
; Warning: Inactive or undefined CL-Markdown function NOW
; While executing: #
<STANDARD-METHOD RENDER-SPAN-TO-HTML ((EQL EVAL) T)>
. 
</P>

As you can see, the functions weren't ones that Cl-Markdown was ready to recognize, so we got warnings and no text was generated. If we tell CL-Markdown that today and now should be treated as functions, then we see a far prettier picture:

? (let ((*render-active-functions* 
         (append '(today now) *render-active-functions*)))
    (markdown "Today is {today}. It is {now}." 
        :format :html :stream t))
<P>
Today is 1 August 2006. It is 11:36. 
</P>

By now, we've seen how to include function calls in CL-Markdown documents and how to generate those documents with CL-Markdown. The final piece of the puzzle is actually writing the extensions.

Writing Cl-Markdown extensions

There are several ways to write Cl-Markdown extensions. The easiest is one is to write functions active during rendering that return the text that you wish to be included in the document. For example:

(defun today (phase arguments result)
  (declare (ignore phase arguments result))
  (format-date "%e %B %Y" (get-universal-time)))

The format-date command is part of metatilities; it returns a string of the date using the C library inspired date format. This string is placed in the document whereever the function call ({today}) is found.

Alternately, one can use the *output-stream* variable to insert more complicated text. This would look like:

(defun now (phase arguments result)
  (declare (ignore phase arguments result))
  (format *output-stream* "~a" 
    (format-date "%H:%M" (get-universal-time)))
  nil)

(Note that now returns nil so that the date isn't inserted twice!).

The other alternative is to use your function calls to alter the structure of the CL-Markdown document and then let Markdown deal with some or all of the rest. The anchor extension provides an example of this:

(defun anchor (phase &rest args)
  (ecase phase
    (:parse
     (let ((name (caar args))
           (title (cadar args)))
       (setf (item-at (link-info *current-document*) name)
             (make-instance 'link-info
               :id name :url (format nil "#~a" name) 
               :title (or title "")))))
    (:render (let ((name (caar args)))
               (format nil "
<a name='~a' id='~a'>
</a>
"
                       name name)))))

Anchor makes it easier to insert anchors into your document and to link to those anchors from elsewhere. It is active during both parsing and rendering. During the parsing phase, it uses it's arguments to determine the name and title of the link and places this into the current document's link information table. During rendering, it outputs the HTML needed to mark the link.

An even more complex example is the table-of-contents extension:

(defun table-of-contents (phase &rest args)
  (bind ((arg1 (ignore-errors
                (read-from-string (string-upcase 
                                   (first args)))))
         (arg2 (ignore-errors
                (parse-integer (second args))))
         (depth (and arg1 (eq arg1 :depth) arg2)))
    (ecase phase 
      (:parse
       (push (lambda (document)
               (add-anchors document :depth depth))
             (item-at-1 (properties *current-document*)
                        :cleanup-functions))
       nil) 
      (:render
       (bind ((headers (collect-elements
                        (chunks *current-document*)
                        :filter
                        (lambda (x) (header-p x :depth depth)))))
         (when headers
           (format *output-stream*
                   "
<div class='table-of-contents'>
")
           (iterate-elements
            headers
            (lambda (header)
              (bind (((index level text)
                      (item-at-1 (properties header) :anchor)))
                (format *output-stream* "
<a href='#~a' title='~a'>
"
                        (make-ref index level text)
                        (or text ""))
                (render-to-html header)
                (format *output-stream* "
</a>
"))))
           (format *output-stream* "
</div>
")))))))

Because we can't generate a table of contents until the entire document has been parsed, the table-of-contents extension adds a function to the cleanup-functions of the current document. Cleanup functions are called when parsing is complete. The add-anchors functions adds additional chunks to the document before each header (down to some fixed depth). These anchors can then be used by the rendering phase of the table-of-contents extension to link the headers to the sections in the document.


|

Home | About | Quotes | Recent | Archives

Copyright -- Gary Warren King, 2004 - 2006