diff --git a/openlp/core/lib/formattingtags.py b/openlp/core/lib/formattingtags.py index ae9d5c1cf..ea5547f27 100644 --- a/openlp/core/lib/formattingtags.py +++ b/openlp/core/lib/formattingtags.py @@ -27,9 +27,13 @@ """ Provide HTML Tag management and Formatting Tag access class """ +import cPickle + +from PyQt4 import QtCore from openlp.core.lib import translate + class FormattingTags(object): """ Static Class to HTML Tags to be access around the code the list is managed @@ -42,6 +46,8 @@ class FormattingTags(object): """ Provide access to the html_expands list. """ + # Load user defined tags otherwise user defined tags are not present. + FormattingTags.load_tags() return FormattingTags.html_expands @staticmethod @@ -49,6 +55,8 @@ class FormattingTags(object): """ Resets the html_expands list. """ + temporary_tags = [tag for tag in FormattingTags.html_expands + if tag.get(u'temporary')] FormattingTags.html_expands = [] base_tags = [] # Append the base tags. @@ -56,75 +64,160 @@ class FormattingTags(object): base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Red'), u'start tag': u'{r}', u'start html': u'', - u'end tag': u'{/r}', u'end html': u'', u'protected': True}) - base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Black'), + u'end tag': u'{/r}', u'end html': u'', u'protected': True, + u'temporary': False}) + base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Black'), u'start tag': u'{b}', u'start html': u'', - u'end tag': u'{/b}', u'end html': u'', u'protected': True}) + u'end tag': u'{/b}', u'end html': u'', u'protected': True, + u'temporary': False}) base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Blue'), u'start tag': u'{bl}', u'start html': u'', - u'end tag': u'{/bl}', u'end html': u'', u'protected': True}) + u'end tag': u'{/bl}', u'end html': u'', u'protected': True, + u'temporary': False}) base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Yellow'), u'start tag': u'{y}', u'start html': u'', - u'end tag': u'{/y}', u'end html': u'', u'protected': True}) + u'end tag': u'{/y}', u'end html': u'', u'protected': True, + u'temporary': False}) base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Green'), u'start tag': u'{g}', u'start html': u'', - u'end tag': u'{/g}', u'end html': u'', u'protected': True}) + u'end tag': u'{/g}', u'end html': u'', u'protected': True, + u'temporary': False}) base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Pink'), u'start tag': u'{pk}', u'start html': u'', - u'end tag': u'{/pk}', u'end html': u'', u'protected': True}) + u'end tag': u'{/pk}', u'end html': u'', u'protected': True, + u'temporary': False}) base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Orange'), u'start tag': u'{o}', u'start html': u'', - u'end tag': u'{/o}', u'end html': u'', u'protected': True}) + u'end tag': u'{/o}', u'end html': u'', u'protected': True, + u'temporary': False}) base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Purple'), u'start tag': u'{pp}', u'start html': u'', - u'end tag': u'{/pp}', u'end html': u'', u'protected': True}) + u'end tag': u'{/pp}', u'end html': u'', u'protected': True, + u'temporary': False}) base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'White'), u'start tag': u'{w}', u'start html': u'', - u'end tag': u'{/w}', u'end html': u'', u'protected': True}) + u'end tag': u'{/w}', u'end html': u'', u'protected': True, + u'temporary': False}) base_tags.append({ u'desc': translate('OpenLP.FormattingTags', 'Superscript'), u'start tag': u'{su}', u'start html': u'', - u'end tag': u'{/su}', u'end html': u'', u'protected': True}) + u'end tag': u'{/su}', u'end html': u'', u'protected': True, + u'temporary': False}) base_tags.append({ u'desc': translate('OpenLP.FormattingTags', 'Subscript'), u'start tag': u'{sb}', u'start html': u'', - u'end tag': u'{/sb}', u'end html': u'', u'protected': True}) + u'end tag': u'{/sb}', u'end html': u'', u'protected': True, + u'temporary': False}) base_tags.append({ u'desc': translate('OpenLP.FormattingTags', 'Paragraph'), u'start tag': u'{p}', u'start html': u'

', u'end tag': u'{/p}', - u'end html': u'

', u'protected': True}) + u'end html': u'

', u'protected': True, + u'temporary': False}) base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Bold'), u'start tag': u'{st}', u'start html': u'', u'end tag': u'{/st}', u'end html': u'', - u'protected': True}) + u'protected': True, u'temporary': False}) base_tags.append({ u'desc': translate('OpenLP.FormattingTags', 'Italics'), u'start tag': u'{it}', u'start html': u'', u'end tag': u'{/it}', - u'end html': u'', u'protected': True}) + u'end html': u'', u'protected': True, u'temporary': False}) base_tags.append({ u'desc': translate('OpenLP.FormattingTags', 'Underline'), u'start tag': u'{u}', u'start html': u'', - u'end tag': u'{/u}', u'end html': u'', u'protected': True}) + u'end tag': u'{/u}', u'end html': u'', u'protected': True, + u'temporary': False}) base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Break'), u'start tag': u'{br}', u'start html': u'
', u'end tag': u'', - u'end html': u'', u'protected': True}) + u'end html': u'', u'protected': True, u'temporary': False}) FormattingTags.add_html_tags(base_tags) + FormattingTags.add_html_tags(temporary_tags) @staticmethod - def add_html_tags(tags): + def save_html_tags(): """ - Add a list of tags to the list + Saves all formatting tags except protected ones. + """ + tags = [] + for tag in FormattingTags.html_expands: + if not tag[u'protected'] and not tag.get(u'temporary'): + tags.append(tag) + # Remove key 'temporary' from tags. It is not needed to be saved. + for tag in tags: + if u'temporary' in tag: + del tag[u'temporary'] + # Formatting Tags were also known as display tags. + QtCore.QSettings().setValue(u'displayTags/html_tags', + QtCore.QVariant(cPickle.dumps(tags) if tags else u'')) + + @staticmethod + def load_tags(): + """ + Load the Tags from store so can be used in the system or used to + update the display. If Cancel was selected this is needed to reset the + dsiplay to the correct version. + """ + # Initial Load of the Tags + FormattingTags.reset_html_tags() + # Formatting Tags were also known as display tags. + user_expands = QtCore.QSettings().value(u'displayTags/html_tags', + QtCore.QVariant(u'')).toString() + # cPickle only accepts str not unicode strings + user_expands_string = str(unicode(user_expands).encode(u'utf8')) + if user_expands_string: + user_tags = cPickle.loads(user_expands_string) + # If we have some user ones added them as well + FormattingTags.add_html_tags(user_tags) + + @staticmethod + def add_html_tags(tags, save=False): + """ + Add a list of tags to the list. + + ``tags`` + The list with tags to add. + + ``save`` + Defaults to ``False``. If set to ``True`` the given ``tags`` are + saved to the config. + + Each **tag** has to be a ``dict`` and should have the following keys: + + * desc + The formatting tag's description, e. g. **Red** + + * start tag + The start tag, e. g. ``{r}`` + + * end tag + The end tag, e. g. ``{/r}`` + + * start html + The start html tag. For instance ```` + + * end html + The end html tag. For example ```` + + * protected + A boolean stating whether this is a build-in tag or not. Should be + ``True`` in most cases. + + * temporary + A temporary tag will not be saved, but is also considered when + displaying text containing the tag. It has to be a ``boolean``. """ FormattingTags.html_expands.extend(tags) + if save: + FormattingTags.save_html_tags() @staticmethod def remove_html_tag(tag_id): diff --git a/openlp/core/ui/formattingtagform.py b/openlp/core/ui/formattingtagform.py index 2a8625b1a..e7435e5b7 100644 --- a/openlp/core/ui/formattingtagform.py +++ b/openlp/core/ui/formattingtagform.py @@ -30,14 +30,13 @@ protected and included each time loaded. Custom tags can be defined and saved. The Custom Tag arrays are saved in a pickle so QSettings works on them. Base Tags cannot be changed. """ -import cPickle - from PyQt4 import QtCore, QtGui from openlp.core.lib import translate, FormattingTags from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui.formattingtagdialog import Ui_FormattingTagDialog + class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): """ The :class:`FormattingTagForm` manages the settings tab . @@ -48,7 +47,6 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): """ QtGui.QDialog.__init__(self, parent) self.setupUi(self) - self._loadFormattingTags() QtCore.QObject.connect(self.tagTableWidget, QtCore.SIGNAL(u'clicked(QModelIndex)'), self.onRowSelected) QtCore.QObject.connect(self.newPushButton, @@ -59,41 +57,24 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): QtCore.SIGNAL(u'pressed()'), self.onDeletePushed) QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(u'rejected()'), self.close) + # Forces reloading of tags from openlp configuration. + FormattingTags.load_tags() def exec_(self): """ Load Display and set field state. """ # Create initial copy from master - self._loadFormattingTags() self._resetTable() self.selected = -1 return QtGui.QDialog.exec_(self) - def _loadFormattingTags(self): - """ - Load the Tags from store so can be used in the system or used to - update the display. If Cancel was selected this is needed to reset the - dsiplay to the correct version. - """ - # Initial Load of the Tags - FormattingTags.reset_html_tags() - # Formatting Tags were also known as display tags. - user_expands = QtCore.QSettings().value(u'displayTags/html_tags', - QtCore.QVariant(u'')).toString() - # cPickle only accepts str not unicode strings - user_expands_string = str(unicode(user_expands).encode(u'utf8')) - if user_expands_string: - user_tags = cPickle.loads(user_expands_string) - # If we have some user ones added them as well - FormattingTags.add_html_tags(user_tags) - def onRowSelected(self): """ Table Row selected so display items and set field state. """ row = self.tagTableWidget.currentRow() - html = FormattingTags.get_html_tags()[row] + html = FormattingTags.html_expands[row] self.selected = row self.descriptionLineEdit.setText(html[u'desc']) self.tagLineEdit.setText(self._strip(html[u'start tag'])) @@ -118,7 +99,7 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): """ Add a new tag to list only if it is not a duplicate. """ - for html in FormattingTags.get_html_tags(): + for html in FormattingTags.html_expands: if self._strip(html[u'start tag']) == u'n': critical_error_message_box( translate('OpenLP.FormattingTagForm', 'Update Error'), @@ -132,7 +113,8 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): u'start html': translate('OpenLP.FormattingTagForm', ''), u'end tag': u'{/n}', u'end html': translate('OpenLP.FormattingTagForm', ''), - u'protected': False + u'protected': False, + u'temporary': False } FormattingTags.add_html_tags([tag]) self._resetTable() @@ -149,13 +131,13 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): FormattingTags.remove_html_tag(self.selected) self.selected = -1 self._resetTable() - self._saveTable() + FormattingTags.save_html_tags() def onSavedPushed(self): """ Update Custom Tag details if not duplicate and save the data. """ - html_expands = FormattingTags.get_html_tags() + html_expands = FormattingTags.html_expands if self.selected != -1: html = html_expands[self.selected] tag = unicode(self.tagLineEdit.text()) @@ -172,21 +154,11 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): html[u'end html'] = unicode(self.endTagLineEdit.text()) html[u'start tag'] = u'{%s}' % tag html[u'end tag'] = u'{/%s}' % tag + # Keep temporary tags when the user changes one. + html[u'temporary'] = False self.selected = -1 self._resetTable() - self._saveTable() - - def _saveTable(self): - """ - Saves all formatting tags except protected ones. - """ - tags = [] - for tag in FormattingTags.get_html_tags(): - if not tag[u'protected']: - tags.append(tag) - # Formatting Tags were also known as display tags. - QtCore.QSettings().setValue(u'displayTags/html_tags', - QtCore.QVariant(cPickle.dumps(tags) if tags else u'')) + FormattingTags.save_html_tags() def _resetTable(self): """ @@ -197,9 +169,8 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): self.newPushButton.setEnabled(True) self.savePushButton.setEnabled(False) self.deletePushButton.setEnabled(False) - for linenumber, html in enumerate(FormattingTags.get_html_tags()): - self.tagTableWidget.setRowCount( - self.tagTableWidget.rowCount() + 1) + for linenumber, html in enumerate(FormattingTags.html_expands): + self.tagTableWidget.setRowCount(self.tagTableWidget.rowCount() + 1) self.tagTableWidget.setItem(linenumber, 0, QtGui.QTableWidgetItem(html[u'desc'])) self.tagTableWidget.setItem(linenumber, 1, @@ -208,6 +179,9 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): QtGui.QTableWidgetItem(html[u'start html'])) self.tagTableWidget.setItem(linenumber, 3, QtGui.QTableWidgetItem(html[u'end html'])) + # Permanent (persistent) tags do not have this key. + if u'temporary' not in html: + html[u'temporary'] = False self.tagTableWidget.resizeRowsToContents() self.descriptionLineEdit.setText(u'') self.tagLineEdit.setText(u'') diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 3612bb002..fbf185474 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -127,6 +127,9 @@ class AppLocation(object): CacheDir = 6 LanguageDir = 7 + # Base path where data/config/cache dir is located + BaseDir = None + @staticmethod def get_directory(dir_type=1): """ @@ -152,6 +155,8 @@ class AppLocation(object): os.path.abspath(os.path.split(sys.argv[0])[0]), _get_os_dir_path(dir_type)) return os.path.join(app_path, u'i18n') + elif dir_type == AppLocation.DataDir and AppLocation.BaseDir: + return os.path.join(AppLocation.BaseDir, 'data') else: return _get_os_dir_path(dir_type) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index f20095500..950eff870 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -562,6 +562,9 @@ class SongMediaItem(MediaManagerItem): self._updateBackgroundAudio(song, item) editId = song.id self.onSearchTextButtonClick() + else: + # Make sure we temporary import formatting tags. + self.openLyrics.xml_to_song(item.xml_version, True) # Update service with correct song id. if editId: Receiver.send_message(u'service_item_update', diff --git a/openlp/plugins/songs/lib/xml.py b/openlp/plugins/songs/lib/xml.py index 9da7a0a65..5b36a7cb9 100644 --- a/openlp/plugins/songs/lib/xml.py +++ b/openlp/plugins/songs/lib/xml.py @@ -61,19 +61,21 @@ The XML of an `OpenLyrics `_ song looks like this:: """ -import datetime import logging import re from lxml import etree, objectify +from openlp.core.lib import FormattingTags from openlp.plugins.songs.lib import clean_song, VerseType from openlp.plugins.songs.lib.db import Author, Book, Song, Topic from openlp.core.utils import get_application_version log = logging.getLogger(__name__) -CHORD_REGEX = re.compile(u'') +NAMESPACE = u'http://openlyrics.info/namespace/2009/song' +NSMAP = '{' + NAMESPACE + '}' + '%s' + class SongXML(object): """ @@ -173,7 +175,7 @@ class SongXML(object): class OpenLyrics(object): """ - This class represents the converter for OpenLyrics XML (version 0.7) + This class represents the converter for OpenLyrics XML (version 0.8) to/from a song. As OpenLyrics has a rich set of different features, we cannot support them @@ -198,11 +200,15 @@ class OpenLyrics(object): ```` This property is not supported. + ```` + The custom formatting tags are fully supported. + ```` This property is not supported. ```` - The attribute *part* is not supported. + The attribute *part* is not supported. The *break* attribute is + supported. ```` This property is not supported. @@ -227,15 +233,35 @@ class OpenLyrics(object): ```` The attribute *translit* is not supported. Note, the attribute *lang* is - considered, but there is not further functionality implemented yet. + considered, but there is not further functionality implemented yet. The + following verse "types" are supported by OpenLP: + + * v + * c + * b + * p + * i + * e + * o + + The verse "types" stand for *Verse*, *Chorus*, *Bridge*, *Pre-Chorus*, + *Intro*, *Ending* and *Other*. Any numeric value is allowed after the + verse type. The complete verse name in OpenLP always consists of the + verse type and the verse number. If not number is present *1* is + assumed. + OpenLP will merge verses which are split up by appending a letter to the + verse name, such as *v1a*. ```` OpenLP supports this property. """ - IMPLEMENTED_VERSION = u'0.7' + IMPLEMENTED_VERSION = u'0.8' + def __init__(self, manager): self.manager = manager + self.start_tags_regex = re.compile(r'\{(\w+)\}') # {abc} -> abc + self.end_tags_regex = re.compile(r'\{\/(\w+)\}') # {/abc} -> abc def song_to_xml(self, song): """ @@ -244,13 +270,14 @@ class OpenLyrics(object): sxml = SongXML() song_xml = objectify.fromstring(u'') # Append the necessary meta data to the song. - song_xml.set(u'xmlns', u'http://openlyrics.info/namespace/2009/song') + song_xml.set(u'xmlns', NAMESPACE) song_xml.set(u'version', OpenLyrics.IMPLEMENTED_VERSION) application_name = u'OpenLP ' + get_application_version()[u'version'] song_xml.set(u'createdIn', application_name) song_xml.set(u'modifiedIn', application_name) + # "Convert" 2011-08-27 11:49:15 to 2011-08-27T11:49:15. song_xml.set(u'modifiedDate', - datetime.datetime.now().strftime(u'%Y-%m-%dT%H:%M:%S')) + unicode(song.last_modified).replace(u' ', u'T')) properties = etree.SubElement(song_xml, u'properties') titles = etree.SubElement(properties, u'titles') self._add_text_to_element(u'title', titles, song.title) @@ -284,29 +311,40 @@ class OpenLyrics(object): themes = etree.SubElement(properties, u'themes') for topic in song.topics: self._add_text_to_element(u'theme', themes, topic.name) + # Process the formatting tags. + # Have we any tags in song lyrics? + tags_element = None + match = re.search(u'\{/?\w+\}', song.lyrics, re.UNICODE) + if match: + # Reset available tags. + FormattingTags.reset_html_tags() + # Named 'formatting' - 'format' is built-in fuction in Python. + format_ = etree.SubElement(song_xml, u'format') + tags_element = etree.SubElement(format_, u'tags') + tags_element.set(u'application', u'OpenLP') # Process the song's lyrics. lyrics = etree.SubElement(song_xml, u'lyrics') verse_list = sxml.get_verses(song.lyrics) for verse in verse_list: verse_tag = verse[0][u'type'][0].lower() verse_number = verse[0][u'label'] + verse_def = verse_tag + verse_number + verse_element = \ + self._add_text_to_element(u'verse', lyrics, None, verse_def) + if u'lang' in verse[0]: + verse_element.set(u'lang', verse[0][u'lang']) # Create a list with all "virtual" verses. virtual_verses = verse[1].split(u'[---]') for index, virtual_verse in enumerate(virtual_verses): - verse_def = verse_tag + verse_number - # We need "v1a" because we have more than one virtual verse. - if len(virtual_verses) > 1: - verse_def += list(u'abcdefghijklmnopqrstuvwxyz')[index] - element = \ - self._add_text_to_element(u'verse', lyrics, None, verse_def) - if verse[0].has_key(u'lang'): - element.set(u'lang', verse[0][u'lang']) - element = self._add_text_to_element(u'lines', element) - for line in virtual_verse.strip(u'\n').split(u'\n'): - self._add_text_to_element(u'line', element, line) + # Add formatting tags to text + lines_element = self._add_text_with_tags_to_lines(verse_element, + virtual_verse, tags_element) + # Do not add the break attribute to the last lines element. + if index < len(virtual_verses) - 1: + lines_element.set(u'break', u'optional') return self._extract_xml(song_xml) - def xml_to_song(self, xml): + def xml_to_song(self, xml, parse_and_not_save=False): """ Create and save a song from OpenLyrics format xml to the database. Since we also export XML from external sources (e. g. OpenLyrics import), we @@ -314,19 +352,26 @@ class OpenLyrics(object): ``xml`` The XML to parse (unicode). + + ``parse_and_not_save`` + Switch to skip processing the whole song and to prevent storing the + songs to the database. Defaults to ``False``. """ # No xml get out of here. if not xml: return None if xml[:5] == u' 0.7: + self._process_formatting_tags(song_xml, parse_and_not_save) + if parse_and_not_save: + return song = Song() # Values will be set when cleaning the song. song.search_lyrics = u'' @@ -336,7 +381,7 @@ class OpenLyrics(object): self._process_cclinumber(properties, song) self._process_titles(properties, song) # The verse order is processed with the lyrics! - self._process_lyrics(properties, song_xml.lyrics, song) + self._process_lyrics(properties, song_xml, song) self._process_comments(properties, song) self._process_authors(properties, song) self._process_songbooks(properties, song) @@ -355,6 +400,57 @@ class OpenLyrics(object): parent.append(element) return element + def _add_tag_to_formatting(self, tag_name, tags_element): + """ + Add new formatting tag to the element ```` + if the tag is not present yet. + """ + available_tags = FormattingTags.get_html_tags() + start_tag = '{%s}' % tag_name + for t in available_tags: + if t[u'start tag'] == start_tag: + # Create new formatting tag in openlyrics xml. + el = self._add_text_to_element(u'tag', tags_element) + el.set(u'name', tag_name) + el_open = self._add_text_to_element(u'open', el) + el_open.text = etree.CDATA(t[u'start html']) + # Check if formatting tag contains end tag. Some formatting + # tags e.g. {br} has only start tag. If no end tag is present + # element has not to be in OpenLyrics xml. + if t['end tag']: + el_close = self._add_text_to_element(u'close', el) + el_close.text = etree.CDATA(t[u'end html']) + + def _add_text_with_tags_to_lines(self, verse_element, text, tags_element): + """ + Convert text with formatting tags from OpenLP format to OpenLyrics + format and append it to element ````. + """ + start_tags = self.start_tags_regex.findall(text) + end_tags = self.end_tags_regex.findall(text) + # Replace start tags with xml syntax. + for tag in start_tags: + # Tags already converted to xml structure. + xml_tags = tags_element.xpath(u'tag/attribute::name') + # Some formatting tag has only starting part e.g.
. + # Handle this case. + if tag in end_tags: + text = text.replace(u'{%s}' % tag, u'' % tag) + else: + text = text.replace(u'{%s}' % tag, u'' % tag) + # Add tag to element if tag not present. + if tag not in xml_tags: + self._add_tag_to_formatting(tag, tags_element) + # Replace end tags. + for t in end_tags: + text = text.replace(u'{/%s}' % t, u'') + # Replace \n with
. + text = text.replace(u'\n', u'
') + text = u'' + text + u'' + element = etree.XML(text) + verse_element.append(element) + return element + def _extract_xml(self, xml): """ Extract our newly created XML song. @@ -362,20 +458,6 @@ class OpenLyrics(object): return etree.tostring(xml, encoding=u'UTF-8', xml_declaration=True) - def _get(self, element, attribute): - """ - This returns the element's attribute as unicode string. - - ``element`` - The element. - - ``attribute`` - The element's attribute (unicode). - """ - if element.get(attribute) is not None: - return unicode(element.get(attribute)) - return u'' - def _text(self, element): """ This returns the text of an element as unicode string. @@ -457,29 +539,155 @@ class OpenLyrics(object): if hasattr(properties, u'copyright'): song.copyright = self._text(properties.copyright) - def _process_lyrics(self, properties, lyrics, song): + def _process_formatting_tags(self, song_xml, temporary): + """ + Process the formatting tags from the song and either add missing tags + temporary or permanently to the formatting tag list. + """ + if not hasattr(song_xml, u'format'): + return + found_tags = [] + for tag in song_xml.format.tags.getchildren(): + name = tag.get(u'name') + if name is None: + continue + start_tag = u'{%s}' % name[:5] + # Some tags have only start tag e.g. {br} + end_tag = u'{/' + name[:5] + u'}' if hasattr(tag, 'close') else u'' + openlp_tag = { + u'desc': name, + u'start tag': start_tag, + u'end tag': end_tag, + u'start html': tag.open.text, + # Some tags have only start html e.g. {br} + u'end html': tag.close.text if hasattr(tag, 'close') else u'', + u'protected': False, + } + # Add 'temporary' key in case the formatting tag should not be + # saved otherwise it is supposed that formatting tag is permanent. + if temporary: + openlp_tag[u'temporary'] = temporary + found_tags.append(openlp_tag) + existing_tag_ids = [tag[u'start tag'] + for tag in FormattingTags.get_html_tags()] + new_tags = [tag for tag in found_tags + if tag[u'start tag'] not in existing_tag_ids] + FormattingTags.add_html_tags(new_tags, True) + + def _process_lines_mixed_content(self, element, newlines=True): + """ + Converts the xml text with mixed content to OpenLP representation. + Chords are skipped and formatting tags are converted. + + ``element`` + The property object (lxml.etree.Element). + + ``newlines`` + The switch to enable/disable processing of line breaks
. + The
is used since OpenLyrics 0.8. + """ + text = u'' + use_endtag = True + # Skip elements - not yet supported. + if element.tag == NSMAP % u'comment': + if element.tail: + # Append tail text at chord element. + text += element.tail + return text + # Skip element - not yet supported. + elif element.tag == NSMAP % u'chord': + if element.tail: + # Append tail text at chord element. + text += element.tail + return text + # Convert line breaks
to \n. + elif newlines and element.tag == NSMAP % u'br': + text += u'\n' + if element.tail: + text += element.tail + return text + # Start formatting tag. + if element.tag == NSMAP % u'tag': + text += u'{%s}' % element.get(u'name') + # Some formattings may have only start tag. + # Handle this case if element has no children and contains no text. + if len(element) == 0 and not element.text: + use_endtag = False + # Append text from element. + if element.text: + text += element.text + # Process nested formatting tags. + for child in element: + # Use recursion since nested formatting tags are allowed. + text += self._process_lines_mixed_content(child, newlines) + # Append text from tail and add formatting end tag. + if element.tag == NSMAP % 'tag' and use_endtag: + text += u'{/%s}' % element.get(u'name') + # Append text from tail. + if element.tail: + text += element.tail + return text + + def _process_verse_lines(self, lines, version): + """ + Converts lyrics lines to OpenLP representation. + + ``lines`` + The lines object (lxml.objectify.ObjectifiedElement). + """ + text = u'' + # Convert lxml.objectify to lxml.etree representation. + lines = etree.tostring(lines) + element = etree.XML(lines) + + # OpenLyrics 0.8 uses
for new lines. + # Append text from "lines" element to verse text. + if version > '0.7': + text = self._process_lines_mixed_content(element) + # OpenLyrics version <= 0.7 contais elements to represent lines. + # First child element is tested. + else: + # Loop over the "line" elements removing comments and chords. + for line in element: + # Skip comment lines. + if line.tag == NSMAP % u'comment': + continue + if text: + text += u'\n' + text += self._process_lines_mixed_content(line, newlines=False) + return text + + def _process_lyrics(self, properties, song_xml, song_obj): """ Processes the verses and search_lyrics for the song. ``properties`` The properties object (lxml.objectify.ObjectifiedElement). - ``lyrics`` - The lyrics object (lxml.objectify.ObjectifiedElement). + ``song_xml`` + The objectified song (lxml.objectify.ObjectifiedElement). - ``song`` + ``song_obj`` The song object. """ sxml = SongXML() verses = {} verse_def_list = [] + lyrics = song_xml.lyrics + # Loop over the "verse" elements. for verse in lyrics.verse: text = u'' + # Loop over the "lines" elements. for lines in verse.lines: if text: text += u'\n' - text += u'\n'.join([unicode(line) for line in lines.line]) - verse_def = self._get(verse, u'name').lower() + # Append text from "lines" element to verse text. + text += self._process_verse_lines(lines, + version=song_xml.get(u'version')) + # Add a virtual split to the verse text. + if lines.get(u'break') is not None: + text += u'\n[---]' + verse_def = verse.get(u'name', u' ').lower() if verse_def[0] in VerseType.Tags: verse_tag = verse_def[0] else: @@ -489,11 +697,16 @@ class OpenLyrics(object): # not correct the verse order. if not verse_number: verse_number = u'1' - lang = None - if self._get(verse, u'lang'): - lang = self._get(verse, u'lang') - if verses.has_key((verse_tag, verse_number, lang)): + lang = verse.get(u'lang') + # In OpenLP 1.9.6 we used v1a, v1b ... to represent visual slide + # breaks. In OpenLyrics 0.7 an attribute has been added. + if song_xml.get(u'modifiedIn') in (u'1.9.6', u'OpenLP 1.9.6') and \ + song_xml.get(u'version') == u'0.7' and \ + (verse_tag, verse_number, lang) in verses: verses[(verse_tag, verse_number, lang)] += u'\n[---]\n' + text + # Merge v1a, v1b, .... to v1. + elif (verse_tag, verse_number, lang) in verses: + verses[(verse_tag, verse_number, lang)] += u'\n' + text else: verses[(verse_tag, verse_number, lang)] = text verse_def_list.append((verse_tag, verse_number, lang)) @@ -501,10 +714,10 @@ class OpenLyrics(object): for verse in verse_def_list: sxml.add_verse_to_lyrics( verse[0], verse[1], verses[verse], verse[2]) - song.lyrics = unicode(sxml.extract_xml(), u'utf-8') + song_obj.lyrics = unicode(sxml.extract_xml(), u'utf-8') # Process verse order if hasattr(properties, u'verseOrder'): - song.verse_order = self._text(properties.verseOrder) + song_obj.verse_order = self._text(properties.verseOrder) def _process_songbooks(self, properties, song): """ @@ -520,7 +733,7 @@ class OpenLyrics(object): song.song_number = u'' if hasattr(properties, u'songbooks'): for songbook in properties.songbooks.songbook: - bookname = self._get(songbook, u'name') + bookname = songbook.get(u'name', u'') if bookname: book = self.manager.get_object_filtered(Book, Book.name == bookname) @@ -529,7 +742,7 @@ class OpenLyrics(object): book = Book.populate(name=bookname, publisher=u'') self.manager.save_object(book) song.song_book_id = book.id - song.song_number = self._get(songbook, u'entry') + song.song_number = songbook.get(u'entry', u'') # We only support one song book, so take the first one. break