Element Styling

This section describes how styles defined in a style sheet are applied to document elements. Understanding how this works will help you when designing a custom style sheet.

rinohtype’s style sheets are heavily inspired by CSS, but add some additional functionality. Similar to CSS, rinohtype makes use of so-called selectors to select document elements in the document tree to style. Unlike CSS however, these selectors are not directly specified in a style sheet. Instead, all selectors are collected in a matcher where they are mapped to descriptive labels for the selected elements. A style sheet assigns style properties to these labels. Besides the usefulness of having these labels instead of the more cryptic selectors, a matcher can be reused by multiple style sheets, avoiding duplication.

Note

This section currently assumes some Python or general object-oriented programming knowledge. A future update will move Python-specific details to another section, making things more accessible for non-programmers.

Document Tree

A Flowable is a document element that is placed on a page. It is usually a part of a document tree. Flowables at one level in a document tree are rendered one below the other.

Here is schematic representation of an example document tree:

|- Section
|   |- Paragraph
|   \- Paragraph
\- Section
    |- Paragraph
    |- List
    |   |- ListItem
    |   |   |- Paragraph (item label; a number or bullet symbol)
    |   |   \- StaticGroupedFlowables (item body)
    |   |       \- Paragraph
    |   \- ListItem
    |       \- Paragraph
    |   |   \- StaticGroupedFlowables
    |   |       \- List
    |   |           |- ListItem
    |   |           |   \- ...
    |   |           \- ...
    \- Paragraph

This represents a document consisting of two sections. The first section contains two paragraphs. The second section contains a paragraph followed by a list and another paragraph. All of the elements in this tree are instances of Flowable subclasses.

Section and List are subclasses of GroupedFlowables; they group a number of flowables. In the case of List, these are always of the ListItem type. Each list item contains an item number (ordered list) or a bullet symbol (unordered list) and an item body. For simple lists, the item body is typically a single Paragraph. The second list item contains a nested List.

A Paragraph does not have any Flowable children. It is however the root node of a tree of inline elements. This is an example paragraph in which several text styles are combined:

Paragraph
 |- SingleStyledText('Text with ')
 |- MixedStyledText(style='emphasis')
 |   |- SingleStyledText('multiple ')
 |   \- MixedStyledText(style='strong')
 |       |- SingleStyledText('nested ')
 |       \- SingleStyledText('styles', style='small caps')
 \- SingleStyledText('.')

The visual representation of the words in this paragraph is determined by the applied style sheet. Read more about how this works in the next section.

Besides SingleStyledText and MixedStyledText elements (subclasses of StyledText), paragraphs can also contain InlineFlowables. Currently, the only inline flowable is InlineImage.

The common superclass for flowable and inline elements is Styled, which indicates that these elements can be styled using the style sheets.

Selectors

Selectors in rinohtype select elements of a particular type. The class of a document element serves as a selector for all instances of the class (and its subclasses). The Paragraph class is a selector that matches all paragraphs in the document, for example:

Paragraph

As with CSS selectors, elements can also be matched based on their context. For example, the following matches any paragraph that is a direct child of a list item or in other words, a list item label:

ListItem / Paragraph

Python’s ellipsis can be used to match any number of levels of elements in the document tree. The following selector matches paragraphs at any level inside a table cell:

TableCell / ... / Paragraph

To help avoid duplicating selector definitions, context selectors can reference other selectors defined in the same matcher using SelectorByName:

SelectorByName('definition term') / ... / Paragraph

Selectors can select all instances of Styled subclasses. These include Flowable and StyledText, but also TableSection, TableRow, Line and Shape. Elements of some of the latter classes only appear as children of other flowables (such as Table).

Similar to a HTML element’s class attribute, Styled elements can have an optional style attribute which can be used when constructing a selector. This one selects all styled text elements with the emphasis style, for example:

StyledText.like('emphasis')

The Styled.like() method can also match arbitrary attributes of elements by passing them as keyword arguments. This can be used to do more advanced things such as selecting the background objects on all odd rows of a table, limited to the cells not spanning multiple rows:

TableCell.like(row_index=slice(0, None, 2), rowspan=1) / TableCellBackground

The argument passed as row_index is a slice object that is used for extended indexing[1]. To make this work, TableCell.row_index is an object with a custom __eq__() that allows comparison to a slice.

