← 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:
  Andreas Preikschat (googol)
  Tim Bentley (trb143)
  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/76652

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.

Fixed issue with reading onlder OpenLP.conf with formatting tags without 'temporary' key.
-- 
https://code.launchpad.net/~mzibricky/openlp/openlyrics/+merge/76652
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 20:39:04 +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.get(u'temporary')]
         FormattingTags.html_expands = []
         base_tags = []
         # Append the base tags.
@@ -56,75 +64,160 @@
         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.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 ``<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 20:39:04 +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']))
+            # 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'')

=== 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 20:39:04 +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 20:39:04 +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 20:39:04 +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,148 @@
         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 <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 +690,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 +707,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 +726,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 +735,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