Designing styles

Rich text

Pybtex has a set of classes for working with formatted text and producing formatted output. A piece of formatted text in Pybtex is represented by a Text object. A Text is basically a container that holds a list of

  • plain text parts, represented by String objects,

  • formatted parts, represented by Tag and HRef objects.

The basic workflow is:

  1. Construct a Text object.

  2. Render it as LaTeX, HTML or other markup.

>>> from pybtex.richtext import Text, Tag
>>> text = Text('How to be ', Tag('em', 'a cat'), '.')
>>> print(text.render_as('html'))
How to be <em>a cat</em>.
>>> print(text.render_as('latex'))
How to be \emph{a cat}.

Rich text classes

There are several rich text classes in Pybtex:

Text is the top level container that may contain String, Tag, and HRef objects. When a Text object is rendered into markup, it renders all of its child objects, then concatenates the result.

String is just a wrapper for a single Python string.

Tag and HRef are also containers that may contain other String, Tag, and HRef objects. This makes nested formatting possible. For example, this stupidly formatted text:

is represented by this object tree:

>>> text = Text(
...     HRef('', Tag('em', 'Comprehensive'), ' TeX Archive Network'),
...     ' is ',
...     Tag('em', 'comprehensive'),
...     '.',
... )
>>> print(text.render_as('html'))
<a href=""><em>Comprehensive</em> TeX Archive Network</a> is <em>comprehensive</em>.

Protected represents a “protected” piece of text, something like {braced text} in BibTeX. It is not affected by case-changing operations, like Text.upper() or Text.lower(), and is not splittable by Text.split().

All rich text classes share the same API which is more or less similar to plain Python strings.

Like Python strings, rich text objects are supposed to be immutable. Methods like Text.append() or Text.upper() return a new Text object instead of modifying the data in place. Attempting to modify the contents of an existing Text object is not supported and may lead to weird results.

Here we document the methods of the Text class. The other classes have the same methods.

class pybtex.richtext.Text(*parts)

The Text class is the top level container that may contain String, Tag or HRef objects.


Create a text object consisting of one or more parts.

Empty parts are ignored:

>>> Text() == Text('') == Text('', '', '')
>>> Text('Word', '') == Text('Word')

Text() objects are unpacked and their children are included directly:

>>> Text(Text('Multi', ' '), Tag('em', 'part'), Text(' ', Text('text!')))
Text('Multi ', Tag('em', 'part'), ' text!')
>>> Tag('strong', Text('Multi', ' '), Tag('em', 'part'), Text(' ', 'text!'))
Tag('strong', 'Multi ', Tag('em', 'part'), ' text!')

Similar objects are merged together:

>>> Text('Multi', Tag('em', 'part'), Text(Tag('em', ' ', 'text!')))
Text('Multi', Tag('em', 'part text!'))
>>> Text('Please ', HRef('/', 'click'), HRef('/', ' here'), '.')
Text('Please ', HRef('/', 'click here'), '.')

Rich text objects support equality comparison:

>>> Text('Cat') == Text('cat')
>>> Text('Cat') == Text('Cat')

len(text) returns the number of characters in the text, ignoring the markup:

>>> len(Text('Long cat'))
>>> len(Text(Tag('em', 'Long'), ' cat'))
>>> len(Text(HRef('', 'Long'), ' cat'))

value in text returns True if any part of the text contains the substring value:

>>> 'Long cat' in Text('Long cat!')

Substrings splitted across multiple text parts are not matched:

>>> 'Long cat' in Text(Tag('em', 'Long'), 'cat!')

Slicing and extracting characters works like with regular strings, formatting is preserved.

>>> Text('Longcat is ', Tag('em', 'looooooong!'))[:15]
Text('Longcat is ', Tag('em', 'looo'))
>>> Text('Longcat is ', Tag('em', 'looooooong!'))[-1]
Text(Tag('em', '!'))

Concatenate this Text with another Text or string.

>>> Text('Longcat is ') + Tag('em', 'long')
Text('Longcat is ', Tag('em', 'long'))

Add a period to the end of text, if the last character is not “.”, “!” or “?”.

>>> text = Text("That's all, folks")
>>> print(str(text.add_period()))
That's all, folks.
>>> text = Text("That's all, folks!")
>>> print(str(text.add_period()))
That's all, folks!

Append text to the end of this text.

For Tags, HRefs, etc. the appended text is placed inside the tag.

>>> text = Tag('strong', 'Chuck Norris')
>>> print((text +  ' wins!').render_as('html'))
<strong>Chuck Norris</strong> wins!
>>> print(text.append(' wins!').render_as('html'))
<strong>Chuck Norris wins!</strong>

Capitalize the first letter of the text.

>>> Text(Tag('em', 'long Cat')).capfirst()
Text(Tag('em', 'Long Cat'))

Capitalize the first letter of the text and lowercase the rest.

>>> Text(Tag('em', 'LONG CAT')).capitalize()
Text(Tag('em', 'Long cat'))

Return True if the text ends with the given suffix.

>>> Text('Longcat!').endswith('cat!')

Suffixes split across multiple parts are not matched:

>>> Text('Long', Tag('em', 'cat'), '!').endswith('cat!')

Return True if all characters in the string are alphabetic and there is at least one character, False otherwise.


Join a list using this text (like string.join)

