diff --git a/patacrep/authors.py b/patacrep/authors.py index 8cde76cf..83a072e3 100644 --- a/patacrep/authors.py +++ b/patacrep/authors.py @@ -10,6 +10,8 @@ DEFAULT_AUTHWORDS = { "ignore": ["unknown"], "sep": ["and"], } +RE_AFTER = r"^.*\b{}\b(.*)$" +RE_SEPARATOR = r"^(.*)\b *{} *(\b.*)?$" def compile_authwords(authwords): """Convert strings of authwords to compiled regular expressions. @@ -23,11 +25,11 @@ def compile_authwords(authwords): # Compilation authwords['after'] = [ - re.compile(r"^.*\b%s\b(.*)$" % word, re.LOCALE) + re.compile(RE_AFTER.format(word), re.LOCALE) for word in authwords['after'] ] authwords['sep'] = [ - re.compile(r"^(.*)%s +(.*)$" % word, re.LOCALE) + re.compile(RE_SEPARATOR.format(word), re.LOCALE) for word in ([" %s" % word for word in authwords['sep']] + [',', ';']) ] @@ -37,31 +39,23 @@ def compile_authwords(authwords): def split_author_names(string): r"""Split author between first and last name. - The last space separates first and last name, but spaces following a - backslash or a command are not separators. - Examples: - - Edgar Allan Poe => Poe, Edgar Allan - - Edgar Allan \emph {Poe} => \emph {Poe}, Edgar Allan - - The Rolling\ Stones => Rolling\ Stones, The - - The {Rolling Stones} => {Rolling Stones}, The + The last space separates first and last name. LaTeX commands are ignored. + + >>> split_author_names("Edgar Allan Poe") + ('Poe', 'Edgar Allan') + >>> split_author_names("Edgar Allan \emph {Poe}") + ('{Poe}', 'Edgar Allan \\emph') + >>> split_author_names(r"The Rolling\ Stones") + ('Stones', 'The Rolling\\') + >>> split_author_names("The {Rolling Stones}") + ('Stones}', 'The {Rolling') + >>> split_author_names("The Rolling Stones") + ('Rolling\xa0Stones', 'The') + >>> split_author_names(" John Doe ") + ('Doe', 'John') """ - ignore_space = False - last_space = index = 0 - brace_count = 0 - for char in string.strip(): - index += 1 - if brace_count == 0: - if char == "\\": - ignore_space = True - elif not char.isalnum() and ignore_space: - ignore_space = False - elif char == " ": - last_space = index - if char == "}": - brace_count += 1 - if char == "{": - brace_count -= 1 - return string[last_space:], string[:last_space] + chunks = string.strip().split(" ") + return (chunks[-1].strip(), " ".join(chunks[:-1]).strip()) def split_sep_author(string, sep): @@ -71,16 +65,19 @@ def split_sep_author(string, sep): - string: string containing authors names ; - sep: regexp matching a separator. - >>> split_sep_author("Tintin and Milou", re.compile('^(.*) and (.*)$')) + >>> split_sep_author("Tintin and Milou", re.compile(RE_SEPARATOR.format("and"))) ['Tintin', 'Milou'] + >>> split_sep_author("Tintin,", re.compile(RE_SEPARATOR.format(","))) + ['Tintin'] """ authors = [] match = sep.match(string) while match: - authors.append(match.group(2)) + if match.group(2) is not None: + authors.append(match.group(2).strip()) string = match.group(1) match = sep.match(string) - authors.insert(0, string) + authors.insert(0, string.strip()) return authors ################################################################################ @@ -91,6 +88,9 @@ def processauthors_removeparen(authors_string): """Remove parentheses See docstring of processauthors() for more information. + + >>> processauthors_removeparen("This (foo) string (bar) contains (baz) parenthesis") + 'This string contains parenthesis' """ opening = 0 dest = "" @@ -107,6 +107,16 @@ def processauthors_split_string(authors_string, sep): """Split strings See docstring of processauthors() for more information. + + >>> processauthors_split_string("Tintin and Milou", [re.compile(RE_SEPARATOR.format("and"))]) + ['Tintin', 'Milou'] + >>> processauthors_split_string("Tintin, Milou", [re.compile(RE_SEPARATOR.format(","))]) + ['Tintin', 'Milou'] + >>> processauthors_split_string( + ... "Tintin, and Milou", + ... [re.compile(RE_SEPARATOR.format(word)) for word in ['and', ',']] + ... ) + ['Tintin', 'Milou'] """ authors_list = [authors_string] for sepword in sep: @@ -160,45 +170,47 @@ def processauthors_clean_authors(authors_list): ] def processauthors(authors_string, after=None, ignore=None, sep=None): - r"""Return a list of authors - - For example, we are processing: - # processauthors( - # [ - # " - # Lyrics by William Blake (from Milton, 1808), - # music by Hubert Parry (1916), - # and sung by The Royal\ Choir~of~Nowhere - # (just here to show you how processing is done) - # ", - # ], - # after = ["by"], - # ignore = ["anonymous"], - # sep = [re.compile('^(.*) and (.*)$')], - # ) + r"""Return an iterator of authors + + For example, in the following call: + + >>> set(processauthors( + ... ( + ... "Lyrics by William Blake (from Milton, 1808), " + ... "music by Hubert Parry (1916), " + ... "and sung by The Royal~Choir~of~FooBar " + ... "(just here to show you how processing is done)" + ... ), + ... **compile_authwords({ + ... 'after': ["by"], + ... 'ignore': ["anonymous"], + ... 'sep': ["and", ","], + ... }) + ... )) == {("Blake", "William"), ("Parry", "Hubert"), ("Royal~Choir~of~FooBar", "The")} + True The "authors_string" is processed as: 1) First, parenthesis (and its content) are removed. # "Lyrics by William Blake, music by Hubert Parry, - and sung by The Royal\ Choir~of~Nowhere" + and sung by The Royal~Choir~of~FooBar" 2) String is split, separators being comma and words from "sep". # ["Lyrics by William Blake", "music by Hubert Parry", - "sung by The Royal\ Choir~of~Nowhere"] + "sung by The Royal~Choir~of~FooBar"] 3) Everything before words in "after" is removed. - # ["William Blake", "Hubert Parry", "The Royal\ Choir~of~Nowhere"] + # ["William Blake", "Hubert Parry", "The Royal~Choir~of~FooBar"] 4) Strings containing words of "ignore" are dropped. - # ["William Blake", "Hubert Parry", The Royal\ Choir~of~Nowhere"] + # ["William Blake", "Hubert Parry", The Royal~Choir~of~FooBar"] 5) First and last names are splitted # [ # ("Blake", "William"), # ("Parry", "Hubert"), - # ("Royal\ Choir~of~Nowhere", "The"), + # ("Royal~Choir~of~FooBar", "The"), # ] """ @@ -209,10 +221,7 @@ def processauthors(authors_string, after=None, ignore=None, sep=None): if not ignore: ignore = [] - return [ - split_author_names(author) - for author - in processauthors_clean_authors( + for author in processauthors_clean_authors( processauthors_ignore_authors( processauthors_remove_after( processauthors_split_string( @@ -222,8 +231,8 @@ def processauthors(authors_string, after=None, ignore=None, sep=None): sep), after), ignore) - ) - ] + ): + yield split_author_names(author) def process_listauthors(authors_list, after=None, ignore=None, sep=None): """Process a list of authors, and return the list of resulting authors.""" diff --git a/patacrep/data/examples/datadir2/img/datadir2.ly b/patacrep/data/examples/datadir2/img/datadir2.ly new file mode 100644 index 00000000..1fd2bccb --- /dev/null +++ b/patacrep/data/examples/datadir2/img/datadir2.ly @@ -0,0 +1,19 @@ +\include "_lilypond/header" +\paper{paper-height = 6.5\cm} + +{ + \key a \minor + \time 6/8 + \partial 8 a'8 + \relative c''{ + c4 d8 e8. (f16) e8 d4 b8 g8. (a16) b8 + c4 a8 a8. (gis16) a8 b4 gis8 e4 a8 + c4 d8 e8. (f16 e8) d4 b8 g8. (a16) b8 + c8. (b16) a8 gis8. (fis16) gis8 a4 a8 a4. + + g'4. g8. (fis16) e8 d4 b8 g8. (a16) b8 + c4 (a8) a8. (gis16) a8 b4 gis8 e4. + g'4. g8. (fis16) e8 d4 b8 g8. (a16) b8 + c8. (b16) a8 gis8. (fis16) gis8 a4. a4. + } +} diff --git a/patacrep/data/examples/datadir2/img/datadir2.png b/patacrep/data/examples/datadir2/img/datadir2.png new file mode 100644 index 00000000..90a75c1d Binary files /dev/null and b/patacrep/data/examples/datadir2/img/datadir2.png differ diff --git a/patacrep/data/examples/example-subdir.sb b/patacrep/data/examples/example-subdir.sb index c7d23631..b1c93e9f 100644 --- a/patacrep/data/examples/example-subdir.sb +++ b/patacrep/data/examples/example-subdir.sb @@ -5,6 +5,7 @@ "lilypond", "pictures" ], +"datadir": ["datadir2"], "booktype" : "chorded", "template" : "patacrep.tex", "lang" : "french", diff --git a/patacrep/data/examples/example-test.sb b/patacrep/data/examples/example-test.sb new file mode 100644 index 00000000..4190c003 --- /dev/null +++ b/patacrep/data/examples/example-test.sb @@ -0,0 +1,13 @@ +{ +"bookoptions" : [ + "diagram", + "repeatchords", + "lilypond", + "pictures" + ], +"booktype" : "chorded", +"template" : "patacrep.tex", +"encoding": "utf8", + "content": ["tests/*.sg", "tests/*.sgc"] + +} diff --git a/patacrep/data/examples/songs/subdir/datadir2.sg b/patacrep/data/examples/songs/subdir/datadir2.sg new file mode 100644 index 00000000..f5bad860 --- /dev/null +++ b/patacrep/data/examples/songs/subdir/datadir2.sg @@ -0,0 +1,10 @@ +\beginsong{Image included from a different datadir\\\LaTeX} + [cov={datadir2}] + + \cover + + \lilypond{datadir2.ly} + + \image{datadir2} + +\endsong diff --git a/patacrep/data/examples/songs/subdir/datadir2.sgc b/patacrep/data/examples/songs/subdir/datadir2.sgc new file mode 100644 index 00000000..b83122d4 --- /dev/null +++ b/patacrep/data/examples/songs/subdir/datadir2.sgc @@ -0,0 +1,6 @@ +{title : Image included from a different datadir} +{subtitle: Chordpro} +{cover: datadir2.png} + +{partition: datadir2.ly} +{image: datadir2.png} diff --git a/patacrep/data/examples/songs/chords.sgc b/patacrep/data/examples/songs/tests/chords.sgc similarity index 100% rename from patacrep/data/examples/songs/chords.sgc rename to patacrep/data/examples/songs/tests/chords.sgc diff --git a/patacrep/data/examples/songs/errors.sgc b/patacrep/data/examples/songs/tests/errors.sgc similarity index 100% rename from patacrep/data/examples/songs/errors.sgc rename to patacrep/data/examples/songs/tests/errors.sgc diff --git a/patacrep/data/examples/songs/tests/nolyrics.sgc b/patacrep/data/examples/songs/tests/nolyrics.sgc new file mode 100644 index 00000000..beebbdb6 --- /dev/null +++ b/patacrep/data/examples/songs/tests/nolyrics.sgc @@ -0,0 +1,11 @@ +{title: No lyrics} +{subtitle: Test of verses without any lyrics} + +[A]This is a [Bb]verse +With [C#]lyrics + +[Adim] [Dmaj] +[Em3] + +[G4]This is [Emaj3]another +[Absus8]With lyrics diff --git a/patacrep/songs/chordpro/ast.py b/patacrep/songs/chordpro/ast.py index 7b331ecd..80949e10 100644 --- a/patacrep/songs/chordpro/ast.py +++ b/patacrep/songs/chordpro/ast.py @@ -157,6 +157,15 @@ class Verse(AST): return False return True + @property + def nolyrics(self): + """Return `True` iff verse contains only notes (no lyrics)""" + for line in self.lines: + for item in line.line: + if not (isinstance(item, Space) or isinstance(item, ChordList)): + return False + return True + class Chorus(Verse): """Chorus""" type = 'chorus' diff --git a/patacrep/songs/chordpro/data/chordpro/song_header b/patacrep/songs/chordpro/data/chordpro/song_header index 5289d96f..ac5d8329 100644 --- a/patacrep/songs/chordpro/data/chordpro/song_header +++ b/patacrep/songs/chordpro/data/chordpro/song_header @@ -13,7 +13,7 @@ (* endfor -*) (*- for author in authors -*) - {artist: (( author[1] ))(( author[0] ))} + {artist: (( author[1] )) (( author[0] ))} (* endfor *) (*- for key in ['album', 'copyright', 'tag'] *) diff --git a/patacrep/songs/chordpro/data/html/song_header b/patacrep/songs/chordpro/data/html/song_header index dbc40433..963ef686 100644 --- a/patacrep/songs/chordpro/data/html/song_header +++ b/patacrep/songs/chordpro/data/html/song_header @@ -7,7 +7,7 @@ (* endfor -*) (*- for author in authors -*) -

