diff --git a/src/epub/create_epub.py b/src/epub/create_epub.py index 7973c45d..58d2d546 100644 --- a/src/epub/create_epub.py +++ b/src/epub/create_epub.py @@ -8,7 +8,10 @@ from zipfile import ZipFile from datetime import datetime -import src.html.create_songs_html as cash +from src.html.kindle_html_converter import KindleHtmlConverter +from src.html import html_converter_utils as converter_utils +from src.html import song_utils + import src.lib.songbook as sb import src.lib.any_index_generator as aig import src.lib.img2cover.img2cover as img2cover @@ -25,7 +28,7 @@ def actual_datetime(): # def name_of_file(song): # return os.path.splitext(os.path.split(song)[1])[0] -CSS_FILES = ["song.css", "common.css", "song_common.css", "index.css"] +CSS_FILES = ["song_kindle.css", "common.css", "song_common.css", "index.css"] def create_content_opf(songbook, list_of_songs_meta, target_dir, pre_files=[], post_files=[]): tmp_path =os.path.join(sb.repo_dir(), "src", "epub", "templates", "content.opf") @@ -220,11 +223,11 @@ def create_toc_xhtml(list_of_songs_meta, target_dir, page_suffix): et = etree.ElementTree(root) et.write(out_path, pretty_print=True, method='xml', encoding='utf-8', xml_declaration=True) - cash.replace_in_file(out_path, out_path, lambda s: s.replace("
  • ","
  • \n\t\t
  • ").replace(">
      ",">\n
        ")) + converter_utils.replace_in_file(out_path, out_path, lambda s: s.replace("
      1. ","
      2. \n\t\t
      3. ").replace(">
          ",">\n
            ")) return files def resolveTemplate(songbook, from_file, to_file): - return cash.replace_in_file(from_file, to_file, + return converter_utils.replace_in_file(from_file, to_file, lambda s: (s.replace(":date:", actual_date()) .replace(":datetime:", actual_datetime()) .replace(":title:", songbook.title()) @@ -290,7 +293,8 @@ def create_full_epub(songbook, target_dir): create_template_epub(songbook, target_dir) path_out = os.path.join(target_dir, "epub", "OEBPS") - cash.create_all_songs_html(los, path_out, list([suffix])) + converter = KindleHtmlConverter() + song_utils.create_all_songs_html(converter, los, path_out, list([suffix])) files = [] files.extend(create_toc_xhtml(los, target_dir, page_suffix = suffix)) diff --git a/src/epub/templates/content.opf b/src/epub/templates/content.opf index 01dca7b3..71a1bb97 100755 --- a/src/epub/templates/content.opf +++ b/src/epub/templates/content.opf @@ -23,7 +23,7 @@ - + diff --git a/src/epub/templates/song_kindle.css b/src/epub/templates/song_kindle.css new file mode 100644 index 00000000..7d72a1a1 --- /dev/null +++ b/src/epub/templates/song_kindle.css @@ -0,0 +1,162 @@ +@import url('common.css'); + +body { + font-family: sans-serif; +} + +div.song_body { + width: max-content; + display: table; +} + +/* Per block settings - Chorus, Verses, Instrumental */ + +div.verse, div.chorus, div.other { + display: contents; +} + +div.chorus div.row span.lyric { + padding-left: 3ex; +} + +div.block_spacer { + display: table-row; + empty-cells: show; + height: 2.5ex; +} + +/* Per row settings*/ + +div.row { + display: table-row; + page-break-inside: avoid; + page-break-after: avoid; +} + +div.row:last-of-type { + page-break-after: auto; +} + +/* Lyrics */ + +span.block_id, span.lyric, span.chords, span.bis_inactive, span.bis_active, span.chords_ins { + display: table-cell; +} + +span.block_id { + padding-top: 1ex; + page-break-inside: avoid; + page-break-after: avoid; +} +/* margin-right: 0.5em;*/ +/*}*/ + +/* As there is no call-span in CSS we allow the instrumental row to overflow */ +span.chords_ins { + max-width: 5em; + padding-left: 1em; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Chords above the text */ + +span.lyric span.chunk { + display: inline-table; + /*border: 1px solid green;*/ + vertical-align: bottom; +} + +span.lyric span.chord { + display: table-row; + /*border: 1px solid orange;*/ + max-height: 0; +} + +span.lyric span.chord span.ch{ + display: table-cell; +} + +span.lyric span.content { + display: table-row; + /*border: 1px solid red;*/ + /*overflow-wrap: normal;*/ + /*white-space: nowrap;*/ + /*hyphens: none;*/ +} + +span.lyric span.content-i { + display: table-cell; + /*border: 1px solid red;*/ + white-space: pre-line; + overflow-wrap: normal; + /*white-space: nowrap;*/ + /*empty-cells: show;*/ + hyphens: none; + height: 2.2ex; +} + +span.ws { + display: inline-block; + width: 0.5ex; + opacity: 0; +} + +div.over_false span.lyric span.ch { + display: none; +} + +/*!* Generic styling*!*/ + +h1 { + text-align: left; +} + +span.lyric span.ch { + font-family: sans-serif; + font-size: xx-small; + font-weight: bolder; +} + +span.ch { + font-family: sans-serif; + font-weight: bolder; +} + +span.chords span.ch { + padding-right: 0.3em; +} + +span.chords { + padding-left: 1em; + padding-right: 1em; + max-width: 10em; + overflow-wrap: break-word; + + /* not sure if needed... commenting for now*/ + /* overflow-wrap: break-word;*/ + /* hyphens: none;*/ + /* white-space: nowrap;*/ +} + +div.row > span.bis_inactive { + padding-right: 0.4em; + empty-cells: show; + width: 2em; +} + +div.row > span.bis_active { + padding-left: 0.4em; + padding-right: 0.3em; + border-right: solid; + empty-cells: show; + text-align: right; + width: 2em; /* fixed length makes it more likely on kindle to finish at the same place */ +} + +div.comment { + margin-top: 1.5em; + font-style: italic; + white-space: pre-wrap; + font-size: 0.7em; +} diff --git a/src/html/__init__.py b/src/html/__init__.py new file mode 100644 index 00000000..80c75f65 --- /dev/null +++ b/src/html/__init__.py @@ -0,0 +1 @@ +# Empty file to mark directory as Python package diff --git a/src/html/create_songs_html.py b/src/html/create_songs_html.py deleted file mode 100644 index a4176ace..00000000 --- a/src/html/create_songs_html.py +++ /dev/null @@ -1,278 +0,0 @@ -# Tworzy piosenki w xhtml -import os -import copy -from lxml import etree -import src.lib.read_song_xml as rsx - - -def _add_chunk(chunk, parent, position): - """class chunk -> html span chunk""" - span_chunk = etree.SubElement(parent, "span", attrib={"class": "chunk"}) - chord = etree.SubElement(span_chunk, "span", attrib={"class": "chord"}) - span_ch = etree.SubElement(chord, "span", attrib={"class": "ch"}) - span_ch.text = chunk.chord - span_content = etree.SubElement(span_chunk, "span", attrib={"class": "content"}) - # Nested spans help with formatting as table. - span_content = etree.SubElement(span_content, "span", attrib={"class": "content-i"}) - if position == 0: - if chunk.content.startswith(' '): - span_content.text = chunk.content - else: - span_content.text = ' ' + chunk.content - else: - if chunk.content=='': - span_content.text = ' ' - else: - span_content.text = chunk.content - - -def _add_lyric(row, parent): - """class lyric -> html span lyric""" - span_lyric = etree.SubElement(parent, "span", attrib={"class": "lyric"}) - for i, chunk in enumerate(row.chunks): - _add_chunk(chunk, span_lyric, i) - - -def _add_chords(row, parent, class_name): - """class chords -> html span ch""" - span_chords = etree.SubElement(parent, "span", attrib={"class": class_name}) - if row.sidechords: - for chunk in row.sidechords.split(" "): - span_ch = etree.SubElement(span_chords, "span", attrib={"class": "ch"}) - span_ch.text = chunk - else: - for chunk in row.chunks: - if len(chunk.chord) > 0: - span_ch = etree.SubElement(span_chords, "span", attrib={"class": "ch"}) - span_ch.text = chunk.chord - - -def _add_bis(row, div_row): - if str(type(row.bis)) == "" and row.bis is True: - span_bis = etree.SubElement(div_row, "span", attrib={"class": "bis_active"}) - span_bis.text = u'\u00a0' - elif str(type(row.bis)) == "": - span_bis = etree.SubElement(div_row, "span", attrib={"class": "bis_active"}) - span_bis.text = 'x' + str(row.bis) - else: - span_unbis = etree.SubElement(div_row, "span", attrib={"class": "bis_inactive"}) - span_unbis.text = u'\u00a0' - - -def _add_row(row, parent): - """class row -> html div row with content""" - if (row.new_chords == 0) or (row.new_chords is False): - chords_over = "over_false" - else: - chords_over = "over_true" - div_row = etree.SubElement(parent, "div", attrib={"class": "row " + chords_over}) - div_row.text = u'\u200d' # Not visible spaces -> forces the row to be generated as a single line. We remove it in post-processing. - _add_chords(row, div_row, "chords") - _add_lyric(row, div_row) - if str(type(row.bis)) == "" and row.bis is True: - span_bis = etree.SubElement(div_row, "span", attrib={"class": "bis_active"}) - span_bis.text = u'\u00a0' - elif str(type(row.bis)) == "": - span_bis = etree.SubElement(div_row, "span", attrib={"class": "bis_active"}) - span_bis.text = 'x' + str(row.bis) - else: - span_unbis = etree.SubElement(div_row, "span", attrib={"class": "bis_inactive"}) - span_unbis.text =u'\u00a0' - -def _add_instrumental_row(row, parent): - """class row instrumental-> html div row with content""" - div_row = etree.SubElement(parent, "div", attrib={"class": "row"}) - if str(type(row.bis)) == "" and row.bis is True: - _add_chords(row, div_row, "chords_ins") - _ = etree.SubElement(div_row, "span", attrib={"class": "lyric"}) - span_bis = etree.SubElement(div_row, "span", attrib={"class": "bis_active"}) - span_bis.text = u'\u00a0' - elif str(type(row.bis)) == "": - _add_chords(row, div_row, "chords_ins") - _ = etree.SubElement(div_row, "span", attrib={"class": "lyric"}) - span_bis = etree.SubElement(div_row, "span", attrib={"class": "bis_active"}) - span_bis.text = 'x' + str(row.bis) - else: - _add_chords(row, div_row, "chords_ins") - _ = etree.SubElement(div_row, "span", attrib={"class": "lyric"}) - span_unbis = etree.SubElement(div_row, "span", attrib={"class": "bis_inactive"}) - span_unbis.text = u'\u00a0' - -def _add_block(block, parent, block_type, verse_cnt): - """class verse -> html div verse/chorus/other with content""" - div_verse = etree.SubElement(parent, "div", attrib={"class": block_type}) - - for ro in block.rows: - if ro.instr: - _add_instrumental_row(ro, div_verse) - else: - _add_row(ro, div_verse) - - -def _add_creator(creator, describe, parent): - """class creator -> html div creator # metadane piosenki""" - div = etree.SubElement(parent, "div", attrib={"class": "creator"}) - span_label = etree.SubElement(div, "span", attrib={"class": "label"}) - span_label.text = describe - span_content = etree.SubElement(div, "span", attrib={"class": "content_creator"}) - span_content.text = creator - -def _add_blocks(song, parent): - body_song = parent.find("body") - """class song -> html div body # blok z metadanymi o piosence i piosenką""" - h1_title = etree.SubElement(body_song, "h1", attrib={"class": "title", "id": "title"}) - h1_title.text = song.title - if song.original_title: - _add_creator(song.original_title, "Tytuł oryginalny: ", body_song) - if song.alias: - _add_creator(song.alias, "Tytuł alternatywny: ", body_song) - if song.text_author: - _add_creator(song.text_author, "Słowa: ", body_song) - if song.translator: - _add_creator(song.translator, "Tłumaczenie: ", body_song) - if song.composer: - _add_creator(song.composer, "Muzyka: ", body_song) - if song.music_source: - _add_creator(song.music_source, "Melodia oparta na: ", body_song) - if song.artist: - _add_creator(song.artist, "Wykonawca: ", body_song) - if song.album: - _add_creator(song.album, "Album: ", body_song) - if song.metre: - _add_creator(song.metre, "Metrum: ", body_song) - if song.barre and int(song.barre) > 0: - _add_creator(song.barre, "Kapodaster: ", body_song) - - corpse = etree.SubElement(body_song, "div", attrib={"class": "song_body", "id": "song_body"}) - verse_cnt = 0 - for block in song.blocks: - if block.block_type.value == 'V': - b_type = "verse" - verse_cnt = verse_cnt + 1 - elif block.block_type.value == 'C': - b_type = "chorus" - else: - b_type = "other" - blockid="" - if b_type == "verse": - blockid=str(verse_cnt) + "." - if b_type == "chorus": - blockid="Ref:" - - - spacer = etree.SubElement(corpse, "div", attrib={"class": "block_spacer"}) - block_id = etree.SubElement(spacer, "span", attrib={"class": "block_id"}) - block_id.text = blockid - _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 substitute_from_dict(text, replacements): - """ - Substitutes placeholders in a string with values from a dictionary. - - Args: - text (str): The input string containing placeholders (e.g., '{{placeholder}}'). - replacements (dict): A dictionary where keys are the placeholders to find - and values are the strings to substitute them with. - - Returns: - str: The string with all placeholders substituted. - """ - for old_string, new_string in replacements.items(): - text = text.replace(old_string, new_string) - return text - - -def interpret(c, substitions): - # recursively replace in c all attributes and texts that contains keys from substitions with provided values. - if isinstance(c, etree._Element): - for attr in c.attrib: - c.attrib[attr] = substitute_from_dict(c.attrib[attr], substitions) - for child in c: - interpret(child, substitions) - if c.text: - c.text = substitute_from_dict(c.text, substitions) - return c - - -def xml2html(src_xml_path, path_out, song_suffix = None, song_prefix = None, song_head = None, substitions={}): # tworzy piosenkę w wersji html - - 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" - head = etree.SubElement(root_html, "head") - 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"}) - for s in song_prefix: - body.append(interpret(copy.deepcopy(s), substitions)) - - _add_blocks(song, root_html) - title = etree.SubElement(head, "title") - title.text = song.title - 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) - - replace_in_file(path_out, path_out, lambda s: s.replace(u'\u200d', '').replace(" ", "_").replace(' ', '_')) - -def replace_in_file(sourceFile, targetFile, f): - with open(sourceFile, 'r') as file: - filedata = file.read() - - # Replace the target string - filedata = f(filedata) - - # Write the file out again - with open(targetFile, 'w') as file: - file.write(filedata) - -def create_list_of_songs(song_set): - """ Dostaje jako argument listę piosenek lub ścieżkę do katalogu i zwraca listę piosenek """ - if str(type(song_set)) == "": - for i in range(len(song_set)): - if song_set[i][-4:] == '.xml': - song_set[i] = song_set[i][0:-4] - return song_set - else: - songs_list = os.listdir(song_set) - for i in range(len(songs_list)): - if songs_list[i][-4:] == '.xml': - songs_list[i] = songs_list[i][0:-4] - return songs_list - - -def create_all_songs_html_from_dir(path_in, path_out, song_suffix=[]): - """Tworzy wszystkie piosenki z listy w formacie html w katalogu path_out""" - - return create_all_songs_html( create_list_of_songs(path_in), path_out, song_suffix) - -def create_all_songs_html(list_of_songs, path_out, song_suffix=[], song_prefix=[], song_head=[], substitions={}): - """Tworzy wszystkie piosenki z listy w formacie html w katalogu path_out""" - - if not os.path.exists(path_out): - os.mkdir(path_out) - - 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")) - xml2html(song.plik(), os.path.join(path_out, song.base_file_name() + '.xhtml'), song_suffix=song_suffix, song_prefix=song_prefix, song_head=song_head, substitions=substitions | {"{{SRC}}": relative_path, "{{BASE_FILENAME}}": song.base_file_name()}) diff --git a/src/html/css/song.css b/src/html/css/song.css index 5e253543..6e1e8b92 100644 --- a/src/html/css/song.css +++ b/src/html/css/song.css @@ -1 +1,141 @@ -@import url('song_common.css'); \ No newline at end of file +.lyric { + display: block; + white-space: normal; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; +} + +.ch-stack { + width: 0px; + bottom: 100%; + height: 2em; + top: -2ex; + display: inline-block; + vertical-align: bottom; + word-break: keep-all; + overflow-wrap: normal; +} + +div.lyric span.ch { + font-weight: bolder; + white-space: nowrap; + padding-left: 0px; + padding-right: 0.3em; + font-size: x-small; +} + +div.lyric span.ch[aria-hidden="true"] { + display: none; +} + +.sidechords { + float: right; + margin-left: 1em; +} + +.sidechords .chord { + margin-left: 0.5em; +} + +.chords, .chords_ins { + display: inline-flex; + font-weight: bold; +} + +.bis { + display: flex; + flex-direction: column; + justify-content: center; +} + +.bis_inactive { + visibility: hidden; +} + +div.block_spacer { + margin-top: 4ex; +} + +div.row { + page-break-inside: avoid; + page-break-after: avoid; +} + +div.row:last-of-type { + page-break-after: auto; +} + +/* TODO: Check instrumental row behavior */ + +div.over_false span.lyric span.ch { + display: none; +} + + +div.comment { + margin-top: 1.5em; + font-style: italic; + white-space: pre-wrap; + font-size: smaller; +} + +.chords span.ch, .chords_ins span.ch { + padding-right: 0.3em; +} + + +/** Grid layout for verse, chorus, other */ + +.verse, .chorus, .other { + display: grid; + grid-template-columns: max-content max-content max-content; +} + +.row { + /*As verse is a grid layout, we use display: contents to allow the grid layout to apply to the children of the row*/ + display: contents; +} + +.chords { + grid-column: 1; + align-items: end; + padding-right: 2em; +} + +.chords_ins { + grid-column: 1 / span 2; + align-items: end; + padding-right: 2em; +} + +.lyric { + grid-column: 2; +} + +.bis, .bis_inactive, .bis_active { + grid-column: 3; +} + +/** Intendation of verses */ + +.verse { + padding-left: 1.5em; +} + +.chorus { + padding-left: 3em; +} + +.other { + padding-left: 1.5em; +} + +/* Bis support */ + +.bis_active { + border-left: solid; + margin-left: 1em; + padding-left: 0.3em; + text-align: right; +} \ No newline at end of file diff --git a/src/html/css/song_common.css b/src/html/css/song_common.css index c25d2745..45cc353d 100644 --- a/src/html/css/song_common.css +++ b/src/html/css/song_common.css @@ -5,166 +5,13 @@ body { font-family: sans-serif; } -div.song_body { - width: max-content; - display: table; -} - -/* Per block settings - Chorus, Verses, Instrumental */ - -div.verse, div.chorus, div.other { - display: contents; -} - -div.chorus div.row span.lyric { - padding-left: 3ex; -} - -div.block_spacer { - display: table-row; - empty-cells: show; - height: 2.5ex; -} - -/* Per row settings*/ - -div.row { - display: table-row; - page-break-inside: avoid; - page-break-after: avoid; -} - -div.row:last-of-type { - page-break-after: auto; -} - -/* Lyrics */ - -span.block_id, span.lyric, span.chords, span.bis_inactive, span.bis_active, span.chords_ins { - display: table-cell; -} - -span.block_id { - padding-top: 1ex; - page-break-inside: avoid; - page-break-after: avoid; -} -/* margin-right: 0.5em;*/ -/*}*/ - -/* As there is no call-span in CSS we allow the instrumental row to overflow */ -span.chords_ins { - max-width: 5em; - padding-left: 1em; - text-overflow: ellipsis; - white-space: nowrap; -} - -/* Chords above the text */ - -span.lyric span.chunk { - display: inline-table; - /*border: 1px solid green;*/ - vertical-align: bottom; -} - -span.lyric span.chord { - display: table-row; - /*border: 1px solid orange;*/ - max-height: 0; -} - -span.lyric span.chord span.ch{ - display: table-cell; -} - -span.lyric span.content { - display: table-row; - /*border: 1px solid red;*/ - /*overflow-wrap: normal;*/ - /*white-space: nowrap;*/ - /*hyphens: none;*/ -} - -span.lyric span.content-i { - display: table-cell; - /*border: 1px solid red;*/ - white-space: pre-line; - overflow-wrap: normal; - /*white-space: nowrap;*/ - /*empty-cells: show;*/ - hyphens: none; - height: 2.2ex; -} - -span.ws { - display: inline-block; - width: 0.5ex; - opacity: 0; -} - -div.over_false span.lyric span.ch { - display: none; -} - -/*!* Generic styling*!*/ - h1 { text-align: left; } -span.lyric span.ch { - font-family: sans-serif; - font-size: xx-small; - font-weight: bolder; -} - -span.ch { - font-family: sans-serif; - font-weight: bolder; -} - -span.chords span.ch { - padding-right: 0.3em; -} - -span.chords { - padding-left: 1em; - padding-right: 1em; - max-width: 10em; - overflow-wrap: break-word; - - /* not sure if needed... commenting for now*/ - /* overflow-wrap: break-word;*/ - /* hyphens: none;*/ - /* white-space: nowrap;*/ -} - span.content_creator { margin-top: 0.0em; font-style: italic; font-weight: bold; font-size: small; -} - -div.row > span.bis_inactive { - padding-right: 0.4em; - empty-cells: show; - width: 2em; -} - -div.row > span.bis_active { - padding-left: 0.4em; - padding-right: 0.3em; - border-right: solid; - empty-cells: show; - text-align: right; - width: 2em; /* fixed length makes it more likely on kindle to finish at the same place */ -} - -div.comment { - margin-top: 1.5em; - font-style: italic; - white-space: pre-wrap; - font-size: 0.7em; -} +} \ No newline at end of file diff --git a/src/html/html_converter_utils.py b/src/html/html_converter_utils.py new file mode 100644 index 00000000..dc5daceb --- /dev/null +++ b/src/html/html_converter_utils.py @@ -0,0 +1,24 @@ +import copy +from lxml import etree + +def substitute_from_dict(text, replacements): + for old_string, new_string in replacements.items(): + text = text.replace(old_string, new_string) + return text + +def interpret(c, substitions): + if isinstance(c, etree._Element): + for attr in c.attrib: + c.attrib[attr] = substitute_from_dict(c.attrib[attr], substitions) + for child in c: + interpret(child, substitions) + if c.text: + c.text = substitute_from_dict(c.text, substitions) + return c + +def replace_in_file(sourceFile, targetFile, f): + with open(sourceFile, 'r') as file: + filedata = file.read() + filedata = f(filedata) + with open(targetFile, 'w') as file: + file.write(filedata) diff --git a/src/html/htmls_generator.py b/src/html/htmls_generator.py index 4f336d01..d310d6e6 100644 --- a/src/html/htmls_generator.py +++ b/src/html/htmls_generator.py @@ -1,8 +1,8 @@ import os import shutil - -import src.html.create_songs_html as cash import sys +from src.html.standard_html_converter import StandardHtmlConverter +from src.html.song_utils import create_all_songs_html import src.lib.songbook as sb from lxml import etree @@ -34,7 +34,14 @@ def main(): _song_head = load_template('_song_head.xhtml') _song_suffix = load_template('_song_suffix.xhtml') - cash.create_all_songs_html(songbook.list_of_songs(), path_songs_html, song_prefix=_song_prefix, song_suffix=_song_suffix, song_head=_song_head) + converter = StandardHtmlConverter() + create_all_songs_html(converter, + songbook.list_of_songs(), + path_songs_html, + song_prefix=_song_prefix, + song_suffix=_song_suffix, + song_head=_song_head) + path_css = os.path.join(path_songs_html, "CSS") os.mkdir(path_css) diff --git a/src/html/kindle_html_converter.py b/src/html/kindle_html_converter.py new file mode 100644 index 00000000..6a039cb5 --- /dev/null +++ b/src/html/kindle_html_converter.py @@ -0,0 +1,197 @@ +from lxml import etree +import copy +import src.lib.read_song_xml as rsx +from .song_converter import SongConverter +from .html_converter_utils import interpret, replace_in_file +from .standard_html_converter import StandardHtmlConverter + +class KindleHtmlConverter(SongConverter): + def _add_chunk(self, chunk, parent, position): + """class chunk -> html span chunk""" + span_chunk = etree.SubElement(parent, "span", attrib={"class": "chunk"}) + chord = etree.SubElement(span_chunk, "span", attrib={"class": "chord"}) + span_ch = etree.SubElement(chord, "span", attrib={"class": "ch"}) + span_ch.text = chunk.chord + span_content = etree.SubElement(span_chunk, "span", attrib={"class": "content"}) + # Nested spans help with formatting as table. + span_content = etree.SubElement(span_content, "span", attrib={"class": "content-i"}) + if position == 0: + if chunk.content.startswith(' '): + span_content.text = chunk.content + else: + span_content.text = ' ' + chunk.content + else: + if chunk.content=='': + span_content.text = ' ' + else: + span_content.text = chunk.content + + + def _add_lyric(self, row, parent): + """class lyric -> html span lyric""" + span_lyric = etree.SubElement(parent, "span", attrib={"class": "lyric"}) + for i, chunk in enumerate(row.chunks): + self._add_chunk(chunk, span_lyric, i) + + + def _add_chords(self, row, parent, class_name): + """class chords -> html span ch""" + span_chords = etree.SubElement(parent, "span", attrib={"class": class_name}) + if row.sidechords: + for chunk in row.sidechords.split(" "): + span_ch = etree.SubElement(span_chords, "span", attrib={"class": "ch"}) + span_ch.text = chunk + else: + for chunk in row.chunks: + if len(chunk.chord) > 0: + span_ch = etree.SubElement(span_chords, "span", attrib={"class": "ch"}) + span_ch.text = chunk.chord + + def _add_row(self, row, parent): + """class row -> html div row with content""" + if (row.new_chords == 0) or (row.new_chords is False): + chords_over = "over_false" + else: + chords_over = "over_true" + div_row = etree.SubElement(parent, "div", attrib={"class": "row " + chords_over}) + div_row.text = u'\u200d' # Not visible spaces -> forces the row to be generated as a single line. We remove it in post-processing. + self._add_chords(row, div_row, "chords") + self._add_lyric(row, div_row) + if str(type(row.bis)) == "" and row.bis is True: + span_bis = etree.SubElement(div_row, "span", attrib={"class": "bis_active"}) + span_bis.text = u'\u00a0' + elif str(type(row.bis)) == "": + span_bis = etree.SubElement(div_row, "span", attrib={"class": "bis_active"}) + span_bis.text = 'x' + str(row.bis) + else: + span_unbis = etree.SubElement(div_row, "span", attrib={"class": "bis_inactive"}) + span_unbis.text =u'\u00a0' + + def _add_instrumental_row(self, row, parent): + """class row instrumental-> html div row with content""" + div_row = etree.SubElement(parent, "div", attrib={"class": "row"}) + if str(type(row.bis)) == "" and row.bis is True: + self._add_chords(row, div_row, "chords_ins") + _ = etree.SubElement(div_row, "span", attrib={"class": "lyric"}) + span_bis = etree.SubElement(div_row, "span", attrib={"class": "bis_active"}) + span_bis.text = u'\u00a0' + elif str(type(row.bis)) == "": + self._add_chords(row, div_row, "chords_ins") + _ = etree.SubElement(div_row, "span", attrib={"class": "lyric"}) + span_bis = etree.SubElement(div_row, "span", attrib={"class": "bis_active"}) + span_bis.text = 'x' + str(row.bis) + else: + self._add_chords(row, div_row, "chords_ins") + _ = etree.SubElement(div_row, "span", attrib={"class": "lyric"}) + span_unbis = etree.SubElement(div_row, "span", attrib={"class": "bis_inactive"}) + span_unbis.text = u'\u00a0' + + def _add_block(self, block, parent, block_type, verse_cnt): + """class verse -> html div verse/chorus/other with content""" + div_verse = etree.SubElement(parent, "div", attrib={"class": block_type}) + + for ro in block.rows: + if ro.instr: + self._add_instrumental_row(ro, div_verse) + else: + self._add_row(ro, div_verse) + + + def _add_creator(self, creator, describe, parent): + """class creator -> html div creator # metadane piosenki""" + div = etree.SubElement(parent, "div", attrib={"class": "creator"}) + span_label = etree.SubElement(div, "span", attrib={"class": "label"}) + span_label.text = describe + span_content = etree.SubElement(div, "span", attrib={"class": "content_creator"}) + 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ą""" + h1_title = etree.SubElement(body_song, "h1", attrib={"class": "title", "id": "title"}) + h1_title.text = song.title + if song.original_title: + self._add_creator(song.original_title, "Tytuł oryginalny: ", body_song) + if song.alias: + self._add_creator(song.alias, "Tytuł alternatywny: ", body_song) + if song.text_author: + self._add_creator(song.text_author, "Słowa: ", body_song) + if song.translator: + self._add_creator(song.translator, "Tłumaczenie: ", body_song) + if song.composer: + self._add_creator(song.composer, "Muzyka: ", body_song) + if song.music_source: + self._add_creator(song.music_source, "Melodia oparta na: ", body_song) + if song.artist: + self._add_creator(song.artist, "Wykonawca: ", body_song) + if song.album: + self._add_creator(song.album, "Album: ", body_song) + if song.metre: + self._add_creator(song.metre, "Metrum: ", body_song) + if song.barre and int(song.barre) > 0: + self._add_creator(song.barre, "Kapodaster: ", body_song) + + corpse = etree.SubElement(body_song, "div", attrib={"class": "song_body", "id": "song_body"}) + verse_cnt = 0 + for block in song.blocks: + if block.block_type.value == 'V': + b_type = "verse" + verse_cnt = verse_cnt + 1 + elif block.block_type.value == 'C': + b_type = "chorus" + else: + b_type = "other" + blockid="" + if b_type == "verse": + blockid=str(verse_cnt) + "." + if b_type == "chorus": + blockid="Ref:" + + + spacer = etree.SubElement(corpse, "div", attrib={"class": "block_spacer"}) + block_id = etree.SubElement(spacer, "span", attrib={"class": "block_id"}) + 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 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" + head = etree.SubElement(root_html, "head") + 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_kindle.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"}) + 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 + 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) + + replace_in_file(path_out, path_out, lambda s: s.replace(u'\u200d', '').replace( + " ", "_").replace( + ' ', '_')) \ No newline at end of file diff --git a/src/html/song_converter.py b/src/html/song_converter.py new file mode 100644 index 00000000..9333ccdf --- /dev/null +++ b/src/html/song_converter.py @@ -0,0 +1,7 @@ +from abc import ABC, abstractmethod + +class SongConverter(ABC): + @abstractmethod + 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 diff --git a/src/html/song_utils.py b/src/html/song_utils.py new file mode 100644 index 00000000..f07f2a59 --- /dev/null +++ b/src/html/song_utils.py @@ -0,0 +1,34 @@ +import os + +def create_list_of_songs(song_set): + if str(type(song_set)) == "": + for i in range(len(song_set)): + if song_set[i][-4:] == '.xml': + song_set[i] = song_set[i][0:-4] + return song_set + else: + songs_list = os.listdir(song_set) + for i in range(len(songs_list)): + if songs_list[i][-4:] == '.xml': + songs_list[i] = songs_list[i][0:-4] + return songs_list + +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) + + 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'), + song_suffix=song_suffix, + song_prefix=song_prefix, + song_head=song_head, + substitions=substitions | { + "{{SRC}}": relative_path, + "{{BASE_FILENAME}}": song.base_file_name() + }) diff --git a/src/html/standard_html_converter.py b/src/html/standard_html_converter.py new file mode 100644 index 00000000..64d4a014 --- /dev/null +++ b/src/html/standard_html_converter.py @@ -0,0 +1,217 @@ +# Standard HTML song converter +import os +import copy +from lxml import etree +import src.lib.read_song_xml as rsx +from .song_converter import SongConverter +from .song_utils import create_list_of_songs, create_all_songs_html, create_all_songs_html_from_dir +from .html_converter_utils import interpret, replace_in_file + + +class StandardHtmlConverter(SongConverter): + 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 + parent.append(etree.Comment(', '.join(str(c).replace(u'-', '_') for c in chunks))) + + # Callect all chords and create chord stack + chords = [c.chord for c in chunks if c.chord] + # concatenate all content + content = '' + for c in chunks: + if c.content: + content += c.content + if chords: # If there are any chords + span_stack = etree.SubElement(parent, "span", attrib={"class": "ch-stack"}) + for chord in chords: + span_ch = etree.SubElement(span_stack, "span", attrib={ + "class": "ch", + "role": "note", + "aria-label": f"chord {chord}" + }) + span_ch.text = chord + span_stack.tail = content if content else '' + else: + # if there is previous element, append to tail + last_child = parent[-1] if len(parent) > 0 else None + if last_child is not None: + last_child.tail = (last_child.tail or '') + content + else: + parent.text = (parent.text or '') + content + + def _add_lyric(self, row, parent, chords_over_bool): + """Add line grouping chunks by position""" + div_line = etree.SubElement(parent, "div", attrib={"class": "lyric"}) + + if chords_over_bool: + i = 0 + chunks = row.chunks + while i < len(chunks): + group = [] + while i < len(chunks) and not chunks[i].content: + group.append(chunks[i]) + i += 1 + if i < len(chunks): + group.append(chunks[i]) + i += 1 + self._add_chunk_group(group, div_line, len(div_line)) + else: + t = '' + for chunk in row.chunks: + t += chunk.content + div_line.text = t + + + def _add_chords(self, row, parent, class_name): + """class chords -> html span ch""" + span_chords = etree.SubElement(parent, "span", attrib={"class": class_name}) + if row.sidechords: + for chunk in row.sidechords.split(" "): + span_ch = etree.SubElement(span_chords, "span", attrib={"class": "ch"}) + span_ch.text = chunk + else: + for chunk in row.chunks: + if len(chunk.chord) > 0: + span_ch = etree.SubElement(span_chords, "span", attrib={"class": "ch"}) + span_ch.text = chunk.chord + + def _add_instrumental_row(self, row, parent): + """class row instrumental-> html div row with content""" + div_row = etree.SubElement(parent, "div", attrib={"class": "row"}) + if str(type(row.bis)) == "" and row.bis is True: + self._add_chords(row, div_row, "chords_ins") + span_bis = etree.SubElement(div_row, "span", attrib={"class": "bis_active"}) + span_bis.text = u'\u00a0' + elif str(type(row.bis)) == "": + self._add_chords(row, div_row, "chords_ins") + span_bis = etree.SubElement(div_row, "span", attrib={"class": "bis_active"}) + span_bis.text = 'x' + str(row.bis) + else: + self._add_chords(row, div_row, "chords_ins") + span_unbis = etree.SubElement(div_row, "span", attrib={"class": "bis_inactive"}) + span_unbis.text = u'\u00a0' + + def _add_row(self, row, parent): + """class row -> html div row with content""" + chords_over_bool = not ((row.new_chords == 0) or (row.new_chords is False)) + chords_over = "over_true" if chords_over_bool else "over_false" + div_row = etree.SubElement(parent, "div", attrib={"class": "row " + chords_over}) + div_row.text = u'\u200d' # Not visible spaces -> forces the row to be generated as a single line. We remove it in post-processing. + self._add_chords(row, div_row, "chords") + self._add_lyric(row, div_row, chords_over_bool) + if str(type(row.bis)) == "" and row.bis is True: + span_bis = etree.SubElement(div_row, "span", attrib={"class": "bis_active"}) + span_bis.text = u'\u00a0' + elif str(type(row.bis)) == "": + span_bis = etree.SubElement(div_row, "span", attrib={"class": "bis_active"}) + span_bis.text = 'x' + str(row.bis) + else: + span_unbis = etree.SubElement(div_row, "span", attrib={"class": "bis_inactive"}) + span_unbis.text =u'\u00a0' + + def _add_block(self, block, parent, block_type, verse_cnt): + """Add verse with grid layout structure""" + div_verse = etree.SubElement(parent, "div", attrib={"class": block_type}) + + for ro in block.rows: + if ro.instr: + self._add_instrumental_row(ro, div_verse) + else: + self._add_row(ro, div_verse) + + def _add_creator(self, creator, describe, parent): + """class creator -> html div creator # metadane piosenki""" + div = etree.SubElement(parent, "div", attrib={"class": "creator"}) + span_label = etree.SubElement(div, "span", attrib={"class": "label"}) + span_label.text = describe + span_content = etree.SubElement(div, "span", attrib={"class": "content_creator"}) + 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ą""" + h1_title = etree.SubElement(body_song, "h1", attrib={"class": "title", "id": "title"}) + h1_title.text = song.title + if song.original_title: + self._add_creator(song.original_title, "Tytuł oryginalny: ", body_song) + if song.alias: + self._add_creator(song.alias, "Tytuł alternatywny: ", body_song) + if song.text_author: + self._add_creator(song.text_author, "Słowa: ", body_song) + if song.translator: + self._add_creator(song.translator, "Tłumaczenie: ", body_song) + if song.composer: + self._add_creator(song.composer, "Muzyka: ", body_song) + if song.music_source: + self._add_creator(song.music_source, "Melodia oparta na: ", body_song) + if song.artist: + self._add_creator(song.artist, "Wykonawca: ", body_song) + if song.album: + self._add_creator(song.album, "Album: ", body_song) + if song.metre: + self._add_creator(song.metre, "Metrum: ", body_song) + if song.barre and int(song.barre) > 0: + self._add_creator(song.barre, "Kapodaster: ", body_song) + + corpse = etree.SubElement(body_song, "div", attrib={"class": "song_body", "id": "song_body"}) + verse_cnt = 0 + for block in song.blocks: + if block.block_type.value == 'V': + b_type = "verse" + verse_cnt = verse_cnt + 1 + elif block.block_type.value == 'C': + b_type = "chorus" + else: + b_type = "other" + blockid="" + if b_type == "verse": + blockid=str(verse_cnt) + "." + if b_type == "chorus": + blockid="Ref:" + + + spacer = etree.SubElement(corpse, "div", attrib={"class": "block_spacer"}) + block_id = etree.SubElement(spacer, "span", attrib={"class": "block_id"}) + 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 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" + head = etree.SubElement(root_html, "head") + 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"}) + 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 + 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) + + 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 bff97f0f..ffa6f43b 100644 --- a/src/lib/read_song_xml.py +++ b/src/lib/read_song_xml.py @@ -11,6 +11,9 @@ def __init__(self, chord='', content=None): else: self.content = content.strip('\t\n') + def __str__(self): + return f"[chord='{self.chord}' content='{self.content}']" + class RowType(Enum): FIRST = 'F'