diff --git a/src/html/css/index.css b/src/html/css/index.css
index 0918c9dc..9148e096 100755
--- a/src/html/css/index.css
+++ b/src/html/css/index.css
@@ -101,15 +101,29 @@ a:hover {
#songs li:last-child { border-bottom: none; }
#songs li:hover { background-color: #eef2f5; }
-#songs li a.title {
+/* Main song link wrapper */
+#songs li a.song-link {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 0.5em;
+ flex: 1;
+ min-width: 0;
+ text-decoration: none;
+ color: inherit;
+}
+
+#songs li a.song-link:hover {
+ text-decoration: none;
+}
+
+#songs li a.song-link .title {
font-size: 1.1em;
color: #333;
font-weight: 400;
- margin-left: 1em;
}
-#songs li .alias {
- margin-left: 0.5em; /* Adds space between the title and the alias */
+#songs li a.song-link .alias {
font-size: 0.9em;
color: #6c757d;
font-style: italic;
@@ -123,14 +137,15 @@ a:hover {
content: ')';
}
-#songs li .artist {
+#songs li a.song-link .artist {
margin-left: auto; /* This takes up all available space, pushing the element to the end */
padding-left: 1em; /* Ensures some space between the alias and artist on smaller screens */
+ font-size: 0.95em;
+ color: #6c757d;
}
-#songs li a:hover {
- text-decoration: none;
+#songs li a.song-link:hover .title {
color: #0056b3;
}
diff --git a/src/html/index_generator.py b/src/html/index_generator.py
index d3fadaa1..7229a334 100644
--- a/src/html/index_generator.py
+++ b/src/html/index_generator.py
@@ -7,28 +7,105 @@
# def name_of_file(song):
# return os.path.splitext(os.path.split(song)[1])[0]
-def create_index_xhtml(list_of_songs_meta, target_dir):
- tmp_path = 'index.xhtml'
+def create_index_html(list_of_songs_meta, target_dir):
+ tmp_path = 'index.html'
out_path = os.path.join(target_dir, tmp_path)
- tree = etree.parse(os.path.join(sb.repo_dir(), "./src/html/templates/index.xhtml"))
+
+ # Try to parse as HTML first, fall back to XHTML if needed
+ template_path = os.path.join(sb.repo_dir(), "./src/html/templates/index.xhtml")
+ parser = etree.HTMLParser()
+ tree = etree.parse(template_path, parser)
+
+ # Add SEO meta tags to head
+ head = tree.getroot().find(".//head")
+ if head is None:
+ head = tree.getroot().find(".//{http://www.w3.org/1999/xhtml}head")
+
+ if head is not None:
+ # Add charset and viewport if not present
+ if head.find(".//meta[@charset]") is None:
+ etree.SubElement(head, "meta", attrib={"charset": "utf-8"}, nsmap=None).tail = "\n"
+
+ # Add canonical URL
+ etree.SubElement(head, "link", attrib={"rel": "canonical", "href": "https://spiewaj.com/"}, nsmap=None).tail = "\n"
+
+ # Add meta description with keywords
+ desc_meta = etree.SubElement(head, "meta", attrib={
+ "name": "description",
+ "content": "Śpiewnik harcerski online - teksty i chwyty gitarowe piosenek harcerskich, biesiadnych, turystycznych i obozowych. Darmowy śpiewnik z akordami do druku (PDF) i na Kindle (EPUB)."
+ }, nsmap=None)
+ desc_meta.tail = "\n"
+
+ # Add keywords
+ etree.SubElement(head, "meta", attrib={
+ "name": "keywords",
+ "content": "śpiewnik harcerski, teksty piosenek, chwyty gitarowe, akordy, piosenki harcerskie, piosenki biesiadne, piosenki turystyczne, śpiewnik pdf, śpiewnik kindle"
+ }, nsmap=None).tail = "\n"
+
+ # Add Open Graph tags
+ etree.SubElement(head, "meta", attrib={"property": "og:title", "content": "Spiewaj.com - Śpiewnik harcerski online"}, nsmap=None).tail = "\n"
+ etree.SubElement(head, "meta", attrib={"property": "og:type", "content": "website"}, nsmap=None).tail = "\n"
+ etree.SubElement(head, "meta", attrib={"property": "og:url", "content": "https://spiewaj.com/"}, nsmap=None).tail = "\n"
+ etree.SubElement(head, "meta", attrib={"property": "og:description", "content": "Śpiewnik harcerski - teksty i chwyty piosenek"}, nsmap=None).tail = "\n"
+ etree.SubElement(head, "meta", attrib={"property": "og:locale", "content": "pl_PL"}, nsmap=None).tail = "\n"
+ etree.SubElement(head, "meta", attrib={"property": "og:site_name", "content": "Spiewaj.com"}, nsmap=None).tail = "\n"
+
+ # Add Twitter Card tags
+ etree.SubElement(head, "meta", attrib={"name": "twitter:card", "content": "summary"}, nsmap=None).tail = "\n"
+ etree.SubElement(head, "meta", attrib={"name": "twitter:title", "content": "Spiewaj.com - Śpiewnik harcerski online"}, nsmap=None).tail = "\n"
+ etree.SubElement(head, "meta", attrib={"name": "twitter:description", "content": "Śpiewnik harcerski - teksty i chwyty piosenek"}, nsmap=None).tail = "\n"
+
+ # Add JSON-LD structured data with BreadcrumbList and ItemList
+ script_ld = etree.SubElement(head, "script", attrib={"type": "application/ld+json"}, nsmap=None)
+ import json
+ ld_json = {
+ "@context": "https://schema.org",
+ "@type": "WebSite",
+ "name": "Spiewaj.com",
+ "alternateName": "Śpiewnik",
+ "url": "https://spiewaj.com/",
+ "description": "Śpiewnik - teksty i chwyty piosenek popularnych, harcerskich, turystycznych",
+ "inLanguage": "pl-PL",
+ "potentialAction": {
+ "@type": "SearchAction",
+ "target": "https://spiewaj.com/#songSearch={search_term_string}",
+ "query-input": "required name=search_term_string"
+ }
+ }
+ script_ld.text = json.dumps(ld_json, ensure_ascii=False, indent=2)
+ script_ld.tail = "\n"
- # List of songs
- ul = tree.getroot().find(".//{http://www.w3.org/1999/xhtml}ul[@id='songs']")
+ # List of songs - try both HTML and XHTML namespaces
+ ul = tree.getroot().find(".//ul[@id='songs']")
+ if ul is None:
+ ul = tree.getroot().find(".//{http://www.w3.org/1999/xhtml}ul[@id='songs']")
for i in range(len(list_of_songs_meta)):
song = list_of_songs_meta[i]
if song.is_alias():
continue
- song_html = os.path.join("./songs_html", song.base_file_name() + '.xhtml')
+ song_html = os.path.join("./songs_html", song.base_file_name() + '.html')
li = etree.SubElement(ul, "li")
- # entire li is clickable and should navigate to the song
- li.attrib['onclick'] = "location.href='"+song_html+"';"
-
- #
+
+ # Main anchor wrapping the title - SEO-friendly structure
+ a_main = etree.SubElement(li, "a")
+ a_main.attrib['class'] = 'song-link'
+ a_main.attrib['href'] = song_html
+
+ # Build descriptive title attribute for SEO
+ title_text = song.effectiveTitle()
+ if song.artist():
+ a_main.attrib['title'] = f"{title_text} - {song.artist()} - tekst i chwyty"
+ else:
+ a_main.attrib['title'] = f"{title_text} - tekst i chwyty"
+
+ # Edit button (outside main link for event handling)
button = etree.SubElement(li, "button")
button.attrib['class'] = 'editicon'
button.attrib['onclick'] = "edit('"+os.path.relpath(song.plik(), start=os.path.join(sb.repo_dir(), "songs"))+"');event.stopPropagation();"
+ button.attrib['aria-label'] = f"Edytuj {title_text}"
span = etree.SubElement(button, 'span')
span.attrib['class'] = 'material-symbols-outlined'
+ span.attrib['aria-hidden'] = 'true'
span.text = 'edit'
# A4 PDF link
@@ -36,10 +113,13 @@ def create_index_xhtml(list_of_songs_meta, target_dir):
a_a4.attrib['class'] = 'pdflink'
a_a4.attrib['href'] = f"./songs_pdf/{song.base_file_name()}.a4.pdf"
a_a4.attrib['target'] = '_blank'
- a_a4.attrib['title'] = 'Download A4 PDF'
+ a_a4.attrib['rel'] = 'noopener'
+ a_a4.attrib['title'] = f"{title_text} - PDF A4"
+ a_a4.attrib['aria-label'] = f"Pobierz {title_text} PDF A4"
a_a4.attrib['onclick'] = 'event.stopPropagation();'
span_a4 = etree.SubElement(a_a4, 'span')
span_a4.attrib['class'] = 'material-symbols-outlined'
+ span_a4.attrib['aria-hidden'] = 'true'
span_a4.text = 'picture_as_pdf'
text_a4 = etree.SubElement(a_a4, 'span')
text_a4.attrib['class'] = 'pdf-label'
@@ -50,27 +130,40 @@ def create_index_xhtml(list_of_songs_meta, target_dir):
a_a5.attrib['class'] = 'pdflink'
a_a5.attrib['href'] = f"./songs_pdf/{song.base_file_name()}.a5.pdf"
a_a5.attrib['target'] = '_blank'
- a_a5.attrib['title'] = 'Download A5 PDF'
+ a_a5.attrib['rel'] = 'noopener'
+ a_a5.attrib['title'] = f"{title_text} - PDF A5"
+ a_a5.attrib['aria-label'] = f"Pobierz {title_text} PDF A5"
a_a5.attrib['onclick'] = 'event.stopPropagation();'
span_a5 = etree.SubElement(a_a5, 'span')
span_a5.attrib['class'] = 'material-symbols-outlined'
+ span_a5.attrib['aria-hidden'] = 'true'
span_a5.text = 'picture_as_pdf'
text_a5 = etree.SubElement(a_a5, 'span')
text_a5.attrib['class'] = 'pdf-label'
text_a5.text = 'A5'
- a = etree.SubElement(li, "a")
- a.attrib['class'] = 'title'
- a.attrib['href'] = song_html
- a.text = list_of_songs_meta[i].effectiveTitle()
+
+ # Title span inside main link
+ title_span = etree.SubElement(a_main, "span")
+ title_span.attrib['class'] = 'title'
+ title_span.text = title_text
+
+ # Aliases inside main link
for song_alias in song.aliases():
- alias = etree.SubElement(li, "span", attrib={"class": "alias"})
+ alias = etree.SubElement(a_main, "span", attrib={"class": "alias"})
alias.text = song_alias
+
+ # Artist inside main link
if song.artist():
- artist = etree.SubElement(li, "span", attrib={"class": "artist"})
+ artist = etree.SubElement(a_main, "span", attrib={"class": "artist"})
artist.text = song.artist()
+
+ # Add onclick to li for backward compatibility with existing CSS/JS
+ li.attrib['onclick'] = "if(event.target.tagName.toLowerCase() !== 'a' && event.target.tagName.toLowerCase() !== 'button' && !event.target.closest('a') && !event.target.closest('button')) { location.href='"+song_html+"'; }"
- # List of songbooks
- ul = tree.getroot().find(".//{http://www.w3.org/1999/xhtml}ul[@id='songbooks']")
+ # List of songbooks - try both HTML and XHTML namespaces
+ ul = tree.getroot().find(".//ul[@id='songbooks']")
+ if ul is None:
+ ul = tree.getroot().find(".//{http://www.w3.org/1999/xhtml}ul[@id='songbooks']")
for songbook in sb.songbooks():
if not songbook.hidden():
li = etree.SubElement(ul, "li", attrib={"id": songbook.id()})
@@ -81,8 +174,13 @@ def create_index_xhtml(list_of_songs_meta, target_dir):
a_a5pdf.text = "PDF (a5)"
a_a4pdf = etree.SubElement(ul, "a", attrib={"class": "pdf", "href": os.path.join("songs_tex", songbook.id()+"_a4.pdf")})
a_a4pdf.text = "PDF (a4)"
+ # Update html lang attribute
+ html_root = tree.getroot()
+ if html_root.tag == 'html' or html_root.tag.endswith('}html'):
+ html_root.attrib['lang'] = 'pl-PL'
+
et = etree.ElementTree(tree.getroot())
- et.write(out_path, pretty_print=True, method='xml', encoding='utf-8', xml_declaration=True)
+ et.write(out_path, pretty_print=True, method='html', encoding='utf-8')
def create_sitemap_xml(list_of_songs_meta, target_dir):
@@ -95,7 +193,7 @@ def create_sitemap_xml(list_of_songs_meta, target_dir):
# Add HTML version of the song
url = etree.SubElement(root, "url")
loc = etree.SubElement(url, "loc")
- loc.text = os.path.join(base, "songs_html", song.base_file_name() + ".xhtml")
+ loc.text = os.path.join(base, "songs_html", song.base_file_name() + ".html")
lastmod = etree.SubElement(url, "lastmod")
lastmod.text = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S+00:00")
changefreq = etree.SubElement(url, "changefreq")
@@ -137,7 +235,7 @@ def main():
logging.info(f"Generating HTML index in {target_dir} from songbook spec {songbook_file}, song count: {len(songbook.list_of_songs())}")
- create_index_xhtml(songbook.list_of_songs(), target_dir)
+ create_index_html(songbook.list_of_songs(), target_dir)
create_sitemap_xml(songbook.list_of_songs(), target_dir)
index_js_path =os.path.join(target_dir, "index.js")
diff --git a/src/html/kindle_html_converter.py b/src/html/kindle_html_converter.py
index 6a039cb5..984626c0 100644
--- a/src/html/kindle_html_converter.py
+++ b/src/html/kindle_html_converter.py
@@ -6,6 +6,10 @@
from .standard_html_converter import StandardHtmlConverter
class KindleHtmlConverter(SongConverter):
+ def extension(self):
+ """Return 'xhtml' for Kindle output"""
+ return 'xhtml'
+
def _add_chunk(self, chunk, parent, position):
"""class chunk -> html span chunk"""
span_chunk = etree.SubElement(parent, "span", attrib={"class": "chunk"})
@@ -158,6 +162,13 @@ def _add_blocks(self, song, parent):
span_content = etree.SubElement(div, "span", attrib={"class": "comment"})
span_content.text = song.comment
+ # Add hidden plain lyrics for Kindle indexing
+ # display: none text is still indexed for search in many Kindle versions
+ plain_lyrics = song.extract_plain_lyrics()
+ if plain_lyrics:
+ div_hidden = etree.SubElement(body_song, "div", attrib={"style": "display: none;"})
+ div_hidden.text = plain_lyrics
+
def xml2html(self, src_xml_path, path_out, song_suffix=None, song_prefix=None, song_head=None, substitions={}):
xhtml_namespace = "http://www.w3.org/1999/xhtml"
diff --git a/src/html/song_converter.py b/src/html/song_converter.py
index 9333ccdf..0ef933a5 100644
--- a/src/html/song_converter.py
+++ b/src/html/song_converter.py
@@ -5,3 +5,8 @@ class SongConverter(ABC):
def xml2html(self, src_xml_path, path_out, song_suffix=None, song_prefix=None, song_head=None, substitions={}):
"""Convert song from XML to HTML format"""
pass
+
+ @abstractmethod
+ def extension(self):
+ """Return the file extension for this converter: 'html' or 'xhtml'"""
+ pass
diff --git a/src/html/song_utils.py b/src/html/song_utils.py
index f07f2a59..10f1bf38 100644
--- a/src/html/song_utils.py
+++ b/src/html/song_utils.py
@@ -17,14 +17,14 @@ def create_all_songs_html_from_dir(converter, path_in, path_out, song_suffix=[])
return create_all_songs_html(converter, create_list_of_songs(path_in), path_out, song_suffix)
def create_all_songs_html(converter, list_of_songs, path_out, song_suffix=[], song_prefix=[], song_head=[], substitions={}):
- if not os.path.exists(path_out):
- os.mkdir(path_out)
+ # Ensure output directory exists (mkdir -p equivalent)
+ os.makedirs(path_out, exist_ok=True)
for song in list_of_songs:
if not song.is_alias():
relative_path = os.path.relpath(song.plik(), start=os.path.join(os.path.dirname(__file__), "../../songs"))
converter.xml2html(song.plik(),
- os.path.join(path_out, song.base_file_name() + '.xhtml'),
+ os.path.join(path_out, song.base_file_name() + '.' + converter.extension()),
song_suffix=song_suffix,
song_prefix=song_prefix,
song_head=song_head,
diff --git a/src/html/standard_html_converter.py b/src/html/standard_html_converter.py
index 2e41f699..98db7d49 100644
--- a/src/html/standard_html_converter.py
+++ b/src/html/standard_html_converter.py
@@ -9,6 +9,10 @@
class StandardHtmlConverter(SongConverter):
+ def extension(self):
+ """Return 'html' for standard HTML output"""
+ return 'html'
+
def _add_chunk_group(self, chunks, parent, position):
"""Add a group of chunks that share the same position in text"""
# Debug comment showing chunk group content
@@ -22,7 +26,7 @@ def _add_chunk_group(self, chunks, parent, position):
if c.content:
content += c.content
if chords: # If there are any chords
- span_stack = etree.SubElement(parent, "span", attrib={"class": "ch-stack"})
+ span_stack = etree.SubElement(parent, "span", attrib={"class": "ch-stack", "aria-hidden": "true"})
for chord in chords:
span_ch = etree.SubElement(span_stack, "span", attrib={
"class": "ch",
@@ -64,7 +68,7 @@ def _add_lyric(self, row, parent, chords_over_bool):
def _add_chords(self, row, parent, class_name):
"""class chords -> html span ch"""
- span_chords = etree.SubElement(parent, "span", attrib={"class": class_name})
+ span_chords = etree.SubElement(parent, "span", attrib={"class": class_name, "aria-hidden": "true"})
if row.sidechords:
for chunk in row.sidechords.split(" "):
span_ch = etree.SubElement(span_chords, "span", attrib={"class": "ch"})
@@ -128,8 +132,8 @@ def _add_creator(self, creator, describe, parent):
span_content.text = creator
def _add_blocks(self, song, parent):
- body_song = parent.find("body")
"""class song -> html div body # blok z metadanymi o piosence i piosenką"""
+ body_song = parent.find("body")
h1_title = etree.SubElement(body_song, "h1", attrib={"class": "title", "id": "title"})
h1_title.text = song.title
if song.original_title:
@@ -175,43 +179,118 @@ def _add_blocks(self, song, parent):
block_id.text = blockid
self._add_block(block, corpse, b_type, verse_cnt)
- if song.comment:
- div = etree.SubElement(body_song, "div", attrib={"class": "comment"})
- span_content = etree.SubElement(div, "span", attrib={"class": "comment"})
- span_content.text = song.comment
-
+ def _detect_language(self, song, src_xml_path):
+ """Detect language from song metadata or XML file, returning (lang, lang_code) tuple"""
+ lang = "pl-PL"
+ lang_code = "pl"
+
+ # Check multiple possible attribute names for language
+ song_lang = None
+ for attr in ['lang', 'language', 'xml_lang']:
+ if hasattr(song, attr) and getattr(song, attr):
+ song_lang = getattr(song, attr)
+ break
+
+ # If still no lang found, try to read directly from XML file
+ if not song_lang:
+ try:
+ from lxml import etree as xml_etree
+ xml_tree = xml_etree.parse(src_xml_path)
+ xml_root = xml_tree.getroot()
+ song_lang = xml_root.get('lang') or xml_root.get('{http://www.w3.org/XML/1998/namespace}lang')
+ except:
+ pass
+
+ if song_lang:
+ # Map language codes
+ lang_map = {
+ 'pl': ('pl-PL', 'pl'),
+ 'en': ('en-US', 'en'),
+ 'de': ('de-DE', 'de'),
+ 'fr': ('fr-FR', 'fr'),
+ 'es': ('es-ES', 'es'),
+ 'it': ('it-IT', 'it')
+ }
+ mapped = lang_map.get(song_lang.lower(), None)
+ if mapped:
+ lang, lang_code = mapped
+ else:
+ lang_code = song_lang.lower()
+ lang = f"{lang_code}-{lang_code.upper()}"
+
+ return lang, lang_code
def xml2html(self, src_xml_path, path_out, song_suffix=None, song_prefix=None, song_head=None, substitions={}):
- xhtml_namespace = "http://www.w3.org/1999/xhtml"
- epub_namespace = "http://www.idpf.org/2007/ops"
- xhtml = "{%s}" % xhtml_namespace
- nsmap = {None: xhtml_namespace,
- "epub": epub_namespace}
-
song = rsx.parse_song_xml(src_xml_path)
- root_html = etree.Element(xhtml + "html", nsmap=nsmap)
- root_html.attrib[etree.QName("lang")] = "pl-PL"
+
+ # Determine language from song metadata or default to Polish
+ lang, lang_code = self._detect_language(song, src_xml_path)
+
+ root_html = etree.Element("html")
+ root_html.attrib["lang"] = lang
head = etree.SubElement(root_html, "head")
+ etree.SubElement(head, "meta", attrib={"charset": "utf-8"})
+ etree.SubElement(head, "meta", attrib={"name": "viewport", "content": "width=device-width, initial-scale=0.8"})
+
+ # SEO meta tags
+ title_text = song.title
+ if song.artist:
+ title_text = f"{song.title} - {song.artist}"
+
+ # Localized description text
+ desc_text = "Tekst i chwyty piosenki" if lang_code == 'pl' else "Lyrics and chords for"
+ etree.SubElement(head, "meta", attrib={"name": "description", "content": f"{desc_text} {title_text}"})
+ etree.SubElement(head, "meta", attrib={"property": "og:title", "content": title_text})
+ etree.SubElement(head, "meta", attrib={"property": "og:type", "content": "music.song"})
+
+ if song.artist:
+ etree.SubElement(head, "meta", attrib={"name": "author", "content": song.artist})
+
+ # Extract plain lyrics for SEO
+ plain_lyrics = song.extract_plain_lyrics()
+
+ # JSON-LD structured data for rich snippets with lyrics
+ script_ld = etree.SubElement(head, "script", attrib={"type": "application/ld+json"})
+ ld_json = {
+ "@context": "https://schema.org",
+ "@type": "MusicComposition",
+ "name": song.title,
+ "inLanguage": lang_code
+ }
+ if song.composer:
+ ld_json["composer"] = {"@type": "Person", "name": song.composer}
+ if song.text_author:
+ ld_json["lyricist"] = {"@type": "Person", "name": song.text_author}
+ if song.artist:
+ ld_json["author"] = {"@type": "Person", "name": song.artist}
+ if plain_lyrics:
+ ld_json["lyrics"] = {
+ "@type": "CreativeWork",
+ "text": plain_lyrics
+ }
+
+ import json
+ script_ld.text = json.dumps(ld_json, ensure_ascii=False, indent=2)
+
etree.SubElement(head, "link",
attrib={"rel": "stylesheet", "type": "text/css", "href": "CSS/song_common.css", "media": "all"})
etree.SubElement(head, "link",
attrib={"rel": "stylesheet", "type": "text/css", "href": "CSS/song.css", "media": "all"})
- etree.SubElement(head, "meta", attrib={"name": "viewport", "content": "width=device-width, initial-scale=0.8"})
for s in song_head:
head.append(interpret(copy.deepcopy(s), substitions))
- body = etree.SubElement(root_html, "body", attrib={"class": "song", etree.QName("http://www.idpf.org/2007/ops", "type"):"bodymatter"})
+ body = etree.SubElement(root_html, "body", attrib={"class": "song"})
for s in song_prefix:
body.append(interpret(copy.deepcopy(s), substitions))
self._add_blocks(song, root_html)
title = etree.SubElement(head, "title")
- title.text = song.title
+ title.text = title_text
et = etree.ElementTree(root_html)
for s in song_suffix:
body.append(interpret(copy.deepcopy(s), substitions))
- et.write(path_out, doctype='', pretty_print=True, method='xml', encoding='utf-8', xml_declaration=True)
+ et.write(path_out, doctype='', pretty_print=True, method='html', encoding='utf-8')
replace_in_file(path_out, path_out, lambda s: s.replace(u'\u200d', ''))
diff --git a/src/lib/read_song_xml.py b/src/lib/read_song_xml.py
index ffa6f43b..011e769d 100644
--- a/src/lib/read_song_xml.py
+++ b/src/lib/read_song_xml.py
@@ -109,6 +109,20 @@ def __init__(self, title='', text_author='', composer='', artist='', original_ti
self.metre = metre if metre else ''
self.barre = barre if barre else ''
+ def extract_plain_lyrics(self):
+ """Extract plain text lyrics without chords for SEO"""
+ lyrics_lines = []
+ for block in self.blocks:
+ for row in block.rows:
+ if not row.instr: # Skip instrumental rows
+ line_text = ''
+ for chunk in row.chunks:
+ if chunk.content:
+ line_text += chunk.content
+ if line_text.strip():
+ lyrics_lines.append(line_text.strip())
+ return '\n'.join(lyrics_lines)
+
@staticmethod
def parseDOM(root):
# A child of 'lyric' element may either be a text block, a reference to a text block (e.g. to a chorus), or a tablature.