diff --git a/README.md b/README.md index 99ff6bf..52bc292 100644 --- a/README.md +++ b/README.md @@ -38,32 +38,45 @@ See [web/example.php](web/example.php) for demo with CSS styling. require __DIR__ . '/vendor/autoload.php'; -$txt = "{t:Chordpro-PHP Song} -{st:Nicolas Wurtz} -{c:GPL3 2019 Nicolas Wurtz} +$txt = "{t:A Nice Sample Song} +{st:Grzegorz Pietrzak} {key:C} # Let's start it! -[C]This is the [Dm]beautiful [Em]song -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} -[F] [G] [C]This is the refrain -[F] [G] [C]We could sing it twice +[C]Let's sing this [G]song [Am]together [Em]aloud +[F]It's an [C]example [Dm]with some nice [G]sound + +{soc: Chorus} +[Bb]Whenever you [Am7]need to [Bb]format your [Am7]chords +[Dm]The solution to your [F]problems [G]is very close {eoc} -{c:Final} -[Em/D]This is the [Bb]end. -"; +{comment: Now we recite some text} +Sometimes you write text +And there's no more room for chords + +{comment: Sometimes you play music without any words} +[C] [G] [Am] [Em] + +You don't know where the chords are? ~ [F] [C] +You don't have to know ~ [G] [G/F#] + +{sot: Outro} +E-12---------------------| +B----11-12---------------| +G----------11s13-14------| +D-------------------10-12| +A------------------------| +E------------------------| +{eot} + +{comment: The end} +Let's finish this song. [G] It's the end of the show."; $parser = new ChordPro\Parser(); // 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. $song = $parser->parse($txt); @@ -72,20 +85,18 @@ $song = $parser->parse($txt); $transposer = new ChordPro\Transposer(); // Define how many semitones you want to transpose by. -$transposer->transpose($song, -5); +// $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', + 'ignore_metadata' => 'copyright', ]; // Render ! $html = $htmlFormatter->format($song, $options); -$monospaced = $monospaceFormatter->format($song, $options); -$json = $jsonFormatter->format($song, $options); ``` ![Screenshot](web/example.png) @@ -189,12 +200,12 @@ The ChordPro format allows to organize your songs into sections. The following s Will be converted to: ```html -
Verse 1
-
+
Verse 1
+
...
-
+
...
``` @@ -219,13 +230,18 @@ The library reads ChordPro metadata and renders it as HTML in the following way: ```chordpro {title: Let's Sing!} {c: Very loud} +{key: C} ``` Becomes: ``` html -
Let's Sing!
-
Very loud
+ + + ``` 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: @@ -235,3 +251,18 @@ The names of metadata are not restricted in any way, however, there are some sta - `{c}` → `{comment}` - `{ci}` → `{comment_italic}` - `{cb}` → `{comment_box}` + +## Extensions to ChordPro 6.x + +The library provides some non-standard features that can be useful for songbook creators. + +### Inline chords + +If you don't know how to assign the chord to syllables, or it's not important to you, you can use the inline chords assigned to the lyric line: + +```chordpro +You don't know where the chords are? ~ [F] [C] +You don't have to know ~ [G] [G/F#] +``` + +The chords appear above the line (in the HTML formatter) or to the right (in the monospace formatter). diff --git a/src/Block.php b/src/Block.php index 833f6d7..69e8cbd 100644 --- a/src/Block.php +++ b/src/Block.php @@ -11,7 +11,7 @@ class Block /** * @param Chord[] $chords The chords. */ - public function __construct(private array $chords, private string $text) + public function __construct(private array $chords, private string $text, private bool $lineEnd = false) { } @@ -29,4 +29,9 @@ public function getText(): string { return $this->text; } + + public function isLineEnd(): bool + { + return $this->lineEnd; + } } diff --git a/src/Formatter/HtmlFormatter.php b/src/Formatter/HtmlFormatter.php index 29fe460..8821742 100644 --- a/src/Formatter/HtmlFormatter.php +++ b/src/Formatter/HtmlFormatter.php @@ -52,26 +52,50 @@ private function getMetadataHtml(Metadata $metadata): string return ''; } - $match = []; - if (preg_match('/^start_of_(.*)/', $metadata->getName(), $match) === 1) { - $type = preg_replace('/[\W_\-]/', '', $match[1]); + if ($metadata->isSectionStart()) { + $type = $metadata->getSectionType(); $content = ''; if (null !== $metadata->getValue()) { - $content = '
'.$metadata->getValue()."
\n"; + $content = '\n"; } - return $content.'
'."\n"; - } elseif (preg_match('/^end_of_(.*)/', $metadata->getName()) === 1) { + return $content.'
'."\n"; + } elseif ($metadata->isSectionEnd()) { return "
\n"; } else { - $name = preg_replace('/[\W_\-]/', '', mb_strtolower($metadata->getName())); - return '
'.$metadata->getValue()."
\n"; + if ($metadata->getName() === 'key' && null != $this->notation) { + $value = $this->notation->convertChordRootToNotation($metadata->getValue() ?? ''); + } else { + $value = $metadata->getValue(); + } + $type = $metadata->getNameSlug(); + $output = '\n"; + return $output; } } private function getLyricsHtml(Lyrics $lyrics): string { - $line = '
'."\n"; - foreach ($lyrics->getBlocks() as $block) { + $classes = ['chordpro-line']; + if ($lyrics->hasInlineChords()) { + $classes[] = 'chordpro-line-inline-chords'; + } + if (!$lyrics->hasChords()) { + $classes[] = 'chordpro-line-text-only'; + } + if (!$lyrics->hasText()) { + $classes[] = 'chordpro-line-chords-only'; + } + $line = '
'."\n"; + $lineChords = ''; + $lineText = ''; + foreach ($lyrics->getBlocks() as $num => $block) { $originalChords = []; $chords = []; @@ -96,22 +120,51 @@ private function getLyricsHtml(Lyrics $lyrics): string $originalChord = implode('/', $originalChords); $text = $this->blankChars($block->getText()); - $line .= '' . - ''.$chord.'' . - ''.$text.'' . - ''; + if ($lyrics->hasInlineChords()) { + if ($num === 0) { + $lineText = '
'.$text.'
'; + } else { + $lineChords .= ''.$chord.''; + } + } elseif ($lyrics->hasChords() && $lyrics->hasText()) { + $line .= '' . + ''.$chord.'' . + ''.$text.'' . + ''; + } elseif ($lyrics->hasChords()) { + $line .= '' . + ''.$chord.'' . + ''; + + } elseif ($lyrics->hasText()) { + $line .= '' . + ''.$text.'' . + ''; + } } + + if ($lyrics->hasInlineChords()) { + $line .= '
'; + $line .= '
' . $lineChords . '
'; + $line .= $lineText; + $line .= '
'; + } + $line .= "\n
\n"; return $line; } private function getLyricsOnlyHtml(Lyrics $lyrics): string { - $line = '
'."\n"; + $text = ''; foreach ($lyrics->getBlocks() as $block) { - $line .= ltrim($block->getText()); + $text .= ltrim($block->getText()); + } + $text = rtrim($text); + if ($text === '') { + return ''; + } else { + return '
'."\n".rtrim($text)."\n
\n"; } - $line .= "\n
\n"; - return $line; } } diff --git a/src/Formatter/JSONFormatter.php b/src/Formatter/JSONFormatter.php index 186cc87..dfc3dc3 100644 --- a/src/Formatter/JSONFormatter.php +++ b/src/Formatter/JSONFormatter.php @@ -54,11 +54,32 @@ private function getMetadataJSON(Metadata $metadata): array if (in_array($metadata->getName(), $this->ignoreMetadata, true)) { return []; } - return [ + + if ($metadata->isSectionStart()) { + $metadataItem = [ + 'type' => 'section_start', + 'sectionType' => $metadata->getSectionType(), + ]; + if (null !== $metadata->getValue()) { + $metadataItem['label'] = $metadata->getValue(); + } + } elseif ($metadata->isSectionEnd()) { + $metadataItem = [ + 'type' => 'section_end', + 'sectionType' => $metadata->getSectionType(), + ]; + } else { + $metadataItem = [ 'type' => 'metadata', 'name' => $metadata->getName(), 'value' => $metadata->getValue(), - ]; + ]; + if ($metadata->getHumanName() != $metadata->getName()) { + $metadataItem['humanName'] = $metadata->getHumanName(); + } + } + + return $metadataItem; } /** @@ -80,14 +101,26 @@ private function getLyricsJSON(Lyrics $lyrics): array $originalChords[] = $slicedChord->getOriginalName(); } } - $chord = implode('/', $chords).' '; - $originalChord = implode('/', $originalChords).' '; + $chord = implode('/', $chords); + $originalChord = implode('/', $originalChords); $text = $block->getText(); - $return[] = ['chord' => trim($chord), 'text' => $text, 'originalChord' => trim($originalChord)]; + $blockArray = []; + if ($text !== '') { + $blockArray['text'] = rtrim($text); + } + $chord = trim($chord); + if ($chord !== '') { + $blockArray['chord'] = $chord; + $blockArray['originalChord'] = $originalChord; + } + if ($block->isLineEnd()) { + $blockArray['lineEnd'] = true; + } + $return[] = $blockArray; } return [ - 'type' => 'line', + 'type' => $lyrics->hasInlineChords() ? 'line_inline' : 'line', 'blocks' => $return, ]; } @@ -97,13 +130,18 @@ private function getLyricsJSON(Lyrics $lyrics): array */ private function getLyricsOnlyJSON(Lyrics $lyrics): array { - $return = ''; + $text = ''; foreach ($lyrics->getBlocks() as $block) { - $return .= ltrim($block->getText()); + $text .= ltrim($block->getText()); + } + $text = rtrim($text); + if ($text === '') { + return []; + } else { + return [ + 'type' => 'line', + 'text' => $text, + ]; } - return [ - 'type' => 'line', - 'text' => $return, - ]; } } diff --git a/src/Formatter/MonospaceFormatter.php b/src/Formatter/MonospaceFormatter.php index 4cc2a51..5cb233b 100644 --- a/src/Formatter/MonospaceFormatter.php +++ b/src/Formatter/MonospaceFormatter.php @@ -16,11 +16,13 @@ public function format(Song $song, array $options = []): string { $this->setOptions($options); - $monospace = ''; + $lines = []; foreach ($song->getLines() as $line) { - $monospace .= $this->getLineMonospace($line); + $lines[] = $this->getLineMonospace($line); } - return $monospace; + + $this->transformInlineChords($lines); + return implode("", $lines); } private function getLineMonospace(Line $line): string @@ -43,14 +45,18 @@ private function getMetadataMonospace(Metadata $metadata): string return ''; } - $match = []; - if (preg_match('/^start_of_(.*)/', $metadata->getName(), $match) === 1) { - $content = (null !== $metadata->getValue()) ? $metadata->getValue()."\n" : mb_strtoupper($match[1]) . "\n"; + if ($metadata->isSectionStart()) { + $type = $metadata->getSectionType(); + $content = (null !== $metadata->getValue()) ? mb_strtoupper($metadata->getValue())."\n" : mb_strtoupper($type) . "\n"; return $content; - } elseif (preg_match('/^end_of_(.*)/', $metadata->getName()) === 1) { - return "\n"; + } elseif ($metadata->isSectionEnd()) { + return ''; } else { - return $metadata->getValue()."\n"; + if ($metadata->isNameNecessary()) { + return $metadata->getHumanName().': '.$metadata->getValue()."\n"; + } else { + return $metadata->getValue()."\n"; + } } } @@ -69,8 +75,10 @@ private function generateBlank(int $count): string private function getLyricsMonospace(Lyrics $lyrics): string { - $lineChords = ''; + $lineChords = []; + $lineChordsWithBlanks = ''; $lineTexts = ''; + $lineTextsWithBlanks = ''; foreach ($lyrics->getBlocks() as $block) { $chords = []; @@ -85,16 +93,59 @@ private function getLyricsMonospace(Lyrics $lyrics): string $chord = implode('/', $chords); $text = $block->getText(); + $textWithBlanks = $text; if (mb_strlen($text) < mb_strlen($chord)) { - $text = $text.$this->generateBlank(mb_strlen($chord) - mb_strlen($text)); + $textWithBlanks = $text.$this->generateBlank(mb_strlen($chord) - mb_strlen($text)); } - $lineChords .= $chord.$this->generateBlank(mb_strlen($text) - mb_strlen($chord)); + $lineChordsWithBlanks .= $chord.$this->generateBlank(mb_strlen($text) - mb_strlen($chord)); + $lineChords[] = $chord; $lineTexts .= $text; + $lineTextsWithBlanks .= $textWithBlanks; } - return $lineChords."\n".$lineTexts."\n"; + $output = ''; + if ($lyrics->hasInlineChords()) { + $output .= '~'.$lineTexts."~".implode(' ', $lineChords)."\n"; + } else { + if ($lyrics->hasChords() && $lyrics->hasText()) { + $output .= $lineChordsWithBlanks."\n"; + $output .= $lineTextsWithBlanks."\n"; + } elseif ($lyrics->hasChords()) { + $output .= implode(' ', $lineChords)."\n"; + } elseif ($lyrics->hasText()) { + $output .= $lineTexts."\n"; + } + } + return $output; + } + + /** + * @param string[] $lines + */ + private function transformInlineChords(array &$lines): void + { + $linesToFix = []; + $longest = 0; + foreach ($lines as $num => $line) { + $match = []; + if (preg_match('/^~(.+)~(.+)/', $line, $match) === 1) { + $linesToFix[$num] = [ + 'text' => trim($match[1]), + 'chords' => trim($match[2]), + ]; + if (mb_strlen($match[1]) > $longest) { + $longest = mb_strlen($match[1]); + } + } + } + + $inlineChordPosition = $longest + 4; + foreach ($linesToFix as $num => $lineToFix) { + $lines[$num] = $lineToFix['text'].$this->generateBlank($inlineChordPosition - mb_strlen($lineToFix['text'])); + $lines[$num] .= $lineToFix['chords']."\n"; + } } private function getLyricsOnlyMonospace(Lyrics $lyrics): string @@ -103,6 +154,6 @@ private function getLyricsOnlyMonospace(Lyrics $lyrics): string foreach ($lyrics->getBlocks() as $block) { $texts .= ltrim($block->getText()); } - return $texts."\n"; + return ($texts !== '') ? rtrim($texts)."\n" : ''; } } diff --git a/src/Line/Lyrics.php b/src/Line/Lyrics.php index 7e8ad79..7bb793f 100644 --- a/src/Line/Lyrics.php +++ b/src/Line/Lyrics.php @@ -12,7 +12,7 @@ class Lyrics extends Line /** * @param \ChordPro\Block[] $blocks The blocks of the line. */ - public function __construct(private array $blocks) + public function __construct(private array $blocks = [], private bool $hasChords = false, private bool $hasText = false, private bool $hasInlineChords = false) { } @@ -25,4 +25,19 @@ public function getBlocks(): array { return $this->blocks; } + + public function hasChords(): bool + { + return $this->hasChords; + } + + public function hasText(): bool + { + return $this->hasText; + } + + public function hasInlineChords(): bool + { + return $this->hasInlineChords; + } } diff --git a/src/Line/Metadata.php b/src/Line/Metadata.php index fb71ccd..f113143 100644 --- a/src/Line/Metadata.php +++ b/src/Line/Metadata.php @@ -4,6 +4,8 @@ namespace ChordPro\Line; +use PhpParser\Node\Expr\BinaryOp\BooleanOr; + /** * A class that represents a metadata line in a song. */ @@ -21,6 +23,11 @@ public function getName(): string return $this->name; } + public function getNameSlug(): string + { + return $this->slugify($this->name); + } + public function getValue(): ?string { return $this->value; @@ -54,4 +61,83 @@ private function convertToFullName(string $name): string default => $name, }; } + + /** + * Get the human readable name of the metadata. + */ + public function getHumanName(): string + { + return match($this->name) { + 'title' => 'Title', + 'subtitle' => 'Subtitle', + 'sorttitle' => 'Sort title', + 'comment' => 'Comment', + 'comment_italic' => 'Comment', + 'comment_box' => 'Comment', + 'key' => 'Key', + 'time' => 'Time', + 'tempo' => 'Tempo', + 'duration' => 'Duration', + 'capo' => 'Capo', + 'artist' => 'Artist', + 'composer' => 'Composer', + 'lyricist' => 'Lyricist', + 'album' => 'Album', + 'year' => 'Year', + 'copyright' => 'Copyright', + 'meta' => 'Meta', + default => $this->name, + }; + } + + public function isNameNecessary(): bool + { + return match($this->name) { + 'title' => false, + 'subtitle' => false, + 'comment' => false, + 'comment_italic' => false, + 'comment_box' => false, + 'artist' => false, + default => true, + }; + } + + public function isSectionStart(): bool + { + return str_starts_with($this->name, 'start_of_'); + } + + public function isSectionEnd(): bool + { + return str_starts_with($this->name, 'end_of_'); + } + + public function getSectionType(): string + { + $match = []; + if (preg_match('/^start_of_(.*)/', $this->name, $match) === 1) { + return $this->slugify($match[1]); + } elseif (preg_match('/^end_of_(.*)/', $this->name, $match) === 1) { + return $this->slugify($match[1]); + } else { + return ''; + } + } + + private function slugify(string $text): string + { + $text = preg_replace('~[^\pL\d]+~u', '-', $text); + $text = iconv('utf-8', 'us-ascii//TRANSLIT', (string) $text); + $text = preg_replace('~[^-\w]+~', '', (string) $text); + $text = trim((string) $text, '-'); + $text = preg_replace('~-+~', '-', $text); + $text = strtolower((string) $text); + + if ($text === '') { + return 'n-a'; + } + return $text; + } + } diff --git a/src/Parser.php b/src/Parser.php index 7a1f9ed..6865d2b 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -23,22 +23,24 @@ class Parser public function parse(string $text, array $sourceNotations = []): Song { $lines = []; - $split = preg_split('~\R~', $text); + $split = preg_split("/\\r\\n|\\r|\\n/", $text); if ($split !== false) { foreach ($split as $line) { - $line = trim($line); - switch (substr($line, 0, 1)) { + $line = preg_replace('/^\s+/', '', $line); + $line = preg_replace('/\s+$/', '', (string) $line); + + switch (substr((string) $line, 0, 1)) { case "{": - $lines[] = $this->parseMetadata($line); + $lines[] = $this->parseMetadata((string) $line); break; case "#": - $lines[] = new Comment(trim(substr($line, 1))); + $lines[] = new Comment(trim(substr((string) $line, 1))); break; case "": $lines[] = new EmptyLine(); break; default: - $lines[] = $this->parseLyrics($line, $sourceNotations); + $lines[] = $this->parseLyrics((string) $line, $sourceNotations); } } } @@ -82,6 +84,40 @@ private function parseMetadata(string $line): Metadata private function parseLyrics(string $line, array $sourceNotations = []): Lyrics { $blocks = []; + $possiblyEmptyBlocks = []; + + $hasText = false; + $hasChords = false; + + // First, check for ~ symbol. + $match = []; + if (preg_match('/^([^\[\]]+)~(.+)/', $line, $match) === 1) { + $matchChords = []; + $lineText = $match[1]; + $result = preg_match_all('/\[([^\[\]]+)\]/', $match[2], $matchChords); + if (is_numeric($result) && $result > 0) { + $lineChords = $matchChords[1]; + $blocks[] = new Block( + chords: [], + text: trim($lineText) + ); + foreach ($lineChords as $chord) { + $blocks[] = new Block( + chords: Chord::fromSlice($chord, $sourceNotations), + text: '', + lineEnd: true + ); + } + return new Lyrics( + blocks: $blocks, + hasInlineChords: true, + hasChords: true, + hasText: true, + ); + } + + } + $explodedLine = explode('[', $line); foreach($explodedLine as $num => $lineFragment) { if ($lineFragment !== '') { @@ -89,19 +125,28 @@ private function parseLyrics(string $line, array $sourceNotations = []): Lyrics // If the fragment consists of only a chord without text. if (isset($chordWithText[1]) && $chordWithText[1] == '') { + $hasChords = true; $blocks[] = new Block( chords: Chord::fromSlice($chordWithText[0], $sourceNotations), text: '' ); } - // If first line begins with text and not a chord. + // If first block begins with text and not a chord. elseif ($num == 0 && count($chordWithText) == 1) { $blocks[] = new Block( chords: [], text: $chordWithText[0], ); - // If there is a space after "]", threat it as separate blocks. - } elseif (substr($chordWithText[1], 0, 1) == " ") { + if (preg_match('/\S/', $chordWithText[0]) === 1) { + $hasText = true; + } else { + // Save the block as possibly empty, so we can remove it later. + $possiblyEmptyBlocks[] = array_key_last($blocks); + } + + } elseif (isset($chordWithText[1]) && substr($chordWithText[1], 0, 1) == " ") { + // If there is a space after "]", threat it as two separate blocks. + $hasChords = true; $blocks[] = new Block( chords: Chord::fromSlice($chordWithText[0], $sourceNotations), text: '' @@ -110,15 +155,45 @@ private function parseLyrics(string $line, array $sourceNotations = []): Lyrics chords: [], text: $chordWithText[1] ); - // If there is no space after "]", threat it as chord with text. + if (preg_match('/\S/', $chordWithText[1]) === 1) { + $hasText = true; + } else { + // Save the block as possibly empty, so we can remove it later. + $possiblyEmptyBlocks[] = array_key_last($blocks); + } + } else { + // If there is no space after "]", threat it as chord with text. + $hasChords = true; $blocks[] = new Block( chords: Chord::fromSlice($chordWithText[0], $sourceNotations), - text: $chordWithText[1] + text: $chordWithText[1] ?? '' ); + if (preg_match('/\S/', $chordWithText[1] ?? '') === 1) { + $hasText = true; + } else { + // Save the block as possibly empty, so we can remove it later. + $possiblyEmptyBlocks[] = array_key_last($blocks); + } + } } } - return new Lyrics($blocks); + + // If there are only chords and no text, set text to empty string. + if ($hasChords && !$hasText) { + foreach ($possiblyEmptyBlocks as $blockKey) { + unset($blocks[$blockKey]); + } + } elseif (!$hasChords && !$hasText) { + $blocks = []; + } + + return new Lyrics( + blocks: $blocks, + hasInlineChords: false, + hasChords: $hasChords, + hasText: $hasText, + ); } } diff --git a/tests/BlockTest.php b/tests/BlockTest.php index d67dd74..211b927 100644 --- a/tests/BlockTest.php +++ b/tests/BlockTest.php @@ -12,9 +12,10 @@ public function testBlockClassAccess(): void { $text = 'This is a test'; $chord = new Chord('C'); - $block = new Block([$chord], $text); + $block = new Block([$chord], $text, true); $this->assertSame([$chord], $block->getChords(), 'Chords are not returned'); $this->assertSame($text, $block->getText(), 'Text is not returned'); + $this->assertTrue($block->isLineEnd()); } } diff --git a/tests/Formatter/HtmlFormatterTest.php b/tests/Formatter/HtmlFormatterTest.php index 0b47f38..1e72212 100644 --- a/tests/Formatter/HtmlFormatterTest.php +++ b/tests/Formatter/HtmlFormatterTest.php @@ -10,61 +10,47 @@ final class HtmlFormatterTest extends TestCase { public function testWithChords(): void { - $text = "{title: Test}\n\n{sov}\n[C7]Test [D]Test2\n{eov}\n# Comment"; + $text = file_get_contents(__DIR__ . '/../data/song2.pro'); $parser = new Parser(); $song = $parser->parse($text); $formatter = new HtmlFormatter(); $html = $formatter->format($song); - - $expected = '
Test
' . "\n" . - '
' . "\n" . - '
' . "\n" . - '
' . "\n" . - 'C7Test DTest2' . "\n" . - '
' . "\n" . - '
' . "\n"; - + $expected = file_get_contents(__DIR__ . '/../data/song2.html'); $this->assertSame($expected, $html, 'HTML output is not as expected'); + for ($i = 1; $i <= 11; $i++) { + $this->assertStringContainsString('Test' . $i, $html); + } } public function testWithoutChords(): void { - $text = "{title: Test}\n\n{sov}\n[C7]Test [D]Test2\n{eov}\n# Comment"; + $text = file_get_contents(__DIR__ . '/../data/song2.pro'); $parser = new Parser(); $song = $parser->parse($text); $formatter = new HtmlFormatter(); $html = $formatter->format($song, [ 'no_chords' => true ]); - - $expected = '
Test
' . "\n" . - '
' . "\n" . - '
' . "\n" . - '
' . "\n" . - 'Test Test2' . "\n" . - '
' . "\n" . - '
' . "\n"; - + $expected = file_get_contents(__DIR__ . '/../data/song2_no_chords.html'); $this->assertSame($expected, $html, 'HTML output is not as expected'); + for ($i = 1; $i <= 11; $i++) { + $this->assertStringContainsString('Test' . $i, $html); + } } public function testWithoutMetadata(): void { - $text = "{title: Test}\n\n{sov}\n[C7]Test [D]Test2\n{eov}\n# Comment"; + $text = file_get_contents(__DIR__ . '/../data/song2.pro'); $parser = new Parser(); $song = $parser->parse($text); $formatter = new HtmlFormatter(); $html = $formatter->format($song, [ 'ignore_metadata' => ['title'] ]); - - $expected = '
' . "\n" . - '
' . "\n" . - '
' . "\n" . - 'C7Test DTest2' . "\n" . - '
' . "\n" . - '
' . "\n"; - + $expected = file_get_contents(__DIR__ . '/../data/song2_no_metadata.html'); $this->assertSame($expected, $html, 'HTML output is not as expected'); + for ($i = 1; $i <= 11; $i++) { + $this->assertStringContainsString('Test' . $i, $html); + } } } diff --git a/tests/Formatter/JSONFormatterTest.php b/tests/Formatter/JSONFormatterTest.php index 94ba9b3..84ab737 100644 --- a/tests/Formatter/JSONFormatterTest.php +++ b/tests/Formatter/JSONFormatterTest.php @@ -10,55 +10,47 @@ final class JSONFormatterTest extends TestCase { public function testWithChords(): void { - $text = "{title: Test}\n\n{sov}\n[C7]Test [D]Test2\n{eov}\n# Comment"; + $text = file_get_contents(__DIR__ . '/../data/song2.pro'); $parser = new Parser(); $song = $parser->parse($text); $formatter = new JSONFormatter(); $json = $formatter->format($song); - - $result = json_decode($json, true); - $this->assertSame('metadata', $result[0]['type']); - $this->assertSame('title', $result[0]['name']); - $this->assertSame('Test', $result[0]['value']); - $this->assertSame('empty_line', $result[1]['type']); - $this->assertSame('metadata', $result[2]['type']); - $this->assertSame('start_of_verse', $result[2]['name']); - $this->assertNull($result[2]['value']); - $this->assertSame('line', $result[3]['type']); - $this->assertSame(2, count($result[3]['blocks'])); - $this->assertSame('metadata', $result[4]['type']); - $this->assertSame('end_of_verse', $result[4]['name']); - $this->assertNull($result[4]['value']); - $this->assertSame('comment', $result[5]['type']); - $this->assertSame('Comment', $result[5]['content']); + $expected = file_get_contents(__DIR__ . '/../data/song2.json'); + $this->assertSame($expected, $json, 'JSON output is not as expected'); + for ($i = 1; $i <= 11; $i++) { + $this->assertStringContainsString('Test' . $i, $json); + } } public function testWithoutChords(): void { - $text = "{title: Test}\n\n{sov}\n[C7]Test [D]Test2\n{eov}\n# Comment"; + $text = file_get_contents(__DIR__ . '/../data/song2.pro'); $parser = new Parser(); $song = $parser->parse($text); $formatter = new JSONFormatter(); $json = $formatter->format($song, [ 'no_chords' => true ]); - - $result = json_decode($json, true); - $this->assertSame('line', $result[3]['type']); - $this->assertSame('Test Test2', $result[3]['text']); + $expected = file_get_contents(__DIR__ . '/../data/song2_no_chords.json'); + $this->assertSame($expected, $json, 'JSON output is not as expected'); + for ($i = 1; $i <= 11; $i++) { + $this->assertStringContainsString('Test' . $i, $json); + } } - public function textWithoutMetadata(): void + public function testWithoutMetadata(): void { - $text = "{title: Test}\n\n{sov}\n[C7]Test [D]Test2\n{eov}\n# Comment"; + $text = file_get_contents(__DIR__ . '/../data/song2.pro'); $parser = new Parser(); $song = $parser->parse($text); $formatter = new JSONFormatter(); $json = $formatter->format($song, [ 'ignore_metadata' => ['title'] ]); - - $result = json_decode($json, true); - $this->assertSame('empty_line', $result[0]['type']); + $expected = file_get_contents(__DIR__ . '/../data/song2_no_metadata.json'); + $this->assertSame($expected, $json, 'JSON output is not as expected'); + for ($i = 1; $i <= 11; $i++) { + $this->assertStringContainsString('Test' . $i, $json); + } } } diff --git a/tests/Formatter/MonospaceFormatterTest.php b/tests/Formatter/MonospaceFormatterTest.php index df86cfe..f02d752 100644 --- a/tests/Formatter/MonospaceFormatterTest.php +++ b/tests/Formatter/MonospaceFormatterTest.php @@ -10,40 +10,47 @@ final class MonospaceFormatterTest extends TestCase { public function testWithChords(): void { - $text = "{title: Test}\n\n{sov}\n[C7]Test [D]Test2\n{eov}\n# Comment"; + $text = file_get_contents(__DIR__ . '/../data/song2.pro'); $parser = new Parser(); $song = $parser->parse($text); $formatter = new MonospaceFormatter(); $monospace = $formatter->format($song); - $expected = "Test\n\nVERSE\nC7 D \nTest Test2\n\n"; + $expected = file_get_contents(__DIR__ . '/../data/song2.txt'); $this->assertSame($expected, $monospace, 'Monospace output is not as expected'); + for ($i = 1; $i <= 11; $i++) { + $this->assertStringContainsString('Test' . $i, $monospace); + } } public function testWithoutChords(): void { - $text = "{title: Test}\n\n{sov}\n[C7]Test [D]Test2\n{eov}\n# Comment"; + $text = file_get_contents(__DIR__ . '/../data/song2.pro'); $parser = new Parser(); $song = $parser->parse($text); $formatter = new MonospaceFormatter(); $monospace = $formatter->format($song, [ 'no_chords' => true ]); - $expected = "Test\n\nVERSE\nTest Test2\n\n"; - + $expected = file_get_contents(__DIR__ . '/../data/song2_no_chords.txt'); $this->assertSame($expected, $monospace, 'Monospace output is not as expected'); + for ($i = 1; $i <= 11; $i++) { + $this->assertStringContainsString('Test' . $i, $monospace); + } } public function testWithoutMetadata(): void { - $text = "{title: Test}\n\n{sov}\n[C7]Test [D]Test2\n{eov}\n# Comment"; + $text = file_get_contents(__DIR__ . '/../data/song2.pro'); $parser = new Parser(); $song = $parser->parse($text); $formatter = new MonospaceFormatter(); $monospace = $formatter->format($song, [ 'ignore_metadata' => ['title'] ]); - $expected = "\nVERSE\nC7 D \nTest Test2\n\n"; - + $expected = file_get_contents(__DIR__ . '/../data/song2_no_metadata.txt'); $this->assertSame($expected, $monospace, 'Monospace output is not as expected'); + for ($i = 1; $i <= 11; $i++) { + $this->assertStringContainsString('Test' . $i, $monospace); + } } } diff --git a/tests/Line/LyricsTest.php b/tests/Line/LyricsTest.php index ee3c76b..86bc917 100644 --- a/tests/Line/LyricsTest.php +++ b/tests/Line/LyricsTest.php @@ -15,7 +15,10 @@ public function testClassAccess(): void $chord = new Chord('C'); $block = new Block([$chord], $text); - $lyrics = new Lyrics([$block]); + $lyrics = new Lyrics([$block], true, true, true); $this->assertSame([$block], $lyrics->getBlocks(), 'Blocks are not returned'); + $this->assertTrue($lyrics->hasChords()); + $this->assertTrue($lyrics->hasText()); + $this->assertTrue($lyrics->hasInlineChords()); } } diff --git a/tests/Line/MetadataTest.php b/tests/Line/MetadataTest.php index b5bd1a4..580ee7e 100644 --- a/tests/Line/MetadataTest.php +++ b/tests/Line/MetadataTest.php @@ -17,6 +17,10 @@ public function testClassAccess(): void $metadata2 = new Metadata('Test Name', null); $this->assertEquals('Test Name', $metadata2->getName()); $this->assertNull($metadata2->getValue()); + + $metadata3 = new Metadata('key', 'C'); + $this->assertEquals('Key', $metadata3->getHumanName()); + $this->assertTrue($metadata3->isNameNecessary()); } public static function shortcutProvider(): array @@ -47,4 +51,23 @@ public function testShortcuts($shortName, $longName): void $this->assertEquals($longName, $metadata->getName()); } + + public function testSections(): void + { + $metadata = new Metadata('start_of_Zażółć%_JAŹŃ', 'TEST'); + $this->assertEquals('start_of_Zażółć%_JAŹŃ', $metadata->getName()); + $this->assertEquals('zazolc-jazn', $metadata->getSectionType()); + $this->assertEquals('start-of-zazolc-jazn', $metadata->getNameSlug()); + $this->assertEquals('TEST', $metadata->getValue()); + $this->assertTrue($metadata->isSectionStart()); + $this->assertFalse($metadata->isSectionEnd()); + + $metadata = new Metadata('end_of_Zażółć%_JAŹŃ', null); + $this->assertEquals('end_of_Zażółć%_JAŹŃ', $metadata->getName()); + $this->assertEquals('zazolc-jazn', $metadata->getSectionType()); + $this->assertEquals('end-of-zazolc-jazn', $metadata->getNameSlug()); + $this->assertNull($metadata->getValue()); + $this->assertFalse($metadata->isSectionStart()); + $this->assertTrue($metadata->isSectionEnd()); + } } diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 623cbb6..3d25c19 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -78,12 +78,12 @@ public function testParse(): void assert($line instanceof Lyrics); $this->assertCount(4, $line->getBlocks()); - // Sing it [D]loud, sing it [G7/E]cle[A#m]ar, + // Sing it [D]like ą, sing it [G7/E]cle[A#m]ar, $this->assertSame('Sing it ', $line->getBlocks()[0]->getText()); $this->assertCount(0, $line->getBlocks()[0]->getChords()); - $this->assertSame('loud, sing it ', $line->getBlocks()[1]->getText()); + $this->assertSame('like ą, sing it ', $line->getBlocks()[1]->getText()); $this->assertCount(1, $line->getBlocks()[1]->getChords()); $this->assertSame('D', $line->getBlocks()[1]->getChords()[0]->getRootChord()); $this->assertSame('', $line->getBlocks()[1]->getChords()[0]->getExt()); diff --git a/tests/data/song1.pro b/tests/data/song1.pro index ba8c892..0b43490 100644 --- a/tests/data/song1.pro +++ b/tests/data/song1.pro @@ -8,7 +8,7 @@ {c: Seriously!} {start_of_chorus: Refrain} -Sing it [D]loud, sing it [G7/E]cle[A#m]ar, +Sing it [D]like ą, sing it [G7/E]cle[A#m]ar, {end_of_chorus} {start_of_verse} diff --git a/tests/data/song2.html b/tests/data/song2.html new file mode 100644 index 0000000..111cae1 --- /dev/null +++ b/tests/data/song2.html @@ -0,0 +1,47 @@ + + + + + +
+
+Test1 Test2 +
+
+C7/ETest3 DTest4 +
+
+
+Test5 E  Test6 +
+
+C7D +
+
+
+CG +
+
+CG +
+
+C G  +
+
+Test7~ +
+
+
+
+C7Test8 ~ D  +
+
+Test9 ~ D  +
+
+
DC
Test10
+
+
+
EFMaj7/E
Test11 Longer Line Tab
+
+
diff --git a/tests/data/song2.json b/tests/data/song2.json new file mode 100644 index 0000000..5f2906a --- /dev/null +++ b/tests/data/song2.json @@ -0,0 +1,216 @@ +[ + { + "type": "metadata", + "name": "title", + "value": "Test", + "humanName": "Title" + }, + { + "type": "metadata", + "name": "subtitle", + "value": "TestSub", + "humanName": "Subtitle" + }, + { + "type": "metadata", + "name": "comment", + "value": "Comment1", + "humanName": "Comment" + }, + { + "type": "metadata", + "name": "key", + "value": "C", + "humanName": "Key" + }, + { + "type": "section_start", + "sectionType": "verse", + "label": "Verse" + }, + { + "type": "line", + "blocks": [ + { + "text": "Test1 Test2" + } + ] + }, + { + "type": "line", + "blocks": [ + { + "text": "Test3", + "chord": "C7\/E", + "originalChord": "C7\/E" + }, + { + "text": "Test4", + "chord": "D", + "originalChord": "D" + } + ] + }, + { + "type": "empty_line" + }, + { + "type": "line", + "blocks": [ + { + "text": "Test5" + }, + { + "chord": "E", + "originalChord": "E" + }, + { + "text": " Test6" + } + ] + }, + { + "type": "line", + "blocks": [ + { + "chord": "C7", + "originalChord": "C7" + }, + { + "chord": "D", + "originalChord": "D" + } + ] + }, + { + "type": "section_end", + "sectionType": "verse" + }, + { + "type": "line", + "blocks": [ + { + "chord": "C", + "originalChord": "C" + }, + { + "chord": "G", + "originalChord": "G" + } + ] + }, + { + "type": "line", + "blocks": [ + { + "chord": "C", + "originalChord": "C" + }, + { + "chord": "G", + "originalChord": "G" + } + ] + }, + { + "type": "line", + "blocks": [ + { + "text": "x" + }, + { + "chord": "C", + "originalChord": "C" + }, + { + "chord": "G", + "originalChord": "G" + } + ] + }, + { + "type": "line", + "blocks": [ + { + "text": "Test7~" + } + ] + }, + { + "type": "empty_line" + }, + { + "type": "section_start", + "sectionType": "chorus" + }, + { + "type": "line", + "blocks": [ + { + "text": "Test8 ~", + "chord": "C7", + "originalChord": "C7" + }, + { + "chord": "D", + "originalChord": "D" + } + ] + }, + { + "type": "line", + "blocks": [ + { + "text": "Test9 ~" + }, + { + "chord": "D", + "originalChord": "D" + } + ] + }, + { + "type": "line_inline", + "blocks": [ + { + "text": "Test10" + }, + { + "chord": "D", + "originalChord": "D", + "lineEnd": true + }, + { + "chord": "C", + "originalChord": "C", + "lineEnd": true + } + ] + }, + { + "type": "line_inline", + "blocks": [ + { + "text": "Test11 Longer Line Tab" + }, + { + "chord": "E", + "originalChord": "E", + "lineEnd": true + }, + { + "chord": "FMaj7\/E", + "originalChord": "FMaj7\/E", + "lineEnd": true + } + ] + }, + { + "type": "section_end", + "sectionType": "chorus" + }, + { + "type": "comment", + "content": "Comment" + } +] \ No newline at end of file diff --git a/tests/data/song2.pro b/tests/data/song2.pro new file mode 100644 index 0000000..b76a343 --- /dev/null +++ b/tests/data/song2.pro @@ -0,0 +1,23 @@ +{title: Test} +{subtitle: TestSub} +{c: Comment1} +{key: C} +{sov: Verse} +Test1 Test2 +[C7/E]Test3 [D]Test4 + +Test5 [E] Test6 +[C7] [D] +{eov} +[C][G] + [C][G] + x [C][G] +Test7~ + +{soc} + [C7]Test8 ~ [D] + Test9 ~ [D + Test10 ~ [D][C] + Test11 Longer Line Tab ~ [E][FMaj7/E] +{eoc} +# Comment \ No newline at end of file diff --git a/tests/data/song2.txt b/tests/data/song2.txt new file mode 100644 index 0000000..c4ab203 --- /dev/null +++ b/tests/data/song2.txt @@ -0,0 +1,25 @@ +Test +TestSub +Comment1 +Key: C +VERSE +Test1 Test2 +C7/E D +Test3 Test4 + + E +Test5 Test6 +C7 D +C G +C G + CG +x +Test7~ + +CHORUS +C7 D +Test8 ~ + D +Test9 ~ +Test10 D C +Test11 Longer Line Tab E FMaj7/E diff --git a/tests/data/song2_no_chords.html b/tests/data/song2_no_chords.html new file mode 100644 index 0000000..2c3416f --- /dev/null +++ b/tests/data/song2_no_chords.html @@ -0,0 +1,38 @@ + + + + + +
+
+Test1 Test2 +
+
+Test3 Test4 +
+
+
+Test5 Test6 +
+
+
+x +
+
+Test7~ +
+
+
+
+Test8 ~ +
+
+Test9 ~ +
+
+Test10 +
+
+Test11 Longer Line Tab +
+
diff --git a/tests/data/song2_no_chords.json b/tests/data/song2_no_chords.json new file mode 100644 index 0000000..c8e514d --- /dev/null +++ b/tests/data/song2_no_chords.json @@ -0,0 +1,89 @@ +[ + { + "type": "metadata", + "name": "title", + "value": "Test", + "humanName": "Title" + }, + { + "type": "metadata", + "name": "subtitle", + "value": "TestSub", + "humanName": "Subtitle" + }, + { + "type": "metadata", + "name": "comment", + "value": "Comment1", + "humanName": "Comment" + }, + { + "type": "metadata", + "name": "key", + "value": "C", + "humanName": "Key" + }, + { + "type": "section_start", + "sectionType": "verse", + "label": "Verse" + }, + { + "type": "line", + "text": "Test1 Test2" + }, + { + "type": "line", + "text": "Test3 Test4" + }, + { + "type": "empty_line" + }, + { + "type": "line", + "text": "Test5 Test6" + }, + { + "type": "section_end", + "sectionType": "verse" + }, + { + "type": "line", + "text": "x" + }, + { + "type": "line", + "text": "Test7~" + }, + { + "type": "empty_line" + }, + { + "type": "section_start", + "sectionType": "chorus" + }, + { + "type": "line", + "text": "Test8 ~" + }, + { + "type": "line", + "text": "Test9 ~" + }, + { + "type": "line", + "text": "Test10" + }, + { + "type": "line", + "text": "Test11 Longer Line Tab" + }, + { + "type": "section_end", + "sectionType": "chorus" + }, + { + "type": "comment", + "content": "Comment" + } +] \ No newline at end of file diff --git a/tests/data/song2_no_chords.txt b/tests/data/song2_no_chords.txt new file mode 100644 index 0000000..5b7a165 --- /dev/null +++ b/tests/data/song2_no_chords.txt @@ -0,0 +1,17 @@ +Test +TestSub +Comment1 +Key: C +VERSE +Test1 Test2 +Test3 Test4 + +Test5 Test6 +x +Test7~ + +CHORUS +Test8 ~ +Test9 ~ +Test10 +Test11 Longer Line Tab diff --git a/tests/data/song2_no_metadata.html b/tests/data/song2_no_metadata.html new file mode 100644 index 0000000..3d29754 --- /dev/null +++ b/tests/data/song2_no_metadata.html @@ -0,0 +1,46 @@ + + + + +
+
+Test1 Test2 +
+
+C7/ETest3 DTest4 +
+
+
+Test5 E  Test6 +
+
+C7D +
+
+
+CG +
+
+CG +
+
+C G  +
+
+Test7~ +
+
+
+
+C7Test8 ~ D  +
+
+Test9 ~ D  +
+
+
DC
Test10
+
+
+
EFMaj7/E
Test11 Longer Line Tab
+
+
diff --git a/tests/data/song2_no_metadata.json b/tests/data/song2_no_metadata.json new file mode 100644 index 0000000..3f0cb27 --- /dev/null +++ b/tests/data/song2_no_metadata.json @@ -0,0 +1,210 @@ +[ + { + "type": "metadata", + "name": "subtitle", + "value": "TestSub", + "humanName": "Subtitle" + }, + { + "type": "metadata", + "name": "comment", + "value": "Comment1", + "humanName": "Comment" + }, + { + "type": "metadata", + "name": "key", + "value": "C", + "humanName": "Key" + }, + { + "type": "section_start", + "sectionType": "verse", + "label": "Verse" + }, + { + "type": "line", + "blocks": [ + { + "text": "Test1 Test2" + } + ] + }, + { + "type": "line", + "blocks": [ + { + "text": "Test3", + "chord": "C7\/E", + "originalChord": "C7\/E" + }, + { + "text": "Test4", + "chord": "D", + "originalChord": "D" + } + ] + }, + { + "type": "empty_line" + }, + { + "type": "line", + "blocks": [ + { + "text": "Test5" + }, + { + "chord": "E", + "originalChord": "E" + }, + { + "text": " Test6" + } + ] + }, + { + "type": "line", + "blocks": [ + { + "chord": "C7", + "originalChord": "C7" + }, + { + "chord": "D", + "originalChord": "D" + } + ] + }, + { + "type": "section_end", + "sectionType": "verse" + }, + { + "type": "line", + "blocks": [ + { + "chord": "C", + "originalChord": "C" + }, + { + "chord": "G", + "originalChord": "G" + } + ] + }, + { + "type": "line", + "blocks": [ + { + "chord": "C", + "originalChord": "C" + }, + { + "chord": "G", + "originalChord": "G" + } + ] + }, + { + "type": "line", + "blocks": [ + { + "text": "x" + }, + { + "chord": "C", + "originalChord": "C" + }, + { + "chord": "G", + "originalChord": "G" + } + ] + }, + { + "type": "line", + "blocks": [ + { + "text": "Test7~" + } + ] + }, + { + "type": "empty_line" + }, + { + "type": "section_start", + "sectionType": "chorus" + }, + { + "type": "line", + "blocks": [ + { + "text": "Test8 ~", + "chord": "C7", + "originalChord": "C7" + }, + { + "chord": "D", + "originalChord": "D" + } + ] + }, + { + "type": "line", + "blocks": [ + { + "text": "Test9 ~" + }, + { + "chord": "D", + "originalChord": "D" + } + ] + }, + { + "type": "line_inline", + "blocks": [ + { + "text": "Test10" + }, + { + "chord": "D", + "originalChord": "D", + "lineEnd": true + }, + { + "chord": "C", + "originalChord": "C", + "lineEnd": true + } + ] + }, + { + "type": "line_inline", + "blocks": [ + { + "text": "Test11 Longer Line Tab" + }, + { + "chord": "E", + "originalChord": "E", + "lineEnd": true + }, + { + "chord": "FMaj7\/E", + "originalChord": "FMaj7\/E", + "lineEnd": true + } + ] + }, + { + "type": "section_end", + "sectionType": "chorus" + }, + { + "type": "comment", + "content": "Comment" + } +] \ No newline at end of file diff --git a/tests/data/song2_no_metadata.txt b/tests/data/song2_no_metadata.txt new file mode 100644 index 0000000..4e8d41d --- /dev/null +++ b/tests/data/song2_no_metadata.txt @@ -0,0 +1,24 @@ +TestSub +Comment1 +Key: C +VERSE +Test1 Test2 +C7/E D +Test3 Test4 + + E +Test5 Test6 +C7 D +C G +C G + CG +x +Test7~ + +CHORUS +C7 D +Test8 ~ + D +Test9 ~ +Test10 D C +Test11 Longer Line Tab E FMaj7/E diff --git a/web/example.css b/web/example.css index 4192e3e..53c735c 100644 --- a/web/example.css +++ b/web/example.css @@ -1,9 +1,15 @@ body { - font-family: Helvetica; + font-family: Helvetica; } /* Metadata */ +.chordpro-section-label, +.chordpro-metadata { + font-size: 0.9em; + font-style: italic; + margin: 0.5em 0; +} .chordpro-title { font-size: 2em; font-weight: bold; @@ -12,10 +18,6 @@ body { font-size: 1.5em; font-weight: light; } -.chordpro-comment { - font-style: italic; - font-size: 0.9em; -} .chordpro-key { display: inline-block; font-size: 1.5em; @@ -27,33 +29,34 @@ body { font-weight: bold; margin: 1em 0; } +.chordpro-tab { + font-family: monospace; +} /* Lines & Chorus */ -body.chordpro-line:first-of-type { - border-top: 1px solid #000; - padding-top: 1em; - margin-top: 1em; -} .chordpro-line { - height: 2.5em; + height: 2.5em; +} +.chordpro-line-text-only, .chordpro-line-chords-only { + height: 1.5em; } .chordpro-chorus { - padding-left: 10px; - border-left: 4px solid #777; + padding-left: 10px; + border-left: 4px solid #777; } .chordpro-block { - position: relative; - display: inline-block; + position: relative; + display: inline-block; } .chordpro-chord { - position: relative; - display: block; - padding-right: 5px; - font-weight: bold; - font-size: 0.9em; + position: relative; + display: inline-block; + padding-right: 5px; + font-weight: bold; + font-size: 0.9em; } .chordpro-text { - position: relative; - display: block; + position: relative; + display: block; } diff --git a/web/example.php b/web/example.php index 705d8fd..611e044 100644 --- a/web/example.php +++ b/web/example.php @@ -1,23 +1,39 @@