css2xsl


css2xsl


CSS2XSL ("CSS to XSL") is a utility that reads a Rexx highlighting CSS theme (e.g. rexx-print.css) and generates an XSL stylesheet with fo:inline templates for all rexx_* elements. The generated .xsl file is meant to be xsl:include'd from the pdf.xsl customization layer used by DocBook XSL + Apache FOP.

This utility is part of the DocBook highlighting workflow, which allows Rexx code in DocBook <programlisting> blocks to be syntax-highlighted in PDF output produced by Publican or any other DocBook XSL + FOP toolchain.

Usage

[rexx] css2xsl [options] [output.xsl]

If no output file is specified, the default is rexx-highlight.xsl.

Options

-s, --style STYLE CSS theme name (default: print)
--css FILE CSS file path (overrides --style)
--operator MODE Operator granularity: group|full|detail
--special MODE Special char granularity: group|full|detail
--constant MODE Constant granularity: group|full|detail
--assignment MODE Assignment granularity: group|full|detail
-h, --help Display help and exit


The default granularity is group for all categories.

Granularity modes

The granularity options control how much detail is preserved in the generated XSL templates for operators, specials, constants and assignments. Each category can be set independently:

  • group — All elements in a category share the same visual style, determined by the generic CSS class (e.g. all operators use the style of rx-op). This is the default.
  • full — Each element gets both generic and specific classes (e.g. rx-op rx-add), generating one template per combination. This is how the CSS cascade works in HTML.
  • detail — Only elements that have their own specific CSS rules get individual templates. Elements without specific rules inherit the group style. This produces the fewest templates when the CSS theme does not differentiate within a category.

Element naming convention

The XSL templates match XML elements whose names are derived from the CSS classes by stripping the rx- prefix, replacing hyphens with underscores, and joining them under the rexx_ prefix:

  • rx-kwrexx_kw
  • rx-op rx-addrexx_op_add
  • rx-const rx-method rx-oquorexx_const_method_oquo

This is the same naming convention used by the DocBook highlighting driver in highlight.rex.

Generated output

The output is a valid XSL stylesheet containing xsl:template elements that match rexx_* elements and emit fo:inline with the appropriate visual attributes (font-weight, font-style, text-decoration, color). Background color is not emitted for inline elements, since the <programlisting> already has a background set by the DocBook XSL.

For example, a keyword template might look like:

<xsl:template match="rexx_kw">
  <fo:inline font-weight="bold" color="#4169E1">
    <xsl:apply-templates/>
  </fo:inline>
</xsl:template>

The number of templates depends on the granularity: group mode with the default print theme generates 83 templates; full mode generates 234 templates.

Integration with DocBook

To use the generated XSL in a DocBook toolchain:

  1. Run css2xsl to generate the XSL file.
  2. Copy the generated file to your DocBook customization directory.
  3. Add <xsl:include href="rexx-highlight.xsl"/> to your pdf.xsl customization layer.
  4. In your DocBook XML source, use <programlisting> blocks containing rexx_* elements (generated by highlight --docbook).

Prerequisites

No external dependencies beyond the Rexx Parser itself. The generated XSL file is standalone and does not require css2xsl at runtime.

Program source

#!/usr/bin/env rexx
/******************************************************************************/
/*                                                                            */
/* css2xsl.rex - Generate XSL templates for DocBook Rexx highlighting         */
/* ==================================================================         */
/*                                                                            */
/* Reads a Rexx highlighting CSS theme (e.g. rexx-print.css) and generates    */
/* an XSL stylesheet with fo:inline templates for all rexx_* elements.        */
/* The generated .xsl is meant to be xsl:include'd from the pdf.xsl           */
/* customization layer used by DocBook XSL + Apache FOP.                      */
/*                                                                            */
/* Usage:                                                                     */
/*   css2xsl [options] [output.xsl]                                           */
/*                                                                            */
/* Options:                                                                   */
/*   -s, --style STYLE    CSS theme name (default: print)                     */
/*       --css FILE       CSS file path (overrides --style)                   */
/*       --operator MODE  Operator granularity: group|full|detail             */
/*       --special MODE   Special char granularity: group|full|detail         */
/*       --constant MODE  Constant granularity: group|full|detail             */
/*       --assignment MODE Assignment granularity: group|full|detail          */
/*   -h, --help           Show this help                                      */
/*                                                                            */
/* Granularity modes:                                                         */
/*   group  - All elements in a category share the generic class color.       */
/*   detail - Each element gets its own specific color.                       */
/*   full   - Elements get both generic and specific classes (CSS cascade).   */
/*                                                                            */
/* Default granularity is "group" for all categories, meaning operators       */
/* share one color, specials share one color, etc.                            */
/*                                                                            */
/* This program is part of the Rexx Parser package                            */
/* [See https://rexx.epbcn.com/rexx-parser/]                                  */
/*                                                                            */
/* Copyright (c) 2024-2026 Josep Maria Blasco <josep.maria.blasco@epbcn.com>  */
/*                                                                            */
/* License: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0)  */
/*                                                                            */
/* Version history:                                                           */
/*                                                                            */
/* Date     Version Details                                                   */
/* -------- ------- --------------------------------------------------------- */
/* 20260401    0.5  First version                                             */
/*                                                                            */
/******************************************************************************/

  Signal On Syntax

  CLIhelper    = InitCLI()
  myName       = CLIhelper~name
  myHelp       = CLIhelper~help
  args         = CLIhelper~args

