← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~mzibricky/openlp/openlyrics into lp:openlp

 

matysek has proposed merging lp:~mzibricky/openlp/openlyrics into lp:openlp.

Requested reviews:
  Tim Bentley (trb143)
  Andreas Preikschat (googol)
  Raoul Snyman (raoul-snyman)
Related bugs:
  Bug #745636 in OpenLP: "OpenLyrics xml including OpenLP specifics"
  https://bugs.launchpad.net/openlp/+bug/745636
  Bug #852424 in OpenLP: "Formatting tags for multiple lines"
  https://bugs.launchpad.net/openlp/+bug/852424

For more details, see:
https://code.launchpad.net/~mzibricky/openlp/openlyrics/+merge/76596

Added export to openlyrics format with formatting tags.

I removed:
- print statement
- db_file_path option

I'm now overriding data dir in AppLocation.BaseDir.
I consolidated the test code and made it more structured.

I moved tests to another branch.

Renamed only_process_format_tags

Added some docstrings and reformatted some comments.

Finished import/export of openlyrics with formatting tags.
Reflected syntax changes in openlyrics 0.8,

Added importing OpenLyrics with formatting tags.

Fixed issues with users' formatting tags.
-- 
https://code.launchpad.net/~mzibricky/openlp/openlyrics/+merge/76596
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/lib/formattingtags.py'
--- openlp/core/lib/formattingtags.py	2011-07-30 08:07:48 +0000
+++ openlp/core/lib/formattingtags.py	2011-09-22 14:48:33 +0000
@@ -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 @@
         """
         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 @@
         """
         Resets the html_expands list.
         """
+        temporary_tags = [tag for tag in FormattingTags.html_expands
+            if tag[u'temporary']]
         FormattingTags.html_expands = []
         base_tags = []
         # Append the base tags.
@@ -56,75 +64,156 @@
         base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Red'),
             u'start tag': u'{r}',
             u'start html': u'<span style="-webkit-text-fill-color:red">',
-            u'end tag': u'{/r}', u'end html': u'</span>', u'protected': True})
-        base_tags.append({u'desc':  translate('OpenLP.FormattingTags', 'Black'),
+            u'end tag': u'{/r}', u'end html': u'</span>', u'protected': True,
+            u'temporary': False})
+        base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Black'),
             u'start tag': u'{b}',
             u'start html': u'<span style="-webkit-text-fill-color:black">',
-            u'end tag': u'{/b}', u'end html': u'</span>', u'protected': True})
+            u'end tag': u'{/b}', u'end html': u'</span>', u'protected': True,
+            u'temporary': False})
         base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Blue'),
             u'start tag': u'{bl}',
             u'start html': u'<span style="-webkit-text-fill-color:blue">',
-            u'end tag': u'{/bl}', u'end html': u'</span>', u'protected': True})
+            u'end tag': u'{/bl}', u'end html': u'</span>', u'protected': True,
+            u'temporary': False})
         base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Yellow'),
             u'start tag': u'{y}',
             u'start html': u'<span style="-webkit-text-fill-color:yellow">',
-            u'end tag': u'{/y}', u'end html': u'</span>', u'protected': True})
+            u'end tag': u'{/y}', u'end html': u'</span>', u'protected': True,
+            u'temporary': False})
         base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Green'),
             u'start tag': u'{g}',
             u'start html': u'<span style="-webkit-text-fill-color:green">',
-            u'end tag': u'{/g}', u'end html': u'</span>', u'protected': True})
+            u'end tag': u'{/g}', u'end html': u'</span>', u'protected': True,
+            u'temporary': False})
         base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Pink'),
             u'start tag': u'{pk}',
             u'start html': u'<span style="-webkit-text-fill-color:#FFC0CB">',
-            u'end tag': u'{/pk}', u'end html': u'</span>', u'protected': True})
+            u'end tag': u'{/pk}', u'end html': u'</span>', u'protected': True,
+            u'temporary': False})
         base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Orange'),
             u'start tag': u'{o}',
             u'start html': u'<span style="-webkit-text-fill-color:#FFA500">',
-            u'end tag': u'{/o}', u'end html': u'</span>', u'protected': True})
+            u'end tag': u'{/o}', u'end html': u'</span>', u'protected': True,
+            u'temporary': False})
         base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Purple'),
             u'start tag': u'{pp}',
             u'start html': u'<span style="-webkit-text-fill-color:#800080">',
-            u'end tag': u'{/pp}', u'end html': u'</span>', u'protected': True})
+            u'end tag': u'{/pp}', u'end html': u'</span>', u'protected': True,
+            u'temporary': False})
         base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'White'),
             u'start tag': u'{w}',
             u'start html': u'<span style="-webkit-text-fill-color:white">',
-            u'end tag': u'{/w}', u'end html': u'</span>', u'protected': True})
+            u'end tag': u'{/w}', u'end html': u'</span>', u'protected': True,
+            u'temporary': False})
         base_tags.append({
             u'desc': translate('OpenLP.FormattingTags', 'Superscript'),
             u'start tag': u'{su}', u'start html': u'<sup>',
-            u'end tag': u'{/su}', u'end html': u'</sup>', u'protected': True})
+            u'end tag': u'{/su}', u'end html': u'</sup>', u'protected': True,
+            u'temporary': False})
         base_tags.append({
             u'desc': translate('OpenLP.FormattingTags', 'Subscript'),
             u'start tag': u'{sb}', u'start html': u'<sub>',
-            u'end tag': u'{/sb}', u'end html': u'</sub>', u'protected': True})
+            u'end tag': u'{/sb}', u'end html': u'</sub>', u'protected': True,
+            u'temporary': False})
         base_tags.append({
             u'desc': translate('OpenLP.FormattingTags', 'Paragraph'),
             u'start tag': u'{p}', u'start html': u'<p>', u'end tag': u'{/p}',
-            u'end html': u'</p>', u'protected': True})
+            u'end html': u'</p>', u'protected': True,
+            u'temporary': False})
         base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Bold'),
             u'start tag': u'{st}', u'start html': u'<strong>',
             u'end tag': u'{/st}', u'end html': u'</strong>',
-            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'<em>', u'end tag': u'{/it}',
-            u'end html': u'</em>', u'protected': True})
+            u'end html': u'</em>', u'protected': True, u'temporary': False})
         base_tags.append({
             u'desc': translate('OpenLP.FormattingTags', 'Underline'),
             u'start tag': u'{u}',
             u'start html': u'<span style="text-decoration: underline;">',
-            u'end tag': u'{/u}', u'end html': u'</span>', u'protected': True})
+            u'end tag': u'{/u}', u'end html': u'</span>', u'protected': True,
+            u'temporary': False})
         base_tags.append({u'desc': translate('OpenLP.FormattingTags', 'Break'),
             u'start tag': u'{br}', u'start html': u'<br>', 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)
-
-    @staticmethod
-    def add_html_tags(tags):
-        """
-        Add a list of tags to the list
+        FormattingTags.add_html_tags(temporary_tags)
+
+    @staticmethod
+    def save_html_tags():
+        """
+        Saves all formatting tags except protected ones.
+        """
+        tags = []
+        for tag in FormattingTags.html_expands:
+            if not tag[u'protected'] and not tag[u'temporary']:
+                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''))
+
+    @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 ``<span style="
+            -webkit-text-fill-color:red">``
+
+        * end html
+            The end html tag. For example ``</span>``
+
+        * 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):

