Skip to main content

ChordPro parser, renderer, and Flask extension

Project description

chordpro

A library that renders ChordPro-formatted song content as HTML, plain text, PDF, or Quill Delta, with support for multiple chord notations.

It was originally designed as a Flask extension, but can be installed without Flask to use the command-line conversion tool or the Python API directly.

Installation

# Command-line tool and Python API only:
pip install chordpro-renderer

# Flask extension (also installs Flask):
pip install chordpro-renderer[flask]

# PDF rendering (also installs reportlab):
pip install chordpro-renderer[pdf]

If you use flask-babel for i18n, install the optional extra so section labels are translatable (also installs Flask):

pip install chordpro-renderer[babel]

See Internationalization for setup details.

Flask extension

Initialization

Direct:

from flask import Flask
from chordpro import ChordPro

app = Flask(__name__)
ChordPro(app)

Application factory:

from chordpro import ChordPro

chordpro = ChordPro()

def create_app():
    app = Flask(__name__)
    chordpro.init_app(app)
    return app

Template filters

The extension registers two Jinja2 filters: chordpro and format_key.

chordpro

Converts a ChordPro string to the requested format. Chord roots are translated to the active notation (see Chord notation).

{{ song.content | chordpro }}                  {# HTML (default) #}
{{ song.content | chordpro("text") }}          {# plain text #}
{{ song.content | chordpro("quill-delta") }}   {# Quill Delta dict #}
{{ song.content | chordpro("pdf") }}           {# PDF bytes (requires chordpro[pdf]) #}

Example input:

{start_of_verse}
[G]Amazing [D]grace, how [Em]sweet the [C]sound
{end_of_verse}

Example output (HTML, simplified):

<div class="cp-section" data-section="verse">
  <div class="cp-section-label">Verse</div>
  <div class="cp-line">
    <span class="cp-unit">
      <span class="cp-chord" data-chord="G">G</span>
      <span class="cp-lyric">Amazing </span>
    </span>
    <span class="cp-unit">
      <span class="cp-chord" data-chord="D">D</span>
      <span class="cp-lyric">grace, how </span>
    </span>
    ...
  </div>
</div>

format_key

Formats a key integer (0–23) as a human-readable key name in the active notation. Keys 0–11 are major; keys 12–23 are minor.

{{ song.key | format_key }}   {# e.g. "C", "F#m" #}

Key integer mapping (standard notation):

key_int Major key_int Minor
0 A 12 F#m
1 Bb 13 Gm
2 B 14 G#m
3 C 15 Am
4 C# 16 Bbm
5 D 17 Bm
6 D# 18 Cm
7 E 19 C#m
8 F 20 Dm
9 F# 21 D#m
10 G 22 Em
11 Ab 23 Fm

Flask CLI

The extension also registers a flask chordpro command group:

flask chordpro convert song.cho
flask chordpro convert song.cho --format text --notation german

Chord notation

Both filters read flask.g.notation at render time. Set it in a before_request hook to change how chord roots and key names are displayed.

from flask import g

@app.before_request
def set_notation():
    g.notation = current_user.notation_preference  # e.g. "standard", "german", "latin", "nashville"

If g.notation is not set, "standard" is used.

Supported notations

Value Description C major becomes
"standard" English letter names C
"german" German convention (B♭ → B, B → H) C
"latin" Solfège (Do Re Mi…) Do
"nashville" Nashville Number System (relative to key) 1

Nashville notation

When g.notation is "nashville", the filter also reads g.key to determine the root. If g.key is not set, the song's first {key:} metadata value is used, falling back to "C".

@app.before_request
def set_notation():
    g.notation = "nashville"
    g.key = current_user.key_preference  # e.g. "G", "Bb", "Am"

Command-line tool

# Convert to HTML (default)
chordpro song.cho

# Convert to plain text
chordpro song.cho --format text

# Convert to Quill Delta JSON
chordpro song.cho --format quill-delta

# Use German notation
chordpro song.cho --notation german

# Use Nashville notation (key defaults to song metadata or C)
chordpro song.cho --notation nashville --key G

# Read from stdin
cat song.cho | chordpro -

Multiple files

Pass multiple files to combine them into a single output. For PDF, each song starts on a new page.

# Combine into a single HTML document
chordpro song1.cho song2.cho song3.cho

# Build a multi-song PDF songbook
chordpro *.cho --format pdf > songbook.pdf

# Combine from stdin
cat song1.cho song2.cho | chordpro -

Options:

Option Short Choices Default
--format -f html, text, quill-delta, pdf html
--notation -n standard, german, latin, nashville standard
--key -k any key string (e.g. C, G, Bb) song metadata or C

Nashville + multiple files: when --notation nashville is used with multiple files, pass --key explicitly. Without it, the key defaults to the first song's metadata.

Supported ChordPro directives

Metadata directives

These populate the SongMeta object and produce no output in the body.

Directive Short form Field
{title: …} {t: …} meta.title
{subtitle: …} {st: …} meta.subtitle
{artist: …} meta.artist
{album: …} meta.album
{composer: …} meta.composer
{lyricist: …} meta.lyricist
{copyright: …} meta.copyright
{year: …} meta.year
{key: …} meta.key
{time: …} meta.time
{tempo: …} meta.tempo
{duration: …} meta.duration
{capo: …} meta.capo
{meta: name value} meta.meta[name]
{tag: …} meta.meta["tag"]

Fields that may appear multiple times (subtitle, artist, key, etc.) are stored as list[str]. Singular fields (title, year, duration, capo) are str | None.

Section directives

All section directives accept an optional label override: {start_of_verse: Verse 2}.

Directive Short form Section type
{start_of_verse} / {end_of_verse} {sov} / {eov} Verse
{start_of_chorus} / {end_of_chorus} {soc} / {eoc} Chorus
{start_of_bridge} / {end_of_bridge} {sob} / {eob} Bridge
{start_of_prechorus} / {end_of_prechorus} PreChorus
{start_of_outro} / {end_of_outro} Outro
{start_of_intro} / {end_of_intro} Intro
{start_of_tab} / {end_of_tab} {sot} / {eot} Tab
{start_of_grid} / {end_of_grid} {sog} / {eog} Grid
{start_of_tag} / {end_of_tag} Tag
{start_of_interlude} / {end_of_interlude} Interlude
{start_of_solo} / {end_of_solo} Solo
{start_of_instrumental} / {end_of_instrumental} Instrumental
{start_of_abc} / {end_of_abc} Abc
{start_of_ly} / {end_of_ly} Lilypond
{start_of_svg} / {end_of_svg} Svg
{start_of_textblock} / {end_of_textblock} TextBlock
{start_of_*} / {end_of_*} Section (generic)

All sections render as <div class="cp-section" data-section="<kind>"> in HTML.

Content directives

Directive Short form HTML output
{comment: text} {c: text} <div class="cp-comment">
{comment_italic: text} {ci: text} <div class="cp-comment cp-comment-italic">
{comment_box: text} <div class="cp-comment cp-comment-box">
{highlight: text} <div class="cp-highlight">
{chorus} / {chorus: label} <div class="cp-chorus-ref">
{image: …} <div class="cp-image" data-raw="…">
{chord: name} <div class="cp-chord-diagram" data-chord="…">
{define: name …} Parsed; no visual output
{transpose: N} <span class="cp-transpose" data-semitones="N" hidden>

Layout directives

Directive Short form HTML output
{new_page} {np} <div class="cp-new-page">
{new_physical_page} {npp} <div class="cp-new-physical-page">
{column_break} {cb} <div class="cp-column-break">
{columns: N} {col: N} <div class="cp-columns" data-count="N">
{grid} {g} <span class="cp-grid-on" hidden>
{no_grid} {ng} <span class="cp-grid-off" hidden>
{new_song} {ns} <hr class="cp-new-song">

All other directives are silently ignored.

CSS classes

Style the rendered HTML output with these classes:

Class Element
cp-song Wrapper for a single song in multi-song render_many() HTML output (div)
cp-section Section wrapper (div); data-section holds the kind
cp-section-label Section heading (div)
cp-line A line containing chords and lyrics (div)
cp-unit A chord+lyric pair (span)
cp-chord Chord name (span); data-chord holds the standard-notation root
cp-lyric Lyric beneath a chord (span)
cp-lyric-only Lyric with no chord above it (span)
cp-lyric-line A plain lyric line with no chords (div)
cp-break An empty line between paragraphs (div)
cp-comment A comment directive (div)
cp-comment-italic Added alongside cp-comment for {comment_italic}
cp-comment-box Added alongside cp-comment for {comment_box}
cp-highlight A {highlight} directive (div)
cp-chorus-ref A {chorus} reference directive (div)
cp-chorus-ref-label Label inside a chorus reference (span)
cp-image An {image} directive (div); data-raw holds the full value
cp-chord-diagram A {chord} directive (div); data-chord holds the chord name
cp-transpose A {transpose} directive (span, hidden); data-semitones holds the offset
cp-new-page A {new_page} directive (div)
cp-new-physical-page A {new_physical_page} directive (div)
cp-column-break A {column_break} directive (div)
cp-columns A {columns} directive (div); data-count holds the count
cp-grid-on A {grid} directive (span, hidden)
cp-grid-off A {no_grid} directive (span, hidden)
cp-new-song A {new_song} directive (hr)

Public API

All public symbols are importable directly from chordpro.

Parsing

from chordpro import parse, Song, SongMeta

song = parse(content)   # returns a Song dataclass
song.meta.title         # str | None
song.meta.artist        # list[str]
song.meta.key           # list[str]
song.body               # list of SongItem (sections and lines)

Rendering a single song

from chordpro import render, build_chord_semi_to_name, build_nashville_semi_to_name, key_to_semitone

semi_to_name = build_chord_semi_to_name("latin")
html  = render(song, semi_to_name, format="html")
text  = render(song, semi_to_name, format="text")
delta = render(song, semi_to_name, format="quill-delta")
pdf   = render(song, semi_to_name, format="pdf")   # bytes; requires chordpro[pdf]

# Nashville
semi_to_name = build_nashville_semi_to_name(key_to_semitone("G"))
html = render(song, semi_to_name)

Backward-compatible one-shot helpers are also available:

from chordpro import chordpro_to_html, render_html

html = chordpro_to_html(content, semi_to_name)   # parse + render in one call
html = render_html(song, semi_to_name)

Rendering multiple songs

Use render_many() to combine a list of Song objects into a single output. Songs are merged in order with format-appropriate separators.

from chordpro import parse, render_many, build_chord_semi_to_name

songs = [parse(open(f).read()) for f in ["song1.cho", "song2.cho", "song3.cho"]]
semi_to_name = build_chord_semi_to_name("standard")

html  = render_many(songs, semi_to_name, format="html")         # each song in <div class="cp-song">
text  = render_many(songs, semi_to_name, format="text")         # songs joined by form-feed (\f)
delta = render_many(songs, semi_to_name, format="quill-delta")  # page_break op between songs
pdf   = render_many(songs, semi_to_name, format="pdf")          # new page per song (bytes)

with open("songbook.pdf", "wb") as f:
    f.write(pdf)
Format Separator
"html" Each song wrapped in <div class="cp-song">
"text" Songs joined by a form-feed character (\f)
"quill-delta" A {"insert": "\n", "attributes": {"page_break": true}} op between songs
"pdf" A hard page break (PageBreak) between songs

Custom renderers that do not override render_many() receive a list of individual render() results.

PDF rendering

PdfRenderer is built in and registered as "pdf". It requires the reportlab package (pip install chordpro[pdf]).

from chordpro import parse, render, render_many

# Single song
song = parse(open("song.cho").read())
pdf_bytes = render(song, format="pdf")

# Multiple songs — each starts on a new page
songs = [parse(open(f).read()) for f in ["song1.cho", "song2.cho"]]
pdf_bytes = render_many(songs, format="pdf")

with open("songbook.pdf", "wb") as f:
    f.write(pdf_bytes)

The rendered PDF includes a header with title, subtitle, artist, and key metadata. Each section is labelled. Chord lines use the classic two-row layout — chords printed in blue directly above the corresponding lyric syllables, columns aligned by segment width. {new_page} emits a hard page break within a song; render_many() inserts a page break between songs.

You can subclass PdfRenderer to adjust fonts, sizes, margins, or page size:

from reportlab.lib.pagesizes import A4
from chordpro import PdfRenderer

class MyRenderer(PdfRenderer):
    PAGE_SIZE = A4
    MARGIN = 1.5        # inches
    LYRIC_SIZE = 12
    CHORD_SIZE = 10

Custom renderers

Subclass BaseRenderer, implement render(), and register the class under a name. Once registered, the name works as the format argument to render(), render_many(), and the chordpro Jinja2 filter.

from chordpro import BaseRenderer, register_renderer

class SvgRenderer(BaseRenderer):
    def render(self, song, semi_to_name=None):
        ...  # return SVG string

    def render_many(self, songs, semi_to_name=None):
        ...  # return combined output; omit to get list of render() results

register_renderer("svg", SvgRenderer)
{{ song.content | chordpro("svg") }}

You can also pass a renderer instance directly to the filter:

{{ song.content | chordpro(my_renderer_instance) }}

Internationalization

Section labels (Verse, Chorus, Bridge, etc.) are marked for translation using flask-babel's lazy_gettext. The package ships compiled translations for German (de), French (fr), and Spanish (es).

Enabling translations in your app

Tell flask-babel where to find the chordpro translation catalog by including the package's translations/ directory alongside your own:

import chordpro
import os

app = Flask(__name__)
babel = Babel(app)

# Merge chordpro's translations with your app's own catalog
chordpro_translations = os.path.join(os.path.dirname(chordpro.__file__), "translations")
app.config["BABEL_TRANSLATION_DIRECTORIES"] = f"{chordpro_translations};translations"

With that in place, flask-babel resolves the active locale automatically (via get_locale) and the section labels rendered by the chordpro Jinja2 filter will be translated accordingly.

Adding a new language

# 1. Re-extract strings (keeps existing translations)
pybabel extract -F babel.cfg -o messages.pot .

# 2. Initialize a new locale (e.g. Portuguese)
pybabel init -i messages.pot -d chordpro/translations -l pt

# 3. Fill in the msgstr values in chordpro/translations/pt/LC_MESSAGES/messages.po

# 4. Compile
pybabel compile -d chordpro/translations

Updating after source changes

pybabel extract -F babel.cfg -o messages.pot .
pybabel update -i messages.pot -d chordpro/translations
pybabel compile -d chordpro/translations

Development

# Install with dev dependencies
uv sync --group dev

# Run the test suite
uv run pytest

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

chordpro_renderer-1.0.6.tar.gz (24.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

chordpro_renderer-1.0.6-py3-none-any.whl (34.8 kB view details)

Uploaded Python 3

File details

Details for the file chordpro_renderer-1.0.6.tar.gz.

File metadata

  • Download URL: chordpro_renderer-1.0.6.tar.gz
  • Upload date:
  • Size: 24.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.6.3

File hashes

Hashes for chordpro_renderer-1.0.6.tar.gz
Algorithm Hash digest
SHA256 57916f723ccfe8a41fbbfa1b971fe470e66b8051fd1bc7239cc2bab755e0f45c
MD5 2b288938083e3f7d1dbb32570d1c13db
BLAKE2b-256 6a69a2f7e0ea1dc965d2dc9eda669ac278c53e91340407c4fbcfa6bffa2b9218

See more details on using hashes here.

File details

Details for the file chordpro_renderer-1.0.6-py3-none-any.whl.

File metadata

File hashes

Hashes for chordpro_renderer-1.0.6-py3-none-any.whl
Algorithm Hash digest
SHA256 4f73513934eb8296974d1052361a48572a0b02af2be68cef1b51d7a79680b526
MD5 9340beca1692e26d4090c096ace95816
BLAKE2b-256 769a49dfca3bf44f404ab20535d6a5747152c2fbc1af57da8ef4c75ba22a8736

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page