/******************************************************************************/
/* Parse command-line options                                                 */
/******************************************************************************/

  style       = "print"
  cssFile     = ""
  opOperator  = "group"
  opSpecial   = "group"
  opConstant  = "group"
  opAssignment= "group"
  outputFile  = ""

  validModes  = "group full detail"

  Loop While args~size > 0, args[1][1] == "-"
    option = args[1]
    args~delete(1)

    Select Case Lower(option)
      When "-h", "--help"      Then Signal Help
      When "-s", "--style"     Then Do
        If args~size == 0 Then
          Call Error "Missing style after '"option"' option."
        style = args[1]
        args~delete(1)
      End
      When "--css"             Then Do
        If args~size == 0 Then
          Call Error "Missing file after '"option"' option."
        cssFile = args[1]
        args~delete(1)
      End
      When "--operator"        Then Do
        If args~size == 0 Then
          Call Error "Missing mode after '"option"' option."
        opOperator = Lower(args[1])
        args~delete(1)
        If WordPos(opOperator, validModes) == 0 Then
          Call Error "Invalid operator mode '"opOperator"'." -
            "Use group, full, or detail."
      End
      When "--special"         Then Do
        If args~size == 0 Then
          Call Error "Missing mode after '"option"' option."
        opSpecial = Lower(args[1])
        args~delete(1)
        If WordPos(opSpecial, validModes) == 0 Then
          Call Error "Invalid special mode '"opSpecial"'." -
            "Use group, full, or detail."
      End
      When "--constant"        Then Do
        If args~size == 0 Then
          Call Error "Missing mode after '"option"' option."
        opConstant = Lower(args[1])
        args~delete(1)
        If WordPos(opConstant, validModes) == 0 Then
          Call Error "Invalid constant mode '"opConstant"'." -
            "Use group, full, or detail."
      End
      When "--assignment"      Then Do
        If args~size == 0 Then
          Call Error "Missing mode after '"option"' option."
        opAssignment = Lower(args[1])
        args~delete(1)
        If WordPos(opAssignment, validModes) == 0 Then
          Call Error "Invalid assignment mode '"opAssignment"'." -
            "Use group, full, or detail."
      End
      Otherwise
        Call Error "Unknown option '"option"'. Use --help for usage."
    End
  End

  -- Remaining argument is the output file
  If args~size > 0 Then Do
    outputFile = args[1]
    args~delete(1)
  End
  If args~size > 0 Then
    Call Error "Too many arguments. Use --help for usage."

  -- Default output file
  If outputFile == "" Then outputFile = "rexx-highlight.xsl"

/******************************************************************************/
/* Resolve the CSS file path                                                  */
/******************************************************************************/

  myPath = FileSpec("Location", .context~package~name)

  If cssFile \== "" Then Do
    -- Explicit CSS file: use as-is (GetHighlight will validate)
    style = cssFile
  End

/******************************************************************************/
/* Build HTMLClasses mapping with the requested granularity                    */
/******************************************************************************/

  hlOptions. = ""
  hlOptions.operator   = opOperator
  hlOptions.special    = opSpecial
  hlOptions.constant   = opConstant
  hlOptions.assignment = opAssignment
  hlOptions.classprefix = "rx-"

  HTMLClass. = HTMLClasses( hlOptions. )

