Browse Source

Merge pull request #112 from patacrep/newline

Implement `{newline}` directive
pull/118/head
Louis 9 years ago
parent
commit
f812d8b4ba
  1. 2
      patacrep/data/examples/songs/tests/errors.sgc
  2. 39
      patacrep/data/examples/songs/tests/newline.sgc
  3. 6
      patacrep/latex/lexer.py
  4. 4
      patacrep/latex/syntax.py
  5. 120
      patacrep/songs/chordpro/ast.py
  6. 1
      patacrep/songs/chordpro/data/chordpro/content_endofline
  7. 2
      patacrep/songs/chordpro/data/chordpro/content_newline
  8. 2
      patacrep/songs/chordpro/data/chordpro/song_header
  9. 1
      patacrep/songs/chordpro/data/html/content_endofline
  10. 1
      patacrep/songs/chordpro/data/html/content_newline
  11. 8
      patacrep/songs/chordpro/data/html/content_verse
  12. 2
      patacrep/songs/chordpro/data/html/song_header
  13. 2
      patacrep/songs/chordpro/data/latex/content_endofline
  14. 3
      patacrep/songs/chordpro/data/latex/content_newline
  15. 6
      patacrep/songs/chordpro/data/latex/content_verse
  16. 2
      patacrep/songs/chordpro/data/latex/song
  17. 25
      patacrep/songs/chordpro/lexer.py
  18. 98
      patacrep/songs/chordpro/syntax.py
  19. 8
      patacrep/songs/syntax.py
  20. 1
      test/test_chordpro/greensleeves.sgc
  21. 1
      test/test_chordpro/greensleeves.tex
  22. 5
      test/test_chordpro/metadata.sgc
  23. 4
      test/test_chordpro/metadata.source
  24. 8
      test/test_chordpro/metadata.tex
  25. 50
      test/test_chordpro/newline.html
  26. 41
      test/test_chordpro/newline.sgc
  27. 32
      test/test_chordpro/newline.source
  28. 61
      test/test_chordpro/newline.tex
  29. 1
      test/test_chordpro/test_parser.py

2
patacrep/data/examples/songs/tests/errors.sgc

@ -10,4 +10,6 @@
Bla []bla
Bla [H]bla
{fo#: bar}

39
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.

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

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

120
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.

1
patacrep/songs/chordpro/data/chordpro/content_endofline

@ -0,0 +1 @@

2
patacrep/songs/chordpro/data/chordpro/content_newline

@ -1 +1 @@
{newline}

2
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 *)

1
patacrep/songs/chordpro/data/html/content_endofline

@ -0,0 +1 @@

1
patacrep/songs/chordpro/data/html/content_newline

@ -1 +0,0 @@

8
patacrep/songs/chordpro/data/html/content_verse

@ -1,6 +1,8 @@
<p class="(( content.type ))">
(*- for line in content.lines -*)
(* if not loop.first *)<br>(* endif -*)
(* for line in content.lines *)
(( render(line) ))
(* endfor -*)
(*- if not loop.last *)<br>
(* endif *)
(* endfor *)
</p>

2
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 *)

2
patacrep/songs/chordpro/data/latex/content_endofline

@ -0,0 +1,2 @@

3
patacrep/songs/chordpro/data/latex/content_newline

@ -1,2 +1 @@
~\\

6
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 *)

2
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 *)
]

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

98
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,
)

8
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."""

1
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

1
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

5
test/test_chordpro/metadata.sgc

@ -15,5 +15,8 @@
{comment: Comment}
{guitar_comment: GuitarComment}
{image: Image}
{partition: Lilypond}
{image: Image}
Foo

4
test/test_chordpro/metadata.source

@ -15,5 +15,7 @@
{key: foo: Foo}
{comment: Comment}
{guitar_comment: GuitarComment}
{image: Image}
{partition: Lilypond}
{image: Image}
Foo

8
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

50
test/test_chordpro/newline.html

@ -0,0 +1,50 @@
<span class="song-language">Language: english</span><br>
<div class="song_content">
<p class="verse">
This is a verse<br>
With a new line<br>
<br>
The second part of the verse<br>
Is this line
</p>
<p class="verse">
Here is a new line at the end<br>
</p>
<p class="verse">
Foo bar
</p>
<p class="verse">
<br>
And a new line<br>
At the beginning
</p>
<p class="chorus">
New lines can also<br>
<br>
Be in chorus
</p>
<p class="bridge">
New lines can also<br>
<br>
Be in bridges
</p>
<p class="verse">
New lines can also<br>
<br>
Be surrounded by spaces
</p>
<p class="verse">
New lines cannot
</p>
</div>

41
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

32
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

61
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

1
test/test_chordpro/test_parser.py

@ -14,6 +14,7 @@ from .. import disable_logging
LANGUAGES = {
'tex': 'latex',
'sgc': 'chordpro',
'html': 'html',
}
class FileTestMeta(type):

Loading…
Cancel
Save