=== modified file 'openlp/core/ui/formattingtagform.py'
--- openlp/core/ui/formattingtagform.py	2011-07-30 07:43:19 +0000
+++ openlp/core/ui/formattingtagform.py	2011-09-22 14:48:33 +0000
@@ -30,14 +30,13 @@
 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 @@
         """
         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,
@@ -65,29 +63,10 @@
         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.
@@ -132,7 +111,8 @@
             u'start html': translate('OpenLP.FormattingTagForm', '<HTML here>'),
             u'end tag': u'{/n}',
             u'end html': translate('OpenLP.FormattingTagForm', '</and here>'),
-            u'protected': False
+            u'protected': False,
+            u'temporary': False
         }
         FormattingTags.add_html_tags([tag])
         self._resetTable()
@@ -149,7 +129,7 @@
             FormattingTags.remove_html_tag(self.selected)
             self.selected = -1
         self._resetTable()
-        self._saveTable()
+        FormattingTags.save_html_tags()
 
     def onSavedPushed(self):
         """
@@ -172,21 +152,11 @@
             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):
         """
@@ -198,8 +168,7 @@
         self.savePushButton.setEnabled(False)
         self.deletePushButton.setEnabled(False)
         for linenumber, html in enumerate(FormattingTags.get_html_tags()):
-            self.tagTableWidget.setRowCount(
-                self.tagTableWidget.rowCount() + 1)
+            self.tagTableWidget.setRowCount(self.tagTableWidget.rowCount() + 1)
             self.tagTableWidget.setItem(linenumber, 0,
                 QtGui.QTableWidgetItem(html[u'desc']))
             self.tagTableWidget.setItem(linenumber, 1,
@@ -208,6 +177,9 @@
                 QtGui.QTableWidgetItem(html[u'start html']))
             self.tagTableWidget.setItem(linenumber, 3,
                 QtGui.QTableWidgetItem(html[u'end html']))
+            # Tags saved prior to 1.9.7 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'')

=== modified file 'openlp/core/utils/__init__.py'
--- openlp/core/utils/__init__.py	2011-09-20 16:26:04 +0000
+++ openlp/core/utils/__init__.py	2011-09-22 14:48:33 +0000
@@ -127,6 +127,9 @@
     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 @@
                 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)
 

=== modified file 'openlp/plugins/songs/lib/mediaitem.py'
--- openlp/plugins/songs/lib/mediaitem.py	2011-09-16 08:15:37 +0000
+++ openlp/plugins/songs/lib/mediaitem.py	2011-09-22 14:48:33 +0000
@@ -559,6 +559,9 @@
                 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',

=== modified file 'openlp/plugins/songs/lib/xml.py'
--- openlp/plugins/songs/lib/xml.py	2011-08-28 20:51:44 +0000
+++ openlp/plugins/songs/lib/xml.py	2011-09-22 14:48:33 +0000
@@ -61,19 +61,21 @@
     </song>
 """
 