Rinohtype borrows CSS’s concept of specificity to determine the “winning” selector when multiple selectors match a given document element. Each part of a selector adds to the specificity of a selector. Roughly stated, the more specific selector will win. For example:

ListItem / Paragraph                      # specificity (0, 0, 0, 0, 2)

wins over:

Paragraph                                 # specificity (0, 0, 0, 0, 1)

since it matches two elements instead of just one.

Specificity is represented as a 5-tuple. The last four elements represent the number of location (currently not used), style, attribute and class matches. Here are some selectors along with their specificity:

StyledText.like('emphasis')               # specificity (0, 0, 1, 0, 1)
TableCell / ... / Paragraph               # specificity (0, 0, 0, 0, 2)
TableCell.like(row_index=2, rowspan=1)    # specificity (0, 0, 0, 2, 1)

Specificity ordering is the same as tuple ordering, so (0, 0, 1, 0, 0) wins over (0, 0, 0, 5, 0) and (0, 0, 0, 0, 3) for example. Only when the number of style matches are equal, the attributes match count is compared and so on.

In practice, the class match count is dependent on the element being matched. If the class of the element exactly matches the selector, the right-most specificity value is increased by 2. If the element’s class is a subclass of the selector, it is only increased by 1.

The first element of the specificity tuple is the priority of the selector. For most selectors, the priority will have the default value of 0. The priority of a selector only needs to be set in some cases. For example, we want the CodeBlock selector to match a CodeBlock instance. However, because CodeBlock is a Paragraph subclass, another selector with a higher specificity will also match it:

CodeBlock                                 # specificity (0, 0, 0, 0, 2)
DefinitionList / Definition / Paragraph   # specificity (0, 0, 0, 0, 3)

To make sure the CodeBlock selector wins, we increase the priority of the CodeBlock selector by prepending it with a + sign:

+CodeBlock                                # specificity (1, 0, 0, 0, 2)

In general, you can use multiple + or - signs to adjust the priority:

++CodeBlock                               # specificity (2, 0, 0, 0, 2)
---CodeBlock                              # specificity (-3, 0, 0, 0, 2)

Matchers

At the most basic level, a StyledMatcher is a dictionary that maps labels to selectors:

matcher = StyledMatcher()
...
matcher['emphasis'] = StyledText.like('emphasis')
matcher['chapter'] = Section.like(level=1)
matcher['list item number'] = ListItem / Paragraph
matcher['nested line block'] = (GroupedFlowables.like('line block')
                                / GroupedFlowables.like('line block'))
...

Rinohtype currently includes one matcher which defines labels for all common elements in documents:

from rinoh.stylesheets import matcher

Style Sheets

A StyleSheet takes a StyledMatcher to provide element labels to assign style properties to:

styles = StyleSheet('IEEE', matcher=matcher)
...
styles['strong'] = TextStyle(font_weight=BOLD)
styles('emphasis', font_slant=ITALIC)
styles('nested line block', margin_left=0.5*CM)
...

Each Styled has a Style class associated with it. For Paragraph, this is ParagraphStyle. These style classes determine which style attributes are accepted for the styled element. Because the style class can automatically be determined from the selector, it is possible to simply pass the style properties to the style sheet by calling the StyleSheet instance as shown above.

Style sheets are usually loaded from a .rts file using StyleSheetFile. An example style sheet file is shown in Style Sheets.

A style sheet file contains a number of sections, denoted by a section title enclosed in square brackets. There are two special sections:

  • [STYLESHEET] describes global style sheet information (see StyleSheetFile for details)

  • [VARIABLES] collects variables that can be referenced elsewhere in the style sheet

Other sections define the style for a document elements. The section titles correspond to the labels associated with selectors in the StyledMatcher. Each entry in a section sets a value for a style attribute. The style for enumerated lists is defined like this, for example:

[enumerated list]
margin_left=8pt
space_above=5pt
space_below=5pt
ordered=true
flowable_spacing=5pt
number_format=NUMBER
label_suffix=')'

Since this is an enumerated list, ordered is set to true. number_format and label_suffix are set to produce list items labels of the style 1), 2), …. Other entries control margins and spacing. See ListStyle for the full list of accepted style attributes.

Base Styles