/******************************************************************************/
/* Collect all unique CSS class combinations from HTMLClasses                  */
/******************************************************************************/

  -- We need to collect all unique CSS class strings and, for each one,
  -- derive the DocBook element name and look up its visual properties.
  --
  -- The element name is derived from the CSS classes:
  --   - For single-class entries (e.g. "rx-kw"): rexx_kw
  --   - For multi-class entries (e.g. "rx-op rx-add"): rexx_add
  --     (the most specific class, i.e. the last one)
  --   - For TAKEN_CONSTANT compound entries with 3 classes
  --     (e.g. "rx-const rx-method rx-oquo"): rexx_method_oquo
  --     (specific class + variant)
  --
  -- We skip whitespace ("rx-ws") since the DocBook driver emits it
  -- as plain text without a wrapper element.

  classSet    = .Set~new      -- Tracks unique CSS class strings
  elements    = .Array~new    -- Array of directories: name, tags

  supplier = HTMLClass.~supplier
  Do While supplier~available
    tags = supplier~item
    supplier~next

    -- Skip the default entry and empty values
    If tags == "rexx" Then Iterate
    If tags == ""     Then Iterate

    -- Skip whitespace (emitted as plain text by the driver)
    If tags == "rx-ws"   Then Iterate

    -- Skip duplicates
    If classSet~hasIndex(tags) Then Iterate
    classSet~put(tags)

    -- Derive the DocBook element name from the CSS classes
    elementName = Tags2Element(tags, style)

    entry       = .Directory~new
    entry~name  = elementName
    entry~tags  = tags
    elements~append(entry)
  End

/******************************************************************************/
/* Add compound number elements                                               */
/******************************************************************************/

  -- The Highlighter emits compound tags for number sub-parts by
  -- concatenating the parent tag (e.g. "rx-int") with the child tag
  -- (e.g. "rx-ipart"), producing "rx-int rx-ipart".  These compound
  -- tags are converted to element names like "rexx_int_ipart" by the
  -- DocBook driver, but the loop above only sees them as individual
  -- entries.  We generate all valid parent+child combinations here.
  --
  -- The parent is one of: int, deci, exp, or any string type.
  -- The children depend on the number type:
  --   int:  nsign, ipart
  --   deci: nsign, ipart, dpoint, fpart
  --   exp:  nsign, ipart, dpoint, fpart, emark, esign, expon
  --
  -- When a number appears inside a string (e.g. "3.14"), the
  -- Highlighter uses the string type as the parent (str, bstr,
  -- xstr, etc.).  A number inside a string can have all the same
  -- sub-parts as an exponential number, so each string type gets
  -- the full set of children.

  prefix = hlOptions.classprefix

  numberCombinations = .Array~of( -
    "int  nsign",                  -
    "int  ipart",                  -
    "deci nsign",                  -
    "deci ipart",                  -
    "deci dpoint",                 -
    "deci fpart",                  -
    "exp  nsign",                  -
    "exp  ipart",                  -
    "exp  dpoint",                 -
    "exp  fpart",                  -
    "exp  emark",                  -
    "exp  esign",                  -
    "exp  expon"                   -
  )

  -- Numbers inside strings: each string type can contain a full
  -- number (with all sub-parts).  The string types are defined
  -- in HTMLClasses.cls.
  stringTypes = .Array~of( -
    "str", "bstr", "xstr", "ystr", "pstr", "gstr", "tstr", "ustr" -
  )
  numberChildren = .Array~of( -
    "nsign", "ipart", "dpoint", "fpart", -
    "emark", "esign", "expon"            -
  )
  Do strType Over stringTypes
    Do child Over numberChildren
      numberCombinations~append(strType "  " child)
    End
  End

  Do combo Over numberCombinations
    Parse Var combo parent child
    child = child~strip
    tags = prefix || parent" "prefix || child

    If classSet~hasIndex(tags) Then Iterate
    classSet~put(tags)

    elementName = Tags2Element(tags, style)

    entry       = .Directory~new
    entry~name  = elementName
    entry~tags  = tags
    elements~append(entry)
  End