(( author[1] ))(( author[0] ))

+

(( author[1] )) (( author[0] ))

(* endfor *) diff --git a/patacrep/songs/chordpro/data/latex/content_verse b/patacrep/songs/chordpro/data/latex/content_verse index 8417534f..ae3aa34f 100644 --- a/patacrep/songs/chordpro/data/latex/content_verse +++ b/patacrep/songs/chordpro/data/latex/content_verse @@ -2,7 +2,15 @@ (* for line in content.lines -*) ((- render(line) )) (* endfor -*) -(*- else -*) +(*- elif content.nolyrics -*) +\ifchorded +\begin{verse*} + (* for line in content.lines *) + \musicnote {\nolyrics (( render(line) ))} + (* endfor *) +\end{verse*} +\fi +(*- else *) \begin{(( content.type ))} (* for line in content.lines *) (( render(line) )) diff --git a/patacrep/songs/chordpro/data/latex/song b/patacrep/songs/chordpro/data/latex/song index 044ee87b..6f51d673 100644 --- a/patacrep/songs/chordpro/data/latex/song +++ b/patacrep/songs/chordpro/data/latex/song @@ -16,7 +16,7 @@ }[ by={ (* for author in authors *) - (( author[1] ))(( author[0] )) + (( author[1] )) (( author[0] )) (*- if not loop.last -*) , (* endif *) diff --git a/test/test_authors.py b/test/test_authors.py new file mode 100644 index 00000000..f9dfc053 --- /dev/null +++ b/test/test_authors.py @@ -0,0 +1,73 @@ +"""Tests of author parsing.""" + +# pylint: disable=too-few-public-methods + +import unittest + +from patacrep import authors + +SPLIT_AUTHORS_DATA = [ + ("Edgar Allan Poe", ("Poe", "Edgar Allan")), + ("Richard M. Stallman", ("Stallman", "Richard M.")), + ("Georges Brassens", ("Brassens", "Georges")), + ("The Who", ("Who", "The")), + ("Cher", ("Cher", "")), + ("Red~Hot~Chili~Peppers", ("Red~Hot~Chili~Peppers", "")), + ("The mamas~and~the~papas", ("mamas~and~the~papas", "The")), + ("The mamas and the papas", ("mamas and the papas", "The")), # Unbreakable spaces + (r"\LaTeX command", ("command", r"\LaTeX")), # LaTeX commands are ignored + (r"\emph{Some braces}", ("braces}", r"\emph{Some")), # LaTeX commands are ignored + (r"The Rolling\ Stones", ("Stones", 'The Rolling\\')), # LaTeX commands are ignored + ] + +PROCESS_AUTHORS_DATA = [ + ( + ( + "Lyrics by William Blake (from Milton, 1808), music by Hubert " + "Parry (1916), and sung by The Royal~Choir~of~FooBar (just here to " + "show you how processing is done)" + ), + [ + ("Blake", "William"), + ("Parry", "Hubert"), + ("Royal~Choir~of~FooBar", "The"), + ] + ), + ( + "Anonyme (1967)", + [], + ), + ( + "Lucky Luke et Jolly Jumper", + [ + ("Luke", "Lucky"), + ("Jumper", "Jolly"), + ], + ), +] + +AUTHWORDS = authors.compile_authwords({ + "after": ["by"], + "ignore": ["anonymous", "Anonyme", "anonyme"], + "sep": ['and', 'et'], + }) + +class TestAutors(unittest.TestCase): + """Test of author parsing.""" + + def test_split_author_names(self): + """Test of :func:`patacrep.authors.split_author_names` function.""" + for argument, expected in SPLIT_AUTHORS_DATA: + with self.subTest(argument=argument, expected=expected): + self.assertEqual(authors.split_author_names(argument), expected) + + def test_processauthors(self): + """Test of :func:`patacrep.authors.processauthors` function.""" + for argument, expected in PROCESS_AUTHORS_DATA: + with self.subTest(argument=argument, expected=expected): + self.assertEqual( + set( + authors.processauthors(argument, **AUTHWORDS) + ), + set(expected) + ) diff --git a/test/test_chordpro/author_names.sgc b/test/test_chordpro/author_names.sgc index a6818f7c..36a39230 100644 --- a/test/test_chordpro/author_names.sgc +++ b/test/test_chordpro/author_names.sgc @@ -1,6 +1,6 @@ {language: english} {title: Title} {artist: The Beatles} -{artist: Oasis} +{artist: Oasis} {artist: The the beatles} diff --git a/test/test_chordpro/author_names.tex b/test/test_chordpro/author_names.tex index 123a58d3..8bf4fcfc 100644 --- a/test/test_chordpro/author_names.tex +++ b/test/test_chordpro/author_names.tex @@ -3,10 +3,10 @@ \beginsong{Title}[ by={ The Beatles, - Oasis, + Oasis, The the beatles }, ] -\endsong \ No newline at end of file +\endsong diff --git a/test/test_chordpro/greensleeves.sgc b/test/test_chordpro/greensleeves.sgc index eafed1a2..389f14d1 100644 --- a/test/test_chordpro/greensleeves.sgc +++ b/test/test_chordpro/greensleeves.sgc @@ -3,7 +3,7 @@ {title: Greensleeves} {title: Un autre sous-titre} {title: Un sous titre} -{artist: Traditionnel} +{artist: Traditionnel} {album: Angleterre} {cov: traditionnel} diff --git a/test/test_chordpro/greensleeves.tex b/test/test_chordpro/greensleeves.tex index f760253c..a0b602c0 100644 --- a/test/test_chordpro/greensleeves.tex +++ b/test/test_chordpro/greensleeves.tex @@ -5,7 +5,7 @@ Un autre sous-titre\\ Un sous titre}[ by={ - Traditionnel }, + Traditionnel }, album={Angleterre}, cov={traditionnel}, ] diff --git a/test/test_chordpro/metadata.sgc b/test/test_chordpro/metadata.sgc index 499bb833..979c1388 100644 --- a/test/test_chordpro/metadata.sgc +++ b/test/test_chordpro/metadata.sgc @@ -6,8 +6,8 @@ {title: Subtitle3} {title: Subtitle4} {title: Subtitle5} -{artist: Author1} -{artist: Author2} +{artist: Author1} +{artist: Author2} {album: Album} {copyright: Copyright} {cov: Cover} diff --git a/test/test_chordpro/metadata.tex b/test/test_chordpro/metadata.tex index bb5393a1..fa29ec3c 100644 --- a/test/test_chordpro/metadata.tex +++ b/test/test_chordpro/metadata.tex @@ -7,8 +7,8 @@ Subtitle3\\ Subtitle4\\ Subtitle5}[ by={ - Author1, - Author2 }, + Author1, + Author2 }, album={Album}, copyright={Copyright}, cov={Cover}, diff --git a/test/test_chordpro/nolyrics.sgc b/test/test_chordpro/nolyrics.sgc new file mode 100644 index 00000000..fd59c884 --- /dev/null +++ b/test/test_chordpro/nolyrics.sgc @@ -0,0 +1,14 @@ +{language: english} + +A chorus [A]with lyrics +[Emaj3]maj et nombre + + +[A B#] + + +[A] [B] +[C] + + +A chorus [C]with lyrics diff --git a/test/test_chordpro/nolyrics.source b/test/test_chordpro/nolyrics.source new file mode 100644 index 00000000..881c1ab9 --- /dev/null +++ b/test/test_chordpro/nolyrics.source @@ -0,0 +1,9 @@ +A chorus [A]with lyrics +[Emaj3]maj et nombre + +[A B#] + +[A] [B] +[C] + +A chorus [C]with lyrics diff --git a/test/test_chordpro/nolyrics.tex b/test/test_chordpro/nolyrics.tex new file mode 100644 index 00000000..343f4e63 --- /dev/null +++ b/test/test_chordpro/nolyrics.tex @@ -0,0 +1,34 @@ +\selectlanguage{english} + +\beginsong{}[ + by={ + }, +] + + +\begin{verse} + A chorus \[A]with lyrics + \[Emaj3]maj et nombre +\end{verse} + + +\ifchorded +\begin{verse*} + \musicnote {\nolyrics \[A B#]} +\end{verse*} +\fi + + +\ifchorded +\begin{verse*} + \musicnote {\nolyrics \[A] \[B]} + \musicnote {\nolyrics \[C]} +\end{verse*} +\fi + + +\begin{verse} + A chorus \[C]with lyrics +\end{verse} + +\endsong