Browse Source

Directive {newline} is correctly parsed and rendered

* Moreover, error messages also mention file name
* Directives can now appear inside text (and not only on their own line)
pull/112/head
Louis 9 years ago
parent
commit
39b61959f8
  1. 2
      patacrep/data/examples/songs/errors.sgc
  2. 35
      patacrep/data/examples/songs/tests/newline.sgc
  3. 43
      patacrep/songs/chordpro/ast.py
  4. 6
      patacrep/songs/chordpro/data/latex/content_verse
  5. 19
      patacrep/songs/chordpro/lexer.py
  6. 10
      patacrep/songs/chordpro/syntax.py
  7. 8
      patacrep/songs/syntax.py
  8. 1
      test/test_chordpro/greensleeves.sgc
  9. 1
      test/test_chordpro/greensleeves.tex
  10. 1
      test/test_chordpro/metadata.sgc
  11. 3
      test/test_chordpro/metadata.tex

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

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

35
patacrep/data/examples/songs/tests/newline.sgc

@ -0,0 +1,35 @@
{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 can {newline} appear in the middle of a line

43
patacrep/songs/chordpro/ast.py

@ -13,11 +13,12 @@ def _indent(string):
#: List of properties that are to be displayed in the flow of the song (not as #: List of properties that are to be displayed in the flow of the song (not as
#: metadata at the beginning or end of song. #: metadata at the beginning or end of song.
INLINE_PROPERTIES = { INLINE_DIRECTIVES = {
"partition", "partition",
"comment", "comment",
"guitar_comment", "guitar_comment",
"image", "image",
"newline",
} }
#: Some directive have alternative names. For instance `{title: Foo}` and `{t: #: Some directive have alternative names. For instance `{title: Foo}` and `{t:
@ -63,6 +64,9 @@ class Line(AST):
super().__init__() super().__init__()
self.line = [] self.line = []
def __iter__(self):
yield from self.line
def prepend(self, data): def prepend(self, data):
"""Add an object at the beginning of line. """Add an object at the beginning of line.
@ -86,6 +90,7 @@ class Line(AST):
return self return self
def is_empty(self): def is_empty(self):
"""Return `True` iff line is empty."""
return len(self.strip().line) == 0 return len(self.strip().line) == 0
class LineElement(AST): class LineElement(AST):
@ -144,6 +149,14 @@ class Verse(AST):
self.lines.insert(0, data) self.lines.insert(0, data)
return self 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
class Chorus(Verse): class Chorus(Verse):
"""Chorus""" """Chorus"""
type = 'chorus' type = 'chorus'
@ -191,7 +204,7 @@ class Song(AST):
# New line # New line
if not (self.content and isinstance(self.content[0], EndOfLine)): if not (self.content and isinstance(self.content[0], EndOfLine)):
self.content.insert(0, EndOfLine()) self.content.insert(0, EndOfLine())
elif isinstance(data, Line) or isinstance(data, NewLine): elif isinstance(data, Line):
# Add a new line, maybe in the current verse. # Add a new line, maybe in the current verse.
if not data.is_empty(): if not data.is_empty():
if not (self.content and isinstance(self.content[0], Verse)): if not (self.content and isinstance(self.content[0], Verse)):
@ -273,7 +286,7 @@ class EndOfLine(AST):
class Directive(AST): class Directive(AST):
"""A directive""" """A directive"""
meta = True inline = False
def __init__(self, keyword, argument=None): def __init__(self, keyword, argument=None):
super().__init__() super().__init__()
@ -288,29 +301,23 @@ class Directive(AST):
""" """
return self.keyword return self.keyword
@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): def __str__(self):
return self.argument return self.argument
@classmethod @classmethod
def create(cls, keyword, argument=None): def create(cls, keyword, argument=None):
if keyword == "newline": """Create a :class:`Directive` or :class:`InlineDirective`.
return NewLine(keyword, argument)
Depending on the keyword.
"""
if keyword in INLINE_DIRECTIVES:
return InlineDirective(keyword, argument)
else: else:
return cls(keyword, argument) return cls(keyword, argument)
class NewLine(Directive): class InlineDirective(Directive):
keyword = "newline" """Directive displayed in the flow of the song"""
_template = "newline" inline = True
meta = False
def strip(self):
return self
class Define(Directive): class Define(Directive):
"""A chord definition. """A chord definition.

6
patacrep/songs/chordpro/data/latex/content_verse

