diff --git a/patacrep/data/examples/songs/tests/errors.sgc b/patacrep/data/examples/songs/tests/errors.sgc
index 0b006cab..56cb61b7 100644
--- a/patacrep/data/examples/songs/tests/errors.sgc
+++ b/patacrep/data/examples/songs/tests/errors.sgc
@@ -10,4 +10,6 @@
Bla []bla
Bla [H]bla
+{fo#: bar}
+
diff --git a/patacrep/data/examples/songs/tests/newline.sgc b/patacrep/data/examples/songs/tests/newline.sgc
new file mode 100644
index 00000000..c14acb20
--- /dev/null
+++ b/patacrep/data/examples/songs/tests/newline.sgc
@@ -0,0 +1,39 @@
+{title: Newline}
+{subtitle: Test of "newline" directive}
+
+This is a verse
+With a new line
+{newline}
+The second part of the verse
+Is this line
+
+Here is a new line at the end
+{newline}
+
+Foo bar
+
+{newline}
+And a new line
+At the beginning
+
+{soc}
+New lines can also
+{newline}
+Be in chorus
+{eoc}
+
+{sob}
+New lines can also
+{newline}
+Be in bridges
+{eob}
+
+New lines can also
+ {newline}
+Be surrounded by spaces
+
+New lines cannot have text before them {newline}
+
+{newline} New lines cannot have text after them
+
+New lines cannot be {newline} surrounded by text.
diff --git a/patacrep/latex/lexer.py b/patacrep/latex/lexer.py
index b8a762c0..f76500f8 100644
--- a/patacrep/latex/lexer.py
+++ b/patacrep/latex/lexer.py
@@ -12,7 +12,7 @@ tokens = (
'LBRACE',
'RBRACE',
'COMMAND',
- 'NEWLINE',
+ 'ENDOFLINE',
'COMMA',
'EQUAL',
'CHARACTER',
@@ -34,7 +34,7 @@ class SimpleLexer:
t_LBRACE = r'{'
t_RBRACE = r'}'
t_COMMAND = r'\\([@a-zA-Z]+|[^\\])'
- t_NEWLINE = r'\\\\'
+ t_ENDOFLINE = r'\\\\'
SPECIAL_CHARACTERS = (
t_LBRACKET +
t_RBRACKET +
@@ -59,7 +59,7 @@ class SimpleLexer:
# Define a rule so we can track line numbers
@staticmethod
- def t_newline(token):
+ def t_endofline(token):
r'\n+'
token.lexer.lineno += len(token.value)
diff --git a/patacrep/latex/syntax.py b/patacrep/latex/syntax.py
index 46c42ea1..8915816b 100644
--- a/patacrep/latex/syntax.py
+++ b/patacrep/latex/syntax.py
@@ -27,7 +27,7 @@ class LatexParser(Parser):
"""expression : brackets expression
| braces expression
| command expression
- | NEWLINE expression
+ | ENDOFLINE expression
| beginsong expression
| word expression
| SPACE expression
@@ -172,7 +172,7 @@ class LatexParser(Parser):
@staticmethod
def p_titles_next(symbols):
- """titles_next : NEWLINE title titles_next
+ """titles_next : ENDOFLINE title titles_next
| empty
"""
if len(symbols) == 2:
diff --git a/patacrep/songs/chordpro/ast.py b/patacrep/songs/chordpro/ast.py
index fd9d33c0..18805e9b 100644
--- a/patacrep/songs/chordpro/ast.py
+++ b/patacrep/songs/chordpro/ast.py
@@ -2,58 +2,23 @@
# pylint: disable=too-few-public-methods
+from collections import OrderedDict
import logging
LOGGER = logging.getLogger()
-class OrderedLifoDict:
- """Ordered (LIFO) dictionary.
-
- Mimics the :class:`dict` dictionary, with:
- - dictionary is ordered: the order the keys are kept (as with
- :class:`collections.OrderedDict`), excepted that:
- - LIFO: the last item is reterned first when iterating.
- """
-
- def __init__(self, default=None):
- if default is None:
- self._keys = []
- self._values = {}
- else:
- self._keys = list(default.keys())
- self._values = default.copy()
-
- def values(self):
- """Same as :meth:`dict.values`."""
- for key in self:
- yield self._values[key]
-
- def __iter__(self):
- yield from self._keys
-
- def __setitem__(self, key, value):
- if key not in self._keys:
- self._keys.insert(0, key)
- self._values[key] = value
-
- def __getitem__(self, key):
- return self._values[key]
-
- def get(self, key, default=None):
- """Same as :meth:`dict.get`."""
- return self._values.get(key, default)
-
def _indent(string):
"""Return and indented version of argument."""
return "\n".join([" {}".format(line) for line in string.split('\n')])
#: List of properties that are to be displayed in the flow of the song (not as
#: metadata at the beginning or end of song.
-INLINE_PROPERTIES = {
+INLINE_DIRECTIVES = {
"partition",
"comment",
"guitar_comment",
"image",
+ "newline",
}
#: Some directive have alternative names. For instance `{title: Foo}` and `{t:
@@ -95,13 +60,20 @@ class Line(AST):
_template = "line"
- def __init__(self):
+ def __init__(self, *items):
super().__init__()
- self.line = []
+ self.line = list(items)
+
+ def __iter__(self):
+ yield from self.line
def prepend(self, data):
- """Add an object at the beginning of line."""
- self.line.insert(0, data)
+ """Add an object at the beginning of line.
+
+ Does nothing if argument is `None`.
+ """
+ if data is not None:
+ self.line.insert(0, data)
return self
def strip(self):
@@ -109,14 +81,18 @@ class Line(AST):
while True:
if not self.line:
return self
- if isinstance(self.line[0], Space):
+ if isinstance(self.line[0], Space) or isinstance(self.line[0], Error):
del self.line[0]
continue
- if isinstance(self.line[-1], Space):
+ if isinstance(self.line[-1], Space) or isinstance(self.line[-1], Error):
del self.line[-1]
continue
return self
+ def is_empty(self):
+ """Return `True` iff line is empty."""
+ return len(self.strip().line) == 0
+
class LineElement(AST):
"""Something present on a line."""
# pylint: disable=abstract-method
@@ -173,6 +149,14 @@ class Verse(AST):
self.lines.insert(0, data)
return self
+ def directive(self):
+ """Return `True` iff the verse is composed only of directives."""
+ for line in self.lines:
+ for element in line:
+ if not isinstance(element, Directive):
+ return False
+ return True
+
@property
def nolyrics(self):
"""Return `True` iff verse contains only notes (no lyrics)"""
@@ -215,26 +199,29 @@ class Song(AST):
def __init__(self, filename):
super().__init__()
self.content = []
- self.meta = OrderedLifoDict()
+ self.meta = OrderedDict()
self._authors = []
self._titles = []
self._subtitles = []
- self._keys = []
self.filename = filename
def add(self, data):
"""Add an element to the song"""
if isinstance(data, Error):
- return self
+ pass
elif data is None:
# New line
- if not (self.content and isinstance(self.content[0], Newline)):
- self.content.insert(0, Newline())
+ if not (self.content and isinstance(self.content[0], EndOfLine)):
+ self.content.insert(0, EndOfLine())
elif isinstance(data, Line):
# Add a new line, maybe in the current verse.
- if not (self.content and isinstance(self.content[0], Verse)):
- self.content.insert(0, Verse())
- self.content[0].prepend(data.strip())
+ if not data.is_empty():
+ if not (self.content and isinstance(self.content[0], Verse)):
+ self.content.insert(0, Verse())
+ self.content[0].prepend(data.strip())
+ elif isinstance(data, Directive) and data.inline:
+ # Add a directive in the content of the song.
+ self.content.append(data)
elif data.inline:
# Add an object in the content of the song.
self.content.insert(0, data)
@@ -251,13 +238,13 @@ class Song(AST):
def add_title(self, data):
"""Add a title"""
- self._titles.insert(0, data.argument)
+ self._titles.append(data.argument)
def add_cumulative(self, data):
"""Add a cumulative argument into metadata"""
if data.keyword not in self.meta:
self.meta[data.keyword] = []
- self.meta[data.keyword].insert(0, data)
+ self.meta[data.keyword].append(data)
def get_data_argument(self, keyword, default):
"""Return `self.meta[keyword].argument`.
@@ -276,7 +263,7 @@ class Song(AST):
def add_subtitle(self, data):
"""Add a subtitle"""
- self._subtitles.insert(0, data.argument)
+ self._subtitles.append(data.argument)
@property
def titles(self):
@@ -285,7 +272,7 @@ class Song(AST):
def add_author(self, data):
"""Add an auhor."""
- self._authors.insert(0, data.argument)
+ self._authors.append(data.argument)
@property
def authors(self):
@@ -295,16 +282,16 @@ class Song(AST):
def add_key(self, data):
"""Add a new {key: foo: bar} directive."""
key, *argument = data.argument.split(":")
- if 'keys' not in self.meta:
- self.meta['keys'] = []
- self.meta['keys'].insert(0, Directive(
+ if 'morekeys' not in self.meta:
+ self.meta['morekeys'] = []
+ self.meta['morekeys'].append(Directive(
key.strip(),
":".join(argument).strip(),
))
-class Newline(AST):
+class EndOfLine(AST):
"""New line"""
- _template = "newline"
+ _template = "endofline"
class Directive(AST):
"""A directive"""
@@ -322,14 +309,13 @@ class Directive(AST):
"""
return self.keyword
+ def __str__(self):
+ return str(self.argument)
+
@property
def inline(self):
- """True iff this directive is to be rendered in the flow on the song.
- """
- return self.keyword in INLINE_PROPERTIES
-
- def __str__(self):
- return self.argument
+ """Return `True` iff `self` is an inline directive."""
+ return self.keyword in INLINE_DIRECTIVES
class Define(Directive):
"""A chord definition.
diff --git a/patacrep/songs/chordpro/data/chordpro/content_endofline b/patacrep/songs/chordpro/data/chordpro/content_endofline
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/patacrep/songs/chordpro/data/chordpro/content_endofline
@@ -0,0 +1 @@
+
diff --git a/patacrep/songs/chordpro/data/chordpro/content_newline b/patacrep/songs/chordpro/data/chordpro/content_newline
index 8b137891..01d04291 100644
--- a/patacrep/songs/chordpro/data/chordpro/content_newline
+++ b/patacrep/songs/chordpro/data/chordpro/content_newline
@@ -1 +1 @@
-
+{newline}
diff --git a/patacrep/songs/chordpro/data/chordpro/song_header b/patacrep/songs/chordpro/data/chordpro/song_header
index f111271f..ac5d8329 100644
--- a/patacrep/songs/chordpro/data/chordpro/song_header
+++ b/patacrep/songs/chordpro/data/chordpro/song_header
@@ -25,7 +25,7 @@
{(( 'cov' )): (( metadata['cov'].argument|search_image ))}
(* endif *)
-(*- for key in metadata.keys -*)
+(*- for key in metadata.morekeys -*)
{key: (( key.keyword )): (( key.argument ))}
(* endfor *)
diff --git a/patacrep/songs/chordpro/data/html/content_endofline b/patacrep/songs/chordpro/data/html/content_endofline
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/patacrep/songs/chordpro/data/html/content_endofline
@@ -0,0 +1 @@
+
diff --git a/patacrep/songs/chordpro/data/html/content_newline b/patacrep/songs/chordpro/data/html/content_newline
index 8b137891..e69de29b 100644
--- a/patacrep/songs/chordpro/data/html/content_newline
+++ b/patacrep/songs/chordpro/data/html/content_newline
@@ -1 +0,0 @@
-
diff --git a/patacrep/songs/chordpro/data/html/content_verse b/patacrep/songs/chordpro/data/html/content_verse
index eab4d361..6fe21f6b 100644
--- a/patacrep/songs/chordpro/data/html/content_verse
+++ b/patacrep/songs/chordpro/data/html/content_verse
@@ -1,6 +1,8 @@
- (*- for line in content.lines -*)
- (* if not loop.first *)
(* endif -*)
+ (* for line in content.lines *)
(( render(line) ))
- (* endfor -*)
+ (*- if not loop.last *)
+ (* endif *)
+ (* endfor *)
+
diff --git a/patacrep/songs/chordpro/data/html/song_header b/patacrep/songs/chordpro/data/html/song_header
index d85adbd8..963ef686 100644
--- a/patacrep/songs/chordpro/data/html/song_header
+++ b/patacrep/songs/chordpro/data/html/song_header
@@ -23,7 +23,7 @@
(* include 'content_metadata_cover' *)
-(*- for key in metadata.keys -*)
+(*- for key in metadata.morekeys -*)
{key: (( key.keyword )): (( key.argument ))}
(* endfor *)
diff --git a/patacrep/songs/chordpro/data/latex/content_endofline b/patacrep/songs/chordpro/data/latex/content_endofline
new file mode 100644
index 00000000..139597f9
--- /dev/null
+++ b/patacrep/songs/chordpro/data/latex/content_endofline
@@ -0,0 +1,2 @@
+
+
diff --git a/patacrep/songs/chordpro/data/latex/content_newline b/patacrep/songs/chordpro/data/latex/content_newline
index 139597f9..c0846fba 100644
--- a/patacrep/songs/chordpro/data/latex/content_newline
+++ b/patacrep/songs/chordpro/data/latex/content_newline
@@ -1,2 +1 @@
-
-
+~\\
diff --git a/patacrep/songs/chordpro/data/latex/content_verse b/patacrep/songs/chordpro/data/latex/content_verse
index 176b104b..ae3aa34f 100644
--- a/patacrep/songs/chordpro/data/latex/content_verse
+++ b/patacrep/songs/chordpro/data/latex/content_verse
@@ -1,4 +1,8 @@
-(*- if content.nolyrics -*)
+(* if content.directive() *)
+ (* for line in content.lines -*)
+ ((- render(line) ))
+ (* endfor -*)
+(*- elif content.nolyrics -*)
\ifchorded
\begin{verse*}
(* for line in content.lines *)
diff --git a/patacrep/songs/chordpro/data/latex/song b/patacrep/songs/chordpro/data/latex/song
index 2568f7f7..6f51d673 100644
--- a/patacrep/songs/chordpro/data/latex/song
+++ b/patacrep/songs/chordpro/data/latex/song
@@ -30,7 +30,7 @@
(* if 'cov' in metadata *)
cov={(( metadata["cov"].argument|search_image ))},
(* endif *)
- (* for key in metadata.keys *)
+ (* for key in metadata.morekeys *)
(( key.keyword ))={(( key.argument ))},
(* endfor *)
]
diff --git a/patacrep/songs/chordpro/lexer.py b/patacrep/songs/chordpro/lexer.py
index aba37a1c..a0b57826 100644
--- a/patacrep/songs/chordpro/lexer.py
+++ b/patacrep/songs/chordpro/lexer.py
@@ -9,7 +9,7 @@ LOGGER = logging.getLogger()
tokens = (
'LBRACE',
'RBRACE',
- 'NEWLINE',
+ 'ENDOFLINE',
'COLON',
'WORD',
'SPACE',
@@ -81,14 +81,15 @@ class ChordProLexer:
return token
t_tablature_TEXT = r'[^\n]+'
- t_tablature_NEWLINE = r'\n'
+ t_tablature_ENDOFLINE = r'\n'
- def __init__(self):
+ def __init__(self, *, filename=None):
self.__class__.lexer = lex.lex(module=self)
+ self.filename = filename
# Define a rule so we can track line numbers
@staticmethod
- def t_NEWLINE(token):
+ def t_ENDOFLINE(token):
r'[\n\r]'
token.lexer.lineno += 1
return token
@@ -132,16 +133,16 @@ class ChordProLexer:
self.lexer.push_state('directiveargument')
return token
- @staticmethod
- def error(token, more=""):
+ def error(self, token, more=""):
"""Display error message, and skip illegal token."""
- LOGGER.error(
- "Line {line}: Illegal character '{char}'{more}.".format(
- line=token.lexer.lineno,
- char=token.value[0],
- more=more,
- )
+ message = "Line {line}: Illegal character '{char}'{more}.".format(
+ line=token.lexer.lineno,
+ char=token.value[0],
+ more=more,
)
+ if self.filename is not None:
+ message = "File {}: {}".format(self.filename, message)
+ LOGGER.error(message)
token.lexer.skip(1)
def t_error(self, token):
diff --git a/patacrep/songs/chordpro/syntax.py b/patacrep/songs/chordpro/syntax.py
index a5d1b102..e88d7360 100644
--- a/patacrep/songs/chordpro/syntax.py
+++ b/patacrep/songs/chordpro/syntax.py
@@ -1,5 +1,6 @@
"""ChordPro parser"""
+import logging
import ply.yacc as yacc
import re
@@ -7,6 +8,8 @@ from patacrep.songs.syntax import Parser
from patacrep.songs.chordpro import ast
from patacrep.songs.chordpro.lexer import tokens, ChordProLexer
+LOGGER = logging.getLogger()
+
class ChordproParser(Parser):
"""ChordPro parser class"""
# pylint: disable=too-many-public-methods
@@ -16,26 +19,40 @@ class ChordproParser(Parser):
def __init__(self, filename=None):
super().__init__()
self.tokens = tokens
+ self.song = ast.Song(filename)
self.filename = filename
+ self.parser = yacc.yacc(
+ module=self,
+ debug=0,
+ write_tables=0,
+ )
+
+ def parse(self, *args, **kwargs):
+ """Parse file
+
+ This is a shortcut to `yacc.yacc(...).parse()`. The arguments are
+ transmitted to this method.
+ """
+ return self.parser.parse(*args, **kwargs)
def p_song(self, symbols):
"""song : block song
| empty
"""
if len(symbols) == 2:
- symbols[0] = ast.Song(self.filename)
+ symbols[0] = self.song
else:
symbols[0] = symbols[2].add(symbols[1])
@staticmethod
def p_block(symbols):
"""block : SPACE block
- | directive NEWLINE
- | line NEWLINE
- | chorus NEWLINE
- | tab NEWLINE
- | bridge NEWLINE
- | NEWLINE
+ | line ENDOFLINE
+ | line_error ENDOFLINE
+ | chorus ENDOFLINE
+ | tab ENDOFLINE
+ | bridge ENDOFLINE
+ | ENDOFLINE
"""
if len(symbols) == 3 and isinstance(symbols[1], str):
symbols[0] = symbols[2]
@@ -133,16 +150,23 @@ class ChordproParser(Parser):
symbols[0] = ast.Error()
return
- symbols[0] = self._parse_define(match.groupdict())
- if symbols[0] is None:
+ define = self._parse_define(match.groupdict())
+ if define is None:
self.error(
line=symbols.lexer.lineno,
message="Invalid chord definition '{}'.".format(argument),
)
symbols[0] = ast.Error()
+ return
+ self.song.add(define)
else:
- symbols[0] = ast.Directive(keyword, argument)
+ directive = ast.Directive(keyword, argument)
+ if directive.inline:
+ symbols[0] = directive
+ else:
+ self.song.add(directive)
+
@staticmethod
def p_directive_next(symbols):
@@ -160,12 +184,29 @@ class ChordproParser(Parser):
else:
symbols[0] = None
+ @staticmethod
+ def p_line_error(symbols):
+ """line_error : error directive"""
+ LOGGER.error("Directive can only be preceded or followed by spaces")
+ symbols[0] = ast.Line()
+
@staticmethod
def p_line(symbols):
"""line : word line_next
| chord line_next
+ | directive maybespace
"""
- symbols[0] = symbols[2].prepend(symbols[1])
+ if isinstance(symbols[2], ast.Line):
+ # Line with words, etc.
+ symbols[0] = symbols[2].prepend(symbols[1])
+ else:
+ # Directive
+ if symbols[1] is None:
+ # Meta directive. Nothing to do
+ symbols[0] = ast.Line()
+ else:
+ # Inline directive
+ symbols[0] = ast.Line(symbols[1])
@staticmethod
def p_line_next(symbols):
@@ -196,13 +237,14 @@ class ChordproParser(Parser):
@staticmethod
def p_chorus(symbols):
- """chorus : SOC maybespace NEWLINE chorus_content EOC maybespace
+ """chorus : SOC maybespace ENDOFLINE chorus_content EOC maybespace
"""
symbols[0] = symbols[4]
@staticmethod
def p_chorus_content(symbols):
- """chorus_content : line NEWLINE chorus_content
+ """chorus_content : line ENDOFLINE chorus_content
+ | line_error ENDOFLINE chorus_content
| SPACE chorus_content
| empty
"""
@@ -215,13 +257,14 @@ class ChordproParser(Parser):
@staticmethod
def p_bridge(symbols):
- """bridge : SOB maybespace NEWLINE bridge_content EOB maybespace
+ """bridge : SOB maybespace ENDOFLINE bridge_content EOB maybespace
"""
symbols[0] = symbols[4]
@staticmethod
def p_bridge_content(symbols):
- """bridge_content : line NEWLINE bridge_content
+ """bridge_content : line ENDOFLINE bridge_content
+ | line_error ENDOFLINE bridge_content
| SPACE bridge_content
| empty
"""
@@ -235,13 +278,13 @@ class ChordproParser(Parser):
@staticmethod
def p_tab(symbols):
- """tab : SOT maybespace NEWLINE tab_content EOT maybespace
+ """tab : SOT maybespace ENDOFLINE tab_content EOT maybespace
"""
symbols[0] = symbols[4]
@staticmethod
def p_tab_content(symbols):
- """tab_content : NEWLINE tab_content
+ """tab_content : ENDOFLINE tab_content
| TEXT tab_content
| SPACE tab_content
| empty
@@ -258,13 +301,18 @@ class ChordproParser(Parser):
"""empty :"""
symbols[0] = None
+ def p_error(self, token):
+ super().p_error(token)
+ while True:
+ token = self.parser.token()
+ if not token or token.type == "ENDOFLINE":
+ break
+ self.parser.errok()
+ return token
+
def parse_song(content, filename=None):
"""Parse song and return its metadata."""
- return yacc.yacc(
- module=ChordproParser(filename),
- debug=0,
- write_tables=0,
- ).parse(
- content,
- lexer=ChordProLexer().lexer,
- )
+ return ChordproParser(filename).parse(
+ content,
+ lexer=ChordProLexer(filename=filename).lexer,
+ )
diff --git a/patacrep/songs/syntax.py b/patacrep/songs/syntax.py
index 7f019374..ee7d95da 100644
--- a/patacrep/songs/syntax.py
+++ b/patacrep/songs/syntax.py
@@ -20,8 +20,7 @@ class Parser:
column = (token.lexpos - last_cr) + 1
return column
- @staticmethod
- def error(*, line=None, column=None, message=""):
+ def error(self, *, line=None, column=None, message=""):
"""Display an error message"""
coordinates = []
if line is not None:
@@ -35,7 +34,10 @@ class Parser:
text += message
else:
text += "."
- LOGGER.error(text)
+ if self.filename is None:
+ LOGGER.error(text)
+ else:
+ LOGGER.error("File {}: {}".format(self.filename, text))
def p_error(self, token):
"""Manage parsing errors."""
diff --git a/test/test_chordpro/greensleeves.sgc b/test/test_chordpro/greensleeves.sgc
index 1f0ea7f3..389f14d1 100644
--- a/test/test_chordpro/greensleeves.sgc
+++ b/test/test_chordpro/greensleeves.sgc
@@ -10,6 +10,7 @@
{partition: greensleeves.ly}
+
A[Am]las, my love, ye [G]do me wrong
To [Am]cast me oft dis[E]curteously
And [Am]I have loved [G]you so long
diff --git a/test/test_chordpro/greensleeves.tex b/test/test_chordpro/greensleeves.tex
index 6a745a81..a0b602c0 100644
--- a/test/test_chordpro/greensleeves.tex
+++ b/test/test_chordpro/greensleeves.tex
@@ -17,6 +17,7 @@ Un sous titre}[
\lilypond{greensleeves.ly}
+
\begin{verse}
A\[Am]las, my love, ye \[G]do me wrong
To \[Am]cast me oft dis\[E]curteously
diff --git a/test/test_chordpro/metadata.sgc b/test/test_chordpro/metadata.sgc
index e4bb1b21..979c1388 100644
--- a/test/test_chordpro/metadata.sgc
+++ b/test/test_chordpro/metadata.sgc
@@ -15,5 +15,8 @@
{comment: Comment}
{guitar_comment: GuitarComment}
-{image: Image}
{partition: Lilypond}
+{image: Image}
+
+
+Foo
diff --git a/test/test_chordpro/metadata.source b/test/test_chordpro/metadata.source
index 2e106444..65a2359a 100644
--- a/test/test_chordpro/metadata.source
+++ b/test/test_chordpro/metadata.source
@@ -15,5 +15,7 @@
{key: foo: Foo}
{comment: Comment}
{guitar_comment: GuitarComment}
-{image: Image}
{partition: Lilypond}
+{image: Image}
+
+Foo
diff --git a/test/test_chordpro/metadata.tex b/test/test_chordpro/metadata.tex
index 11004ef6..fa29ec3c 100644
--- a/test/test_chordpro/metadata.tex
+++ b/test/test_chordpro/metadata.tex
@@ -19,7 +19,13 @@ Subtitle5}[
\textnote{Comment}
\musicnote{GuitarComment}
-\image{Image}
\lilypond{Lilypond}
+\image{Image}
+
+
+
+\begin{verse}
+ Foo
+\end{verse}
\endsong
diff --git a/test/test_chordpro/newline.html b/test/test_chordpro/newline.html
new file mode 100644
index 00000000..1fbb02a3
--- /dev/null
+++ b/test/test_chordpro/newline.html
@@ -0,0 +1,50 @@
+Language: english
+
+
+
+
+
+ This is a verse
+ With a new line
+
+ The second part of the verse
+ Is this line
+
+
+
+ Here is a new line at the end
+
+
+
+
+ Foo bar
+
+
+
+
+ And a new line
+ At the beginning
+
+
+
+ New lines can also
+
+ Be in chorus
+
+
+
+ New lines can also
+
+ Be in bridges
+
+
+
+ New lines can also
+
+ Be surrounded by spaces
+
+
+
+ New lines cannot
+
+
diff --git a/test/test_chordpro/newline.sgc b/test/test_chordpro/newline.sgc
new file mode 100644
index 00000000..72bdc1a2
--- /dev/null
+++ b/test/test_chordpro/newline.sgc
@@ -0,0 +1,41 @@
+{language: english}
+
+This is a verse
+With a new line
+{newline}
+The second part of the verse
+Is this line
+
+
+Here is a new line at the end
+{newline}
+
+
+Foo bar
+
+
+{newline}
+And a new line
+At the beginning
+
+
+{start_of_chorus}
+ New lines can also
+ {newline}
+ Be in chorus
+{end_of_chorus}
+
+
+{start_of_bridge}
+ New lines can also
+ {newline}
+ Be in bridges
+{end_of_bridge}
+
+
+New lines can also
+{newline}
+Be surrounded by spaces
+
+
+New lines cannot
diff --git a/test/test_chordpro/newline.source b/test/test_chordpro/newline.source
new file mode 100644
index 00000000..03095b88
--- /dev/null
+++ b/test/test_chordpro/newline.source
@@ -0,0 +1,32 @@
+This is a verse
+With a new line
+{newline}
+The second part of the verse
+Is this line
+
+Here is a new line at the end
+{newline}
+
+Foo bar
+
+{newline}
+And a new line
+At the beginning
+
+{soc}
+New lines can also
+{newline}
+Be in chorus
+{eoc}
+
+{sob}
+New lines can also
+{newline}
+Be in bridges
+{eob}
+
+New lines can also
+ {newline}
+Be surrounded by spaces
+
+New lines cannot {newline} appear in the middle of a line
diff --git a/test/test_chordpro/newline.tex b/test/test_chordpro/newline.tex
new file mode 100644
index 00000000..d557de5f
--- /dev/null
+++ b/test/test_chordpro/newline.tex
@@ -0,0 +1,61 @@
+\selectlanguage{english}
+
+\beginsong{}[
+ by={
+ },
+]
+
+
+\begin{verse}
+ This is a verse
+ With a new line
+ ~\\
+ The second part of the verse
+ Is this line
+\end{verse}
+
+
+\begin{verse}
+ Here is a new line at the end
+ ~\\
+\end{verse}
+
+
+\begin{verse}
+ Foo bar
+\end{verse}
+
+
+\begin{verse}
+ ~\\
+ And a new line
+ At the beginning
+\end{verse}
+
+
+\begin{chorus}
+ New lines can also
+ ~\\
+ Be in chorus
+\end{chorus}
+
+
+\begin{bridge}
+ New lines can also
+ ~\\
+ Be in bridges
+\end{bridge}
+
+
+\begin{verse}
+ New lines can also
+ ~\\
+ Be surrounded by spaces
+\end{verse}
+
+
+\begin{verse}
+ New lines cannot
+\end{verse}
+
+\endsong
diff --git a/test/test_chordpro/test_parser.py b/test/test_chordpro/test_parser.py
index 8f857156..805d1583 100644
--- a/test/test_chordpro/test_parser.py
+++ b/test/test_chordpro/test_parser.py
@@ -14,6 +14,7 @@ from .. import disable_logging
LANGUAGES = {
'tex': 'latex',
'sgc': 'chordpro',
+ 'html': 'html',
}
class FileTestMeta(type):