/******************************************************************************/
/* Look up visual properties and generate the XSL                             */
/******************************************************************************/

  -- Sort elements by name for predictable output
  elements = elements~sortWith(.ElementComparator~new)

  -- Collect all templates
  templates = .Array~new

  Do entry Over elements
    elementName = entry~name
    tags        = entry~tags

    -- Look up visual properties via GetHighlight
    Parse Value GetHighlight(style, tags) -
      With bold italic underline color":"background

    -- Build the fo:inline attributes
    foAttrs = BuildFOAttrs(bold, italic, underline, color, background)

    -- Skip elements with no visual differentiation
    If foAttrs == "" Then Iterate

    templates~append( BuildTemplate(elementName, foAttrs) )
  End

  -- Get the block-level background and default text color from the
  -- "rexx" class.  This will be used to generate an XSL template for
  -- programlisting[@style='STYLE'] that sets the fo:block background.
  Parse Value GetHighlight(style, "rexx") -
    With . . . blockColor":"blockBackground

  -- Generate the complete XSL file
  xslContent = BuildXSL(templates, style, -
    opOperator, opSpecial, opConstant, opAssignment, -
    blockColor, blockBackground)

/******************************************************************************/
/* Write the output file                                                      */
/******************************************************************************/

  -- Delete any previous version to avoid leftovers from CharOut
  If Stream(outputFile, "C", "Q Exists") \== "" Then
    Call SysFileDelete outputFile

  Call CharOut outputFile, xslContent
  Call CharOut outputFile  -- Close the stream

  Say myName": generated" outputFile -
    "("templates~items "templates from style '"style"')."

  Exit 0

/******************************************************************************/
/* TAGS2ELEMENT: Convert CSS class string to DocBook element name              */
/******************************************************************************/
/*                                                                            */
/* Converts a CSS class string to a DocBook element name by stripping the    */
/* "rx-" prefix from each class, replacing "-" with "_", and joining them    */
/* with "_" under the "rexx_STYLE" prefix.                                   */
/*                                                                            */
/* The style name is always included in the element name for symmetry:       */
/*   Tags2Element("rx-kw", "print")  -> "rexx_print_kw"                     */
/*   Tags2Element("rx-op rx-add", "dark") -> "rexx_dark_op_add"             */
/*   Tags2Element("rx-const rx-method rx-oquo", "print")                    */
/*                                       -> "rexx_print_const_method_oquo"  */
/*                                                                            */
/******************************************************************************/

Tags2Element:
  Use Strict Arg cssTags, xslStyle

  words = cssTags~makeArray(" ")
  name  = "rexx_" || xslStyle~changeStr("-", "_")
  Do w Over words
    name ||= "_" || StripPrefix(w)~changeStr("-", "_")
  End
  Return name

/******************************************************************************/
/* STRIPPREFIX: Remove the "rx-" prefix from a CSS class name                 */
/******************************************************************************/

StripPrefix:
  Use Strict Arg className
  If className~caselessStartsWith("rx-") Then
    Return className~substr(4)
  Return className

/******************************************************************************/
/* BUILDFOSTRS: Build fo:inline attribute string from visual properties       */
/******************************************************************************/

BuildFOAttrs: Procedure
  Use Strict Arg bold, italic, underline, color, background

  attrs = ""

  If bold      == "B" Then attrs ||= ' font-weight="bold"'
  If italic    == "I" Then attrs ||= ' font-style="italic"'
  If underline == "U" Then attrs ||= ' text-decoration="underline"'

  If color \== "" Then Do
    -- Colors from GetHighlight are RRGGBBaa (8 hex chars with alpha).
    -- We take the first 6 hex chars for the #RRGGBB value.
    hex = Left(color, 6)
    If hex~length == 6, hex~dataType("X") Then
      attrs ||= ' color="#'hex'"'
  End

  -- We don't emit background-color for inline elements in FOP;
  -- the programlisting already has a background set by the DocBook XSL.

  Return attrs

/******************************************************************************/
/* BUILDTEMPLATE: Generate one XSL match template                              */
/******************************************************************************/

BuildTemplate: Procedure
  Use Strict Arg elementName, foAttrs

  Return '  <xsl:template match="'elementName'">' ||     "0A"x || -
         '    <fo:inline'foAttrs'>'               ||     "0A"x || -
         '      <xsl:apply-templates/>'           ||     "0A"x || -
         '    </fo:inline>'                       ||     "0A"x || -
         '  </xsl:template>'

/******************************************************************************/
/* BUILDXSL: Generate the complete XSL file                                   */
/******************************************************************************/