@ -1,5 +1,11 @@
(* if content.directive() *)
(* for line in content.lines -*)
((- render(line) ))
(* endfor -*)
(*- else -*)
\begin{(( content.type ))} \begin{(( content.type ))}
(* for line in content.lines *) (* for line in content.lines *)
(( render(line) )) (( render(line) ))
(* endfor *) (* endfor *)
\end{(( content.type ))} \end{(( content.type ))}
(*- endif *)

19
patacrep/songs/chordpro/lexer.py

@ -83,8 +83,9 @@ class ChordProLexer:
t_tablature_TEXT = r'[^\n]+' t_tablature_TEXT = r'[^\n]+'
t_tablature_ENDOFLINE = r'\n' t_tablature_ENDOFLINE = r'\n'
def __init__(self): def __init__(self, *, filename=None):
self.__class__.lexer = lex.lex(module=self) self.__class__.lexer = lex.lex(module=self)
self.filename = filename
# Define a rule so we can track line numbers # Define a rule so we can track line numbers
@staticmethod @staticmethod
@ -132,16 +133,16 @@ class ChordProLexer:
self.lexer.push_state('directiveargument') self.lexer.push_state('directiveargument')
return token return token
@staticmethod def error(self, token, more=""):
def error(token, more=""):
"""Display error message, and skip illegal token.""" """Display error message, and skip illegal token."""
LOGGER.error( message = "Line {line}: Illegal character '{char}'{more}.".format(
"Line {line}: Illegal character '{char}'{more}.".format( line=token.lexer.lineno,
line=token.lexer.lineno, char=token.value[0],
char=token.value[0], more=more,
more=more,
)
) )
if self.filename is not None:
message = "File {}: {}".format(self.filename, message)
LOGGER.error(message)
token.lexer.skip(1) token.lexer.skip(1)
def t_error(self, token): def t_error(self, token):

10
patacrep/songs/chordpro/syntax.py

@ -17,6 +17,7 @@ class ChordproParser(Parser):
super().__init__() super().__init__()
self.tokens = tokens self.tokens = tokens
self.song = ast.Song(filename) self.song = ast.Song(filename)
self.filename = filename
def p_song(self, symbols): def p_song(self, symbols):
"""song : block song """song : block song
@ -144,10 +145,11 @@ class ChordproParser(Parser):
else: else:
directive = ast.Directive.create(keyword, argument) directive = ast.Directive.create(keyword, argument)
if directive.meta: if directive.inline:
self.song.add(directive)
else:
symbols[0] = directive symbols[0] = directive
else:
self.song.add(directive)
@staticmethod @staticmethod
def p_directive_next(symbols): def p_directive_next(symbols):
@ -273,5 +275,5 @@ def parse_song(content, filename=None):
write_tables=0, write_tables=0,
).parse( ).parse(
content, content,
lexer=ChordProLexer().lexer, lexer=ChordProLexer(filename=filename).lexer,
) )

8
patacrep/songs/syntax.py

@ -20,8 +20,7 @@ class Parser:
column = (token.lexpos - last_cr) + 1 column = (token.lexpos - last_cr) + 1
return column return column
@staticmethod def error(self, *, line=None, column=None, message=""):
def error(*, line=None, column=None, message=""):
"""Display an error message""" """Display an error message"""
coordinates = [] coordinates = []
if line is not None: if line is not None:
@ -35,7 +34,10 @@ class Parser:
text += message text += message
else: else:
text += "." 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): def p_error(self, token):
"""Manage parsing errors.""" """Manage parsing errors."""

1
test/test_chordpro/greensleeves.sgc

@ -10,6 +10,7 @@
{partition: greensleeves.ly} {partition: greensleeves.ly}
A[Am]las, my love, ye [G]do me wrong A[Am]las, my love, ye [G]do me wrong
To [Am]cast me oft dis[E]curteously To [Am]cast me oft dis[E]curteously
And [Am]I have loved [G]you so long 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} \lilypond{greensleeves.ly}
\begin{verse} \begin{verse}
A\[Am]las, my love, ye \[G]do me wrong A\[Am]las, my love, ye \[G]do me wrong
To \[Am]cast me oft dis\[E]curteously To \[Am]cast me oft dis\[E]curteously

1
test/test_chordpro/metadata.sgc

@ -18,4 +18,5 @@
{partition: Lilypond} {partition: Lilypond}
{image: Image} {image: Image}
Foo Foo

3
test/test_chordpro/metadata.tex

@ -22,7 +22,10 @@ Subtitle5}[
\lilypond{Lilypond} \lilypond{Lilypond}
\image{Image} \image{Image}
\begin{verse} \begin{verse}
Foo Foo
\end{verse} \end{verse}
\endsong \endsong

Loading…
Cancel
Save