>>> letters = ['a', 'b', 'c']
>>> print(str(String('-').join(letters)))
>>> print(str(String('-').join(iter(letters))))

Convert rich text to lowercase.

>>> Text(Tag('em', 'Long cat')).lower()
Text(Tag('em', 'long cat'))

Render this Text into markup.


backend – The formatting backend (an instance of pybtex.backends.BaseBackend).


Render this Text into markup. This is a wrapper method that loads a formatting backend plugin and calls Text.render().

>>> text = Text('Longcat is ', Tag('em', 'looooooong'), '!')
>>> print(text.render_as('html'))
Longcat is <em>looooooong</em>!
>>> print(text.render_as('latex'))
Longcat is \emph{looooooong}!
>>> print(text.render_as('text'))
Longcat is looooooong!

backend_name – The name of the output backend (like "latex" or "html").

split(sep=None, keep_empty_parts=None)
>>> Text('a + b').split()
[Text('a'), Text('+'), Text('b')]
>>> Text('a, b').split(', ')
[Text('a'), Text('b')]

Return True if the text starts with the given prefix.

>>> Text('Longcat!').startswith('Longcat')

Prefixes split across multiple parts are not matched:

>>> Text(Tag('em', 'Long'), 'cat!').startswith('Longcat')

Convert rich text to uppsercase.

>>> Text(Tag('em', 'Long cat')).upper()
Text(Tag('em', 'LONG CAT'))
class pybtex.richtext.String(*parts)

A String is a wrapper for a plain Python string.

>>> from pybtex.richtext import String
>>> print(String('Crime & Punishment').render_as('text'))
Crime & Punishment
>>> print(String('Crime & Punishment').render_as('html'))
Crime &amp; Punishment

String supports the same methods as Text.

class pybtex.richtext.Tag(name, *args)

A Tag represents something like an HTML tag or a LaTeX formatting command:

>>> from pybtex.richtext import Tag
>>> tag = Tag('em', 'The TeXbook')
>>> print(tag.render_as('html'))
<em>The TeXbook</em>
>>> print(tag.render_as('latex'))
\emph{The TeXbook}

Tag supports the same methods as Text.

class pybtex.richtext.HRef(url, *args, external=False)

A HRef represends a hyperlink:

>>> from pybtex.richtext import Tag
>>> href = HRef('', 'CTAN')
>>> print(href.render_as('html'))
<a href="">CTAN</a>
>>> print(href.render_as('latex'))
>>> href = HRef(String(''), String(''))
>>> print(href.render_as('latex'))

HRef supports the same methods as Text.

class pybtex.richtext.Protected(*args)

A Protected represents a “protected” piece of text.

  • Protected.lower(), Protected.upper(), Protected.capitalize(), and Protected.capitalize() are no-ops and just return the Protected object itself.

  • Protected.split() never splits the text. It always returns a one-element list containing the Protected object itself.

  • In LaTeX output, Protected is {surrounded by braces}. HTML and plain text backends just output the text as-is.

>>> from pybtex.richtext import Protected
>>> text = Protected('The CTAN archive')
>>> text.lower()
Protected('The CTAN archive')
>>> text.split()
[Protected('The CTAN archive')]
>>> print(text.render_as('latex'))
{The CTAN archive}
>>> print(text.render_as('html'))
<span class="bibtex-protected">The CTAN archive</span>

New in version 0.20.

class pybtex.richtext.Symbol(name)

A special symbol. This class is rarely used and may be removed in future versions.

Examples of special symbols are non-breaking spaces and dashes.

Symbol supports the same methods as Text.

Style API

A formatting style in Pybtex is a class inherited from

class, name_style=None, sorting_style=None, abbreviate_names=False, min_crossrefs=2, **kwargs)

The base class for pythonic formatting styles.

format_bibliography(bib_data, citations=None)

Format bibliography entries with the given keys and return a FormattedBibliography object.


Pybtex loads the style class as a plugin, instantiates it with proper parameters and calls the format_bibliography() method that does the actual formatting job. The default implementation of format_bibliography() calls a format_<type>() method for each bibliography entry, where <type> is the entry type, in lowercase. For example, to format an entry of type book, the format_book() method is called. The method must return a Text object. Style classes are supposed to implement format_<type>() methods for all entry types they support. If a formatting method is not found for some entry, Pybtex complains about unsupported entry type.

An example minimalistic style:

from import BaseStyle
from pybtex.richtext import Text, Tag

class MyStyle(BaseStyle):
    def format_article(self, entry):
        return Text('Article ', Tag('em', entry.fields['title']))

Template language

Manually creating Text objects may be tedious. Pybtex has a small template language to simplify common formatting tasks, like joining words with spaces, adding commas and periods, or handling missing fields.

The template language is is not very documented for now, so you should look at the code in the module and the existing styles.

An example formatting style using template language:

from import BaseStyle, toplevel
from import field, join, optional

class MyStyle(BaseStyle):
    def format_article(self, entry):
        if entry.fields['volume']:
            volume_and_pages = join [field('volume'), optional [':', pages]]
            volume_and_pages = words ['pages', optional [pages]]
        template = toplevel [
            sentence [field('title')],
            sentence [
                tag('emph') [field('journal')], volume_and_pages, date],
        return template.format_data(entry)