The DSSSL Cookbook

Revision 2

May 10, 1998

This software is copyrighted by its respective authors. The following terms apply to all files associated with the software unless explicitly disclaimed in individual files.

The authors hereby grant permission to use, copy, modify, distribute, and license this software and its documentation for any purpose, provided that existing copyright notices are retained in all copies and that this notice is included verbatim in any distributions. No written agreement, license, or royalty fee is required for any of the authorized uses. Modifications to this software may be copyrighted by their authors and need not follow the licensing terms described here, provided that the new terms are clearly indicated on the first page of each file where they apply.

IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.


Table of Contents
Introduction
First Such Xref
Flexible line spacing with min-leading
Footnotes
Formatting an Address for Output
Hierarchical numbering using specific countable elements
Inheriting start-indent
Line Break
Paragraph Indent Proportional to Font Size or Line Spacing
SGML Identity Transformation

Introduction

The DSSSL Cookbook is a series of hints about using DSSSL, including style and techniques for writing DSSSL stylesheets as well as the use of specific DSSSL functions and flow object classes. Further information about the DSSSL standard, DSSSL engines, and DSSSL applications are listed in the “Further Information” section of Mulberry's DSSSL page (http://www.mulberrytech.com/dsssl/) If you are interested in using DSSSL, you may also subscribe to the DSSSList, the DSSSL users' mailing list operated by Mulberry Technologies, Inc., as a free service to the DSSSL user community (http://www.mulberrytech.com/dsssl/dssslist/).

Although nobody is working on this at present, offers to oversee the project, contributions of cookbook tips, offers to contribute, or suggestions for improvement are actively solicited. Please send any contributions or suggestions for improvement to btusdin@mulberrytech.com.


First Such Xref

(first-such-xref snl) returns the first DocBook xref in the current container to refer to the same target as snl.

(define (first-such-xref #!optional (snl (current-node)))
  (node-list-first (node-list-filter (lambda (candidate)
                                       (equal? (attribute-string (norm "linkend")
                                                                 candidate)
                                               (attribute-string (norm "linkend")
                                                                 snl)))
                                     (descendants (if (node-list-empty? (ancestor (norm "preface")
                                                                                  snl))
                                                      (if (node-list-empty? (ancestor (norm "chapter")
                                                                                      snl))
                                                          (if (node-list-empty? (ancestor (norm "appendix")
                                                                                          snl))
                                                              (error "Where am I?")
                                                              (ancestor (norm "appendix")
                                                                        snl))
                                                          (ancestor (norm "chapter")
                                                                    snl))
                                                      (ancestor (norm "preface")
                                                                snl))))))

Flexible line spacing with min-leading

By default, the paragraph line spacing is fixed to the value of the line-spacing characteristic. To allow flexible line spacing so that, for example, you can include inline graphics that are larger than the line spacing, set the min-leading characteristic to something other than its default value of #f. A useful value for min-leading is the same value as the line-spacing characteristic.

Use min-leading in a style specification, in a paragraph flow object, or where the characteristic will be inherited by a paragraph flow object.

(define para-style
  (style
   font-size: %bf-size%
   font-weight: 'medium
   font-posture: 'upright
   font-family-name: %body-font-family%
   min-leading: %bf-line-spacing%
   line-spacing: %bf-line-spacing%))

Footnotes

This section deals with a simple method of adding numbered footnotes to a document at the end of a high level element. The example given uses a structure of SECTION to hold all footnotes at its end. Numbering restarts with the next section. Alternatives are described in dbblock.dsl, from Norman Walsh docbook work. The stylesheet is based on functions from Norman Walsh and Chris Maden.

Two blocks of code are necessary, the first to handle the footnote within the body of the document and to generate the footnote references, the second to produce the footnotes as part of a footnotes mode

This code sits in the body of the stylesheet to handle the SECTION, where I want the footnotes to appear.

(element section
   (let ((footnotes (select-elements (descendants (current-node))
             "FOOTNOTE")))
     (sosofo-append (process-children)
        (if (not (node-list-empty? footnotes))
            (make rule
            orientation: 'horizontal)
            (empty-sosofo))
        (with-mode footnotes
             (process-node-list footnotes)))))

Description. The 'let' structure assigns to variable footnotes the list of footnotes found by searching the descendants of element section for all FOOTNOTE elements. The sosofo-append causes the content of the section to be output. If any footnotes exist, the node list footnotes will not be empty and the make rule will produce a dividing line beneath the text of the section and the footnotes. Otherwise the rule is not output — the empty-sosofo clause of the if statement. After seperating the footnotes from the text the with-mode footnotes is used to process the footnotes. I'll come to this in a minute

This also sits in the body of the stylesheet to handle the footnotes as they appear. I want to replace the content of the footnote with a reference to it.

(element footnote
  ($ss-seq$ + (literal (footnote-number (current-node)))))

(define (footnote-number footnote)
 (format-number (component-child-number footnote '("SECTION")) "1"))

This code is straight from the docbook stylesheet, file dbblock.dsl. It puts out an in-line sequence, shifted in size and offset by set factors relative to the main text size. The end result is to produce a superscript number, the footnote number of the current-node. This is determined by the element number within the current section.

The only change is the use of SECTION as the collecting point for footnotes. This is defined in the list component element number, defined below. Somewhat redundant, it does permit the flexibility of collecting footnotes under different structural blocks, but brings in a seperate problem not yet resolved, how to avoid duplicate horizontal rules when blocks of footnotes are output together.

(define component-element-list
  (list  "SECTION"=20
  "SUBSECT1"
  "SUBSECT2"
  "REPORT")) ;; just in case nothing else matches...

Next comes the actual processing of the footnotes using the footnotes mode. The element-children function came from Chris Maden and handles the default case.


;; (element-children) returns the children of a node of class element.

(define (element-children #!optional (snl (current-node)))
  (node-list-filter (lambda (node)
                      (equal? (node-property 'class-name
                                             node)
                              'element))
                    (children snl)))


(mode footnotes
  (default (process-node-list (element-children)))


  (element (footnote)
  (make paragraph
  (make sequence
   =20
    (literal "Note")
    (literal (footnote-number (current-node)))
    (literal " ")
    (process-children))))
)

The actual mode simply outputs one note per paragraph, preceeded by the two literals and the footnote number.

All other functions are from the standard docbook code, mostly found in file dblib.dsl. As of my usage, it stands at version 1.08 beta 5. It may change. If it does, search out a file called xref, presently held in the print directory of the distribution. This holds the key to finding your way around.


Formatting an Address for Output

In the DTD, an Organization Address consists of the following elements, all of them optional:

ORGCONTACT

Who to contact in the organization. May contain multiple entries.

ORGNAME

Name of the organization.

ORGDIV

Division within the organization.

POSTBOX

P.O. Box Number.

STREET

Street Address. Multiple lines allowed.

CITY

City

STATE

State

COUNTRY

Country

POSTCODE

Zip or other Postal Code

EMAIL

E-Mail address of contact or organization.

PHONE

Phone number. Multiple entries alllowed.

FAX

FAX number

Example 1. The output address should be formatted as:

        John Smith
  Cork Works, Inc.
  Accounting Department
  P.O. Box 1234
  Cork Oak Building
  726 Fine St.
  Virginia Beach, VA  US  23450
  JSMITH@CWI.COM
  800-123-4567
  901-123-4567
  901-123-4568  (FAX)

Example 2. DSSSL Code to Create the Formatted Address

(element ORGADDR
   (let (
          (orgcontact (select-elements (children (current-node))"ORGCONTACT"))
         (orgname    (select-elements (children (current-node))"ORGNAME"))
        (orgdiv   (select-elements (children (current-node))"ORGDIV"))
        (postbox     (select-elements (children (current-node))"POSTBOX"))
        (street   (select-elements (children (current-node))"STREET"))
        (city      (select-elements (children (current-node))"CITY"))
        (state       (select-elements (children (current-node))"STATE"))
        (country     (select-elements (children (current-node))"COUNTRY"))
        (postcode    (select-elements (children (current-node))"POSTCODE"))
         (email      (select-elements (children (current-node))"EMAIL"))
        (phone    (select-elements (children (current-node))"PHONE"))
        (fax       (select-elements (children (current-node))"FAX"))
          )
          (make sequence
             (make paragraph
              (process-node-list orgcontact))
            (make paragraph
              (process-node-list orgname))
            (make paragraph
              (process-node-list orgdiv))
            (make paragraph
              (process-node-list postbox))
            (make paragraph
              (process-node-list street))
            (make paragraph
              (if(node-list-empty? city)
                (empty-sosofo)
                (make sequence
                  (process-node-list city)
                  (literal ", ")
                 ))
              (if(node-list-empty? state)
                (empty-sosofo)
                (make sequence
                  (process-node-list state)
                  (literal "  ")
                 ))
              (if(node-list-empty? country)
                (empty-sosofo)
                (make sequence
                  (process-node-list country)
                  (literal "  ")
                 ))
              (process-node-list postcode))
           (make paragraph
              (process-node-list email))
          (make paragraph
              (process-node-list phone))
          (make paragraph
              (if(node-list-empty? fax)
                (empty-sosofo)
                (make sequence
                  (process-node-list fax)
                  (literal "  (FAX)")
                 )))
          )
      )
   )

(element STREET
     (make paragraph
       (process-children)))
(element PHONE
     (make paragraph
       (process-children)))

What It's Doing: The “select-element” statements are creating local variables that correspond to all of the elements in the DTD. This appends the element values from the DTD into a single local variable.

The “make sequence” specifies the order of output.

The “if(node-list-empty?” statements test to see if the variable has a value. If it does, it'll use it and add the literal text after it. Otherwise it continues. If this weren't done, the literal text would appear even without the variable. (if no “city” exists, there would be just a comma) This is only necessary for variables that have a literal attached to them.

The additional “element” statements make it possible to express multiple DTD element entries on separate lines. In this case, multiple PHONES would be appended (800-123-4567908-123-4567) on a single line. The “make paragraph” statements put the numbers on separate lines.

Unused elements from the DTD are not a problem. The address remains properly formatted.


Hierarchical numbering using specific countable elements

Sometimes sections within a document have to be numbered according to the familiar ‘X.X.X’ scheme, but only specified element types ‘count’ as contributors to this numbering. Also, there can be a glorious mixture of said element types at each level. Thus (child-number) is no use, and even counting preceding siblings isn't good enough.

This code uses (node-list-filter), which in turn uses (node-list-reduce) - these are both printed in the DSSSL standard, but are not implemented in Jade v1.0 so you need to type them in. This is the core routine:

; preced-count: gives the number of 'significant' (for numbering)
; sibling elements prior to node. This routine relies on a specific
; list of element types, which must include all elements that
; are numbered:
(define (preced-count node gilist)
      (node-list-length
            (node-list-filter
                (lambda (n1)
                        (member (gi n1) gilist)
                )
                (preced node)
            )
      )
)

You need to declare your ‘countable elements’ as a list of strings:

(define countable-elements '("AAA" "BBB" "CCC" ...))

Then you add one to the number returned by (preced-count) to get the correct number for the current element in a (number-clause) routine:

(define (number-clause node top-level)
        (case (gi node)
              (("ROOT-NODE-GI")
               (list)
               )
              (else
               (if top-level
                   (list (+ (preced-count node countable-elements) 1))
                   (cons (+ (preced-count node countable-elements) 1)
                         (number-clause (parent node) top-level)
                   )
               )
              )
        )
)

... which in turn is called by a (number-heading) routine which returns a formatted string ...

(define (number-heading node top-level)
  (literal
   (format-number-list (reverse (number-clause node top-level))
                       "1"
                       ".")))

... that you can stick in front of the relevant heading:

                ...
                (make sequence
                      (number-heading (parent (current-node)) #t)
                      (literal " ")
                      (process-children)
                )
                ...

(This assumes that the heading is a child of the structural element which is being counted – hence (number-heading) is applied to the parent of the heading element.)


Inheriting start-indent

As an alternative to setting the start-indent characteristic for each paragraph, title, list item, table, etc., use a sequence flow object at the top-level element to set the start-indent characteristic to the default start indent for the document:

(element TOP-LEVEL-ELEMENT
   (make sequence
         start-indent: %body-start-indent%))

The majority of uses of the start-indent characteristic can then use the inherited start indent to set a flow object's start indent:

(element (BUL.LIST ITEM)
  (make paragraph
  start-indent: (+ (inherited-start-indent) (* 2 (BLSTEP)))
  first-line-start-indent: (BLSTEP)
  (process-children)))

Line Break

To force a newline in a paragraph with the same spacing as if the text had wrapped at the end of the line, use the paragraph-break flow object class.

A paragraph-break flow object is allowed only in paragraph flow objects, and, unfortunately for its use in making a “line break”, all the characteristics of the containing paragraph flow object, including the otherwise non-inherited space-before and space-after characteristics, are inherited by a paragraph-break flow object.

Using a paragraph-break flow object to implement a line break therefore requires that the enclosing paragraph flow object has no specification for its space-before and space-after characteristics. A technique that allows a chunk of formatted text to still have space before and after it is to enclose the paragraph flow object within a display-group flow object and specify the space-before and space-after characteristics on the display-group so they're not “inherited” from the paragraph by the paragraph-break.

For the markup:
<chapter>
<title>Watch me<lb>split</title>
the following element construction rules will format the chapter title as:
Watch me
 split
;; Chapter title
(element (CHAPTER TITLE)
   (make display-group
               quadding: 'center
         keep-with-next?: #t
         min-leading: 16pt
         space-before: 0pt
         space-after: 12pt
         (make paragraph)))

;; Line break
(element LB
   (make paragraph-break))


Paragraph Indent Proportional to Font Size or Line Spacing

An indent at the start of a paragraph is conventionally 1 em to 3 ems deep, where an em is as wide as the point size of the type.

When using indents, an alternative to explicitly setting the indent is to let the DSSSL engine derive it from the actual font size.

In the following example, the value of the font-size characteristic could be declared in the declaration for the para-style style object or it could be inherited, yet, without knowing how it was declared, the first line's start indent is always twice the font size since the (actual-font-size) procedure (and there's one for every inherited characteristic) returns the actual value of the font-size characteristic:
;; Indent 2 ems
(element PARA
         (make paragraph
               use: para-style
               first-line-start-indent: (* 2 (actual-font-size))))

Note that typography books uniformly recommend against indenting the first line of the first paragraph of running text, particularly after a heading, but this is just an example, so we can get away with it. They also recommend against also inserting space between paragraphs if you indent the first line, so at least we got that part right.

In the following example, every line except the first is indented (or the first line has a hanging indent) that is equal to the line spacing. Again, we neither know nor care where the line-spacing characteristic was declared, since the actual value will always be supplied.
;; Hanging indent
(element ITEM
         (make paragraph
               use: para-style
               start-indent: (+ (inherited-start-indent)
                                (actual-line-spacing))
               first-line-start-indent: (- (actual-line-spacing))))

Note that it is an error to call an (actual-characteristic) procedure in the course of determining the value for characteristic.


SGML Identity Transformation

The following, which owes much to James Clark's example in transform.htm from the Jade documentation and to Dan Speck's DSSSList post of October 10, 1997, performs a rudimentary SGML–SGML identity transformation. It does not regenerate the DOCTYPE declaration, nor does it handle processing instructions.

<!doctype style-sheet PUBLIC "-//James Clark//DTD DSSSL Style Sheet//EN">

(declare-flow-object-class element
  "UNREGISTERED::James Clark//Flow Object Class::element")
(declare-flow-object-class empty-element
  "UNREGISTERED::James Clark//Flow Object Class::empty-element")
(declare-characteristic preserve-sdata?
  "UNREGISTERED::James Clark//Characteristic::preserve-sdata?"
  #t)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Default rule
(default (output-element))

(define (output-element #!optional (node (current-node)))
  (if (node-property "must-omit-end-tag?" node)
      (make empty-element
      attributes: (copy-attributes))
      (make element
      attributes: (copy-attributes))))

(define (copy-attributes #!optional (nd (current-node)))
  (let loop ((atts (named-node-list-names (attributes nd))))
    (if (null? atts)
        '()
        (let* ((name (car atts))
               (value (attribute-string name nd)))
          (if value
              (cons (list name value)
                    (loop (cdr atts)))
              (loop (cdr atts)))))))