BuildXSL: Procedure
  Use Strict Arg templates, style, -
    opOperator, opSpecial, opConstant, opAssignment, -
    blockColor, blockBackground

  nl = "0A"x
  styleSafe = style~changeStr("-", "_")

  xsl = '<?xml version="1.0" encoding="UTF-8"?>'                    || nl
  xsl ||= "<!--"                                                    || nl
  xsl ||= "  rexx-highlight.xsl — XSL templates for Rexx syntax"   || nl
  xsl ||= "  highlighting in DocBook/FOP output."                   || nl
  xsl ||= ""                                                        || nl
  xsl ||= "  Generated by css2xsl.rex from style '"style"'."       || nl
  xsl ||= "  Options: operator="opOperator "special="opSpecial     -
           " constant="opConstant " assignment="opAssignment        || nl
  xsl ||= ""                                                        || nl
  xsl ||= "  Include this file in your pdf.xsl customization:"     || nl
  xsl ||= '    <xsl:include href="rexx-highlight.xsl"/>'           || nl
  xsl ||= "-->"                                                     || nl
  xsl ||= ""                                                        || nl
  xsl ||= '<xsl:stylesheet version="1.0"'                          || nl
  xsl ||= '  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"'    || nl
  xsl ||= '  xmlns:fo="http://www.w3.org/1999/XSL/Format">'       || nl
  xsl ||= ""                                                        || nl

  -- Block-level template: matches the rexx_style_STYLE wrapper element
  -- emitted by ProcessProgramListings, and sets background-color and
  -- default text color on an fo:block.  This fo:block sits inside the
  -- standard DocBook programlisting fo:block (which provides monospace
  -- font, padding, borders, etc.), overriding its background and color.
  bgHex = Left(blockBackground, 6)
  fgHex = Left(blockColor, 6)
  If bgHex~length == 6, bgHex~dataType("X") Then Do
    foAttrs = ' background-color="#'bgHex'"'
    If fgHex~length == 6, fgHex~dataType("X") Then
      foAttrs ||= ' color="#'fgHex'"'
    wrapperName = "rexx_style_"styleSafe

    xsl ||= '  <!-- Block background for style "'style'" -->'        || nl
    xsl ||= '  <!-- Negative margins expand the inner fo:block to cover -->' || nl
    xsl ||= '  <!-- the padding of the outer shade.verbatim.style block. -->' || nl
    xsl ||= '  <xsl:template match="'wrapperName'">'                 || nl
    xsl ||= '    <fo:block'foAttrs                                       -
                 ' margin-top="-6pt" margin-bottom="-6pt"'               -
                 ' margin-left="-6pt"'                                   -
                 ' padding-top="6pt" padding-bottom="6pt"'               -
                 ' padding-left="6pt">'                               || nl
    xsl ||= '      <xsl:apply-templates/>'                           || nl
    xsl ||= '    </fo:block>'                                        || nl
    xsl ||= '  </xsl:template>'                                      || nl
    xsl ||= ""                                                        || nl
  End

  Do t Over templates
    xsl ||= t || nl
    xsl ||= ""  || nl
  End

  xsl ||= "</xsl:stylesheet>" || nl

  Return xsl

/******************************************************************************/
/* ERROR, HELP, SYNTAX handlers                                               */
/******************************************************************************/

Error:
  Say myName":" Arg(1)
  Exit 1

Help:
  Say ""
  Say "Usage:" myName "[options] [output.xsl]"
  Say ""
  Say "Options:"
  Say "  -s, --style STYLE     CSS theme (default: print)"
  Say "      --css FILE        CSS file path (overrides --style)"
  Say "      --operator MODE   group|full|detail (default: group)"
  Say "      --special MODE    group|full|detail (default: group)"
  Say "      --constant MODE   group|full|detail (default: group)"
  Say "      --assignment MODE group|full|detail (default: group)"
  Say "  -h, --help            Show this help"
  Say ""
  Say "See:" myHelp
  Exit 0

Syntax:
  co = Condition("O")
  Say myName": Error" co~rc "running" co~position":" co~message
  Exit 1

/******************************************************************************/
/* Comparator for sorting elements by name                                    */
/******************************************************************************/

::Class ElementComparator Public
::Method compare
  Use Strict Arg left, right
  Return left~name~compareTo(right~name)

/******************************************************************************/
/* Required packages                                                          */
/******************************************************************************/

::Requires "CLISupport.cls"
::Requires "HTMLClasses.cls"
::Requires "StyleSheet.cls"