-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'<chord name=".*?"/>')
+NAMESPACE = u'http://openlyrics.info/namespace/2009/song'
+NSMAP = '{' + NAMESPACE + '}' + '%s'
+
 
 class SongXML(object):
     """
@@ -173,7 +175,7 @@
 
 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 @@
     ``<key>``
         This property is not supported.
 
+    ``<format>``
+        The custom formatting tags are fully supported.
+
     ``<keywords>``
         This property is not supported.
 
     ``<lines>``
-        The attribute *part* is not supported.
+        The attribute *part* is not supported. The *break* attribute is
+        supported.
 
     ``<publisher>``
         This property is not supported.
@@ -227,15 +233,35 @@
 
     ``<verse name="v1a" lang="he" translit="en">``
         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*.
 
     ``<verseOrder>``
         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 @@
         sxml = SongXML()
         song_xml = objectify.fromstring(u'<song/>')
         # 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 @@
             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 @@
 
         ``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'<?xml':
             xml = xml[38:]
-        # Remove chords from xml.
-        xml = CHORD_REGEX.sub(u'', xml)
         song_xml = objectify.fromstring(xml)
         if hasattr(song_xml, u'properties'):
             properties = song_xml.properties
         else:
             return None
+        # Formatting tags are new in OpenLyrics 0.8
+        if float(song_xml.get(u'version')) > 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 @@
         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 @@
         parent.append(element)
         return element
 
+    def _add_tag_to_formatting(self, tag_name, tags_element):
+        """
+        Add new formatting tag to the element ``<format>``
+        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
+                # <close> 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 ``<lines>``.
+        """
+        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. <br>.
+            # Handle this case.
+            if tag in end_tags:
+                text = text.replace(u'{%s}' % tag, u'<tag name="%s">' % tag)
+            else:
+                text = text.replace(u'{%s}' % tag, u'<tag name="%s"/>' % tag)
+            # Add tag to <format> 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'</tag>')
+        # Replace \n with <br/>.
+        text = text.replace(u'\n', u'<br/>')
+        text = u'<lines>' + text + u'</lines>'
+        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 @@
         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,145 @@
         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,
+                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 <br/>.
+            The <br/> is used since OpenLyrics 0.8.
+        """
+        text = u''
+        use_endtag = True
+        # Skip <comment> elements - not yet supported.
+        if element.tag == NSMAP % u'comment' and element.tail:
+            # Append tail text at chord element.
+            text += element.tail
+            return text
+        # Skip <chord> element - not yet supported.
+        elif element.tag == NSMAP % u'chord' and element.tail:
+            # Append tail text at chord element.
+            text += element.tail
+            return text
+        # Convert line breaks <br/> 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):
+        """
+        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 version <= 0.7 contais <line> elements to represent lines.
+        # First child element is tested.
+        if element[0].tag == NSMAP % 'line':
+            # Loop over the "line" elements removing comments and chords.
+            for line in element:
+                if text:
+                    text += u'\n'
+                text += self._process_lines_mixed_content(line, newlines=False)
+        # OpenLyrics 0.8 uses <br/> for new lines.
+        # Append text from "lines" element to verse text.
+        else:
+            text = self._process_lines_mixed_content(element)
+        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)
+                # 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 +687,16 @@
             # 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 +704,10 @@
         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 +723,7 @@
         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 +732,7 @@
                         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
 


Follow ups