It is possible to define styles which are not linked to a selector. These can be useful to collect common attributes in a base style for a set of style definitions. For example, the Sphinx style sheet defines the header_footer style to serve as a base for the header and footer styles:

[header_footer : Paragraph]
base=default
typeface=$(sans_typeface)
font_size=10pt
font_weight=BOLD
indent_first=0pt
tab_stops=50% CENTER, 100% RIGHT

[header]
base=header_footer
padding_bottom=2pt
border_bottom=$(thin_black_stroke)
space_below=24pt

[footer]
base=header_footer
padding_top=4pt
border_top=$(thin_black_stroke)
space_above=18pt

Because there is no selector associated with header_footer, the element type needs to be specified manually. This is done by adding the name of the relevant Styled subclass to the section name, using a colon (:) to separate it from the style name, optionally surrounded by spaces.

Custom Selectors

It is also possible to define new selectors directly in a style sheet file. This allows making tweaks to an existing style sheet without having to create a new StyledMatcher. However, this should be used sparingly. If a great number of custom selectors are required, it is better to create a new StyledMatcher

The syntax for specifying a selector for a style is similar to that when constructing selectors in a Python source code (see Matchers), but with a number of important differences. A Styled subclass name followed by parentheses represents a simple class selector (without context). Arguments to be passed to Styled.like() can be included within the parentheses.

[special text : StyledText('special')]
font_color=#FF00FF

[accept button : InlineImage(filename='images/ok_button.png')]
baseline=20%

Even if no arguments are passed to the class selector, it is important that the class name is followed by parentheses. If the parentheses are omitted, the selector is not registered with the matcher and the style can only be used as a base style for other style definitions (see Base Styles).

As in Python source code, context selectors are constructed using forward slashes (/) and the ellipsis (...). Another selector can be referenced in a context selector by enclosing its name in single or double quotes.

[admonition title colon : Admonition / ... / StyledText('colon')]
font_size=10pt

[chapter title : LabeledFlowable('chapter title')]
label_spacing=1cm
align_baselines=false

[chapter title number : 'chapter title' / Paragraph('number')]
font_size=96pt
text_align=right

Variables

Variables can be used for values that are used in multiple style definitions. This example declares a number of typefaces to allow easily replacing the fonts in a style sheet:

[VARIABLES]
mono_typeface=TeX Gyre Cursor
serif_typeface=TeX Gyre Pagella
sans_typeface=Tex Gyre Heros
thin_black_stroke=0.5pt,#000
blue=#20435c

It also defines the thin_black_stroke line style for use in table and frame styles, and a specific color labelled blue. These variables can be referenced in style definitions as follows:

[code block]
typeface=$(mono_typeface)
font_size=9pt
text_align=LEFT
indent_first=0
space_above=6pt
space_below=4pt
border=$(thin_black_stroke)
padding_left=5pt
padding_top=1pt
padding_bottom=3pt

Another stylesheet can inherit (see below) from this one and easily replace fonts in the document by overriding the variables.

Style Attribute Resolution

The element styling system makes a distinction between text (inline) elements and flowables with respect to how attribute values are resolved.

Text elements by default inherit the properties from their parent. Take for example the emphasis style definition from the example above. The value for style properties other than font_slant (which is defined in the emphasis style itself) will be looked up in the style definition corresponding to the parent element, which can be either another StyledText instance, or a Paragraph. If the parent element is a StyledText that neither defines the style attribute, lookup proceeds recursively, moving up in the document tree.

For flowables, there is no fall-back to the parent element’s style by default.

Style definitions for both types of elements accept a base attribute. When set, attribute lookup is first attempted in the referenced style before fallback to the parent element’s style or default style (described above). This can help avoid duplication of style information and the resulting maintenance difficulties. In the following example, the unnumbered heading level 1 style inherits all properties from heading level 1, overriding only the number_format attribute:

[heading level 1]
typeface=$(sans_typeface)
font_weight=BOLD
font_size=16pt
font_color=$(blue)
line_spacing=SINGLE
space_above=18pt
space_below=12pt
number_format=NUMBER
label_suffix=' '

[unnumbered heading level 1]
base=heading level 1
number_format=None

In addition to the names of other styles, the base attribute accepts two special values:

NEXT_STYLE

