mirror of https://github.com/patacrep/patacrep.git
Louis
10 years ago
58 changed files with 853 additions and 119 deletions
@ -1,71 +1,264 @@ |
|||
# -*- coding: utf-8 -*- |
|||
"""Abstract Syntax Tree for ChordPro code.""" |
|||
|
|||
class AST: |
|||
"""Base class for the tree.""" |
|||
# pylint: disable=no-init |
|||
metadata = None |
|||
|
|||
@classmethod |
|||
def init_metadata(cls): |
|||
"""Clear metadata |
|||
|
|||
As this attribute is a class attribute, it as to be reset at each new |
|||
parsing. |
|||
""" |
|||
cls.metadata = { |
|||
'@languages': set(), |
|||
import functools |
|||
|
|||
def _indent(string): |
|||
return "\n".join([" {}".format(line) for line in string.split('\n')]) |
|||
|
|||
INLINE_PROPERTIES = { |
|||
"lilypond", |
|||
"comment", |
|||
"guitar_comment", |
|||
"image", |
|||
} |
|||
|
|||
DIRECTIVE_SHORTCUTS = { |
|||
"t": "title", |
|||
"st": "subtitle", |
|||
"a": "album", |
|||
"by": "artist", |
|||
"c": "comment", |
|||
"gc": "guitar_comment", |
|||
} |
|||
|
|||
class Expression(AST): |
|||
"""ChordPro expression""" |
|||
def directive_name(text): |
|||
if text in DIRECTIVE_SHORTCUTS: |
|||
return DIRECTIVE_SHORTCUTS[text] |
|||
return text |
|||
|
|||
|
|||
class AST: |
|||
inline = False |
|||
|
|||
class Line(AST): |
|||
"""A line is a sequence of (possibly truncated) words, spaces and chords.""" |
|||
|
|||
def __init__(self): |
|||
super().__init__() |
|||
self.line = [] |
|||
|
|||
def prepend(self, data): |
|||
self.line.insert(0, data) |
|||
return self |
|||
|
|||
def __str__(self): |
|||
return "".join([str(item) for item in self.line]) |
|||
|
|||
def strip(self): |
|||
while True: |
|||
if not self.line: |
|||
return self |
|||
if isinstance(self.line[0], Space): |
|||
del self.line[0] |
|||
continue |
|||
if isinstance(self.line[-1], Space): |
|||
del self.line[-1] |
|||
continue |
|||
return self |
|||
|
|||
class LineElement(AST): |
|||
"""Something present on a line.""" |
|||
pass |
|||
|
|||
class Word(LineElement): |
|||
"""A chunk of word.""" |
|||
|
|||
def __init__(self, value): |
|||
super().__init__() |
|||
self.content = [value] |
|||
self.value = value |
|||
|
|||
def __str__(self): |
|||
return self.value |
|||
|
|||
class Space(LineElement): |
|||
"""A space between words""" |
|||
|
|||
def __init__(self): |
|||
super().__init__() |
|||
|
|||
def __str__(self): |
|||
return " " |
|||
|
|||
def prepend(self, value): |
|||
"""Add a value at the beginning of the content list.""" |
|||
if value is not None: |
|||
self.content.insert(0, value) |
|||
class Chord(LineElement): |
|||
"""A chord.""" |
|||
|
|||
def __init__(self, value): |
|||
super().__init__() |
|||
self.value = value |
|||
|
|||
def __str__(self): |
|||
return "[{}]".format(self.value) |
|||
|
|||
class Verse(AST): |
|||
"""A verse (or bridge, or chorus)""" |
|||
type = "verse" |
|||
inline = True |
|||
|
|||
def __init__(self, block=None): |
|||
super().__init__() |
|||
self.lines = [] # TODO check block |
|||
|
|||
def prepend(self, data): |
|||
self.lines.insert(0, data) |
|||
return self |
|||
|
|||
def __str__(self): |
|||
return "".join([str(item) for item in self.content]) |
|||
|
|||
class SongPart(AST): |
|||
"""ChordPro start_of/end_of command |
|||
|
|||
{start_of_chorus}, {end_of_tab}, {eov} ... |
|||
""" |
|||
|
|||
class Type: |
|||
CHORUS = ("chorus", |
|||
"start_of_chorus", "end_of_chorus", |
|||
"soc", "eoc") |
|||
VERSE = ("verse", |
|||
"start_of_verse", "end_of_verse", |
|||
"sov", "eov") |
|||
BRIDGE = ("bridge", |
|||
"start_of_bridge", "end_of_bridge", |
|||
"sob", "eob") |
|||
TAB = ("tab", |
|||
"start_of_tab", "end_of_tab", |
|||
"sot", "eot") |
|||
|
|||
def __init__(self, name): |
|||
if "_" in name: |
|||
self.init_long_form(name) |
|||
return '{{start_of_{type}}}\n{content}\n{{end_of_{type}}}'.format( |
|||
type = self.type, |
|||
content = _indent("\n".join([str(line) for line in self.lines])), |
|||
) |
|||
|
|||
class Chorus(Verse): |
|||
type = 'chorus' |
|||
|
|||
class Bridge(Verse): |
|||
type = 'bridge' |
|||
|
|||
class Song(AST): |
|||
"""A song""" |
|||
|
|||
METADATA_TYPE = { |
|||
"title": "add_title", |
|||
"subtitle": "add_subtitle", |
|||
"language": "add_language", |
|||
"artist": "add_author", |
|||
} |
|||
|
|||
def __init__(self): |
|||
super().__init__() |
|||
self.content = [] |
|||
self.meta = [] |
|||
self._authors = [] |
|||
self._titles = [] |
|||
self._subtitles = [] |
|||
self._languages = set() |
|||
|
|||
def add(self, data): |
|||
if data is None: |
|||
if not (self.content and isinstance(self.content[0], Newline)): |
|||
self.content.insert(0, Newline()) |
|||
elif isinstance(data, Line): |
|||
if not (self.content and isinstance(self.content[0], Verse)): |
|||
self.content.insert(0, Verse()) |
|||
self.content[0].prepend(data.strip()) |
|||
elif data.inline: |
|||
self.content.insert(0, data) |
|||
elif isinstance(data, Directive): |
|||
name = directive_name(data.keyword) |
|||
if name in self.METADATA_TYPE: |
|||
getattr(self, self.METADATA_TYPE[name])(*data.as_tuple) |
|||
else: |
|||
self.meta.append(data) |
|||
else: |
|||
self.init_short_form(name) |
|||
raise Exception() |
|||
return self |
|||
|
|||
def str_meta(self): |
|||
for title in self.titles: |
|||
yield "{{title: {}}}".format(title) |
|||
for language in sorted(self.languages): |
|||
yield "{{language: {}}}".format(language) |
|||
for author in self.authors: |
|||
yield "{{by: {}}}".format(author) |
|||
for key in sorted(self.meta): |
|||
yield str(key) |
|||
|
|||
def __str__(self): |
|||
return ( |
|||
"\n".join(self.str_meta()).strip() |
|||
+ |
|||
"\n========\n" |
|||
+ |
|||
"\n".join([str(item) for item in self.content]).strip() |
|||
) |
|||
|
|||
|
|||
def add_title(self, __ignored, title): |
|||
self._titles.insert(0, title) |
|||
|
|||
def add_subtitle(self, __ignored, title): |
|||
self._subtitles.insert(0, title) |
|||
|
|||
@property |
|||
def titles(self): |
|||
return self._titles + self._subtitles |
|||
|
|||
def add_author(self, __ignored, title): |
|||
self._authors.insert(0, title) |
|||
|
|||
@property |
|||
def authors(self): |
|||
return self._authors |
|||
|
|||
def add_language(self, __ignored, language): |
|||
self._languages.add(language) |
|||
|
|||
@property |
|||
def languages(self): |
|||
return self._languages |
|||
|
|||
|
|||
|
|||
class Newline(AST): |
|||
def __str__(self): |
|||
return self.name |
|||
return "" |
|||
|
|||
@functools.total_ordering |
|||
class Directive(AST): |
|||
"""A directive""" |
|||
|
|||
def __init__(self): |
|||
super().__init__() |
|||
self.keyword = "" |
|||
self.argument = None |
|||
|
|||
def init_short_form(self, name): |
|||
self.type = "" |
|||
@property |
|||
def keyword(self): |
|||
return self._keyword |
|||
|
|||
@property |
|||
def inline(self): |
|||
return self.keyword in INLINE_PROPERTIES |
|||
|
|||
@keyword.setter |
|||
def keyword(self, value): |
|||
self._keyword = value.strip() |
|||
|
|||
def __str__(self): |
|||
if self.argument is not None: |
|||
return "{{{}: {}}}".format( |
|||
self.keyword, |
|||
self.argument, |
|||
) |
|||
else: |
|||
return "{{{}}}".format(self.keyword) |
|||
|
|||
def init_long_form(self, name): |
|||
self.type = "" |
|||
@property |
|||
def as_tuple(self): |
|||
return (self.keyword, self.argument) |
|||
|
|||
def __eq__(self, other): |
|||
return self.as_tuple == other.as_tuple |
|||
|
|||
def __lt__(self, other): |
|||
return self.as_tuple < other.as_tuple |
|||
|
|||
class Tab(AST): |
|||
"""Tablature""" |
|||
|
|||
inline = True |
|||
|
|||
def __init__(self): |
|||
super().__init__() |
|||
self.content = [] |
|||
|
|||
def prepend(self, data): |
|||
self.content.insert(0, data) |
|||
return self |
|||
|
|||
def __str__(self): |
|||
return '{{start_of_tab}}\n{}\n{{end_of_tab}}'.format( |
|||
_indent("\n".join(self.content)), |
|||
) |
|||
|
|||
|
@ -0,0 +1 @@ |
|||
======== |
@ -0,0 +1 @@ |
|||
A verse line |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_verse} |
|||
A verse line |
|||
{end_of_verse} |
@ -0,0 +1 @@ |
|||
{title : A directive} |
@ -0,0 +1,2 @@ |
|||
{title: A directive} |
|||
======== |
@ -0,0 +1,4 @@ |
|||
|
|||
|
|||
|
|||
|
@ -0,0 +1 @@ |
|||
======== |
@ -0,0 +1,3 @@ |
|||
{soc} |
|||
A one line chorus |
|||
{eoc} |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_chorus} |
|||
A one line chorus |
|||
{end_of_chorus} |
@ -0,0 +1,3 @@ |
|||
{sob} |
|||
A one line bridge |
|||
{eob} |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_bridge} |
|||
A one line bridge |
|||
{end_of_bridge} |
@ -0,0 +1 @@ |
|||
# A comment |
@ -0,0 +1 @@ |
|||
======== |
@ -0,0 +1,3 @@ |
|||
{sot} |
|||
A tab |
|||
{eot} |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_tab} |
|||
A tab |
|||
{end_of_tab} |
@ -0,0 +1,10 @@ |
|||
|
|||
|
|||
|
|||
# comment |
|||
# comment |
|||
|
|||
|
|||
A lot of new lines |
|||
|
|||
# comment |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_verse} |
|||
A lot of new lines |
|||
{end_of_verse} |
@ -0,0 +1,15 @@ |
|||
|
|||
|
|||
|
|||
# comment |
|||
# comment |
|||
|
|||
|
|||
A lot of new lines |
|||
|
|||
# comment |
|||
|
|||
{title: and a directive} |
|||
|
|||
# comment |
|||
|
@ -0,0 +1,5 @@ |
|||
{title: and a directive} |
|||
======== |
|||
{start_of_verse} |
|||
A lot of new lines |
|||
{end_of_verse} |
@ -0,0 +1 @@ |
|||
A line[A] with a chord |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_verse} |
|||
A line[A] with a chord |
|||
{end_of_verse} |
@ -0,0 +1 @@ |
|||
A line ending with a chord[A] |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_verse} |
|||
A line ending with a chord[A] |
|||
{end_of_verse} |
@ -0,0 +1 @@ |
|||
[A]A line starting with a chord |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_verse} |
|||
[A]A line starting with a chord |
|||
{end_of_verse} |
@ -0,0 +1,5 @@ |
|||
{sot} |
|||
A table |
|||
wit many # weir [ |
|||
[ symbols |
|||
{eot} |
@ -0,0 +1,6 @@ |
|||
======== |
|||
{start_of_tab} |
|||
A table |
|||
wit many # weir [ |
|||
[ symbols |
|||
{end_of_tab} |
@ -0,0 +1 @@ |
|||
A verse line |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_verse} |
|||
A verse line |
|||
{end_of_verse} |
@ -0,0 +1 @@ |
|||
{title : A directive} |
@ -0,0 +1,2 @@ |
|||
{title: A directive} |
|||
======== |
@ -0,0 +1,4 @@ |
|||
|
|||
|
|||
|
|||
|
@ -0,0 +1 @@ |
|||
======== |
@ -0,0 +1,3 @@ |
|||
{soc} |
|||
A one line chorus |
|||
{eoc} |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_chorus} |
|||
A one line chorus |
|||
{end_of_chorus} |
@ -0,0 +1,3 @@ |
|||
{sob} |
|||
A one line bridge |
|||
{eob} |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_bridge} |
|||
A one line bridge |
|||
{end_of_bridge} |
@ -0,0 +1 @@ |
|||
# A comment |
@ -0,0 +1 @@ |
|||
======== |
@ -0,0 +1,3 @@ |
|||
{sot} |
|||
A tab |
|||
{eot} |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_tab} |
|||
A tab |
|||
{end_of_tab} |
@ -0,0 +1,10 @@ |
|||
|
|||
|
|||
|
|||
# comment |
|||
# comment |
|||
|
|||
|
|||
A lot of new lines |
|||
|
|||
# comment |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_verse} |
|||
A lot of new lines |
|||
{end_of_verse} |
@ -0,0 +1,15 @@ |
|||
|
|||
|
|||
|
|||
# comment |
|||
# comment |
|||
|
|||
|
|||
A lot of new lines |
|||
|
|||
# comment |
|||
|
|||
{title: and a directive} |
|||
|
|||
# comment |
|||
|
@ -0,0 +1,5 @@ |
|||
{title: and a directive} |
|||
======== |
|||
{start_of_verse} |
|||
A lot of new lines |
|||
{end_of_verse} |
@ -0,0 +1 @@ |
|||
[A]A line starting with a chord |
@ -0,0 +1,44 @@ |
|||
{language : english} |
|||
{columns : 2} |
|||
{subtitle : Un sous titre} |
|||
{ title : Greensleeves} |
|||
{title : Un autre sous-titre} |
|||
{artist: Traditionnel} |
|||
{cover : traditionnel } |
|||
{album :Angleterre} |
|||
|
|||
{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 |
|||
De[Am]lighting [E]in your [Am]companie |
|||
|
|||
{start_of_chorus} |
|||
[C]Green[B]sleeves was [G]all my joy |
|||
[Am]Greensleeves was [E]my delight |
|||
[C]Greensleeves was my [G]heart of gold |
|||
And [Am]who but [E]Ladie [Am]Greensleeves |
|||
{end_of_chorus} |
|||
|
|||
I [Am]have been ready [G]at your hand |
|||
To [Am]grant what ever [E]you would crave |
|||
I [Am]have both waged [G]life and land |
|||
Your [Am]love and [E]good will [Am]for to have |
|||
|
|||
I [Am]bought thee kerchers [G]to thy head |
|||
That [Am]were wrought fine and [E]gallantly |
|||
I [Am]kept thee both at [G]boord and bed |
|||
Which [Am]cost my [E]purse well [Am]favouredly |
|||
|
|||
I [Am]bought thee peticotes [G]of the best |
|||
The [Am]cloth so fine as [E]fine might be |
|||
I [Am]gave thee jewels [G]for thy chest |
|||
And [Am]all this [E]cost I [Am]spent on thee |
|||
|
|||
|
|||
Thy [Am]smock of silke, both [G]faire and white |
|||
With [Am]gold embrodered [E]gorgeously |
|||
Thy [Am]peticote of [G]sendall right |
|||
And [Am]this I [E]bought thee [Am]gladly |
@ -0,0 +1,51 @@ |
|||
{title: Greensleeves} |
|||
{title: Un autre sous-titre} |
|||
{title: Un sous titre} |
|||
{language: english} |
|||
{by: Traditionnel} |
|||
{album: Angleterre} |
|||
{columns: 2} |
|||
{cover: traditionnel} |
|||
{partition: greensleeves.ly} |
|||
======== |
|||
{start_of_verse} |
|||
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 |
|||
De[Am]lighting [E]in your [Am]companie |
|||
{end_of_verse} |
|||
|
|||
{start_of_chorus} |
|||
[C]Green[B]sleeves was [G]all my joy |
|||
[Am]Greensleeves was [E]my delight |
|||
[C]Greensleeves was my [G]heart of gold |
|||
And [Am]who but [E]Ladie [Am]Greensleeves |
|||
{end_of_chorus} |
|||
|
|||
{start_of_verse} |
|||
I [Am]have been ready [G]at your hand |
|||
To [Am]grant what ever [E]you would crave |
|||
I [Am]have both waged [G]life and land |
|||
Your [Am]love and [E]good will [Am]for to have |
|||
{end_of_verse} |
|||
|
|||
{start_of_verse} |
|||
I [Am]bought thee kerchers [G]to thy head |
|||
That [Am]were wrought fine and [E]gallantly |
|||
I [Am]kept thee both at [G]boord and bed |
|||
Which [Am]cost my [E]purse well [Am]favouredly |
|||
{end_of_verse} |
|||
|
|||
{start_of_verse} |
|||
I [Am]bought thee peticotes [G]of the best |
|||
The [Am]cloth so fine as [E]fine might be |
|||
I [Am]gave thee jewels [G]for thy chest |
|||
And [Am]all this [E]cost I [Am]spent on thee |
|||
{end_of_verse} |
|||
|
|||
{start_of_verse} |
|||
Thy [Am]smock of silke, both [G]faire and white |
|||
With [Am]gold embrodered [E]gorgeously |
|||
Thy [Am]peticote of [G]sendall right |
|||
And [Am]this I [E]bought thee [Am]gladly |
|||
{end_of_verse} |
@ -0,0 +1,20 @@ |
|||
{subtitle: Subtitle3} |
|||
{title: Title} |
|||
{title: Subtitle1} |
|||
{subtitle: Subtitle4} |
|||
{t: Subtitle2} |
|||
{st: Subtitle5} |
|||
{language: french} |
|||
{language: english} |
|||
{by: Author1} |
|||
{artist: Author2} |
|||
{album: Albom} |
|||
{copyright: Copyright} |
|||
{cover: Cover} |
|||
{vcover: VCover} |
|||
{capo: Capo} |
|||
{foo: Foo} |
|||
{comment: Comment} |
|||
{guitar_comment: GuitarComment} |
|||
{image: Image} |
|||
{lilypond: Lilypond} |
@ -0,0 +1,21 @@ |
|||
{title: Title} |
|||
{title: Subtitle1} |
|||
{title: Subtitle2} |
|||
{title: Subtitle3} |
|||
{title: Subtitle4} |
|||
{title: Subtitle5} |
|||
{language: english} |
|||
{language: french} |
|||
{by: Author1} |
|||
{by: Author2} |
|||
{album: Albom} |
|||
{capo: Capo} |
|||
{copyright: Copyright} |
|||
{cover: Cover} |
|||
{foo: Foo} |
|||
{vcover: VCover} |
|||
======== |
|||
{comment: Comment} |
|||
{guitar_comment: GuitarComment} |
|||
{image: Image} |
|||
{lilypond: Lilypond} |
@ -0,0 +1,36 @@ |
|||
import glob |
|||
import os |
|||
import unittest |
|||
|
|||
from patacrep.songs.chordpro import syntax as chordpro |
|||
|
|||
class ParserTestCase(unittest.TestCase): |
|||
|
|||
def test_txt(self): |
|||
for txt in sorted(glob.glob(os.path.join( |
|||
os.path.dirname(__file__), |
|||
'*.txt', |
|||
))): |
|||
basename = txt[:-len('.txt')] |
|||
with open("{}.sgc".format(basename), 'r', encoding='utf8') as sourcefile: |
|||
with open("{}.txt".format(basename), 'r', encoding='utf8') as expectfile: |
|||
#print(os.path.basename(sourcefile.name)) |
|||
#with open("{}.txt.diff".format(basename), 'w', encoding='utf8') as difffile: |
|||
# difffile.write( |
|||
# str(chordpro.parse_song( |
|||
# sourcefile.read(), |
|||
# os.path.basename(sourcefile.name), |
|||
# )).strip() |
|||
# ) |
|||
# sourcefile.seek(0) |
|||
self.assertMultiLineEqual( |
|||
str(chordpro.parse_song( |
|||
sourcefile.read(), |
|||
os.path.basename(sourcefile.name), |
|||
)).strip(), |
|||
expectfile.read().strip(), |
|||
) |
|||
|
|||
def test_tex(self): |
|||
# TODO |
|||
pass |
Loading…
Reference in new issue