Skip to content

Commit

Permalink
Additional tests & bugfixes, updated README, changed the way of handl…
Browse files Browse the repository at this point in the history
…ing parsing notations
  • Loading branch information
intelektron committed Jan 15, 2024
1 parent 0e9ac48 commit e40e3bc
Show file tree
Hide file tree
Showing 23 changed files with 1,098 additions and 169 deletions.
206 changes: 150 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
# chordpro-php
# The chordpro-php library

A simple tool to parse, transpose & format [ChordPro](https://www.chordpro.org) songs with lyrics & chords.

Forked from <https://github.com/nicolaswurtz/chordpro-php> by [Nicolaz Wurtz](https://github.com/nicolaswurtz), on LGPL-3 license.

It currently supports the following output formats :
- HTML (verses contain blocks with embricated `span` for alignement of chords with lyrics)
- JSON (verses are array of arrays of chords and lyrics for alignement purpose)
- Plain text (chords are aligned with monospace text thanks to whitespaces)
The following output formats are currently supported

And provides some extra functionnalities :
- Tranpose chords (can be very clever if original key is known)
- Display french chords
- Guess tonality key of a song
- HTML (verses contain blocks with embedded `span` for aligning chords with lyrics)
- JSON (verses are arrays of chords and lyrics for alignment purposes)
- Plain text (chords are aligned with monospace text thanks to whitespace)

_I'm french, so there's probably a lot of mistakes, my english is not always accurate — je fais ce que je peux hein :P_
And provides some extra functionality:

- Tranpose chords by semitones or to the target key.
- Parse and display various chord notations:
- French (`Do`, ``, `Mi`)
- German (`Fis`, `a`)
- With UTF characters (`A♭`, `F♯`)
- Guess the key of a song.

## Install

Via composer :
Via composer:

``` bash
$ composer require intelektron/chordpro-php
composer require intelektron/chordpro-php
```

## Usage
Expand All @@ -38,8 +41,8 @@ $txt = "{t:ChordpropPHP Song}
{c:GPL3 2019 Nicolas Wurtz}
{key:C}
[C]This is the [Dm]beautiful [Em]song
I [Dm]wroted in [F/G]Chordpro for[C]mat [Dm/F]
Let's singing a[C/E]long
I [Dm]wrote in [F/G]Chordpro for[C]mat [Dm/F]
Let's sing it a[C/E]long
[Bb] It's ea[Dm]sy to do [F]that [C]

{soc}
Expand All @@ -52,81 +55,172 @@ Let's singing a[C/E]long

$parser = new ChordPro\Parser();

// Choose one (or all !) of those formatters following your needs
$html_formatter = new ChordPro\HtmlFormatter();
$monospace_formatter = new ChordPro\MonospaceFormatter();
$json_formatter = new ChordPro\JSONFormatter();
// Choose one of these formatters according to your needs.
$htmlFormatter = new ChordPro\Formatter\HtmlFormatter();
$monospaceFormatter = new ChordPro\Formatter\MonospaceFormatter();
$jsonFormatter = new ChordPro\Formatter\JSONFormatter();

// Create song object after parsing txt
// Create song object after parsing $txt.
$song = $parser->parse($txt);

// You can tranpose your song, put how many semitones you want to transpose in second argument OR desired key (only if metadata "key" is defined)
// You can transpose your song.
$transposer = new ChordPro\Transposer();
$transposer->transpose($song,-5); // Simple transpose, but could produce some musical errors (sharp instead of flat)
//$transposer->transpose($song,'Abm');

// Some options are mandatory, you could use en empty array if none
$options = array(
'french' => true,
'no_chords' => true
);
// Define how many semitones you want to transpose by.
$transposer->transpose($song,-5);

// If the song key is known, you can also transpose from key to key.
// $transposer->transpose($song,'Abm');

// The formatter has some options
$options = [
'ignore_metadata' => 'title',
];

// Render !
$html = $html_formatter->format($song,$options);
$plaintext = $monospace_formatter->format($song,$options);
$json = $json_formatter->format($song,$options);
$html = $htmlFormatter->format($song, $options);
$monospaced = $monospaceFormatter->format($song, $options);
$json = $jsonFormatter->format($song, $options);
```

## Formatting options

Simply give an array with values at true or false for each key/option.

``` php
array(
'french' => true, // to display french chords (Do, Ré, Mi, Fa, Sol, La, Si, Do), including Song Key.
'no_chords' => true // to only get text (it removes "block" system for chords alignements)
);
[
'ignore_metadata' => ['title', 'subtitle'], // Do not render these types of metadata.
'no_chords' => true // Render text without chords.
'notation' => new ChordPro\Notation\GermanChordNotation(), // Choose output chord notation.
];
```

## Specific methods
## The song key

The key can be set/changed in the following ways:

### Song
- `$song->getKey([])` to obtain key of song, **with transposition**, you can alter langage english by default, or French ```$song->getKey(['french' => true]);```, options array is mandatory, you could use en empty array if none
- `$song->getMetadataKey()` to obtain key of song, as defined in metadata's field "key"
- Manually, by calling `$song->setKey('A')`.
- By parsing a song with metadata, e.g. `{key:A}`
- By transposing the song to another key.

You can get the key by calling:

- `$song->getKey()` - get the key if it is defined by `setKey()`, otherwise, use the key from the metadata.
- `$song->getMetadataKey()` - get the key from the metadata.

If the song has no key defined, there is a possibility to guess it. This feature is experimental and not reliable (20% error rate, tested with ~1000 songs), but can be very useful.

### Guess key of a song
This fonctionnality is experimental and not reliable (20% of mistakes, tested with ~1000 songs), but can be very useful.
Usage is very simple (you have to parse a song before as described before):
``` php
$guess = new ChordPro\GuessKey();
$key = $guess->guessKey($song);
```

## CSS Classes you can use with _HTML_ Formatter
## Chord notations

### Verses
_Verses_ are one line composed by blocks of text + chords, chord with class `chordpro-chord` and text with class `chordpro-text`.
The library supports several chord notations. You can also create your own (by implementing `ChordNotationInterface`). Notations are used for both parsing and formatting. So you can parse a song in German notation and display it as French:

A typical `div` will be like this :
``` html
<div class="chordpro-verse">
<span class="chordpro-elem">
```php
$txt = 'A typical [fis]German [a]verse';
$parser = new ChordPro\Parser();
$notation = new ChordPro\Notation\GermanChordNotation();
$song = $parser->parse($song, [$notation])
```

At this point, `fis` is recognized and saved as `F#m`, and `a` is saved as `Am`. Note that you can pass multiple notations to the parser, in order of precedence. This can be useful if you have mixed up chord notations in one song.

Now, to show this song in French:

```php
$monospaceFormatter = new ChordPro\Formatter\MonospaceFormatter();
$html = $monospaceFormatter->format($song, [
'notation' => $frenchNotation
]);

// Fa♯m Lam
// A typical German verse
```

## Styling the HTML code

### Song lines

Lines are made up of blocks. Each block consists of a text and a chord. The chord has the class `chordpro-chord' and the text has the class `chordpro-text'.

A typical line of the song looks like this:

```html
<div class="chordpro-line">
<span class="chordpro-block">
<span class="chordpro-chord">C</span>
<span class="chordpro-text">This is the </span>
</span>
<span class="chordpro-elem">
<span class="chordpro-block">
<span class="chordpro-chord">Dm</span>
<span class="chordpro-text">beautiful song</span>
</span>
</div>
```

### Chorus
The _chorus_ (`soc`/`start_of_chorus`) will be contained inside ```<div class="chordpro-chorus"></div>```.
### Song sections

The ChordPro format allows to organize your songs into sections. The following song fragment:

```chordpro
{start_of_verse Verse 1}
...
{end_of_verse}
{start_of_foobar}
...
{end_of_foobar}
```

Will be converted to:

```html
<div class="chordpro-verse-comment">Verse 1</div>
<div class="chordpro-verse">
...
</div>

<div class="chordpro-foobar">
...
</div>
```

You can use anything in place of `foobar`. However, the following shortcuts are supported:

- `{soc}``{start_of_chorus}`
- `{eoc}``{end_of_chorus}`
- `{sov}``{start_of_verse}`
- `{eov}``{end_of_verse}`
- `{sob}``{start_of_bridge}`
- `{eob}``{end_of_bridge}`
- `{sot}``{start_of_tab}`
- `{eot}``{end_of_tab}`
- `{sog}``{start_of_grid}`
- `{eog}``{end_of_grid}`

### Metadata
By default, all _metadatas_ are placed inside ```<div class="chordpro-metadataname"></div>```.
For example, the _title_ will be

The library reads ChordPro metadata and renders it as HTML in the following way:

```chordpro
{title: Let's Sing!}
{c: Very loud}
```

Becomes:

``` html
<div class="chordpro-title">It's a great title !</div>
<div class="chordpro-title">Let's Sing!</div>
<div class="chordpro-comment">Very loud</div>
```
_ChordproPHP doesn't care about the metadata names, it just puts it after `chordpro-` :)_
> Metatada's names are always converted to their long form (`c` will be recorded as `comment`) when using short names from [official directives](https://www.chordpro.org/chordpro/ChordPro-Directives.html)

The names of metadata are not restricted in any way, however, there are some standard ones described by ChordPro format. The following shortcuts are supported:

- `{t}``{title}`
- `{st}``{subtitle}`
- `{c}``{comment}`
- `{ci}``{comment_italic}`
- `{cb}``{comment_box}`
12 changes: 6 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@
"description": "Parse, transpose and format (html, json, plaintext) ChordPro format for songs lyrics with chords.",
"type": "library",
"require": {
"php": ">=8.1",
"phpcompatibility/php-compatibility": "^9.3"
"php": ">=8.1"
},
"require-dev": {
"phpunit/phpunit": "^10.0",
"phpstan/phpstan": "^1.10",
"phpstan/phpstan-deprecation-rules": "^1.1",
"ekino/phpstan-banned-code": "^1.0",
"friendsofphp/php-cs-fixer": "^3.46",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/extension-installer": "^1.3",
"phpstan/phpstan": "^1.10",
"phpstan/phpstan-deprecation-rules": "^1.1",
"phpstan/phpstan-strict-rules": "^1.5",
"phpunit/phpunit": "^10.0",
"thecodingmachine/phpstan-strict-rules": "^1.0",
"phpstan/extension-installer": "^1.3",
"voku/phpstan-rules": "^3.2"
},
"autoload": {
Expand Down
42 changes: 13 additions & 29 deletions src/Chord.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace ChordPro;

use ChordPro\Notation\ChordNotationInterface;
use PHPUnit\Event\Runtime\PHP;

/**
* A class for chord manipulations.
Expand All @@ -29,17 +30,16 @@ class Chord
private bool $isKnown = false;

/**
* Static cache of different notation roots.
*
* @var string[][]
* @param string $originalName The original name of the chord.
* @param ChordNotationInterface[] $sourceNotations The notations to use, ordered by precedence.
*/
private static array $notationRoots = [];

public function __construct(private string $originalName, ?ChordNotationInterface $sourceNotation = null)
public function __construct(private string $originalName, array $sourceNotations = [])
{
$rootChordTable = $this->getRootChordTable($sourceNotation);
foreach ($sourceNotations as $sourceNotation) {
$originalName = $sourceNotation->convertChordRootFromNotation($originalName);
}

foreach ($rootChordTable as $rootChord) {
foreach (self::ROOT_CHORDS as $rootChord) {
if (str_starts_with($originalName, $rootChord)) {
$this->rootChord = $rootChord;
$this->ext = substr($originalName, strlen($rootChord));
Expand All @@ -55,17 +55,20 @@ public function __construct(private string $originalName, ?ChordNotationInterfac
/**
* Create multiple chords from slices like [C/E].
*
* @param string $text The text to parse.
* @param ChordNotationInterface[] $notations The notations to use, ordered by precedence.
*
* @return Chord[]
*/
public static function fromSlice(string $text, ?ChordNotationInterface $notation = null): array
public static function fromSlice(string $text, array $notations = []): array
{
if ($text === '') {
return [];
}
$chords = explode('/', $text);
$result = [];
foreach ($chords as $chord) {
$result[] = new Chord($chord, $notation);
$result[] = new Chord($chord, $notations);
}
return $result;
}
Expand All @@ -80,25 +83,6 @@ public function isMinor(): bool
return substr($this->rootChord, -1) === 'm';
}

/**
* @return string[] The root chords in the notation.
*/
private function getRootChordTable(?ChordNotationInterface $notation): array
{
if (is_null($notation)) {
return self::ROOT_CHORDS;
} elseif (isset($this->notationRoots[$notation::class])) {
return $this->notationRoots[$notation::class];
} else {
$rootChordTable = [];
foreach (self::ROOT_CHORDS as $rootChord) {
$rootChordTable[] = $notation->convertChordRootFromNotation($rootChord);
}
$this->notationRoots[$notation::class] = $rootChordTable;
return $rootChordTable;
}
}

public function getRootChord(?ChordNotationInterface $targetNotation = null): string
{
if (!is_null($targetNotation)) {
Expand Down
6 changes: 5 additions & 1 deletion src/Formatter/Formatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

abstract class Formatter
{
protected ?ChordNotationInterface $notation;
protected ?ChordNotationInterface $notation = null;
protected bool $noChords = false;

/**
Expand All @@ -21,6 +21,10 @@ abstract class Formatter
*/
public function setOptions(array $options): void
{
$this->notation = null;
$this->noChords = false;
$this->ignoreMetadata = [];

if (isset($options['notation']) && $options['notation'] instanceof ChordNotationInterface) {
$this->notation = $options['notation'];
}
Expand Down
Loading

0 comments on commit e40e3bc

Please sign in to comment.