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):