If a style attribute is not set for the style, lookup will continue in the next style matching the element (with a lower specificity). This is similar to how CSS works. You can use this together with the + priority modifier to override some attributes for a set of elements that match different styles.

PARENT_STYLE

This enables fallback to the parent element’s style for flowables. Note that this requires that the current element type is the same or a subclass of the parent type, so it cannot be used for all styles.

When a value for a particular style attribute is set nowhere in the style definition lookup hierarchy, its default value is returned. The default values for all style properties are defined in the class definition for each of the Style subclasses.

Style Logs

When rendering a document, rinohtype will create a style log. It is written to disk using the same base name as the output file, but with a .stylelog extension. The information logged in the style log is invaluable when developing a style sheet. It tells you which style maps to each element in the document.

The style log lists the document elements that have been rendered to each page as a tree. The corresponding location of each document element in the source document is displayed on the same line, if the frontend provides this information. This typically includes the filename, line number and possibly other information such as a node name.

For each document element, all matching styles are listed together with their specificity, ordered from high to low specificity. No styles are listed when there aren’t any selectors matching the element; the default attribute value is used (for flowables) or looked up in the parent element(s) (for text/inline elements). See attribute-resolution for specifics.

The winning style is indicated with a > symbol in front of it. Styles that are not defined in the style sheet or its base(s) are marked with an x. Each style name is followed by the (file)name of the top-most stylesheet where it is defined (within brackets) and the name of its base style (after >).

Here is an example excerpt from a style log:

...
  Paragraph('January 03, 2012', style='title page date')
       > (0,0,1,0,2) title page date [Sphinx] > DEFAULT
         (0,0,0,0,2) body [Sphinx] > default
    SingleStyledText('January 03, 2012')
---------------------------------- page 3 ----------------------------------
#### FootnoteContainer('footnotes')
  StaticGroupedFlowables()
#### DownExpandingContainer('floats')
  StaticGroupedFlowables()
#### ChainedContainer('column1')
  DocumentTree(id='restructuredtext-demonstration')
    Section(id='structural-elements')           demo.txt:62 <section>
         > (0,0,0,1,4) content chapter [Sphinx] > chapter
           (0,0,0,1,2) chapter [Sphinx] > DEFAULT
      Heading('1 Structural Elements')          demo.txt:62 <title>
           > (0,0,0,1,2) heading level 1 [Sphinx] > DEFAULT
             (0,0,0,0,2) other heading levels [Sphinx] > heading level 5
        MixedStyledText('1 ')
          SingleStyledText('1')
          SingleStyledText(' ')
        SingleStyledText('Structural Elements')
      Paragraph('A paragraph.')                 demo.txt:64 <paragraph>
           > (0,0,0,0,2) body
        MixedStyledText('A paragraph.')
          SingleStyledText('A paragraph.')
      List(style='bulleted')                    demo.txt:124 <bullet_list>
           > (0,0,1,0,2) bulleted list [Sphinx] > enumerated list
        ListItem()                              None:None <list_item>
             x (0,0,1,0,4) bulleted list item
          ListItemLabel('•')
               > (0,0,1,0,6) bulleted list item label [Sphinx] > list item label
                 (0,0,0,0,2) list item label [Sphinx] > default
            SingleStyledText('•')
          StaticGroupedFlowables()
               > (0,0,0,0,3) list item body [Sphinx] > DEFAULT
            Paragraph('A bullet list')          demo.txt:124 <paragraph>
                 > (0,0,0,0,5) list item paragraph [Sphinx] > default
                   (0,0,0,0,2) body [Sphinx] > default
              MixedStyledText('A bullet list')
                MixedStyledText('A bullet list')
                  SingleStyledText('A bullet list')
            List(style='bulleted')              demo.txt:126 <bullet_list>
                 > (0,0,1,0,5) nested bulleted list [Sphinx] > bulleted list
                   (0,0,1,0,2) bulleted list [Sphinx] > enumerated list
              ListItem()                        None:None <list_item>
                   x (0,0,1,0,4) bulleted list item
                ListItemLabel('•')
                     > (0,0,1,0,6) bulleted list item label [Sphinx] > list item label
                       (0,0,0,0,2) list item label [Sphinx] > default
                  SingleStyledText('•')
                StaticGroupedFlowables()
                     > (0,0,0,0,3) list item body [Sphinx] > DEFAULT
...