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.
[rexx] css2xsl [options] [output.xsl]
If no output file is specified, the default is
rexx-highlight.xsl.
-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.
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:
rx-op). This is the default.rx-op rx-add), generating one template per
combination. This is how the CSS cascade works in HTML.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-kw → rexx_kwrx-op rx-add → rexx_op_addrx-const rx-method rx-oquo →
rexx_const_method_oquoThis is the same naming convention used by the DocBook highlighting driver in
highlight.rex.
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.
To use the generated XSL in a DocBook toolchain:
css2xsl to generate the XSL file.<xsl:include href="rexx-highlight.xsl"/> to
your pdf.xsl customization layer.<programlisting>
blocks containing rexx_* elements (generated by
highlight --docbook).No external dependencies beyond the Rexx Parser itself. The generated XSL file is standalone and does not require css2xsl at runtime.
#!/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"